From 9201f92a4f29a53a014b462469f27b32dca8fc5a Mon Sep 17 00:00:00 2001 From: Hiral Bhavsar Date: Thu, 22 Mar 2018 18:06:53 +0100 Subject: [PATCH] [IMP] mrp: add new bom and cost report Add a new report in order to display: - bom structure and cost - bom structure - bom cost The user is allowed to print multiple bom reports at once. Print the report dynamically depending what have been unfolded. In html the rendering is lazy, when unfolding the client make a request to the server in order to get the html that will be append to the DOM. For the pdf the kwargs unfolded could be used in order to render everything at once. With the help of: Dhaval Suthar Jigar Patel task-36741 --- addons/mrp/__manifest__.py | 2 + addons/mrp/report/__init__.py | 1 + addons/mrp/report/mrp_report_bom_structure.py | 220 ++++++++++++++++++ .../mrp/report/mrp_report_bom_structure.xml | 191 +++++++++++++++ addons/mrp/report/mrp_report_views_main.xml | 8 + addons/mrp/static/src/js/mrp_bom_report.js | 195 ++++++++++++++++ .../mrp/static/src/less/mrp_bom_report.less | 40 ++++ addons/mrp/static/src/xml/mrp.xml | 37 +++ addons/mrp/views/mrp_bom_views.xml | 8 + addons/mrp/views/mrp_templates.xml | 7 + 10 files changed, 709 insertions(+) create mode 100644 addons/mrp/report/mrp_report_bom_structure.py create mode 100644 addons/mrp/report/mrp_report_bom_structure.xml create mode 100644 addons/mrp/static/src/js/mrp_bom_report.js create mode 100644 addons/mrp/static/src/less/mrp_bom_report.less create mode 100644 addons/mrp/static/src/xml/mrp.xml 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 @@
+