[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>
This commit is contained in:
JF Aubert
2020-08-18 13:51:25 +00:00
committed by Arnold Moyaux
parent da28fd8815
commit 9b53ab050d
8 changed files with 167 additions and 4 deletions
+17
View File
@@ -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:
+2 -1
View File
@@ -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()
+15
View File
@@ -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="{&quot;warn&quot;: 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="{&quot;warn&quot;: true}"/>
</tree>
</field>
+62
View File
@@ -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')
+18 -3
View File
@@ -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
+43
View File
@@ -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)
@@ -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"
@@ -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'])