commit cf8bb6ced1596c5160ad4bcedd58539912820a7f Author: Suherdy Yacob Date: Tue Mar 3 18:09:01 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f7d18b --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python +*.py[cod] +*$py.class +__pycache__/ + +# Odoo +*.log +*.pot + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e55e09 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# MRP Components Report + +## Overview +This custom Odoo 19 module adds a new "Components Required" report under the **Manufacturing > Reporting** menu. It allows users to view a consolidated list of components needed for Manufacturing Orders on a specific date (or within a selected date range). + +## Key Features +- **Wizard Entry Point**: Allows users to filter required components by a custom date range. +- **Reporting Views**: Provides robust List and Pivot views for analyzing component demand data. +- **Multi-Level Grouping**: Components are pre-grouped by **Work Center** and then by **Parent Product** for clear visibility into production station needs. +- **Excel Export**: Native Odoo list views support immediate export to `.xlsx`. +- **Custom PDF Generation**: Includes a dedicated "Print PDF" action within the wizard for a neatly formatted printable report. + +## Technical Details +This module utilizes a PostgreSQL `VIEW` (`mrp.components.report`) to performantly aggregate data from `mrp_production` (Manufacturing Orders) and `stock_move` (Component requirements). Work Center mapping is derived from standard Odoo routing and workorder relationships, gracefully falling back to the Manufacturing Order's primary Work Center if specific operations are undefined. + +## Author +Kipas System Implementation diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c4e388b --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from . import report +from . import wizard diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..73d24a4 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'MRP Components Report', + 'version': '19.0.1.0.0', + 'category': 'Manufacturing/Manufacturing', + 'summary': 'Report showing needed components for a certain date grouped by work center and parent product', + 'depends': ['mrp'], + 'data': [ + 'security/ir.model.access.csv', + 'report/mrp_components_report_views.xml', + 'wizard/mrp_components_wizard_views.xml', + 'report/mrp_components_qweb_report.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/report/__init__.py b/report/__init__.py new file mode 100644 index 0000000..2072e44 --- /dev/null +++ b/report/__init__.py @@ -0,0 +1,2 @@ +from . import mrp_components_report +from . import mrp_components_pdf_report diff --git a/report/mrp_components_pdf_report.py b/report/mrp_components_pdf_report.py new file mode 100644 index 0000000..37de225 --- /dev/null +++ b/report/mrp_components_pdf_report.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +from odoo import models, api +from odoo.exceptions import UserError + +class ReportMrpComponents(models.AbstractModel): + _name = 'report.mrp_components_report.report_mrp_components_template' + _description = 'MRP Components PDF Report' + + @api.model + def _get_report_values(self, docids, data=None): + if not data or not data.get('form'): + raise UserError("Form content is missing, this report cannot be printed.") + + date_start = data['form'].get('date_start') + date_end = data['form'].get('date_end') + + domain = [ + ('date', '>=', date_start), + ('date', '<=', date_end) + ] + + report_records = self.env['mrp.components.report'].search(domain) + + grouped_data = {} + + for record in report_records: + wc_name = record.workcenter_id.name or 'No Work Center' + prod_name = record.product_id.display_name or 'No Parent Product' + + if wc_name not in grouped_data: + grouped_data[wc_name] = {} + if prod_name not in grouped_data[wc_name]: + grouped_data[wc_name][prod_name] = [] + + grouped_data[wc_name][prod_name].append({ + 'component_name': record.component_id.display_name, + 'qty': record.product_qty, + 'uom_name': record.product_uom_id.name, + 'date': record.date, + 'mo': record.production_id.name, + }) + + # Optional: consolidate same components if needed. + # But showing individual MOs might be better. Let's consolidate if same component in same MO? + # Standard reports usually aggregate quantities. Let's aggregate by component within the product. + + aggregated_data = {} + for wc, products in grouped_data.items(): + aggregated_data[wc] = {} + for prod, components in products.items(): + agg_comps = {} + for comp in components: + key = comp['component_name'] + if key not in agg_comps: + agg_comps[key] = { + 'component_name': key, + 'qty': 0.0, + 'uom_name': comp['uom_name'] + } + agg_comps[key]['qty'] += comp['qty'] + aggregated_data[wc][prod] = list(agg_comps.values()) + + return { + 'doc_ids': data.get('ids'), + 'doc_model': 'mrp.components.wizard', + 'date_start': date_start, + 'date_end': date_end, + 'grouped_data': aggregated_data, + } diff --git a/report/mrp_components_qweb_report.xml b/report/mrp_components_qweb_report.xml new file mode 100644 index 0000000..4a83c25 --- /dev/null +++ b/report/mrp_components_qweb_report.xml @@ -0,0 +1,58 @@ + + + + Components Required Report + mrp.components.wizard + qweb-pdf + mrp_components_report.report_mrp_components_template + mrp_components_report.report_mrp_components_template + 'Components Required - %s to %s' % (object.date_start, object.date_end) + + report + + + + diff --git a/report/mrp_components_report.py b/report/mrp_components_report.py new file mode 100644 index 0000000..1833f5f --- /dev/null +++ b/report/mrp_components_report.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from odoo import tools +from odoo import models, fields + +class MrpComponentsReport(models.Model): + _name = 'mrp.components.report' + _description = 'MRP Components Report' + _auto = False + + id = fields.Id(readonly=True) + date = fields.Date('Date Needed', readonly=True) + production_id = fields.Many2one('mrp.production', 'Manufacturing Order', readonly=True) + product_id = fields.Many2one('product.product', 'Parent Product', readonly=True) + component_id = fields.Many2one('product.product', 'Component', readonly=True) + workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', readonly=True) + product_qty = fields.Float('Quantity Needed', readonly=True) + product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', readonly=True) + state = fields.Selection([ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('progress', 'In Progress'), + ('to_close', 'To Close'), + ('done', 'Done'), + ('cancel', 'Cancelled')], string='MO State', readonly=True) + + def _select(self): + return """ + SELECT + sm.id AS id, + CAST(mp.date_start AS DATE) AS date, + mp.id AS production_id, + mp.product_id AS product_id, + sm.product_id AS component_id, + COALESCE(mw.id, (SELECT workcenter_id FROM mrp_workorder WHERE production_id = mp.id ORDER BY id ASC LIMIT 1)) AS workcenter_id, + sm.product_uom_qty AS product_qty, + sm.product_uom AS product_uom_id, + mp.state AS state + """ + + def _from(self): + return """ + FROM stock_move sm + JOIN mrp_production mp ON sm.raw_material_production_id = mp.id + LEFT JOIN mrp_bom_line mbl ON sm.bom_line_id = mbl.id + LEFT JOIN mrp_routing_workcenter mrw ON sm.operation_id = mrw.id + LEFT JOIN mrp_routing_workcenter mrw_bom ON mbl.operation_id = mrw_bom.id + LEFT JOIN mrp_workorder mwo ON sm.workorder_id = mwo.id + LEFT JOIN mrp_workcenter mw ON COALESCE(mrw.workcenter_id, mwo.workcenter_id, mrw_bom.workcenter_id) = mw.id + """ + + def _where(self): + return """ + WHERE sm.raw_material_production_id IS NOT NULL + AND sm.state != 'cancel' + AND mp.state != 'cancel' + """ + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute("""CREATE or REPLACE VIEW %s as ( + %s + %s + %s + )""" % (self._table, self._select(), self._from(), self._where())) diff --git a/report/mrp_components_report_views.xml b/report/mrp_components_report_views.xml new file mode 100644 index 0000000..e2f847e --- /dev/null +++ b/report/mrp_components_report_views.xml @@ -0,0 +1,51 @@ + + + + mrp.components.report.tree + mrp.components.report + + + + + + + + + + + + + + + + mrp.components.report.pivot + mrp.components.report + + + + + + + + + + + + mrp.components.report.search + mrp.components.report + + + + + + + + + + + + + + + + diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..4dfd94b --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mrp_components_report,mrp.components.report,model_mrp_components_report,mrp.group_mrp_user,1,0,0,0 +access_mrp_components_wizard,mrp.components.wizard,model_mrp_components_wizard,mrp.group_mrp_user,1,1,1,1 diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..bf0e88a --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1 @@ +from . import mrp_components_wizard diff --git a/wizard/mrp_components_wizard.py b/wizard/mrp_components_wizard.py new file mode 100644 index 0000000..c104d31 --- /dev/null +++ b/wizard/mrp_components_wizard.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields + +class MrpComponentsWizard(models.TransientModel): + _name = 'mrp.components.wizard' + _description = 'MRP Components Report Wizard' + + date_start = fields.Date(string='Start Date', required=True, default=fields.Date.context_today) + date_end = fields.Date(string='End Date', required=True, default=fields.Date.context_today) + + def action_view_report(self): + self.ensure_one() + domain = [ + ('date', '>=', self.date_start), + ('date', '<=', self.date_end) + ] + + return { + 'name': 'Components Needed', + 'type': 'ir.actions.act_window', + 'res_model': 'mrp.components.report', + 'view_mode': 'list,pivot', + 'domain': domain, + 'context': { + 'search_default_group_by_workcenter': 1, + 'search_default_group_by_product': 1, + } + } + + def action_print_pdf(self): + self.ensure_one() + data = { + 'ids': self.ids, + 'model': self._name, + 'form': { + 'date_start': self.date_start, + 'date_end': self.date_end, + }, + } + return self.env.ref('mrp_components_report.action_report_mrp_components').report_action(self, data=data) diff --git a/wizard/mrp_components_wizard_views.xml b/wizard/mrp_components_wizard_views.xml new file mode 100644 index 0000000..f5bcf27 --- /dev/null +++ b/wizard/mrp_components_wizard_views.xml @@ -0,0 +1,35 @@ + + + + mrp.components.wizard.form + mrp.components.wizard + +
+ + + + + + +
+
+
+
+
+ + + Components Required + mrp.components.wizard + form + new + + + +