diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index 45238c1add0..55d09e1c1de 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -44,6 +44,7 @@ 'report/mrp_report_bom_structure.xml', 'report/mrp_production_templates.xml', 'report/report_stock_forecasted.xml', + 'report/report_stock_reception.xml', 'report/report_stock_rule.xml', 'report/mrp_zebra_production_templates.xml', ], diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index b316d69ced9..2f3f86dc6d2 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -266,6 +266,9 @@ class MrpProduction(models.Model): show_lot_ids = fields.Boolean('Display the serial number shortcut on the moves', compute='_compute_show_lot_ids') forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue') show_serial_mass_produce = fields.Boolean('Display the serial mass produce wizard action', compute='_compute_show_serial_mass_produce') + show_allocation = fields.Boolean( + compute='_compute_show_allocation', + help='Technical Field used to decide whether the button "Allocation" should be displayed.') @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') def _compute_mrp_production_child_count(self): @@ -542,6 +545,30 @@ class MrpProduction(models.Model): float_compare(order.qty_producing, order.product_qty, precision_rounding=order.product_uom_id.rounding) < 0: order.show_serial_mass_produce = True + @api.depends('state', 'move_finished_ids') + def _compute_show_allocation(self): + self.show_allocation = False + if not self.user_has_groups('mrp.group_mrp_reception_report'): + return + for mo in self: + if not mo.picking_type_id: + return + lines = mo.move_finished_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel') + if lines: + allowed_states = ['confirmed', 'partially_available', 'waiting'] + if mo.state == 'done': + allowed_states += ['assigned'] + wh_location_ids = self.env['stock.location']._search([('id', 'child_of', mo.picking_type_id.warehouse_id.view_location_id.id), ('usage', '!=', 'supplier')]) + if self.env['stock.move'].search([ + ('state', 'in', allowed_states), + ('product_qty', '>', 0), + ('location_id', 'in', wh_location_ids), + ('raw_material_production_id', '!=', mo.id), + ('product_id', 'in', lines.product_id.ids), + '|', ('move_orig_ids', '=', False), + ('move_orig_ids', 'in', lines.ids)], limit=1): + mo.show_allocation = True + _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), @@ -1708,6 +1735,12 @@ class MrpProduction(models.Model): 'res_id': self.id, 'target': 'main', } + if self.user_has_groups('mrp.group_mrp_reception_report') and self.picking_type_id.auto_show_reception_report: + lines = self.move_finished_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel' and m.quantity_done and not m.move_dest_ids) + if lines: + if any(mo.show_allocation for mo in self): + action = self.action_view_reception_report() + return action return True context = self.env.context.copy() context = {k: v for k, v in context.items() if not k.startswith('default_')} @@ -1782,6 +1815,9 @@ class MrpProduction(models.Model): action['context'] = dict(self._context, default_origin=self.name) return action + def action_view_reception_report(self): + return self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_reception_action") + @api.model def get_empty_list_help(self, help): self = self.with_context( diff --git a/addons/mrp/models/res_config_settings.py b/addons/mrp/models/res_config_settings.py index a103fd3ac04..6d1b0a53f7e 100644 --- a/addons/mrp/models/res_config_settings.py +++ b/addons/mrp/models/res_config_settings.py @@ -20,6 +20,7 @@ class ResConfigSettings(models.TransientModel): group_mrp_routings = fields.Boolean("MRP Work Orders", implied_group='mrp.group_mrp_routings') group_unlocked_by_default = fields.Boolean("Unlock Manufacturing Orders", implied_group='mrp.group_unlocked_by_default') + group_mrp_reception_report = fields.Boolean("Allocation Report for Manufacturing Orders", implied_group='mrp.group_mrp_reception_report') @api.onchange('use_manufacturing_lead') def _onchange_use_manufacturing_lead(self): diff --git a/addons/mrp/report/report_stock_reception.py b/addons/mrp/report/report_stock_reception.py index a7cc717bca4..a58ce816375 100644 --- a/addons/mrp/report/report_stock_reception.py +++ b/addons/mrp/report/report_stock_reception.py @@ -8,7 +8,40 @@ from odoo.tools import format_date class ReceptionReport(models.AbstractModel): _inherit = 'report.stock.report_reception' + def _get_docs(self, docids): + if self.env.context.get('default_production_ids'): + return self.env['mrp.production'].search([('id', 'in', self.env.context.get('default_production_ids')), ('state', '!=', 'cancel')]) + return super()._get_docs(docids) + + def _get_doc_model(self): + if self.env.context.get('default_production_ids'): + return 'mrp.production' + return super()._get_doc_model() + + def _get_doc_types(self): + return super()._get_doc_types() + " or manufacturing orders" + + def _get_moves(self, docs): + if self.env.context.get('default_production_ids'): + return docs.move_finished_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel') + return super()._get_moves(docs) + + def _get_extra_domain(self, docs): + if self.env.context.get('default_production_ids'): + return [('raw_material_production_id', 'not in', docs.ids)] + return super()._get_extra_domain(docs) + def _get_formatted_scheduled_date(self, source): if source._name == 'mrp.production': return format_date(self.env, source.date_planned_start) return super()._get_formatted_scheduled_date(source) + + def _action_assign(self, in_move, out_move): + if in_move.production_id: + in_move.production_id.move_dest_ids |= out_move + if not out_move.group_id and out_move._get_source_document() not in [False, out_move.picking_id]: + out_move.group_id = out_move._get_source_document() + + def _action_unassign(self, in_move, out_move): + if in_move.production_id: + in_move.production_id.move_dest_ids -= out_move diff --git a/addons/mrp/report/report_stock_reception.xml b/addons/mrp/report/report_stock_reception.xml new file mode 100644 index 00000000000..9ac7b5381c8 --- /dev/null +++ b/addons/mrp/report/report_stock_reception.xml @@ -0,0 +1,9 @@ + + + + MRP Reception Report + reception_report + report.stock.report_reception + {'default_production_ids': active_ids} + + diff --git a/addons/mrp/security/mrp_security.xml b/addons/mrp/security/mrp_security.xml index e350b76bfdf..5df12caeb51 100644 --- a/addons/mrp/security/mrp_security.xml +++ b/addons/mrp/security/mrp_security.xml @@ -35,6 +35,11 @@ + + Use Reception Report with Manufacturing Orders + + + diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 9f4f8e57815..fb508d73f64 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -126,7 +126,12 @@ +
+
+
+
+ +
+
+
+

