[REF] mrp: workorders for enterprise
Removed abstract workorder Removed the integration of expiry wizard in tablet view since it should be moved in enterprise in the tablet view implementation (not possible to set consumed lot on workorders on community anymore) Refactor _set_quantity_done to use ORM command in order to not return a dict with vals to_create/to_write task-2241471
This commit is contained in:
committed by
Simon Lejeune
parent
4060387988
commit
ed7012e8fd
@@ -2,7 +2,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_document
|
||||
from . import mrp_abstract_workorder
|
||||
from . import res_config_settings
|
||||
from . import mrp_bom
|
||||
from . import mrp_routing
|
||||
|
||||
@@ -539,7 +539,6 @@ class MrpProduction(models.Model):
|
||||
|
||||
@api.onchange('product_qty', 'product_uom_id')
|
||||
def _onchange_product_qty(self):
|
||||
product_qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_uom_id)
|
||||
for workorder in self.workorder_ids:
|
||||
if not workorder.product_uom_id:
|
||||
workorder.product_uom_id = self.product_uom_id
|
||||
@@ -601,25 +600,7 @@ class MrpProduction(models.Model):
|
||||
|
||||
@api.onchange('qty_producing', 'lot_producing_id')
|
||||
def _onchange_producing(self):
|
||||
if self.product_id.tracking == 'serial':
|
||||
qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if qty_producing_uom != 1:
|
||||
self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
|
||||
for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
|
||||
if move.state in ('done', 'cancel'):
|
||||
continue
|
||||
# Do not update extra product quantities
|
||||
if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
|
||||
continue
|
||||
if move.has_tracking != 'none' or move.state == 'done':
|
||||
continue
|
||||
vals = move._update_quantity_done(self)
|
||||
if vals.get('to_create'):
|
||||
for res in vals['to_create']:
|
||||
move.move_line_ids.new(res)
|
||||
if vals.get('to_write'):
|
||||
for move_line, res in vals['to_write']:
|
||||
move_line.update(res)
|
||||
self._set_qty_producing()
|
||||
|
||||
@api.onchange('bom_id')
|
||||
def _onchange_workorder_ids(self):
|
||||
@@ -723,7 +704,6 @@ class MrpProduction(models.Model):
|
||||
'product_uom_id': production.product_id.uom_id.id,
|
||||
'operation_id': operation.id,
|
||||
'state': 'pending',
|
||||
'qty_producing': production.product_id.tracking == 'serial' and 1.0 or product_qty,
|
||||
'consumption': production.consumption,
|
||||
}]
|
||||
production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values]
|
||||
@@ -826,6 +806,19 @@ class MrpProduction(models.Model):
|
||||
}
|
||||
return data
|
||||
|
||||
def _set_qty_producing(self):
|
||||
if self.product_id.tracking == 'serial':
|
||||
qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if qty_producing_uom != 1:
|
||||
self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
|
||||
|
||||
for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
|
||||
if move._should_bypass_set_qty_producing():
|
||||
continue
|
||||
new_qty = self.product_uom_id._compute_quantity((self.qty_producing - self.qty_produced) * move.unit_factor, self.product_uom_id, rounding_method='HALF-UP')
|
||||
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
|
||||
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
|
||||
|
||||
def _update_raw_move(self, bom_line, line_data):
|
||||
""" :returns update_move, old_quantity, new_quantity """
|
||||
quantity = line_data['qty']
|
||||
@@ -981,24 +974,7 @@ class MrpProduction(models.Model):
|
||||
if self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids:
|
||||
self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids.lot_id = self.lot_producing_id
|
||||
if self.product_id.tracking == 'serial':
|
||||
# copy/paste from _onchange_producing
|
||||
qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if qty_producing_uom != 1:
|
||||
self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
|
||||
for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
|
||||
if move.state in ('done', 'cancel'):
|
||||
continue
|
||||
if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
|
||||
continue
|
||||
if move.has_tracking != 'none':
|
||||
continue
|
||||
vals = move._update_quantity_done(self)
|
||||
if vals.get('to_create'):
|
||||
for res in vals['to_create']:
|
||||
move.move_line_ids.create(res)
|
||||
if vals.get('to_write'):
|
||||
for move_line, res in vals['to_write']:
|
||||
move_line.write(res)
|
||||
self._set_qty_producing()
|
||||
|
||||
def action_confirm(self):
|
||||
self._check_company()
|
||||
@@ -1265,8 +1241,6 @@ class MrpProduction(models.Model):
|
||||
|
||||
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
moves_to_finish = moves_to_finish._action_done(cancel_backorder=cancel_backorder)
|
||||
order.workorder_ids.mapped('raw_workorder_line_ids').unlink()
|
||||
order.workorder_ids.mapped('finished_workorder_line_ids').unlink()
|
||||
order.action_assign()
|
||||
consume_move_lines = moves_to_do.mapped('move_line_ids')
|
||||
order.move_finished_ids.move_line_ids.consume_line_ids = [(6, 0, consume_move_lines.ids)]
|
||||
@@ -1294,7 +1268,7 @@ class MrpProduction(models.Model):
|
||||
'origin': mo_source.origin
|
||||
}
|
||||
|
||||
def _generate_backorder_productions(self):
|
||||
def _generate_backorder_productions(self, close_mo=True):
|
||||
backorders = self.env['mrp.production']
|
||||
for production in self:
|
||||
if production.backorder_sequence == 0: # Activate backorder naming
|
||||
@@ -1307,17 +1281,26 @@ class MrpProduction(models.Model):
|
||||
wo.qty_producing = 1
|
||||
else:
|
||||
wo.qty_producing = wo.qty_remaining
|
||||
|
||||
production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
|
||||
'raw_material_production_id': backorder_mo.id,
|
||||
})
|
||||
if close_mo:
|
||||
production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
|
||||
'raw_material_production_id': backorder_mo.id,
|
||||
})
|
||||
production.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
|
||||
'production_id': backorder_mo.id,
|
||||
})
|
||||
else:
|
||||
for move in production.move_raw_ids | production.move_finished_ids:
|
||||
new_move = self.env['stock.move'].browse(move._split(move.unit_factor * production.qty_producing))
|
||||
if move.raw_material_production_id:
|
||||
new_move.raw_material_production_id = backorder_mo.id
|
||||
else:
|
||||
new_move.production_id = backorder_mo.id
|
||||
backorders |= backorder_mo
|
||||
|
||||
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
|
||||
|
||||
for wo in backorder_mo.workorder_ids:
|
||||
wo.duration_expected = wo._get_duration_expected()
|
||||
|
||||
wo.duration_expected = wo._get_duration_expected(wo.workcenter_id)
|
||||
backorders.action_confirm()
|
||||
# Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves
|
||||
# So those move lines are duplicated.
|
||||
|
||||
+106
-291
@@ -14,7 +14,7 @@ from odoo.tools import float_compare, float_round, format_datetime
|
||||
class MrpWorkorder(models.Model):
|
||||
_name = 'mrp.workorder'
|
||||
_description = 'Work Order'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'mrp.abstract.workorder']
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def _read_group_workcenter_id(self, workcenters, domain, order):
|
||||
workcenter_ids = self.env.context.get('default_workcenter_id')
|
||||
@@ -25,10 +25,6 @@ class MrpWorkorder(models.Model):
|
||||
name = fields.Char(
|
||||
'Work Order', required=True,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.company,
|
||||
required=True, index=True, readonly=True)
|
||||
workcenter_id = fields.Many2one(
|
||||
'mrp.workcenter', 'Work Center', required=True,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)], 'progress': [('readonly', True)]},
|
||||
@@ -36,6 +32,11 @@ class MrpWorkorder(models.Model):
|
||||
working_state = fields.Selection(
|
||||
string='Workcenter Status', related='workcenter_id.working_state', readonly=False,
|
||||
help='Technical: used in views only')
|
||||
product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True, check_company=True)
|
||||
product_tracking = fields.Selection(related="product_id.tracking")
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True)
|
||||
use_create_components_lots = fields.Boolean(related="production_id.picking_type_id.use_create_components_lots")
|
||||
production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, check_company=True)
|
||||
production_availability = fields.Selection(
|
||||
string='Stock Availability', readonly=True,
|
||||
related='production_id.reservation_state', store=True,
|
||||
@@ -45,6 +46,10 @@ class MrpWorkorder(models.Model):
|
||||
related='production_id.state',
|
||||
help='Technical: used in views only.')
|
||||
qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty')
|
||||
company_id = fields.Many2one(related='production_id.company_id')
|
||||
qty_producing = fields.Float(
|
||||
compute='_compute_qty_producing', inverse='_set_qty_producing',
|
||||
string='Currently Produced Quantity', digits='Product Unit of Measure')
|
||||
qty_remaining = fields.Float('Quantity To Be Produced', compute='_compute_qty_remaining', digits='Product Unit of Measure')
|
||||
qty_produced = fields.Float(
|
||||
'Quantity', default=0.0,
|
||||
@@ -118,8 +123,9 @@ class MrpWorkorder(models.Model):
|
||||
'stock.move.line', 'workorder_id', 'Moves to Track',
|
||||
help="Inventory moves for which you must scan a lot number at this work order")
|
||||
finished_lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Lot/Serial Number', domain="[('id', 'in', allowed_lots_domain)]",
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, check_company=True)
|
||||
'stock.production.lot', string='Lot/Serial Number', compute='_compute_finished_lot_id',
|
||||
inverse='_set_finished_lot_id', domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]",
|
||||
check_company=True)
|
||||
time_ids = fields.One2many(
|
||||
'mrp.workcenter.productivity', 'workorder_id', copy=False)
|
||||
is_user_working = fields.Boolean(
|
||||
@@ -132,14 +138,15 @@ class MrpWorkorder(models.Model):
|
||||
scrap_ids = fields.One2many('stock.scrap', 'workorder_id')
|
||||
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
|
||||
production_date = fields.Datetime('Production Date', related='production_id.date_planned_start', store=True, readonly=False)
|
||||
raw_workorder_line_ids = fields.One2many('mrp.workorder.line',
|
||||
'raw_workorder_id', string='Components')
|
||||
finished_workorder_line_ids = fields.One2many('mrp.workorder.line',
|
||||
'finished_workorder_id', string='By-products')
|
||||
allowed_lots_domain = fields.One2many(comodel_name='stock.production.lot', compute="_compute_allowed_lots_domain")
|
||||
is_finished_lines_editable = fields.Boolean(compute='_compute_is_finished_lines_editable')
|
||||
json_popover = fields.Char('Popover Data JSON', compute='_compute_json_popover')
|
||||
show_json_popover = fields.Boolean('Show Popover?', compute='_compute_json_popover')
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('warning', 'Warning'),
|
||||
('flexible', 'Flexible')],
|
||||
required=True,
|
||||
)
|
||||
|
||||
@api.depends('production_state', 'date_planned_start', 'date_planned_finished')
|
||||
def _compute_json_popover(self):
|
||||
@@ -197,6 +204,25 @@ class MrpWorkorder(models.Model):
|
||||
'replan': color_icon not in [False, 'text-primary']
|
||||
})
|
||||
|
||||
@api.depends('production_id.lot_producing_id')
|
||||
def _compute_finished_lot_id(self):
|
||||
for workorder in self:
|
||||
workorder.finished_lot_id = workorder.production_id.lot_producing_id
|
||||
|
||||
def _set_finished_lot_id(self):
|
||||
for workorder in self:
|
||||
workorder.production_id.lot_producing_id = workorder.finished_lot_id
|
||||
|
||||
@api.depends('production_id.qty_producing')
|
||||
def _compute_qty_producing(self):
|
||||
for workorder in self:
|
||||
workorder.qty_producing = workorder.production_id.qty_producing
|
||||
|
||||
def _set_qty_producing(self):
|
||||
for workorder in self:
|
||||
workorder.production_id.qty_producing = workorder.qty_producing
|
||||
workorder.production_id._set_qty_producing()
|
||||
|
||||
# Both `date_planned_start` and `date_planned_finished` are related fields on `leave_id`. Let's say
|
||||
# we slide a workorder on a gantt view, a single call to write is made with both
|
||||
# fields Changes. As the ORM doesn't batch the write on related fields and instead
|
||||
@@ -224,53 +250,6 @@ class MrpWorkorder(models.Model):
|
||||
workorder.is_finished_lines_editable = True
|
||||
else:
|
||||
workorder.is_finished_lines_editable = False
|
||||
|
||||
@api.depends('production_id.workorder_ids.finished_workorder_line_ids',
|
||||
'production_id.workorder_ids.finished_workorder_line_ids.qty_done',
|
||||
'production_id.workorder_ids.finished_workorder_line_ids.lot_id')
|
||||
def _compute_allowed_lots_domain(self):
|
||||
""" Check if all the finished products has been assigned to a serial
|
||||
number or a lot in other workorders. If yes, restrict the selectable lot
|
||||
to the lot/sn used in other workorders.
|
||||
"""
|
||||
productions = self.mapped('production_id')
|
||||
treated = self.browse()
|
||||
for production in productions:
|
||||
if production.state == 'draft':
|
||||
continue
|
||||
if production.product_id.tracking == 'none':
|
||||
continue
|
||||
|
||||
rounding = production.product_uom_id.rounding
|
||||
finished_workorder_lines = production.workorder_ids.mapped('finished_workorder_line_ids').filtered(lambda wl: wl.product_id == production.product_id)
|
||||
qties_done_per_lot = defaultdict(list)
|
||||
for finished_workorder_line in finished_workorder_lines:
|
||||
# It is possible to have finished workorder lines without a lot (eg using the dummy
|
||||
# test type). Ignore them when computing the allowed lots.
|
||||
if finished_workorder_line.lot_id:
|
||||
qties_done_per_lot[finished_workorder_line.lot_id.id].append(finished_workorder_line.qty_done)
|
||||
|
||||
qty_to_produce = production.product_qty
|
||||
allowed_lot_ids = self.env['stock.production.lot']
|
||||
qty_produced = sum([max(qty_dones) for qty_dones in qties_done_per_lot.values()])
|
||||
if float_compare(qty_produced, qty_to_produce, precision_rounding=rounding) < 0:
|
||||
# If we haven't produced enough, all lots are available
|
||||
allowed_lot_ids = self.env['stock.production.lot'].search([
|
||||
('product_id', '=', production.product_id.id),
|
||||
('company_id', '=', production.company_id.id),
|
||||
])
|
||||
else:
|
||||
# If we produced enough, only the already produced lots are available
|
||||
allowed_lot_ids = self.env['stock.production.lot'].browse(qties_done_per_lot.keys())
|
||||
workorders = production.workorder_ids.filtered(lambda wo: wo.state not in ('done', 'cancel'))
|
||||
for workorder in workorders:
|
||||
if workorder.product_tracking == 'serial':
|
||||
workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.filtered(lambda wl: wl.product_id == production.product_id).mapped('lot_id')
|
||||
else:
|
||||
workorder.allowed_lots_domain = allowed_lot_ids
|
||||
treated |= workorder
|
||||
(self - treated).allowed_lots_domain = False
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for wo in self:
|
||||
@@ -339,18 +318,6 @@ class MrpWorkorder(models.Model):
|
||||
for workorder in self:
|
||||
workorder.scrap_count = count_data.get(workorder.id, 0)
|
||||
|
||||
@api.onchange('finished_lot_id')
|
||||
def _onchange_finished_lot_id(self):
|
||||
"""When the user changes the lot being currently produced, suggest
|
||||
a quantity to produce consistent with the previous workorders. """
|
||||
previous_wo = self.env['mrp.workorder'].search([
|
||||
('next_work_order_id', '=', self.id)
|
||||
])
|
||||
if previous_wo:
|
||||
line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
|
||||
if line:
|
||||
self.qty_producing = line.qty_done
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
@@ -453,196 +420,15 @@ class MrpWorkorder(models.Model):
|
||||
'workorder_id': workorders_by_bom[production.bom_id][-1:].id
|
||||
})
|
||||
|
||||
for workorder in workorders:
|
||||
workorder._apply_update_workorder_lines()
|
||||
|
||||
for workorders in workorders_by_bom.values():
|
||||
if workorders[0].state == 'pending':
|
||||
workorders[0].state = 'ready'
|
||||
for workorder in workorders:
|
||||
workorder._start_nextworkorder()
|
||||
|
||||
def _generate_wo_lines(self):
|
||||
""" Generate workorder line """
|
||||
self.ensure_one()
|
||||
moves = (self.move_raw_ids | self.move_finished_ids).filtered(
|
||||
lambda move: move.state not in ('done', 'cancel')
|
||||
)
|
||||
for move in moves:
|
||||
qty_to_consume = self._prepare_component_quantity(move, self.qty_producing)
|
||||
line_values = self._generate_lines_values(move, qty_to_consume)
|
||||
self.env['mrp.workorder.line'].create(line_values)
|
||||
|
||||
def _apply_update_workorder_lines(self):
|
||||
""" update existing line on the workorder. It could be trigger manually
|
||||
after a modification of qty_producing.
|
||||
"""
|
||||
self.ensure_one()
|
||||
line_values = self._update_workorder_lines()
|
||||
self.env['mrp.workorder.line'].create(line_values['to_create'])
|
||||
if line_values['to_delete']:
|
||||
line_values['to_delete'].unlink()
|
||||
for line, vals in line_values['to_update'].items():
|
||||
line.write(vals)
|
||||
|
||||
def _refresh_wo_lines(self):
|
||||
""" Modify exisiting workorder line in order to match the reservation on
|
||||
stock move line. The strategy is to remove the line that were not
|
||||
processed yet then call _generate_lines_values that recreate workorder
|
||||
line depending the reservation.
|
||||
"""
|
||||
for workorder in self:
|
||||
raw_moves = workorder.move_raw_ids.filtered(
|
||||
lambda move: move.state not in ('done', 'cancel')
|
||||
)
|
||||
wl_to_unlink = self.env['mrp.workorder.line']
|
||||
for move in raw_moves:
|
||||
rounding = move.product_uom.rounding
|
||||
qty_already_consumed = 0.0
|
||||
workorder_lines = workorder.raw_workorder_line_ids.filtered(lambda w: w.move_id == move)
|
||||
for wl in workorder_lines:
|
||||
if not wl.qty_done:
|
||||
wl_to_unlink |= wl
|
||||
continue
|
||||
|
||||
qty_already_consumed += wl.qty_done
|
||||
qty_to_consume = self._prepare_component_quantity(move, workorder.qty_producing)
|
||||
wl_to_unlink.unlink()
|
||||
if float_compare(qty_to_consume, qty_already_consumed, precision_rounding=rounding) > 0:
|
||||
line_values = workorder._generate_lines_values(move, qty_to_consume - qty_already_consumed)
|
||||
self.env['mrp.workorder.line'].create(line_values)
|
||||
|
||||
def _defaults_from_finished_workorder_line(self, reference_lot_lines):
|
||||
for r_line in reference_lot_lines:
|
||||
# see which lot we could suggest and its related qty_producing
|
||||
if not r_line.lot_id:
|
||||
continue
|
||||
candidates = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == r_line.lot_id)
|
||||
rounding = self.product_uom_id.rounding
|
||||
if not candidates:
|
||||
self.write({
|
||||
'finished_lot_id': r_line.lot_id.id,
|
||||
'qty_producing': r_line.qty_done,
|
||||
})
|
||||
return True
|
||||
elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0:
|
||||
self.write({
|
||||
'finished_lot_id': r_line.lot_id.id,
|
||||
'qty_producing': r_line.qty_done - candidates.qty_done,
|
||||
})
|
||||
return True
|
||||
return False
|
||||
|
||||
def record_production(self):
|
||||
if not self:
|
||||
return True
|
||||
|
||||
self.ensure_one()
|
||||
self._check_sn_uniqueness()
|
||||
self._check_company()
|
||||
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
|
||||
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
|
||||
if self.production_id.product_id.tracking != 'none' and not self.finished_lot_id and self.move_raw_ids:
|
||||
raise UserError(_('You should provide a lot for the final product'))
|
||||
if 'check_ids' not in self:
|
||||
for line in self.raw_workorder_line_ids | self.finished_workorder_line_ids:
|
||||
line._check_line_sn_uniqueness()
|
||||
# If last work order, then post lots used
|
||||
if not self.next_work_order_id:
|
||||
self._update_finished_move()
|
||||
|
||||
# Transfer quantities from temporary to final move line or make them final
|
||||
self._update_moves()
|
||||
|
||||
# Transfer lot (if present) and quantity produced to a finished workorder line
|
||||
if self.product_tracking != 'none':
|
||||
self._create_or_update_finished_line()
|
||||
|
||||
# Update workorder quantity produced
|
||||
self.qty_produced += self.qty_producing
|
||||
|
||||
# Suggest a finished lot on the next workorder
|
||||
if self.next_work_order_id and self.product_tracking != 'none' and (not self.next_work_order_id.finished_lot_id or self.next_work_order_id.finished_lot_id == self.finished_lot_id):
|
||||
self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids)
|
||||
# As we may have changed the quantity to produce on the next workorder,
|
||||
# make sure to update its wokorder lines
|
||||
self.next_work_order_id._apply_update_workorder_lines()
|
||||
|
||||
# One a piece is produced, you can launch the next work order
|
||||
self._start_nextworkorder()
|
||||
|
||||
# Test if the production is done
|
||||
rounding = self.production_id.product_uom_id.rounding
|
||||
if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0:
|
||||
previous_wo = self.env['mrp.workorder']
|
||||
if self.product_tracking != 'none':
|
||||
previous_wo = self.env['mrp.workorder'].search([
|
||||
('next_work_order_id', '=', self.id)
|
||||
])
|
||||
candidate_found_in_previous_wo = False
|
||||
if previous_wo:
|
||||
candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids)
|
||||
if not candidate_found_in_previous_wo:
|
||||
# self is the first workorder
|
||||
self.qty_producing = self.qty_remaining
|
||||
self.finished_lot_id = False
|
||||
if self.product_tracking == 'serial':
|
||||
self.qty_producing = 1
|
||||
|
||||
self._apply_update_workorder_lines()
|
||||
else:
|
||||
self.qty_producing = 0
|
||||
self.button_finish()
|
||||
return True
|
||||
|
||||
def _get_byproduct_move_to_update(self):
|
||||
return self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
|
||||
|
||||
def _create_or_update_finished_line(self):
|
||||
"""
|
||||
1. Check that the final lot and the quantity producing is valid regarding
|
||||
other workorders of this production
|
||||
2. Save final lot and quantity producing to suggest on next workorder
|
||||
"""
|
||||
self.ensure_one()
|
||||
final_lot_quantity = self.qty_production
|
||||
rounding = self.product_uom_id.rounding
|
||||
# Get the max quantity possible for current lot in other workorders
|
||||
for workorder in (self.production_id.workorder_ids - self):
|
||||
# We add the remaining quantity to the produced quantity for the
|
||||
# current lot. For 5 finished products: if in the first wo it
|
||||
# creates 4 lot A and 1 lot B and in the second it create 3 lot A
|
||||
# and it remains 2 units to product, it could produce 5 lot A.
|
||||
# In this case we select 4 since it would conflict with the first
|
||||
# workorder otherwise.
|
||||
line = workorder.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.finished_lot_id)
|
||||
line_without_lot = workorder.finished_workorder_line_ids.filtered(lambda line: line.product_id == workorder.product_id and not line.lot_id)
|
||||
quantity_remaining = workorder.qty_remaining + line_without_lot.qty_done
|
||||
quantity = line.qty_done + quantity_remaining
|
||||
if line and float_compare(quantity, final_lot_quantity, precision_rounding=rounding) <= 0:
|
||||
final_lot_quantity = quantity
|
||||
elif float_compare(quantity_remaining, final_lot_quantity, precision_rounding=rounding) < 0:
|
||||
final_lot_quantity = quantity_remaining
|
||||
|
||||
# final lot line for this lot on this workorder.
|
||||
current_lot_lines = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.finished_lot_id)
|
||||
|
||||
# this lot has already been produced
|
||||
if float_compare(final_lot_quantity, current_lot_lines.qty_done + self.qty_producing, precision_rounding=rounding) < 0:
|
||||
raise UserError(_('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one') %
|
||||
(final_lot_quantity, self.product_id.uom_id.name, self.finished_lot_id.name, current_lot_lines.qty_done + self.qty_producing))
|
||||
|
||||
# Update workorder line that regiter final lot created
|
||||
if not current_lot_lines:
|
||||
current_lot_lines = self.env['mrp.workorder.line'].create({
|
||||
'finished_workorder_id': self.id,
|
||||
'product_id': self.product_id.id,
|
||||
'lot_id': self.finished_lot_id.id,
|
||||
'qty_done': self.qty_producing,
|
||||
})
|
||||
else:
|
||||
current_lot_lines.qty_done += self.qty_producing
|
||||
|
||||
def _start_nextworkorder(self):
|
||||
rounding = self.product_id.uom_id.rounding
|
||||
if self.next_work_order_id.state == 'pending' and (
|
||||
@@ -910,46 +696,75 @@ class MrpWorkorder(models.Model):
|
||||
res[wo1].append(wo2)
|
||||
return res
|
||||
|
||||
|
||||
class MrpWorkorderLine(models.Model):
|
||||
_name = 'mrp.workorder.line'
|
||||
_inherit = ["mrp.abstract.workorder.line"]
|
||||
_description = "Workorder move line"
|
||||
|
||||
raw_workorder_id = fields.Many2one('mrp.workorder', 'Component for Workorder',
|
||||
ondelete='cascade')
|
||||
finished_workorder_id = fields.Many2one('mrp.workorder', 'Finished Product for Workorder',
|
||||
ondelete='cascade')
|
||||
|
||||
@api.onchange('qty_to_consume')
|
||||
def _onchange_qty_to_consume(self):
|
||||
# Update qty_done for products added in ready state
|
||||
wo = self.raw_workorder_id or self.finished_workorder_id
|
||||
if wo.state == 'ready':
|
||||
self.qty_done = self.qty_to_consume
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
for line in res:
|
||||
wo = line.raw_workorder_id
|
||||
if wo and\
|
||||
wo.consumption == 'strict' and\
|
||||
wo.state == 'progress' and\
|
||||
line.product_id.id not in wo.production_id.bom_id.bom_line_ids.product_id.ids:
|
||||
raise UserError(_('You cannot consume additional component as the consumption defined on the Bill of Material is set to "strict"'))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_raw_workorder_inverse_name(self):
|
||||
return 'raw_workorder_id'
|
||||
def _prepare_component_quantity(self, move, qty_producing):
|
||||
""" helper that computes quantity to consume (or to create in case of byproduct)
|
||||
depending on the quantity producing and the move's unit factor"""
|
||||
if move.product_id.tracking == 'serial':
|
||||
uom = move.product_id.uom_id
|
||||
else:
|
||||
uom = move.product_uom
|
||||
return move.product_uom._compute_quantity(
|
||||
qty_producing * move.unit_factor,
|
||||
uom,
|
||||
round=False
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_finished_workoder_inverse_name(self):
|
||||
return 'finished_workorder_id'
|
||||
def _update_finished_move(self):
|
||||
""" Update the finished move & move lines in order to set the finished
|
||||
product lot on it as well as the produced quantity. This method get the
|
||||
information either from the last workorder or from the Produce wizard."""
|
||||
production_move = self.production_id.move_finished_ids.filtered(
|
||||
lambda move: move.product_id == self.product_id and
|
||||
move.state not in ('done', 'cancel')
|
||||
)
|
||||
if production_move and production_move.product_id.tracking != 'none':
|
||||
if not self.finished_lot_id:
|
||||
raise UserError(_('You need to provide a lot for the finished product.'))
|
||||
move_line = production_move.move_line_ids.filtered(
|
||||
lambda line: line.lot_id.id == self.finished_lot_id.id
|
||||
)
|
||||
if move_line:
|
||||
if self.product_id.tracking == 'serial':
|
||||
raise UserError(_('You cannot produce the same serial number twice.'))
|
||||
move_line.product_uom_qty += self.qty_producing
|
||||
move_line.qty_done += self.qty_producing
|
||||
else:
|
||||
location_dest_id = production_move.location_dest_id._get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id
|
||||
move_line.create({
|
||||
'move_id': production_move.id,
|
||||
'product_id': production_move.product_id.id,
|
||||
'lot_id': self.finished_lot_id.id,
|
||||
'product_uom_qty': self.qty_producing,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'qty_done': self.qty_producing,
|
||||
'location_id': production_move.location_id.id,
|
||||
'location_dest_id': location_dest_id,
|
||||
})
|
||||
else:
|
||||
rounding = production_move.product_uom.rounding
|
||||
production_move._set_quantity_done(
|
||||
float_round(self.qty_producing, precision_rounding=rounding)
|
||||
)
|
||||
|
||||
def _get_final_lots(self):
|
||||
return (self.raw_workorder_id or self.finished_workorder_id).finished_lot_id
|
||||
def _strict_consumption_check(self):
|
||||
if self.consumption == 'strict':
|
||||
for move in self.move_raw_ids:
|
||||
qty_done = 0.0
|
||||
for line in move.move_line_ids:
|
||||
qty_done += line.product_uom_id._compute_quantity(line.qty_done, move.product_uom)
|
||||
rounding = move.product_uom_id.rounding
|
||||
if float_compare(qty_done, move.product_uom_qty, precision_rounding=rounding) != 0:
|
||||
raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % move.product_id.name)
|
||||
|
||||
def _get_production(self):
|
||||
return (self.raw_workorder_id or self.finished_workorder_id).production_id
|
||||
def _check_sn_uniqueness(self):
|
||||
""" Alert the user if the serial number as already been produced """
|
||||
if self.product_tracking == 'serial' and self.finished_lot_id:
|
||||
sml = self.env['stock.move.line'].search_count([
|
||||
('lot_id', '=', self.finished_lot_id.id),
|
||||
('location_id.usage', '=', 'production'),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
])
|
||||
if sml:
|
||||
raise UserError(_('This serial number for product %s has already been produced') % self.product_id.name)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_round, float_repr, float_is_zero
|
||||
from odoo.tools import float_compare, float_round, float_is_zero
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
@@ -188,12 +188,6 @@ class StockMove(models.Model):
|
||||
if self.raw_material_production_id and self.has_tracking == 'none':
|
||||
mo = self.raw_material_production_id
|
||||
vals = self._update_quantity_done(mo)
|
||||
if vals.get('to_create'):
|
||||
for res in vals['to_create']:
|
||||
self.move_line_ids.new(res)
|
||||
if vals.get('to_write'):
|
||||
for move_line, res in vals['to_write']:
|
||||
move_line.update(res)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
@@ -277,6 +271,11 @@ class StockMove(models.Model):
|
||||
production._action_cancel()
|
||||
return res
|
||||
|
||||
def _prepare_move_split_vals(self, qty):
|
||||
defaults = super()._prepare_move_split_vals(qty)
|
||||
defaults['workorder_id'] = False
|
||||
return defaults
|
||||
|
||||
def _prepare_phantom_move_values(self, bom_line, product_qty, quantity_done):
|
||||
return {
|
||||
'picking_id': self.picking_id.id if self.picking_id else False,
|
||||
@@ -298,10 +297,10 @@ class StockMove(models.Model):
|
||||
return vals
|
||||
|
||||
def _get_upstream_documents_and_responsibles(self, visited):
|
||||
if self.production_id and self.production_id.state not in ('done', 'cancel'):
|
||||
return [(self.production_id, self.production_id.user_id, visited)]
|
||||
else:
|
||||
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
|
||||
if self.production_id and self.production_id.state not in ('done', 'cancel'):
|
||||
return [(self.production_id, self.production_id.user_id, visited)]
|
||||
else:
|
||||
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
|
||||
|
||||
def _delay_alert_get_documents(self):
|
||||
res = super(StockMove, self)._delay_alert_get_documents()
|
||||
@@ -312,6 +311,16 @@ class StockMove(models.Model):
|
||||
res = super(StockMove, self)._should_be_assigned()
|
||||
return bool(res and not (self.production_id or self.raw_material_production_id))
|
||||
|
||||
def _should_bypass_set_qty_producing(self):
|
||||
if self.state in ('done', 'cancel'):
|
||||
return True
|
||||
# Do not update extra product quantities
|
||||
if float_is_zero(self.product_uom_qty, precision_rounding=self.product_uom.rounding):
|
||||
return True
|
||||
if self.has_tracking != 'none' or self.state == 'done':
|
||||
return True
|
||||
return False
|
||||
|
||||
def _should_bypass_reservation(self):
|
||||
res = super(StockMove, self)._should_bypass_reservation()
|
||||
return bool(res and not self.production_id)
|
||||
@@ -390,7 +399,7 @@ class StockMove(models.Model):
|
||||
new_qty = mo.product_uom_id._compute_quantity((mo.qty_producing - mo.qty_produced) * self.unit_factor, mo.product_uom_id, rounding_method='HALF-UP')
|
||||
if not self.is_quantity_done_editable:
|
||||
self.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
|
||||
ml_values = self._set_quantity_done_prepare_vals(new_qty)
|
||||
self.move_line_ids = self._set_quantity_done_prepare_vals(new_qty)
|
||||
else:
|
||||
self.quantity_done = new_qty
|
||||
return ml_values
|
||||
|
||||
@@ -26,8 +26,6 @@ access_product_supplierinfo_user,product.supplierinfo user,product.model_product
|
||||
access_res_partner,res.partner,base.model_res_partner,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_workorder_mrp_user,mrp.workorder.user,model_mrp_workorder,mrp.group_mrp_user,1,1,1,1
|
||||
access_mrp_workorder_mrp_manager,mrp.workorder,model_mrp_workorder,mrp.group_mrp_manager,1,1,1,1
|
||||
access_mrp_workorder_line_mrp_user,mrp.workorder.user,model_mrp_workorder_line,mrp.group_mrp_user,1,1,1,1
|
||||
access_mrp_workorder_line_mrp_manager,mrp.workorder,model_mrp_workorder_line,mrp.group_mrp_manager,1,1,1,1
|
||||
access_resource_calendar_leaves_user,mrp.resource.calendar.leaves.user,resource.model_resource_calendar_leaves,mrp.group_mrp_user,1,1,1,1
|
||||
access_resource_calendar_leaves_manager,mrp.resource.calendar.leaves.manager,resource.model_resource_calendar_leaves,mrp.group_mrp_manager,1,0,0,0
|
||||
access_resource_calendar_attendance_mrp_user,mrp.resource.calendar.attendance.mrp.user,resource.model_resource_calendar_attendance,mrp.group_mrp_user,1,1,1,1
|
||||
|
||||
|
@@ -6,7 +6,6 @@ from . import test_cancel_mo
|
||||
from . import test_order
|
||||
from . import test_stock
|
||||
from . import test_warehouse_multistep_manufacturing
|
||||
from . import test_workorder_operation
|
||||
from . import test_procurement
|
||||
from . import test_unbuild
|
||||
from . import test_oee
|
||||
|
||||
@@ -327,41 +327,3 @@ class TestMrpWorkorderBackorder(SavepointCase):
|
||||
})
|
||||
cls.bom_finished1.bom_line_ids[0].operation_id = cls.bom_finished1.operation_ids[0].id
|
||||
cls.bom_finished1.bom_line_ids[1].operation_id = cls.bom_finished1.operation_ids[1].id
|
||||
|
||||
def test_1(self):
|
||||
"""Operations are set on `self.bom_kit1` but none on `self.bom_finished1`."""
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.finished1
|
||||
mo_form.bom_id = self.bom_finished1
|
||||
mo_form.product_qty = 2.0
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
|
||||
self.assertEqual(len(mo.workorder_ids), 2)
|
||||
|
||||
workorder1 = mo.workorder_ids[0]
|
||||
workorder2 = mo.workorder_ids[1]
|
||||
|
||||
self.assertEqual(workorder1.raw_workorder_line_ids.product_id, self.compfinished1)
|
||||
self.assertEqual(workorder1.raw_workorder_line_ids.qty_to_consume, 2)
|
||||
|
||||
workorder1.button_start()
|
||||
workorder1.button_finish()
|
||||
|
||||
self.assertEqual(workorder2.raw_workorder_line_ids.product_id, self.compfinished2)
|
||||
self.assertEqual(workorder2.raw_workorder_line_ids.qty_to_consume, 2)
|
||||
|
||||
workorder2.button_start()
|
||||
workorder2.button_finish()
|
||||
|
||||
action = mo.with_context(debug=True).button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
mo_backorder.button_plan()
|
||||
self.assertEqual(len(mo_backorder.workorder_ids), 2)
|
||||
|
||||
self.assertEqual(workorder1.state, 'done')
|
||||
self.assertEqual(workorder2.state, 'done')
|
||||
|
||||
@@ -76,73 +76,6 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
|
||||
"The other move finished is cancelled like its MO.")
|
||||
|
||||
def test_cancel_mo_with_routing(self):
|
||||
""" Cancel a Manufacturing Order with routing (so generate a Work Order)
|
||||
and produce some quantities. When cancelled, the MO must be marked as
|
||||
done and the WO must be cancelled.
|
||||
"""
|
||||
# Create MO
|
||||
product_to_build = self.env['product.product'].create({
|
||||
'name': 'Young Tom',
|
||||
'type': 'product',
|
||||
})
|
||||
product_to_use_1 = self.env['product.product'].create({
|
||||
'name': 'Botox',
|
||||
'type': 'product',
|
||||
})
|
||||
product_to_use_2 = self.env['product.product'].create({
|
||||
'name': 'Old Tom',
|
||||
'type': 'product',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_to_build.id,
|
||||
'product_tmpl_id': product_to_build.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'strict',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': 4}),
|
||||
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': 1})
|
||||
],
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
]
|
||||
})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_to_build
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = 5.0
|
||||
manufacturing_order = mo_form.save()
|
||||
manufacturing_order.action_confirm()
|
||||
|
||||
manufacturing_order.button_plan()
|
||||
workorder = manufacturing_order.workorder_ids
|
||||
# Produce some quantity
|
||||
workorder.button_start()
|
||||
workorder.qty_producing = 2
|
||||
workorder._apply_update_workorder_lines()
|
||||
workorder.record_production()
|
||||
# Post Inventory
|
||||
manufacturing_order._post_inventory()
|
||||
# Cancel it
|
||||
manufacturing_order.action_cancel()
|
||||
# Check MO is done, WO is cancelled and its SML are done or cancelled
|
||||
self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.")
|
||||
self.assertEqual(workorder.state, 'cancel', "WO should be cancelled.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done',
|
||||
"Due to 'post_inventory', some move raw must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done',
|
||||
"Due to 'post_inventory', some move raw must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel',
|
||||
"The other move raw are cancelled like their MO.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel',
|
||||
"The other move raw are cancelled like their MO.")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done',
|
||||
"Due to 'post_inventory', a move finished must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
|
||||
"The other move finished is cancelled like its MO.")
|
||||
|
||||
def test_unlink_mo(self):
|
||||
""" Try to unlink a Manufacturing Order, and check it's possible or not
|
||||
depending of the MO state (must be in cancel state to be unlinked, but
|
||||
|
||||
@@ -39,7 +39,7 @@ class TestProcurement(TestMrpCommon):
|
||||
production_product_6.action_assign()
|
||||
|
||||
# check production state is Confirmed
|
||||
self.assertEqual(production_product_6.state, 'confirmed')
|
||||
self.assertEqual(production_product_6.state, 'to_close')
|
||||
|
||||
# Check procurement for product 4 created or not.
|
||||
# Check it created a purchase order
|
||||
|
||||
@@ -158,47 +158,6 @@ class TestWarehouse(common.TestMrpCommon):
|
||||
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
|
||||
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
|
||||
|
||||
def test_putaway_after_manufacturing_1(self):
|
||||
""" This test checks a manufactured product without tracking will go to
|
||||
location defined in putaway strategy.
|
||||
"""
|
||||
mo_laptop = self.new_mo_laptop()
|
||||
|
||||
mo_laptop.button_plan()
|
||||
workorder = mo_laptop.workorder_ids[0]
|
||||
|
||||
workorder.button_start()
|
||||
workorder.record_production()
|
||||
mo_laptop.button_mark_done()
|
||||
|
||||
# We check if the laptop go in the depot and not in the stock
|
||||
move = mo_laptop.move_finished_ids
|
||||
location_dest = move.move_line_ids.location_dest_id
|
||||
self.assertEqual(location_dest.id, self.depot_location.id)
|
||||
self.assertNotEqual(location_dest.id, self.stock_location.id)
|
||||
|
||||
def test_putaway_after_manufacturing_2(self):
|
||||
""" This test checks a tracked manufactured product will go to location
|
||||
defined in putaway strategy.
|
||||
"""
|
||||
self.laptop.tracking = 'serial'
|
||||
mo_laptop = self.new_mo_laptop()
|
||||
|
||||
mo_laptop.button_plan()
|
||||
workorder = mo_laptop.workorder_ids[0]
|
||||
|
||||
workorder.button_start()
|
||||
serial = self.env['stock.production.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
|
||||
workorder.finished_lot_id = serial
|
||||
workorder.record_production()
|
||||
mo_laptop.button_mark_done()
|
||||
|
||||
# We check if the laptop go in the depot and not in the stock
|
||||
move = mo_laptop.move_finished_ids
|
||||
location_dest = move.move_line_ids.location_dest_id
|
||||
self.assertEqual(location_dest.id, self.depot_location.id)
|
||||
self.assertNotEqual(location_dest.id, self.stock_location.id)
|
||||
|
||||
def test_putaway_after_manufacturing_3(self):
|
||||
""" This test checks a tracked manufactured product will go to location
|
||||
defined in putaway strategy when the production is recorded with
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -286,43 +286,7 @@
|
||||
</field>
|
||||
</page>
|
||||
<page string="Operations" name="operations" groups="mrp.group_mrp_routings">
|
||||
<field name="workorder_ids" attrs="{'readonly': [('state', 'in', ['cancel', 'done'])]}" context="{'default_production_id': id, 'default_qty_producing': product_qty, 'default_product_uom_id': product_uom_id, 'default_consumption': consumption, 'default_company_id': company_id}">
|
||||
<tree editable="bottom">
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_produced" invisible="1"/>
|
||||
<field name="is_user_working" invisible="1"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="product_uom_id" invisible="1" readonly="0"/>
|
||||
<field name="qty_producing" invisible="1"/>
|
||||
<field name="time_ids" invisible="1"/>
|
||||
<field name="working_state" invisible="1"/>
|
||||
<field name="operation_id" invisible="1" domain="['|', ('bom_id', '=', parent.bom_id), ('bom_id', '=', False)]" context="{'default_workcenter_id': workcenter_id, 'default_company_id': company_id}"/>
|
||||
<field name="name" string="Operation"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="date_planned_start" optional="show"/>
|
||||
<field name="date_planned_finished" optional="hide"/>
|
||||
<field name="date_start" optional="hide" readonly="1"/>
|
||||
<field name="date_finished" optional="hide" readonly="1"/>
|
||||
<field name="duration_expected" widget="float_time" sum="Total excepted duration"/>
|
||||
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."
|
||||
attrs="{'invisible': ['|', ('parent.state','=', 'draft'), ('time_ids', '=', [])]}"/>
|
||||
<button name="action_open_wizard" type="object" icon="fa-external-link" width="0" class="oe_edit_only"
|
||||
context="{'default_workcenter_id': workcenter_id}"/>
|
||||
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/>
|
||||
<button name="button_start" type="object" string="Start" width="0" class="btn-success"
|
||||
attrs="{'invisible': ['|', '|', '|', ('parent.state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
|
||||
<button name="button_pending" type="object" string="Pause" width="0" class="btn-warning"
|
||||
attrs="{'invisible': ['|', '|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_finish" type="object" string="Done" width="0" class="btn-success"
|
||||
attrs="{'invisible': ['|', '|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="%(mrp.act_mrp_block_workcenter_wo)d" type="action" string="Block" context="{'default_workcenter_id': workcenter_id}" width="0" class="btn-danger"
|
||||
attrs="{'invisible': ['|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked')]}"/>
|
||||
<button name="button_unblock" type="object" string="Unblock" context="{'default_workcenter_id': workcenter_id}" width="0" class="btn-danger"
|
||||
attrs="{'invisible': ['|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '!=', 'blocked')]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<field name="workorder_ids" attrs="{'readonly': [('state', 'in', ['cancel', 'done'])]}" context="{'tree_view_ref': 'mrp.mrp_production_workorder_tree_editable_view', 'default_production_id': id, 'default_product_uom_id': product_uom_id, 'default_consumption': consumption, 'default_company_id': company_id}"/> </page>
|
||||
<page string="Miscellaneous" name="miscellaneous" groups="stock.group_stock_multi_locations">
|
||||
<group>
|
||||
<group>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
<field name="is_user_working" invisible="1"/>
|
||||
<field name="working_state" invisible="1"/>
|
||||
<field name="production_state" invisible="1"/>
|
||||
<field name="allowed_lots_domain" invisible="1"/>
|
||||
<field name="is_finished_lines_editable" invisible="1"/>
|
||||
<header>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending,ready,progress,done"/>
|
||||
@@ -107,6 +106,7 @@
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="production_id" invisible="1"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<field name="finished_lot_id" invisible="1"/>
|
||||
<field name="qty_producing" invisible="1"/>
|
||||
<group attrs="{'invisible': [('date_planned_start', '=', False)]}">
|
||||
|
||||
@@ -50,11 +50,11 @@ class ChangeProductionQty(models.TransientModel):
|
||||
format_qty = '%.{precision}f'.format(precision=precision)
|
||||
raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced))
|
||||
old_production_qty = production.product_qty
|
||||
production.write({'product_qty': wizard.product_qty})
|
||||
new_production_qty = wizard.product_qty
|
||||
done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id)
|
||||
qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id)
|
||||
|
||||
factor = (production.product_qty - qty_produced) / (old_production_qty - qty_produced)
|
||||
factor = (new_production_qty - qty_produced) / (old_production_qty - qty_produced)
|
||||
update_info = production._update_raw_moves(factor)
|
||||
documents = {}
|
||||
for move, old_qty, new_qty in update_info:
|
||||
@@ -67,9 +67,10 @@ class ChangeProductionQty(models.TransientModel):
|
||||
else:
|
||||
documents[key] = [value]
|
||||
production._log_manufacture_exception(documents)
|
||||
finished_moves_modification = self._update_finished_moves(production, production.product_qty - qty_produced, old_production_qty - qty_produced)
|
||||
finished_moves_modification = self._update_finished_moves(production, new_production_qty - qty_produced, old_production_qty - qty_produced)
|
||||
if finished_moves_modification:
|
||||
production._log_downside_manufactured_quantity(finished_moves_modification)
|
||||
production.write({'product_qty': new_production_qty})
|
||||
|
||||
for wo in production.workorder_ids:
|
||||
operation = wo.operation_id
|
||||
@@ -80,9 +81,9 @@ class ChangeProductionQty(models.TransientModel):
|
||||
else:
|
||||
quantity = quantity if (quantity > 0) else 0
|
||||
if float_is_zero(quantity, precision_digits=precision):
|
||||
wo.finished_lot_id = False
|
||||
wo._workorder_line_ids().unlink()
|
||||
wo.qty_producing = quantity
|
||||
wo.check_ids.unlink()
|
||||
else:
|
||||
wo.qty_producing = quantity
|
||||
if wo.qty_produced < wo.qty_production and wo.state == 'done':
|
||||
wo.state = 'progress'
|
||||
if wo.qty_produced == wo.qty_production and wo.state == 'progress':
|
||||
|
||||
@@ -1,16 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mrp.tests.test_workorder_operation import TestWorkOrderProcessCommon
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestMrpAccount(TestWorkOrderProcessCommon):
|
||||
class TestMrpAccount(TestMrpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpAccount, cls).setUpClass()
|
||||
cls.source_location_id = cls.stock_location_14.id
|
||||
cls.warehouse = cls.env.ref('stock.warehouse0')
|
||||
# setting up alternative workcenters
|
||||
cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter bis',
|
||||
'capacity': 3,
|
||||
'time_start': 9,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter ter',
|
||||
'capacity': 1,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 85,
|
||||
})
|
||||
cls.product_4.uom_id = cls.uom_unit
|
||||
cls.planning_bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||||
]})
|
||||
cls.dining_table = cls.env['product.product'].create({
|
||||
'name': 'Table (MTO)',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.product_table_sheet = cls.env['product.product'].create({
|
||||
'name': 'Table Top',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.product_table_leg = cls.env['product.product'].create({
|
||||
'name': 'Table Leg',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
cls.product_bolt = cls.env['product.product'].create({
|
||||
'name': 'Bolt',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.product_screw = cls.env['product.product'].create({
|
||||
'name': 'Screw',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
cls.mrp_workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.mrp_bom_desk = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 3,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
|
||||
],
|
||||
})
|
||||
cls.mrp_bom_desk.write({
|
||||
'bom_line_ids': [
|
||||
(0, 0, {
|
||||
'product_id': cls.product_table_sheet.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 1,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_table_leg.id,
|
||||
'product_qty': 4,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 2,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_bolt.id,
|
||||
'product_qty': 4,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 3,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_screw.id,
|
||||
'product_qty': 10,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 4,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
]
|
||||
})
|
||||
cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Drill Station 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.categ_standard = cls.env['product.category'].create({
|
||||
'name': 'STANDARD',
|
||||
'property_cost_method': 'standard'
|
||||
@@ -66,10 +170,11 @@ class TestMrpAccount(TestWorkOrderProcessCommon):
|
||||
})
|
||||
inventory.action_validate
|
||||
bom = self.mrp_bom_desk.copy()
|
||||
bom.operation_ids = False
|
||||
production_table_form = Form(self.env['mrp.production'])
|
||||
production_table_form.product_id = self.dining_table
|
||||
production_table_form.bom_id = bom
|
||||
production_table_form.product_qty = 5.0
|
||||
production_table_form.product_qty = 1
|
||||
production_table = production_table_form.save()
|
||||
|
||||
production_table.extra_cost = 20
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_production
|
||||
from . import mrp_workorder
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class MrpWorkorder(models.Model):
|
||||
_inherit = 'mrp.workorder'
|
||||
|
||||
def record_production(self):
|
||||
confirm_expired_lots = self._check_expired_lots()
|
||||
if confirm_expired_lots:
|
||||
return confirm_expired_lots
|
||||
return super().record_production()
|
||||
|
||||
def _check_expired_lots(self):
|
||||
# We use the 'skip_expired' context key to avoid to make the check when
|
||||
# user already confirmed the wizard about using expired lots.
|
||||
if self.env.context.get('skip_expired'):
|
||||
return False
|
||||
expired_lot_ids = self.raw_workorder_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids
|
||||
if expired_lot_ids:
|
||||
return {
|
||||
'name': _('Confirmation'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'expiry.picking.confirmation',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': self._get_expired_context(expired_lot_ids),
|
||||
}
|
||||
|
||||
def _get_expired_context(self, expired_lot_ids):
|
||||
context = dict(self.env.context)
|
||||
context.update({
|
||||
'default_lot_ids': [(6, 0, expired_lot_ids)],
|
||||
'default_workorder_id': self.id,
|
||||
})
|
||||
return context
|
||||
@@ -113,71 +113,3 @@ class TestStockProductionLot(TestStockCommon):
|
||||
self.assertNotEqual(res, None)
|
||||
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
|
||||
|
||||
def test_03_workorder_without_expired_lot(self):
|
||||
""" Checks user doesn't get a confirmation wizard when they makes a
|
||||
workorder without expired components. """
|
||||
# Set a routing on the BOM.
|
||||
self.bom_apple_pie.write({
|
||||
'operation_ids': [(0, 0, {
|
||||
'name': 'Bake in the oven',
|
||||
'workcenter_id': self.workcenter.id,
|
||||
'time_cycle': 15,
|
||||
'sequence': 1,
|
||||
})],
|
||||
})
|
||||
# Creates the MO, starts it and plans the Work Order.
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_apple_pie
|
||||
mo_form.bom_id = self.bom_apple_pie
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
|
||||
wo = mo.workorder_ids[0]
|
||||
wo.button_start()
|
||||
# Set a non-expired lot.
|
||||
wo.raw_workorder_line_ids.write({
|
||||
'qty_done': 3,
|
||||
'lot_id': self.lot_good_apple,
|
||||
})
|
||||
|
||||
res = wo.record_production()
|
||||
# Try to record the production using non-expired lot must not return a wizard.
|
||||
self.assertEqual(res, True)
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_04_workorder_with_expired_lot(self):
|
||||
""" Checks user doesn't get a confirmation wizard when they makes a
|
||||
workorder without expired components. """
|
||||
# Set a routing on the BOM.
|
||||
self.bom_apple_pie.write({
|
||||
'operation_ids': [(0, 0, {
|
||||
'name': 'Bake in the oven',
|
||||
'workcenter_id': self.workcenter.id,
|
||||
'time_cycle': 15,
|
||||
'sequence': 1,
|
||||
})],
|
||||
})
|
||||
# Creates the MO, starts it and plans the Work Order.
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_apple_pie
|
||||
mo_form.bom_id = self.bom_apple_pie
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
|
||||
wo = mo.workorder_ids[0]
|
||||
wo.button_start()
|
||||
# Set an expired lot.
|
||||
wo.raw_workorder_line_ids.write({
|
||||
'qty_done': 3,
|
||||
'lot_id': self.lot_expired_apple,
|
||||
})
|
||||
|
||||
res = wo.record_production()
|
||||
# Try to record the production using expired lot must return a
|
||||
# confirmation wizard.
|
||||
self.assertNotEqual(res, None)
|
||||
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
|
||||
|
||||
@@ -73,13 +73,7 @@ class StockPicking(models.Model):
|
||||
(production.qty_producing - production.qty_produced) * move.unit_factor, production.product_uom_id,
|
||||
rounding_method='HALF-UP')
|
||||
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
|
||||
vals = move._set_quantity_done_prepare_vals(new_qty)
|
||||
if vals['to_create']:
|
||||
for res in vals['to_create']:
|
||||
move.move_line_ids.create(res)
|
||||
if vals['to_write']:
|
||||
for move_line, res in vals['to_write']:
|
||||
move_line.write(res)
|
||||
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
|
||||
productions |= production
|
||||
for subcontracted_production in productions:
|
||||
if subcontracted_production.state == 'progress':
|
||||
|
||||
@@ -1552,7 +1552,7 @@ class StockMove(models.Model):
|
||||
return [(self.picking_id, self.product_id.responsible_id, visited)]
|
||||
|
||||
def _set_quantity_done_prepare_vals(self, qty):
|
||||
res = {'to_write': [], 'to_create': []}
|
||||
res = []
|
||||
for ml in self.move_line_ids:
|
||||
ml_qty = ml.product_uom_qty - ml.qty_done
|
||||
if float_compare(ml_qty, 0, precision_rounding=ml.product_uom_id.rounding) <= 0:
|
||||
@@ -1569,7 +1569,7 @@ class StockMove(models.Model):
|
||||
# Assign qty_done and explicitly round to make sure there is no inconsistency between
|
||||
# ml.qty_done and qty.
|
||||
taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding)
|
||||
res['to_write'].append((ml, {'qty_done': ml.qty_done + taken_qty}))
|
||||
res.append((1, ml.id, {'qty_done': ml.qty_done + taken_qty}))
|
||||
if ml.product_uom_id != self.product_uom:
|
||||
taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)
|
||||
qty -= taken_qty
|
||||
@@ -1580,14 +1580,14 @@ class StockMove(models.Model):
|
||||
if self.product_id.tracking != 'serial':
|
||||
vals = self._prepare_move_line_vals(quantity=0)
|
||||
vals['qty_done'] = qty
|
||||
res['to_create'].append(vals)
|
||||
res.append((0, 0, vals))
|
||||
else:
|
||||
uom_qty = self.product_uom._compute_quantity(qty, self.product_id.uom_id)
|
||||
for i in range(0, int(uom_qty)):
|
||||
vals = self._prepare_move_line_vals(quantity=0)
|
||||
vals['qty_done'] = 1
|
||||
vals['product_uom_id'] = self.product_id.uom_id.id
|
||||
res['to_create'].append(vals)
|
||||
res.append((0, 0, vals))
|
||||
return res
|
||||
|
||||
def _set_quantity_done(self, qty):
|
||||
@@ -1597,13 +1597,7 @@ class StockMove(models.Model):
|
||||
looking for trouble...).
|
||||
@param qty: quantity in the UoM of move.product_uom
|
||||
"""
|
||||
vals = self._set_quantity_done_prepare_vals(qty)
|
||||
if vals['to_create']:
|
||||
for res in vals['to_create']:
|
||||
self.env['stock.move.line'].create(res)
|
||||
if vals['to_write']:
|
||||
for move_line, vals in vals['to_write']:
|
||||
move_line.write(vals)
|
||||
self.move_line_ids = self._set_quantity_done_prepare_vals(qty)
|
||||
|
||||
def _adjust_procure_method(self):
|
||||
""" This method will try to apply the procure method MTO on some moves if
|
||||
|
||||
Reference in New Issue
Block a user