diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mrp/report | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mrp/report')
| -rw-r--r-- | addons/mrp/report/__init__.py | 6 | ||||
| -rw-r--r-- | addons/mrp/report/mrp_production_templates.xml | 204 | ||||
| -rw-r--r-- | addons/mrp/report/mrp_report_bom_structure.py | 261 | ||||
| -rw-r--r-- | addons/mrp/report/mrp_report_bom_structure.xml | 221 | ||||
| -rw-r--r-- | addons/mrp/report/mrp_report_views_main.xml | 44 | ||||
| -rw-r--r-- | addons/mrp/report/mrp_zebra_production_templates.xml | 41 | ||||
| -rw-r--r-- | addons/mrp/report/report_deliveryslip.xml | 81 | ||||
| -rw-r--r-- | addons/mrp/report/report_stock_forecasted.py | 37 | ||||
| -rw-r--r-- | addons/mrp/report/report_stock_forecasted.xml | 19 | ||||
| -rw-r--r-- | addons/mrp/report/report_stock_rule.py | 17 | ||||
| -rw-r--r-- | addons/mrp/report/report_stock_rule.xml | 32 |
11 files changed, 963 insertions, 0 deletions
diff --git a/addons/mrp/report/__init__.py b/addons/mrp/report/__init__.py new file mode 100644 index 00000000..b2dc5ecc --- /dev/null +++ b/addons/mrp/report/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import mrp_report_bom_structure +from . import report_stock_forecasted +from . import report_stock_rule diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml new file mode 100644 index 00000000..650123b8 --- /dev/null +++ b/addons/mrp/report/mrp_production_templates.xml @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> +<template id="report_mrporder"> + <t t-call="web.html_container"> + <t t-foreach="docs" t-as="o"> + <t t-call="web.internal_layout"> + <div class="page"> + <div class="oe_structure"/> + <div class="row"> + <div class="col-7"> + <h2><span t-field="o.name"/></h2> + </div> + <div class="col-5"> + <span class="text-right"> + <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', o.name, 600, 100)" style="width:350px;height:60px"/> + </span> + </div> + </div> + <div class="row mt32 mb32"> + <div class="col-3" t-if="o.origin"> + <strong>Source Document:</strong><br/> + <span t-field="o.origin"/> + </div> + <div class="col-3"> + <strong>Responsible:</strong><br/> + <span t-field="o.user_id"/> + </div> + </div> + + <div class="row mt32 mb32"> + <div class="col-3"> + <strong>Finished Product:</strong><br/> + <span t-field="o.product_id"/> + </div> + <div class="col-3" t-if="o.product_description_variants"> + <strong>Description:</strong><br/> + <span t-field="o.product_description_variants"/> + </div> + <div class="col-3"> + <strong>Quantity to Produce:</strong><br/> + <span t-field="o.product_qty"/> + <span t-field="o.product_uom_id.name" groups="uom.group_uom"/> + </div> + </div> + + <div t-if="o.workorder_ids"> + <h3> + <span t-if="o.state == 'done'">Operations Done</span> + <span t-else="">Operations Planned</span> + </h3> + <table class="table table-sm"> + <tr> + <th><strong>Operation</strong></th> + <th><strong>WorkCenter</strong></th> + <th><strong>No. Of Minutes</strong></th> + </tr> + <tr t-foreach="o.workorder_ids" t-as="line2"> + <td><span t-field="line2.name"/></td> + <td><span t-field="line2.workcenter_id.name"/></td> + <td> + <span t-if="o.state != 'done'" t-field="line2.duration_expected"/> + <span t-if="o.state == 'done'" t-field="line2.duration"/> + </td> + </tr> + </table> + </div> + + <h3 t-if="o.move_raw_ids"> + <span t-if="o.state == 'done'"> + Consumed Products + </span> + <span t-else=""> + Products to Consume + </span> + </h3> + + <table class="table table-sm" t-if="o.move_raw_ids"> + <t t-set="has_product_barcode" t-value="any(m.product_id.barcode for m in o.move_raw_ids)"/> + <thead> + <tr> + <th>Product</th> + <th t-attf-class="{{ 'text-right' if not has_product_barcode else '' }}">Quantity</th> + <th t-if="has_product_barcode" width="15%" class="text-center">Barcode</th> + </tr> + </thead> + <tbody> + <t t-if="o.move_raw_ids"> + <tr t-foreach="o.move_raw_ids" t-as="raw_line"> + <td> + <span t-field="raw_line.product_id"/> + </td> + <td t-attf-class="{{ 'text-right' if not has_product_barcode else '' }}"> + <span t-field="raw_line.product_uom_qty"/> + <span t-field="raw_line.product_uom" groups="uom.group_uom"/> + </td> + <td t-if="has_product_barcode" width="15%" class="text-center"> + <t t-if="raw_line.product_id.barcode"> + <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', raw_line.product_id.barcode, 600, 100)" style="width:100%;height:35px" alt="Barcode"/> + </t> + </td> + </tr> + </t> + </tbody> + </table> + <div class="oe_structure"/> + </div> + </t> + </t> + </t> +</template> + +<template id="label_production_view_pdf"> + <t t-call="web.basic_layout"> + <div class="page"> + <t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/> + <t t-foreach="docs" t-as="production"> + <t t-foreach="production.move_finished_ids" t-as="move"> + <t t-if="production.state == 'done'"> + <t t-set="move_lines" t-value="move.move_line_ids.filtered(lambda x: x.state == 'done' and x.qty_done)"/> + </t> + <t t-else=""> + <t t-set="move_lines" t-value="move.move_line_ids.filtered(lambda x: x.state != 'done' and x.product_qty)"/> + </t> + <t t-foreach="move_lines" t-as="move_line"> + <t t-if="move_line.product_uom_id.category_id == uom_categ_unit"> + <t t-set="qty" t-value="int(move_line.qty_done)"/> + </t> + <t t-else=""> + <t t-set="qty" t-value="1"/> + </t> + <t t-foreach="range(qty)" t-as="item"> + <t t-translation="off"> + <div style="display: inline-table; height: 10rem; width: 32%;"> + <table class="table table-bordered" style="border: 2px solid black;" t-if="production.move_finished_ids"> + <tr> + <th class="table-active text-left" style="height:4rem;"> + <span t-esc="move.product_id.display_name"/> + <br/> + <span>Quantity:</span> + <t t-if="move_line.product_uom_id.category_id == uom_categ_unit"> + <span>1.0</span> + <span t-field="move_line.product_uom_id" groups="uom.group_uom"/> + </t> + <t t-else=""> + <span t-esc="move_line.product_uom_qty" t-if="move_line.state !='done'"/> + <span t-esc="move_line.qty_done" t-if="move_line.state =='done'"/> + <span t-field="move_line.product_uom_id" groups="uom.group_uom"/> + </t> + </th> + </tr> + <t t-if="move_line.product_id.tracking != 'none'"> + <tr> + <td class="text-center align-middle"> + <t t-if="move_line.lot_name or move_line.lot_id"> + <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move_line.lot_name or move_line.lot_id.name, 600, 150)" style="width:100%;height:4rem" alt="Barcode"/> + <span t-esc="move_line.lot_name or move_line.lot_id.name"/> + </t> + <t t-else=""> + <span class="text-muted">No barcode available</span> + </t> + </td> + </tr> + </t> + <t t-if="move_line.product_id.tracking == 'none'"> + <tr> + <td class="text-center align-middle" style="height: 6rem;"> + <t t-if="move_line.product_id.barcode"> + <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move_line.product_id.barcode, 600, 150)" style="width:100%;height:4rem" alt="Barcode"/> + <span t-esc="move_line.product_id.barcode"/> + </t> + <t t-else=""> + <span class="text-muted">No barcode available</span> + </t> + </td> + </tr> + </t> + </table> + </div> + </t> + </t> + </t> + </t> + </t> + </div> + </t> +</template> + +<template id="production_message"> + <t t-if="move.move_id.raw_material_production_id"> + <t t-set="message">Consumed</t> + </t> + <t t-if="move.move_id.production_id"> + <t t-set="message">Produced</t> + </t> + <strong><t t-esc="message"/> quantity has been updated.</strong> +</template> + +<template id="track_production_move_template"> + <div> + <t t-call="mrp.production_message"/> + <t t-call="stock.message_body"/> + </div> +</template> +</odoo> 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 00000000..a726e342 --- /dev/null +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -0,0 +1,261 @@ +# -*- 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' + _description = 'BOM Structure Report' + + @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.get('variant') + candidates = variant and self.env['product.product'].browse(variant) or bom.product_id or bom.product_tmpl_id.product_variant_ids + quantity = float(data.get('quantity', 1)) + for product_variant_id in candidates.ids: + if data and data.get('childs'): + doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, child_bom_ids=json.loads(data.get('childs'))) + else: + doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=quantity, unfolded=True) + doc['report_type'] = 'pdf' + doc['report_structure'] = data and data.get('report_type') or 'all' + docs.append(doc) + if not candidates: + if data and data.get('childs'): + doc = self._get_pdf_line(bom_id, qty=quantity, child_bom_ids=json.loads(data.get('childs'))) + else: + doc = self._get_pdf_line(bom_id, qty=quantity, 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']['has_attachments'] = res['lines']['attachments'] or any(component['attachments'] for component in res['lines']['components']) + 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, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level) + values = { + 'bom_id': bom_id, + 'currency': self.env.company.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 or 1 + 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) + company = bom.company_id or self.env.company + 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) or 0 + # 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 + if product: + price = product.uom_id._compute_price(product.with_company(company).standard_price, bom.product_uom_id) * bom_quantity + attachments = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'), + ('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', product.product_tmpl_id.id)]) + else: + # Use the product template instead of the variant + price = bom.product_tmpl_id.uom_id._compute_price(bom.product_tmpl_id.with_company(company).standard_price, bom.product_uom_id) * bom_quantity + attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)]) + operations = self._get_operation_line(bom, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0) + lines = { + 'bom': bom, + 'bom_qty': bom_quantity, + 'bom_prod_name': product.display_name, + 'currency': company.currency_id, + 'product': product, + 'code': bom and bom.display_name or '', + 'price': price, + 'total': sum([op['total'] for op in operations]), + 'level': level or 0, + 'operations': operations, + 'operations_cost': sum([op['total'] for op in operations]), + 'attachments': attachments, + '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 / (bom.product_qty or 1.0)) * line.product_qty + if line._skip_bom_line(product): + continue + company = bom.company_id or self.env.company + price = line.product_id.uom_id._compute_price(line.product_id.with_company(company).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 + sub_total = self._get_price(line.child_bom_id, factor, line.product_id) + else: + sub_total = price + sub_total = self.env.company.currency_id.round(sub_total) + components.append({ + 'prod_id': line.product_id.id, + 'prod_name': line.product_id.display_name, + 'code': line.child_bom_id and line.child_bom_id.display_name or '', + 'prod_qty': line_quantity, + 'prod_uom': line.product_uom_id.name, + 'prod_cost': company.currency_id.round(price), + 'parent_id': bom.id, + 'line_id': line.id, + 'level': level or 0, + 'total': sub_total, + 'child_bom': line.child_bom_id.id, + 'phantom_bom': line.child_bom_id and line.child_bom_id.type == 'phantom' or False, + 'attachments': self.env['mrp.document'].search(['|', '&', + ('res_model', '=', 'product.product'), ('res_id', '=', line.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_id.product_tmpl_id.id)]), + + }) + total += sub_total + return components, total + + def _get_operation_line(self, bom, qty, level): + operations = [] + total = 0.0 + for operation in bom.operation_ids: + operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP') + duration_expected = operation_cycle * 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': self.env.company.currency_id.round(total), + }) + return operations + + def _get_price(self, bom, factor, product): + price = 0 + if bom.operation_ids: + # routing are defined on a BoM and don't have a concept of quantity. + # It means that the operation time are defined for the quantity on + # the BoM (the user produces a batch of products). E.g the user + # product a batch of 10 units with a 5 minutes operation, the time + # will be the 5 for a quantity between 1-10, then doubled for + # 11-20,... + operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP') + operations = self._get_operation_line(bom, operation_cycle, 0) + price += sum([op['total'] for op in operations]) + + for line in bom.bom_line_ids: + if line._skip_bom_line(product): + continue + if line.child_bom_id: + qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty + sub_price = self._get_price(line.child_bom_id, qty, line.product_id) + price += sub_price + else: + prod_qty = line.product_qty * factor + company = bom.company_id or self.env.company + not_rounded_price = line.product_id.uom_id._compute_price(line.product_id.with_context(force_comany=company.id).standard_price, line.product_uom_id) * prod_qty + price += company.currency_id.round(not_rounded_price) + return price + + def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False): + + 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'], + 'code': bom_line['code'], + 'child_bom': bom_line['child_bom'], + 'prod_id': bom_line['prod_id'] + }) + 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.id, bom_line['prod_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_id = product_id or bom.product_id.id or bom.product_tmpl_id.product_variant_id.id + data = self._get_bom(bom_id=bom_id, product_id=product_id, line_qty=qty) + pdf_lines = get_sub_lines(bom, product_id, 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 00000000..28963924 --- /dev/null +++ b/addons/mrp/report/mrp_report_bom_structure.xml @@ -0,0 +1,221 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + <template id="report_mrp_bom"> + <div class="container o_mrp_bom_report_page"> + <t t-if="data.get('components') or data.get('lines')"> + <div class="row"> + <div class="col-lg-12"> + <h1 style="display:inline;">BoM Structure </h1> + <h1 style="display:inline;" t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost">& Cost</h1> + <h3> + <a href="#" t-if="data['report_type'] == 'html'" t-att-data-res-id="data['product'].id" t-att-data-model="data['product']._name" class="o_mrp_bom_action"> + <t t-esc="data['bom_prod_name']"/> + </a> + <t t-else="" t-esc="data['bom_prod_name']"/> + </h3> + <h6 t-if="data['bom'].code">Reference: <t t-esc="data['bom'].code"/></h6> + </div> + </div> + <t t-set="currency" t-value="data['currency']"/> + <div class="row"> + <div class="col-lg-12"> + <div class="mt16"> + <table width="100%" class="o_mrp_bom_expandable"> + <thead> + <tr> + <th>Product</th> + <th name="th_mrp_bom_h">BoM</th> + <th class="text-right">Quantity</th> + <th class="text-left" groups="uom.group_uom">Unit of Measure</th> + <th t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-right" title="This is the cost defined on the product.">Product Cost</th> + <th t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-right" title="This is the cost based on the BoM of the product. It is computed by summing the costs of the components and operations needed to build the product.">BoM Cost</th> + <th t-if="data['report_type'] == 'html' and data['has_attachments']" class="o_mrp_has_attachments" title="Files attached to the product">Attachments</th> + </tr> + </thead> + <tbody> + <tr> + <td> + <span><a href="#" t-if="data['report_type'] == 'html'" t-att-data-res-id="data['product'].id" t-att-data-model="'product.product'" class="o_mrp_bom_action"><t t-esc="data['bom_prod_name']"/></a><t t-else="" t-esc="data['bom_prod_name']"/></span> + </td> + <td name="td_mrp_bom"> + <div><a href="#" t-if="data['report_type'] == 'html'" t-att-data-res-id="data['bom'].id" t-att-data-model="'mrp.bom'" class="o_mrp_bom_action"><t t-esc="data['code']"/></a><t t-else="" t-esc="data['code']"/></div> + </td> + <td class="text-right"><span><t t-esc="data['bom_qty']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/></span></td> + <td groups="uom.group_uom"><span><t t-esc="data['bom'].product_uom_id.name"/></span></td> + <td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-right"> + <span><t t-esc="data['price']" t-options='{"widget": "monetary", "display_currency": currency}'/></span> + </td> + <td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-right"> + <span><t t-esc="data['total']" t-options='{"widget": "monetary", "display_currency": currency}'/></span> + </td> + <td t-if="data['report_type'] == 'html'"> + <span> + <t t-if="data['attachments']"> + <a href="#" role="button" t-att-data-res-id="data['attachments'].ids" t-att-data-model="'mrp.document'" class="o_mrp_show_attachment_action fa fa-fw o_button_icon fa-files-o"/> + </t> + </span> + </td> + </tr> + <t t-if="data['report_type'] == 'html'" t-call="mrp.report_mrp_bom_line"/> + <t t-if="data['report_type'] == 'pdf'" t-call="mrp.report_mrp_bom_pdf_line"/> + </tbody> + <tfoot> + <tr> + <td></td> + <td name="td_mrp_bom_f"></td> + <td t-if="data['report_structure'] != 'bom_structure'" class="text-right o_mrp_prod_cost"><span><strong>Unit Cost</strong></span></td> + <td groups="uom.group_uom"></td> + <td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-right"> + <span><t t-esc="data['price']/data['bom_qty']" t-options='{"widget": "monetary", "display_currency": currency}'/></span> + </td> + <td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-right"> + <span><t t-esc="data['total']/data['bom_qty']" t-options='{"widget": "monetary", "display_currency": currency}'/></span> + </td> + </tr> + </tfoot> + </table> + </div> + </div> + </div> + </t> + <t t-else=""> + <h1 class="text-center">No data available.</h1> + </t> + </div> + </template> + + <template id="report_mrp_bom_line"> + <t t-set="currency" t-value="data['currency']"/> + <t t-foreach="data['components']" t-as="l"> + <t t-set="space_td" t-value="'margin-left: '+ str(l['level'] * 20) + 'px;'"/> + <tr class="o_mrp_bom_report_line" t-att-data-id="l['child_bom']" t-att-parent_id="l['parent_id']" t-att-data-line="l['line_id']" t-att-data-product_id="l['prod_id']" t-att-data-qty="l['prod_qty']" t-att-data-level="l['level']"> + <td> + <div t-att-style="space_td"> + <t t-if="l['child_bom']"> + <div t-att-data-function="'get_bom'" class="o_mrp_bom_unfoldable fa fa-fw fa-caret-right" style="display:inline-block;" role="img" aria-label="Unfold" title="Unfold"/> + </t> + <div t-att-class="None if l['child_bom'] else 'o_mrp_bom_no_fold'" style="display:inline-block;"> + <a href="#" t-att-data-res-id="l['prod_id']" t-att-data-model="'product.product'" class="o_mrp_bom_action"><t t-esc="l['prod_name']"/></a> + </div> + <t t-if="l['phantom_bom']"> + <div class="fa fa-dropbox" title="This is a BoM of type Kit!" role="img" aria-label="This is a BoM of type Kit!"/> + </t> + </div> + </td> + <td name="td_mrp_bom"> + <div> + <a href="#" t-att-data-res-id="l['child_bom']" t-att-data-model="'mrp.bom'" class="o_mrp_bom_action"><t t-esc="l['code']"/></a> + </div> + </td> + <td class="text-right"><span><t t-esc="l['prod_qty']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/></span></td> + <td groups="uom.group_uom"><span><t t-esc="l['prod_uom']"/></span></td> + <td class="o_mrp_prod_cost text-right"> + <span t-esc="l['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/> + </td> + <td class="o_mrp_bom_cost text-right"> + <span t-esc="l['total']" t-options='{"widget": "monetary", "display_currency": currency}'/> + </td> + <td> + <span> + <t t-if="l['attachments']"> + <a href="#" role="button" t-att-data-res-id="l['attachments'].ids" t-att-data-model="'mrp.document'" class="o_mrp_show_attachment_action fa fa-fw o_button_icon fa-files-o"/> + </t> + </span> + </td> + </tr> + </t> + <t t-if="data['operations']"> + <t t-set="space_td" t-value="'margin-left: '+ str(data['level'] * 20) + 'px;'"/> + <tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-data-id="'operation-' + str(data['bom'].id)" t-att-data-bom-id="data['bom'].id" t-att-parent_id="data['bom'].id" t-att-data-qty="data['bom_qty']" t-att-data-level="data['level']"> + <td name="td_opr"> + <span t-att-style="space_td"/> + <span class="o_mrp_bom_unfoldable fa fa-fw fa-caret-right" t-att-data-function="'get_operations'" role="img" aria-label="Unfold" title="Unfold"/> + Operations + </td> + <td/> + <td class="text-right"> + <span t-esc="data['operations_time']" t-options='{"widget": "float_time"}'/> + </td> + <td groups="uom.group_uom"><span>Minutes</span></td> + <td class="o_mrp_prod_cost"> + </td> + <td class="o_mrp_bom_cost text-right"> + <span t-esc="data['operations_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/> + </td> + <td/> + </tr> + </t> + </template> + + <template id="report_mrp_operation_line"> + <t t-set="currency" t-value="data['currency']"/> + <t t-foreach="data['operations']" t-as="op"> + <t t-set="space_td" t-value="'margin-left: '+ str(op['level'] * 20) + 'px;'"/> + <tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-parent_id="'operation-' + str(data['bom_id'])"> + <td name="td_opr_line"> + <span t-att-style="space_td"/> + <a href="#" t-att-data-res-id="op['operation'].id" t-att-data-model="'mrp.routing.workcenter'" class="o_mrp_bom_action"><t t-esc="op['name']"/></a> + </td> + <td/> + <td class="text-right"> + <span t-esc="op['duration_expected']" t-options='{"widget": "float_time"}'/> + </td> + <td groups="uom.group_uom"><span>Minutes</span></td> + <td class="o_mrp_prod_cost"></td> + <td class="o_mrp_bom_cost text-right"> + <span t-esc="op['total']" t-options='{"widget": "monetary", "display_currency": currency}'/> + </td> + <td/> + </tr> + </t> + </template> + + <template id="report_mrp_bom_pdf"> + <t t-call="web.html_container"> + <t t-call="mrp.report_mrp_bom"/> + </t> + </template> + + <template id="report_mrp_bom_pdf_line"> + <t t-set="currency" t-value="data['currency']"/> + <t t-foreach="data['lines']" t-as="l"> + <t t-set="space_td" t-value="'margin-left: '+ str(l['level'] * 20) + 'px;'"/> + <tr t-if="data['report_structure'] != 'bom_structure' or l['type'] != 'operation'"> + <td> + <div t-att-style="space_td"> + <div><t t-esc="l['name']"/></div> + </div> + </td> + <td name="td_mrp_code"> + <div t-if="l.get('code')" t-esc="l['code']" /> + </td> + <td class="text-right"> + <span> + <t t-if="l['type'] == 'operation'" t-esc="l['quantity']" t-options='{"widget": "float_time"}'/> + <t t-if="l['type'] == 'bom'" t-esc="l['quantity']" t-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> + </span> + </td> + <td groups="uom.group_uom"><span><t t-esc="l['uom']"/></span></td> + <td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_prod_cost text-right"> + <span t-if="'prod_cost' in l" t-esc="l['prod_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/> + </td> + <td t-if="data['report_structure'] != 'bom_structure'" class="o_mrp_bom_cost text-right"> + <span t-esc="l['bom_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/> + </td> + </tr> + </t> + </template> + + <template id="report_bom_structure"> + <t t-set="data_report_landscape" t-value="True"/> + <t t-call="web.basic_layout"> + <t t-call-assets="mrp.assets_common" t-js="False"/> + <t t-foreach="docs" t-as="data"> + <div class="page"> + <t t-call="mrp.report_mrp_bom"/> + </div> + <p style="page-break-before:always;"> </p> + </t> + </t> + </template> +</odoo> diff --git a/addons/mrp/report/mrp_report_views_main.xml b/addons/mrp/report/mrp_report_views_main.xml new file mode 100644 index 00000000..2f74cf7c --- /dev/null +++ b/addons/mrp/report/mrp_report_views_main.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + <record id="action_report_production_order" model="ir.actions.report"> + <field name="name">Production Order</field> + <field name="model">mrp.production</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">mrp.report_mrporder</field> + <field name="report_file">mrp.report.mrp_production_templates</field> + <field name="print_report_name">'Production Order - %s' % object.name</field> + <field name="binding_model_id" ref="model_mrp_production"/> + <field name="binding_type">report</field> + </record> + <record id="action_report_bom_structure" model="ir.actions.report"> + <field name="name">BoM Structure</field> + <field name="model">mrp.bom</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">mrp.report_bom_structure</field> + <field name="report_file">mrp.report_bom_structure</field> + <field name="print_report_name">'Bom Structure - %s' % object.display_name</field> + <field name="binding_model_id" ref="model_mrp_bom"/> + <field name="binding_type">report</field> + </record> + <record id="label_manufacture_template" model="ir.actions.report"> + <field name="name">Finished Product Label (ZPL)</field> + <field name="model">mrp.production</field> + <field name="report_type">qweb-text</field> + <field name="report_name">mrp.label_production_view</field> + <field name="report_file">mrp.label_production_view</field> + <field name="binding_model_id" ref="model_mrp_production"/> + <field name="binding_type">report</field> + </record> + <record id="action_report_finished_product" model="ir.actions.report"> + <field name="name">Finished Product Label (PDF)</field> + <field name="model">mrp.production</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">mrp.label_production_view_pdf</field> + <field name="report_file">mrp.label_production_view_pdf</field> + <field name="print_report_name">'Finished products - %s' % object.name</field> + <field name="binding_model_id" ref="model_mrp_production"/> + <field name="binding_type">report</field> + </record> + </data> +</odoo> diff --git a/addons/mrp/report/mrp_zebra_production_templates.xml b/addons/mrp/report/mrp_zebra_production_templates.xml new file mode 100644 index 00000000..5244d68e --- /dev/null +++ b/addons/mrp/report/mrp_zebra_production_templates.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <template id="label_production_view">
+ <t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
+ <t t-foreach="docs" t-as="production">
+ <t t-foreach="production.move_finished_ids" t-as="move">
+ <t t-foreach="move.move_line_ids" t-as="move_line">
+ <t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
+ <t t-set="qty" t-value="int(move_line.qty_done)"/>
+ </t>
+ <t t-else="">
+ <t t-set="qty" t-value="1"/>
+ </t>
+ <t t-foreach="range(qty)" t-as="item">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="move_line.product_id.display_name"/>^FS
+<t t-if="move_line.product_id.tracking != 'none' and move_line.lot_id">
+^FO100,100
+^A0N,44,33^FDLN/SN: <t t-esc="move_line.lot_id.name"/>^FS
+^FO100,150^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="move_line.lot_id.name"/>^FS
+^XZ
+</t>
+<t t-if="move_line.product_id.tracking == 'none' and move_line.product_id.barcode">
+^FO100,100^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="move_line.product_id.barcode"/>^FS
+^XZ
+</t>
+ </t>
+ </t>
+ </t>
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/mrp/report/report_deliveryslip.xml b/addons/mrp/report/report_deliveryslip.xml new file mode 100644 index 00000000..88370ca6 --- /dev/null +++ b/addons/mrp/report/report_deliveryslip.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="stock_report_delivery_document_inherit_mrp" inherit_id="stock.report_delivery_document"> + <!-- needs to be set before so elif directly follows if later on --> + <xpath expr="//t[@name='has_packages']" position="before"> + <!-- get only the top level kits' (i.e. no subkit) move lines for easier mapping later on + we ignore subkit groupings--> + <!-- note that move.name uses top level kit's product.template.display_name value instead of product.template.name --> + <t t-set="has_kits" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id and l.move_id.bom_line_id.bom_id.product_tmpl_id.display_name == l.move_id.name)"/> + </xpath> + <xpath expr="//t[@name='no_package_section']" position="before"> + <t t-set="has_kits" t-value="move_lines.filtered(lambda l: l.move_id.bom_line_id and l.move_id.bom_line_id.bom_id.product_tmpl_id.display_name == l.move_id.name)"/> + <t t-if="has_kits"> + <!-- print the products not in a package or kit first --> + <t t-set="move_lines" t-value="move_lines.filtered(lambda m: not m.move_id.bom_line_id)"/> + </t> + </xpath> + <xpath expr="//t[@name='no_package_move_lines']" position="inside"> + <t t-call="mrp.stock_report_delivery_kit_sections"/> + </xpath> + <xpath expr="//t[@name='has_packages']" position="after"> + <!-- Additional use case: group by kits when no packages exist and then apply use case 1. (serial/lot numbers used/printed) --> + <t t-elif="has_kits and not has_packages"> + <t t-call="mrp.stock_report_delivery_kit_sections"/> + <t t-call="mrp.stock_report_delivery_no_kit_section"/> + </t> + </xpath> + </template> + + <template id="stock_report_delivery_kit_sections"> + <!-- do another map to get unique top level kits --> + <t t-set="kits" t-value="has_kits.mapped('move_id.bom_line_id.bom_id.product_tmpl_id')"/> + <t t-foreach="kits" t-as="kit"> + <tr t-att-class="'bg-200 font-weight-bold o_line_section'"> + <td colspan="99"> + <span t-esc="kit.display_name"/> + </td> + </tr> + <t t-set="kit_move_lines" t-value="has_kits.filtered(lambda l: l.move_id.name == kit.display_name)"/> + <t t-if="has_serial_number"> + <tr t-foreach="kit_move_lines" t-as="move_line"> + <t t-set="description" t-as="move_line.move_id.description_picking"/> + <t t-if="description == kit.name"> + <t t-set="description" t-value=""/> + </t> + <t t-call="stock.stock_report_delivery_has_serial_move_line"/> + </tr> + </t> + <t t-else=""> + <!-- move line description by default is the product_template.name (kit name), instead of display_name--> + <t t-set="aggregated_lines" t-value="kit_move_lines._get_aggregated_product_quantities(kit_name=kit.name)"/> + <t t-if="aggregated_lines"> + <t t-call="stock.stock_report_delivery_aggregated_move_lines"/> + </t> + </t> + </t> + </template> + + <!-- No kit section is expected to only be called in no packages case --> + <template id="stock_report_delivery_no_kit_section"> + <!-- Do another section for kit-less products if they exist --> + <t t-set="no_kit_move_lines" t-value="o.move_line_ids.filtered(lambda l: not l.move_id.bom_line_id)"/> + <t t-if="no_kit_move_lines"> + <tr t-att-class="'bg-200 font-weight-bold o_line_section'"> + <td colspan="99"> + <span>Products not associated with a kit</span> + </td> + </tr> + <t t-if="has_serial_number"> + <tr t-foreach="no_kit_move_lines" t-as="move_line"> + <t t-call="stock.stock_report_delivery_has_serial_move_line"/> + </tr> + </t> + <t t-else=""> + <t t-set="aggregated_lines" t-value="no_kit_move_lines._get_aggregated_product_quantities()"/> + <t t-if="aggregated_lines"> + <t t-call="stock.stock_report_delivery_aggregated_move_lines"/> + </t> + </t> + </t> + </template> +</odoo> diff --git a/addons/mrp/report/report_stock_forecasted.py b/addons/mrp/report/report_stock_forecasted.py new file mode 100644 index 00000000..0e403f97 --- /dev/null +++ b/addons/mrp/report/report_stock_forecasted.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class ReplenishmentReport(models.AbstractModel): + _inherit = 'report.stock.report_product_product_replenishment' + + def _move_draft_domain(self, product_template_ids, product_variant_ids, wh_location_ids): + in_domain, out_domain = super()._move_draft_domain(product_template_ids, product_variant_ids, wh_location_ids) + in_domain += [('production_id', '=', False)] + out_domain += [('raw_material_production_id', '=', False)] + return in_domain, out_domain + + def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids): + res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids) + res['draft_production_qty'] = {} + domain = self._product_domain(product_template_ids, product_variant_ids) + domain += [('state', '=', 'draft')] + + # Pending incoming quantity. + mo_domain = domain + [('location_dest_id', 'in', wh_location_ids)] + grouped_mo = self.env['mrp.production'].read_group(mo_domain, ['product_qty:sum'], 'product_id') + res['draft_production_qty']['in'] = sum(mo['product_qty'] for mo in grouped_mo) + + # Pending outgoing quantity. + move_domain = domain + [ + ('raw_material_production_id', '!=', False), + ('location_id', 'in', wh_location_ids), + ] + grouped_moves = self.env['stock.move'].read_group(move_domain, ['product_qty:sum'], 'product_id') + res['draft_production_qty']['out'] = sum(move['product_qty'] for move in grouped_moves) + res['qty']['in'] += res['draft_production_qty']['in'] + res['qty']['out'] += res['draft_production_qty']['out'] + + return res diff --git a/addons/mrp/report/report_stock_forecasted.xml b/addons/mrp/report/report_stock_forecasted.xml new file mode 100644 index 00000000..fb074afd --- /dev/null +++ b/addons/mrp/report/report_stock_forecasted.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="mrp_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment"> + <xpath expr="//tr[@name='draft_picking_in']" position="after"> + <tr t-if="docs['draft_production_qty']['in']" name="draft_mo_in"> + <td colspan="2">Production of Draft MO</td> + <td t-esc="docs['draft_production_qty']['in']" class="text-right"/> + <td t-esc="docs['uom']" groups="uom.group_uom"/> + </tr> + </xpath> + <xpath expr="//tr[@name='draft_picking_out']" position="after"> + <tr t-if="docs['draft_production_qty']['out']" name="draft_mo_out"> + <td colspan="2">Component of Draft MO</td> + <td t-esc="-docs['draft_production_qty']['out']" class="text-right"/> + <td t-esc="docs['uom']" groups="uom.group_uom"/> + </tr> + </xpath> + </template> +</odoo> diff --git a/addons/mrp/report/report_stock_rule.py b/addons/mrp/report/report_stock_rule.py new file mode 100644 index 00000000..2f25ca6a --- /dev/null +++ b/addons/mrp/report/report_stock_rule.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class ReportStockRule(models.AbstractModel): + _inherit = 'report.stock.report_stock_rule' + + @api.model + def _get_rule_loc(self, rule, product_id): + """ We override this method to handle manufacture rule which do not have a location_src_id. + """ + res = super(ReportStockRule, self)._get_rule_loc(rule, product_id) + if rule.action == 'manufacture': + res['source'] = product_id.property_stock_production + return res diff --git a/addons/mrp/report/report_stock_rule.xml b/addons/mrp/report/report_stock_rule.xml new file mode 100644 index 00000000..e87c60e9 --- /dev/null +++ b/addons/mrp/report/report_stock_rule.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <template id="mrp_report_stock_rule" inherit_id="stock.report_stock_rule"> + <xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t" position="before"> + <t t-if="rule[0].action == 'manufacture'"> + <t t-if="rule[1] == 'origin'"> + <t t-call="stock.report_stock_rule_left_arrow"/> + </t> + </t> + </xpath> + <xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t[last()]" position="after"> + <t t-if="rule[0].action == 'manufacture'"> + <t t-if="rule[1] == 'destination'"> + <t t-call="stock.report_stock_rule_right_arrow"/> + </t> + </t> + </xpath> + <xpath expr="//div[hasclass('o_report_stock_rule_rule_name')]/span" position="before"> + <t t-if="rule[0].action == 'manufacture'"> + <i class="fa fa-wrench fa-fw" t-attf-style="color: #{color};"/> + </t> + </xpath> + <xpath expr="//div[hasclass('o_report_stock_rule_legend')]" position="inside"> + <div class="o_report_stock_rule_legend_line"> + <div class="o_report_stock_rule_legend_label">Manufacture</div> + <div class="o_report_stock_rule_legend_symbol"> + <div class="fa fa-wrench fa-fw" t-attf-style="color: #{color};"/> + </div> + </div> + </xpath> + </template> +</odoo> |