Planning

diff --git a/addons/mrp/views/stock_picking_views.xml b/addons/mrp/views/stock_picking_views.xml index b00d5bde610..b1e4ab388f8 100644 --- a/addons/mrp/views/stock_picking_views.xml +++ b/addons/mrp/views/stock_picking_views.xml @@ -137,6 +137,9 @@ + + + diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 5574a30baa0..24941da3527 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -74,7 +74,7 @@ class PickingType(models.Model): reservation_days_before_priority = fields.Integer('Days when starred', help="Maximum number of days before scheduled date that priority picking products should be reserved.") auto_show_reception_report = fields.Boolean( "Show Reception Report at Validation", - help="If this checkbox is ticked, Odoo will automatically show the reception report (if there are moves to allocate to) when validating a picking.") + help="If this checkbox is ticked, Odoo will automatically show the reception report (if there are moves to allocate to) when validating.") count_picking_draft = fields.Integer(compute='_compute_picking_count') count_picking_ready = fields.Integer(compute='_compute_picking_count') diff --git a/addons/stock/report/report_stock_reception.py b/addons/stock/report/report_stock_reception.py index facd8166d71..e5bb901c87b 100644 --- a/addons/stock/report/report_stock_reception.py +++ b/addons/stock/report/report_stock_reception.py @@ -15,16 +15,16 @@ class ReceptionReport(models.AbstractModel): def _get_report_values(self, docids, data=None): ''' This report is flexibly designed to work with both individual and batch pickings. ''' - docids = self.env.context.get('default_picking_ids', docids) - pickings = self.env['stock.picking'].search([('id', 'in', docids), ('picking_type_code', '!=', 'outgoing'), ('state', '!=', 'cancel')]) - picking_states = pickings.mapped('state') + docs = self._get_docs(docids) + doc_states = docs.mapped('state') # unsupported cases - if not pickings: - msg = _("No transfers selected or a delivery order selected") - elif 'done' in picking_states and len(set(picking_states)) > 1: - pickings = False - msg = _("This report cannot be used for done and not done transfers at the same time") - if not pickings: + doc_types = self._get_doc_types() + if not docs: + msg = _("No %s selected or a delivery order selected", doc_types) + elif 'done' in doc_states and len(set(doc_states)) > 1: + docs = False + msg = _("This report cannot be used for done and not done %s at the same time", doc_types) + if not docs: return {'pickings': False, 'reason': msg} # incoming move qtys @@ -33,7 +33,7 @@ class ReceptionReport(models.AbstractModel): product_to_total_assigned = defaultdict(lambda: [0.0, []]) # to support batch pickings we need to track the total already assigned - move_ids = pickings.move_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel') + move_ids = self._get_moves(docs) assigned_moves = move_ids.mapped('move_dest_ids') product_to_assigned_qty = defaultdict(float) for assigned in assigned_moves: @@ -58,11 +58,11 @@ class ReceptionReport(models.AbstractModel): product_to_qty_to_assign[move.product_id].append((quantity_to_assign - qty_already_assigned, move)) # only match for non-mto moves in same warehouse - warehouse = pickings[0].picking_type_id.warehouse_id + warehouse = docs[0].picking_type_id.warehouse_id wh_location_ids = self.env['stock.location']._search([('id', 'child_of', warehouse.view_location_id.id), ('usage', '!=', 'supplier')]) allowed_states = ['confirmed', 'partially_available', 'waiting'] - if 'done' in picking_states: + if 'done' in doc_states: # only done moves are allowed to be assigned to already reserved moves allowed_states += ['assigned'] @@ -72,10 +72,9 @@ class ReceptionReport(models.AbstractModel): ('product_qty', '>', 0), ('location_id', 'in', wh_location_ids), ('move_orig_ids', '=', False), - ('picking_id', 'not in', pickings.ids), ('product_id', 'in', [p.id for p in list(product_to_qty_to_assign.keys()) + list(product_to_qty_draft.keys())]), - ], + ] + self._get_extra_domain(docs), order='reservation_date, priority desc, date, id') products_to_outs = defaultdict(list) @@ -95,7 +94,7 @@ class ReceptionReport(models.AbstractModel): qty_to_reserve = out.product_qty product_uom = out.product_id.uom_id - if 'done' not in picking_states and out.state == 'partially_available': + if 'done' not in doc_states and out.state == 'partially_available': qty_to_reserve -= out.product_uom._compute_quantity(out.reserved_availability, product_uom) moves_in_ids = [] qty_done = 0 @@ -154,10 +153,10 @@ class ReceptionReport(models.AbstractModel): return { 'data': data, 'doc_ids': docids, - 'doc_model': 'stock.picking', + 'doc_model': self._get_doc_model(), 'sources_to_lines': sources_to_lines, 'precision': self.env['decimal.precision'].precision_get('Product Unit of Measure'), - 'pickings': pickings, + 'docs': docs, 'sources_to_formatted_scheduled_date': sources_to_formatted_scheduled_date, } @@ -176,6 +175,22 @@ class ReceptionReport(models.AbstractModel): 'move_ins': move_ins and move_ins.ids or False, } + def _get_docs(self, docids): + docids = self.env.context.get('default_picking_ids', docids) + return self.env['stock.picking'].search([('id', 'in', docids), ('picking_type_code', '!=', 'outgoing'), ('state', '!=', 'cancel')]) + + def _get_doc_model(self): + return 'stock.picking' + + def _get_doc_types(self): + return "transfers" + + def _get_moves(self, docs): + return docs.move_ids.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel') + + def _get_extra_domain(self, docs): + return [('picking_id', 'not in', docs.ids)] + def _get_formatted_scheduled_date(self, source): """ Unfortunately different source record types have different field names for their "Scheduled Date" Therefore an extendable method is needed. @@ -238,6 +253,7 @@ class ReceptionReport(models.AbstractModel): linked_qty = min(in_move.product_qty, qty_to_link) in_move.move_dest_ids |= out + self._action_assign(in_move, out) out.procure_method = 'make_to_order' quantity_remaining -= linked_qty qty_to_link -= linked_qty @@ -263,6 +279,7 @@ class ReceptionReport(models.AbstractModel): if out.id not in in_move.move_dest_ids.ids: continue in_move.move_dest_ids -= out + self._action_unassign(in_move, out) amount_unassigned += min(qty, in_move.product_qty) if float_compare(qty, amount_unassigned, precision_rounding=out.product_id.uom_id.rounding) <= 0: break @@ -296,3 +313,11 @@ class ReceptionReport(models.AbstractModel): new_out._recompute_state() out.procure_method = 'make_to_stock' out._recompute_state() + + def _action_assign(self, in_move, out_move): + """ For extension purposes only """ + return + + def _action_unassign(self, in_move, out_move): + """ For extension purposes only """ + return diff --git a/addons/stock/report/report_stock_reception.xml b/addons/stock/report/report_stock_reception.xml index 7000b459dbe..e9d62a5761e 100644 --- a/addons/stock/report/report_stock_reception.xml +++ b/addons/stock/report/report_stock_reception.xml @@ -44,13 +44,13 @@

