diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index 9a6505f037f..3141d3a6372 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -37,8 +37,10 @@ 'views/mrp_templates.xml', 'views/stock_scrap_views.xml', 'report/mrp_report_views_main.xml', + 'report/mrp_report_bom_structure.xml', 'report/mrp_production_templates.xml', ], + 'qweb': ['static/src/xml/mrp.xml'], 'demo': [ 'data/mrp_demo.xml', ], diff --git a/addons/mrp/report/__init__.py b/addons/mrp/report/__init__.py index 2903e8d9121..29fee71b20b 100644 --- a/addons/mrp/report/__init__.py +++ b/addons/mrp/report/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import mrp_report_bom_structure \ No newline at end of file diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py new file mode 100644 index 00000000000..f60c0693ff4 --- /dev/null +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- + +import json + +from odoo import api, models, _ +from odoo.tools import float_round + +class ReportBomStructure(models.AbstractModel): + _name = 'report.mrp.report_bom_structure' + + @api.model + def get_report_values(self, docids, data=None): + docs = [] + for bom_id in docids: + bom = self.env['mrp.bom'].browse(bom_id) + variant = data and data.get('variant') + candidates = variant and self.env['product.product'].browse(variant) or bom.product_tmpl_id.product_variant_ids + for product_variant_id in candidates: + if data and data.get('childs'): + doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=int(data.get('quantity')), child_bom_ids=json.loads(data.get('childs'))) + else: + doc = self._get_pdf_line(bom_id, product_id=product_variant_id, unfolded=True) + doc['report_type'] = 'pdf' + doc['report_structure'] = data and data.get('report_type') or 'all' + docs.append(doc) + return { + 'doc_ids': docids, + 'doc_model': 'mrp.bom', + 'docs': docs, + } + + @api.model + def get_html(self, bom_id=False, searchQty=1, searchVariant=False): + res = self._get_report_data(bom_id=bom_id, searchQty=searchQty, searchVariant=searchVariant) + res['lines']['report_type'] = 'html' + res['lines']['report_structure'] = 'all' + res['lines'] = self.env.ref('mrp.report_mrp_bom').render({'data': res['lines']}) + return res + + @api.model + def get_bom(self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False): + lines = self._get_bom(bom_id=bom_id, product_id=product_id, line_qty=line_qty, line_id=line_id, level=level) + return self.env.ref('mrp.report_mrp_bom_line').render({'data': lines}) + + @api.model + def get_operations(self, bom_id=False, qty=0, level=0): + bom = self.env['mrp.bom'].browse(bom_id) + lines = self._get_operation_line(bom.routing_id, qty, level) + values = { + 'bom_id': bom_id, + 'currency': self.env.user.company_id.currency_id, + 'operations': lines, + } + return self.env.ref('mrp.report_mrp_operation_line').render({'data': values}) + + @api.model + def _get_report_data(self, bom_id, searchQty=0, searchVariant=False): + lines = {} + bom = self.env['mrp.bom'].browse(bom_id) + bom_quantity = searchQty or bom.product_qty + bom_product_variants = {} + bom_uom_name = '' + + if bom: + bom_uom_name = bom.product_uom_id.name + + # Get variants used for search + if not bom.product_id: + for variant in bom.product_tmpl_id.product_variant_ids: + bom_product_variants[variant.id] = variant.display_name + + lines = self._get_bom(bom_id, product_id=searchVariant, line_qty=bom_quantity, level=1) + + return { + 'lines': lines, + 'variants': bom_product_variants, + 'bom_uom_name': bom_uom_name, + 'bom_qty': bom_quantity, + 'is_variant_applied': self.env.user.user_has_groups('product.group_product_variant') and len(bom_product_variants) > 1, + 'is_uom_applied': self.env.user.user_has_groups('uom.group_uom') + } + + def _get_bom(self, bom_id=False, product_id=False, line_qty=False, line_id=False, level=False): + bom = self.env['mrp.bom'].browse(bom_id) + bom_quantity = line_qty + if line_id: + current_line = self.env['mrp.bom.line'].browse(int(line_id)) + bom_quantity = current_line.product_uom_id._compute_quantity(line_qty, bom.product_uom_id) + # Display bom components for current selected product variant + if product_id: + product = self.env['product.product'].browse(int(product_id)) + else: + product = bom.product_id or bom.product_tmpl_id.product_variant_id + operations = self._get_operation_line(bom.routing_id, (bom_quantity / bom.product_qty), 0) + lines = { + 'bom': bom, + 'bom_qty': bom_quantity, + 'bom_prod_name': product.display_name, + 'currency': self.env.user.company_id.currency_id, + 'product': product, + 'price': product.uom_id._compute_price(product.standard_price, bom.product_uom_id) * bom_quantity, + 'total': sum([op['total'] for op in operations]), + 'level': level or 0, + 'operations': operations, + 'operations_cost': sum([op['total'] for op in operations]), + 'operations_time': sum([op['duration_expected'] for op in operations]) + } + components, total = self._get_bom_lines(bom, bom_quantity, product, line_id, level) + lines['components'] = components + lines['total'] += total + return lines + + def _get_bom_lines(self, bom, bom_quantity, product, line_id, level): + components = [] + total = 0 + for line in bom.bom_line_ids: + line_quantity = (bom_quantity) * line.product_qty + if line._skip_bom_line(product): + continue + price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity + if line.child_bom_id: + factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) * line.child_bom_id.product_qty + total = self._get_price(line.child_bom_id, factor) + else: + total = price + components.append({ + 'prod_id': line.product_id.id, + 'prod_name': line.product_id.display_name, + 'prod_qty': line_quantity, + 'prod_uom': line.product_uom_id.name, + 'prod_cost': price, + 'parent_id': bom.id, + 'line_id': line.id, + 'level': level or 0, + 'total': total, + 'child_bom': line.child_bom_id.id, + 'phantom_bom': line.child_bom_id and line.child_bom_id.type == 'phantom' or False, + }) + total += total + return components, total + + def _get_operation_line(self, routing, qty, level): + operations = [] + total = 0.0 + for operation in routing.operation_ids: + duration_expected = qty * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start + total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour) + operations.append({ + 'level': level or 0, + 'operation': operation, + 'name': operation.name + ' - ' + operation.workcenter_id.name, + 'duration_expected': duration_expected, + 'total': float_round(total, precision_rounding=self.env.user.company_id.currency_id.rounding), + }) + return operations + + def _get_price(self, bom, factor): + price = 0 + if bom.routing_id: + operations = self._get_operation_line(bom.routing_id, factor, 0) + price += sum([op['total'] for op in operations]) + + for line in bom.bom_line_ids: + if line.child_bom_id: + qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id) + sub_price = self._get_price(line.child_bom_id, qty) + price += sub_price * qty + else: + prod_qty = line.product_qty * factor + price += (line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty) + return price + + def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False): + + data = self._get_bom(bom_id=bom_id, product_id=product_id, line_qty=qty) + + def get_sub_lines(bom, product_id, line_qty, line_id, level): + data = self._get_bom(bom_id=bom.id, product_id=product_id, line_qty=line_qty, line_id=line_id, level=level) + bom_lines = data['components'] + lines = [] + for bom_line in bom_lines: + lines.append({ + 'name': bom_line['prod_name'], + 'type': 'bom', + 'quantity': bom_line['prod_qty'], + 'uom': bom_line['prod_uom'], + 'prod_cost': bom_line['prod_cost'], + 'bom_cost': bom_line['total'], + 'level': bom_line['level'], + }) + if bom_line['child_bom'] and (unfolded or bom_line['child_bom'] in child_bom_ids): + line = self.env['mrp.bom.line'].browse(bom_line['line_id']) + lines += (get_sub_lines(line.child_bom_id, line.product_id, line.product_qty * data['bom_qty'], line, level + 1)) + if data['operations']: + lines.append({ + 'name': _('Operations'), + 'type': 'operation', + 'quantity': data['operations_time'], + 'uom': _('minutes'), + 'bom_cost': data['operations_cost'], + 'level': level, + }) + for operation in data['operations']: + if unfolded or 'operation-' + str(bom.id) in child_bom_ids: + lines.append({ + 'name': operation['name'], + 'type': 'operation', + 'quantity': operation['duration_expected'], + 'uom': _('minutes'), + 'bom_cost': operation['total'], + 'level': level + 1, + }) + return lines + + bom = self.env['mrp.bom'].browse(bom_id) + product = product_id or bom.product_id or bom.product_tmpl_id.product_variant_id + pdf_lines = get_sub_lines(bom, product, qty, False, 1) + data['components'] = [] + data['lines'] = pdf_lines + return data diff --git a/addons/mrp/report/mrp_report_bom_structure.xml b/addons/mrp/report/mrp_report_bom_structure.xml new file mode 100644 index 00000000000..2122a9e10f7 --- /dev/null +++ b/addons/mrp/report/mrp_report_bom_structure.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + diff --git a/addons/mrp/report/mrp_report_views_main.xml b/addons/mrp/report/mrp_report_views_main.xml index 1faaa04ca69..c78c6de4fa3 100644 --- a/addons/mrp/report/mrp_report_views_main.xml +++ b/addons/mrp/report/mrp_report_views_main.xml @@ -10,6 +10,14 @@ report_type="qweb-pdf" print_report_name="'Production Order - %s' % object.name" /> + 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 00000000000..326275cbe71 --- /dev/null +++ b/addons/mrp/static/src/js/mrp_bom_report.js @@ -0,0 +1,195 @@ +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 MrpBomReport = stock_report_generic.extend({ + events: { + 'click .o_mrp_bom_unfoldable': '_onClickUnfold', + 'click .o_mrp_bom_foldable': '_onClickFold', + 'click .o_mrp_bom_action': '_onClickAction', + }, + get_html: function() { + var self = this; + var args = [ + this.given_context.active_id, + this.given_context.searchQty || 1, + this.given_context.searchVariant, + ]; + return this._rpc({ + model: 'report.mrp.report_bom_structure', + method: 'get_html', + args: args + }) + .then(function (result) { + self.data = result; + }); + }, + set_html: function() { + var self = this; + return this._super().then(function () { + self.$el.html(self.data.lines); + self.renderSearch(); + self.update_cp(); + }); + }, + render_html: function(event, $el, result){ + $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_buttons: this.$searchView + }, + }; + return this.update_control_panel(status); + }, + renderSearch: function () { + this.$buttonPrint = $(QWeb.render('mrp.button')); + this.$buttonPrint.filter('.o_mrp_bom_print').on('click', this._onClickPrint.bind(this)); + this.$buttonPrint.filter('.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; + if (! $(ev.currentTarget).hasClass('o_mrp_bom_print_unfolded')) { + reportname += '&quantity=' + (this.given_context.searchQty || 1) + + '&childs=' + JSON.stringify(childBomIDs) + + '&report_type=' + this.given_context.report_type; + } + 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) { + return this.do_action({ + type: 'ir.actions.act_window', + res_model: $(ev.currentTarget).data('model'), + res_id: $(ev.currentTarget).data('res-id'), + views: [[false, 'form']], + target: 'current' + }); + }, + _reload: function () { + var self = this; + return this.get_html().then(function () { + self.$el.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').toggleClass('o_hidden'); + } + if (this.given_context.report_type === 'bom_cost') { + this.$('.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/less/mrp_bom_report.less b/addons/mrp/static/src/less/mrp_bom_report.less new file mode 100644 index 00000000000..423e6f4f065 --- /dev/null +++ b/addons/mrp/static/src/less/mrp_bom_report.less @@ -0,0 +1,40 @@ +.o_mrp_bom_report_page { + background-color: @odoo-view-background-color; + margin: 16px auto; + padding: 24px 16px; + .o_mrp_bom_action { + cursor: pointer; + } + // 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; + } + } + thead tr th { + padding-left: 10px; + } + 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; + } + } + } + } +} diff --git a/addons/mrp/static/src/xml/mrp.xml b/addons/mrp/static/src/xml/mrp.xml new file mode 100644 index 00000000000..75c2ce15422 --- /dev/null +++ b/addons/mrp/static/src/xml/mrp.xml @@ -0,0 +1,37 @@ + + + + + + + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 535ea3466aa..3ae43fc9515 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -2,6 +2,12 @@ + + Cost & Structure + mrp_bom_report + + + mrp.bom.form mrp.bom @@ -9,6 +15,8 @@
+