[IMP] mrp,stock: expand reception report to MOs

Allow use of reception report for MOs. Same behavior is expected as with
(non-outgoing) pickings. With exception that procurement group linkage
should be included when assigning.

Original Reception Report Task: 2500844
Follow Up Reception Report Task: 2632884

Task: 2662730
Part-of: odoo/odoo#79627
This commit is contained in:
Tiffany Chang (tic)
2022-01-20 12:30:04 +00:00
parent d0f299e985
commit 420db386d3
13 changed files with 155 additions and 26 deletions
+1
View File
@@ -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',
],
+36
View File
@@ -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(
+1
View File
@@ -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):
@@ -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
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mrp_reception_action" model="ir.actions.client">
<field name="name">MRP Reception Report</field>
<field name="tag">reception_report</field>
<field name="res_model">report.stock.report_reception</field>
<field name="context">{'default_production_ids': active_ids}</field>
</record>
</odoo>
+5
View File
@@ -35,6 +35,11 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_mrp_reception_report" model="res.groups">
<field name="name">Use Reception Report with Manufacturing Orders</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
<data noupdate="1">
<record id="base.default_user" model="res.users">
@@ -126,7 +126,12 @@
<field name="reserve_visible" invisible="1"/>
<field name="consumption" invisible="1"/>
<field name="is_planned" invisible="1"/>
<field name="show_allocation" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button name="action_view_reception_report" string="Allocation" type="object"
class="oe_stat_button" icon="fa-list"
attrs="{'invisible': [('show_allocation', '=', False)]}"
groups="mrp.group_mrp_reception_report"/>
<button class="oe_stat_button" name="action_view_mrp_production_childs" type="object" icon="fa-wrench" attrs="{'invisible': [('mrp_production_child_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="mrp_production_child_count"/></span>
@@ -85,6 +85,17 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="mrp_reception_report">
<div class="o_setting_left_pane">
<field name="group_mrp_reception_report"/>
</div>
<div class="o_setting_right_pane">
<label for="group_mrp_reception_report"/>
<div class="text-muted">
View and allocate manufactured quantities.
</div>
</div>
</div>
</div>
<h2>Planning</h2>
<div class="row mt16 o_settings_container">
+3
View File
@@ -137,6 +137,9 @@
<field name="use_create_components_lots"/>
</group>
</xpath>
<field name="auto_show_reception_report" position="after">
<field name="auto_show_reception_report" attrs="{'invisible': [('code', '!=', 'mrp_operation')]}" groups="mrp.group_mrp_reception_report"/>
</field>
</field>
</record>
</odoo>
+1 -1
View File
@@ -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')
+42 -17
View File
@@ -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
@@ -44,13 +44,13 @@
<div class="o_report_reception justify-content-between">
<div class="o_report_reception_header my-4">
<h3>
<t t-if="pickings">
<t t-foreach="pickings" t-as="receipt">
<t t-if="docs">
<t t-foreach="docs" t-as="doc">
<div>
<a href="#" res-model="stock.picking" view-type="form" t-att-res-id="receipt.id">
<t t-esc="receipt.display_name"/>
<a href="#" t-att-res-model="doc_model" view-type="form" t-att-res-id="doc.id">
<t t-esc="doc.display_name"/>
</a>
<span t-field="receipt.state" t-attf-class="badge badge-pill #{'bg-success-light' if receipt.state == 'done' else 'bg-info-light'}"/>
<span t-field="doc.state" t-attf-class="badge badge-pill #{'bg-success-light' if doc.state == 'done' else 'bg-info-light'}"/>
</div>
</t>
</t>
@@ -150,7 +150,7 @@
</t>
</table>
<p t-else="">
No allocation need found for incoming products.
No allocation need found.
</p></div>
</div>
</template>
+2 -2
View File
@@ -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