[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:
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user