summaryrefslogtreecommitdiff
path: root/addons/mrp/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mrp/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mrp/static/src')
-rw-r--r--addons/mrp/static/src/img/mrp-tablet.pngbin0 -> 22438 bytes
-rw-r--r--addons/mrp/static/src/js/mrp.js268
-rw-r--r--addons/mrp/static/src/js/mrp_bom_report.js228
-rw-r--r--addons/mrp/static/src/js/mrp_document_kanban_view.js20
-rw-r--r--addons/mrp/static/src/js/mrp_documents_controller_mixin.js126
-rw-r--r--addons/mrp/static/src/js/mrp_documents_document_viewer.js19
-rw-r--r--addons/mrp/static/src/js/mrp_documents_kanban_controller.js37
-rw-r--r--addons/mrp/static/src/js/mrp_documents_kanban_record.js50
-rw-r--r--addons/mrp/static/src/js/mrp_documents_kanban_renderer.js27
-rw-r--r--addons/mrp/static/src/js/mrp_should_consume.js81
-rw-r--r--addons/mrp/static/src/js/mrp_workorder_popover.js51
-rw-r--r--addons/mrp/static/src/scss/mrp_bom_report.scss50
-rw-r--r--addons/mrp/static/src/scss/mrp_document_kanban_view.scss3
-rw-r--r--addons/mrp/static/src/scss/mrp_fields.scss11
-rw-r--r--addons/mrp/static/src/scss/mrp_gantt.scss19
-rw-r--r--addons/mrp/static/src/scss/mrp_workorder_kanban.scss10
-rw-r--r--addons/mrp/static/src/xml/mrp.xml49
-rw-r--r--addons/mrp/static/src/xml/mrp_document_template.xml10
18 files changed, 1059 insertions, 0 deletions
diff --git a/addons/mrp/static/src/img/mrp-tablet.png b/addons/mrp/static/src/img/mrp-tablet.png
new file mode 100644
index 00000000..f60fbbb7
--- /dev/null
+++ b/addons/mrp/static/src/img/mrp-tablet.png
Binary files differ
diff --git a/addons/mrp/static/src/js/mrp.js b/addons/mrp/static/src/js/mrp.js
new file mode 100644
index 00000000..f895aef9
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp.js
@@ -0,0 +1,268 @@
+odoo.define('mrp.mrp_state', function (require) {
+"use strict";
+
+var AbstractField = require('web.AbstractField');
+var core = require('web.core');
+var fields = require('web.basic_fields');
+var fieldUtils = require('web.field_utils');
+var field_registry = require('web.field_registry');
+var time = require('web.time');
+
+var _t = core._t;
+
+/**
+ * This widget is used to display the availability on a workorder.
+ */
+var SetBulletStatus = AbstractField.extend({
+ // as this widget is based on hardcoded values, use it in another context
+ // probably won't work
+ // supportedFieldTypes: ['selection'],
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.classes = this.nodeOptions && this.nodeOptions.classes || {};
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @override
+ */
+ _renderReadonly: function () {
+ this._super.apply(this, arguments);
+ var bullet_class = this.classes[this.value] || 'default';
+ if (this.value) {
+ var title = this.value === 'waiting' ? _t('Waiting Materials') : '';
+ this.$el.attr({'title': title, 'style': 'display:inline'});
+ this.$el.removeClass('text-success text-danger text-default');
+ this.$el.html($('<span>' + title + '</span>').addClass('badge badge-' + bullet_class));
+ }
+ }
+});
+
+var TimeCounter = fields.FieldFloatTime.extend({
+
+ init: function () {
+ this._super.apply(this, arguments);
+ this.duration = this.record.data.duration;
+ },
+
+ willStart: function () {
+ var self = this;
+ var def = this._rpc({
+ model: 'mrp.workcenter.productivity',
+ method: 'search_read',
+ domain: [
+ ['workorder_id', '=', this.record.data.id],
+ ['date_end', '=', false],
+ ],
+ }).then(function (result) {
+ var currentDate = new Date();
+ var duration = 0;
+ if (result.length > 0) {
+ duration += self._getDateDifference(time.auto_str_to_date(result[0].date_start), currentDate);
+ }
+ var minutes = duration / 60 >> 0;
+ var seconds = duration % 60;
+ self.duration += minutes + seconds / 60;
+ if (self.mode === 'edit') {
+ self.value = self.duration;
+ }
+ });
+ return Promise.all([this._super.apply(this, arguments), def]);
+ },
+
+ destroy: function () {
+ this._super.apply(this, arguments);
+ clearTimeout(this.timer);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ isSet: function () {
+ return true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Compute the difference between two dates.
+ *
+ * @private
+ * @param {string} dateStart
+ * @param {string} dateEnd
+ * @returns {integer} the difference in millisecond
+ */
+ _getDateDifference: function (dateStart, dateEnd) {
+ return moment(dateEnd).diff(moment(dateStart), 'seconds');
+ },
+ /**
+ * @override
+ */
+ _renderReadonly: function () {
+ if (this.record.data.is_user_working) {
+ this._startTimeCounter();
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
+ * @private
+ */
+ _startTimeCounter: function () {
+ var self = this;
+ clearTimeout(this.timer);
+ if (this.record.data.is_user_working) {
+ this.timer = setTimeout(function () {
+ self.duration += 1/60;
+ self._startTimeCounter();
+ }, 1000);
+ } else {
+ clearTimeout(this.timer);
+ }
+ this.$el.text(fieldUtils.format.float_time(this.duration));
+ },
+});
+
+var FieldEmbedURLViewer = fields.FieldChar.extend({
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.page = 1;
+ this.srcDirty = false;
+ },
+
+ /**
+ * force to set 'src' for embed iframe viewer when its value has changed
+ *
+ * @override
+ *
+ */
+ reset: function () {
+ this._super.apply(this, arguments);
+ this._updateIframePreview();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Initializes and returns an iframe for the viewer
+ *
+ * @private
+ * @returns {jQueryElement}
+ */
+ _prepareIframe: function () {
+ return $('<iframe>', {
+ class: 'o_embed_iframe d-none',
+ allowfullscreen: true,
+ });
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _renderEdit: function () {
+ if (!this.$('iframe.o_embed_iframe').length) {
+ this.$input = this.$el;
+ this.setElement(this.$el.wrap('<div class="o_embed_url_viewer o_field_widget"/>').parent());
+ this.$el.append(this._prepareIframe());
+ }
+ this._prepareInput(this.$input);
+
+ // Do not set iframe src if widget is invisible
+ if (!this.record.evalModifiers(this.attrs.modifiers).invisible) {
+ this._updateIframePreview();
+ } else {
+ this.srcDirty = true;
+ }
+ },
+ /**
+ * @override
+ * @private
+ */
+ _renderReadonly: function () {
+ if (!this.$('iframe.o_embed_iframe').length) {
+ this.$el.addClass('o_embed_url_viewer');
+ this.$el.append(this._prepareIframe());
+ }
+ this._updateIframePreview();
+ },
+ /**
+ * Set the associated src for embed iframe viewer
+ *
+ * @private
+ * @returns {string} source of the google slide
+ */
+ _getEmbedSrc: function () {
+ var src = false;
+ if (this.value) {
+ // check given google slide url is valid or not
+ var googleRegExp = /(^https:\/\/docs.google.com).*(\/d\/e\/|\/d\/)([A-Za-z0-9-_]+)/;
+ var google = this.value.match(googleRegExp);
+ if (google && google[3]) {
+ src = 'https://docs.google.com/presentation' + google[2] + google[3] + '/preview?slide=' + this.page;
+ }
+ }
+ return src || this.value;
+ },
+ /**
+ * update iframe attrs
+ *
+ * @private
+ */
+ _updateIframePreview: function () {
+ var $iframe = this.$('iframe.o_embed_iframe');
+ var src = this._getEmbedSrc();
+ $iframe.toggleClass('d-none', !src);
+ if (src) {
+ $iframe.attr('src', src);
+ } else {
+ $iframe.removeAttr('src');
+ }
+ },
+ /**
+ * Listen to modifiers updates to and only render iframe when it is necessary
+ *
+ * @override
+ */
+ updateModifiersValue: function () {
+ this._super.apply(this, arguments);
+ if (!this.attrs.modifiersValue.invisible && this.srcDirty) {
+ this._updateIframePreview();
+ this.srcDirty = false;
+ }
+ },
+});
+
+
+field_registry
+ .add('bullet_state', SetBulletStatus)
+ .add('mrp_time_counter', TimeCounter)
+ .add('embed_viewer', FieldEmbedURLViewer);
+
+fieldUtils.format.mrp_time_counter = fieldUtils.format.float_time;
+
+return FieldEmbedURLViewer;
+});
diff --git a/addons/mrp/static/src/js/mrp_bom_report.js b/addons/mrp/static/src/js/mrp_bom_report.js
new file mode 100644
index 00000000..cb56c797
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_bom_report.js
@@ -0,0 +1,228 @@
+odoo.define('mrp.mrp_bom_report', function (require) {
+'use strict';
+
+var core = require('web.core');
+var framework = require('web.framework');
+var stock_report_generic = require('stock.stock_report_generic');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+var MrpBomReport = stock_report_generic.extend({
+ events: {
+ 'click .o_mrp_bom_unfoldable': '_onClickUnfold',
+ 'click .o_mrp_bom_foldable': '_onClickFold',
+ 'click .o_mrp_bom_action': '_onClickAction',
+ 'click .o_mrp_show_attachment_action': '_onClickShowAttachment',
+ },
+ get_html: function() {
+ var self = this;
+ var args = [
+ this.given_context.active_id,
+ this.given_context.searchQty || false,
+ this.given_context.searchVariant,
+ ];
+ return this._rpc({
+ model: 'report.mrp.report_bom_structure',
+ method: 'get_html',
+ args: args,
+ context: this.given_context,
+ })
+ .then(function (result) {
+ self.data = result;
+ if (! self.given_context.searchVariant) {
+ self.given_context.searchVariant = result.is_variant_applied && Object.keys(result.variants)[0];
+ }
+ });
+ },
+ set_html: function() {
+ var self = this;
+ return this._super().then(function () {
+ self.$('.o_content').html(self.data.lines);
+ self.renderSearch();
+ self.update_cp();
+ });
+ },
+ render_html: function(event, $el, result){
+ if (result.indexOf('mrp.document') > 0) {
+ if (this.$('.o_mrp_has_attachments').length === 0) {
+ var column = $('<th/>', {
+ class: 'o_mrp_has_attachments',
+ title: 'Files attached to the product Attachments',
+ text: 'Attachments',
+ });
+ this.$('table thead th:last-child').after(column);
+ }
+ }
+ $el.after(result);
+ $(event.currentTarget).toggleClass('o_mrp_bom_foldable o_mrp_bom_unfoldable fa-caret-right fa-caret-down');
+ this._reload_report_type();
+ },
+ get_bom: function(event) {
+ var self = this;
+ var $parent = $(event.currentTarget).closest('tr');
+ var activeID = $parent.data('id');
+ var productID = $parent.data('product_id');
+ var lineID = $parent.data('line');
+ var qty = $parent.data('qty');
+ var level = $parent.data('level') || 0;
+ return this._rpc({
+ model: 'report.mrp.report_bom_structure',
+ method: 'get_bom',
+ args: [
+ activeID,
+ productID,
+ parseFloat(qty),
+ lineID,
+ level + 1,
+ ]
+ })
+ .then(function (result) {
+ self.render_html(event, $parent, result);
+ });
+ },
+ get_operations: function(event) {
+ var self = this;
+ var $parent = $(event.currentTarget).closest('tr');
+ var activeID = $parent.data('bom-id');
+ var qty = $parent.data('qty');
+ var level = $parent.data('level') || 0;
+ return this._rpc({
+ model: 'report.mrp.report_bom_structure',
+ method: 'get_operations',
+ args: [
+ activeID,
+ parseFloat(qty),
+ level + 1
+ ]
+ })
+ .then(function (result) {
+ self.render_html(event, $parent, result);
+ });
+ },
+ update_cp: function () {
+ var status = {
+ cp_content: {
+ $buttons: this.$buttonPrint,
+ $searchview: this.$searchView
+ },
+ };
+ return this.updateControlPanel(status);
+ },
+ renderSearch: function () {
+ this.$buttonPrint = $(QWeb.render('mrp.button', {'is_variant_applied': this.data.is_variant_applied}));
+ this.$buttonPrint.find('.o_mrp_bom_print').on('click', this._onClickPrint.bind(this));
+ this.$buttonPrint.find('.o_mrp_bom_print_all_variants').on('click', this._onClickPrint.bind(this));
+ this.$buttonPrint.find('.o_mrp_bom_print_unfolded').on('click', this._onClickPrint.bind(this));
+ this.$searchView = $(QWeb.render('mrp.report_bom_search', _.omit(this.data, 'lines')));
+ this.$searchView.find('.o_mrp_bom_report_qty').on('change', this._onChangeQty.bind(this));
+ this.$searchView.find('.o_mrp_bom_report_variants').on('change', this._onChangeVariants.bind(this));
+ this.$searchView.find('.o_mrp_bom_report_type').on('change', this._onChangeType.bind(this));
+ },
+ _onClickPrint: function (ev) {
+ var childBomIDs = _.map(this.$el.find('.o_mrp_bom_foldable').closest('tr'), function (el) {
+ return $(el).data('id');
+ });
+ framework.blockUI();
+ var reportname = 'mrp.report_bom_structure?docids=' + this.given_context.active_id +
+ '&report_type=' + this.given_context.report_type +
+ '&quantity=' + (this.given_context.searchQty || 1);
+ if (! $(ev.currentTarget).hasClass('o_mrp_bom_print_unfolded')) {
+ reportname += '&childs=' + JSON.stringify(childBomIDs);
+ }
+ if ($(ev.currentTarget).hasClass('o_mrp_bom_print_all_variants')) {
+ reportname += '&all_variants=' + 1;
+ } else if (this.given_context.searchVariant) {
+ reportname += '&variant=' + this.given_context.searchVariant;
+ }
+ var action = {
+ 'type': 'ir.actions.report',
+ 'report_type': 'qweb-pdf',
+ 'report_name': reportname,
+ 'report_file': 'mrp.report_bom_structure',
+ };
+ return this.do_action(action).then(function (){
+ framework.unblockUI();
+ });
+ },
+ _onChangeQty: function (ev) {
+ var qty = $(ev.currentTarget).val().trim();
+ if (qty) {
+ this.given_context.searchQty = parseFloat(qty);
+ this._reload();
+ }
+ },
+ _onChangeType: function (ev) {
+ var report_type = $("option:selected", $(ev.currentTarget)).data('type');
+ this.given_context.report_type = report_type;
+ this._reload_report_type();
+ },
+ _onChangeVariants: function (ev) {
+ this.given_context.searchVariant = $(ev.currentTarget).val();
+ this._reload();
+ },
+ _onClickUnfold: function (ev) {
+ var redirect_function = $(ev.currentTarget).data('function');
+ this[redirect_function](ev);
+ },
+ _onClickFold: function (ev) {
+ this._removeLines($(ev.currentTarget).closest('tr'));
+ $(ev.currentTarget).toggleClass('o_mrp_bom_foldable o_mrp_bom_unfoldable fa-caret-right fa-caret-down');
+ },
+ _onClickAction: function (ev) {
+ ev.preventDefault();
+ return this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: $(ev.currentTarget).data('model'),
+ res_id: $(ev.currentTarget).data('res-id'),
+ context: {
+ 'active_id': $(ev.currentTarget).data('res-id')
+ },
+ views: [[false, 'form']],
+ target: 'current'
+ });
+ },
+ _onClickShowAttachment: function (ev) {
+ ev.preventDefault();
+ var ids = $(ev.currentTarget).data('res-id');
+ return this.do_action({
+ name: _t('Attachments'),
+ type: 'ir.actions.act_window',
+ res_model: $(ev.currentTarget).data('model'),
+ domain: [['id', 'in', ids]],
+ views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
+ view_mode: 'kanban,list,form',
+ target: 'current',
+ });
+ },
+ _reload: function () {
+ var self = this;
+ return this.get_html().then(function () {
+ self.$('.o_content').html(self.data.lines);
+ self._reload_report_type();
+ });
+ },
+ _reload_report_type: function () {
+ this.$('.o_mrp_bom_cost.o_hidden, .o_mrp_prod_cost.o_hidden').toggleClass('o_hidden');
+ if (this.given_context.report_type === 'bom_structure') {
+ this.$('.o_mrp_bom_cost, .o_mrp_prod_cost').toggleClass('o_hidden');
+ }
+ },
+ _removeLines: function ($el) {
+ var self = this;
+ var activeID = $el.data('id');
+ _.each(this.$('tr[parent_id='+ activeID +']'), function (parent) {
+ var $parent = self.$(parent);
+ var $el = self.$('tr[parent_id='+ $parent.data('id') +']');
+ if ($el.length) {
+ self._removeLines($parent);
+ }
+ $parent.remove();
+ });
+ },
+});
+
+core.action_registry.add('mrp_bom_report', MrpBomReport);
+return MrpBomReport;
+
+});
diff --git a/addons/mrp/static/src/js/mrp_document_kanban_view.js b/addons/mrp/static/src/js/mrp_document_kanban_view.js
new file mode 100644
index 00000000..e4e93663
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_document_kanban_view.js
@@ -0,0 +1,20 @@
+odoo.define('mrp.MrpDocumentsKanbanView', function (require) {
+"use strict";
+
+const KanbanView = require('web.KanbanView');
+const MrpDocumentsKanbanController = require('mrp.MrpDocumentsKanbanController');
+const MrpDocumentsKanbanRenderer = require('mrp.MrpDocumentsKanbanRenderer');
+const viewRegistry = require('web.view_registry');
+
+const MrpDocumentsKanbanView = KanbanView.extend({
+ config: Object.assign({}, KanbanView.prototype.config, {
+ Controller: MrpDocumentsKanbanController,
+ Renderer: MrpDocumentsKanbanRenderer,
+ }),
+});
+
+viewRegistry.add('mrp_documents_kanban', MrpDocumentsKanbanView);
+
+return MrpDocumentsKanbanView;
+
+});
diff --git a/addons/mrp/static/src/js/mrp_documents_controller_mixin.js b/addons/mrp/static/src/js/mrp_documents_controller_mixin.js
new file mode 100644
index 00000000..9bcaf3d6
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_documents_controller_mixin.js
@@ -0,0 +1,126 @@
+odoo.define('mrp.controllerMixin', function (require) {
+'use strict';
+
+const { _t, qweb } = require('web.core');
+const fileUploadMixin = require('web.fileUploadMixin');
+const DocumentViewer = require('mrp.MrpDocumentViewer');
+
+const MrpDocumentsControllerMixin = Object.assign({}, fileUploadMixin, {
+ events: {
+ 'click .o_mrp_documents_kanban_upload': '_onClickMrpDocumentsUpload',
+ },
+ custom_events: Object.assign({}, fileUploadMixin.custom_events, {
+ kanban_image_clicked: '_onKanbanPreview',
+ upload_file: '_onUploadFile',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called right after the reload of the view.
+ */
+ async reload() {
+ await this._renderFileUploads();
+ },
+
+ /**
+ * @override
+ * @param {jQueryElement} $node
+ */
+ renderButtons($node) {
+ this.$buttons = $(qweb.render('MrpDocumentsKanbanView.buttons'));
+ this.$buttons.appendTo($node);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _getFileUploadRoute() {
+ return '/mrp/upload_attachment';
+ },
+
+ /**
+ * @override
+ * @param {integer} param0.recordId
+ */
+ _makeFileUploadFormDataKeys() {
+ const context = this.model.get(this.handle, { raw: true }).getContext();
+ return {
+ res_id: context.default_res_id,
+ res_model: context.default_res_model,
+ };
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onClickMrpDocumentsUpload() {
+ const $uploadInput = $('<input>', {
+ type: 'file',
+ name: 'files[]',
+ multiple: 'multiple'
+ });
+ $uploadInput.on('change', async ev => {
+ await this._uploadFiles(ev.target.files);
+ $uploadInput.remove();
+ });
+ $uploadInput.click();
+ },
+
+ /**
+ * Handles custom event to display the document viewer.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {integer} ev.data.recordID
+ * @param {Array<Object>} ev.data.recordList
+ */
+ _onKanbanPreview(ev) {
+ ev.stopPropagation();
+ const documents = ev.data.recordList;
+ const documentID = ev.data.recordID;
+ const documentViewer = new DocumentViewer(this, documents, documentID);
+ documentViewer.appendTo(this.$('.o_mrp_documents_kanban_view'));
+ },
+
+ /**
+ * Specially created to call `_uploadFiles` method from tests.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ async _onUploadFile(ev) {
+ await this._uploadFiles(ev.data.files);
+ },
+
+ /**
+ * @override
+ * @param {Object} param0
+ * @param {XMLHttpRequest} param0.xhr
+ */
+ _onUploadLoad({ xhr }) {
+ const result = xhr.status === 200
+ ? JSON.parse(xhr.response)
+ : {
+ error: _.str.sprintf(_t("status code: %s </br> message: %s"), xhr.status, xhr.response)
+ };
+ if (result.error) {
+ this.do_notify(_t("Error"), result.error, true);
+ }
+ fileUploadMixin._onUploadLoad.apply(this, arguments);
+ },
+});
+
+return MrpDocumentsControllerMixin;
+
+});
diff --git a/addons/mrp/static/src/js/mrp_documents_document_viewer.js b/addons/mrp/static/src/js/mrp_documents_document_viewer.js
new file mode 100644
index 00000000..c0d7f3e8
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_documents_document_viewer.js
@@ -0,0 +1,19 @@
+odoo.define('mrp.MrpDocumentViewer', function (require) {
+"use strict";
+
+const DocumentViewer = require('mail.DocumentViewer');
+
+/**
+ * This file defines the DocumentViewer for the MRP Documents Kanban view.
+ */
+const MrpDocumentsDocumentViewer = DocumentViewer.extend({
+ init(parent, attachments, activeAttachmentID) {
+ this._super(...arguments);
+ this.modelName = 'mrp.document';
+ },
+});
+
+return MrpDocumentsDocumentViewer;
+
+});
+
diff --git a/addons/mrp/static/src/js/mrp_documents_kanban_controller.js b/addons/mrp/static/src/js/mrp_documents_kanban_controller.js
new file mode 100644
index 00000000..9a123dfb
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_documents_kanban_controller.js
@@ -0,0 +1,37 @@
+odoo.define('mrp.MrpDocumentsKanbanController', function (require) {
+"use strict";
+
+/**
+ * This file defines the Controller for the MRP Documents Kanban view, which is an
+ * override of the KanbanController.
+ */
+
+const MrpDocumentsControllerMixin = require('mrp.controllerMixin');
+
+const KanbanController = require('web.KanbanController');
+
+const MrpDocumentsKanbanController = KanbanController.extend(MrpDocumentsControllerMixin, {
+ events: Object.assign({}, KanbanController.prototype.events, MrpDocumentsControllerMixin.events),
+ custom_events: Object.assign({}, KanbanController.prototype.custom_events, MrpDocumentsControllerMixin.custom_events),
+
+ /**
+ * @override
+ */
+ init() {
+ this._super(...arguments);
+ MrpDocumentsControllerMixin.init.apply(this, arguments);
+ },
+ /**
+ * Override to update the records selection.
+ *
+ * @override
+ */
+ async reload() {
+ await this._super(...arguments);
+ await MrpDocumentsControllerMixin.reload.apply(this, arguments);
+ },
+});
+
+return MrpDocumentsKanbanController;
+
+});
diff --git a/addons/mrp/static/src/js/mrp_documents_kanban_record.js b/addons/mrp/static/src/js/mrp_documents_kanban_record.js
new file mode 100644
index 00000000..44558834
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_documents_kanban_record.js
@@ -0,0 +1,50 @@
+odoo.define('mrp.MrpDocumentsKanbanRecord', function (require) {
+"use strict";
+
+/**
+ * This file defines the KanbanRecord for the MRP Documents Kanban view.
+ */
+
+const KanbanRecord = require('web.KanbanRecord');
+
+const MrpDocumentsKanbanRecord = KanbanRecord.extend({
+ events: Object.assign({}, KanbanRecord.prototype.events, {
+ 'click .o_mrp_download': '_onDownload',
+ 'click .o_kanban_previewer': '_onImageClicked',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Handles the click on the download link to save the attachment locally.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onDownload(ev) {
+ ev.preventDefault();
+ window.location = `/web/content/${this.modelName}/${this.id}/datas?download=true`;
+ },
+
+ /**
+ * Handles the click on the preview image. Triggers up `_onKanbanPreview` to
+ * display `DocumentViewer`.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onImageClicked(ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.trigger_up('kanban_image_clicked', {
+ recordList: [this.recordData],
+ recordID: this.recordData.id
+ });
+ },
+});
+
+return MrpDocumentsKanbanRecord;
+
+});
diff --git a/addons/mrp/static/src/js/mrp_documents_kanban_renderer.js b/addons/mrp/static/src/js/mrp_documents_kanban_renderer.js
new file mode 100644
index 00000000..79ffffe7
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_documents_kanban_renderer.js
@@ -0,0 +1,27 @@
+odoo.define('mrp.MrpDocumentsKanbanRenderer', function (require) {
+"use strict";
+
+/**
+ * This file defines the Renderer for the MRP Documents Kanban view, which is an
+ * override of the KanbanRenderer.
+ */
+
+const KanbanRenderer = require('web.KanbanRenderer');
+const MrpDocumentsKanbanRecord = require('mrp.MrpDocumentsKanbanRecord');
+
+const MrpDocumentsKanbanRenderer = KanbanRenderer.extend({
+ config: Object.assign({}, KanbanRenderer.prototype.config, {
+ KanbanRecord: MrpDocumentsKanbanRecord,
+ }),
+ /**
+ * @override
+ */
+ async start() {
+ this.$el.addClass('o_mrp_documents_kanban_view');
+ await this._super(...arguments);
+ },
+});
+
+return MrpDocumentsKanbanRenderer;
+
+});
diff --git a/addons/mrp/static/src/js/mrp_should_consume.js b/addons/mrp/static/src/js/mrp_should_consume.js
new file mode 100644
index 00000000..d432c5cf
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_should_consume.js
@@ -0,0 +1,81 @@
+odoo.define('mrp.should_consume', function (require) {
+"use strict";
+
+const BasicFields = require('web.basic_fields');
+const FieldFloat = BasicFields.FieldFloat;
+const fieldRegistry = require('web.field_registry');
+const field_utils = require('web.field_utils');
+
+/**
+ * This widget is used to display alongside the total quantity to consume of a production order,
+ * the exact quantity that the worker should consume depending on the BoM. Ex:
+ * 2 components to make 1 finished product.
+ * The production order is created to make 5 finished product and the quantity producing is set to 3.
+ * The widget will be '3.000 / 5.000'.
+ */
+const MrpShouldConsume = FieldFloat.extend({
+ /**
+ * @override
+ */
+ init: function (parent, name, params) {
+ this._super.apply(this, arguments);
+ this.displayShouldConsume = !['done', 'draft', 'cancel'].includes(params.data.state);
+ this.should_consume_qty = field_utils.format.float(params.data.should_consume_qty, params.fields.should_consume_qty, this.nodeOptions);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prefix the classic float field (this.$el) by a static value.
+ *
+ * @private
+ * @param {float} [value] quantity to display before the input `el`
+ * @param {bool} [edit] whether the field will be editable or readonly
+ */
+ _addShouldConsume: function (value, edit=false) {
+ const $to_consume_container = $('<span class="o_should_consume"/>');
+ if (edit) {
+ $to_consume_container.addClass('o_row');
+ }
+ $to_consume_container.text(value + ' / ');
+ this.setElement(this.$el.wrap($to_consume_container).parent());
+ },
+
+ /**
+ * @private
+ * @override
+ */
+ _renderEdit: function () {
+ if (this.displayShouldConsume) {
+ if (!this.$el.text().includes('/')) {
+ this.$input = this.$el;
+ this._addShouldConsume(this.should_consume_qty, true);
+ }
+ this._prepareInput(this.$input);
+ } else {
+ this._super.apply(this);
+ }
+ },
+ /**
+ * Resets the content to the formated value in readonly mode.
+ *
+ * @override
+ * @private
+ */
+ _renderReadonly: function () {
+ this.$el.text(this._formatValue(this.value));
+ if (this.displayShouldConsume) {
+ this._addShouldConsume(this.should_consume_qty);
+ }
+ },
+});
+
+fieldRegistry.add('mrp_should_consume', MrpShouldConsume);
+
+return {
+ MrpShouldConsume: MrpShouldConsume,
+};
+
+});
diff --git a/addons/mrp/static/src/js/mrp_workorder_popover.js b/addons/mrp/static/src/js/mrp_workorder_popover.js
new file mode 100644
index 00000000..7e392c09
--- /dev/null
+++ b/addons/mrp/static/src/js/mrp_workorder_popover.js
@@ -0,0 +1,51 @@
+odoo.define('mrp.mrp_workorder_popover', function (require) {
+'use strict';
+
+var PopoverWidget = require('stock.popover_widget');
+var fieldRegistry = require('web.field_registry');
+var core = require('web.core');
+var _t = core._t;
+
+
+/**
+ * Link to a Char field representing a JSON:
+ * {
+ * 'replan': <REPLAN_BOOL>, // Show the replan btn
+ * 'color': '<COLOR_CLASS>', // Color Class of the icon (d-none to hide)
+ * 'infos': [
+ * {'msg' : '<MESSAGE>', 'color' : '<COLOR_CLASS>'},
+ * {'msg' : '<MESSAGE>', 'color' : '<COLOR_CLASS>'},
+ * ... ]
+ * }
+ */
+var MrpWorkorderPopover = PopoverWidget.extend({
+ popoverTemplate: 'mrp.workorderPopover',
+ title: _t('Scheduling Information'),
+
+ _render: function () {
+ this._super.apply(this, arguments);
+ if (! this.$popover) {
+ return;
+ }
+ var self = this;
+ this.$popover.find('.action_replan_button').click(function (e) {
+ self._onReplanClick(e);
+ });
+ },
+
+ _onReplanClick:function (e) {
+ var self = this;
+ this._rpc({
+ model: 'mrp.workorder',
+ method: 'action_replan',
+ args: [[self.res_id]]
+ }).then(function () {
+ self.trigger_up('reload');
+ });
+ },
+});
+
+fieldRegistry.add('mrp_workorder_popover', MrpWorkorderPopover);
+
+return MrpWorkorderPopover;
+});
diff --git a/addons/mrp/static/src/scss/mrp_bom_report.scss b/addons/mrp/static/src/scss/mrp_bom_report.scss
new file mode 100644
index 00000000..ca2e1085
--- /dev/null
+++ b/addons/mrp/static/src/scss/mrp_bom_report.scss
@@ -0,0 +1,50 @@
+.o_mrp_bom_report_page {
+ background-color: $o-view-background-color;
+ margin: 16px auto;
+ padding: 24px 16px;
+ // Manage expand icon
+ table.o_mrp_bom_expandable {
+ thead tr {
+ border-width: 2px;
+ border-top-style: groove;
+ border-bottom-style: groove;
+ }
+ tbody tr {
+ border-width: 1px;
+ border-top-style: solid;
+ border-bottom-style: groove;
+ .o_mrp_bom_unfoldable, .o_mrp_bom_foldable {
+ cursor: pointer;
+ }
+ }
+ tbody, tfoot {
+ & > tr > td ~ td > span {
+ margin-left: 10px;
+ }
+ tr {
+ .o_mrp_bom_no_fold {
+ margin-left: 18px;
+ }
+ .o_mrp_bom_unfoldable, .o_mrp_bom_foldable {
+ margin-left: -2px;
+ }
+ }
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ .o_mrp_bom_report_line {
+ & .fa-caret-down, & .fa-caret-right {
+ font-size: 2em;
+ }
+ }
+ }
+}
+
+.o_mrp_bom_report_buttons {
+ @include media-breakpoint-down(sm) {
+ display: grid;
+ grid-gap: 4px; // For compatibility
+ gap: 4px;
+ }
+}
diff --git a/addons/mrp/static/src/scss/mrp_document_kanban_view.scss b/addons/mrp/static/src/scss/mrp_document_kanban_view.scss
new file mode 100644
index 00000000..536ef77d
--- /dev/null
+++ b/addons/mrp/static/src/scss/mrp_document_kanban_view.scss
@@ -0,0 +1,3 @@
+.o_kanban_previewer:hover {
+ cursor: zoom-in;
+}
diff --git a/addons/mrp/static/src/scss/mrp_fields.scss b/addons/mrp/static/src/scss/mrp_fields.scss
new file mode 100644
index 00000000..da959565
--- /dev/null
+++ b/addons/mrp/static/src/scss/mrp_fields.scss
@@ -0,0 +1,11 @@
+.o_field_widget.o_embed_url_viewer{
+ width: 100% !important;
+ iframe {
+ width: 100%;
+ height: 30rem;
+ border: none;
+ }
+}
+.o_should_consume{
+ padding-left: 0.3em;
+}
diff --git a/addons/mrp/static/src/scss/mrp_gantt.scss b/addons/mrp/static/src/scss/mrp_gantt.scss
new file mode 100644
index 00000000..5d6ed467
--- /dev/null
+++ b/addons/mrp/static/src/scss/mrp_gantt.scss
@@ -0,0 +1,19 @@
+@mixin gantt-decoration-color($color) {
+ background-image: linear-gradient($color, $color);
+ background-color: lighten($color, 10%);
+ &:before {
+ content: none;
+ }
+}
+
+.o_mrp_workorder_gantt .o_gantt_view .o_gantt_row_container .o_gantt_row .o_gantt_cell .o_gantt_pill_wrapper {
+ div.o_gantt_pill.decoration-success {
+ @include gantt-decoration-color($success);
+ }
+ div.o_gantt_pill.decoration-warning {
+ @include gantt-decoration-color(gray('500'));
+ }
+ div.o_gantt_pill.decoration-danger {
+ @include gantt-decoration-color($danger);
+ }
+}
diff --git a/addons/mrp/static/src/scss/mrp_workorder_kanban.scss b/addons/mrp/static/src/scss/mrp_workorder_kanban.scss
new file mode 100644
index 00000000..d2580da4
--- /dev/null
+++ b/addons/mrp/static/src/scss/mrp_workorder_kanban.scss
@@ -0,0 +1,10 @@
+.o_kanban_dashboard.o_kanban_view {
+ &.o_mrp_workorder_kanban,&.o_workcenter_kanban {
+ .o_kanban_group:not(.o_column_folded) {
+ width: 400px + $o-kanban-group-padding;
+ }
+ .o_kanban_record {
+ width: 400px;
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/mrp/static/src/xml/mrp.xml b/addons/mrp/static/src/xml/mrp.xml
new file mode 100644
index 00000000..ee4eb10e
--- /dev/null
+++ b/addons/mrp/static/src/xml/mrp.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <t t-name="mrp.button">
+ <div class="o_list_buttons o_mrp_bom_report_buttons">
+ <button type="button" class="btn btn-primary o_mrp_bom_print">Print</button>
+ <t t-if="is_variant_applied">
+ <button type="button" class="btn btn-primary o_mrp_bom_print_all_variants">Print All Variants</button>
+ </t>
+ <button type="button" class="btn btn-primary o_mrp_bom_print_unfolded">Print Unfolded</button>
+ </div>
+ </t>
+
+ <form class="form-inline" t-name="mrp.report_bom_search">
+ <div class="form-group col-lg-4">
+ <label>Quantity:</label>
+ <div class="row">
+ <div class="col-lg-6">
+ <input type="number" step="any" t-att-value="bom_qty" min="1" class="o_input o_mrp_bom_report_qty"/>
+ </div>
+ <div class="col-lg-6">
+ <t t-if="is_uom_applied" t-esc="bom_uom_name"/>
+ </div>
+ </div>
+ </div>
+ <div t-if="is_variant_applied" class="form-group col-lg-4">
+ <label>Variant:</label>
+ <select class="o_input o_mrp_bom_report_variants">
+ <option t-foreach="variants" t-as="variant" t-att-value="variant">
+ <t t-esc="variants[variant]"/>
+ </option>
+ </select>
+ </div>
+ <div t-attf-class="form-group #{is_variant_applied ? 'col-lg-4' : 'col-lg-8'}">
+ <label>Report:</label>
+ <select class="o_input o_mrp_bom_report_type">
+ <option t-att-data-type="'all'">BoM Structure &amp; Cost</option>
+ <option t-att-data-type="'bom_structure'">BoM Structure</option>
+ </select>
+ </div>
+ </form>
+
+ <div t-name="mrp.workorderPopover">
+ <t t-foreach="infos" t-as="info">
+ <i t-attf-class="fa fa-arrow-right mr-2 #{ info.color }"></i><t t-esc="info.msg"/><br/>
+ </t>
+ <button t-if="replan" class="btn btn-primary action_replan_button">Replan</button>
+ </div>
+
+</templates>
diff --git a/addons/mrp/static/src/xml/mrp_document_template.xml b/addons/mrp/static/src/xml/mrp_document_template.xml
new file mode 100644
index 00000000..23ea08d8
--- /dev/null
+++ b/addons/mrp/static/src/xml/mrp_document_template.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+<div t-name="MrpDocumentsKanbanView.buttons">
+ <button type="button" t-attf-class="btn btn-primary o_mrp_documents_kanban_upload">
+ Upload
+ </button>
+</div>
+</templates>
+