diff options
| author | JF Aubert <ajf@odoo.com> | 2020-08-17 11:22:02 +0000 |
|---|---|---|
| committer | Arnold Moyaux <arm@odoo.com> | 2020-08-18 13:51:25 +0000 |
| commit | 9b53ab050d021c87fd216593c983df960a3b97f7 (patch) | |
| tree | dcca95943f772165e6b79afdcf25686225d65525 | |
| parent | da28fd88151f0360a80e605c141b0614f6b21767 (diff) | |
[IMP] mrp,stock: allow to encode lots in a m2m on the move
Allow to add serial numbers as tags on the move in production order and
stock picking. The purpose is to earn time for database with
only tracking installed.
Also when only create_lots is checked on picking type. New lots
could select existing lot and an onchange trigger a warning if
it's used.
closes odoo/odoo#55883
Task: 2280985
Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
| -rw-r--r-- | addons/mrp/models/mrp_production.py | 17 | ||||
| -rw-r--r-- | addons/mrp/tests/test_order.py | 3 | ||||
| -rw-r--r-- | addons/mrp/views/mrp_production_views.xml | 15 | ||||
| -rw-r--r-- | addons/stock/models/stock_move.py | 62 | ||||
| -rw-r--r-- | addons/stock/models/stock_move_line.py | 21 | ||||
| -rw-r--r-- | addons/stock/tests/test_move2.py | 43 | ||||
| -rw-r--r-- | addons/stock/views/stock_picking_views.xml | 9 | ||||
| -rw-r--r-- | addons/stock_dropshipping/tests/test_lifo_price.py | 1 |
8 files changed, 167 insertions, 4 deletions
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 7f84efbf104..068e1d23a33 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -109,6 +109,7 @@ class MrpProduction(models.Model): 'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", default=_get_default_picking_type, required=True, check_company=True) + use_create_components_lots = fields.Boolean(related='picking_type_id.use_create_components_lots') location_src_id = fields.Many2one( 'stock.location', 'Components Location', default=_get_default_location_src_id, @@ -668,6 +669,22 @@ class MrpProduction(models.Model): def _onchange_producing(self): self._set_qty_producing() + @api.onchange('lot_producing_id') + def _onchange_lot_producing(self): + if self.product_id.tracking == 'serial': + if self.env['stock.move.line'].search_count([ + ('company_id', '=', self.company_id.id), + ('product_id', '=', self.product_id.id), + ('lot_id', '=', self.lot_producing_id.id), + ('state', '!=', 'cancel') + ]): + return { + 'warning': { + 'title': _('Warning'), + 'message': _('Existing Serial number (%s). Please correct the serial numbers encoded.') % self.lot_producing_id.name + } + } + @api.onchange('bom_id') def _onchange_workorder_ids(self): if self.bom_id: diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 3dfa1500625..a7e024f146e 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -976,7 +976,8 @@ class TestMrpOrder(TestMrpCommon): mo2.action_confirm() mo_form = Form(mo2) - mo_form.lot_producing_id = sn + with self.assertLogs(level="WARNING"): + mo_form.lot_producing_id = sn mo2 = mo_form.save() with self.assertRaises(UserError): mo2.button_mark_done() diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index c0a86603c16..400dafccf5d 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -143,6 +143,7 @@ <group> <group> <field name="id" invisible="1"/> + <field name="use_create_components_lots" invisible="1"/> <field name="allowed_product_ids" invisible="1"/> <field name="product_tracking" invisible="1"/> <field name="product_id" context="{'default_type': 'product'}" attrs="{'readonly': [('state', '!=', 'draft')]}"/> @@ -270,6 +271,13 @@ attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('show_details_visible', '=', True)]}"/> <field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/> <field name="show_details_visible" invisible="1"/> + <field name="lot_ids" widget="many2many_tags" + groups="stock.group_production_lot" + attrs="{'invisible': ['|', '|', ('show_details_visible', '=', False), ('has_tracking', '!=', 'serial'), ('parent.state', '=', 'draft')]}" + options="{'create': [('parent.use_create_components_lots', '!=', False)]}" + context="{'default_company_id': company_id, 'default_product_id': product_id}" + domain="[('product_id','=',product_id)]" + /> <button name="action_show_details" type="object" icon="fa-list" context="{'default_product_uom_qty': 0}" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{"warn": true}"/> </tree> </field> @@ -323,6 +331,13 @@ <field name="quantity_done" string="Produced" attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_quantity_done_editable', '=', False)]}"/> <field name="product_uom" groups="uom.group_uom"/> <field name="show_details_visible" invisible="1"/> + <field name="lot_ids" widget="many2many_tags" + groups="stock.group_production_lot" + attrs="{'invisible': ['|', '|', ('show_details_visible', '=', False), ('has_tracking', '!=', 'serial'), ('parent.state', '=', 'draft')]}" + options="{'create': [('parent.use_create_components_lots', '!=', False)]}" + context="{'default_company_id': company_id, 'default_product_id': product_id}" + domain="[('product_id','=',product_id)]" + /> <button name="action_show_details" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{"warn": true}"/> </tree> </field> diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 0fa3fef230a..1070049fdeb 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -176,6 +176,7 @@ class StockMove(models.Model): next_serial_count = fields.Integer('Number of SN') orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Original Reordering Rule', check_company=True) json_forecast = fields.Char('JSON data for the forecast widget', compute='_compute_json_forecast') + lot_ids = fields.Many2many('stock.production.lot', compute='_compute_lot_ids', inverse='_set_lot_ids', string='Serial Numbers', readonly=False) @api.onchange('product_id', 'picking_type_id') def onchange_product(self): @@ -466,6 +467,35 @@ class StockMove(models.Model): move_linked = (move.move_dest_ids | move.move_orig_ids).filtered(lambda m: m.state not in ('done', 'cancel')) move_linked.filtered(lambda m: m.date_deadline != move.date_deadline).date_deadline = move.date_deadline + @api.depends('move_line_ids', 'move_line_ids.lot_id', 'move_line_ids.qty_done') + def _compute_lot_ids(self): + for move in self: + if move.picking_type_id.show_reserved is False: + move.lot_ids = move.move_line_nosuggest_ids.filtered(lambda ml: ml.lot_id and not float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding)).lot_id + else: + move.lot_ids = move.move_line_ids.filtered(lambda ml: ml.lot_id and not float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding)).lot_id + + def _set_lot_ids(self): + for move in self: + move_lines_commands = [] + if move.picking_type_id.show_reserved is False: + mls = move.move_line_nosuggest_ids + else: + mls = move.move_line_ids + mls = mls.filtered(lambda ml: ml.lot_id) + for ml in mls: + if ml.lot_id not in move.lot_ids: + move_lines_commands.append((2, ml.id)) + ls = move.move_line_ids.lot_id + for lot in move.lot_ids: + if lot not in ls: + move_line_vals = self._prepare_move_line_vals(quantity=0) + move_line_vals['lot_id'] = lot.id + move_line_vals['lot_name'] = lot.name + move_line_vals['qty_done'] = 1 + move_lines_commands.append((0, 0, move_line_vals)) + move.write({'move_line_ids': move_lines_commands}) + @api.constrains('product_uom') def _check_uom(self): moves_error = self.filtered(lambda move: move.product_id.uom_id.category_id != move.product_uom.category_id) @@ -830,6 +860,29 @@ class StockMove(models.Model): self.name = product.partner_ref self.product_uom = product.uom_id.id + @api.onchange('lot_ids') + def _onchange_lot_ids(self): + used_lots = self.env['stock.move.line'].search([ + ('company_id', '=', self.company_id.id), + ('product_id', '=', self.product_id.id), + ('lot_id', 'in', self.lot_ids.ids), + ('move_id', '!=', self._origin.id), + ('state', '!=', 'cancel') + ]) + + counter = self.env['stock.move.line'].search_count([ + ('company_id', '=', self.company_id.id), + ('product_id', '=', self.product_id.id), + ('move_id', '=', self._origin.id), + ('lot_id', '=', False), + ('lot_name', '!=', False), + ]) + self.update({'quantity_done': len(self.lot_ids) + counter}) + if used_lots: + return { + 'warning': {'title': _('Warning'), 'message': _('Existing Serial numbers (%s). Please correct the serial numbers encoded.') % ','.join(used_lots.lot_id.mapped('display_name'))} + } + @api.onchange('move_line_ids', 'move_line_nosuggest_ids') def onchange_move_line_ids(self): if not self.picking_type_id.use_create_lots: @@ -857,6 +910,15 @@ class StockMove(models.Model): self.update({'move_line_ids': move_lines_commands}) else: self.update({'move_line_nosuggest_ids': move_lines_commands}) + existing_lots = self.env['stock.production.lot'].search([ + ('company_id', '=', self.company_id.id), + ('product_id', '=', self.product_id.id), + ('name', 'in', split_lines), + ]) + if existing_lots: + return { + 'warning': {'title': _('Warning'), 'message': _('Existing Serial Numbers (%s). Please correct the serial numbers encoded.') % ','.join(existing_lots.mapped('display_name'))} + } break @api.onchange('product_uom') diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 12176eaeac8..d98f5778a6b 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -144,11 +144,18 @@ class StockMoveLine(models.Model): counter = Counter([line.lot_name for line in move_lines_to_check]) if counter.get(self.lot_name) and counter[self.lot_name] > 1: message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.') + elif not self.lot_id: + counter = self.env['stock.production.lot'].search_count([ + ('company_id', '=', self.company_id.id), + ('product_id', '=', self.product_id.id), + ('name', '=', self.lot_name), + ]) + if counter > 0: + message = _('Existing Serial number (%s). Please correct the serial number encoded.') % self.lot_name elif self.lot_id: counter = Counter([line.lot_id.id for line in move_lines_to_check]) if counter.get(self.lot_id.id) and counter[self.lot_id.id] > 1: message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.') - if message: res['warning'] = {'title': _('Warning'), 'message': message} return res @@ -423,8 +430,16 @@ class StockMoveLine(models.Model): # If a picking type is linked, we may have to create a production lot on # the fly before assigning it to the move line if the user checked both # `use_create_lots` and `use_existing_lots`. - if ml.lot_name: - ml_to_create_lot |= ml + if ml.lot_name and not ml.lot_id: + lot = self.env['stock.production.lot'].search([ + ('company_id', '=', ml.company_id.id), + ('product_id', '=', ml.product_id.id), + ('name', '=', ml.lot_name), + ]) + if lot: + ml.lot_id = lot.id + else: + ml_to_create_lot |= ml elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots: # If the user disabled both `use_create_lots` and `use_existing_lots` # checkboxes on the picking type, he's allowed to enter tracked diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index cf3d945428e..46e0f10ca0e 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -2772,3 +2772,46 @@ class TestAutoAssign(TestStockCommon): self.assertEqual(picking_client.state, 'waiting', "MTO moves can't be automatically assigned.") self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 10.0) + + def test_serial_lot_ids(self): + self.stock_location = self.env.ref('stock.stock_location_stock') + self.customer_location = self.env.ref('stock.stock_location_customers') + self.supplier_location = self.env.ref('stock.stock_location_suppliers') + self.uom_unit = self.env.ref('uom.product_uom_unit') + self.product_serial = self.env['product.product'].create({ + 'name': 'PSerial', + 'type': 'product', + 'tracking': 'serial', + 'categ_id': self.env.ref('product.product_category_all').id, + }) + + move = self.env['stock.move'].create({ + 'name': 'TestReceive', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product_serial.id, + 'product_uom': self.uom_unit.id, + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + }) + self.assertEqual(move.state, 'draft') + lot1 = self.env['stock.production.lot'].create({ + 'name': 'serial1', + 'product_id': self.product_serial.id, + 'company_id': self.env.company.id, + }) + lot2 = self.env['stock.production.lot'].create({ + 'name': 'serial2', + 'product_id': self.product_serial.id, + 'company_id': self.env.company.id, + }) + lot3 = self.env['stock.production.lot'].create({ + 'name': 'serial3', + 'product_id': self.product_serial.id, + 'company_id': self.env.company.id, + }) + move.lot_ids = [(4, lot1.id)] + move.lot_ids = [(4, lot2.id)] + move.lot_ids = [(4, lot3.id)] + self.assertEqual(move.quantity_done, 3.0) + move.lot_ids = [(3, lot2.id)] + self.assertEqual(move.quantity_done, 2.0) diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index ddef51d0e5a..83100224c6e 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -233,6 +233,7 @@ <field name="move_line_exist" invisible="1"/> <field name="has_packages" invisible="1"/> <field name="picking_type_entire_packs" invisible="1"/> + <field name="use_create_lots" invisible="1"/> <header> <button name="action_confirm" attrs="{'invisible': [('show_mark_as_todo', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight" groups="base.group_user"/> @@ -380,6 +381,14 @@ <field name="json_forecast" string="Reserved" attrs="{'column_invisible': ['|', ('parent.picking_type_code', '!=', 'outgoing'), ('parent.state','=', 'done')]}" widget="forecast_widget"/> <field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/> <field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/> + <field name="lot_ids" widget="many2many_tags" + groups="stock.group_production_lot" + attrs="{'invisible': ['|', ('show_details_visible', '=', False), ('has_tracking', '!=', 'serial')]}" + optional="hide" + options="{'create': [('parent.use_create_lots', '=', True)]}" + context="{'default_company_id': company_id, 'default_product_id': product_id, 'active_picking_id': parent.id}" + domain="[('product_id','=',product_id)]" + /> <button name="action_show_details" type="object" icon="fa-list" width="0.1" attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/> <button name="action_assign_serial" type="object" diff --git a/addons/stock_dropshipping/tests/test_lifo_price.py b/addons/stock_dropshipping/tests/test_lifo_price.py index 98e8adc2a65..081a085a731 100644 --- a/addons/stock_dropshipping/tests/test_lifo_price.py +++ b/addons/stock_dropshipping/tests/test_lifo_price.py @@ -19,6 +19,7 @@ class TestLifoPrice(ValuationReconciliationTestCommon): 'property_cost_method': 'fifo', }) res_partner_3 = self.env['res.partner'].create({'name': 'My Test Partner'}) + self.company_data['default_warehouse'].out_type_id.show_operations = False # Set a product as using lifo price product_form = Form(self.env['product.product']) |
