summaryrefslogtreecommitdiff
path: root/addons/mrp/static
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
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mrp/static')
-rw-r--r--addons/mrp/static/description/icon.pngbin0 -> 8088 bytes
-rw-r--r--addons/mrp/static/description/icon.svg1
-rw-r--r--addons/mrp/static/img/assebly-worksheet.pdfbin0 -> 92845 bytes
-rw-r--r--addons/mrp/static/img/cutting-worksheet.pdfbin0 -> 177627 bytes
-rw-r--r--addons/mrp/static/img/drill-worksheet.pdfbin0 -> 76728 bytes
-rw-r--r--addons/mrp/static/img/product_product_computer_desk_bolt.pngbin0 -> 49257 bytes
-rw-r--r--addons/mrp/static/img/product_product_computer_desk_screw.pngbin0 -> 45921 bytes
-rw-r--r--addons/mrp/static/img/product_product_drawer_black.pngbin0 -> 76074 bytes
-rw-r--r--addons/mrp/static/img/product_product_drawer_case_black.pngbin0 -> 28379 bytes
-rw-r--r--addons/mrp/static/img/product_product_plastic_laminate.pngbin0 -> 20785 bytes
-rw-r--r--addons/mrp/static/img/product_product_ply_veneer.pngbin0 -> 22026 bytes
-rw-r--r--addons/mrp/static/img/product_product_table_kit.pngbin0 -> 26543 bytes
-rw-r--r--addons/mrp/static/img/product_product_wood_panel.pngbin0 -> 50782 bytes
-rw-r--r--addons/mrp/static/img/product_product_wood_ply.pngbin0 -> 77428 bytes
-rw-r--r--addons/mrp/static/img/product_product_wood_wear.pngbin0 -> 37683 bytes
-rw-r--r--addons/mrp/static/img/table.pngbin0 -> 16573 bytes
-rw-r--r--addons/mrp/static/img/table_leg.pngbin0 -> 4014 bytes
-rw-r--r--addons/mrp/static/img/table_top.pngbin0 -> 14178 bytes
-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
-rw-r--r--addons/mrp/static/tests/mrp_document_kanban_tests.js172
-rw-r--r--addons/mrp/static/tests/mrp_tests.js133
-rw-r--r--addons/mrp/static/xls/mrp_bom.xlsbin0 -> 31232 bytes
39 files changed, 1365 insertions, 0 deletions
diff --git a/addons/mrp/static/description/icon.png b/addons/mrp/static/description/icon.png
new file mode 100644
index 00000000..2fa36974
--- /dev/null
+++ b/addons/mrp/static/description/icon.png
Binary files differ
diff --git a/addons/mrp/static/description/icon.svg b/addons/mrp/static/description/icon.svg
new file mode 100644
index 00000000..cca7bcfd
--- /dev/null
+++ b/addons/mrp/static/description/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#7CC098"/><stop offset="100%" stop-color="#5F8A71"/></linearGradient><path id="d" d="M53.373 32.454c.742 0 1.206.784.824 1.4-2.009 3.235-5.668 5.4-9.848 5.4-6.318 0-11.445-4.944-11.484-11.056C32.825 22.058 38.012 17 44.349 17c4.176 0 7.831 2.16 9.842 5.389.385.62-.07 1.41-.818 1.41h-8.386l-3.19 4.328 3.19 4.327h8.386zM39.684 39.64l-15.97 15.473c-1.994 1.931-5.226 1.931-7.22 0a4.837 4.837 0 0 1 0-6.994l15.971-15.472c1.289 3.183 3.932 5.744 7.219 6.993zm-16.39 10.74c0-1.024-.857-1.854-1.914-1.854s-1.914.83-1.914 1.854.857 1.854 1.914 1.854 1.914-.83 1.914-1.854z"/><path id="e" d="M53.373 30.454c.742 0 1.206.784.824 1.4-2.009 3.235-5.668 5.4-9.848 5.4-6.318 0-11.445-4.944-11.484-11.056C32.825 20.058 38.012 15 44.349 15c4.176 0 7.831 2.16 9.842 5.389.385.62-.07 1.41-.818 1.41h-8.386l-3.19 4.328 3.19 4.327h8.386zM39.684 37.64l-15.97 15.473c-1.994 1.931-5.226 1.931-7.22 0a4.837 4.837 0 0 1 0-6.994l15.971-15.472c1.289 3.183 3.932 5.744 7.219 6.993zm-16.39 10.74c0-1.024-.857-1.854-1.914-1.854s-1.914.83-1.914 1.854.857 1.854 1.914 1.854 1.914-.83 1.914-1.854z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.075l15.053-15.2 18.452.218 2.734 6.93v4.524L53.62 51.98 42.667 69H4z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" transform="matrix(-1 0 0 1 69.334 0)" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" transform="matrix(-1 0 0 1 69.334 0)" xlink:href="#e"/></g></g></svg> \ No newline at end of file
diff --git a/addons/mrp/static/img/assebly-worksheet.pdf b/addons/mrp/static/img/assebly-worksheet.pdf
new file mode 100644
index 00000000..c5975da4
--- /dev/null
+++ b/addons/mrp/static/img/assebly-worksheet.pdf
Binary files differ
diff --git a/addons/mrp/static/img/cutting-worksheet.pdf b/addons/mrp/static/img/cutting-worksheet.pdf
new file mode 100644
index 00000000..10b86755
--- /dev/null
+++ b/addons/mrp/static/img/cutting-worksheet.pdf
Binary files differ
diff --git a/addons/mrp/static/img/drill-worksheet.pdf b/addons/mrp/static/img/drill-worksheet.pdf
new file mode 100644
index 00000000..86d58a6c
--- /dev/null
+++ b/addons/mrp/static/img/drill-worksheet.pdf
Binary files differ
diff --git a/addons/mrp/static/img/product_product_computer_desk_bolt.png b/addons/mrp/static/img/product_product_computer_desk_bolt.png
new file mode 100644
index 00000000..4bfe2ee8
--- /dev/null
+++ b/addons/mrp/static/img/product_product_computer_desk_bolt.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_computer_desk_screw.png b/addons/mrp/static/img/product_product_computer_desk_screw.png
new file mode 100644
index 00000000..897182ba
--- /dev/null
+++ b/addons/mrp/static/img/product_product_computer_desk_screw.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_drawer_black.png b/addons/mrp/static/img/product_product_drawer_black.png
new file mode 100644
index 00000000..59a1178c
--- /dev/null
+++ b/addons/mrp/static/img/product_product_drawer_black.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_drawer_case_black.png b/addons/mrp/static/img/product_product_drawer_case_black.png
new file mode 100644
index 00000000..5534f67f
--- /dev/null
+++ b/addons/mrp/static/img/product_product_drawer_case_black.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_plastic_laminate.png b/addons/mrp/static/img/product_product_plastic_laminate.png
new file mode 100644
index 00000000..a7b2083c
--- /dev/null
+++ b/addons/mrp/static/img/product_product_plastic_laminate.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_ply_veneer.png b/addons/mrp/static/img/product_product_ply_veneer.png
new file mode 100644
index 00000000..cf2c834c
--- /dev/null
+++ b/addons/mrp/static/img/product_product_ply_veneer.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_table_kit.png b/addons/mrp/static/img/product_product_table_kit.png
new file mode 100644
index 00000000..73d47f10
--- /dev/null
+++ b/addons/mrp/static/img/product_product_table_kit.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_wood_panel.png b/addons/mrp/static/img/product_product_wood_panel.png
new file mode 100644
index 00000000..9b609954
--- /dev/null
+++ b/addons/mrp/static/img/product_product_wood_panel.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_wood_ply.png b/addons/mrp/static/img/product_product_wood_ply.png
new file mode 100644
index 00000000..fc4ed564
--- /dev/null
+++ b/addons/mrp/static/img/product_product_wood_ply.png
Binary files differ
diff --git a/addons/mrp/static/img/product_product_wood_wear.png b/addons/mrp/static/img/product_product_wood_wear.png
new file mode 100644
index 00000000..2b6dc394
--- /dev/null
+++ b/addons/mrp/static/img/product_product_wood_wear.png
Binary files differ
diff --git a/addons/mrp/static/img/table.png b/addons/mrp/static/img/table.png
new file mode 100644
index 00000000..770e74e2
--- /dev/null
+++ b/addons/mrp/static/img/table.png
Binary files differ
diff --git a/addons/mrp/static/img/table_leg.png b/addons/mrp/static/img/table_leg.png
new file mode 100644
index 00000000..22a99e4d
--- /dev/null
+++ b/addons/mrp/static/img/table_leg.png
Binary files differ
diff --git a/addons/mrp/static/img/table_top.png b/addons/mrp/static/img/table_top.png
new file mode 100644
index 00000000..e17aed23
--- /dev/null
+++ b/addons/mrp/static/img/table_top.png
Binary files differ
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>
+
diff --git a/addons/mrp/static/tests/mrp_document_kanban_tests.js b/addons/mrp/static/tests/mrp_document_kanban_tests.js
new file mode 100644
index 00000000..366bb1bd
--- /dev/null
+++ b/addons/mrp/static/tests/mrp_document_kanban_tests.js
@@ -0,0 +1,172 @@
+odoo.define('mrp.document_kanban_tests', function (require) {
+"use strict";
+
+const MrpDocumentsKanbanView = require('mrp.MrpDocumentsKanbanView');
+const MrpDocumentsKanbanController = require('mrp.MrpDocumentsKanbanController');
+const testUtils = require('web.test_utils');
+
+const createView = testUtils.createView;
+
+QUnit.module('Views', {}, function () {
+
+QUnit.module('MrpDocumentsKanbanView', {
+ beforeEach: function () {
+ this.ORIGINAL_CREATE_XHR = MrpDocumentsKanbanController.prototype._createXHR;
+ this.patchDocumentXHR = (mockedXHRs, customSend) => {
+ MrpDocumentsKanbanController.prototype._createXhr = () => {
+ const xhr = {
+ upload: new window.EventTarget(),
+ open() { },
+ send(data) { customSend && customSend(data); },
+ };
+ mockedXHRs.push(xhr);
+ return xhr;
+ };
+ };
+ this.data = {
+ 'mrp.document': {
+ fields: {
+ name: {string: "Name", type: 'char', default: ' '},
+ priority: {string: 'priority', type: 'selection',
+ selection: [['0', 'Normal'], ['1', 'Low'], ['2', 'High'], ['3', 'Very High']]},
+ },
+ records: [
+ {id: 1, name: 'test1', priority: 2},
+ {id: 4, name: 'test2', priority: 1},
+ {id: 3, name: 'test3', priority: 3},
+ ],
+ },
+ };
+ },
+ afterEach() {
+ MrpDocumentsKanbanController.prototype._createXHR = this.ORIGINAL_CREATE_XHR;
+ },
+}, function () {
+ QUnit.test('MRP documents kanban basic rendering', async function (assert) {
+ assert.expect(6);
+
+ const kanban = await createView({
+ View: MrpDocumentsKanbanView,
+ model: 'mrp.document',
+ data: this.data,
+ arch: '<kanban><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ });
+
+ assert.ok(kanban, "kanban is created");
+ assert.ok(kanban.$buttons.find('.o_mrp_documents_kanban_upload'),
+ "should have upload button in kanban buttons");
+ assert.containsN(kanban, '.o_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 3,
+ "should have 3 records in the renderer");
+ // check view layout
+ assert.hasClass(kanban.$('.o_kanban_view'), 'o_mrp_documents_kanban_view',
+ "should have classname 'o_mrp_documents_kanban_view'");
+ // check control panel buttons
+ assert.containsN(kanban, '.o_cp_buttons .btn-primary', 1,
+ "should have only 1 primary button i.e. Upload button");
+ assert.strictEqual(kanban.$('.o_cp_buttons .btn-primary:first').text().trim(), 'Upload',
+ "should have a primary 'Upload' button");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('mrp: upload multiple files', async function (assert) {
+ assert.expect(4);
+
+ const file1 = await testUtils.file.createFile({
+ name: 'text1.txt',
+ content: 'hello, world',
+ contentType: 'text/plain',
+ });
+ const file2 = await testUtils.file.createFile({
+ name: 'text2.txt',
+ content: 'hello, world',
+ contentType: 'text/plain',
+ });
+ const file3 = await testUtils.file.createFile({
+ name: 'text3.txt',
+ content: 'hello, world',
+ contentType: 'text/plain',
+ });
+
+ const mockedXHRs = [];
+ this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
+
+ const kanban = await createView({
+ View: MrpDocumentsKanbanView,
+ model: 'mrp.document',
+ data: this.data,
+ arch: '<kanban><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ });
+
+ kanban.trigger_up('upload_file', {files: [file1]});
+ await testUtils.nextTick();
+ assert.verifySteps(['xhrSend']);
+
+ kanban.trigger_up('upload_file', {files: [file2, file3]});
+ await testUtils.nextTick();
+ assert.verifySteps(['xhrSend']);
+
+ kanban.destroy();
+ });
+
+ QUnit.test('mrp: upload progress bars', async function (assert) {
+ assert.expect(4);
+
+ const file1 = await testUtils.file.createFile({
+ name: 'text1.txt',
+ content: 'hello, world',
+ contentType: 'text/plain',
+ });
+
+ const mockedXHRs = [];
+ this.patchDocumentXHR(mockedXHRs, data => assert.step('xhrSend'));
+
+ const kanban = await createView({
+ View: MrpDocumentsKanbanView,
+ model: 'mrp.document',
+ data: this.data,
+ arch: '<kanban><templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ });
+
+ kanban.trigger_up('upload_file', {files: [file1]});
+ await testUtils.nextTick();
+ assert.verifySteps(['xhrSend']);
+
+ const progressEvent = new Event('progress', { bubbles: true });
+ progressEvent.loaded = 250000000;
+ progressEvent.total = 500000000;
+ progressEvent.lengthComputable = true;
+ mockedXHRs[0].upload.dispatchEvent(progressEvent);
+ assert.strictEqual(
+ kanban.$('.o_file_upload_progress_text_left').text(),
+ "Uploading... (50%)",
+ "the current upload progress should be at 50%"
+ );
+
+ progressEvent.loaded = 350000000;
+ mockedXHRs[0].upload.dispatchEvent(progressEvent);
+ assert.strictEqual(
+ kanban.$('.o_file_upload_progress_text_right').text(),
+ "(350/500Mb)",
+ "the current upload progress should be at (350/500Mb)"
+ );
+
+ kanban.destroy();
+ });
+});
+
+});
+
+});
diff --git a/addons/mrp/static/tests/mrp_tests.js b/addons/mrp/static/tests/mrp_tests.js
new file mode 100644
index 00000000..f05a5b42
--- /dev/null
+++ b/addons/mrp/static/tests/mrp_tests.js
@@ -0,0 +1,133 @@
+odoo.define('mrp.tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require("web.test_utils");
+
+var createView = testUtils.createView;
+
+QUnit.module('mrp', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ state: {
+ string: "State",
+ type: "selection",
+ selection: [['waiting', 'Waiting'], ['chilling', 'Chilling']],
+ },
+ duration: {string: "Duration", type: "float"},
+ },
+ records: [{
+ id: 1,
+ state: 'waiting',
+ duration: 6000,
+ }],
+ onchanges: {},
+ },
+ };
+ },
+}, function () {
+
+ QUnit.test("bullet_state: basic rendering", async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ res_id: 1,
+ arch:
+ '<form>' +
+ '<field name="state" widget="bullet_state" options="{\'classes\': {\'waiting\': \'danger\'}}"/>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('.o_field_widget').text(), "Waiting Materials",
+ "the widget should be correctly named");
+ assert.containsOnce(form, '.o_field_widget .badge-danger',
+ "the badge should be danger");
+
+ form.destroy();
+ });
+
+ QUnit.test("mrp_time_counter: basic rendering", async function (assert) {
+ assert.expect(2);
+ var data = {
+ foo: {
+ fields: { duration: { string: "Duration", type: "float" } },
+ records: [{id: 1, duration:150.5}]
+ },
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'foo',
+ data: data,
+ res_id: 1,
+ arch:
+ '<form>' +
+ '<field name="duration" widget="mrp_time_counter"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read' && args.model === 'mrp.workcenter.productivity') {
+ assert.ok(true, "the widget should fetch the mrp.workcenter.productivity");
+ return Promise.resolve([]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name="duration"]').text(), "150:30",
+ "the timer should be correctly set");
+
+ form.destroy();
+ });
+
+ QUnit.test("embed_viewer rendering in form view", async function (assert) {
+ assert.expect(8);
+ var data = {
+ foo: {
+ fields: { char_url: { string: "URL", type: "char" } },
+ records: [{ id: 1 }]
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'foo',
+ data: data,
+ arch:
+ '<form>' +
+ '<field name="char_url" widget="embed_viewer"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (route === ('http://example.com')) {
+ return Promise.resolve();
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.isNotVisible(form.$('iframe.o_embed_iframe'), "there should be an invisible iframe readonly mode");
+ assert.strictEqual(_.has(form.$('iframe.o_embed_iframe')[0].attributes, "src"), false,
+ "src attribute is not set if there are no values");
+ await testUtils.form.clickEdit(form);
+ assert.isNotVisible(form.$('iframe.o_embed_iframe'), "there should be an invisible iframe in edit mode");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_char'), 'http://example.com', ['input', 'change', 'focusout']);
+ assert.strictEqual(form.$('iframe.o_embed_iframe').attr('src'), 'http://example.com',
+ "src should updated on the iframe");
+ assert.isVisible(form.$('iframe.o_embed_iframe'), "there should be a visible iframe in edit mode");
+ await testUtils.form.clickSave(form);
+ assert.isVisible(form.$('iframe.o_embed_iframe'), "there should be a visible iframe in readonly mode");
+ assert.strictEqual(form.$('iframe.o_embed_iframe').attr('data-src'), 'http://example.com',
+ "should have updated src in readonly mode");
+
+ // In readonly mode, we are not displaying the URL, only iframe will be there.
+ assert.strictEqual(form.$('.iframe.o_embed_iframe').siblings().length, 0,
+ "there shouldn't be any siblings of iframe in readonly mode");
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/mrp/static/xls/mrp_bom.xls b/addons/mrp/static/xls/mrp_bom.xls
new file mode 100644
index 00000000..4114d545
--- /dev/null
+++ b/addons/mrp/static/xls/mrp_bom.xls
Binary files differ