- - + + @@ -150,7 +150,7 @@

- No allocation need found for incoming products. + No allocation need found.

diff --git a/addons/stock/tests/test_report.py b/addons/stock/tests/test_report.py index 8ae7a67d287..fde4efc6e8f 100644 --- a/addons/stock/tests/test_report.py +++ b/addons/stock/tests/test_report.py @@ -1384,7 +1384,7 @@ class TestReports(TestReportsCommon): # check that report correctly merges not draft incoming quantities report = self.env['report.stock.report_reception'] report_values = report._get_report_values(docids=[receipt1.id, receipt2.id]) - self.assertEqual(len(report_values['pickings']), 2, "There should be 2 receipts to assign from in this report") + self.assertEqual(len(report_values['docs']), 2, "There should be 2 receipts to assign from in this report") sources_to_lines = report_values['sources_to_lines'] self.assertEqual(len(sources_to_lines), 1, "The report has wrong number of outgoing pickings.") all_lines = list(sources_to_lines.values())[0] @@ -1410,7 +1410,7 @@ class TestReports(TestReportsCommon): # check that report doesn't allow done and non-done moves at same time receipt1.button_validate() reason = report._get_report_values(docids=[receipt1.id, receipt2.id])['reason'] - self.assertEqual(reason, "This report cannot be used for done and not done transfers at the same time", "empty report reason not shown") + self.assertEqual(reason, "This report cannot be used for done and not done %s at the same time" % report._get_doc_types(), "empty report reason not shown") def test_report_reception_3_multiwarehouse(self): """ Check that reception report respects same warehouse for