[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:
Arnold Moyaux
2020-06-15 16:59:41 +02:00
committed by Simon Lejeune
parent 4060387988
commit ed7012e8fd
20 changed files with 283 additions and 2459 deletions
-1
View File
@@ -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
+31 -48
View File
@@ -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
View File
@@ -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)
+21 -12
View File
@@ -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
-2
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
26 access_res_partner res.partner base.model_res_partner mrp.group_mrp_user 1 0 0 0
27 access_mrp_workorder_mrp_user mrp.workorder.user model_mrp_workorder mrp.group_mrp_user 1 1 1 1
28 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
29 access_resource_calendar_leaves_user mrp.resource.calendar.leaves.user resource.model_resource_calendar_leaves mrp.group_mrp_user 1 1 1 1
30 access_resource_calendar_leaves_manager mrp.resource.calendar.leaves.manager resource.model_resource_calendar_leaves mrp.group_mrp_manager 1 0 0 0
31 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
-1
View File
@@ -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
-38
View File
@@ -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')
-67
View File
@@ -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
+1 -1
View File
@@ -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
-41
View File
@@ -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
+1 -37
View File
@@ -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>
+1 -1
View File
@@ -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)]}">
+7 -6
View File
@@ -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':
+108 -3
View File
@@ -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
+1 -1
View File
@@ -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':
+5 -11
View File
@@ -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