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