[REF] mrp: no more produce wizard

Use a view similar as the pickings one.

task-2241471
This commit is contained in:
William Henrotin
2020-06-15 16:59:41 +02:00
committed by Simon Lejeune
parent 1b6421285c
commit 0e2765f5fd
48 changed files with 1163 additions and 1621 deletions
-1
View File
@@ -15,7 +15,6 @@
'security/mrp_security.xml',
'security/ir.model.access.csv',
'data/mrp_data.xml',
'wizard/mrp_product_produce_views.xml',
'wizard/change_production_qty_views.xml',
'wizard/mrp_workcenter_block_view.xml',
'wizard/stock_warn_insufficient_qty_views.xml',
+8 -19
View File
@@ -684,29 +684,18 @@
<value eval="[ref('mrp.mrp_production_laptop_cust')]"/>
</function>
<function model="mrp.production" name="write">
<value eval="[ref('mrp.mrp_production_laptop_cust')]"/>
<value eval="{'qty_producing': 5, 'lot_producing_id': ref('mrp.lot_product_27_0')}"/>
</function>
<function model="mrp.production" name="action_assign">
<value eval="[ref('mrp.mrp_production_laptop_cust')]"/>
</function>
<function model="mrp.product.produce" name="create">
<value model="mrp.product.produce" eval="dict(
obj().with_context(active_id=ref('mrp.mrp_production_laptop_cust')).default_get(list(obj().fields_get())),
**{
'qty_producing': obj().env.ref('mrp.mrp_production_laptop_cust').product_qty,
'finished_lot_id': ref('mrp.lot_product_27_0'),
}
)"/>
</function>
<function model="mrp.product.produce.line" name="create">
<value model="mrp.product.produce.line" eval="[dict(item,
**{
'raw_product_produce_id': obj().env['mrp.product.produce'].search([('production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)]).id
}) for item in obj().env['mrp.product.produce'].search([('production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)])._update_workorder_lines()['to_create']]"/>
</function>
<function model="mrp.product.produce" name="do_produce">
<value model="mrp.product.produce" search="[('production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)]"/>
<function model="stock.move" name="write">
<value model="stock.move" eval="obj().env['stock.move'].search([('raw_material_production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)]).ids"/>
<value eval="{'quantity_done': 5}"/>
</function>
<function model="mrp.production" name="post_inventory">
+3 -7
View File
@@ -186,7 +186,7 @@ class MrpAbstractWorkorder(models.AbstractModel):
if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0:
break
# move line already 'used' in workorder (from its lot for instance)
if move_line.lot_produced_ids or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0:
if float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0:
continue
# search wo line on which the lot is not fully consumed or other reserved lot
linked_wo_line = self._workorder_line_ids().filtered(
@@ -371,9 +371,9 @@ class MrpAbstractWorkorderLine(models.AbstractModel):
""" update a move line to save the workorder line data"""
self.ensure_one()
if self.lot_id:
move_lines = self.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_ids)
move_lines = self.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_id)
else:
move_lines = self.move_id.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_ids)
move_lines = self.move_id.move_line_ids.filtered(lambda ml: not ml.lot_id)
# Sanity check: if the product is a serial number and `lot` is already present in the other
# consumed move lines, raise.
@@ -396,14 +396,12 @@ class MrpAbstractWorkorderLine(models.AbstractModel):
if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0:
ml.write({
'qty_done': new_quantity_done,
'lot_produced_ids': self._get_produced_lots(),
})
else:
new_qty_reserved = ml.product_uom_qty - new_quantity_done
default = {
'product_uom_qty': new_quantity_done,
'qty_done': new_quantity_done,
'lot_produced_ids': self._get_produced_lots(),
}
ml.copy(default=default)
ml.with_context(bypass_reservation_update=True).write({
@@ -436,7 +434,6 @@ class MrpAbstractWorkorderLine(models.AbstractModel):
'product_uom_qty': 0,
'product_uom_id': self.product_uom_id.id,
'qty_done': min(quantity, self.qty_done),
'lot_produced_ids': self._get_produced_lots(),
}
if self.lot_id:
vals.update({'lot_id': self.lot_id.id})
@@ -456,7 +453,6 @@ class MrpAbstractWorkorderLine(models.AbstractModel):
'product_uom_qty': 0,
'product_uom_id': self.product_uom_id.id,
'qty_done': self.qty_done,
'lot_produced_ids': self._get_produced_lots(),
}
if self.lot_id:
vals.update({'lot_id': self.lot_id.id})
+1 -1
View File
@@ -66,7 +66,7 @@ class MrpBom(models.Model):
('strict', 'Strict'),
('flexible', 'Flexible')],
help="Defines if you can consume more or less components than the quantity defined on the BoM.",
default='strict',
default='flexible',
string='Consumption',
required=True
)
+171 -32
View File
@@ -72,6 +72,7 @@ class MrpProduction(models.Model):
domain="[('id', 'in', allowed_product_ids)]",
readonly=True, required=True, check_company=True,
states={'draft': [('readonly', False)]})
product_tracking = fields.Selection(related='product_id.tracking')
allowed_product_ids = fields.Many2many('product.product', compute='_compute_allowed_product_ids')
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id')
product_qty = fields.Float(
@@ -83,6 +84,10 @@ class MrpProduction(models.Model):
'uom.uom', 'Product Unit of Measure',
readonly=True, required=True,
states={'draft': [('readonly', False)]}, domain="[('category_id', '=', product_uom_category_id)]")
lot_producing_id = fields.Many2one(
'stock.production.lot', string='Lot/Serial Number', copy=False,
domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True)
qty_producing = fields.Float(string="Quantity Producing", copy=False)
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
picking_type_id = fields.Many2one(
@@ -175,6 +180,7 @@ class MrpProduction(models.Model):
'stock.move', 'production_id', 'Finished Products',
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
move_byproduct_ids = fields.One2many('stock.move', compute='_compute_move_byproduct_ids', inverse='_set_move_byproduct_ids')
finished_move_line_ids = fields.One2many(
'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
)
@@ -454,8 +460,9 @@ class MrpProduction(models.Model):
for order in self:
already_reserved = order.is_locked and order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids')
any_quantity_done = any([m.quantity_done > 0 for m in order.move_raw_ids])
order.unreserve_visible = not any_quantity_done and already_reserved
order.reserve_visible = order.state in ('confirmed', 'planned') and any(move.state in ['confirmed', 'partially_available'] for move in order.move_raw_ids)
order.reserve_visible = order.state in ('confirmed', 'progress', 'to_close') and any(move.state in ['confirmed', 'partially_available'] for move in order.move_raw_ids.filtered(lambda m: m.product_uom_qty))
@api.depends('move_finished_ids.quantity_done', 'move_finished_ids.state', 'is_locked')
def _compute_post_visible(self):
@@ -476,6 +483,14 @@ class MrpProduction(models.Model):
for production in self:
production.scrap_count = count_data.get(production.id, 0)
@api.depends('move_finished_ids')
def _compute_move_byproduct_ids(self):
for order in self:
order.move_byproduct_ids = order.move_finished_ids.filtered(lambda m: m.product_id != order.product_id)
def _set_move_byproduct_ids(self):
self.move_finished_ids |= self.move_byproduct_ids
_sql_constraints = [
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
@@ -502,7 +517,7 @@ class MrpProduction(models.Model):
""" Finds UoM of changed product. """
if not self.product_id:
self.bom_id = False
else:
elif not self.bom_id or self.bom_id.product_tmpl_id != self.product_tmpl_id or (self.bom_id.product_id and self.bom_id.product_id != self.product_id):
bom = self.env['mrp.bom']._bom_find(product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id, bom_type='normal')
if bom:
self.bom_id = bom.id
@@ -563,6 +578,28 @@ class MrpProduction(models.Model):
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
@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)
def write(self, vals):
res = super(MrpProduction, self).write(vals)
@@ -572,7 +609,7 @@ class MrpProduction(models.Model):
raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.'))
if production.workorder_ids and not self.env.context.get('force_date', False):
raise UserError(_('You cannot move a planned manufacturing order.'))
if 'move_raw_ids' in vals and production.state != 'draft':
if ('move_raw_ids' in vals or 'move_finished_ids' in vals) and production.state != 'draft':
production._autoconfirm_production()
if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'):
new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start'))
@@ -595,7 +632,6 @@ class MrpProduction(models.Model):
production = super(MrpProduction, self).create(values)
production.move_raw_ids.write({
'group_id': production.procurement_group_id.id,
'reference': production.name, # set reference when MO name is different than 'New'
})
# Trigger move_raw creation when importing a file
if 'import_file' in self.env.context:
@@ -692,7 +728,6 @@ class MrpProduction(models.Model):
data = {
'sequence': bom_line.sequence if bom_line else 10,
'name': self.name,
'reference': self.name,
'date': self.date_planned_start,
'date_expected': self.date_planned_start,
'bom_line_id': bom_line.id if bom_line else False,
@@ -782,13 +817,14 @@ class MrpProduction(models.Model):
)
additional_moves.write({
'group_id': production.procurement_group_id.id,
'reference': production.name, # set reference when MO name is different than 'New'
})
additional_moves._adjust_procure_method()
moves_to_confirm |= additional_moves
moves_to_confirm |= production.move_finished_ids.filtered(
additional_byproducts = production.move_finished_ids.filtered(
lambda move: move.state == 'draft' and move.additional
)
moves_to_confirm |= additional_byproducts
if moves_to_confirm:
moves_to_confirm._action_confirm()
@@ -826,7 +862,7 @@ class MrpProduction(models.Model):
})
else:
action.update({
'name': _("MO Generated by %s" % self.name),
'name': _("MO Generated by %s") % self.name,
'domain': [('id', 'in', mrp_production_ids)],
'view_mode': 'tree,form',
})
@@ -842,6 +878,34 @@ class MrpProduction(models.Model):
'view_mode': 'tree,form',
}
def action_generate_serial(self):
self.ensure_one()
self.lot_producing_id = self.env['stock.production.lot'].create({
'product_id': self.product_id.id,
'company_id': self.company_id.id
})
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)
def action_confirm(self):
self._check_company()
for production in self:
@@ -862,13 +926,6 @@ class MrpProduction(models.Model):
production.workorder_ids._refresh_wo_lines()
return True
def open_produce_product(self):
self.ensure_one()
if self.bom_id.type == 'phantom':
raise UserError(_('You cannot produce a MO with a bom kit product.'))
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
return action
def button_plan(self):
""" Create work orders. And probably do stuff, like things. """
orders_to_plan = self.filtered(lambda order: order.bom_id.operation_ids and order.state == 'confirmed')
@@ -1122,7 +1179,7 @@ class MrpProduction(models.Model):
self.ensure_one()
return True
def post_inventory(self):
def _post_inventory(self, cancel_backorder=False):
for order in self:
moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done')
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
@@ -1132,23 +1189,26 @@ class MrpProduction(models.Model):
# to get extra moves.
moves_to_do = moves_to_do._action_done()
moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do
finish_moves = order.move_finished_ids.filtered(lambda m: m.product_id == order.product_id and m.state not in ('done', 'cancel'))
# the finish move can already be completed by the workorder.
if not finish_moves.quantity_done:
if order.product_tracking == 'serial':
uom = order.product_id.uom_id
finish_moves.quantity_done = order.product_uom_id._compute_quantity(order.qty_producing, uom, round='HALF-UP')
finish_moves.move_line_ids.product_uom_id = uom
else:
finish_moves.quantity_done = float_round(self.qty_producing - self.qty_produced, precision_rounding=order.product_uom_id.rounding, rounding_method='HALF-UP')
finish_moves.move_line_ids.lot_id = self.lot_producing_id
order._cal_price(moves_to_do)
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
moves_to_finish = moves_to_finish._action_done()
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')
for moveline in moves_to_finish.mapped('move_line_ids'):
if moveline.move_id.has_tracking != 'none' and moveline.product_id == order.product_id or moveline.lot_id in consume_move_lines.mapped('lot_produced_ids'):
if any([not ml.lot_produced_ids for ml in consume_move_lines]):
raise UserError(_('You can not consume without telling for which lot you consumed it'))
# Link all movelines in the consumed with same lot_produced_ids false or the correct lot_produced_ids
filtered_lines = consume_move_lines.filtered(lambda ml: moveline.lot_id in ml.lot_produced_ids)
moveline.write({'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])]})
else:
# Link with everything
moveline.write({'consume_line_ids': [(6, 0, [x for x in consume_move_lines.ids])]})
order.move_finished_ids.move_line_ids.consume_line_ids = [(6, 0, consume_move_lines.ids)]
return True
@api.model
@@ -1189,13 +1249,10 @@ class MrpProduction(models.Model):
production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
'raw_material_production_id': backorder_mo.id,
'reference': backorder_mo.name,
})
backorders |= backorder_mo
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
# update moves references
(production.move_raw_ids | production.move_finished_ids).reference = production.name
for wo in backorder_mo.workorder_ids:
wo.duration_expected = wo._get_duration_expected(wo.workcenter_id)
@@ -1236,8 +1293,9 @@ class MrpProduction(models.Model):
productions_to_backorder = self.env['mrp.production']
self.workorder_ids.button_finish()
self._post_inventory()
productions_not_to_backorder._post_inventory(cancel_backorder=False)
productions_to_backorder._post_inventory(cancel_backorder=True)
backorders = productions_to_backorder._generate_backorder_productions()
# Moves without quantity done are not posted => set them as done instead of canceling. In
@@ -1383,7 +1441,7 @@ class MrpProduction(models.Model):
'res_model': 'mrp.unbuild',
'view_id': self.env.ref('mrp.mrp_unbuild_form_view_simplified').id,
'type': 'ir.actions.act_window',
'context': {'default_mo_id': self.id,
'context': {'default_mo_id': self.id,
'default_company_id': self.company_id.id,
'default_location_id': self.location_dest_id.id,
'default_location_dest_id': self.location_src_id.id,
@@ -1398,3 +1456,84 @@ class MrpProduction(models.Model):
def _get_quantity_to_backorder(self):
self.ensure_one()
return max(self.product_qty - self.qty_produced, 0)
def _check_sn_uniqueness(self):
""" Alert the user if the serial number as already been consumed/produced """
if self.product_tracking == 'serial' and self.lot_producing_id:
sml = self.env['stock.move.line'].search_count([
('lot_id', '=', self.lot_producing_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)
for move in self.move_finished_ids:
if move.has_tracking != 'serial' or move.product_id == self.product_id:
continue
for move_line in move.move_line_ids:
domain = [
('lot_id', '=', move_line.lot_id.id),
('qty_done', '=', 1),
('state', '=', 'done')
]
message = _('The serial number %s used for byproduct %s has already been produced') % (move_line.lot_id.name, move_line.product_id.name)
co_prod_move_lines = self.move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self.product_id)
domain_unbuild = domain + [
('production_id', '=', False),
('location_dest_id.usage', '=', 'production')
]
# Check presence of same sn in previous productions
duplicates = self.env['stock.move.line'].search_count(domain + [
('location_id.usage', '=', 'production')
])
if duplicates:
# Maybe some move lines have been compensated by unbuild
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
raise UserError(message)
# Check presence of same sn in current production
duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
if duplicates:
raise UserError(message)
for move in self.move_raw_ids:
if move.has_tracking != 'serial':
continue
for move_line in move.move_line_ids:
if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding):
continue
domain = [
('lot_id', '=', move_line.lot_id.id),
('qty_done', '=', 1),
('state', '=', 'done')
]
message = _('The serial number %s used for component %s has already been consumed') % (move_line.lot_id.name, move_line.product_id.name)
co_prod_move_lines = self.move_raw_ids.move_line_ids
domain_unbuild = domain + [
('production_id', '=', False),
('location_id.usage', '=', 'production')
]
# Check presence of same sn in previous productions
duplicates = self.env['stock.move.line'].search_count(domain + [
('location_dest_id.usage', '=', 'production')
])
if duplicates:
# Maybe some move lines have been compensated by unbuild
duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
raise UserError(message)
# Check presence of same sn in current production
duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
if duplicates:
raise UserError(message)
def _strict_consumption_check(self):
if self.consumption == 'strict':
for move in self.move_raw_ids:
rounding = move.product_uom.rounding
if float_compare(move.quantity_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)
+1 -1
View File
@@ -184,7 +184,7 @@ class MrpUnbuild(models.Model):
needed_quantity = move.product_qty
moves_lines = original_move.mapped('move_line_ids')
if move in produce_moves and self.lot_id:
moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.lot_produced_ids)
moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.produce_line_ids.lot_id) # FIXME sle: double check with arm
for move_line in moves_lines:
# Iterate over all move_lines until we unbuilded the correct quantity.
taken_quantity = min(needed_quantity, move_line.qty_done)
+79 -15
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_is_zero
from odoo.tools import float_compare, float_round, float_repr, float_is_zero
class StockMoveLine(models.Model):
@@ -11,7 +11,6 @@ class StockMoveLine(models.Model):
workorder_id = fields.Many2one('mrp.workorder', 'Work Order', check_company=True)
production_id = fields.Many2one('mrp.production', 'Production Order', check_company=True)
lot_produced_ids = fields.Many2many('stock.production.lot', string='Finished Lot/Serial Number', check_company=True)
done_move = fields.Boolean('Move Done', related='move_id.is_done', readonly=False, store=True) # TDE FIXME: naming
@api.model_create_multi
@@ -23,8 +22,10 @@ class StockMoveLine(models.Model):
# traceability report
if line.move_id.raw_material_production_id and line.state == 'done':
mo = line.move_id.raw_material_production_id
if line.lot_produced_ids:
produced_move_lines = mo.move_finished_ids.move_line_ids.filtered(lambda sml: sml.lot_id in line.lot_produced_ids)
finished_lots = mo.lot_producing_id
finished_lots |= mo.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id).move_line_ids.lot_id
if finished_lots:
produced_move_lines = mo.move_finished_ids.move_line_ids.filtered(lambda sml: sml.lot_id in finished_lots)
line.produce_line_ids = [(6, 0, produced_move_lines.ids)]
else:
produced_move_lines = mo.move_finished_ids.move_line_ids
@@ -45,7 +46,7 @@ class StockMoveLine(models.Model):
def _reservation_is_updatable(self, quantity, reserved_quant):
self.ensure_one()
if self.lot_produced_ids:
if self.produce_line_ids.lot_id:
ml_remaining_qty = self.qty_done - self.product_uom_qty
ml_remaining_qty = self.product_uom_id._compute_quantity(ml_remaining_qty, self.product_id.uom_id, rounding_method="HALF-UP")
if float_compare(ml_remaining_qty, quantity, precision_rounding=self.product_id.uom_id.rounding) < 0:
@@ -54,10 +55,6 @@ class StockMoveLine(models.Model):
def write(self, vals):
for move_line in self:
if move_line.move_id.production_id and 'lot_id' in vals:
move_line.production_id.move_raw_ids.mapped('move_line_ids')\
.filtered(lambda r: not r.done_move and move_line.lot_id in r.lot_produced_ids)\
.write({'lot_produced_ids': [(4, vals['lot_id'])]})
production = move_line.move_id.production_id or move_line.move_id.raw_material_production_id
if production and move_line.state == 'done' and any(field in vals for field in ('lot_id', 'location_id', 'qty_done')):
move_line._log_message(production, move_line, 'mrp.track_production_move_template', vals)
@@ -95,6 +92,7 @@ class StockMove(models.Model):
needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots')
order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids')
finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids')
should_consume_qty = fields.Float('Quantity To Consume', compute='_compute_should_consume_qty')
def _unreserve_initial_demand(self, new_move):
# If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
@@ -153,22 +151,55 @@ class StockMove(models.Model):
for move in self:
move.is_done = (move.state in ('done', 'cancel'))
@api.depends('product_uom_qty')
@api.depends('product_uom_qty', 'raw_material_production_id', 'raw_material_production_id.product_qty', 'raw_material_production_id.qty_produced')
def _compute_unit_factor(self):
for move in self:
mo = move.raw_material_production_id or move.production_id
if mo:
move.unit_factor = (move.product_uom_qty - move.quantity_done) / ((mo.product_qty - mo.qty_produced) or 1)
move.unit_factor = move.product_uom_qty / ((mo.product_qty - mo.qty_produced) or 1)
else:
move.unit_factor = 1.0
@api.depends('raw_material_production_id', 'raw_material_production_id.name', 'production_id', 'production_id.name')
def _compute_reference(self):
moves_with_reference = self.env['stock.move']
for move in self:
if move.raw_material_production_id and move.raw_material_production_id.name:
move.reference = move.raw_material_production_id.name
moves_with_reference |= move
if move.production_id and move.production_id.name:
move.reference = move.production_id.name
moves_with_reference |= move
super(StockMove, self - moves_with_reference)._compute_reference()
@api.depends('raw_material_production_id.qty_producing', 'product_uom_qty')
def _compute_should_consume_qty(self):
for move in self:
mo = move.raw_material_production_id
if not mo:
move.qty_summary = 0
continue
move.should_consume_qty = mo.product_uom_id._compute_quantity((mo.qty_producing - mo.qty_produced) * move.unit_factor, mo.product_uom_id, rounding_method='HALF-UP')
@api.onchange('product_uom_qty')
def _onchange_product_uom_qty(self):
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):
defaults = super(StockMove, self).default_get(fields_list)
if self.env.context.get('default_raw_material_production_id'):
production_id = self.env['mrp.production'].browse(self.env.context['default_raw_material_production_id'])
if production_id.state in ('confirmed', 'done'):
if production_id.state == 'confirmed':
if self.env.context.get('default_raw_material_production_id') or self.env.context.get('default_production_id'):
production_id = self.env['mrp.production'].browse(self.env.context.get('default_raw_material_production_id') or self.env.context.get('default_production_id'))
if production_id.state not in ('draft', 'cancel'):
if production_id.state != 'done':
defaults['state'] = 'draft'
else:
defaults['state'] = 'done'
@@ -225,6 +256,17 @@ class StockMove(models.Model):
moves_to_return |= phantom_moves.action_explode()
return moves_to_return
def action_show_details(self):
self.ensure_one()
action = super().action_show_details()
if self.raw_material_production_id:
action['views'] = [(self.env.ref('mrp.view_stock_move_operations_raw').id, 'form')]
action['context']['show_destination_location'] = False
elif self.production_id:
action['views'] = [(self.env.ref('mrp.view_stock_move_operations_finished').id, 'form')]
action['context']['show_source_location'] = False
return action
def _action_cancel(self):
res = super(StockMove, self)._action_cancel()
for production in self.mapped('raw_material_production_id'):
@@ -328,3 +370,25 @@ class StockMove(models.Model):
return min(qty_ratios) // 1
else:
return 0.0
@api.depends('raw_material_production_id')
def _compute_is_quantity_done_editable(self):
return super()._compute_is_quantity_done_editable()
def _show_details_in_draft(self):
self.ensure_one()
if self.raw_material_production_id and self.state == 'draft':
return True
else:
return super()._show_details_in_draft()
def _update_quantity_done(self, mo):
self.ensure_one()
ml_values = {}
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)
else:
self.quantity_done = new_qty
return ml_values
-2
View File
@@ -57,8 +57,6 @@ access_mrp_unbuild,mrp.unbuild,model_mrp_unbuild,group_mrp_user,1,1,1,1
access_mrp_unbuild_manager,mrp.unbuild manager,model_mrp_unbuild,group_mrp_manager,1,1,1,1
access_mrp_document_mrp_manager,mrp.document group_user,model_mrp_document,group_mrp_manager,1,1,1,1
access_mrp_document_mrp_user,mrp.document group_user,model_mrp_document,group_mrp_user,1,1,1,1
access_mrp_product_produce,access.mrp.product.produce,model_mrp_product_produce,mrp.group_mrp_user,1,1,1,0
access_mrp_product_produce_line,access.mrp.product.produce.line,model_mrp_product_produce_line,mrp.group_mrp_user,1,1,1,1
access_change_production_qty,access.change.production.qty,model_change_production_qty,mrp.group_mrp_user,1,1,1,0
access_stock_warn_insufficient_qty_unbuild,access.stock.warn.insufficient.qty.unbuild,model_stock_warn_insufficient_qty_unbuild,mrp.group_mrp_user,1,1,1,0
access_mrp_production_backorder,access.mrp.production.backorder,model_mrp_production_backorder,mrp.group_mrp_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
57 access_mrp_unbuild_manager mrp.unbuild manager model_mrp_unbuild group_mrp_manager 1 1 1 1
58 access_mrp_document_mrp_manager mrp.document group_user model_mrp_document group_mrp_manager 1 1 1 1
59 access_mrp_document_mrp_user mrp.document group_user model_mrp_document group_mrp_user 1 1 1 1
access_mrp_product_produce access.mrp.product.produce model_mrp_product_produce mrp.group_mrp_user 1 1 1 0
access_mrp_product_produce_line access.mrp.product.produce.line model_mrp_product_produce_line mrp.group_mrp_user 1 1 1 1
60 access_change_production_qty access.change.production.qty model_change_production_qty mrp.group_mrp_user 1 1 1 0
61 access_stock_warn_insufficient_qty_unbuild access.stock.warn.insufficient.qty.unbuild model_stock_warn_insufficient_qty_unbuild mrp.group_mrp_user 1 1 1 0
62 access_mrp_production_backorder access.mrp.production.backorder model_mrp_production_backorder mrp.group_mrp_user 1 1 1 0
@@ -0,0 +1,81 @@
odoo.define('mrp.should_consume', function (require) {
"use strict";
var BasicFields = require('web.basic_fields');
var FieldFloat = BasicFields.FieldFloat;
var fieldRegistry = require('web.field_registry');
var field_utils = require('web.field_utils');
/**
* This widget is used to display alongside the total quantity to consume of a production order,
* the exact quantity that the worker should consume depending on the BoM. Ex:
* 2 components to make 1 finished product.
* The production order is created to make 5 finished product and the quantity producing is set to 3.
* The widget will be '3.000 / 5.000'.
*/
var MrpShouldConsume = FieldFloat.extend({
/**
* @override
*/
init: function (parent, name, params) {
this._super.apply(this, arguments);
this.displayShouldConsume = !['done', 'draft', 'cancel'].includes(params.data.state);
let options = {'digits': [false, 3]};
this.should_consume_qty = field_utils.format.float(params.data.should_consume_qty, false, options);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} [el] jquery input element that will be surrounded by a new span
* @param {float} [value] quantity to display before the input `el`
* @return {jquery element}
*/
_addShouldConsume: function (el, value) {
var $to_consume_container = $('<span class="o_should_consume"/>');
$to_consume_container.text(value + ' / ');
$to_consume_container.append(el);
return $to_consume_container
},
/**
* @private
* @override
*/
_renderEdit: function () {
// Keep a reference to the input so $el can become something else
// without losing track of the actual input.
var def = this._super.apply(this, arguments);
if (this.displayShouldConsume) {
var $container = this._addShouldConsume(this.$el, this.should_consume_qty);
$container.addClass('o_row');
this.$el = $container;
};
return def;
},
/**
* Resets the content to the formated value in readonly mode.
*
* @override
* @private
*/
_renderReadonly: function () {
var def = this._super.apply(this, arguments);
if (this.displayShouldConsume) {
var $container = this._addShouldConsume(this.$el, this.should_consume_qty);
this.$el = $container;
};
return def;
},
});
fieldRegistry.add('mrp_should_consume', MrpShouldConsume);
return {
MrpShouldConsume: MrpShouldConsume,
};
});
+4 -1
View File
@@ -5,4 +5,7 @@
height: 30rem;
border: none;
}
}
}
.o_should_consume{
padding-left: 0.3em;
}
+2 -1
View File
@@ -7,7 +7,7 @@ from odoo.addons.stock.tests import common2
class TestMrpCommon(common2.TestStockCommon):
@classmethod
def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False):
def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False, consumption=False):
""" This function generate a manufacturing order with one final
product and two consumed product. Arguments allows to choose
the tracking/qty for each different products. It returns the
@@ -34,6 +34,7 @@ class TestMrpCommon(common2.TestStockCommon):
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
'consumption': consumption if consumption else 'flexible',
'bom_line_ids': [
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': qty_base_2}),
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1})
+12 -16
View File
@@ -27,10 +27,9 @@ class TestMrpProductionBackorder(TestMrpCommon):
"""
mo = self.generate_mo(qty_final=4)[0]
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=mo.id))
produce_form.qty_producing = 4
produce = produce_form.save()
produce.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 4
mo = mo_form.save()
# No backorder is proposed
self.assertTrue(mo.button_mark_done())
@@ -61,10 +60,9 @@ class TestMrpProductionBackorder(TestMrpCommon):
self.assertEqual(production.state, 'confirmed')
self.assertEqual(production.reserve_visible, False)
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=production.id))
produce_form.qty_producing = 1
produce = produce_form.save()
produce.do_produce()
mo_form = Form(production)
mo_form.qty_producing = 1
production = mo_form.save()
action = production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
@@ -103,10 +101,9 @@ class TestMrpProductionBackorder(TestMrpCommon):
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
self.assertFalse(pbm_move.move_orig_ids)
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=production.id))
produce_form.qty_producing = 1
produce = produce_form.save()
produce.do_produce()
mo_form = Form(production)
mo_form.qty_producing = 1
production = mo_form.save()
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16)
self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4)
@@ -154,10 +151,9 @@ class TestMrpProductionBackorder(TestMrpCommon):
self.assertEqual(sam_move.product_id.id, product_to_build.id)
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=production.id))
produce_form.qty_producing = 1
produce = produce_form.save()
produce.do_produce()
mo_form = Form(production)
mo_form.qty_producing = 1
production = mo_form.save()
action = production.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
+3 -7
View File
@@ -58,9 +58,6 @@ class TestMrpByProduct(common.TransactionCase):
mnf_product_a = mnf_product_a_form.save()
mnf_product_a.action_confirm()
# I compute the data of production order
context = {"active_model": "mrp.production", "active_ids": [mnf_product_a.id], "active_id": mnf_product_a.id}
# I confirm the production order.
self.assertEqual(mnf_product_a.state, 'confirmed', 'Production order should be in state confirmed')
@@ -71,12 +68,11 @@ class TestMrpByProduct(common.TransactionCase):
# I consume and produce the production of products.
# I create record for selecting mode and quantity of products to produce.
produce_form = Form(self.env['mrp.product.produce'].with_context(context))
produce_form.qty_producing = 2.00
product_consume = produce_form.save()
mo_form = Form(mnf_product_a)
mo_form.qty_producing = 2.00
mnf_product_a = mo_form.save()
# I finish the production order.
self.assertEqual(len(mnf_product_a.move_raw_ids), 1, "Wrong consume move on production order.")
product_consume.do_produce()
consume_move_c = mnf_product_a.move_raw_ids
by_product_move = mnf_product_a.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_b.id)
# Check sub production produced quantity...
+20 -14
View File
@@ -33,10 +33,9 @@ class TestMrpCancelMO(TestMrpCommon):
# Create MO
manufacturing_order = self.generate_mo()[0]
# Produce some quantity
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id))
produce_form.qty_producing = 2
produce = produce_form.save()
produce.do_produce()
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
# Cancel it
manufacturing_order.action_cancel()
# Check it's cancelled
@@ -53,12 +52,11 @@ class TestMrpCancelMO(TestMrpCommon):
after post inventory.
"""
# Create MO
manufacturing_order = self.generate_mo()[0]
manufacturing_order = self.generate_mo(consumption='strict')[0]
# Produce some quantity (not all to avoid to done the MO when post inventory)
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id))
produce_form.qty_producing = 2
produce = produce_form.save()
produce.do_produce()
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
# Post Inventory
manufacturing_order.post_inventory()
# Cancel the MO
@@ -84,7 +82,7 @@ class TestMrpCancelMO(TestMrpCommon):
done and the WO must be cancelled.
"""
# Create MO
mo_data = self.generate_mo()
mo_data = self.generate_mo(consumption='strict')
manufacturing_order = mo_data[0]
bom = mo_data[1]
bom.write({
@@ -92,6 +90,15 @@ class TestMrpCancelMO(TestMrpCommon):
(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()
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
manufacturing_order.button_plan()
workorder = manufacturing_order.workorder_ids
@@ -136,10 +143,9 @@ class TestMrpCancelMO(TestMrpCommon):
# it (cannot be deleted)
manufacturing_order = self.generate_mo()[0]
# Produce some quantity (not all to avoid to done the MO when post inventory)
produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id))
produce_form.qty_producing = 2
produce = produce_form.save()
produce.do_produce()
mo_form = Form(manufacturing_order)
mo_form.qty_producing = 2
manufacturing_order = mo_form.save()
# Post Inventory
manufacturing_order.post_inventory()
# Unlink the MO must raises an UserError since it cannot be really cancelled
+12 -14
View File
@@ -133,15 +133,10 @@ class TestMrpMulticompany(common.TransactionCase):
})
mo_form = Form(self.env['mrp.production'].with_user(self.user_a))
mo_form.product_id = product
mo_form.lot_producing_id = lot_b
mo = mo_form.save()
mo.with_user(self.user_b).action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_user(self.user_b).with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.finished_lot_id = lot_b
with self.assertRaises(UserError):
produce_form.save()
mo.with_user(self.user_b).action_confirm()
def test_product_produce_2(self):
"""Check that using a component lot of company b in the produce wizard of a production
@@ -168,14 +163,17 @@ class TestMrpMulticompany(common.TransactionCase):
mo_form.product_id = product
mo = mo_form.save()
mo.with_user(self.user_b).action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_user(self.user_b).with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = lot_b
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.lot_id = lot_b
ml.qty_done = 1
details_operation_form.save()
with self.assertRaises(UserError):
produce_form.save()
mo.button_mark_done()
def test_partner_1(self):
""" On a product without company, as a user of Company B, check it is not possible to use a
+195 -479
View File
@@ -8,7 +8,6 @@ from odoo.fields import Datetime as Dt
from odoo.exceptions import UserError
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestMrpOrder(TestMrpCommon):
def test_access_rights_manager(self):
@@ -95,13 +94,9 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(first_move.product_qty, test_quantity / self.bom_1.product_qty * self.product_4.uom_id.factor_inv * 4)
# produce product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': man_order.id,
'active_ids': [man_order.id],
}))
produce_form.qty_producing = 1.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(man_order)
mo_form.qty_producing = 1.0
man_order = mo_form.save()
action = man_order.button_mark_done()
self.assertEqual(man_order.state, 'progress', "Production order should be open a backorder wizard, then not done yet.")
@@ -160,15 +155,12 @@ class TestMrpOrder(TestMrpCommon):
mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=10, qty_final=1, qty_base_2=1)
bom.consumption = 'flexible'
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
for i in range(len(produce_form.raw_workorder_line_ids)):
with produce_form.raw_workorder_line_ids.edit(i) as line:
line.qty_done += 1
product_produce = produce_form.save()
product_produce.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1
for i in range(len(mo_form.move_raw_ids)):
with mo_form.move_raw_ids.edit(i) as move:
move.quantity_done += 1
mo = mo_form.save()
self.assertEqual(len(mo.move_raw_ids), 2)
self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 2)
self.assertEqual(mo.move_raw_ids[0].move_line_ids.mapped('qty_done'), [2])
@@ -248,11 +240,9 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(mo_custom_laptop.reservation_state, 'assigned')
# produce one item, call `post_inventory`
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
produce_form = Form(self.env['mrp.product.produce'].with_context(context))
produce_form.qty_producing = 1.00
custom_laptop_produce = produce_form.save()
custom_laptop_produce.do_produce()
mo_form = Form(mo_custom_laptop)
mo_form.qty_producing = 1.00
mo_custom_laptop = mo_form.save()
mo_custom_laptop.post_inventory()
# check the consumed quants of the produced quant
@@ -264,11 +254,9 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(second_move.quantity_done, 0, "There is still one product to pruduce")
# produce the second item, call `post_inventory`
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
produce_form = Form(self.env['mrp.product.produce'].with_context(context))
produce_form.qty_producing = 1.00
custom_laptop_produce = produce_form.save()
custom_laptop_produce.do_produce()
mo_form = Form(mo_custom_laptop)
mo_form.qty_producing = 2.00
mo_custom_laptop = mo_form.save()
mo_custom_laptop.post_inventory()
self.assertEqual(second_move.quantity_done, 1, "Order produce the second product")
quant_after2 = custom_laptop.qty_available
@@ -301,14 +289,15 @@ class TestMrpOrder(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
mo.move_finished_ids.move_line_ids.qty_done -= 1
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = lot_1
ml.qty_done = 20
details_operation_form.save()
update_quantity_wizard = self.env['change.production.qty'].create({
'mo_id': mo.id,
'product_qty': 4,
@@ -333,67 +322,25 @@ class TestMrpOrder(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 2
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 2
mo = mo_form.save()
mo._post_inventory()
mo.post_inventory()
update_quantity_wizard = self.env['change.production.qty'].create({
'mo_id': mo.id,
'product_qty': 5,
})
update_quantity_wizard.change_prod_qty()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 5
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).mapped('quantity_done')), 20)
self.assertEqual(sum(mo.move_finished_ids.mapped('quantity_done')), 5)
def test_update_quantity_3(self):
""" Build 1 final products then update the Manufacturing
order quantity. Check the remaining quantity to produce
take care of the first quantity produced."""
self.stock_location = self.env.ref('stock.stock_location_stock')
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2)
self.assertEqual(len(mo), 1, 'MO should have been created')
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
produce_wizard.do_produce()
update_quantity_wizard = self.env['change.production.qty'].create({
'mo_id': mo.id,
'product_qty': 3,
})
update_quantity_wizard.change_prod_qty()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).mapped('quantity_done')), 12)
self.assertEqual(sum(mo.move_finished_ids.mapped('quantity_done')), 3)
def test_rounding(self):
""" Checks we round up when bringing goods to produce and round half-up when producing.
This implementation allows to implement an efficiency notion (see rev 347f140fe63612ee05e).
@@ -422,13 +369,9 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up')
# produce product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production.id,
'active_ids': [production.id],
}))
produce_form.qty_producing = 8
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(production)
mo_form.qty_producing = 8
production = mo_form.save()
self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing')
self.assertEqual(production.move_raw_ids[1].quantity_done, 34, 'Should use half-up rounding when producing')
@@ -443,27 +386,21 @@ class TestMrpOrder(TestMrpCommon):
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
# change the quantity done in one line
produce_form.raw_workorder_line_ids._records[0]['qty_done'] = 1
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 1
details_operation_form.save()
# change the quantity producing
produce_form.qty_producing = 3
mo_form = Form(mo)
mo_form.qty_producing = 3
# check than all quantities are update correctly
line1 = produce_form.raw_workorder_line_ids._records[0]
line2 = produce_form.raw_workorder_line_ids._records[1]
self.assertEqual(line1['qty_to_consume'], 3, "Wrong quantity to consume")
self.assertEqual(line1['qty_done'], 3, "Wrong quantity done")
self.assertEqual(line2['qty_to_consume'], 12, "Wrong quantity to consume")
self.assertEqual(line2['qty_done'], 12, "Wrong quantity done")
product_produce = produce_form.save()
self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'You should have produce lines even the consumed products are not tracked.')
product_produce.do_produce()
self.assertEqual(mo_form.move_raw_ids._records[0]['product_uom_qty'], 5, "Wrong quantity to consume")
self.assertEqual(mo_form.move_raw_ids._records[0]['quantity_done'], 3, "Wrong quantity done")
self.assertEqual(mo_form.move_raw_ids._records[1]['product_uom_qty'], 20, "Wrong quantity to consume")
self.assertEqual(mo_form.move_raw_ids._records[1]['quantity_done'], 12, "Wrong quantity done")
def test_product_produce_2(self):
""" Checks that, for a BOM where one of the components is tracked by serial number and the
@@ -490,22 +427,19 @@ class TestMrpOrder(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
self.assertEqual(len(produce_form.raw_workorder_line_ids), 3, 'You should have 3 produce lines. One for each serial to consume and for the untracked product.')
produce_form.qty_producing = 1
self.assertEqual(len(mo.move_raw_ids.move_line_ids), 3, 'You should have 3 stock move lines. One for each serial to consume and for the untracked product.')
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
# get the proposed lot
consumed_lots = self.env['stock.production.lot']
for workorder_line in produce_form.raw_workorder_line_ids._records:
if workorder_line['product_id'] == p1.id:
consumed_lots |= self.env['stock.production.lot'].browse(workorder_line['lot_id'])
consumed_lots.ensure_one()
product_produce = produce_form.save()
product_produce.do_produce()
details_operation_form = Form(mo.move_raw_ids.filtered(lambda move: move.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
self.assertEqual(len(details_operation_form.move_line_ids), 2)
with details_operation_form.move_line_ids.edit(0) as ml:
consumed_lots = ml.lot_id
ml.qty_done = 1
details_operation_form.save()
remaining_lot = (lot_p1_1 | lot_p1_2) - consumed_lots
remaining_lot.ensure_one()
@@ -513,22 +447,22 @@ class TestMrpOrder(TestMrpCommon):
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_backorder()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
product_produce = produce_form.save()
self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'You should have 2 produce lines left.')
for line in product_produce.raw_workorder_line_ids.filtered(lambda x: x.lot_id):
self.assertEqual(line.lot_id, remaining_lot, 'Wrong lot proposed.')
# Check MO backorder
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
mo_form = Form(mo_backorder)
mo_form.qty_producing = 1
mo_backorder = mo_form.save()
details_operation_form = Form(mo_backorder.move_raw_ids.filtered(lambda move: move.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
self.assertEqual(len(details_operation_form.move_line_ids), 1)
with details_operation_form.move_line_ids.edit(0) as ml:
self.assertEqual(ml.lot_id, remaining_lot)
def test_product_produce_3(self):
""" Checks that, for a BOM where one of the components is tracked by lot and the other is
not tracked, when creating a manufacturing order for 1 finished product and reserving, the
produce wizard proposes the corrects lines. Then, checks the generated move lines when over
consuming.
reserved lines are displayed. Then, over-consume by creating new line.
"""
# FIXME: some asserts on the quants after overproducing would be nice
self.stock_location = self.env.ref('stock.stock_location_stock')
self.stock_shelf_1 = self.stock_location_components
@@ -560,22 +494,31 @@ class TestMrpOrder(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1.0
for i in range(len(produce_form.raw_workorder_line_ids)):
with produce_form.raw_workorder_line_ids.edit(i) as line:
line.qty_done += 1
product_produce = produce_form.save()
product_produce.finished_lot_id = final_product_lot.id
# product 1 lot 1 shelf1
# product 1 lot 1 shelf2
# product 1 lot 2
self.assertEqual(len(product_produce.raw_workorder_line_ids), 4, 'You should have 4 produce lines. lot 1 shelf_1, lot 1 shelf_2, lot2 and for product which have tracking None')
mo_form = Form(mo)
mo_form.qty_producing = 1.0
mo_form.lot_producing_id = final_product_lot
mo = mo_form.save()
# p2
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as line:
line.qty_done = line.product_uom_qty
with details_operation_form.move_line_ids.new() as line:
line.qty_done = 1
details_operation_form.save()
product_produce.do_produce()
# p1
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
for i in range(len(details_operation_form.move_line_ids)):
# reservation in shelf1: 3 lot1, shelf2: 3 lot1, stock: 4 lot2
with details_operation_form.move_line_ids.edit(i) as line:
line.qty_done = line.product_uom_qty
with details_operation_form.move_line_ids.new() as line:
line.qty_done = 2
line.lot_id = first_lot_for_p1
with details_operation_form.move_line_ids.new() as line:
line.qty_done = 1
line.lot_id = second_lot_for_p1
details_operation_form.save()
move_1 = mo.move_raw_ids.filtered(lambda m: m.product_id == p1)
# qty_done/product_uom_qty lot
@@ -616,18 +559,15 @@ class TestMrpOrder(TestMrpCommon):
ml_p1[0].qty_done = 1.0
# Produce baby!
product_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
product_produce = product_form.save()
product_produce.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
m_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1)
ml_p1 = m_p1.mapped('move_line_ids')
self.assertEqual(len(ml_p1), 3)
self.assertEqual(sorted(ml_p1.mapped('qty_done')), [1.0, 2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0')
self.assertEqual(m_p1.quantity_done, 6.0, 'Total qty done should be 6.0')
self.assertEqual(len(ml_p1), 2)
self.assertEqual(sorted(ml_p1.mapped('qty_done')), [2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0')
self.assertEqual(m_p1.quantity_done, 5.0, 'Total qty done should be 6.0')
self.assertEqual(sum(ml_p1.mapped('product_uom_qty')), 5.0, 'Total qty reserved should be 5.0')
mo.button_mark_done()
@@ -636,7 +576,7 @@ class TestMrpOrder(TestMrpCommon):
def test_product_produce_6(self):
""" Plan 5 finished products, reserve and produce 3. Post the current production.
Simulate an unlock and edit and, on the opened moves, set the consumed quantity
to 3. Now, try to update the quantity to produce to 3. It should fail since there
to 3. Now, try to update the quantity to mo2 to 3. It should fail since there
are consumed quantities. Unlock and edit, remove the consumed quantities and
update the quantity to produce to 3."""
self.stock_location = self.env.ref('stock.stock_location_stock')
@@ -648,13 +588,9 @@ class TestMrpOrder(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 3
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 3
mo = mo_form.save()
mo.post_inventory()
self.assertEqual(len(mo.move_raw_ids), 4)
@@ -675,127 +611,11 @@ class TestMrpOrder(TestMrpCommon):
self.assertTrue(all(s == 'done' for s in mo.move_raw_ids.mapped('state')))
self.assertEqual(sum(mo.move_raw_ids.mapped('move_line_ids.product_uom_qty')), 0)
def test_product_produce_7(self):
""" Add components in 2 different sub location. Do not reserve the MO
and checks that the move line created takes stock from location that
contains needed raw materials.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2)
self.assertEqual(len(mo), 1, 'MO should have been created')
self.stock_location = self.env.ref('stock.stock_location_stock')
self.stock_shelf_1 = self.stock_location_components
self.stock_shelf_2 = self.stock_location_14
self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 3)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 3)
self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 2)
self.env['stock.quant']._update_available_quantity(p2, self.stock_shelf_1, 1)
self.env['stock.quant']._update_available_quantity(p2, self.stock_shelf_2, 1)
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2)
produce_wizard.do_produce()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2)
produce_wizard.do_produce()
mo.button_mark_done()
mo_move_line_p1 = mo.move_raw_ids[1].move_line_ids
self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_location).mapped('qty_done')), 3)
self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_shelf_1).mapped('qty_done')), 3)
self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_shelf_2).mapped('qty_done')), 2)
self.assertEqual(sum(mo.move_finished_ids.move_line_ids.mapped('qty_done')), 2)
self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_location, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_shelf_1, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_shelf_2, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_shelf_1, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_shelf_2, strict=True).quantity, 0)
self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 2)
def test_product_produce_8(self):
""" Produce more than reserved and planned. Check that produce wizard
only propose one line for product not reserved.
"""
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2)
self.assertEqual(len(mo), 1, 'MO should have been created')
self.stock_location = self.env.ref('stock.stock_location_stock')
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 5)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1).qty_reserved, 4)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p2).qty_reserved, 1)
produce_wizard.do_produce()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
produce_wizard = produce_form.save()
# p1 1 1 1
# p1 3 0 3
# p2 1 1 1
self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 3)
self.assertEqual(sum(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1).mapped('qty_reserved')), 1)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and l.qty_reserved).qty_to_consume, 1)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and not l.qty_reserved).qty_to_consume, 3)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p2).qty_reserved, 1)
with Form(produce_wizard) as produce_form:
produce_form.qty_producing = 2
# p1 1 1 1
# p1 7 0 7
# p2 1 1 1
# p2 1 0 1
self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 4)
self.assertEqual(sum(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1).mapped('qty_reserved')), 1)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and l.qty_reserved).qty_to_consume, 1)
self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and not l.qty_reserved).qty_to_consume, 7)
self.assertEqual(sum(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p2).mapped('qty_reserved')), 1)
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_location, strict=True).quantity, -7)
self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_location, strict=True).quantity, -1)
self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 3)
def test_product_produce_9(self):
""" Checks the constraints of a strict BOM without tracking when playing around in the
produce wizard.
"""
""" Checks the constraints of a strict BOM without tracking when playing around
quantities to consume."""
self.stock_location = self.env.ref('stock.stock_location_stock')
mo, bom, p_final, p1, p2 = self.generate_mo()
mo, bom, p_final, p1, p2 = self.generate_mo(consumption='strict')
self.assertEqual(len(mo), 1, 'MO should have been created')
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
@@ -803,52 +623,22 @@ class TestMrpOrder(TestMrpCommon):
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
mo_form = Form(mo.with_context({'debug': True}))
with self.assertRaises(UserError):
# try adding another line for a bom product to increase the quantity
produce_form.qty_producing = 1
with produce_form.raw_workorder_line_ids.new() as line:
line.product_id = p1
line.qty_done = 1
product_produce = produce_form.save()
product_produce.do_produce()
mo_form.qty_producing = 1
with mo_form.move_raw_ids.new() as move:
move.product_id = p1
move.quantity_done = 1
mo = mo_form.save()
mo.button_mark_done()
with self.assertRaises(UserError):
# Try updating qty_done
product_produce = produce_form.save()
product_produce.raw_workorder_line_ids[0].qty_done += 1
product_produce.do_produce()
with self.assertRaises(UserError):
# try adding another product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
with produce_form.raw_workorder_line_ids.new() as line:
line.product_id = self.product_4
line.qty_done = 1
product_produce = produce_form.save()
product_produce.do_produce()
# try adding another line for a bom product but the total quantity is good
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
with produce_form.raw_workorder_line_ids.new() as line:
line.product_id = p1
line.qty_done = 1
product_produce = produce_form.save()
product_produce.raw_workorder_line_ids[1].qty_done -= 1
product_produce.do_produce()
mo = mo_form.save()
mo.move_raw_ids[0].move_line_ids[0].qty_done += 1
mo.button_mark_done()
def test_product_produce_10(self):
""" Produce byproduct with serial, lot and not tracked.
@@ -1027,43 +817,39 @@ class TestMrpOrder(TestMrpCommon):
mo.bom_id.consumption = 'flexible' # Because we'll over-consume with a product not defined in the BOM
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 3
self.assertEqual(len(produce_form.raw_workorder_line_ids._records), 4, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_done'] for x in produce_form.raw_workorder_line_ids._records]), 15, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_reserved'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
produce_form.qty_producing = 4
self.assertEqual(len(produce_form.raw_workorder_line_ids._records), 4, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_done'] for x in produce_form.raw_workorder_line_ids._records]), 20, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_reserved'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
produce_form.qty_producing = 1
self.assertEqual(len(produce_form.raw_workorder_line_ids._records), 2, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_done'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['qty_reserved'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
mo_form = Form(mo.with_context({'debug': True}))
mo_form.qty_producing = 3
self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 15, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
mo_form.qty_producing = 4
self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 20, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
mo_form.qty_producing = 1
self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should change the components quantity.')
self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
# try adding another product that doesn't belong to the BoM
with produce_form.raw_workorder_line_ids.new() as line:
line.product_id = self.product_4
line.qty_done = 1
produce_wizard = produce_form.save()
produce_wizard.do_produce()
with mo_form.move_raw_ids.new() as move:
move.product_id = self.product_4
move.quantity_done = 10
# Check that this new product is not updated by qty_producing
mo_form.qty_producing = 2
for move in mo_form.move_raw_ids._records:
if move['product_id'] == self.product_4.id:
self.assertEqual(move['quantity_done'], 10)
break
mo = mo_form.save()
mo.button_mark_done()
def test_product_produce_duplicate_1(self):
""" produce a finished product tracked by serial number 2 times with the
same SN. Check that an error is raised the second time"""
mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=1, qty_base_1=1,)
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo1.id,
'active_ids': [mo1.id],
}))
product_produce = produce_form.save()
product_produce.action_generate_serial()
sn = product_produce.finished_lot_id
product_produce.do_produce()
mo_form = Form(mo1)
mo_form.qty_producing = 1
mo1 = mo_form.save()
mo1.action_generate_serial()
sn = mo1.lot_producing_id
mo1.button_mark_done()
mo_form = Form(self.env['mrp.production'])
@@ -1073,14 +859,11 @@ class TestMrpOrder(TestMrpCommon):
mo2 = mo_form.save()
mo2.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
produce_form.finished_lot_id = sn
product_produce = produce_form.save()
mo_form = Form(mo2)
mo_form.lot_producing_id = sn
mo2 = mo_form.save()
with self.assertRaises(UserError):
product_produce.do_produce()
mo2.button_mark_done()
def test_product_produce_duplicate_2(self):
""" produce a finished product with component tracked by serial number 2
@@ -1091,14 +874,13 @@ class TestMrpOrder(TestMrpCommon):
'product_id': p2.id,
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo1.id,
'active_ids': [mo1.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
product_produce.do_produce()
mo_form = Form(mo1)
mo_form.qty_producing = 1
mo1 = mo_form.save()
details_operation_form = Form(mo1.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = sn
details_operation_form.save()
mo1.button_mark_done()
mo_form = Form(self.env['mrp.production'])
@@ -1108,15 +890,15 @@ class TestMrpOrder(TestMrpCommon):
mo2 = mo_form.save()
mo2.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
mo_form = Form(mo2)
mo_form.qty_producing = 1
mo2 = mo_form.save()
details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = sn
details_operation_form.save()
with self.assertRaises(UserError):
product_produce.do_produce()
mo2.button_mark_done()
def test_product_produce_duplicate_3(self):
""" produce a finished product with by-product tracked by serial number 2
@@ -1148,15 +930,15 @@ class TestMrpOrder(TestMrpCommon):
'product_id': byproduct.id,
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
with produce_form.finished_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
product_produce.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
move_byproduct = mo.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id)
details_operation_form = Form(move_byproduct, view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = sn
details_operation_form.save()
mo.button_mark_done()
mo_form = Form(self.env['mrp.production'])
@@ -1166,15 +948,16 @@ class TestMrpOrder(TestMrpCommon):
mo2 = mo_form.save()
mo2.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
with produce_form.finished_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
mo_form = Form(mo2)
mo_form.qty_producing = 1
mo2 = mo_form.save()
move_byproduct = mo2.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id)
details_operation_form = Form(move_byproduct, view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = sn
details_operation_form.save()
with self.assertRaises(UserError):
product_produce.do_produce()
mo2.button_mark_done()
def test_product_produce_duplicate_4(self):
""" Consuming the same serial number two times should not give an error if
@@ -1185,14 +968,13 @@ class TestMrpOrder(TestMrpCommon):
'product_id': p2.id,
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo1.id,
'active_ids': [mo1.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
product_produce.do_produce()
mo_form = Form(mo1)
mo_form.qty_producing = 1
mo1 = mo_form.save()
details_operation_form = Form(mo1.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = sn
details_operation_form.save()
mo1.button_mark_done()
unbuild_form = Form(self.env['mrp.unbuild'])
@@ -1210,15 +992,15 @@ class TestMrpOrder(TestMrpCommon):
mo2 = mo_form.save()
mo2.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo2.id,
'active_ids': [mo2.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = sn
product_produce = produce_form.save()
product_produce.do_produce()
mo_form = Form(mo2)
mo_form.qty_producing = 1
mo2 = mo_form.save()
details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = sn
details_operation_form.save()
mo2.button_mark_done()
def test_product_produce_uom(self):
""" Produce a finished product tracked by serial number. Set another
UoM on the bom. The produce wizard should keep the UoM of the product (unit)
@@ -1268,86 +1050,20 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(mo.move_raw_ids.product_qty, 12, '12 units should be reserved.')
# produce product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.finished_lot_id = final_product_lot
product_produce = produce_form.save()
self.assertEqual(product_produce.qty_producing, 1)
self.assertEqual(product_produce.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.')
product_produce.finished_lot_id = final_product_lot.id
mo_form = Form(mo)
mo_form.qty_producing = 1/12.0
mo_form.lot_producing_id = final_product_lot
mo = mo_form.save()
product_produce.do_produce()
move_line_raw = mo.move_raw_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done)
self.assertEqual(move_line_raw.qty_done, 1)
self.assertEqual(move_line_raw.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.')
mo._post_inventory()
move_line_finished = mo.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done)
self.assertEqual(move_line_finished.qty_done, 1)
self.assertEqual(move_line_finished.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.')
def test_product_produce_uom_2(self):
""" Create a bom with a serial tracked component and a pair UoM (2 x unit).
The produce wizard should create 2 line with quantity = 1 and UoM = unit for
this component. """
unit = self.env.ref("uom.product_uom_unit")
categ_unit_id = self.env.ref('uom.product_uom_categ_unit')
paire = self.env['uom.uom'].create({
'name': 'Paire',
'factor_inv': 2,
'uom_type': 'bigger',
'rounding': 0.001,
'category_id': categ_unit_id.id
})
binocular = self.env['product.product'].create({
'name': 'Binocular',
'type': 'product',
'uom_id': unit.id,
'uom_po_id': unit.id
})
nocular = self.env['product.product'].create({
'name': 'Nocular',
'type': 'product',
'tracking': 'serial',
'uom_id': unit.id,
'uom_po_id': unit.id
})
bom_binocular = self.env['mrp.bom'].create({
'product_tmpl_id': binocular.product_tmpl_id.id,
'product_qty': 1,
'product_uom_id': unit.id,
'bom_line_ids': [(0, 0, {
'product_id': nocular.id,
'product_qty': 1,
'product_uom_id': paire.id
})]
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = binocular
mo_form.bom_id = bom_binocular
mo_form.product_uom_id = unit
mo_form.product_qty = 1
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.move_raw_ids.product_uom_qty, 1, 'Quantity should be 1.')
self.assertEqual(mo.move_raw_ids.product_uom, paire, 'Move UoM should be "Paire".')
# produce product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
product_produce = produce_form.save()
self.assertEqual(product_produce.qty_producing, 1)
self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'Should be 2 lines since the component tracking is serial and quantity 2.')
self.assertEqual(product_produce.raw_workorder_line_ids[0].qty_to_consume, 1, 'Should be 1 unit since the tracking is serial and quantity 2.')
self.assertEqual(product_produce.raw_workorder_line_ids[0].product_uom_id, unit, 'Should be the product uom so "unit"')
self.assertEqual(product_produce.raw_workorder_line_ids[1].qty_to_consume, 1, 'Should be 1 unit since the tracking is serial and quantity 2.')
self.assertEqual(product_produce.raw_workorder_line_ids[1].product_uom_id, unit, 'should be the product uom so "unit"')
def test_product_type_service_1(self):
# Create finished product
finished_product = self.env['product.product'].create({
+12 -29
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', 'Production order should be for Confirmed state')
self.assertEqual(production_product_6.state, 'confirmed')
# Check procurement for product 4 created or not.
# Check it created a purchase order
@@ -69,14 +69,9 @@ class TestProcurement(TestMrpCommon):
# produce product4
# ---------------
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': produce_product_4.id,
'active_ids': [produce_product_4.id],
}))
produce_form.qty_producing = produce_product_4.product_qty
product_produce = produce_form.save()
product_produce.do_produce()
produce_product_4.post_inventory()
mo_form = Form(produce_product_4)
mo_form.qty_producing = produce_product_4.product_qty
produce_product_4 = mo_form.save()
# Check procurement and Production state for product 4.
produce_product_4.button_mark_done()
self.assertEqual(produce_product_4.state, 'done', 'Production order should be in state done')
@@ -95,14 +90,9 @@ class TestProcurement(TestMrpCommon):
# ------------------------------------
self.assertEqual(production_product_6.reservation_state, 'assigned', "Consume material not available")
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production_product_6.id,
'active_ids': [production_product_6.id],
}))
produce_form.qty_producing = production_product_6.product_qty
product_produce = produce_form.save()
product_produce.do_produce()
production_product_6.post_inventory()
mo_form = Form(production_product_6)
mo_form.qty_producing = production_product_6.product_qty
production_product_6 = mo_form.save()
# Check procurement and Production state for product 6.
production_product_6.button_mark_done()
self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done')
@@ -191,13 +181,9 @@ class TestProcurement(TestMrpCommon):
self.assertEqual(picking_qc_to_stock.state, 'done')
mo.action_assign()
self.assertEqual(mo.move_raw_ids.reserved_availability, 3.0)
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form = Form(mo)
produce_form.qty_producing = 3.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo = produce_form.save()
self.assertEqual(mo.move_raw_ids.quantity_done, 3.0)
picking_qc_to_stock.move_line_ids.qty_done = 5.0
self.assertEqual(mo.move_raw_ids.reserved_availability, 5.0)
@@ -419,12 +405,9 @@ class TestProcurement(TestMrpCommon):
self.env['stock.move'].create(move_values)
production.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production.id,
'active_ids': [production.id],
}))
product_produce = produce_form.save()
product_produce.do_produce()
produce_form = Form(production)
produce_form.qty_producing = production.product_qty
production = produce_form.save()
production.button_mark_done()
move_dest._action_assign()
+4 -8
View File
@@ -207,14 +207,10 @@ class TestWarehouse(common.TestMrpCommon):
mo_laptop = self.new_mo_laptop()
serial = self.env['stock.production.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
product_produce = self.env['mrp.product.produce'].with_context({
'active_id': mo_laptop.id,
'active_ids': [mo_laptop.id],
}).create({
"qty_producing": 1.0,
"finished_lot_id": serial.id,
})
product_produce.do_produce()
mo_form = Form(mo_laptop)
mo_form.qty_producing = 1
mo_form.lot_producing_id = serial
mo_laptop = mo_form.save()
mo_laptop.button_mark_done()
# We check if the laptop go in the depot and not in the stock
+91 -48
View File
@@ -63,19 +63,22 @@ class TestTraceability(TestMrpCommon):
mo.action_assign()
# Start MO production
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
if finished_product.tracking != 'serial':
produce_form.qty_producing = 1
mo_form = Form(mo)
mo_form.qty_producing = 1
if finished_product.tracking != 'none':
produce_form.finished_lot_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
produce_wizard = produce_form.save()
mo_form.lot_producing_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id})
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 1
details_operation_form.save()
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@@ -160,41 +163,56 @@ class TestTraceability(TestMrpCommon):
mo = mo_form.save()
mo.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.finished_lot_id = self.env['stock.production.lot'].create({
mo_form = Form(mo)
mo_form.lot_producing_id = self.env['stock.production.lot'].create({
'product_id': product_final.id,
'name': 'Final_lot_1',
'company_id': self.env.company.id,
})
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = self.env['stock.production.lot'].create({
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': product_1.id,
'name': 'Raw_1_lot_1',
'company_id': self.env.company.id,
})
with produce_form.raw_workorder_line_ids.edit(1) as line:
line.lot_id = self.env['stock.production.lot'].create({
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': product_2.id,
'name': 'Raw_2_lot_1',
'company_id': self.env.company.id,
})
with produce_form.finished_workorder_line_ids.edit(0) as line:
line.lot_id = self.env['stock.production.lot'].create({
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': byproduct_1.id,
'name': 'Byproduct_1_lot_1',
'company_id': self.env.company.id,
})
with produce_form.finished_workorder_line_ids.edit(1) as line:
line.lot_id = self.env['stock.production.lot'].create({
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': byproduct_2.id,
'name': 'Byproduct_2_lot_1',
'company_id': self.env.company.id,
})
produce_wizard = produce_form.save()
produce_wizard.continue_production()
ml.qty_done = 1
details_operation_form.save()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
@@ -206,59 +224,84 @@ class TestTraceability(TestMrpCommon):
'name': 'Final_lot_2',
'company_id': self.env.company.id,
})
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = self.env['stock.production.lot'].create({
mo_form.qty_producing = 1
mo_backorder = mo_form.save()
details_operation_form = Form(
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': product_1.id,
'name': 'Raw_1_lot_2',
'company_id': self.env.company.id,
})
with produce_form.raw_workorder_line_ids.edit(1) as line:
line.lot_id = self.env['stock.production.lot'].create({
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': product_2.id,
'name': 'Raw_2_lot_2',
'company_id': self.env.company.id,
})
with produce_form.finished_workorder_line_ids.edit(0) as line:
line.lot_id = self.env['stock.production.lot'].create({
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': byproduct_1.id,
'name': 'Byproduct_1_lot_2',
'company_id': self.env.company.id,
})
with produce_form.finished_workorder_line_ids.edit(1) as line:
line.lot_id = self.env['stock.production.lot'].create({
ml.qty_done = 1
details_operation_form.save()
details_operation_form = Form(
mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2),
view=self.env.ref('stock.view_stock_move_operations')
)
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = self.env['stock.production.lot'].create({
'product_id': byproduct_2.id,
'name': 'Byproduct_2_lot_2',
'company_id': self.env.company.id,
})
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo.button_mark_done()
ml.qty_done = 1
details_operation_form.save()
self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
mo_backorder.button_mark_done()
# self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4)
# self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6)
mo = mo | mo_backorder
raw_move_lines = mo.move_raw_ids.mapped('move_line_ids')
raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1')
self.assertEqual(set(raw_line_raw_1_lot_1.lot_produced_ids.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
self.assertEqual(set(raw_line_raw_1_lot_2.lot_produced_ids.mapped('name')), set(['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2']))
self.assertEqual(set(raw_line_raw_1_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1')
self.assertEqual(set(raw_line_raw_2_lot_1.lot_produced_ids.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
self.assertEqual(set(raw_line_raw_2_lot_2.lot_produced_ids.mapped('name')), set(['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2']))
self.assertEqual(set(raw_line_raw_2_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1']))
finished_move_lines = mo.move_finished_ids.mapped('move_line_ids')
finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1')
self.assertEqual(finished_move_line_lot_1.consume_line_ids, raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2')
raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2')
raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2')
self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1')
self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids, raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
byproduct_move_line_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2')
self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1')
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids, raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1)
byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2')
self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2)
+67 -63
View File
@@ -5,6 +5,7 @@ from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.exceptions import UserError
class TestUnbuild(TestMrpCommon):
def setUp(self):
super(TestUnbuild, self).setUp()
@@ -26,14 +27,9 @@ class TestUnbuild(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 5.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@@ -97,15 +93,10 @@ class TestUnbuild(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 5.0
produce_form.finished_lot_id = lot
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.lot_producing_id = lot
mo = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@@ -182,14 +173,15 @@ class TestUnbuild(TestMrpCommon):
if ml.product_id.tracking != 'none':
self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.')
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 5.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
# FIXME sle: behavior change
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.lot_id = lot
ml.qty_done = 20
details_operation_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@@ -272,15 +264,19 @@ class TestUnbuild(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 5.0
produce_form.finished_lot_id = lot_final
produce_wizard = produce_form.save()
produce_wizard.do_produce()
# FIXME sle: behavior change
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo_form.lot_producing_id = lot_final
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 5
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 20
details_operation_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@@ -326,7 +322,7 @@ class TestUnbuild(TestMrpCommon):
x.save().action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 92 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock')
x = Form(self.env['mrp.unbuild'])
@@ -381,14 +377,18 @@ class TestUnbuild(TestMrpCommon):
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3)
mo.action_assign()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 5.0
produce_wizard = produce_form.save()
mo_form = Form(mo)
mo_form.qty_producing = 5.0
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids.filtered(lambda ml: ml.product_id == p2), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = ml.product_uom_qty
with details_operation_form.move_line_ids.edit(1) as ml:
ml.qty_done = ml.product_uom_qty
with details_operation_form.move_line_ids.edit(2) as ml:
ml.qty_done = ml.product_uom_qty
details_operation_form.save()
produce_wizard.do_produce()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
# Check quantity in stock before unbuild.
@@ -415,6 +415,9 @@ class TestUnbuild(TestMrpCommon):
""" This test produces an MO in two times and checks that the move lines are linked in a correct way
"""
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot')
# Young Tom
# \ Botox - 4 - p1
# \ Old Tom - 1 - p2
lot_1 = self.env['stock.production.lot'].create({
'name': 'lot_1',
'product_id': p2.id,
@@ -456,21 +459,26 @@ class TestUnbuild(TestMrpCommon):
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 2.0
produce_form.finished_lot_id = lot_finished_2
mo = mo.procurement_group_id.mrp_production_ids[1]
# FIXME sle: issue in backorder?
mo.move_raw_ids.move_line_ids.unlink()
self.assertEqual(mo.product_qty, 2)
mo_form = Form(mo)
mo_form.qty_producing = 2
mo_form.lot_producing_id = lot_finished_2
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.qty_done = 2
ml.lot_id = lot_2
details_operation_form.save()
action = mo.button_mark_done()
produce_wizard = produce_form.save()
produce_wizard._workorder_line_ids()[0].lot_id = lot_2
produce_wizard.do_produce()
mo.button_mark_done()
ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.lot_produced_ids)
self.assertEqual(ml[0].qty_done, 12.0, 'Should have consumed 12 for the first lot')
ml = mo.finished_move_line_ids[1].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.lot_produced_ids)
self.assertEqual(ml[0].qty_done, 8.0, 'Should have consumed 8 for the second lot')
mo1 = mo.procurement_group_id.mrp_production_ids[0]
ml = mo1.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.produce_line_ids.lot_id)
self.assertEqual(sum(ml.mapped('qty_done')), 12.0, 'Should have consumed 12 for the first lot')
ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.produce_line_ids.lot_id)
self.assertEqual(sum(ml.mapped('qty_done')), 8.0, 'Should have consumed 8 for the second lot')
def test_unbuild_with_routes(self):
""" This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock
@@ -544,13 +552,9 @@ class TestUnbuild(TestMrpCommon):
mo.action_assign()
# Produce the final product
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1.0
produce_wizard = mo_form.save()
mo.button_mark_done()
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
@@ -184,13 +184,9 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
self.assertEqual(production_order.reservation_state, 'assigned')
self.assertEqual(picking_stock_postprod.state, 'waiting')
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production_order.id,
'active_ids': [production_order.id],
}))
produce_form = Form(production_order)
produce_form.qty_producing = production_order.product_qty
product_produce = produce_form.save()
product_produce.do_produce()
production_order = produce_form.save()
production_order.button_mark_done()
self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity')))
+27 -160
View File
@@ -151,7 +151,6 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
production_table_form.product_qty = 1.0
production_table_form.product_uom_id = dining_table.uom_id
production_table = production_table_form.save()
production_table.action_confirm()
# Set tracking lot on finish and consume products.
dining_table.tracking = 'lot'
@@ -159,6 +158,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
product_table_leg.tracking = 'lot'
product_bolt.tracking = "lot"
production_table.action_confirm()
# Initial inventory of product sheet, lags and bolt
lot_sheet = self.env['stock.production.lot'].create({'product_id': product_table_sheet.id, 'company_id': self.env.company.id})
lot_leg = self.env['stock.production.lot'].create({'product_id': product_table_leg.id, 'company_id': self.env.company.id})
@@ -626,9 +626,6 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
# -----------------
# Produce 6 Unit of custom laptop will consume ( 12 Unit of keybord and 12 Unit of charger)
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
product_form = Form(self.env['mrp.product.produce'].with_context(context))
product_form.qty_producing = 6.00
laptop_lot_001 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id , 'company_id': self.env.company.id})
mo_form = Form(mo_custom_laptop)
mo_form.qty_producing = 6
@@ -651,11 +648,8 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
for move in mo_custom_laptop.move_raw_ids:
self.assertEqual(move.quantity_done, 12, "Wrong produced quantity on raw material %s" % (move.product_id.name))
self.assertEqual(len(mo_custom_laptop.move_raw_ids), 2)
mo_custom_laptop.post_inventory()
self.assertEqual(len(mo_custom_laptop.move_raw_ids), 4)
# Check done move and confirmed move quantity.
charger_done_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state == 'done')
keybord_done_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state == 'done')
self.assertEqual(charger_done_move.product_uom_qty, 12)
@@ -665,165 +659,26 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
# ----------------------------
# Produce 4 Unit of custom laptop will consume ( 8 Unit of keybord and 8 Unit of charger).
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
produce_form = Form(self.env['mrp.product.produce'].with_context(context))
produce_form.qty_producing = 4.00
laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id, 'company_id': self.env.company.id})
produce_form.finished_lot_id = laptop_lot_002
product_consume = produce_form.save()
self.assertEqual(len(product_consume._workorder_line_ids()), 2)
product_consume._workorder_line_ids()[0].qty_done = 8
product_consume.do_produce()
mo_custom_laptop = mo_custom_laptop.procurement_group_id.mrp_production_ids[1]
mo_form = Form(mo_custom_laptop)
mo_form.qty_producing = 4
mo_form.lot_producing_id = laptop_lot_002
mo_custom_laptop = mo_form.save()
details_operation_form = Form(mo_custom_laptop.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 8
details_operation_form.save()
details_operation_form = Form(mo_custom_laptop.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.qty_done = 8
details_operation_form.save()
charger_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state != 'done')
keybord_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state !='done')
self.assertEqual(charger_move.quantity_done, 8, "Wrong consumed quantity of %s" % charger_move.product_id.name)
self.assertEqual(keybord_move.quantity_done, 8, "Wrong consumed quantity of %s" % keybord_move.product_id.name)
# Post Inventory of production order.
mo_custom_laptop.post_inventory()
# raw_moves_state = any(move.state != 'done' for move in mo_custom_laptop.move_raw_ids)
# finsh_moves_state = any(move.state != 'done' for move in mo_custom_laptop.move_finished_ids)
# self.assertFalse(raw_moves_state, "Wrong state in consumed moves of production order.")
# self.assertFalse(finsh_moves_state, "Wrong state in consumed moves of production order.")
#
# # Finished move quants of production order
#
# finshed_quant_lot_001 = mo_custom_laptop.move_finished_ids.filtered(lambda x: x.product_id.id == custom_laptop.id and x.product_uom_qty==6).mapped('quant_ids')
# finshed_quant_lot_002 = mo_custom_laptop.move_finished_ids.filtered(lambda x: x.product_id.id == custom_laptop.id and x.product_uom_qty==4).mapped('quant_ids')
#
# # Check total quantity consumed of charger, keybord
# # --------------------------------------------------
# charger_quants = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state == 'done').mapped('quant_ids')
# keybord_moves = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state == 'done').mapped('quant_ids')
# self.assertEqual(sum(charger_quants.mapped('qty')), 20)
# self.assertEqual(sum(keybord_moves.mapped('qty')), 20)
def test_02_different_uom_on_bomlines(self):
""" Testing bill of material with different unit of measure."""
route_manufacture = self.warehouse.manufacture_pull_id.route_id.id
route_mto = self.warehouse.mto_pull_id.route_id.id
unit = self.ref("uom.product_uom_unit")
dozen = self.ref("uom.product_uom_dozen")
kg = self.ref("uom.product_uom_kgm")
gm = self.ref("uom.product_uom_gram")
# Create Product A, B, C
product_A = self.env['product.product'].create({
'name': 'Product A',
'type': 'product',
'tracking': 'lot',
'uom_id': dozen,
'uom_po_id': dozen,
'route_ids': [(6, 0, [route_manufacture, route_mto])]})
product_B = self.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'tracking': 'lot',
'uom_id': dozen,
'uom_po_id': dozen})
product_C = self.env['product.product'].create({
'name': 'Product C',
'type': 'product',
'tracking': 'lot',
'uom_id': kg,
'uom_po_id': kg})
# Bill of materials
# -----------------
#===================================
# Product A 1 Unit
# Product B 4 Unit
# Product C 600 gram
# -----------------------------------
bom_a = self.env['mrp.bom'].create({
'product_tmpl_id': product_A.product_tmpl_id.id,
'product_qty': 2,
'product_uom_id': unit,
'bom_line_ids': [(0, 0, {
'product_id': product_B.id,
'product_qty': 4,
'product_uom_id': unit
}), (0, 0, {
'product_id': product_C.id,
'product_qty': 600,
'product_uom_id': gm
})]
})
# Create production order with product A 10 Unit.
# -----------------------------------------------
mo_custom_product_form = Form(self.env['mrp.production'])
mo_custom_product_form.product_id = product_A
mo_custom_product_form.bom_id = bom_a
mo_custom_product_form.product_qty = 10.0
mo_custom_product_form.product_uom_id = self.env.ref("uom.product_uom_unit")
mo_custom_product = mo_custom_product_form.save()
move_product_b = mo_custom_product.move_raw_ids.filtered(lambda x: x.product_id == product_B)
move_product_c = mo_custom_product.move_raw_ids.filtered(lambda x: x.product_id == product_C)
# Check move correctly created or not.
self.assertEqual(move_product_b.product_uom_qty, 20)
self.assertEqual(move_product_b.product_uom.id, unit)
self.assertEqual(move_product_c.product_uom_qty, 3000)
self.assertEqual(move_product_c.product_uom.id, gm)
# Lot create for product B and product C
# ---------------------------------------
lot_a = self.env['stock.production.lot'].create({'product_id': product_A.id, 'company_id': self.env.company.id})
lot_b = self.env['stock.production.lot'].create({'product_id': product_B.id, 'company_id': self.env.company.id})
lot_c = self.env['stock.production.lot'].create({'product_id': product_C.id, 'company_id': self.env.company.id})
# Inventory Update
# ----------------
inventory = self.env['stock.inventory'].create({
'name': 'Inventory Product B and C',
'line_ids': [(0, 0, {
'product_id': product_B.id,
'product_uom_id': product_B.uom_id.id,
'product_qty': 3,
'prod_lot_id': lot_b.id,
'location_id': self.source_location_id
}), (0, 0, {
'product_id': product_C.id,
'product_uom_id': product_C.uom_id.id,
'product_qty': 3,
'prod_lot_id': lot_c.id,
'location_id': self.source_location_id
})]
})
inventory.action_start()
inventory.action_validate()
# Start Production ...
# --------------------
mo_custom_product.action_confirm()
mo_custom_product.action_assign()
context = {"active_ids": [mo_custom_product.id], "active_id": mo_custom_product.id}
produce_form = Form(self.env['mrp.product.produce'].with_context(context))
produce_form.qty_producing = 10.00
produce_form.finished_lot_id = lot_a
product_consume = produce_form.save()
# laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
self.assertEqual(len(product_consume._workorder_line_ids()), 2)
product_consume._workorder_line_ids().filtered(lambda x: x.product_id == product_C).write({'qty_done': 3000})
product_consume._workorder_line_ids().filtered(lambda x: x.product_id == product_B).write({'qty_done': 20})
product_consume.do_produce()
mo_custom_product.post_inventory()
# Check correct quant linked with move or not
# -------------------------------------------
#TODO: check original quants qtys diminished
# self.assertEqual(len(move_product_b.quant_ids), 1)
# self.assertEqual(len(move_product_c.quant_ids), 1)
# self.assertEqual(move_product_b.quant_ids.qty, move_product_b.product_qty)
# self.assertEqual(move_product_c.quant_ids.qty, 3)
# self.assertEqual(move_product_c.quant_ids.product_uom_id.id, kg)
def test_03_test_serial_number_defaults(self):
""" Test that the correct serial number is suggested on consecutive work orders. """
laptop = self.laptop
@@ -1311,6 +1166,18 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
the reservation slot in the calendar the be able to reserve the next
production sooner """
self.env['mrp.workcenter'].search([]).write({'tz': 'UTC'}) # compute all date in UTC
mrp_workcenter_3 = self.env['mrp.workcenter'].create({
'name': 'assembly line 1',
'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
})
self.planning_bom.operation_ids = False
self.planning_bom.write({
'operation_ids': [(0, 0, {
'workcenter_id': mrp_workcenter_3.id,
'name': 'Manual Assembly',
'time_cycle': 60,
})]
})
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = self.product_4
mo_form.bom_id = self.planning_bom
+123 -28
View File
@@ -14,6 +14,7 @@
<field name="date_deadline" widget="remaining_days" attrs="{'invisible': [('state', 'in', ['done', 'cancel'])]}" optional="hide"/>
<field name="product_id" readonly="1" optional="show"/>
<field name="product_uom_id" string="Unit of Measure" options="{'no_open':True,'no_create':True}" groups="uom.group_uom" optional="show"/>
<field name="lot_producing_id" optional="hide"/>
<field name="bom_id" readonly="1" optional="hide"/>
<field name="origin" optional="show"/>
<field name="user_id" optional="hide" widget="many2one_avatar_user"/>
@@ -52,8 +53,6 @@
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
<button name="button_plan" attrs="{'invisible': ['|', '|', ('state', 'not in', ('confirmed', 'progress')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" type="object" string="Plan" class="oe_highlight"/>
<button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start', '=', False), ('date_planned_finished', '=', False)]}"/>
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', '!=', 'assigned'), ('bom_has_operations', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/>
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', 'not in', ('confirmed', 'waiting')), ('bom_has_operations', '!=', False)]}" string="Produce" type="object"/>
<button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}"/>
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/>
<button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/>
@@ -120,20 +119,27 @@
<group>
<field name="id" invisible="1"/>
<field name="allowed_product_ids" invisible="1"/>
<field name="product_tracking" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="product_tmpl_id" invisible="1"/>
<field name="product_description_variants" attrs="{'invisible': [('product_description_variants', '=', False)], 'readonly': [('state', '!=', 'draft')]}"/>
<label for="product_qty"/>
<field name="product_description_variants" attrs="{'invisible': [('product_description_variants', 'in', (False, ''))], 'readonly': [('state', '!=', 'draft')]}"/>
<label for="product_qty" string="Quantity"/>
<div class="o_row no-gutters d-flex">
<div class="col">
<field name="product_qty" class="mr-1" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<button type="action"
name="%(mrp.action_change_production_qty)d"
context="{'default_mo_id': id}"
string="Update" class="oe_link pt-0" attrs="{'invisible': ['|', '|', ('state', 'in', ('draft', 'done','cancel')), ('id', '=', False), ('bom_id', '=', False)]}"/>
<div attrs="{'invisible': [('state', '=', 'draft')]}" class="o_row">
<field name="qty_producing" digits="[12,3]" class="text-left"/>
/
</div>
<field name="product_qty" class="oe_inline text-left" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<label for="product_uom_id" string="" class="oe_inline"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" class="oe_inline"/>
<span class='text-bf'>To Produce</span>
</div>
<label for="lot_producing_id" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"/>
<div class="o_row">
<field name="lot_producing_id"
context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"/>
<button name="action_generate_serial" type="object" class="btn btn-primary fa fa-plus-square-o" aria-label="Creates a new serial/lot number" title="Creates a new serial/lot number" role="img" attrs="{'invisible': ['|', ('product_tracking', 'in', ('none', False)), ('lot_producing_id', '!=', False)]}"/>
</div>
<field name="bom_id"
context="{'default_product_tmpl_id': product_tmpl_id}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
@@ -193,26 +199,115 @@
</group>
<notebook>
<page string="Components" name="components">
<field name="move_raw_ids" context="{'final_lots': show_final_lots, 'tree_view_ref': 'mrp.view_stock_move_raw_tree', 'form_view_ref': 'mrp.view_stock_move_lots', 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id}" attrs="{'readonly': ['&amp;', '&amp;', ('state', '!=', 'draft'), ('is_locked', '=', True), '|', ('state', '!=', 'confirmed'), ('consumption', '=', 'strict')]}"/>
</page>
<page string="Finished Products" name="finished_products">
<field name="move_finished_ids" invisible="1"/>
<field name="finished_move_line_ids" context="{'form_view_ref': 'mrp.view_finisehd_move_line'}" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('finished_move_line_ids', '=', [])]}">
<tree default_order="done_move" editable="bottom" create="0" delete="0" decoration-muted="state in ('done', 'cancel')">
<field name="product_id" readonly="1"/>
<<<<<<< HEAD
<field name="move_raw_ids" context="{'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id, 'default_company_id': company_id}" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}">
=======
<field name="move_raw_ids"
context="{'default_date_expected': date_planned_start, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id, 'default_company_id': company_id}"
attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}">
>>>>>>> dd9359b6fbe... fixup! [REF] mrp: no more produce wizard
<tree delete="0" default_order="is_done,sequence" editable="bottom">
<field name="product_id" required="1" attrs="{'readonly': ['|', '|', ('has_move_lines', '=', True), ('state', '=', 'cancel'), '&amp;', ('state', '!=', 'draft'), ('additional', '=', False) ]}"/>
<field name="move_line_ids" invisible="1">
<tree>
<field name="lot_id" invisible="1"/>
<field name="owner_id" invisible="1"/>
<field name="package_id" invisible="1"/>
<field name="result_package_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="qty_done" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="product_uom_id" invisible="1"/>
<field name="product_uom_qty" invisible="1"/>
<field name="state" invisible="1"/>
<field name="move_id" invisible="1"/>
<field name="id" invisible="1"/>
</tree>
</field>
<field name="company_id" invisible="1"/>
<field name="lot_id" groups="stock.group_production_lot" context="{'default_product_id': product_id}" attrs="{'invisible': [('lots_visible', '=', False)]}"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="qty_done" string="Produced"/>
<field name="lots_visible" invisible="1"/>
<field name="done_move" invisible="1"/>
<field name="state" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="name" invisible="1"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="additional" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="is_done" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
<field name="state" invisible="1" force_save="1"/>
<field name="should_consume_qty" invisible="1"/>
<field name="product_uom_qty" widget="mrp_should_consume" string="To Consume" attrs="{'readonly': ['&amp;', ('parent.state', '!=', 'draft'), '|', ('parent.state', 'not in', ('confirmed', 'planned', 'progress', 'to_close')), ('parent.is_locked', '=', True)]}" width="1"/>
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved" decoration-danger="not is_done and reserved_availability &lt; product_uom_qty and product_uom_qty - reserved_availability &gt; 0.0001"/>
<field name="is_quantity_done_editable" invisible="1"/>
<field name="quantity_done" string="Consumed"
decoration-success="not is_done and (quantity_done - should_consume_qty == 0)"
decoration-warning="not is_done and (quantity_done - should_consume_qty &gt; 0.0001)"
attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_quantity_done_editable', '=', False)]}"/>
<field name="show_details_visible" invisible="1"/>
<button name="action_show_details" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{&quot;warn&quot;: true}"/>
</tree>
</field>
</page>
<page string="By-Products" name="finished_products" groups="mrp.group_mrp_byproducts">
<field name="move_byproduct_ids" context="{'default_location_id': production_location_id, 'default_location_dest_id': location_src_id, 'default_state': 'draft', 'default_production_id': id, 'default_picking_type_id': picking_type_id, 'default_company_id': company_id}" attrs="{'readonly': [('state', '=', 'done'), ('is_locked', '=', True)]}">
<tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" editable="bottom">
<field name="product_id" domain="[('id', '!=', parent.product_id)]" required="1"/>
<field name="move_line_ids" invisible="1">
<tree>
<field name="lot_id" invisible="1"/>
<field name="owner_id" invisible="1"/>
<field name="package_id" invisible="1"/>
<field name="result_package_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="qty_done" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="product_uom_id" invisible="1"/>
<field name="product_uom_qty" invisible="1"/>
<field name="state" invisible="1"/>
<field name="move_id" invisible="1"/>
<field name="id" invisible="1"/>
</tree>
</field>
<field name="company_id" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="name" invisible="1"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="additional" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="is_done" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
<field name="state" invisible="1" force_save="1"/>
<field name="product_uom_qty" string="To Produce" attrs="{'readonly': ['&amp;', ('parent.state', '!=', 'draft'), '|', ('parent.state', 'not in', ('confirmed', 'planned', 'progress', 'to_close')), ('parent.is_locked', '=', True)]}"/>
<field name="is_quantity_done_editable" invisible="1"/>
<field name="quantity_done" string="Produced" attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_quantity_done_editable', '=', False)]}"/>
<field name="show_details_visible" invisible="1"/>
<button name="action_show_details" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{&quot;warn&quot;: true}"/>
</tree>
</field>
<p attrs="{'invisible': [('finished_move_line_ids', '!=', [])]}">
Use the Produce button or process the work orders to create some finished products.
</p>
</page>
<page string="Miscellaneous" name="miscellaneous" groups="stock.group_stock_multi_locations">
<group>
+1
View File
@@ -12,6 +12,7 @@
<script type="text/javascript" src="/mrp/static/src/js/mrp_documents_kanban_record.js"></script>
<script type="text/javascript" src="/mrp/static/src/js/mrp_documents_kanban_renderer.js"></script>
<script type="text/javascript" src="/mrp/static/src/js/mrp_document_kanban_view.js"></script>
<script type="text/javascript" src="/mrp/static/src/js/mrp_should_consume.js"></script>
</xpath>
</template>
+22 -129
View File
@@ -8,142 +8,35 @@
<field name="domain">['|', ('move_id.raw_material_production_id', '=', active_id), ('move_id.production_id', '=', active_id)]</field>
</record>
<record id="view_stock_move_lots" model="ir.ui.view">
<field name="name">stock.move.lots.form</field>
<record id="view_stock_move_operations_raw" model="ir.ui.view">
<field name="name">stock.move.operations.raw.form</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="priority">1</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="stock.view_stock_move_operations" />
<field name="arch" type="xml">
<form string="Lots">
<field name="state" invisible="1" force_save="1"/>
<group>
<group>
<field name="product_id" attrs="{'readonly': [('id', '!=', False)]}"/>
<label for="product_uom_qty"/>
<div class="o_row">
<span><field name="product_uom_qty" attrs="{'readonly': ['&amp;', ('parent.state', '!=', 'draft'), '|', ('parent.state', '!=', 'confirmed'), ('parent.consumption', '=', 'strict')]}" nolabel="1"/></span>
<span>
<field name="product_uom" attrs="{'readonly': [('id', '!=', False)]}" force_save="1" nolabel="1" groups="uom.group_uom"/>
</span>
</div>
<label for="quantity_done" attrs="{'invisible': ['|', ('parent.state', '=', 'draft'), ('id', '=', False)]}"/>
<div class="o_row" attrs="{'invisible': ['|', ('parent.state', '=', 'draft'), ('id', '=', False)]}">
<span><field name="quantity_done" attrs="{'readonly': ['|', '|', ('move_line_ids', '!=', False), ('is_locked', '=', True), '|', ('finished_lots_exist', '=', True), ('has_tracking', '!=', 'none')]}" nolabel="1"/></span>
<span> / </span>
<span><field name="reserved_availability" nolabel="1"/></span>
</div>
<field name="operation_id" attrs="{'invisible': [('id', '!=', False)]}" groups="mrp.group_mrp_routings"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="additional" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="is_done" invisible="1"/>
<field name="workorder_id" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="production_id" invisible="1"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="raw_material_production_id" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="name" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="order_finished_lot_ids" invisible="1"/>
<field name="finished_lots_exist" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
</group>
</group>
<field name="move_line_ids" attrs="{'invisible': ['|', ('parent.state', '=', 'draft'), ('id', '=', False)], 'readonly': ['|', ('is_locked', '=', True), ('state', '=', 'cancel')]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
<tree editable="bottom" decoration-success="product_uom_qty==qty_done" decoration-danger="(product_uom_qty &gt; 0) and (qty_done&gt;product_uom_qty)">
<field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" context="{'default_product_id': parent.product_id}"/>
<field name="lot_produced_ids" widget="many2many_tags" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/>
<field name="owner_id" groups="stock.group_tracking_owner" options="{'no_open': True, 'no_create': True}" optional="hide"/>
<field name="package_id" groups="stock.group_tracking_lot" options="{'no_open': True, 'no_create': True}" optional="hide"/>
<field name="product_uom_qty" string="Reserved" readonly="1" optional="show"/>
<field name="qty_done"/>
<field name="workorder_id" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="product_uom_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="production_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
</tree>
</field>
</form>
</field>
</record>
<record id="view_stock_move_raw_tree" model="ir.ui.view">
<field name="name">stock.move.raw.tree</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty &gt; 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty &lt; 0.0001" decoration-danger="not is_done and reserved_availability &lt; product_uom_qty and product_uom_qty - reserved_availability &gt; 0.0001">
<field name="product_id" required="1"/>
<field name="company_id" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="name" invisible="1"/>
<field name="unit_factor" invisible="1"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="date" invisible="1"/>
<field name="date_expected" invisible="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="operation_id" invisible="1"/>
<field name="needs_lots" readonly="1" groups="stock.group_production_lot"/>
<field name="is_done" invisible="1"/>
<field name="bom_line_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="warehouse_id" invisible="1"/>
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
<field name="state" invisible="1" force_save="1"/>
<field name="product_uom_qty" string="To Consume"/>
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved"/>
<field name="quantity_done" string="Consumed" attrs="{'column_invisible': [('parent.state', '=', 'draft')]}" readonly="1"/>
</tree>
</field>
</record>
<record id="view_move_kanban_inherit_mrp" model="ir.ui.view">
<field name="name">stock.move.kanban.inherit.mrp</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_kandan"/>
<field name="arch" type="xml">
<xpath expr="//templates" position="before">
<field name="bom_line_id"/>
<xpath expr="//label[@for='product_uom_qty']" position="attributes">
<attribute name="string">Total To Consume</attribute>
</xpath>
<xpath expr="//label[@for='quantity_done']" position="attributes">
<attribute name="string">Consumed</attribute>
</xpath>
</field>
</record>
<record id="view_finisehd_move_line" model="ir.ui.view">
<field name="name">mrp.finished.move.line.form</field>
<field name="priority">1000</field>
<field name="model">stock.move.line</field>
<record id="view_stock_move_operations_finished" model="ir.ui.view">
<field name="name">stock.move.operations.finished.form</field>
<field name="model">stock.move</field>
<field name="priority">1</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="stock.view_stock_move_operations" />
<field name="arch" type="xml">
<form string="Finished Product">
<group>
<group>
<field name="company_id" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="product_uom_category_id" readonly="1"/>
<label for="qty_done" string ="Quantity"/>
<div class="o_row">
<span><field name="qty_done" readonly="1" nolabel="1"/></span>
<span>/</span>
<span><field name="product_uom_qty" readonly="1" nolabel="1"/></span>
<span><field name="product_uom_id" attrs="{'readonly': [('id', '!=', False)]}" nolabel="1"/></span>
</div>
<field name="lot_id" string="Lot/Serial number" groups="stock.group_production_lot" readonly="1" attrs="{'invisible': [('lots_visible', '=', False)]}"/>
<field name="lots_visible" invisible="1"/>
</group>
</group>
</form>
<xpath expr="//label[@for='product_uom_qty']" position="attributes">
<attribute name="string">To Produce</attribute>
</xpath>
<xpath expr="//label[@for='quantity_done']" position="attributes">
<attribute name="string">Produced</attribute>
</xpath>
</field>
</record>
-1
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_product_produce
from . import change_production_qty
from . import stock_warn_insufficient_qty
from . import mrp_production_backorder
-154
View File
@@ -1,154 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_compare
class MrpProductProduce(models.TransientModel):
_name = "mrp.product.produce"
_description = "Record Production"
_inherit = ["mrp.abstract.workorder"]
@api.model
def default_get(self, fields):
res = super(MrpProductProduce, self).default_get(fields)
production = self.env['mrp.production']
production_id = self.env.context.get('default_production_id') or self.env.context.get('active_id')
if production_id:
production = self.env['mrp.production'].browse(production_id)
if production.exists():
serial_finished = (production.product_id.tracking == 'serial')
todo_uom = production.product_uom_id.id
todo_quantity = self._get_todo(production)
if serial_finished:
todo_quantity = 1.0
if production.product_uom_id.uom_type != 'reference':
todo_uom = self.env['uom.uom'].search([('category_id', '=', production.product_uom_id.category_id.id), ('uom_type', '=', 'reference')]).id
if 'production_id' in fields:
res['production_id'] = production.id
if 'product_id' in fields:
res['product_id'] = production.product_id.id
if 'product_uom_id' in fields:
res['product_uom_id'] = todo_uom
if 'serial' in fields:
res['serial'] = bool(serial_finished)
if 'qty_producing' in fields:
res['qty_producing'] = todo_quantity
if 'consumption' in fields:
res['consumption'] = production.bom_id.consumption if production.bom_id else 'flexible'
return res
serial = fields.Boolean('Requires Serial')
product_tracking = fields.Selection(related="product_id.tracking")
is_pending_production = fields.Boolean(compute='_compute_pending_production')
move_raw_ids = fields.One2many(related='production_id.move_raw_ids', string="PO Components")
move_finished_ids = fields.One2many(related='production_id.move_finished_ids')
raw_workorder_line_ids = fields.One2many('mrp.product.produce.line',
'raw_product_produce_id', string='Components')
finished_workorder_line_ids = fields.One2many('mrp.product.produce.line',
'finished_product_produce_id', string='By-products')
production_id = fields.Many2one('mrp.production', 'Manufacturing Order',
required=True, ondelete='cascade')
@api.depends('qty_producing')
def _compute_pending_production(self):
""" Compute if it exits remaining quantity once the quantity on the
current wizard will be processed. The purpose is to display or not
button 'continue'.
"""
for product_produce in self:
remaining_qty = product_produce._get_todo(product_produce.production_id)
product_produce.is_pending_production = remaining_qty - product_produce.qty_producing > 0.0
def continue_production(self):
""" Save current wizard and directly opens a new. """
self.ensure_one()
self._record_production()
action = self.production_id.open_produce_product()
action['context'] = {'default_production_id': self.production_id.id}
return action
def action_generate_serial(self):
self.ensure_one()
product_produce_wiz = self.env.ref('mrp.view_mrp_product_produce_wizard', False)
self.finished_lot_id = self.env['stock.production.lot'].create({
'product_id': self.product_id.id,
'company_id': self.production_id.company_id.id
})
return {
'name': _('Produce'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mrp.product.produce',
'res_id': self.id,
'view_id': product_produce_wiz.id,
'target': 'new',
}
def do_produce(self):
""" Save the current wizard and go back to the MO. """
self.ensure_one()
self._record_production()
self._check_company()
return {'type': 'ir.actions.act_window_close'}
def _get_todo(self, production):
""" This method will return remaining todo quantity of production. """
main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id)
todo_quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done'))
todo_quantity = todo_quantity if (todo_quantity > 0) else 0
return todo_quantity
def _record_production(self):
# Check all the product_produce line have a move id (the user can add product
# to consume directly in the wizard)
for line in self._workorder_line_ids():
line._check_line_sn_uniqueness()
# because of an ORM limitation (fields on transient models are not
# recomputed by updates in non-transient models), the related fields on
# this model are not recomputed by the creations above
self.invalidate_cache(['move_raw_ids', 'move_finished_ids'])
# Save product produce lines data into stock moves/move lines
quantity = self.qty_producing
if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_("The production order for '%s' has no quantity specified.") % self.product_id.display_name)
self._check_sn_uniqueness()
self._update_finished_move()
self._update_moves()
if self.production_id.state == 'confirmed':
self.production_id.write({
'date_start': datetime.now(),
})
class MrpProductProduceLine(models.TransientModel):
_name = 'mrp.product.produce.line'
_inherit = ["mrp.abstract.workorder.line"]
_description = "Record production line"
raw_product_produce_id = fields.Many2one('mrp.product.produce', 'Component in Produce wizard')
finished_product_produce_id = fields.Many2one('mrp.product.produce', 'Finished Product in Produce wizard')
@api.model
def _get_raw_workorder_inverse_name(self):
return 'raw_product_produce_id'
@api.model
def _get_finished_workoder_inverse_name(self):
return 'finished_product_produce_id'
def _get_final_lots(self):
product_produce_id = self.raw_product_produce_id or self.finished_product_produce_id
return product_produce_id.finished_lot_id | product_produce_id.finished_workorder_line_ids.mapped('lot_id')
def _get_production(self):
product_produce_id = self.raw_product_produce_id or self.finished_product_produce_id
return product_produce_id.production_id
@@ -1,134 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mrp_product_produce_wizard" model="ir.ui.view">
<field name="name">MRP Product Produce</field>
<field name="model">mrp.product.produce</field>
<field name="arch" type="xml">
<form string="Produce">
<group>
<group>
<field name="serial" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="production_id" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="is_pending_production" invisible="1"/>
<label for="qty_producing" string="Quantity"/>
<div class="o_row">
<field name="qty_producing" attrs="{'readonly': [('serial', '=', True)]}"/>
<field name="product_uom_id" readonly="1" groups="uom.group_uom"/>
</div>
<field name="product_tracking" invisible="1"/>
<label for="finished_lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')]}"/>
<div class="o_row">
<field name="finished_lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')], 'required': [('product_tracking', '!=', 'none'), ('finished_lot_id', '!=', False)]}" context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
<button name="action_generate_serial" type="object" class="btn btn-primary fa fa-plus-square-o" aria-label="Creates a new serial/lot number" title="Creates a new serial/lot number" role="img" attrs="{'invisible': ['|', ('product_tracking', '=', 'none'), ('finished_lot_id', '!=', False)]}"/>
</div>
</group>
</group>
<h4 attrs="{'invisible': [('raw_workorder_line_ids', '=', [])]}">Components</h4>
<group>
<field name="raw_workorder_line_ids" attrs="{'invisible': [('raw_workorder_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_finished_lot_id': finished_lot_id}">
<tree editable="bottom" delete="0" decoration-danger="(qty_to_consume &lt; qty_done)">
<field name="company_id" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('move_id', '!=', False)]}" required="1" domain="[('id', '!=', parent.product_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" force_save="1"/>
<field name="product_tracking" invisible="1"/>
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id, 'active_mo_id': parent.production_id, 'default_company_id': parent.company_id}" groups="stock.group_production_lot"/>
<field name="qty_to_consume" readonly="1" force_save="1"/>
<field name="qty_reserved" readonly="1" force_save="1" optional="show"/>
<field name="qty_done"/>
<field name="product_uom_id" readonly="1" force_save="1" groups="uom.group_uom"/>
<field name="move_id" invisible="1"/>
</tree>
</field>
</group>
<h4 attrs="{'invisible': [('finished_workorder_line_ids', '=', [])]}">By-products</h4>
<group>
<field name="finished_workorder_line_ids" attrs="{'invisible': [('finished_workorder_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_finished_lot_id': finished_lot_id}">
<tree editable="bottom" delete="0" decoration-danger="(qty_to_consume &lt; qty_done)">
<field name="company_id" invisible="1"/>
<field name="product_id" attrs="{'readonly': [('move_id', '!=', False)]}" required="1" domain="[('id', '!=', parent.product_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" force_save="1"/>
<field name="product_tracking" invisible="1"/>
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id, 'default_company_id': company_id}" groups="stock.group_production_lot"/>
<field name="qty_to_consume" string="To Produce" readonly="1" force_save="1"/>
<field name="qty_done" string="Produced"/>
<field name="product_uom_id" readonly="1" force_save="1" groups="uom.group_uom"/>
<field name="move_id" invisible="1"/>
</tree>
</field>
</group>
<footer>
<button name="continue_production" attrs="{'invisible':[('is_pending_production', '=', False)]}" type="object" string="Continue" class="btn-primary"/>
<button name="do_produce" type="object" string="Save" class="btn-default btn-secondary" attrs="{'invisible':[('is_pending_production', '=', False)]}"/>
<button name="do_produce" type="object" string="Save" class="btn-primary" attrs="{'invisible':[('is_pending_production', '=', True)]}"/>
<button string="Discard" class="btn-default btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="mrp_product_produce_line_form" model="ir.ui.view">
<field name="name">MRP Product Produce Line</field>
<field name="model">mrp.product.produce.line</field>
<field name="priority">100</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="company_id" invisible="1"/>
<field name="product_id"/>
<field name="product_tracking" invisible="1"/>
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
<field name="qty_to_consume" readonly="1" force_save="1"/>
<field name="qty_reserved" readonly="1" force_save="1" optional="show"/>
<field name="qty_done"/>
<field name="product_uom_id" readonly="1" force_save="1" groups="uom.group_uom"/>
<field name="move_id" invisible="1"/>
</group>
</group>
</form>
</field>
</record>
<record id="mrp_product_produce_line_kanban" model="ir.ui.view">
<field name="name">MRP Product Produce Line</field>
<field name="model">mrp.product.produce.line</field>
<field name="priority">100</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="product_id"/>
<field name="lot_id"/>
<field name="qty_to_consume"/>
<field name="qty_reserved"/>
<field name="qty_done"/>
<field name="product_uom_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title"><span><field name="product_id"/></span></strong>
</div>
</div>
<div class="o_kanban_record_body">
<span>Lot <field name="lot_id"/></span><br/>
<span>Quantity To Consume <field name="qty_to_consume"/></span><br/>
<span>Quantity Reserved <field name="qty_reserved"/></span><br/>
<span>Quantity Done <field name="qty_done"/></span><br/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="act_mrp_product_produce" model="ir.actions.act_window">
<field name="name">Produce</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.product.produce</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="target">new</field>
</record>
</odoo>
+3 -7
View File
@@ -75,13 +75,9 @@ class TestMrpAccount(TestWorkOrderProcessCommon):
production_table.extra_cost = 20
production_table.action_confirm()
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production_table.id,
'active_ids': [production_table.id],
}))
produce_form.qty_producing = 1.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(production_table)
mo_form.qty_producing = 1
production_table = mo_form.save()
production_table.post_inventory()
move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").stock_valuation_layer_ids.value
@@ -39,14 +39,11 @@ class TestMrpValuationCommon(TestStockValuationCommon):
return mo
def _produce(self, mo, quantity=0):
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
if quantity:
produce_form.qty_producing = quantity
product_produce = produce_form.save()
product_produce.do_produce()
mo_form = Form(mo)
if not quantity:
quantity = mo.product_qty - mo.qty_produced
mo_form.qty_producing += quantity
mo = mo_form.save()
class TestMrpValuationStandard(TestMrpValuationCommon):
@@ -94,7 +91,10 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
self._make_in_move(self.component, 1, 20)
mo = self._make_mo(self.bom, 2)
self._produce(mo, 1)
mo.post_inventory()
action = mo.button_mark_done()
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder.save().action_close_mo()
mo = mo.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(self.component.value_svl, 20)
self.assertEqual(self.product1.value_svl, 10)
self.assertEqual(self.component.quantity_svl, 1)
@@ -5,7 +5,7 @@
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
<field name="groups_id" eval="[(4, ref('stock.group_stock_manager'))]"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='finished_move_line_ids']" position="before">
<xpath expr="//field[@name='product_id']" position="before">
<field name="show_valuation" invisible="1"/>
</xpath>
<xpath expr="//button[@name='action_view_mrp_production_sources']" position="after">
@@ -117,13 +117,10 @@ class TestStockLandedCostsMrp(StockAccountTestCommon):
self.assertEqual(first_move.product_qty, 2.0)
# produce product
produce_form = Form(self.env['mrp.product.produce'].with_user(self.allow_user).with_context({
'active_id': man_order.id,
'active_ids': [man_order.id],
}))
produce_form.qty_producing = 2.0
produce_wizard = produce_form.save()
produce_wizard.do_produce()
mo_form = Form(man_order.with_user(self.allow_user))
mo_form.qty_producing = 2
man_order = mo_form.save()
man_order.button_mark_done()
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_abstract_workorder
from . import mrp_production
from . import mrp_workorder
@@ -4,15 +4,21 @@
from odoo import models, _
class MrpAbstractWorkorder(models.AbstractModel):
_inherit = 'mrp.abstract.workorder'
class MrpWorkorder(models.Model):
_inherit = 'mrp.production'
def _pre_button_mark_done(self):
confirm_expired_lots = self._check_expired_lots()
if confirm_expired_lots:
return confirm_expired_lots
return super()._pre_button_mark_done()
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
expired_lot_ids = self.move_raw_ids.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids
if expired_lot_ids:
return {
'name': _('Confirmation'),
@@ -27,5 +33,6 @@ class MrpAbstractWorkorder(models.AbstractModel):
context = dict(self.env.context)
context.update({
'default_lot_ids': [(6, 0, expired_lot_ids)],
'default_production_ids': self.ids,
})
return context
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import models, _
class MrpWorkorder(models.Model):
@@ -11,9 +11,28 @@ class MrpWorkorder(models.Model):
confirm_expired_lots = self._check_expired_lots()
if confirm_expired_lots:
return confirm_expired_lots
return super(MrpWorkorder, self).record_production()
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 = super(MrpWorkorder, self)._get_expired_context(expired_lot_ids)
context['default_workorder_id'] = self.id
context = dict(self.env.context)
context.update({
'default_lot_ids': [(6, 0, expired_lot_ids)],
'default_workorder_id': self.id,
})
return context
@@ -76,16 +76,17 @@ class TestStockProductionLot(TestStockCommon):
mo = mo_form.save()
mo.action_confirm()
# ... and tries to product with a non-expired lot as component.
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = self.lot_good_apple
product_produce = produce_form.save()
res = product_produce.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.qty_done = 3
ml.lot_id = self.lot_good_apple
details_operation_form.save()
res = mo.button_mark_done()
# Producing must not return a wizard in this case.
self.assertEqual(res['type'], 'ir.actions.act_window_close')
self.assertEqual(res, True)
def test_02_product_produce_using_expired(self):
""" Checks user gets a confirmation wizard when they produces with
@@ -98,14 +99,15 @@ class TestStockProductionLot(TestStockCommon):
mo = mo_form.save()
mo.action_confirm()
# ... and tries to product with an expired lot as component.
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
with produce_form.raw_workorder_line_ids.edit(0) as line:
line.lot_id = self.lot_expired_apple
product_produce = produce_form.save()
res = product_produce.do_produce()
mo_form = Form(mo)
mo_form.qty_producing = 1
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.qty_done = 3
ml.lot_id = self.lot_expired_apple
details_operation_form.save()
res = mo.button_mark_done()
# Producing must return a confirmation wizard.
self.assertNotEqual(res, None)
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
@@ -178,7 +180,3 @@ class TestStockProductionLot(TestStockCommon):
# confirmation wizard.
self.assertNotEqual(res, None)
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
# FIXME: when should we get the "Work order %s is still running" error?
# with self.assertRaises(UserError):
# # Cannot finish the MO as the Work Order is still ongoing.
# mo.button_mark_done()
@@ -2,4 +2,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import confirm_expiry
from . import mrp_product_produce
@@ -7,12 +7,12 @@ from odoo import api, fields, models, _
class ConfirmExpiry(models.TransientModel):
_inherit = 'expiry.picking.confirmation'
produce_id = fields.Many2one('mrp.product.produce', readonly=True)
production_ids = fields.Many2many('mrp.production', readonly=True)
workorder_id = fields.Many2one('mrp.workorder', readonly=True)
@api.depends('lot_ids')
def _compute_descriptive_fields(self):
if self.produce_id or self.workorder_id:
if self.production_ids or self.workorder_id:
# Shows expired lots only if we are more than one expired lot.
self.show_lots = len(self.lot_ids) > 1
if self.show_lots:
@@ -34,13 +34,8 @@ class ConfirmExpiry(models.TransientModel):
super(ConfirmExpiry, self)._compute_descriptive_fields()
def confirm_produce(self):
return self.produce_id.with_context(skip_expired=True).do_produce()
return self.production_ids.with_context(skip_expired=True).button_mark_done()
def confirm_workorder(self):
return self.workorder_id.with_context(skip_expired=True).record_production()
def return_to_produce_wizard(self):
production = self.produce_id.production_id
action = production.open_produce_product()
action['context'] = {'default_production_id': production.id}
return action
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='description']" position="after">
<field name="picking_ids" invisible="1"/>
<field name="produce_id" invisible="1"/>
<field name="production_ids" invisible="1"/>
<field name="workorder_id" invisible="1"/>
</xpath>
<xpath expr="//button[@name='process']" position="attributes">
@@ -17,19 +17,18 @@
<attribute name="attrs">{'invisible': [('picking_ids', '=', [])]}</attribute>
</xpath>
<xpath expr="//button[@special='cancel']" position="attributes">
<attribute name="attrs">{'invisible': [('produce_id', '!=', False)]}</attribute>
<attribute name="attrs">{'invisible': [('production_ids', '!=', [])]}</attribute>
</xpath>
<xpath expr="//button[@name='process']" position="after">
<!-- From Produce Product wizard -->
<button name="confirm_produce"
string="Confirm"
type="object"
attrs="{'invisible': [('produce_id', '=', False)]}"
attrs="{'invisible': [('production_ids', '=', [])]}"
class="btn-primary"/>
<button name="return_to_produce_wizard"
<button special="cancel"
string="Discard"
type="object"
attrs="{'invisible': [('produce_id', '=', False)]}"
attrs="{'invisible': [('production_ids', '=', [])]}"
class="btn-secondary"/>
<!-- From a Workorder -->
<button name="confirm_workorder"
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class MrpProductProduce(models.TransientModel):
_inherit = 'mrp.product.produce'
def do_produce(self):
confirm_expired_lots = self._check_expired_lots()
if confirm_expired_lots:
return confirm_expired_lots
return super(MrpProductProduce, self).do_produce()
def _get_expired_context(self, expired_lot_ids):
context = super(MrpProductProduce, self)._get_expired_context(expired_lot_ids)
context['default_produce_id'] = self.id
return context
+17 -8
View File
@@ -126,12 +126,21 @@ class StockMove(models.Model):
return res
def _action_record_components(self):
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
action['context'] = dict(
default_production_id=self.move_orig_ids.production_id.id,
default_subcontract_move_id=self.id
)
return action
self.ensure_one()
production = self.move_orig_ids.production_id
view = self.env.ref('mrp.mrp_production_form_view')
return {
'name': _('Subcontract'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mrp.production',
'views': [(view.id, 'form')],
'view_id': view.id,
'target': 'new',
'res_id': production.id,
'context': dict(self.env.context, subcontract_move_id=self.id),
}
def _check_overprocessed_subcontract_qty(self):
""" If a subcontracted move use tracked components. Do not allow to add
@@ -147,8 +156,8 @@ class StockMove(models.Model):
if not move._has_tracked_subcontract_components():
continue
rounding = move.product_uom.rounding
if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0:
overprocessed_moves |= move
# if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0:
# overprocessed_moves |= move
if overprocessed_moves:
raise UserError(_("""
You have to use 'Records Components' button in order to register quantity for a
@@ -4,6 +4,7 @@
from datetime import timedelta
from odoo import api, fields, models
from odoo.tools import float_is_zero
class StockPicking(models.Model):
@@ -43,7 +44,9 @@ class StockPicking(models.Model):
for move in picking.move_lines:
if not move.is_subcontract:
continue
production = move.move_orig_ids.production_id
production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
if len(production) > 1:
production = production[-1]
if move._has_tracked_subcontract_components():
move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel')).move_line_ids.unlink()
move_finished_ids = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
@@ -58,16 +61,25 @@ class StockPicking(models.Model):
'location_dest_id': move_finished_ids.location_dest_id.id,
})
else:
for move_line in move.move_line_ids:
produce = self.env['mrp.product.produce'].with_context(default_production_id=production.id).create({
'production_id': production.id,
'qty_producing': move_line.qty_done,
'product_uom_id': move_line.product_uom_id.id,
'finished_lot_id': move_line.lot_id.id,
'consumption': 'strict',
})
produce._generate_produce_lines()
produce._record_production()
if not move.move_line_ids.lot_id:
qty_done_production_uom = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id)
production.qty_producing += qty_done_production_uom
for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)):
if move.state in ('done', 'cancel'):
continue
if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
continue
new_qty = production.product_uom_id._compute_quantity(
(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)
productions |= production
for subcontracted_production in productions:
if subcontracted_production.state == 'progress':
@@ -415,7 +415,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = self.finished
move.product_uom_qty = 5
move.product_uom_qty = 3 # FIXME sle: need to handle the backorder in subcontract
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
mo = picking_receipt.move_lines.move_orig_ids.production_id
@@ -442,39 +442,26 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
'product_id': self.finished.id,
'company_id': self.env.company.id,
})
mo_form = Form(picking_receipt._get_subcontracted_productions().with_context(subcontract_move_id=picking_receipt.move_lines.id))
mo_form.qty_producing = 3
mo_form.lot_producing_id = lot_f1
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = lot_c1
ml.qty_done = 3
details_operation_form.save()
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.lot_id = lot_c2
ml.qty_done = 3
details_operation_form.save()
register_form = Form(self.env['mrp.product.produce'].with_context(
active_id=picking_receipt._get_subcontracted_productions().id,
default_subcontract_move_id=picking_receipt.move_lines.id
))
register_form.qty_producing = 3.0
self.assertEqual(len(register_form._values['raw_workorder_line_ids']), 2,
'Register Components Form should contains one line per component.')
self.assertTrue(all(p[2]['product_id'] in (self.comp1 | self.comp2).ids for p in register_form._values['raw_workorder_line_ids']),
'Register Components Form should contains component.')
with register_form.raw_workorder_line_ids.edit(0) as pl:
pl.lot_id = lot_c1
with register_form.raw_workorder_line_ids.edit(1) as pl:
pl.lot_id = lot_c2
register_form.finished_lot_id = lot_f1
register_wizard = register_form.save()
action = register_wizard.continue_production()
register_form = Form(self.env['mrp.product.produce'].with_context(
**action['context']
))
with register_form.raw_workorder_line_ids.edit(0) as pl:
pl.lot_id = lot_c1
with register_form.raw_workorder_line_ids.edit(1) as pl:
pl.lot_id = lot_c2
register_form.finished_lot_id = lot_f1
register_wizard = register_form.save()
register_wizard.do_produce()
self.assertEqual(move_comp1.quantity_done, 5.0)
self.assertEqual(move_comp1.quantity_done, 3)
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C1')
self.assertEqual(move_comp2.quantity_done, 5.0)
self.assertEqual(move_comp2.quantity_done, 3)
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C2')
self.assertEqual(move_finished.quantity_done, 5.0)
self.assertEqual(move_finished.quantity_done, 3)
self.assertEqual(move_finished.move_line_ids.filtered(lambda ml: ml.product_uom_qty).lot_id.name, 'LOT F1')
corrected_final_lot = self.env['stock.production.lot'].create({
@@ -484,31 +471,17 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
})
details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations'))
for i in range(len(details_operation_form._values['move_line_ids'])):
with details_operation_form.move_line_ids.edit(i) as ml:
if ml._values['qty_done']:
ml.lot_id = corrected_final_lot
with details_operation_form.move_line_ids.edit(0) as ml:
ml.lot_id = corrected_final_lot
details_operation_form.save()
move_raw_comp_1 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp1)
move_raw_comp_2 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp2)
details_subcontract_moves_form = Form(move_raw_comp_1, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view'))
for i in range(len(details_subcontract_moves_form._values['move_line_ids'])):
with details_subcontract_moves_form.move_line_ids.edit(i) as sc:
if sc._values['qty_done']:
sc.lot_produced_ids.remove(index=0)
sc.lot_produced_ids.add(corrected_final_lot)
details_subcontract_moves_form.save()
details_subcontract_moves_form = Form(move_raw_comp_2, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view'))
for i in range(len(details_subcontract_moves_form._values['move_line_ids'])):
with details_subcontract_moves_form.move_line_ids.edit(i) as sc:
if sc._values['qty_done']:
sc.lot_produced_ids.remove(index=0)
sc.lot_produced_ids.add(corrected_final_lot)
details_subcontract_moves_form.save()
picking_receipt.move_lines.move_orig_ids.production_id.lot_producing_id = corrected_final_lot
picking_receipt.move_lines.move_orig_ids.production_id.button_mark_done()
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2')
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2')
self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2')
self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2')
def test_flow_8(self):
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
@@ -693,20 +666,20 @@ class TestSubcontractingTracking(TransactionCase):
'product_id': self.comp1_sn.id,
'company_id': self.env.company.id,
})
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.finished_lot_id = lot_id
produce_form.raw_workorder_line_ids._records[0]['lot_id'] = serial_id.id
wiz_produce = produce_form.save()
wiz_produce.do_produce()
mo_form = Form(mo.with_context(subcontract_move_id=picking_receipt.move_lines.id))
mo_form.qty_producing = 1
mo_form.lot_producing_id = lot_id
mo = mo_form.save()
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.new() as ml:
ml.qty_done = 1
ml.lot_id = serial_id
details_operation_form.save()
# We should not be able to call the 'record_components' button
self.assertFalse(picking_receipt.display_action_record_components)
picking_receipt.move_lines.quantity_done = 1
picking_receipt.move_lines.move_line_ids.lot_id = lot_id.id
picking_receipt.button_validate()
self.assertEqual(mo.state, 'done')
@@ -17,11 +17,6 @@
<field name="state" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="lot_produced_ids"
widget="many2many_tags"
context="{'default_product_id': parent.product_id}"
attrs="{'column_invisible': [('parent.finished_lots_exist', '!=', True)]}"
/>
<field name="qty_done"/>
<field name="lot_id" context="{'default_product_id': product_id}"/>
</tree>
@@ -30,17 +25,17 @@
</form>
</field>
</record>
<record id="mrp_subcontracting_move_tree_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.tree.view</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="create">0</attribute>
<attribute name="delete">0</attribute>
</xpath>
</field>
</record>
<!-- <record id="mrp_subcontracting_move_tree_view" model="ir.ui.view"> -->
<!-- <field name="name">mrp.subcontracting.move.tree.view</field> -->
<!-- <field name="model">stock.move</field> -->
<!-- <field name="priority">1000</field> -->
<!-- <field name="mode">primary</field> -->
<!-- <field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/> -->
<!-- <field name="arch" type="xml"> -->
<!-- <xpath expr="//tree" position="attributes"> -->
<!-- <attribute name="create">0</attribute> -->
<!-- <attribute name="delete">0</attribute> -->
<!-- </xpath> -->
<!-- </field> -->
<!-- </record> -->
</odoo>
@@ -2,42 +2,36 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.tools.float_utils import float_is_zero, float_compare
from odoo.tools.float_utils import float_compare, float_is_zero
class MrpProductProduce(models.TransientModel):
_inherit = 'mrp.product.produce'
subcontract_move_id = fields.Many2one('stock.move', 'stock move from the subcontract picking', check_company=True)
class MrpProduction(models.Model):
_inherit = 'mrp.production'
def continue_production(self):
action = super(MrpProductProduce, self).continue_production()
action['context'] = dict(action['context'], default_subcontract_move_id=self.subcontract_move_id.id)
return action
def write(self, vals):
res = super().write(vals)
if self.env.context.get('subcontract_move_id'):
for production in self:
if production.state not in ('cancel', 'done') and \
('lot_producing_id' in vals or 'qty_producing' in vals):
production._update_finished_move()
def _generate_produce_lines(self):
""" When the wizard is called in backend, the onchange that create the
produce lines is not trigger. This method generate them and is used with
_record_production to appropriately set the lot_produced_id and
appropriately create raw stock move lines.
"""
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.product.produce.line'].create(line_values)
def _pre_button_mark_done(self):
if self.env.context.get('subcontract_move_id'):
return True
return super()._pre_button_mark_done()
def _update_finished_move(self):
""" After producing, set the move line on the subcontract picking. """
res = super(MrpProductProduce, self)._update_finished_move()
if self.subcontract_move_id:
subcontract_move_id = self.env.context.get('subcontract_move_id')
res = None
if subcontract_move_id:
subcontract_move_id = self.env['stock.move'].browse(subcontract_move_id)
quantity = self.qty_producing
if self.finished_lot_id:
move_lines = self.subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.finished_lot_id or not ml.lot_id)
if self.lot_producing_id:
move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_producing_id or not ml.lot_id)
else:
move_lines = self.subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id)
move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id)
# Update reservation and quantity done
for ml in move_lines:
rounding = ml.product_uom_id.rounding
@@ -52,16 +46,14 @@ class MrpProductProduce(models.TransientModel):
if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0:
ml.write({
'qty_done': new_quantity_done,
'lot_id': self.finished_lot_id and self.finished_lot_id.id,
'lot_name': self.finished_lot_id and self.finished_lot_id.name,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
else:
new_qty_reserved = ml.product_uom_qty - new_quantity_done
default = {
'product_uom_qty': new_quantity_done,
'qty_done': new_quantity_done,
'lot_id': self.finished_lot_id and self.finished_lot_id.id,
'lot_name': self.finished_lot_id and self.finished_lot_id.name,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
}
ml.copy(default=default)
ml.with_context(bypass_reservation_update=True).write({
@@ -71,24 +63,30 @@ class MrpProductProduce(models.TransientModel):
if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0:
self.env['stock.move.line'].create({
'move_id': self.subcontract_move_id.id,
'picking_id': self.subcontract_move_id.picking_id.id,
'move_id': subcontract_move_id.id,
'picking_id': subcontract_move_id.picking_id.id,
'product_id': self.product_id.id,
'location_id': self.subcontract_move_id.location_id.id,
'location_dest_id': self.subcontract_move_id.location_dest_id.id,
'location_id': subcontract_move_id.location_id.id,
'location_dest_id': subcontract_move_id.location_dest_id.id,
'product_uom_qty': 0,
'product_uom_id': self.product_uom_id.id,
'qty_done': quantity,
'lot_id': self.finished_lot_id and self.finished_lot_id.id,
'lot_name': self.finished_lot_id and self.finished_lot_id.name,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
if not self._get_todo(self.production_id):
ml_reserved = self.subcontract_move_id.move_line_ids.filtered(lambda ml:
if not self._get_todo():
ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml:
float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id.rounding))
ml_reserved.unlink()
for ml in self.subcontract_move_id.move_line_ids:
for ml in subcontract_move_id.move_line_ids:
ml.product_uom_qty = ml.qty_done
self.subcontract_move_id._recompute_state()
subcontract_move_id._recompute_state()
return res
def _get_todo(self):
""" This method will return remaining todo quantity of production. """
main_product_moves = self.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_id.id)
todo_quantity = self.product_qty - sum(main_product_moves.mapped('quantity_done'))
todo_quantity = todo_quantity if (todo_quantity > 0) else 0
return todo_quantity
+6 -15
View File
@@ -20,7 +20,6 @@ class TestSaleMrpFlow(AccountTestCommon):
cls.MrpProduction = cls.env['mrp.production']
cls.Inventory = cls.env['stock.inventory']
cls.InventoryLine = cls.env['stock.inventory.line']
cls.ProductProduce = cls.env['mrp.product.produce']
cls.ProductCategory = cls.env['product.category']
cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit')
@@ -432,14 +431,9 @@ class TestSaleMrpFlow(AccountTestCommon):
# produce product D.
# ------------------
produce_form = Form(self.ProductProduce.with_context({
'active_id': mnf_product_d.id,
'active_ids': [mnf_product_d.id],
}))
produce_form.qty_producing = 20
produce_d = produce_form.save()
# produce_d.on_change_qty()
produce_d.do_produce()
mo_form = Form(mnf_product_d)
mo_form.qty_producing = 20
mnf_product_d = mo_form.save()
mnf_product_d.post_inventory()
# Check state of manufacturing order.
@@ -487,12 +481,9 @@ class TestSaleMrpFlow(AccountTestCommon):
# Produce product A.
# ------------------
produce_form = Form(self.ProductProduce.with_context({
'active_id': mnf_product_a.id,
'active_ids': [mnf_product_a.id],
}))
produce_a = produce_form.save()
produce_a.do_produce()
mo_form = Form(mnf_product_a)
mo_form.qty_producing = mo_form.product_qty
mnf_product_a = mo_form.save()
mnf_product_a.post_inventory()
# Check state of manufacturing order product A.
self.assertEqual(mnf_product_a.state, 'done', 'Manufacturing order should still be in the progress state.')
+5 -1
View File
@@ -219,7 +219,7 @@ class StockMove(models.Model):
else:
move.show_details_visible = (((consignment_enabled and move.picking_id.picking_type_id.code != 'incoming') or
show_details_visible or move.has_tracking != 'none') and
(move.state != 'draft' or (move.picking_id.immediate_transfer and move.state == 'draft')) and
move._show_details_in_draft() and
move.picking_id.picking_type_id.show_operations is False)
def _compute_show_reserved_availability(self):
@@ -1653,3 +1653,7 @@ class StockMove(models.Model):
mtso_free_qties_by_loc[move.location_id][move.product_id.id] -= needed_qty
else:
move.procure_method = 'make_to_order'
def _show_details_in_draft(self):
self.ensure_one()
return self.state != 'draft' or (self.picking_id.immediate_transfer and self.state == 'draft')
@@ -823,17 +823,15 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing
position: 'bottom',
},
...tour.stepUtils.statusbarButtonsSteps('Check availability', _t("Check availability")),
...tour.stepUtils.statusbarButtonsSteps('Produce', _t("Produce"), "body.o_web_client:not(.oe_wait)"),
{
mobile: false,
trigger: ".modal-footer .btn-primary:nth-child(3)",
content: _t('Record Production'),
position: 'bottom',
trigger: '.o_form_button_edit',
content: _t('Edit the production order'),
extra_trigger: 'body.o_web_client:not(.oe_wait)',
}, {
mobile: true,
trigger: '.modal-footer .btn-primary[name="do_produce"]:not(.o_invisible_modifier)',
content: _t('Record Production'),
position: 'bottom',
trigger: ".o_field_widget[name=qty_producing]",
content: _t("Produce"),
run: "text 1",
extra_trigger: 'body.o_web_client:not(.oe_wait)',
},
...tour.stepUtils.statusbarButtonsSteps('Mark as Done', _t("Mark as Done"), ".o_statusbar_status .btn.dropdown-toggle:contains('To Close')"),
{