[MERGE] mrp, etc: ease production order edition
Ease manufacturing order edition in allowing to edit in the tree view. Consider that one manufacturing order = 1LN/SN and create back-orders in case several of them should be produced in the same MO. Ease definition of operations and their different steps by allowing to do it in the same BoM screen. joint work Simon Lejeune <sle@openerp.com> joint work Arnold Moyaux <arm@odoo.com> joint work William Henrotin <whe@odoo.com> joint work =?UTF-8?q?R=C3=A9my=20Voet=20=28ryv=29?= <ryv@odoo.com> joint work yhu-odoo <yhu@odoo.com> joint work "Tiffany Chang (tic)" <tic@odoo.com> task-2241471 closes odoo/odoo#52949 Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
@@ -15,10 +15,11 @@
|
||||
'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',
|
||||
'wizard/mrp_production_backorder.xml',
|
||||
'wizard/mrp_consumption_warning_views.xml',
|
||||
'views/mrp_views_menus.xml',
|
||||
'views/stock_move_views.xml',
|
||||
'views/mrp_workorder_views.xml',
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="sequence_mrp_route" model="ir.sequence">
|
||||
<field name="name">Routing</field>
|
||||
<field name="code">mrp.routing</field>
|
||||
<field name="prefix">RO/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
<function model="res.company" name="create_missing_unbuild_sequences" />
|
||||
<!--
|
||||
Stock rules and routes
|
||||
|
||||
+94
-102
@@ -28,83 +28,6 @@
|
||||
<field name="resource_calendar_id" ref="resource.resource_calendar_std"/>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
<!-- Resource: mrp.routing -->
|
||||
|
||||
<record id="mrp_routing_0" model="mrp.routing">
|
||||
<field name="name">Primary Assembly</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_1" model="mrp.routing">
|
||||
<field name="name">Secondary Assembly</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_2" model="mrp.routing">
|
||||
<field name="name">Manual Component's Assembly</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_3" model="mrp.routing">
|
||||
<field name="name">Assemble Furniture</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Resource: mrp.routing.workcenter -->
|
||||
|
||||
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
|
||||
<field name="routing_id" ref="mrp_routing_0"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Manual Assembly</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
|
||||
<field name="routing_id" ref="mrp_routing_1"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Long time assembly</field>
|
||||
<field name="time_cycle">180</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
|
||||
<field name="routing_id" ref="mrp_routing_1"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Testing</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
|
||||
<field name="routing_id" ref="mrp_routing_1"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_1"/>
|
||||
<field name="name">Packing</field>
|
||||
<field name="time_cycle">30</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_2" model="mrp.routing.workcenter">
|
||||
<field name="routing_id" ref="mrp_routing_2"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_2"/>
|
||||
<field name="time_cycle">120</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="name">Manual Assembly</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter">
|
||||
<field name="routing_id" ref="mrp_routing_3"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="time_cycle">120</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="name">Assembly</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<!-- Resource: mrp.bom -->
|
||||
|
||||
<record id="product.product_product_3_product_template" model="product.template">
|
||||
@@ -114,7 +37,14 @@
|
||||
<field name="product_tmpl_id" ref="product.product_product_3_product_template"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="routing_id" ref="mrp_routing_0"/>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_manufacture"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Manual Assembly</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_manufacture_line_1" model="mrp.bom.line">
|
||||
@@ -296,7 +226,14 @@
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="consumption">flexible</field>
|
||||
<field name="routing_id" ref="mrp_routing_3"/>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_desk"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="time_cycle">120</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="name">Assembly</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_desk_line_1" model="mrp.bom.line">
|
||||
@@ -350,8 +287,16 @@
|
||||
<field name="product_tmpl_id" ref="product_product_computer_desk_head_product_template"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="routing_id" ref="mrp_routing_0"/>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_table_top"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Manual Assembly</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_bom_line_wood_panel" model="mrp.bom.line">
|
||||
<field name="product_id" ref="product_product_wood_panel"/>
|
||||
<field name="product_qty">2</field>
|
||||
@@ -371,7 +316,32 @@
|
||||
<field name="product_tmpl_id" ref="product_product_plastic_laminate_product_template"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="routing_id" ref="mrp_routing_1"/>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Long time assembly</field>
|
||||
<field name="time_cycle">180</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Testing</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_plastic_laminate"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_1"/>
|
||||
<field name="name">Packing</field>
|
||||
<field name="time_cycle">30</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
<record id="mrp_bom_line_plastic_laminate" model="mrp.bom.line">
|
||||
<field name="product_id" ref="product_product_ply_veneer"/>
|
||||
@@ -596,9 +566,34 @@
|
||||
<field name="product_tmpl_id" ref="product.product_product_27_product_template"/>
|
||||
<field name="product_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="routing_id" ref="mrp_routing_1"/>
|
||||
<field name="code">SEC-ASSEM</field>
|
||||
</record>
|
||||
<record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_laptop_cust_rout"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Long time assembly</field>
|
||||
<field name="time_cycle">180</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_laptop_cust_rout"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_3"/>
|
||||
<field name="name">Testing</field>
|
||||
<field name="time_cycle">60</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter">
|
||||
<field name="bom_id" ref="mrp_bom_laptop_cust_rout"/>
|
||||
<field name="workcenter_id" ref="mrp_workcenter_1"/>
|
||||
<field name="name">Packing</field>
|
||||
<field name="time_cycle">30</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/>
|
||||
</record>
|
||||
<record id="mrp_bom_laptop_cust_rout_line_1" model="mrp.bom.line">
|
||||
<field name="product_id" ref="product_product_drawer_drawer"/>
|
||||
<field name="product_qty">1</field>
|
||||
@@ -630,6 +625,10 @@
|
||||
<value model="stock.move" eval="obj().env.ref('mrp.mrp_production_laptop_cust')._get_moves_raw_values()"/>
|
||||
</function>
|
||||
|
||||
<function model="stock.move" name="create">
|
||||
<value model="stock.move" eval="obj().env.ref('mrp.mrp_production_laptop_cust')._get_moves_finished_values()"/>
|
||||
</function>
|
||||
|
||||
<!-- Run Scheduler -->
|
||||
<function model="procurement.group" name="run_scheduler"/>
|
||||
|
||||
@@ -677,6 +676,10 @@
|
||||
<field name="date_start" eval="(datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<function model="mrp.production" name="_create_workorder">
|
||||
<value eval="[ref('mrp.mrp_production_3')]"/>
|
||||
</function>
|
||||
|
||||
<function model="mrp.production" name="action_confirm">
|
||||
<value eval="[ref('mrp.mrp_production_3')]"/>
|
||||
</function>
|
||||
@@ -689,32 +692,21 @@
|
||||
<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 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.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>
|
||||
|
||||
<function model="mrp.production" name="post_inventory">
|
||||
<function model="mrp.production" name="_post_inventory">
|
||||
<value eval="[ref('mrp.mrp_production_laptop_cust')]"/>
|
||||
</function>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mrp_document
|
||||
from . import mrp_abstract_workorder
|
||||
from . import res_config_settings
|
||||
from . import mrp_bom
|
||||
from . import mrp_routing
|
||||
|
||||
@@ -1,561 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_round, float_is_zero
|
||||
|
||||
|
||||
class MrpAbstractWorkorder(models.AbstractModel):
|
||||
_name = "mrp.abstract.workorder"
|
||||
_description = "Common code between produce wizards and workorders."
|
||||
_check_company_auto = True
|
||||
|
||||
production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, check_company=True)
|
||||
product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True, check_company=True)
|
||||
qty_producing = fields.Float(string='Currently Produced Quantity', digits='Product Unit of Measure')
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True)
|
||||
finished_lot_id = fields.Many2one(
|
||||
'stock.production.lot', string='Lot/Serial Number',
|
||||
domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True)
|
||||
product_tracking = fields.Selection(related="product_id.tracking")
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('flexible', 'Flexible')],
|
||||
required=True,
|
||||
)
|
||||
use_create_components_lots = fields.Boolean(related="production_id.picking_type_id.use_create_components_lots")
|
||||
company_id = fields.Many2one(related='production_id.company_id')
|
||||
|
||||
@api.model
|
||||
def _prepare_component_quantity(self, move, qty_producing):
|
||||
""" helper that computes quantity to consume (or to create in case of byproduct)
|
||||
depending on the quantity producing and the move's unit factor"""
|
||||
if move.product_id.tracking == 'serial':
|
||||
uom = move.product_id.uom_id
|
||||
else:
|
||||
uom = move.product_uom
|
||||
return move.product_uom._compute_quantity(
|
||||
qty_producing * move.unit_factor,
|
||||
uom,
|
||||
round=False
|
||||
)
|
||||
|
||||
def _workorder_line_ids(self):
|
||||
self.ensure_one()
|
||||
return self.raw_workorder_line_ids | self.finished_workorder_line_ids
|
||||
|
||||
@api.onchange('qty_producing')
|
||||
def _onchange_qty_producing(self):
|
||||
""" Modify the qty currently producing will modify the existing
|
||||
workorder line in order to match the new quantity to consume for each
|
||||
component and their reserved quantity.
|
||||
"""
|
||||
if self.qty_producing <= 0:
|
||||
raise UserError(_('You have to produce at least one %s.') % self.product_uom_id.name)
|
||||
line_values = self._update_workorder_lines()
|
||||
for values in line_values['to_create']:
|
||||
self.env[self._workorder_line_ids()._name].new(values)
|
||||
for line in line_values['to_delete']:
|
||||
if line in self.raw_workorder_line_ids:
|
||||
self.raw_workorder_line_ids -= line
|
||||
else:
|
||||
self.finished_workorder_line_ids -= line
|
||||
for line, vals in line_values['to_update'].items():
|
||||
line.update(vals)
|
||||
|
||||
def _update_workorder_lines(self):
|
||||
""" Update workorder lines, according to the new qty currently
|
||||
produced. It returns a dict with line to create, update or delete.
|
||||
It do not directly write or unlink the line because this function is
|
||||
used in onchange and request that write on db (e.g. workorder creation).
|
||||
"""
|
||||
line_values = {'to_create': [], 'to_delete': [], 'to_update': {}}
|
||||
# moves are actual records
|
||||
move_finished_ids = self.move_finished_ids._origin.filtered(lambda move: move.product_id != self.product_id and move.state not in ('done', 'cancel'))
|
||||
move_raw_ids = self.move_raw_ids._origin.filtered(lambda move: move.state not in ('done', 'cancel'))
|
||||
for move in move_raw_ids | move_finished_ids:
|
||||
move_workorder_lines = self._workorder_line_ids().filtered(lambda w: w.move_id == move)
|
||||
|
||||
# Compute the new quantity for the current component
|
||||
rounding = move.product_uom.rounding
|
||||
new_qty = self._prepare_component_quantity(move, self.qty_producing)
|
||||
|
||||
# In case the production uom is different than the workorder uom
|
||||
# it means the product is serial and production uom is not the reference
|
||||
new_qty = self.product_uom_id._compute_quantity(
|
||||
new_qty,
|
||||
self.production_id.product_uom_id,
|
||||
round=False
|
||||
)
|
||||
qty_todo = float_round(new_qty - sum(move_workorder_lines.mapped('qty_to_consume')), precision_rounding=rounding)
|
||||
|
||||
# Remove or lower quantity on exisiting workorder lines
|
||||
if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
|
||||
qty_todo = abs(qty_todo)
|
||||
# Try to decrease or remove lines that are not reserved and
|
||||
# partialy reserved first. A different decrease strategy could
|
||||
# be define in _unreserve_order method.
|
||||
for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl._unreserve_order()):
|
||||
if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0:
|
||||
break
|
||||
# If the quantity to consume on the line is lower than the
|
||||
# quantity to remove, the line could be remove.
|
||||
if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0:
|
||||
qty_todo = float_round(qty_todo - workorder_line.qty_to_consume, precision_rounding=rounding)
|
||||
if line_values['to_delete']:
|
||||
line_values['to_delete'] |= workorder_line
|
||||
else:
|
||||
line_values['to_delete'] = workorder_line
|
||||
# decrease the quantity on the line
|
||||
else:
|
||||
new_val = workorder_line.qty_to_consume - qty_todo
|
||||
# avoid to write a negative reserved quantity
|
||||
new_reserved = max(0, workorder_line.qty_reserved - qty_todo)
|
||||
line_values['to_update'][workorder_line] = {
|
||||
'qty_to_consume': new_val,
|
||||
'qty_done': new_val,
|
||||
'qty_reserved': new_reserved,
|
||||
}
|
||||
qty_todo = 0
|
||||
else:
|
||||
# Search among wo lines which one could be updated
|
||||
qty_reserved_wl = defaultdict(float)
|
||||
# Try to update the line with the greater reservation first in
|
||||
# order to promote bigger batch.
|
||||
for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl.qty_reserved, reverse=True):
|
||||
rounding = workorder_line.product_uom_id.rounding
|
||||
if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0:
|
||||
break
|
||||
move_lines = workorder_line._get_move_lines()
|
||||
qty_reserved_wl[workorder_line.lot_id] += workorder_line.qty_reserved
|
||||
# The reserved quantity according to exisiting move line
|
||||
# already produced (with qty_done set) and other production
|
||||
# lines with the same lot that are currently on production.
|
||||
qty_reserved_remaining = sum(move_lines.mapped('product_uom_qty')) - sum(move_lines.mapped('qty_done')) - qty_reserved_wl[workorder_line.lot_id]
|
||||
if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0:
|
||||
qty_to_add = min(qty_reserved_remaining, qty_todo)
|
||||
line_values['to_update'][workorder_line] = {
|
||||
'qty_done': workorder_line.qty_to_consume + qty_to_add,
|
||||
'qty_to_consume': workorder_line.qty_to_consume + qty_to_add,
|
||||
'qty_reserved': workorder_line.qty_reserved + qty_to_add,
|
||||
}
|
||||
qty_todo -= qty_to_add
|
||||
qty_reserved_wl[workorder_line.lot_id] += qty_to_add
|
||||
|
||||
# If a line exists without reservation and without lot. It
|
||||
# means that previous operations could not find any reserved
|
||||
# quantity and created a line without lot prefilled. In this
|
||||
# case, the system will not find an existing move line with
|
||||
# available reservation anymore and will increase this line
|
||||
# instead of creating a new line without lot and reserved
|
||||
# quantities.
|
||||
if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial':
|
||||
line_values['to_update'][workorder_line] = {
|
||||
'qty_done': workorder_line.qty_to_consume + qty_todo,
|
||||
'qty_to_consume': workorder_line.qty_to_consume + qty_todo,
|
||||
}
|
||||
qty_todo = 0
|
||||
|
||||
# if there are still qty_todo, create new wo lines
|
||||
if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0:
|
||||
for values in self._generate_lines_values(move, qty_todo):
|
||||
line_values['to_create'].append(values)
|
||||
return line_values
|
||||
|
||||
@api.model
|
||||
def _generate_lines_values(self, move, qty_to_consume):
|
||||
""" Create workorder line. First generate line based on the reservation,
|
||||
in order to prefill reserved quantity, lot and serial number.
|
||||
If the quantity to consume is greater than the reservation quantity then
|
||||
create line with the correct quantity to consume but without lot or
|
||||
serial number.
|
||||
"""
|
||||
lines = []
|
||||
is_tracked = move.product_id.tracking == 'serial'
|
||||
if move in self.move_raw_ids._origin:
|
||||
# Get the inverse_name (many2one on line) of raw_workorder_line_ids
|
||||
initial_line_values = {self.raw_workorder_line_ids._get_raw_workorder_inverse_name(): self.id}
|
||||
else:
|
||||
# Get the inverse_name (many2one on line) of finished_workorder_line_ids
|
||||
initial_line_values = {self.finished_workorder_line_ids._get_finished_workoder_inverse_name(): self.id}
|
||||
for move_line in move.move_line_ids:
|
||||
line = dict(initial_line_values)
|
||||
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:
|
||||
continue
|
||||
# search wo line on which the lot is not fully consumed or other reserved lot
|
||||
linked_wo_line = self._workorder_line_ids().filtered(
|
||||
lambda line: line.move_id == move and
|
||||
line.lot_id == move_line.lot_id
|
||||
)
|
||||
if linked_wo_line:
|
||||
if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0:
|
||||
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume')))
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done)
|
||||
line.update({
|
||||
'move_id': move.id,
|
||||
'product_id': move.product_id.id,
|
||||
'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id,
|
||||
'qty_to_consume': to_consume_in_line,
|
||||
'qty_reserved': to_consume_in_line,
|
||||
'lot_id': move_line.lot_id.id,
|
||||
'qty_done': to_consume_in_line,
|
||||
})
|
||||
lines.append(line)
|
||||
qty_to_consume -= to_consume_in_line
|
||||
# The move has not reserved the whole quantity so we create new wo lines
|
||||
if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0:
|
||||
line = dict(initial_line_values)
|
||||
if move.product_id.tracking == 'serial':
|
||||
while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0:
|
||||
line.update({
|
||||
'move_id': move.id,
|
||||
'product_id': move.product_id.id,
|
||||
'product_uom_id': move.product_id.uom_id.id,
|
||||
'qty_to_consume': 1,
|
||||
'qty_done': 1,
|
||||
})
|
||||
lines.append(line)
|
||||
qty_to_consume -= 1
|
||||
else:
|
||||
line.update({
|
||||
'move_id': move.id,
|
||||
'product_id': move.product_id.id,
|
||||
'product_uom_id': move.product_uom.id,
|
||||
'qty_to_consume': qty_to_consume,
|
||||
'qty_done': qty_to_consume,
|
||||
})
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
def _update_finished_move(self):
|
||||
""" Update the finished move & move lines in order to set the finished
|
||||
product lot on it as well as the produced quantity. This method get the
|
||||
information either from the last workorder or from the Produce wizard."""
|
||||
production_move = self.production_id.move_finished_ids.filtered(
|
||||
lambda move: move.product_id == self.product_id and
|
||||
move.state not in ('done', 'cancel')
|
||||
)
|
||||
if production_move and production_move.product_id.tracking != 'none':
|
||||
if not self.finished_lot_id:
|
||||
raise UserError(_('You need to provide a lot for the finished product.'))
|
||||
move_line = production_move.move_line_ids.filtered(
|
||||
lambda line: line.lot_id.id == self.finished_lot_id.id
|
||||
)
|
||||
if move_line:
|
||||
if self.product_id.tracking == 'serial':
|
||||
raise UserError(_('You cannot produce the same serial number twice.'))
|
||||
move_line.product_uom_qty += self.qty_producing
|
||||
move_line.qty_done += self.qty_producing
|
||||
else:
|
||||
location_dest_id = production_move.location_dest_id._get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id
|
||||
move_line.create({
|
||||
'move_id': production_move.id,
|
||||
'product_id': production_move.product_id.id,
|
||||
'lot_id': self.finished_lot_id.id,
|
||||
'product_uom_qty': self.qty_producing,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'qty_done': self.qty_producing,
|
||||
'location_id': production_move.location_id.id,
|
||||
'location_dest_id': location_dest_id,
|
||||
})
|
||||
else:
|
||||
rounding = production_move.product_uom.rounding
|
||||
production_move._set_quantity_done(
|
||||
float_round(self.qty_producing, precision_rounding=rounding)
|
||||
)
|
||||
|
||||
def _update_moves(self):
|
||||
""" Once the production is done. Modify the workorder lines into
|
||||
stock move line with the registered lot and quantity done.
|
||||
"""
|
||||
# add missing move for extra component/byproduct
|
||||
for line in self._workorder_line_ids():
|
||||
if not line.move_id:
|
||||
line._set_move_id()
|
||||
# Before writting produce quantities, we ensure they respect the bom strictness
|
||||
self._strict_consumption_check()
|
||||
vals_list = []
|
||||
workorder_lines_to_process = self._workorder_line_ids().filtered(lambda line: line.product_id != self.product_id and line.qty_done > 0)
|
||||
for line in workorder_lines_to_process:
|
||||
line._update_move_lines()
|
||||
if float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) > 0:
|
||||
vals_list += line._create_extra_move_lines()
|
||||
|
||||
self._workorder_line_ids().filtered(lambda line: line.product_id != self.product_id).unlink()
|
||||
self.env['stock.move.line'].create(vals_list)
|
||||
|
||||
def _strict_consumption_check(self):
|
||||
if self.consumption == 'strict':
|
||||
for move in self.move_raw_ids:
|
||||
lines = self._workorder_line_ids().filtered(lambda l: l.move_id == move)
|
||||
qty_done = 0.0
|
||||
qty_to_consume = 0.0
|
||||
for line in lines:
|
||||
qty_done += line.product_uom_id._compute_quantity(line.qty_done, line.product_id.uom_id)
|
||||
qty_to_consume += line.product_uom_id._compute_quantity(line.qty_to_consume, line.product_id.uom_id)
|
||||
rounding = self.product_uom_id.rounding
|
||||
if float_compare(qty_done, qty_to_consume, 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.') % lines[0].product_id.name)
|
||||
|
||||
def _check_sn_uniqueness(self):
|
||||
""" Alert the user if the serial number as already been produced """
|
||||
if self.product_tracking == 'serial' and self.finished_lot_id:
|
||||
sml = self.env['stock.move.line'].search_count([
|
||||
('lot_id', '=', self.finished_lot_id.id),
|
||||
('location_id.usage', '=', 'production'),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
])
|
||||
if sml:
|
||||
raise UserError(_('This serial number for product %s has already been produced') % self.product_id.name)
|
||||
|
||||
|
||||
class MrpAbstractWorkorderLine(models.AbstractModel):
|
||||
_name = "mrp.abstract.workorder.line"
|
||||
_description = "Abstract model to implement product_produce_line as well as\
|
||||
workorder_line"
|
||||
_check_company_auto = True
|
||||
|
||||
move_id = fields.Many2one('stock.move', check_company=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', required=True, check_company=True)
|
||||
product_tracking = fields.Selection(related="product_id.tracking")
|
||||
lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Lot/Serial Number',
|
||||
check_company=True,
|
||||
domain="[('product_id', '=', product_id), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]")
|
||||
qty_to_consume = fields.Float('To Consume', digits='Product Unit of Measure')
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure')
|
||||
qty_done = fields.Float('Consumed', digits='Product Unit of Measure')
|
||||
qty_reserved = fields.Float('Reserved', digits='Product Unit of Measure')
|
||||
company_id = fields.Many2one('res.company', compute='_compute_company_id')
|
||||
|
||||
@api.onchange('lot_id')
|
||||
def _onchange_lot_id(self):
|
||||
""" When the user is encoding a produce line for a tracked product, we apply some logic to
|
||||
help him. This onchange will automatically switch `qty_done` to 1.0.
|
||||
"""
|
||||
if self.product_id.tracking == 'serial':
|
||||
self.qty_done = 1
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id and not self.move_id:
|
||||
self.product_uom_id = self.product_id.uom_id
|
||||
|
||||
@api.onchange('qty_done')
|
||||
def _onchange_qty_done(self):
|
||||
""" When the user is encoding a produce line for a tracked product, we apply some logic to
|
||||
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
|
||||
"""
|
||||
res = {}
|
||||
if self.product_id.tracking == 'serial' and not float_is_zero(self.qty_done, self.product_uom_id.rounding):
|
||||
if float_compare(self.qty_done, 1.0, precision_rounding=self.product_uom_id.rounding) != 0:
|
||||
message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
|
||||
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||
return res
|
||||
|
||||
def _compute_company_id(self):
|
||||
for line in self:
|
||||
line.company_id = line._get_production().company_id
|
||||
|
||||
def _update_move_lines(self):
|
||||
""" 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)
|
||||
else:
|
||||
move_lines = self.move_id.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_ids)
|
||||
|
||||
# Sanity check: if the product is a serial number and `lot` is already present in the other
|
||||
# consumed move lines, raise.
|
||||
if self.product_id.tracking != 'none' and not self.lot_id:
|
||||
raise UserError(_('Please enter a lot or serial number for %s !' % self.product_id.display_name))
|
||||
|
||||
# Update reservation and quantity done
|
||||
for ml in move_lines:
|
||||
rounding = ml.product_uom_id.rounding
|
||||
if float_compare(self.qty_done, 0, precision_rounding=rounding) <= 0:
|
||||
break
|
||||
quantity_to_process = min(self.qty_done, ml.product_uom_qty - ml.qty_done)
|
||||
self.qty_done -= quantity_to_process
|
||||
|
||||
new_quantity_done = (ml.qty_done + quantity_to_process)
|
||||
# if we produce less than the reserved quantity to produce the finished products
|
||||
# in different lots,
|
||||
# we create different component_move_lines to record which one was used
|
||||
# on which lot of finished product
|
||||
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({
|
||||
'product_uom_qty': new_qty_reserved,
|
||||
'qty_done': 0
|
||||
})
|
||||
|
||||
def _create_extra_move_lines(self):
|
||||
"""Create new sml if quantity produced is bigger than the reserved one"""
|
||||
vals_list = []
|
||||
# apply putaway
|
||||
location_dest_id = self.move_id.location_dest_id._get_putaway_strategy(self.product_id) or self.move_id.location_dest_id
|
||||
quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False)
|
||||
# Search for a sub-locations where the product is available.
|
||||
# Loop on the quants to get the locations. If there is not enough
|
||||
# quantity into stock, we take the move location. Anyway, no
|
||||
# reservation is made, so it is still possible to change it afterwards.
|
||||
for quant in quants:
|
||||
quantity = quant.quantity - quant.reserved_quantity
|
||||
quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id, rounding_method='HALF-UP')
|
||||
rounding = quant.product_uom_id.rounding
|
||||
if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or
|
||||
float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0):
|
||||
continue
|
||||
vals = {
|
||||
'move_id': self.move_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'location_id': quant.location_id.id,
|
||||
'location_dest_id': location_dest_id.id,
|
||||
'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})
|
||||
|
||||
vals_list.append(vals)
|
||||
self.qty_done -= vals['qty_done']
|
||||
# If all the qty_done is distributed, we can close the loop
|
||||
if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0:
|
||||
break
|
||||
|
||||
if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) > 0:
|
||||
vals = {
|
||||
'move_id': self.move_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'location_id': self.move_id.location_id.id,
|
||||
'location_dest_id': location_dest_id.id,
|
||||
'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})
|
||||
|
||||
vals_list.append(vals)
|
||||
|
||||
return vals_list
|
||||
|
||||
def _check_line_sn_uniqueness(self):
|
||||
""" Alert the user if the line serial number as already been consumed/produced """
|
||||
self.ensure_one()
|
||||
if self.product_tracking == 'serial' and self.lot_id:
|
||||
domain = [
|
||||
('lot_id', '=', self.lot_id.id),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
]
|
||||
# Adapt domain and error message in case of component or byproduct
|
||||
if self[self._get_raw_workorder_inverse_name()]:
|
||||
message = _('The serial number %s used for component %s has already been consumed') % (self.lot_id.name, self.product_id.name)
|
||||
co_prod_move_lines = self._get_production().move_raw_ids.move_line_ids
|
||||
co_prod_wo_lines = self[self._get_raw_workorder_inverse_name()].raw_workorder_line_ids
|
||||
domain_unbuild = domain + [
|
||||
('production_id', '=', False),
|
||||
('location_id.usage', '=', 'production')
|
||||
]
|
||||
domain.append(('location_dest_id.usage', '=', 'production'))
|
||||
else:
|
||||
message = _('The serial number %s used for byproduct %s has already been produced') % (self.lot_id.name, self.product_id.name)
|
||||
co_prod_move_lines = self._get_production().move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self._get_production().product_id)
|
||||
co_prod_wo_lines = self[self._get_finished_workoder_inverse_name()].finished_workorder_line_ids
|
||||
domain_unbuild = domain + [
|
||||
('production_id', '=', False),
|
||||
('location_dest_id.usage', '=', 'production')
|
||||
]
|
||||
domain.append(('location_id.usage', '=', 'production'))
|
||||
|
||||
# Check presence of same sn in previous productions
|
||||
duplicates = self.env['stock.move.line'].search_count(domain)
|
||||
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 == self.lot_id)
|
||||
if duplicates:
|
||||
raise UserError(message)
|
||||
# Check presence of same sn in current wizard/workorder
|
||||
duplicates = co_prod_wo_lines.filtered(lambda wol: wol.lot_id == self.lot_id) - self
|
||||
if duplicates:
|
||||
raise UserError(message)
|
||||
|
||||
def _set_move_id(self):
|
||||
""" Check the line has a stock_move on which on the quantity will be
|
||||
transfered at the end of the production """
|
||||
self.ensure_one()
|
||||
# Find move_id that would match
|
||||
mo = self._get_production()
|
||||
if self[self._get_raw_workorder_inverse_name()]:
|
||||
moves = mo.move_raw_ids
|
||||
else:
|
||||
moves = mo.move_finished_ids
|
||||
move_id = moves.filtered(lambda m: m.product_id == self.product_id and m.state not in ('done', 'cancel'))
|
||||
if not move_id:
|
||||
# create a move to assign it to the line
|
||||
if self[self._get_raw_workorder_inverse_name()]:
|
||||
values = mo._get_move_raw_values(self.product_id, self.qty_done, self.product_uom_id)
|
||||
elif self.product_id != mo.product_id:
|
||||
values = mo._get_finished_move_value(self.product_id.id, self.qty_done, self.product_uom_id.id)
|
||||
else:
|
||||
# The line is neither a component nor a byproduct
|
||||
return
|
||||
move_id = self.env['stock.move'].create(values)
|
||||
self.move_id = move_id.id
|
||||
|
||||
def _unreserve_order(self):
|
||||
""" Unreserve line with lower reserved quantity first """
|
||||
self.ensure_one()
|
||||
return (self.qty_reserved,)
|
||||
|
||||
def _get_move_lines(self):
|
||||
return self.move_id.move_line_ids.filtered(lambda ml:
|
||||
ml.lot_id == self.lot_id and ml.product_id == self.product_id)
|
||||
|
||||
def _get_produced_lots(self):
|
||||
return self.move_id in self._get_production().move_raw_ids and self._get_final_lots() and [(4, lot.id) for lot in self._get_final_lots()]
|
||||
|
||||
@api.model
|
||||
def _get_raw_workorder_inverse_name(self):
|
||||
raise NotImplementedError('Method _get_raw_workorder_inverse_name() undefined on %s' % self)
|
||||
|
||||
@api.model
|
||||
def _get_finished_workoder_inverse_name(self):
|
||||
raise NotImplementedError('Method _get_finished_workoder_inverse_name() undefined on %s' % self)
|
||||
|
||||
# To be implemented in specific model
|
||||
def _get_final_lots(self):
|
||||
raise NotImplementedError('Method _get_final_lots() undefined on %s' % self)
|
||||
|
||||
def _get_production(self):
|
||||
raise NotImplementedError('Method _get_production() undefined on %s' % self)
|
||||
@@ -48,10 +48,7 @@ class MrpBom(models.Model):
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_tmpl_id.uom_id.category_id')
|
||||
sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of bills of material.")
|
||||
routing_id = fields.Many2one(
|
||||
'mrp.routing', 'Routing', check_company=True,
|
||||
help="The operations for producing this BoM. When a routing is specified, the production orders will "
|
||||
" be executed through work orders, otherwise everything is processed in the production order itself. ")
|
||||
operation_ids = fields.One2many('mrp.routing.workcenter', 'bom_id', 'Operations', copy=True)
|
||||
ready_to_produce = fields.Selection([
|
||||
('all_available', ' When all components are available'),
|
||||
('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness',
|
||||
@@ -66,11 +63,15 @@ class MrpBom(models.Model):
|
||||
'res.company', 'Company', index=True,
|
||||
default=lambda self: self.env.company)
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('flexible', 'Flexible')],
|
||||
help="Defines if you can consume more or less components than the quantity defined on the BoM.",
|
||||
default='strict',
|
||||
string='Consumption',
|
||||
('flexible', 'Allowed'),
|
||||
('warning', 'Allowed with warning'),
|
||||
('strict', 'Blocked')],
|
||||
help="Defines if you can consume more or less components than the quantity defined on the BoM:\n"
|
||||
" * Allowed: allowed for all manufacturing users.\n"
|
||||
" * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n"
|
||||
" * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.",
|
||||
default='warning',
|
||||
string='Flexible Consumption',
|
||||
required=True
|
||||
)
|
||||
|
||||
@@ -119,11 +120,6 @@ class MrpBom(models.Model):
|
||||
for line in self.bom_line_ids:
|
||||
line.bom_product_template_attribute_value_ids = False
|
||||
|
||||
@api.onchange('routing_id')
|
||||
def onchange_routing_id(self):
|
||||
for line in self.bom_line_ids:
|
||||
line.operation_id = False
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
# prevent to use string as product_tmpl_id
|
||||
@@ -261,12 +257,6 @@ class MrpBomLine(models.Model):
|
||||
sequence = fields.Integer(
|
||||
'Sequence', default=1,
|
||||
help="Gives the sequence order when displaying.")
|
||||
routing_id = fields.Many2one(
|
||||
'mrp.routing', 'Routing',
|
||||
related='bom_id.routing_id', store=True, readonly=False,
|
||||
help="The list of operations to produce the finished product. The routing is mainly used to "
|
||||
"compute work center costs during operations and to plan future loads on work centers "
|
||||
"based on production planning.")
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Parent BoM',
|
||||
index=True, ondelete='cascade', required=True)
|
||||
@@ -276,9 +266,10 @@ class MrpBomLine(models.Model):
|
||||
'product.template.attribute.value', string="Apply on Variants", ondelete='restrict',
|
||||
domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
|
||||
help="BOM Product Variants needed to apply this line.")
|
||||
allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
|
||||
operation_id = fields.Many2one(
|
||||
'mrp.routing.workcenter', 'Consumed in Operation', check_company=True,
|
||||
domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]",
|
||||
domain="[('id', 'in', allowed_operation_ids)]",
|
||||
help="The operation where the components are consumed, or the finished products created.")
|
||||
child_bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Sub BoM', compute='_compute_child_bom_id')
|
||||
@@ -327,6 +318,20 @@ class MrpBomLine(models.Model):
|
||||
for line in self:
|
||||
line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False
|
||||
|
||||
@api.depends('bom_id')
|
||||
def _compute_allowed_operation_ids(self):
|
||||
for bom_line in self:
|
||||
if not bom_line.bom_id.operation_ids:
|
||||
bom_line.allowed_operation_ids = self.env['mrp.routing.workcenter']
|
||||
else:
|
||||
operation_domain = [
|
||||
('id', 'in', bom_line.bom_id.operation_ids.ids),
|
||||
'|',
|
||||
('company_id', '=', bom_line.company_id.id),
|
||||
('company_id', '=', False)
|
||||
]
|
||||
bom_line.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
|
||||
|
||||
@api.onchange('product_uom_id')
|
||||
def onchange_product_uom_id(self):
|
||||
res = {}
|
||||
@@ -403,11 +408,24 @@ class MrpByProduct(models.Model):
|
||||
default=1.0, digits='Product Unit of Measure', required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True)
|
||||
bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade')
|
||||
routing_id = fields.Many2one(
|
||||
'mrp.routing', 'Routing', store=True, related='bom_id.routing_id')
|
||||
allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
|
||||
operation_id = fields.Many2one(
|
||||
'mrp.routing.workcenter', 'Produced in Operation', check_company=True,
|
||||
domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]")
|
||||
domain="[('id', 'in', allowed_operation_ids)]")
|
||||
|
||||
@api.depends('bom_id')
|
||||
def _compute_allowed_operation_ids(self):
|
||||
for byproduct in self:
|
||||
if not byproduct.bom_id.operation_ids:
|
||||
byproduct.allowed_operation_ids = self.env['mrp.routing.workcenter']
|
||||
else:
|
||||
operation_domain = [
|
||||
('id', 'in', byproduct.bom_id.operation_ids.ids),
|
||||
'|',
|
||||
('company_id', '=', byproduct.company_id.id),
|
||||
('company_id', '=', False)
|
||||
]
|
||||
byproduct.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
|
||||
|
||||
@api.onchange('product_id')
|
||||
def onchange_product_id(self):
|
||||
|
||||
+635
-237
File diff suppressed because it is too large
Load Diff
@@ -4,32 +4,6 @@
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class MrpRouting(models.Model):
|
||||
""" Specifies routings of work centers """
|
||||
_name = 'mrp.routing'
|
||||
_description = 'Routings'
|
||||
|
||||
name = fields.Char('Routing', required=True)
|
||||
active = fields.Boolean(
|
||||
'Active', default=True,
|
||||
help="If the active field is set to False, it will allow you to hide the routing without removing it.")
|
||||
code = fields.Char(
|
||||
'Reference',
|
||||
copy=False, default=lambda self: _('New'), readonly=True)
|
||||
note = fields.Text('Description')
|
||||
operation_ids = fields.One2many(
|
||||
'mrp.routing.workcenter', 'routing_id', 'Operations',
|
||||
copy=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', default=lambda self: self.env.company)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if 'code' not in vals or vals['code'] == _('New'):
|
||||
vals['code'] = self.env['ir.sequence'].next_by_code('mrp.routing') or _('New')
|
||||
return super(MrpRouting, self).create(vals)
|
||||
|
||||
|
||||
class MrpRoutingWorkcenter(models.Model):
|
||||
_name = 'mrp.routing.workcenter'
|
||||
_description = 'Work Center Usage'
|
||||
@@ -41,14 +15,14 @@ class MrpRoutingWorkcenter(models.Model):
|
||||
sequence = fields.Integer(
|
||||
'Sequence', default=100,
|
||||
help="Gives the sequence order when displaying a list of routing Work Centers.")
|
||||
routing_id = fields.Many2one(
|
||||
'mrp.routing', 'Parent Routing',
|
||||
index=True, ondelete='cascade', required=True,
|
||||
help="The routing contains all the Work Centers used and for how long. This will create work orders afterwards "
|
||||
"which alters the execution of the manufacturing order.")
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Bill of Material',
|
||||
index=True, ondelete='cascade',
|
||||
help="The Bill of Material this operation is linked to")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
readonly=True, related='routing_id.company_id', store=True)
|
||||
readonly=True, store=True,
|
||||
default=lambda self: self.env.company)
|
||||
worksheet_type = fields.Selection([
|
||||
('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')],
|
||||
string="Work Sheet", default="pdf",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+272
-333
@@ -14,7 +14,7 @@ from odoo.tools import float_compare, float_round, format_datetime
|
||||
class MrpWorkorder(models.Model):
|
||||
_name = 'mrp.workorder'
|
||||
_description = 'Work Order'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'mrp.abstract.workorder']
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def _read_group_workcenter_id(self, workcenters, domain, order):
|
||||
workcenter_ids = self.env.context.get('default_workcenter_id')
|
||||
@@ -25,17 +25,18 @@ class MrpWorkorder(models.Model):
|
||||
name = fields.Char(
|
||||
'Work Order', required=True,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
default=lambda self: self.env.company,
|
||||
required=True, index=True, readonly=True)
|
||||
workcenter_id = fields.Many2one(
|
||||
'mrp.workcenter', 'Work Center', required=True,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)], 'progress': [('readonly', True)]},
|
||||
group_expand='_read_group_workcenter_id', check_company=True)
|
||||
working_state = fields.Selection(
|
||||
string='Workcenter Status', related='workcenter_id.working_state', readonly=False,
|
||||
help='Technical: used in views only')
|
||||
product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True, check_company=True)
|
||||
product_tracking = fields.Selection(related="product_id.tracking")
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True)
|
||||
use_create_components_lots = fields.Boolean(related="production_id.picking_type_id.use_create_components_lots")
|
||||
production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, check_company=True)
|
||||
production_availability = fields.Selection(
|
||||
string='Stock Availability', readonly=True,
|
||||
related='production_id.reservation_state', store=True,
|
||||
@@ -44,12 +45,18 @@ class MrpWorkorder(models.Model):
|
||||
string='Production State', readonly=True,
|
||||
related='production_id.state',
|
||||
help='Technical: used in views only.')
|
||||
production_bom_id = fields.Many2one('mrp.bom', related='production_id.bom_id')
|
||||
qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty')
|
||||
company_id = fields.Many2one(related='production_id.company_id')
|
||||
qty_producing = fields.Float(
|
||||
compute='_compute_qty_producing', inverse='_set_qty_producing',
|
||||
string='Currently Produced Quantity', digits='Product Unit of Measure')
|
||||
qty_remaining = fields.Float('Quantity To Be Produced', compute='_compute_qty_remaining', digits='Product Unit of Measure')
|
||||
qty_produced = fields.Float(
|
||||
'Quantity', default=0.0,
|
||||
readonly=True,
|
||||
digits='Product Unit of Measure',
|
||||
copy=False,
|
||||
help="The number of products already handled by this work order")
|
||||
is_produced = fields.Boolean(string="Has Been Produced",
|
||||
compute='_compute_is_produced')
|
||||
@@ -59,34 +66,32 @@ class MrpWorkorder(models.Model):
|
||||
('progress', 'In Progress'),
|
||||
('done', 'Finished'),
|
||||
('cancel', 'Cancelled')], string='Status',
|
||||
default='pending')
|
||||
default='pending', copy=False, readonly=True)
|
||||
leave_id = fields.Many2one(
|
||||
'resource.calendar.leaves',
|
||||
help='Slot into workcenter calendar once planned',
|
||||
check_company=True)
|
||||
check_company=True, copy=False)
|
||||
date_planned_start = fields.Datetime(
|
||||
'Scheduled Date Start',
|
||||
'Scheduled Start Date',
|
||||
compute='_compute_dates_planned',
|
||||
inverse='_set_dates_planned',
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
store=True,
|
||||
tracking=True)
|
||||
store=True, tracking=True, copy=False)
|
||||
date_planned_finished = fields.Datetime(
|
||||
'Scheduled Date Finished',
|
||||
'Scheduled End Date',
|
||||
compute='_compute_dates_planned',
|
||||
inverse='_set_dates_planned',
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
store=True,
|
||||
tracking=True)
|
||||
store=True, tracking=True, copy=False)
|
||||
date_start = fields.Datetime(
|
||||
'Effective Start Date',
|
||||
'Start Date', copy=False,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
date_finished = fields.Datetime(
|
||||
'Effective End Date',
|
||||
'End Date', copy=False,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
|
||||
duration_expected = fields.Float(
|
||||
'Expected Duration', digits=(16, 2),
|
||||
'Expected Duration', digits=(16, 2), default=60.0,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
help="Expected duration (in minutes)")
|
||||
duration = fields.Float(
|
||||
@@ -101,8 +106,7 @@ class MrpWorkorder(models.Model):
|
||||
progress = fields.Float('Progress Done (%)', digits=(16, 2), compute='_compute_progress')
|
||||
|
||||
operation_id = fields.Many2one(
|
||||
'mrp.routing.workcenter', 'Operation',
|
||||
check_company=True)
|
||||
'mrp.routing.workcenter', 'Operation', check_company=True)
|
||||
# Should be used differently as BoM can change in the meantime
|
||||
worksheet = fields.Binary(
|
||||
'Worksheet', related='operation_id.worksheet', readonly=True)
|
||||
@@ -121,10 +125,11 @@ class MrpWorkorder(models.Model):
|
||||
'stock.move.line', 'workorder_id', 'Moves to Track',
|
||||
help="Inventory moves for which you must scan a lot number at this work order")
|
||||
finished_lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Lot/Serial Number', domain="[('id', 'in', allowed_lots_domain)]",
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, check_company=True)
|
||||
'stock.production.lot', string='Lot/Serial Number', compute='_compute_finished_lot_id',
|
||||
inverse='_set_finished_lot_id', domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]",
|
||||
check_company=True)
|
||||
time_ids = fields.One2many(
|
||||
'mrp.workcenter.productivity', 'workorder_id')
|
||||
'mrp.workcenter.productivity', 'workorder_id', copy=False)
|
||||
is_user_working = fields.Boolean(
|
||||
'Is the Current User Working', compute='_compute_working_users',
|
||||
help="Technical field indicating whether the current user is working. ")
|
||||
@@ -135,15 +140,16 @@ class MrpWorkorder(models.Model):
|
||||
scrap_ids = fields.One2many('stock.scrap', 'workorder_id')
|
||||
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
|
||||
production_date = fields.Datetime('Production Date', related='production_id.date_planned_start', store=True, readonly=False)
|
||||
raw_workorder_line_ids = fields.One2many('mrp.workorder.line',
|
||||
'raw_workorder_id', string='Components')
|
||||
finished_workorder_line_ids = fields.One2many('mrp.workorder.line',
|
||||
'finished_workorder_id', string='By-products')
|
||||
allowed_lots_domain = fields.One2many(comodel_name='stock.production.lot', compute="_compute_allowed_lots_domain")
|
||||
is_finished_lines_editable = fields.Boolean(compute='_compute_is_finished_lines_editable')
|
||||
json_popover = fields.Char('Popover Data JSON', compute='_compute_json_popover')
|
||||
show_json_popover = fields.Boolean('Show Popover?', compute='_compute_json_popover')
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('warning', 'Warning'),
|
||||
('flexible', 'Flexible')],
|
||||
required=True,
|
||||
)
|
||||
|
||||
@api.depends('production_state', 'date_planned_start', 'date_planned_finished')
|
||||
def _compute_json_popover(self):
|
||||
previous_wo_data = self.env['mrp.workorder'].read_group(
|
||||
[('next_work_order_id', 'in', self.ids)],
|
||||
@@ -154,9 +160,14 @@ class MrpWorkorder(models.Model):
|
||||
'date_planned_start': x['date_planned_start'],
|
||||
'date_planned_finished': x['date_planned_finished']})
|
||||
for x in previous_wo_data])
|
||||
conflicted_dict = self._get_conflicted_workorder_ids()
|
||||
if self.ids:
|
||||
conflicted_dict = self._get_conflicted_workorder_ids()
|
||||
for wo in self:
|
||||
infos = []
|
||||
if not wo.date_planned_start or not wo.date_planned_finished or not wo.ids:
|
||||
wo.show_json_popover = False
|
||||
wo.json_popover = False
|
||||
continue
|
||||
if wo.state in ['pending', 'ready']:
|
||||
previous_wo = previous_wo_dict.get(wo.id)
|
||||
prev_start = previous_wo and previous_wo['date_planned_start'] or False
|
||||
@@ -194,6 +205,25 @@ class MrpWorkorder(models.Model):
|
||||
'replan': color_icon not in [False, 'text-primary']
|
||||
})
|
||||
|
||||
@api.depends('production_id.lot_producing_id')
|
||||
def _compute_finished_lot_id(self):
|
||||
for workorder in self:
|
||||
workorder.finished_lot_id = workorder.production_id.lot_producing_id
|
||||
|
||||
def _set_finished_lot_id(self):
|
||||
for workorder in self:
|
||||
workorder.production_id.lot_producing_id = workorder.finished_lot_id
|
||||
|
||||
@api.depends('production_id.qty_producing')
|
||||
def _compute_qty_producing(self):
|
||||
for workorder in self:
|
||||
workorder.qty_producing = workorder.production_id.qty_producing
|
||||
|
||||
def _set_qty_producing(self):
|
||||
for workorder in self:
|
||||
workorder.production_id.qty_producing = workorder.qty_producing
|
||||
workorder.production_id._set_qty_producing()
|
||||
|
||||
# Both `date_planned_start` and `date_planned_finished` are related fields on `leave_id`. Let's say
|
||||
# we slide a workorder on a gantt view, a single call to write is made with both
|
||||
# fields Changes. As the ORM doesn't batch the write on related fields and instead
|
||||
@@ -214,76 +244,6 @@ class MrpWorkorder(models.Model):
|
||||
'date_to': date_to,
|
||||
})
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_is_finished_lines_editable(self):
|
||||
for workorder in self:
|
||||
if self.user_has_groups('mrp.group_mrp_byproducts') and workorder.state not in ('cancel', 'done'):
|
||||
workorder.is_finished_lines_editable = True
|
||||
else:
|
||||
workorder.is_finished_lines_editable = False
|
||||
|
||||
@api.onchange('finished_lot_id')
|
||||
def _onchange_finished_lot_id(self):
|
||||
"""When the user changes the lot being currently produced, suggest
|
||||
a quantity to produce consistent with the previous workorders. """
|
||||
previous_wo = self.env['mrp.workorder'].search([
|
||||
('next_work_order_id', '=', self.id)
|
||||
])
|
||||
if previous_wo:
|
||||
line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
|
||||
if line:
|
||||
self.qty_producing = line.qty_done
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
diff = self.date_planned_finished - self.date_planned_start
|
||||
self.duration_expected = diff.total_seconds() / 60
|
||||
|
||||
@api.depends('production_id.workorder_ids.finished_workorder_line_ids',
|
||||
'production_id.workorder_ids.finished_workorder_line_ids.qty_done',
|
||||
'production_id.workorder_ids.finished_workorder_line_ids.lot_id')
|
||||
def _compute_allowed_lots_domain(self):
|
||||
""" Check if all the finished products has been assigned to a serial
|
||||
number or a lot in other workorders. If yes, restrict the selectable lot
|
||||
to the lot/sn used in other workorders.
|
||||
"""
|
||||
productions = self.mapped('production_id')
|
||||
treated = self.browse()
|
||||
for production in productions:
|
||||
if production.product_id.tracking == 'none':
|
||||
continue
|
||||
|
||||
rounding = production.product_uom_id.rounding
|
||||
finished_workorder_lines = production.workorder_ids.mapped('finished_workorder_line_ids').filtered(lambda wl: wl.product_id == production.product_id)
|
||||
qties_done_per_lot = defaultdict(list)
|
||||
for finished_workorder_line in finished_workorder_lines:
|
||||
# It is possible to have finished workorder lines without a lot (eg using the dummy
|
||||
# test type). Ignore them when computing the allowed lots.
|
||||
if finished_workorder_line.lot_id:
|
||||
qties_done_per_lot[finished_workorder_line.lot_id.id].append(finished_workorder_line.qty_done)
|
||||
|
||||
qty_to_produce = production.product_qty
|
||||
allowed_lot_ids = self.env['stock.production.lot']
|
||||
qty_produced = sum([max(qty_dones) for qty_dones in qties_done_per_lot.values()])
|
||||
if float_compare(qty_produced, qty_to_produce, precision_rounding=rounding) < 0:
|
||||
# If we haven't produced enough, all lots are available
|
||||
allowed_lot_ids = self.env['stock.production.lot'].search([
|
||||
('product_id', '=', production.product_id.id),
|
||||
('company_id', '=', production.company_id.id),
|
||||
])
|
||||
else:
|
||||
# If we produced enough, only the already produced lots are available
|
||||
allowed_lot_ids = self.env['stock.production.lot'].browse(qties_done_per_lot.keys())
|
||||
workorders = production.workorder_ids.filtered(lambda wo: wo.state not in ('done', 'cancel'))
|
||||
for workorder in workorders:
|
||||
if workorder.product_tracking == 'serial':
|
||||
workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.filtered(lambda wl: wl.product_id == production.product_id).mapped('lot_id')
|
||||
else:
|
||||
workorder.allowed_lots_domain = allowed_lot_ids
|
||||
treated |= workorder
|
||||
(self - treated).allowed_lots_domain = False
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for wo in self:
|
||||
@@ -297,12 +257,17 @@ class MrpWorkorder(models.Model):
|
||||
# Removes references to workorder to avoid Validation Error
|
||||
(self.mapped('move_raw_ids') | self.mapped('move_finished_ids')).write({'workorder_id': False})
|
||||
self.mapped('leave_id').unlink()
|
||||
return super(MrpWorkorder, self).unlink()
|
||||
mo_dirty = self.production_id.filtered(lambda mo: mo.state in ("confirmed", "progress", "to_close"))
|
||||
res = super().unlink()
|
||||
# We need to go through `_action_confirm` for all workorders of the current productions to
|
||||
# make sure the links between them are correct (`next_work_order_id` could be obsolete now).
|
||||
mo_dirty.workorder_ids._action_confirm()
|
||||
return res
|
||||
|
||||
@api.depends('production_id.product_qty', 'qty_produced')
|
||||
@api.depends('production_id.product_qty', 'qty_produced', 'production_id.product_uom_id')
|
||||
def _compute_is_produced(self):
|
||||
self.is_produced = False
|
||||
for order in self.filtered(lambda p: p.production_id):
|
||||
for order in self.filtered(lambda p: p.production_id and p.production_id.product_uom_id):
|
||||
rounding = order.production_id.product_uom_id.rounding
|
||||
order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0
|
||||
|
||||
@@ -347,11 +312,27 @@ class MrpWorkorder(models.Model):
|
||||
for workorder in self:
|
||||
workorder.scrap_count = count_data.get(workorder.id, 0)
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
diff = self.date_planned_finished - self.date_planned_start
|
||||
self.duration_expected = diff.total_seconds() / 60
|
||||
|
||||
@api.onchange('operation_id')
|
||||
def _onchange_operation_id(self):
|
||||
if self.operation_id:
|
||||
self.name = self.operation_id.name
|
||||
self.workcenter_id = self.operation_id.workcenter_id.id
|
||||
|
||||
@api.onchange('date_planned_start', 'duration_expected')
|
||||
def _onchange_date_planned_start(self):
|
||||
if self.date_planned_start and self.duration_expected:
|
||||
self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected)
|
||||
|
||||
@api.onchange('operation_id', 'workcenter_id', 'qty_production')
|
||||
def _onchange_expected_duration(self):
|
||||
self.duration_expected = self._get_duration_expected()
|
||||
|
||||
def write(self, values):
|
||||
if 'production_id' in values:
|
||||
raise UserError(_('You cannot link this work order to another manufacturing order.'))
|
||||
@@ -361,7 +342,7 @@ class MrpWorkorder(models.Model):
|
||||
if workorder.state in ('progress', 'done', 'cancel'):
|
||||
raise UserError(_('You cannot change the workcenter of a work order that is in progress or done.'))
|
||||
workorder.leave_id.resource_id = self.env['mrp.workcenter'].browse(values['workcenter_id']).resource_id
|
||||
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
|
||||
if any(k not in ['time_ids', 'duration_expected', 'next_work_order_id'] for k in values.keys()) and any(workorder.state == 'done' for workorder in self):
|
||||
raise UserError(_('You can not change the finished work order.'))
|
||||
if 'date_planned_start' in values or 'date_planned_finished' in values:
|
||||
for workorder in self:
|
||||
@@ -372,196 +353,78 @@ class MrpWorkorder(models.Model):
|
||||
# Update MO dates if the start date of the first WO or the
|
||||
# finished date of the last WO is update.
|
||||
if workorder == workorder.production_id.workorder_ids[0] and 'date_planned_start' in values:
|
||||
workorder.production_id.with_context(force_date=True).write({
|
||||
'date_planned_start': fields.Datetime.to_datetime(values['date_planned_start'])
|
||||
})
|
||||
if values['date_planned_start']:
|
||||
workorder.production_id.with_context(force_date=True).write({
|
||||
'date_planned_start': fields.Datetime.to_datetime(values['date_planned_start'])
|
||||
})
|
||||
if workorder == workorder.production_id.workorder_ids[-1] and 'date_planned_finished' in values:
|
||||
workorder.production_id.with_context(force_date=True).write({
|
||||
'date_planned_finished': fields.Datetime.to_datetime(values['date_planned_finished'])
|
||||
})
|
||||
if values['date_planned_finished']:
|
||||
workorder.production_id.with_context(force_date=True).write({
|
||||
'date_planned_finished': fields.Datetime.to_datetime(values['date_planned_finished'])
|
||||
})
|
||||
return super(MrpWorkorder, self).write(values)
|
||||
|
||||
def _generate_wo_lines(self):
|
||||
""" Generate workorder line """
|
||||
self.ensure_one()
|
||||
moves = (self.move_raw_ids | self.move_finished_ids).filtered(
|
||||
lambda move: move.state not in ('done', 'cancel')
|
||||
)
|
||||
for move in moves:
|
||||
qty_to_consume = self._prepare_component_quantity(move, self.qty_producing)
|
||||
line_values = self._generate_lines_values(move, qty_to_consume)
|
||||
self.env['mrp.workorder.line'].create(line_values)
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
res = super().create(values)
|
||||
# Auto-confirm manually added workorders.
|
||||
# We need to go through `_action_confirm` for all workorders of the current productions to
|
||||
# make sure the links between them are correct.
|
||||
to_confirm = res.filtered(lambda wo: wo.production_id.state in ("confirmed", "progress", "to_close"))
|
||||
to_confirm = to_confirm.production_id.workorder_ids
|
||||
to_confirm._action_confirm()
|
||||
return res
|
||||
|
||||
def _apply_update_workorder_lines(self):
|
||||
""" update existing line on the workorder. It could be trigger manually
|
||||
after a modification of qty_producing.
|
||||
"""
|
||||
self.ensure_one()
|
||||
line_values = self._update_workorder_lines()
|
||||
self.env['mrp.workorder.line'].create(line_values['to_create'])
|
||||
if line_values['to_delete']:
|
||||
line_values['to_delete'].unlink()
|
||||
for line, vals in line_values['to_update'].items():
|
||||
line.write(vals)
|
||||
|
||||
def _refresh_wo_lines(self):
|
||||
""" Modify exisiting workorder line in order to match the reservation on
|
||||
stock move line. The strategy is to remove the line that were not
|
||||
processed yet then call _generate_lines_values that recreate workorder
|
||||
line depending the reservation.
|
||||
"""
|
||||
def _action_confirm(self):
|
||||
workorders_by_production = defaultdict(lambda: self.env['mrp.workorder'])
|
||||
for workorder in self:
|
||||
raw_moves = workorder.move_raw_ids.filtered(
|
||||
lambda move: move.state not in ('done', 'cancel')
|
||||
)
|
||||
wl_to_unlink = self.env['mrp.workorder.line']
|
||||
for move in raw_moves:
|
||||
rounding = move.product_uom.rounding
|
||||
qty_already_consumed = 0.0
|
||||
workorder_lines = workorder.raw_workorder_line_ids.filtered(lambda w: w.move_id == move)
|
||||
for wl in workorder_lines:
|
||||
if not wl.qty_done:
|
||||
wl_to_unlink |= wl
|
||||
continue
|
||||
workorders_by_production[workorder.production_id] |= workorder
|
||||
|
||||
qty_already_consumed += wl.qty_done
|
||||
qty_to_consume = self._prepare_component_quantity(move, workorder.qty_producing)
|
||||
wl_to_unlink.unlink()
|
||||
if float_compare(qty_to_consume, qty_already_consumed, precision_rounding=rounding) > 0:
|
||||
line_values = workorder._generate_lines_values(move, qty_to_consume - qty_already_consumed)
|
||||
self.env['mrp.workorder.line'].create(line_values)
|
||||
for production, workorders in workorders_by_production.items():
|
||||
workorders_by_bom = defaultdict(lambda: self.env['mrp.workorder'])
|
||||
bom = self.env['mrp.bom']
|
||||
moves = production.move_raw_ids | production.move_finished_ids
|
||||
|
||||
def _defaults_from_finished_workorder_line(self, reference_lot_lines):
|
||||
for r_line in reference_lot_lines:
|
||||
# see which lot we could suggest and its related qty_producing
|
||||
if not r_line.lot_id:
|
||||
continue
|
||||
candidates = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == r_line.lot_id)
|
||||
rounding = self.product_uom_id.rounding
|
||||
if not candidates:
|
||||
self.write({
|
||||
'finished_lot_id': r_line.lot_id.id,
|
||||
'qty_producing': r_line.qty_done,
|
||||
for workorder in self:
|
||||
if workorder.operation_id.bom_id:
|
||||
bom = workorder.operation_id.bom_id
|
||||
if not bom:
|
||||
bom = workorder.production_id.bom_id
|
||||
previous_workorder = workorders_by_bom[bom][-1:]
|
||||
previous_workorder.next_work_order_id = workorder.id
|
||||
workorders_by_bom[bom] |= workorder
|
||||
|
||||
moves.filtered(lambda m: m.operation_id == workorder.operation_id).write({
|
||||
'workorder_id': workorder.id
|
||||
})
|
||||
return True
|
||||
elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0:
|
||||
self.write({
|
||||
'finished_lot_id': r_line.lot_id.id,
|
||||
'qty_producing': r_line.qty_done - candidates.qty_done,
|
||||
})
|
||||
return True
|
||||
return False
|
||||
|
||||
def record_production(self):
|
||||
if not self:
|
||||
return True
|
||||
exploded_boms, dummy = production.bom_id.explode(production.product_id, 1, picking_type=production.bom_id.picking_type_id)
|
||||
exploded_boms = {b[0]: b[1] for b in exploded_boms}
|
||||
for move in moves:
|
||||
if move.workorder_id:
|
||||
continue
|
||||
bom = move.bom_line_id.bom_id
|
||||
while bom and bom not in workorders_by_bom:
|
||||
bom_data = exploded_boms.get(bom, {})
|
||||
bom = bom_data.get('parent_line') and bom_data['parent_line'].bom_id or False
|
||||
if bom in workorders_by_bom:
|
||||
move.write({
|
||||
'workorder_id': workorders_by_bom[bom][-1:].id
|
||||
})
|
||||
else:
|
||||
move.write({
|
||||
'workorder_id': workorders_by_bom[production.bom_id][-1:].id
|
||||
})
|
||||
|
||||
self.ensure_one()
|
||||
self._check_sn_uniqueness()
|
||||
self._check_company()
|
||||
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
|
||||
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
|
||||
if self.production_id.product_id.tracking != 'none' and not self.finished_lot_id and self.move_raw_ids:
|
||||
raise UserError(_('You should provide a lot for the final product'))
|
||||
if 'check_ids' not in self:
|
||||
for line in self.raw_workorder_line_ids | self.finished_workorder_line_ids:
|
||||
line._check_line_sn_uniqueness()
|
||||
# If last work order, then post lots used
|
||||
if not self.next_work_order_id:
|
||||
self._update_finished_move()
|
||||
|
||||
# Transfer quantities from temporary to final move line or make them final
|
||||
self._update_moves()
|
||||
|
||||
# Transfer lot (if present) and quantity produced to a finished workorder line
|
||||
if self.product_tracking != 'none':
|
||||
self._create_or_update_finished_line()
|
||||
|
||||
# Update workorder quantity produced
|
||||
self.qty_produced += self.qty_producing
|
||||
|
||||
# Suggest a finished lot on the next workorder
|
||||
if self.next_work_order_id and self.product_tracking != 'none' and (not self.next_work_order_id.finished_lot_id or self.next_work_order_id.finished_lot_id == self.finished_lot_id):
|
||||
self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids)
|
||||
# As we may have changed the quantity to produce on the next workorder,
|
||||
# make sure to update its wokorder lines
|
||||
self.next_work_order_id._apply_update_workorder_lines()
|
||||
|
||||
# One a piece is produced, you can launch the next work order
|
||||
self._start_nextworkorder()
|
||||
|
||||
# Test if the production is done
|
||||
rounding = self.production_id.product_uom_id.rounding
|
||||
if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0:
|
||||
previous_wo = self.env['mrp.workorder']
|
||||
if self.product_tracking != 'none':
|
||||
previous_wo = self.env['mrp.workorder'].search([
|
||||
('next_work_order_id', '=', self.id)
|
||||
])
|
||||
candidate_found_in_previous_wo = False
|
||||
if previous_wo:
|
||||
candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids)
|
||||
if not candidate_found_in_previous_wo:
|
||||
# self is the first workorder
|
||||
self.qty_producing = self.qty_remaining
|
||||
self.finished_lot_id = False
|
||||
if self.product_tracking == 'serial':
|
||||
self.qty_producing = 1
|
||||
|
||||
self._apply_update_workorder_lines()
|
||||
else:
|
||||
self.qty_producing = 0
|
||||
self.button_finish()
|
||||
return True
|
||||
for workorders in workorders_by_bom.values():
|
||||
if workorders[0].state == 'pending':
|
||||
workorders[0].state = 'ready'
|
||||
for workorder in workorders:
|
||||
workorder._start_nextworkorder()
|
||||
|
||||
def _get_byproduct_move_to_update(self):
|
||||
return self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
|
||||
|
||||
def _create_or_update_finished_line(self):
|
||||
"""
|
||||
1. Check that the final lot and the quantity producing is valid regarding
|
||||
other workorders of this production
|
||||
2. Save final lot and quantity producing to suggest on next workorder
|
||||
"""
|
||||
self.ensure_one()
|
||||
final_lot_quantity = self.qty_production
|
||||
rounding = self.product_uom_id.rounding
|
||||
# Get the max quantity possible for current lot in other workorders
|
||||
for workorder in (self.production_id.workorder_ids - self):
|
||||
# We add the remaining quantity to the produced quantity for the
|
||||
# current lot. For 5 finished products: if in the first wo it
|
||||
# creates 4 lot A and 1 lot B and in the second it create 3 lot A
|
||||
# and it remains 2 units to product, it could produce 5 lot A.
|
||||
# In this case we select 4 since it would conflict with the first
|
||||
# workorder otherwise.
|
||||
line = workorder.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.finished_lot_id)
|
||||
line_without_lot = workorder.finished_workorder_line_ids.filtered(lambda line: line.product_id == workorder.product_id and not line.lot_id)
|
||||
quantity_remaining = workorder.qty_remaining + line_without_lot.qty_done
|
||||
quantity = line.qty_done + quantity_remaining
|
||||
if line and float_compare(quantity, final_lot_quantity, precision_rounding=rounding) <= 0:
|
||||
final_lot_quantity = quantity
|
||||
elif float_compare(quantity_remaining, final_lot_quantity, precision_rounding=rounding) < 0:
|
||||
final_lot_quantity = quantity_remaining
|
||||
|
||||
# final lot line for this lot on this workorder.
|
||||
current_lot_lines = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.finished_lot_id)
|
||||
|
||||
# this lot has already been produced
|
||||
if float_compare(final_lot_quantity, current_lot_lines.qty_done + self.qty_producing, precision_rounding=rounding) < 0:
|
||||
raise UserError(_('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one') %
|
||||
(final_lot_quantity, self.product_id.uom_id.name, self.finished_lot_id.name, current_lot_lines.qty_done + self.qty_producing))
|
||||
|
||||
# Update workorder line that regiter final lot created
|
||||
if not current_lot_lines:
|
||||
current_lot_lines = self.env['mrp.workorder.line'].create({
|
||||
'finished_workorder_id': self.id,
|
||||
'product_id': self.product_id.id,
|
||||
'lot_id': self.finished_lot_id.id,
|
||||
'qty_done': self.qty_producing,
|
||||
})
|
||||
else:
|
||||
current_lot_lines.qty_done += self.qty_producing
|
||||
|
||||
def _start_nextworkorder(self):
|
||||
rounding = self.product_id.uom_id.rounding
|
||||
if self.next_work_order_id.state == 'pending' and (
|
||||
@@ -570,6 +433,8 @@ class MrpWorkorder(models.Model):
|
||||
(self.operation_id.batch == 'yes' and
|
||||
float_compare(self.operation_id.batch_size, self.qty_produced, precision_rounding=rounding) <= 0)):
|
||||
self.next_work_order_id.state = 'ready'
|
||||
if self.state == 'done' and self.next_work_order_id.state == 'pending':
|
||||
self.next_work_order_id.state = 'ready'
|
||||
|
||||
@api.model
|
||||
def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None):
|
||||
@@ -615,9 +480,12 @@ class MrpWorkorder(models.Model):
|
||||
if self.state in ('done', 'cancel'):
|
||||
return True
|
||||
|
||||
if self.product_tracking == 'serial':
|
||||
self.qty_producing = 1.0
|
||||
|
||||
# Need a loss in case of the real time exceeding the expected
|
||||
timeline = self.env['mrp.workcenter.productivity']
|
||||
if self.duration < self.duration_expected:
|
||||
if not self.duration_expected or self.duration < self.duration_expected:
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
|
||||
if not len(loss_id):
|
||||
raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
|
||||
@@ -632,7 +500,7 @@ class MrpWorkorder(models.Model):
|
||||
timeline.create({
|
||||
'workorder_id': self.id,
|
||||
'workcenter_id': self.workcenter_id.id,
|
||||
'description': _('Time Tracking: ')+self.env.user.name,
|
||||
'description': _('Time Tracking: ') + self.env.user.name,
|
||||
'loss_id': loss_id[0].id,
|
||||
'date_start': datetime.now(),
|
||||
'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id
|
||||
@@ -640,26 +508,47 @@ class MrpWorkorder(models.Model):
|
||||
})
|
||||
if self.state == 'progress':
|
||||
return True
|
||||
start_date = datetime.now()
|
||||
vals = {
|
||||
'state': 'progress',
|
||||
'date_start': start_date,
|
||||
}
|
||||
if not self.leave_id:
|
||||
leave = self.env['resource.calendar.leaves'].create({
|
||||
'name': self.display_name,
|
||||
'calendar_id': self.workcenter_id.resource_calendar_id.id,
|
||||
'date_from': start_date,
|
||||
'date_to': start_date + relativedelta(minutes=self.duration_expected),
|
||||
'resource_id': self.workcenter_id.resource_id.id,
|
||||
'time_type': 'other'
|
||||
})
|
||||
vals['leave_id'] = leave.id
|
||||
return self.write(vals)
|
||||
else:
|
||||
start_date = datetime.now()
|
||||
vals = {
|
||||
'state': 'progress',
|
||||
'date_start': start_date,
|
||||
'date_planned_start': start_date,
|
||||
}
|
||||
vals['date_planned_start'] = start_date
|
||||
if self.date_planned_finished < start_date:
|
||||
vals['date_planned_finished'] = start_date
|
||||
return self.write(vals)
|
||||
|
||||
def button_finish(self):
|
||||
self.ensure_one()
|
||||
self.end_all()
|
||||
end_date = datetime.now()
|
||||
return self.write({
|
||||
'state': 'done',
|
||||
'date_finished': end_date,
|
||||
'date_planned_finished': end_date
|
||||
})
|
||||
for workorder in self:
|
||||
if workorder.state in ('done', 'cancel'):
|
||||
continue
|
||||
workorder.end_all()
|
||||
vals = {
|
||||
'state': 'done',
|
||||
'date_finished': end_date,
|
||||
'date_planned_finished': end_date
|
||||
}
|
||||
if not workorder.date_start:
|
||||
vals['date_start'] = end_date
|
||||
if not workorder.date_planned_start or end_date < workorder.date_planned_start:
|
||||
vals['date_planned_start'] = end_date
|
||||
workorder.write(vals)
|
||||
|
||||
workorder._start_nextworkorder()
|
||||
return True
|
||||
|
||||
def end_previous(self, doall=False):
|
||||
"""
|
||||
@@ -751,11 +640,32 @@ class MrpWorkorder(models.Model):
|
||||
action['domain'] = [('workorder_id', '=', self.id)]
|
||||
return action
|
||||
|
||||
def action_open_wizard(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('mrp.mrp_workorder_mrp_production_form').read()[0]
|
||||
action['res_id'] = self.id
|
||||
return action
|
||||
|
||||
@api.depends('qty_production', 'qty_produced')
|
||||
def _compute_qty_remaining(self):
|
||||
for wo in self:
|
||||
wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
|
||||
|
||||
def _get_duration_expected(self, alternative_workcenter=False):
|
||||
self.ensure_one()
|
||||
if not self.workcenter_id:
|
||||
return False
|
||||
qty_production = self.production_id.product_uom_id._compute_quantity(self.qty_production, self.production_id.product_id.uom_id)
|
||||
cycle_number = float_round(qty_production / self.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
|
||||
if alternative_workcenter:
|
||||
# TODO : find a better alternative : the settings of workcenter can change
|
||||
duration_expected_working = (self.duration_expected - self.workcenter_id.time_start - self.workcenter_id.time_stop) * self.workcenter_id.time_efficiency / (100.0 * cycle_number)
|
||||
if duration_expected_working < 0:
|
||||
duration_expected_working = 0
|
||||
return alternative_workcenter.time_start + alternative_workcenter.time_stop + cycle_number * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency
|
||||
time_cycle = self.operation_id and self.operation_id.time_cycle or 60.0
|
||||
return self.workcenter_id.time_start + self.workcenter_id.time_stop + cycle_number * time_cycle * 100.0 / self.workcenter_id.time_efficiency
|
||||
|
||||
def _get_conflicted_workorder_ids(self):
|
||||
"""Get conlicted workorder(s) with self.
|
||||
|
||||
@@ -782,46 +692,75 @@ class MrpWorkorder(models.Model):
|
||||
res[wo1].append(wo2)
|
||||
return res
|
||||
|
||||
|
||||
class MrpWorkorderLine(models.Model):
|
||||
_name = 'mrp.workorder.line'
|
||||
_inherit = ["mrp.abstract.workorder.line"]
|
||||
_description = "Workorder move line"
|
||||
|
||||
raw_workorder_id = fields.Many2one('mrp.workorder', 'Component for Workorder',
|
||||
ondelete='cascade')
|
||||
finished_workorder_id = fields.Many2one('mrp.workorder', 'Finished Product for Workorder',
|
||||
ondelete='cascade')
|
||||
|
||||
@api.onchange('qty_to_consume')
|
||||
def _onchange_qty_to_consume(self):
|
||||
# Update qty_done for products added in ready state
|
||||
wo = self.raw_workorder_id or self.finished_workorder_id
|
||||
if wo.state == 'ready':
|
||||
self.qty_done = self.qty_to_consume
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
for line in res:
|
||||
wo = line.raw_workorder_id
|
||||
if wo and\
|
||||
wo.consumption == 'strict' and\
|
||||
wo.state == 'progress' and\
|
||||
line.product_id.id not in wo.production_id.bom_id.bom_line_ids.product_id.ids:
|
||||
raise UserError(_('You cannot consume additional component as the consumption defined on the Bill of Material is set to "strict"'))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_raw_workorder_inverse_name(self):
|
||||
return 'raw_workorder_id'
|
||||
def _prepare_component_quantity(self, move, qty_producing):
|
||||
""" helper that computes quantity to consume (or to create in case of byproduct)
|
||||
depending on the quantity producing and the move's unit factor"""
|
||||
if move.product_id.tracking == 'serial':
|
||||
uom = move.product_id.uom_id
|
||||
else:
|
||||
uom = move.product_uom
|
||||
return move.product_uom._compute_quantity(
|
||||
qty_producing * move.unit_factor,
|
||||
uom,
|
||||
round=False
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_finished_workoder_inverse_name(self):
|
||||
return 'finished_workorder_id'
|
||||
def _update_finished_move(self):
|
||||
""" Update the finished move & move lines in order to set the finished
|
||||
product lot on it as well as the produced quantity. This method get the
|
||||
information either from the last workorder or from the Produce wizard."""
|
||||
production_move = self.production_id.move_finished_ids.filtered(
|
||||
lambda move: move.product_id == self.product_id and
|
||||
move.state not in ('done', 'cancel')
|
||||
)
|
||||
if production_move and production_move.product_id.tracking != 'none':
|
||||
if not self.finished_lot_id:
|
||||
raise UserError(_('You need to provide a lot for the finished product.'))
|
||||
move_line = production_move.move_line_ids.filtered(
|
||||
lambda line: line.lot_id.id == self.finished_lot_id.id
|
||||
)
|
||||
if move_line:
|
||||
if self.product_id.tracking == 'serial':
|
||||
raise UserError(_('You cannot produce the same serial number twice.'))
|
||||
move_line.product_uom_qty += self.qty_producing
|
||||
move_line.qty_done += self.qty_producing
|
||||
else:
|
||||
location_dest_id = production_move.location_dest_id._get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id
|
||||
move_line.create({
|
||||
'move_id': production_move.id,
|
||||
'product_id': production_move.product_id.id,
|
||||
'lot_id': self.finished_lot_id.id,
|
||||
'product_uom_qty': self.qty_producing,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'qty_done': self.qty_producing,
|
||||
'location_id': production_move.location_id.id,
|
||||
'location_dest_id': location_dest_id,
|
||||
})
|
||||
else:
|
||||
rounding = production_move.product_uom.rounding
|
||||
production_move._set_quantity_done(
|
||||
float_round(self.qty_producing, precision_rounding=rounding)
|
||||
)
|
||||
|
||||
def _get_final_lots(self):
|
||||
return (self.raw_workorder_id or self.finished_workorder_id).finished_lot_id
|
||||
def _strict_consumption_check(self):
|
||||
if self.consumption == 'strict':
|
||||
for move in self.move_raw_ids:
|
||||
qty_done = 0.0
|
||||
for line in move.move_line_ids:
|
||||
qty_done += line.product_uom_id._compute_quantity(line.qty_done, move.product_uom)
|
||||
rounding = move.product_uom_id.rounding
|
||||
if float_compare(qty_done, move.product_uom_qty, precision_rounding=rounding) != 0:
|
||||
raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % move.product_id.name)
|
||||
|
||||
def _get_production(self):
|
||||
return (self.raw_workorder_id or self.finished_workorder_id).production_id
|
||||
def _check_sn_uniqueness(self):
|
||||
""" Alert the user if the serial number as already been produced """
|
||||
if self.product_tracking == 'serial' and self.finished_lot_id:
|
||||
sml = self.env['stock.move.line'].search_count([
|
||||
('lot_id', '=', self.finished_lot_id.id),
|
||||
('location_id.usage', '=', 'production'),
|
||||
('qty_done', '=', 1),
|
||||
('state', '=', 'done')
|
||||
])
|
||||
if sml:
|
||||
raise UserError(_('This serial number for product %s has already been produced') % self.product_id.name)
|
||||
|
||||
@@ -18,6 +18,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_mrp_subcontracting = fields.Boolean("Subcontracting")
|
||||
group_mrp_routings = fields.Boolean("MRP Work Orders",
|
||||
implied_group='mrp.group_mrp_routings')
|
||||
group_locked_by_default = fields.Boolean("Lock Quantities To Consume", implied_group='mrp.group_locked_by_default')
|
||||
|
||||
@api.onchange('use_manufacturing_lead')
|
||||
def _onchange_use_manufacturing_lead(self):
|
||||
|
||||
+112
-21
@@ -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)
|
||||
@@ -76,10 +73,10 @@ class StockMove(models.Model):
|
||||
'mrp.unbuild', 'Disassembly Order', check_company=True)
|
||||
consume_unbuild_id = fields.Many2one(
|
||||
'mrp.unbuild', 'Consumed Disassembly Order', check_company=True)
|
||||
allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids')
|
||||
operation_id = fields.Many2one(
|
||||
'mrp.routing.workcenter', 'Operation To Consume', check_company=True,
|
||||
domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]")
|
||||
routing_id = fields.Many2one(related='raw_material_production_id.routing_id')
|
||||
domain="[('id', 'in', allowed_operation_ids)]")
|
||||
workorder_id = fields.Many2one(
|
||||
'mrp.workorder', 'Work Order To Consume', check_company=True)
|
||||
# Quantities to process, in normalized UoMs
|
||||
@@ -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
|
||||
@@ -118,39 +116,88 @@ class StockMove(models.Model):
|
||||
move.order_finished_lot_ids = False
|
||||
move.finished_lots_exist = False
|
||||
|
||||
@api.depends('raw_material_production_id.bom_id')
|
||||
def _compute_allowed_operation_ids(self):
|
||||
for move in self:
|
||||
if (
|
||||
not move.raw_material_production_id or
|
||||
not move.raw_material_production_id.bom_id or not
|
||||
move.raw_material_production_id.bom_id.operation_ids
|
||||
):
|
||||
move.allowed_operation_ids = self.env['mrp.routing.workcenter']
|
||||
else:
|
||||
operation_domain = [
|
||||
('id', 'in', move.raw_material_production_id.bom_id.operation_ids.ids),
|
||||
'|',
|
||||
('company_id', '=', self.company_id.id),
|
||||
('company_id', '=', False)
|
||||
]
|
||||
move.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
|
||||
|
||||
@api.depends('product_id.tracking')
|
||||
def _compute_needs_lots(self):
|
||||
for move in self:
|
||||
move.needs_lots = move.product_id.tracking != 'none'
|
||||
|
||||
@api.depends('raw_material_production_id.is_locked', 'picking_id.is_locked')
|
||||
@api.depends('raw_material_production_id.is_locked', 'production_id.is_locked')
|
||||
def _compute_is_locked(self):
|
||||
super(StockMove, self)._compute_is_locked()
|
||||
for move in self:
|
||||
if move.raw_material_production_id:
|
||||
move.is_locked = move.raw_material_production_id.is_locked
|
||||
if move.production_id:
|
||||
move.is_locked = move.production_id.is_locked
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_is_done(self):
|
||||
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',
|
||||
'production_id', 'production_id.product_qty', '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)
|
||||
|
||||
@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'
|
||||
@@ -207,6 +254,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'):
|
||||
@@ -215,6 +273,11 @@ class StockMove(models.Model):
|
||||
production._action_cancel()
|
||||
return res
|
||||
|
||||
def _prepare_move_split_vals(self, qty):
|
||||
defaults = super()._prepare_move_split_vals(qty)
|
||||
defaults['workorder_id'] = False
|
||||
return defaults
|
||||
|
||||
def _prepare_phantom_move_values(self, bom_line, product_qty, quantity_done):
|
||||
return {
|
||||
'picking_id': self.picking_id.id if self.picking_id else False,
|
||||
@@ -236,10 +299,10 @@ class StockMove(models.Model):
|
||||
return vals
|
||||
|
||||
def _get_upstream_documents_and_responsibles(self, visited):
|
||||
if self.production_id and self.production_id.state not in ('done', 'cancel'):
|
||||
return [(self.production_id, self.production_id.user_id, visited)]
|
||||
else:
|
||||
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
|
||||
if self.production_id and self.production_id.state not in ('done', 'cancel'):
|
||||
return [(self.production_id, self.production_id.user_id, visited)]
|
||||
else:
|
||||
return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
|
||||
|
||||
def _delay_alert_get_documents(self):
|
||||
res = super(StockMove, self)._delay_alert_get_documents()
|
||||
@@ -250,6 +313,16 @@ class StockMove(models.Model):
|
||||
res = super(StockMove, self)._should_be_assigned()
|
||||
return bool(res and not (self.production_id or self.raw_material_production_id))
|
||||
|
||||
def _should_bypass_set_qty_producing(self):
|
||||
if self.state in ('done', 'cancel'):
|
||||
return True
|
||||
# Do not update extra product quantities
|
||||
if float_is_zero(self.product_uom_qty, precision_rounding=self.product_uom.rounding):
|
||||
return True
|
||||
if self.has_tracking != 'none' or self.state == 'done':
|
||||
return True
|
||||
return False
|
||||
|
||||
def _should_bypass_reservation(self):
|
||||
res = super(StockMove, self)._should_bypass_reservation()
|
||||
return bool(res and not self.production_id)
|
||||
@@ -310,3 +383,21 @@ class StockMove(models.Model):
|
||||
return min(qty_ratios) // 1
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
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
|
||||
self.move_line_ids = self._set_quantity_done_prepare_vals(new_qty)
|
||||
else:
|
||||
self.quantity_done = new_qty
|
||||
return ml_values
|
||||
|
||||
@@ -55,6 +55,8 @@ class StockRule(models.Model):
|
||||
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
|
||||
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values)
|
||||
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
|
||||
self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
|
||||
productions._create_workorder()
|
||||
productions.filtered(lambda p: p.move_raw_ids).action_confirm()
|
||||
|
||||
for production in productions:
|
||||
@@ -140,7 +142,7 @@ class StockRule(models.Model):
|
||||
class ProcurementGroup(models.Model):
|
||||
_inherit = 'procurement.group'
|
||||
|
||||
mrp_production_id = fields.One2many('mrp.production', 'procurement_group_id')
|
||||
mrp_production_ids = fields.One2many('mrp.production', 'procurement_group_id')
|
||||
|
||||
@api.model
|
||||
def run(self, procurements, raise_user_error=True):
|
||||
|
||||
@@ -55,7 +55,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
@api.model
|
||||
def get_operations(self, bom_id=False, qty=0, level=0):
|
||||
bom = self.env['mrp.bom'].browse(bom_id)
|
||||
lines = self._get_operation_line(bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
|
||||
lines = self._get_operation_line(bom, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
|
||||
values = {
|
||||
'bom_id': bom_id,
|
||||
'currency': self.env.company.currency_id,
|
||||
@@ -106,7 +106,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
else:
|
||||
product = bom.product_tmpl_id
|
||||
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', product.id)])
|
||||
operations = self._get_operation_line(bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
|
||||
operations = self._get_operation_line(bom, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
|
||||
company = bom.company_id or self.env.company
|
||||
lines = {
|
||||
'bom': bom,
|
||||
@@ -163,10 +163,10 @@ class ReportBomStructure(models.AbstractModel):
|
||||
total += sub_total
|
||||
return components, total
|
||||
|
||||
def _get_operation_line(self, routing, qty, level):
|
||||
def _get_operation_line(self, bom, qty, level):
|
||||
operations = []
|
||||
total = 0.0
|
||||
for operation in routing.operation_ids:
|
||||
for operation in bom.operation_ids:
|
||||
operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
|
||||
duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
|
||||
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
|
||||
@@ -181,7 +181,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
|
||||
def _get_price(self, bom, factor, product):
|
||||
price = 0
|
||||
if bom.routing_id:
|
||||
if bom.operation_ids:
|
||||
# routing are defined on a BoM and don't have a concept of quantity.
|
||||
# It means that the operation time are defined for the quantity on
|
||||
# the BoM (the user produces a batch of products). E.g the user
|
||||
@@ -189,7 +189,7 @@ class ReportBomStructure(models.AbstractModel):
|
||||
# will be the 5 for a quantity between 1-10, then doubled for
|
||||
# 11-20,...
|
||||
operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP')
|
||||
operations = self._get_operation_line(bom.routing_id, operation_cycle, 0)
|
||||
operations = self._get_operation_line(bom, operation_cycle, 0)
|
||||
price += sum([op['total'] for op in operations])
|
||||
|
||||
for line in bom.bom_line_ids:
|
||||
|
||||
@@ -4,14 +4,12 @@ access_mrp_workcenter_productivity_loss,mrp.workcenter.productivity.loss,model_m
|
||||
access_mrp_workcenter_productivity_loss_type,mrp.workcenter.productivity.loss.type,model_mrp_workcenter_productivity_loss_type,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_workcenter_productivity,mrp.workcenter.productivity,model_mrp_workcenter_productivity,mrp.group_mrp_user,1,1,1,1
|
||||
access_mrp_workcenter,mrp.workcenter,model_mrp_workcenter,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_routing,mrp.routing,model_mrp_routing,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_routing_workcenter,mrp.routing.workcenter,model_mrp_routing_workcenter,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_bom,mrp.bom,model_mrp_bom,group_mrp_user,1,0,0,0
|
||||
access_mrp_bom_line,mrp.bom.line,model_mrp_bom_line,group_mrp_user,1,0,0,0
|
||||
access_mrp_bom_byproduct_user,mrp.bom.byproduct,model_mrp_bom_byproduct,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_production,mrp.production user,model_mrp_production,mrp.group_mrp_user,1,1,1,1
|
||||
access_mrp_workcenter_manager,mrp.workcenter.manager,model_mrp_workcenter,mrp.group_mrp_manager,1,1,1,1
|
||||
access_mrp_routing_manager,mrp.routing.manager,model_mrp_routing,mrp.group_mrp_manager,1,1,1,1
|
||||
access_mrp_routing_workcenter_manager,mrp.routing.workcenter.manager,model_mrp_routing_workcenter,mrp.group_mrp_manager,1,1,1,1
|
||||
access_mrp_bom_manager,mrp.bom.manager,model_mrp_bom,mrp.group_mrp_manager,1,1,1,1
|
||||
access_mrp_bom_line_manager,mrp.bom.line.manager,model_mrp_bom_line,mrp.group_mrp_manager,1,1,1,1
|
||||
@@ -28,8 +26,6 @@ access_product_supplierinfo_user,product.supplierinfo user,product.model_product
|
||||
access_res_partner,res.partner,base.model_res_partner,mrp.group_mrp_user,1,0,0,0
|
||||
access_mrp_workorder_mrp_user,mrp.workorder.user,model_mrp_workorder,mrp.group_mrp_user,1,1,1,1
|
||||
access_mrp_workorder_mrp_manager,mrp.workorder,model_mrp_workorder,mrp.group_mrp_manager,1,1,1,1
|
||||
access_mrp_workorder_line_mrp_user,mrp.workorder.user,model_mrp_workorder_line,mrp.group_mrp_user,1,1,1,1
|
||||
access_mrp_workorder_line_mrp_manager,mrp.workorder,model_mrp_workorder_line,mrp.group_mrp_manager,1,1,1,1
|
||||
access_resource_calendar_leaves_user,mrp.resource.calendar.leaves.user,resource.model_resource_calendar_leaves,mrp.group_mrp_user,1,1,1,1
|
||||
access_resource_calendar_leaves_manager,mrp.resource.calendar.leaves.manager,resource.model_resource_calendar_leaves,mrp.group_mrp_manager,1,0,0,0
|
||||
access_resource_calendar_attendance_mrp_user,mrp.resource.calendar.attendance.mrp.user,resource.model_resource_calendar_attendance,mrp.group_mrp_user,1,1,1,1
|
||||
@@ -59,7 +55,9 @@ 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
|
||||
access_mrp_production_backorder_line,access.mrp.production.backorder.line,model_mrp_production_backorder_line,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_consumption_warning,access.mrp.consumption.warning,model_mrp_consumption_warning,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_consumption_warning_line,access.mrp.consumption.warning.line,model_mrp_consumption_warning_line,mrp.group_mrp_user,1,1,1,0
|
||||
|
||||
|
@@ -30,6 +30,11 @@
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_locked_by_default" model="res.groups">
|
||||
<field name="name">Locked by default</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="base.default_user" model="res.users">
|
||||
@@ -78,12 +83,6 @@
|
||||
<field name="domain_force">['|',('company_id', 'in', company_ids),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="mrp_routing_rule">
|
||||
<field name="name">mrp_routing multi-company</field>
|
||||
<field name="model_id" search="[('model','=','mrp.routing')]" model="ir.model"/>
|
||||
<field name="domain_force">['|',('company_id', 'in', company_ids),('company_id','=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="mrp_routing_workcenter_rule">
|
||||
<field name="name">mrp_routing_workcenter multi-company</field>
|
||||
<field name="model_id" search="[('model','=','mrp.routing.workcenter')]" model="ir.model"/>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -24,6 +24,9 @@ var MrpWorkorderPopover = PopoverWidget.extend({
|
||||
|
||||
_render: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (! this.$popover) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
this.$popover.find('.action_replan_button').click(function (e) {
|
||||
self._onReplanClick(e);
|
||||
|
||||
@@ -5,4 +5,7 @@
|
||||
height: 30rem;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_should_consume{
|
||||
padding-left: 0.3em;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ from . import test_cancel_mo
|
||||
from . import test_order
|
||||
from . import test_stock
|
||||
from . import test_warehouse_multistep_manufacturing
|
||||
from . import test_workorder_operation
|
||||
from . import test_procurement
|
||||
from . import test_unbuild
|
||||
from . import test_oee
|
||||
from . import test_traceability
|
||||
from . import test_multicompany
|
||||
from . import test_backorder
|
||||
|
||||
+18
-32
@@ -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):
|
||||
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,11 +34,14 @@ 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})
|
||||
]})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
if picking_type_id:
|
||||
mo_form.picking_type_id = picking_type_id
|
||||
mo_form.product_id = product_to_build
|
||||
mo_form.bom_id = bom_1
|
||||
mo_form.product_qty = qty_final
|
||||
@@ -91,40 +94,15 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.routing_1 = cls.env['mrp.routing'].create({
|
||||
'name': 'Simple Line',
|
||||
})
|
||||
cls.routing_2 = cls.env['mrp.routing'].create({
|
||||
'name': 'Complicated Line',
|
||||
})
|
||||
cls.operation_1 = cls.env['mrp.routing.workcenter'].create({
|
||||
'name': 'Gift Wrap Maching',
|
||||
'workcenter_id': cls.workcenter_1.id,
|
||||
'routing_id': cls.routing_1.id,
|
||||
'time_cycle': 15,
|
||||
'sequence': 1,
|
||||
})
|
||||
cls.operation_2 = cls.env['mrp.routing.workcenter'].create({
|
||||
'name': 'Cutting Machine',
|
||||
'workcenter_id': cls.workcenter_1.id,
|
||||
'routing_id': cls.routing_2.id,
|
||||
'time_cycle': 12,
|
||||
'sequence': 1,
|
||||
})
|
||||
cls.operation_3 = cls.env['mrp.routing.workcenter'].create({
|
||||
'name': 'Weld Machine',
|
||||
'workcenter_id': cls.workcenter_1.id,
|
||||
'routing_id': cls.routing_2.id,
|
||||
'time_cycle': 18,
|
||||
'sequence': 2,
|
||||
})
|
||||
|
||||
cls.bom_1 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'routing_id': cls.routing_2.id,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
@@ -134,8 +112,11 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'product_id': cls.product_5.id,
|
||||
'product_tmpl_id': cls.product_5.product_tmpl_id.id,
|
||||
'product_uom_id': cls.product_5.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'routing_id': cls.routing_1.id,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
'type': 'phantom',
|
||||
'sequence': 2,
|
||||
'bom_line_ids': [
|
||||
@@ -146,14 +127,19 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_dozen.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 2.0,
|
||||
'routing_id': cls.routing_2.id,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}),
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 12})
|
||||
]})
|
||||
|
||||
cls.stock_location_14 = cls.env['stock.location'].create({
|
||||
'name': 'Shelf 2',
|
||||
'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id,
|
||||
@@ -177,4 +163,4 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'type': 'product',
|
||||
'tracking': 'none',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
|
||||
class TestMrpProductionBackorder(TestMrpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.stock_location = cls.env.ref('stock.stock_location_stock')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
warehouse_form = Form(self.env['stock.warehouse'])
|
||||
warehouse_form.name = 'Test Warehouse'
|
||||
warehouse_form.code = 'TWH'
|
||||
self.warehouse = warehouse_form.save()
|
||||
|
||||
def test_no_tracking_1(self):
|
||||
"""Create a MO for 4 product. Produce 4. The backorder button should
|
||||
not appear and hitting mark as done should not open the backorder wizard.
|
||||
The name of the MO should be MO/001.
|
||||
"""
|
||||
mo = self.generate_mo(qty_final=4)[0]
|
||||
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 4
|
||||
mo = mo_form.save()
|
||||
|
||||
# No backorder is proposed
|
||||
self.assertTrue(mo.button_mark_done())
|
||||
self.assertEqual(mo._get_quantity_to_backorder(), 0)
|
||||
self.assertTrue("-001" not in mo.name)
|
||||
|
||||
def test_no_tracking_2(self):
|
||||
"""Create a MO for 4 product. Produce 1. The backorder button should
|
||||
appear and hitting mark as done should open the backorder wizard. In the backorder
|
||||
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
|
||||
created.
|
||||
The sequence of the first MO should be MO/001-01, the sequence of the second MO
|
||||
should be MO/001-02.
|
||||
Check that all MO are reachable through the procurement group.
|
||||
"""
|
||||
production, _, _, product_to_use_1, _ = self.generate_mo(qty_final=4, qty_base_1=3)
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, True)
|
||||
|
||||
# Make some stock and reserve
|
||||
for product in production.move_raw_ids.product_id:
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': product.id,
|
||||
'inventory_quantity': 100,
|
||||
'location_id': production.location_src_id.id,
|
||||
})
|
||||
production.action_assign()
|
||||
self.assertEqual(production.state, 'confirmed')
|
||||
self.assertEqual(production.reserve_visible, False)
|
||||
|
||||
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']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
# Two related MO to the procurement group
|
||||
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), 2)
|
||||
|
||||
# Check MO backorder
|
||||
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.product_id.id, production.product_id.id)
|
||||
self.assertEqual(mo_backorder.product_qty, 3)
|
||||
self.assertEqual(sum(mo_backorder.move_raw_ids.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_uom_qty")), 9)
|
||||
self.assertEqual(mo_backorder.reserve_visible, False) # the reservation of the first MO should've been moved here
|
||||
|
||||
def test_no_tracking_pbm_1(self):
|
||||
"""Create a MO for 4 product. Produce 1. The backorder button should
|
||||
appear and hitting mark as done should open the backorder wizard. In the backorder
|
||||
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
|
||||
created.
|
||||
The sequence of the first MO should be MO/001-01, the sequence of the second MO
|
||||
should be MO/001-02.
|
||||
Check that all MO are reachable through the procurement group.
|
||||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm'
|
||||
|
||||
production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
|
||||
pbm_move = move_raw_ids.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 2)
|
||||
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
|
||||
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)
|
||||
|
||||
action = production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.delivery_count, 1)
|
||||
|
||||
pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
|
||||
# Check that quantity is correct
|
||||
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)
|
||||
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
|
||||
def test_no_tracking_pbm_sam_1(self):
|
||||
"""Create a MO for 4 product. Produce 1. The backorder button should
|
||||
appear and hitting mark as done should open the backorder wizard. In the backorder
|
||||
wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be
|
||||
created.
|
||||
The sequence of the first MO should be MO/001-01, the sequence of the second MO
|
||||
should be MO/001-02.
|
||||
Check that all MO are reachable through the procurement group.
|
||||
"""
|
||||
with Form(self.warehouse) as warehouse:
|
||||
warehouse.manufacture_steps = 'pbm_sam'
|
||||
production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id)
|
||||
|
||||
move_raw_ids = production.move_raw_ids
|
||||
self.assertEqual(len(move_raw_ids), 2)
|
||||
self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
|
||||
pbm_move = move_raw_ids.move_orig_ids
|
||||
self.assertEqual(len(pbm_move), 2)
|
||||
self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2})
|
||||
self.assertFalse(pbm_move.move_orig_ids)
|
||||
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)
|
||||
|
||||
sam_move = production.move_finished_ids.move_dest_ids
|
||||
self.assertEqual(len(sam_move), 1)
|
||||
self.assertEqual(sam_move.product_id.id, product_to_build.id)
|
||||
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
|
||||
|
||||
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']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
mo_backorder = production.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.delivery_count, 2)
|
||||
|
||||
pbm_move |= mo_backorder.move_raw_ids.move_orig_ids
|
||||
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)
|
||||
|
||||
sam_move |= mo_backorder.move_finished_ids.move_orig_ids
|
||||
self.assertEqual(sum(sam_move.mapped("product_qty")), 4)
|
||||
|
||||
def test_tracking_backorder_series_lot_1(self):
|
||||
""" Create a MO of 4 tracked products. all component is tracked by lots
|
||||
Produce one by one with one bakorder for each until end.
|
||||
"""
|
||||
nb_product_todo = 4
|
||||
production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='lot', tracking_base_1='lot', tracking_base_2='lot')
|
||||
lot_final = self.env['stock.production.lot'].create({
|
||||
'name': 'lot_final',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot_consumed_1',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
lot_2 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot_consumed_2',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, nb_product_todo*4, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, nb_product_todo, lot_id=lot_2)
|
||||
|
||||
production.action_assign()
|
||||
active_production = production
|
||||
for i in range(nb_product_todo):
|
||||
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 4
|
||||
ml.lot_id = lot_1
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.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 = 1
|
||||
ml.lot_id = lot_2
|
||||
details_operation_form.save()
|
||||
|
||||
production_form = Form(active_production)
|
||||
production_form.qty_producing = 1
|
||||
production_form.lot_producing_id = lot_final
|
||||
active_production = production_form.save()
|
||||
|
||||
active_production.button_mark_done()
|
||||
if i + 1 != nb_product_todo: # If last MO, don't make a backorder
|
||||
action = active_production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
active_production = active_production.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
|
||||
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
|
||||
|
||||
def test_tracking_backorder_series_serial_1(self):
|
||||
""" Create a MO of 4 tracked products (serial) with pbm_sam.
|
||||
all component is tracked by serial
|
||||
Produce one by one with one bakorder for each until end.
|
||||
"""
|
||||
nb_product_todo = 4
|
||||
production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='serial', tracking_base_1='serial', tracking_base_2='serial', qty_base_1=1)
|
||||
serials_final, serials_p1, serials_p2 = [], [], []
|
||||
for i in range(nb_product_todo):
|
||||
serials_final.append(self.env['stock.production.lot'].create({
|
||||
'name': f'lot_final_{i}',
|
||||
'product_id': p_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
serials_p1.append(self.env['stock.production.lot'].create({
|
||||
'name': f'lot_consumed_1_{i}',
|
||||
'product_id': p1.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
serials_p2.append(self.env['stock.production.lot'].create({
|
||||
'name': f'lot_consumed_2_{i}',
|
||||
'product_id': p2.id,
|
||||
'company_id': self.env.company.id,
|
||||
}))
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=serials_p1[-1])
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=serials_p2[-1])
|
||||
|
||||
production.action_assign()
|
||||
active_production = production
|
||||
for i in range(nb_product_todo):
|
||||
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 1
|
||||
ml.lot_id = serials_p1[i]
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.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 = 1
|
||||
ml.lot_id = serials_p2[i]
|
||||
details_operation_form.save()
|
||||
|
||||
production_form = Form(active_production)
|
||||
production_form.qty_producing = 1
|
||||
production_form.lot_producing_id = serials_final[i]
|
||||
active_production = production_form.save()
|
||||
|
||||
active_production.button_mark_done()
|
||||
if i + 1 != nb_product_todo: # If last MO, don't make a backorder
|
||||
action = active_production.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
active_production = active_production.procurement_group_id.mrp_production_ids[-1]
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), nb_product_todo, f'You should have the {nb_product_todo} final product in stock')
|
||||
self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo)
|
||||
|
||||
|
||||
class TestMrpWorkorderBackorder(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpWorkorderBackorder, cls).setUpClass()
|
||||
cls.uom_unit = cls.env['uom.uom'].search([
|
||||
('category_id', '=', cls.env.ref('uom.product_uom_categ_unit').id),
|
||||
('uom_type', '=', 'reference')
|
||||
], limit=1)
|
||||
cls.finished1 = cls.env['product.product'].create({
|
||||
'name': 'finished1',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.compfinished1 = cls.env['product.product'].create({
|
||||
'name': 'compfinished1',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.compfinished2 = cls.env['product.product'].create({
|
||||
'name': 'compfinished2',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.workcenter1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'workcenter1',
|
||||
})
|
||||
cls.workcenter2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'workcenter2',
|
||||
})
|
||||
|
||||
cls.bom_finished1 = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.finished1.id,
|
||||
'product_tmpl_id': cls.finished1.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}),
|
||||
(0, 0, {'product_id': cls.compfinished2.id, 'product_qty': 1}),
|
||||
],
|
||||
'operation_ids': [
|
||||
(0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter1.id}),
|
||||
(0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter2.id}),
|
||||
],
|
||||
})
|
||||
cls.bom_finished1.bom_line_ids[0].operation_id = cls.bom_finished1.operation_ids[0].id
|
||||
cls.bom_finished1.bom_line_ids[1].operation_id = cls.bom_finished1.operation_ids[1].id
|
||||
@@ -26,9 +26,14 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_7_template.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'routing_id': self.routing_2.id,
|
||||
'type': 'normal',
|
||||
})
|
||||
test_bom.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
],
|
||||
})
|
||||
test_bom_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom.id,
|
||||
'product_id': self.product_2.id,
|
||||
@@ -78,9 +83,13 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_5.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_5.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'routing_id': self.routing_1.id,
|
||||
'type': 'phantom'
|
||||
})
|
||||
test_bom_1.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
})
|
||||
test_bom_1_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom_1.id,
|
||||
'product_id': self.product_3.id,
|
||||
@@ -92,9 +101,14 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_7_template.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'routing_id': self.routing_2.id,
|
||||
'type': 'normal',
|
||||
})
|
||||
test_bom_2.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
]
|
||||
})
|
||||
test_bom_2_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom_2.id,
|
||||
'product_id': self.product_2.id,
|
||||
@@ -143,6 +157,7 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_9.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_9.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal'
|
||||
})
|
||||
test_bom_4 = self.env['mrp.bom'].create({
|
||||
@@ -150,6 +165,7 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_10.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_10.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'phantom'
|
||||
})
|
||||
test_bom_3_l1 = self.env['mrp.bom.line'].create({
|
||||
@@ -278,6 +294,11 @@ class TestBoM(TestMrpCommon):
|
||||
bom_form_crumble.product_uom_id = uom_kg
|
||||
bom_crumble = bom_form_crumble.save()
|
||||
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'costs_hour': 10,
|
||||
'name': 'Deserts Table'
|
||||
})
|
||||
|
||||
with Form(bom_crumble) as bom:
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = butter
|
||||
@@ -287,32 +308,19 @@ class TestBoM(TestMrpCommon):
|
||||
line.product_id = biscuit
|
||||
line.product_uom_id = uom_kg
|
||||
line.product_qty = 6
|
||||
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'costs_hour': 10,
|
||||
'name': 'Deserts Table'
|
||||
})
|
||||
|
||||
routing_form = Form(self.env['mrp.routing'])
|
||||
routing_form.name = "Crumble process"
|
||||
routing_crumble = routing_form.save()
|
||||
|
||||
with Form(routing_crumble) as routing:
|
||||
with routing.operation_ids.new() as operation:
|
||||
with bom.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Prepare biscuits'
|
||||
operation.time_cycle_manual = 5
|
||||
with routing.operation_ids.new() as operation:
|
||||
with bom.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Prepare butter'
|
||||
operation.time_cycle_manual = 3
|
||||
with routing.operation_ids.new() as operation:
|
||||
with bom.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Mix manually'
|
||||
operation.time_cycle_manual = 5
|
||||
|
||||
bom_crumble.routing_id = routing_crumble.id
|
||||
|
||||
# TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
|
||||
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
|
||||
@@ -381,6 +389,13 @@ class TestBoM(TestMrpCommon):
|
||||
bom_form_cheese_cake.product_uom_id = self.uom_unit
|
||||
bom_cheese_cake = bom_form_cheese_cake.save()
|
||||
|
||||
workcenter_2 = self.env['mrp.workcenter'].create({
|
||||
'name': 'cake mounting',
|
||||
'costs_hour': 20,
|
||||
'time_start': 10,
|
||||
'time_stop': 15
|
||||
})
|
||||
|
||||
with Form(bom_cheese_cake) as bom:
|
||||
with bom.bom_line_ids.new() as line:
|
||||
line.product_id = cream
|
||||
@@ -390,29 +405,15 @@ class TestBoM(TestMrpCommon):
|
||||
line.product_id = crumble
|
||||
line.product_uom_id = uom_kg
|
||||
line.product_qty = 5.4
|
||||
|
||||
workcenter_2 = self.env['mrp.workcenter'].create({
|
||||
'name': 'cake mounting',
|
||||
'costs_hour': 20,
|
||||
'time_start': 10,
|
||||
'time_stop': 15
|
||||
})
|
||||
|
||||
routing_form = Form(self.env['mrp.routing'])
|
||||
routing_form.name = "Cheese cake process"
|
||||
routing_cheese = routing_form.save()
|
||||
|
||||
with Form(routing_cheese) as routing:
|
||||
with routing.operation_ids.new() as operation:
|
||||
with bom.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter
|
||||
operation.name = 'Mix cheese and crumble'
|
||||
operation.time_cycle_manual = 10
|
||||
with routing.operation_ids.new() as operation:
|
||||
with bom.operation_ids.new() as operation:
|
||||
operation.workcenter_id = workcenter_2
|
||||
operation.name = 'Cake mounting'
|
||||
operation.time_cycle_manual = 5
|
||||
|
||||
bom_cheese_cake.routing_id = routing_cheese.id
|
||||
|
||||
# TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
|
||||
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
|
||||
|
||||
@@ -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,19 +68,18 @@ 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...
|
||||
self.assertEqual(consume_move_c.product_uom_qty, 4, "Wrong consumed quantity of product c.")
|
||||
self.assertEqual(by_product_move.product_uom_qty, 2, "Wrong produced quantity of sub product.")
|
||||
|
||||
mnf_product_a.post_inventory()
|
||||
mnf_product_a._post_inventory()
|
||||
|
||||
# I see that stock moves of External Hard Disk including Headset USB are done now.
|
||||
self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!')
|
||||
|
||||
@@ -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,14 +52,13 @@ 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()
|
||||
manufacturing_order._post_inventory()
|
||||
# Cancel the MO
|
||||
manufacturing_order.action_cancel()
|
||||
# Check MO is marked as done and its SML are done or cancelled
|
||||
@@ -78,44 +76,6 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
|
||||
"The other move finished is cancelled like its MO.")
|
||||
|
||||
def test_cancel_mo_with_routing(self):
|
||||
""" Cancel a Manufacturing Order with routing (so generate a Work Order)
|
||||
and produce some quantities. When cancelled, the MO must be marked as
|
||||
done and the WO must be cancelled.
|
||||
"""
|
||||
# Create MO
|
||||
mo_data = self.generate_mo()
|
||||
manufacturing_order = mo_data[0]
|
||||
bom = mo_data[1]
|
||||
bom.routing_id = self.routing_1
|
||||
|
||||
manufacturing_order.button_plan()
|
||||
workorder = manufacturing_order.workorder_ids
|
||||
# Produce some quantity
|
||||
workorder.button_start()
|
||||
workorder.qty_producing = 2
|
||||
workorder._apply_update_workorder_lines()
|
||||
workorder.record_production()
|
||||
# Post Inventory
|
||||
manufacturing_order.post_inventory()
|
||||
# Cancel it
|
||||
manufacturing_order.action_cancel()
|
||||
# Check MO is done, WO is cancelled and its SML are done or cancelled
|
||||
self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.")
|
||||
self.assertEqual(workorder.state, 'cancel', "WO should be cancelled.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done',
|
||||
"Due to 'post_inventory', some move raw must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done',
|
||||
"Due to 'post_inventory', some move raw must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel',
|
||||
"The other move raw are cancelled like their MO.")
|
||||
self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel',
|
||||
"The other move raw are cancelled like their MO.")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done',
|
||||
"Due to 'post_inventory', a move finished must stay in done state")
|
||||
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
|
||||
"The other move finished is cancelled like its MO.")
|
||||
|
||||
def test_unlink_mo(self):
|
||||
""" Try to unlink a Manufacturing Order, and check it's possible or not
|
||||
depending of the MO state (must be in cancel state to be unlinked, but
|
||||
@@ -132,12 +92,11 @@ 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()
|
||||
manufacturing_order._post_inventory()
|
||||
# Unlink the MO must raises an UserError since it cannot be really cancelled
|
||||
self.assertEqual(manufacturing_order.exists().state, 'progress')
|
||||
with self.assertRaises(UserError):
|
||||
|
||||
@@ -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,33 +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_workcenter_1(self):
|
||||
"""Check it is not possible to use a routing of Company B in a
|
||||
workcenter of Company A. """
|
||||
|
||||
workcenter = self.env['mrp.workcenter'].create({
|
||||
'name': 'WC1',
|
||||
'company_id': self.company_a.id,
|
||||
'resource_calendar_id': self.company_a.resource_calendar_id.id,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
self.env['mrp.routing'].create({
|
||||
'name': 'WC1',
|
||||
'company_id': self.company_b.id,
|
||||
'operation_ids': [(0, 0, {
|
||||
'name': 'operation_1',
|
||||
'workcenter_id': workcenter.id,
|
||||
})]
|
||||
})
|
||||
|
||||
def test_partner_1(self):
|
||||
""" On a product without company, as a user of Company B, check it is not possible to use a
|
||||
|
||||
+389
-648
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -340,7 +326,10 @@ class TestProcurement(TestMrpCommon):
|
||||
'product_tmpl_id': parent_product.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'routing_id': self.routing_2.id,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
],
|
||||
'type': 'normal',
|
||||
})
|
||||
self.env['mrp.bom.line'].create({
|
||||
@@ -353,7 +342,10 @@ class TestProcurement(TestMrpCommon):
|
||||
'product_tmpl_id': child_product.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'routing_id': self.routing_2.id,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
|
||||
],
|
||||
'type': 'normal',
|
||||
})
|
||||
self.env['mrp.bom.line'].create({
|
||||
@@ -413,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()
|
||||
|
||||
@@ -26,13 +26,6 @@ class TestWarehouse(common.TestMrpCommon):
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
mrp_routing = self.env['mrp.routing'].create({
|
||||
'name': 'Primary Assembly',
|
||||
'operation_ids': [(0, 0, {
|
||||
'workcenter_id': mrp_workcenter.id,
|
||||
'name': 'Manual Assembly',
|
||||
})]
|
||||
})
|
||||
inventory = self.env['stock.inventory'].create({
|
||||
'name': 'Initial inventory',
|
||||
'line_ids': [(0, 0, {
|
||||
@@ -49,12 +42,15 @@ class TestWarehouse(common.TestMrpCommon):
|
||||
'product_tmpl_id': self.laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id,
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': self.graphics_card.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id
|
||||
})],
|
||||
'routing_id': mrp_routing.id
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
],
|
||||
})
|
||||
|
||||
def new_mo_laptop(self):
|
||||
@@ -162,47 +158,6 @@ class TestWarehouse(common.TestMrpCommon):
|
||||
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
|
||||
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
|
||||
|
||||
def test_putaway_after_manufacturing_1(self):
|
||||
""" This test checks a manufactured product without tracking will go to
|
||||
location defined in putaway strategy.
|
||||
"""
|
||||
mo_laptop = self.new_mo_laptop()
|
||||
|
||||
mo_laptop.button_plan()
|
||||
workorder = mo_laptop.workorder_ids[0]
|
||||
|
||||
workorder.button_start()
|
||||
workorder.record_production()
|
||||
mo_laptop.button_mark_done()
|
||||
|
||||
# We check if the laptop go in the depot and not in the stock
|
||||
move = mo_laptop.move_finished_ids
|
||||
location_dest = move.move_line_ids.location_dest_id
|
||||
self.assertEqual(location_dest.id, self.depot_location.id)
|
||||
self.assertNotEqual(location_dest.id, self.stock_location.id)
|
||||
|
||||
def test_putaway_after_manufacturing_2(self):
|
||||
""" This test checks a tracked manufactured product will go to location
|
||||
defined in putaway strategy.
|
||||
"""
|
||||
self.laptop.tracking = 'serial'
|
||||
mo_laptop = self.new_mo_laptop()
|
||||
|
||||
mo_laptop.button_plan()
|
||||
workorder = mo_laptop.workorder_ids[0]
|
||||
|
||||
workorder.button_start()
|
||||
serial = self.env['stock.production.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
|
||||
workorder.finished_lot_id = serial
|
||||
workorder.record_production()
|
||||
mo_laptop.button_mark_done()
|
||||
|
||||
# We check if the laptop go in the depot and not in the stock
|
||||
move = mo_laptop.move_finished_ids
|
||||
location_dest = move.move_line_ids.location_dest_id
|
||||
self.assertEqual(location_dest.id, self.depot_location.id)
|
||||
self.assertNotEqual(location_dest.id, self.stock_location.id)
|
||||
|
||||
def test_putaway_after_manufacturing_3(self):
|
||||
""" This test checks a tracked manufactured product will go to location
|
||||
defined in putaway strategy when the production is recorded with
|
||||
@@ -212,14 +167,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
|
||||
|
||||
@@ -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.")
|
||||
@@ -144,6 +147,7 @@ class TestTraceability(TestMrpCommon):
|
||||
'product_tmpl_id': product_final.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
|
||||
@@ -160,104 +164,145 @@ 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()
|
||||
|
||||
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({
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
mo_form = Form(mo_backorder)
|
||||
mo_form.lot_producing_id = self.env['stock.production.lot'].create({
|
||||
'product_id': product_final.id,
|
||||
'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)
|
||||
|
||||
@@ -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,
|
||||
@@ -428,15 +431,20 @@ 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 = 3.0
|
||||
produce_form.finished_lot_id = lot_finished_1
|
||||
produce_wizard = produce_form.save()
|
||||
produce_wizard._workorder_line_ids()[0].lot_id = lot_1
|
||||
produce_wizard.do_produce()
|
||||
self.assertEqual(mo.product_qty, 5)
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 3.0
|
||||
mo_form.lot_producing_id = lot_finished_1
|
||||
mo = mo_form.save()
|
||||
self.assertEqual(mo.move_raw_ids[1].quantity_done, 12)
|
||||
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 = lot_1
|
||||
details_operation_form.save()
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env[action['res_model']].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
|
||||
lot_2 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot_2',
|
||||
@@ -451,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
|
||||
@@ -539,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')))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Byproduct">
|
||||
<group>
|
||||
<field name="routing_id" invisible="1"/>
|
||||
<field name="allowed_operation_ids" invisible="1"/>
|
||||
<field name="company_id"/>
|
||||
<field name="product_id"/>
|
||||
<label for="product_qty"/>
|
||||
@@ -36,6 +36,11 @@
|
||||
<form string="Bill of Material">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o" groups="mrp.group_mrp_routings">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Routing<br/>Performance</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(action_report_mrp_bom)d" type="action"
|
||||
class="oe_stat_button" icon="fa-bars" string="Structure & Cost"/>
|
||||
</div>
|
||||
@@ -51,7 +56,6 @@
|
||||
<field name="product_qty"/>
|
||||
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
|
||||
</div>
|
||||
<field name="routing_id" attrs="{'invisible': [('type','not in',('normal','phantom'))]}" groups="mrp.group_mrp_routings" context="{'default_company_id': company_id}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="code"/>
|
||||
@@ -72,10 +76,9 @@
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Components" name="components">
|
||||
<field name="bom_line_ids" widget="one2many" context="{'default_parent_product_tmpl_id': product_tmpl_id, 'default_product_id': False, 'default_company_id': company_id, 'default_routing_id': routing_id}">
|
||||
<field name="bom_line_ids" widget="one2many" context="{'default_parent_product_tmpl_id': product_tmpl_id, 'default_product_id': False, 'default_company_id': company_id, 'default_bom_id': id}">
|
||||
<tree string="Components" editable="bottom">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="routing_id" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id" context="{'default_type': 'product'}"/>
|
||||
<field name="product_tmpl_id" invisible="1"/>
|
||||
@@ -88,21 +91,31 @@
|
||||
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
|
||||
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
|
||||
<field name="bom_product_template_attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
|
||||
<field name="operation_id" groups="mrp.group_mrp_routings" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/>
|
||||
<field name="allowed_operation_ids" invisible="1"/>
|
||||
<field name="operation_id" groups="mrp.group_mrp_routings" optional="hidden" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Operations"
|
||||
name="operations"
|
||||
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
|
||||
groups="mrp.group_mrp_routings">
|
||||
<field name="operation_ids"
|
||||
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
|
||||
groups="mrp.group_mrp_routings"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
</page>
|
||||
<page string="By-products"
|
||||
name="by_products"
|
||||
attrs="{'invisible': [('type','!=','normal')]}"
|
||||
groups="mrp.group_mrp_byproducts">
|
||||
<field name="byproduct_ids" context="{'form_view_ref' : 'mrp.mrp_bom_byproduct_form_view', 'default_company_id': company_id, 'default_routing_id': routing_id}">
|
||||
<field name="byproduct_ids" context="{'form_view_ref' : 'mrp.mrp_bom_byproduct_form_view', 'default_company_id': company_id, 'default_bom_id': id}">
|
||||
<tree string="By-products" editable="top">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="routing_id" invisible="1"/>
|
||||
<field name="product_id" context="{'default_type': 'product'}"/>
|
||||
<field name="product_qty"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
<field name="allowed_operation_ids" invisible="1"/>
|
||||
<field name="operation_id" groups="mrp.group_mrp_routings" options="{'no_quick_create':True,'no_create_edit':True}"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -110,8 +123,8 @@
|
||||
<page string="Miscellaneous" name="miscellaneous">
|
||||
<group>
|
||||
<group>
|
||||
<field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" groups="mrp.group_mrp_routings"/>
|
||||
<field name="consumption" attrs="{'invisible': [('type','!=','normal')]}"/>
|
||||
<field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" widget="radio" groups="mrp.group_mrp_routings"/>
|
||||
<field name="consumption" attrs="{'invisible': [('type','!=','normal')]}" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="picking_type_id" attrs="{'invisible': [('type','!=','normal')]}" string="Operation" groups="stock.group_adv_location"/>
|
||||
@@ -142,7 +155,6 @@
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
<field name="product_qty" optional="show"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom" optional="show" string="Unit of Measure"/>
|
||||
<field name="routing_id" groups="mrp.group_mrp_routings" optional="show"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -187,7 +199,6 @@
|
||||
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_tmpl_id'}"/>
|
||||
<filter string='BoM Type' name="group_by_type" domain="[]" context="{'group_by' : 'type'}"/>
|
||||
<filter string='Unit of Measure' name="default_unit_of_measure" domain="[]" context="{'group_by' : 'product_uom_id'}"/>
|
||||
<filter string="Routing" name="routings" domain="[]" context="{'group_by': 'routing_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
@@ -200,7 +211,7 @@
|
||||
<field name="domain">[]</field> <!-- force empty -->
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="search_view_id" ref="view_mrp_bom_filter"/>
|
||||
<field name="context">{'search_default_group_by_type': True, 'default_company_id': allowed_company_ids[0]}</field>
|
||||
<field name="context">{'default_company_id': allowed_company_ids[0]}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a bill of materials
|
||||
@@ -239,8 +250,8 @@
|
||||
</group>
|
||||
<group string="Operation">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="routing_id" invisible="1"/>
|
||||
<field name="sequence" groups="base.group_no_one"/>
|
||||
<field name="allowed_operation_ids" invisible="1"/>
|
||||
<field name="operation_id" groups="mrp.group_mrp_routings"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<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"/>
|
||||
<field name="routing_id" groups="mrp.group_mrp_routings" optional="show"/>
|
||||
<field name="reservation_state" optional="show"/>
|
||||
<field name="product_qty" sum="Total Qty" string="Quantity" readonly="1" optional="show"/>
|
||||
<field name="company_id" readonly="1" groups="base.group_multi_company" optional="show"/>
|
||||
@@ -39,6 +39,20 @@
|
||||
<field name="code">records.button_plan()</field>
|
||||
</record>
|
||||
|
||||
<record id="action_production_order_mark_done" model="ir.actions.server">
|
||||
<field name="name">Mark as Done</field>
|
||||
<field name="model_id" ref="mrp.model_mrp_production"/>
|
||||
<field name="binding_model_id" ref="mrp.model_mrp_production"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
res = records.filtered(lambda mo: mo.state in {'to_close', 'progress'}).button_mark_done()
|
||||
if res is not True:
|
||||
action = res
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_production_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.production.form</field>
|
||||
<field name="model">mrp.production</field>
|
||||
@@ -46,51 +60,37 @@
|
||||
<form string="Manufacturing Orders">
|
||||
<header>
|
||||
<field name="confirm_cancel" invisible="1"/>
|
||||
<field name="show_lock" invisible="1"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('qty_producing', '=', 0)]}" string="Backorder" type="object" class="oe_highlight"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
|
||||
<button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight"/>
|
||||
<button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
|
||||
<button name="button_plan" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('routing_id', '=', False)]}" type="object" string="Plan" class="oe_highlight"/>
|
||||
<button name="action_confirm" attrs="{'invisible': [('state', '!=', 'draft')]}" string="Confirm" type="object" class="oe_highlight"/>
|
||||
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object"/>
|
||||
<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'), ('routing_id', '!=', 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')), ('routing_id', '!=', 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)]}"/>
|
||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': [('state', 'in', ('cancel', 'draft'))]}"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,done"/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', '|', ('state', 'in', ('cancel', 'draft')), ('id', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to correct what has been consumed or produced."/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': [('is_locked', '=', True)]}" string="Lock" class="oe_highlight" groups="mrp.group_mrp_manager" type="object"/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('show_lock', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to adjust what has been consumed or produced."/>
|
||||
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('show_lock', '=', False), ('is_locked', '=', True)]}" string="Lock" groups="mrp.group_mrp_manager" type="object" help="Lock the manufacturing order to prevent changes to what has been consumed or produced."/>/>
|
||||
<button name="action_cancel" type="object" string="Cancel"
|
||||
attrs="{'invisible': ['|', '|', '|', ('id', '=', False), ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', True)]}"/>
|
||||
attrs="{'invisible': ['|', '|', ('id', '=', False), ('state', 'in', ('done', 'cancel')), ('confirm_cancel', '=', True)]}"/>
|
||||
<button name="action_cancel" type="object" string="Cancel"
|
||||
attrs="{'invisible': ['|', '|', '|', ('id', '=', False), ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', False)]}"
|
||||
attrs="{'invisible': ['|', '|', ('id', '=', False), ('state', 'in', ('done', 'cancel')), ('confirm_cancel', '=', False)]}"
|
||||
confirm="Some product moves have already been confirmed, this manufacturing order can't be completely cancelled. Are you still sure you want to process ?"/>
|
||||
<button name="button_unbuild" type="object" string="Unbuild" attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="reservation_state" invisible="1"/>
|
||||
<field name="is_partially_planned" invisible="1"/>
|
||||
<field name="date_planned_finished" invisible="1"/>
|
||||
<field name="is_locked" invisible="1"/>
|
||||
<field name="post_visible" invisible="1"/>
|
||||
<field name="qty_produced" invisible="1"/>
|
||||
<field name="unreserve_visible" invisible="1"/>
|
||||
<field name="reserve_visible" invisible="1"/>
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="is_planned" invisible="1"/>
|
||||
<field name="workorder_ids" invisible="1"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/>
|
||||
<button name="%(action_mrp_workorder_production_specific)d" type="action" attrs="{'invisible': [('workorder_count', '=', 0)]}" class="oe_stat_button" icon="fa-play-circle-o">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="workorder_done_count" widget="statinfo" nolabel="1"/> / <field name="workorder_count" widget="statinfo" nolabel="1"/></span>
|
||||
<span class="o_stat_text">Work Orders</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
|
||||
<button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" groups="base.group_user" attrs="{'invisible': [('delivery_count', '=', 0)]}">
|
||||
<field name="delivery_count" widget="statinfo" string="Transfers"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="scrap_count"/></span>
|
||||
<span class="o_stat_text">Scraps</span>
|
||||
</div>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="action_view_mrp_production_childs" type="object" icon="fa-wrench" attrs="{'invisible': [('mrp_production_child_count', '=', 0)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="mrp_production_child_count"/></span>
|
||||
@@ -103,7 +103,23 @@
|
||||
<span class="o_stat_text">Source MO</span>
|
||||
</div>
|
||||
</button>
|
||||
<field name="workorder_ids" invisible="1"/>
|
||||
<button class="oe_stat_button" name="action_view_mrp_production_backorders" type="object" icon="fa-wrench" attrs="{'invisible': [('mrp_production_backorder_count', '<', 2)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="mrp_production_backorder_count"/></span>
|
||||
<span class="o_stat_text">Backorder MO's</span>
|
||||
</div>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="scrap_count"/></span>
|
||||
<span class="o_stat_text">Scraps</span>
|
||||
</div>
|
||||
</button>
|
||||
<button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" groups="base.group_user" attrs="{'invisible': [('delivery_count', '=', 0)]}">
|
||||
<field name="delivery_count" widget="statinfo" string="Transfers"/>
|
||||
</button>
|
||||
<button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/>
|
||||
<button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" placeholder="Manufacturing Reference" nolabel="1"/></h1>
|
||||
@@ -112,68 +128,41 @@
|
||||
<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" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}"/>
|
||||
/
|
||||
</div>
|
||||
<field name="product_qty" class="oe_inline text-left" attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('state', 'not in', ('draft', 'done'))]}"/>
|
||||
<button type="action" name="%(mrp.action_change_production_qty)d"
|
||||
context="{'default_mo_id': id}" class="oe_link oe_inline" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done','cancel')), ('id', '=', False)]}">
|
||||
<field name="product_qty" class="oe_inline" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
</button>
|
||||
<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" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}">
|
||||
<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')]}"/>
|
||||
<field name="routing_id" groups="mrp.group_mrp_routings"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_deadline" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"/>
|
||||
<div class="o_row o_td_label">
|
||||
<label for="date_planned_start" string="Planned Date"
|
||||
attrs="{'invisible': [
|
||||
('routing_id', '!=', False),
|
||||
('state', 'in', ['draft', 'confirmed'])
|
||||
]}"/>
|
||||
<label for="date_planned_start" string="Plan from"
|
||||
attrs="{'invisible': [
|
||||
'|',
|
||||
('routing_id', '=', False),
|
||||
('state', 'not in', ['draft', 'confirmed'])
|
||||
]}"/>
|
||||
</div>
|
||||
<field name="date_deadline"/>
|
||||
<label for="date_planned_start"/>
|
||||
<div class="o_row">
|
||||
<field name="date_planned_start"
|
||||
attrs="{'readonly': [
|
||||
'|',
|
||||
'&',
|
||||
('routing_id', '=', False),
|
||||
('state', 'in', ['done', 'cancel']),
|
||||
'&',
|
||||
('routing_id', '!=', False),
|
||||
('state', 'not in', ['draft', 'confirmed'])]}"/>
|
||||
<label for="date_planned_finished" string="to" attrs="{'invisible': [
|
||||
'|',
|
||||
('id', '=', False),
|
||||
'&',
|
||||
('routing_id', '!=', False),
|
||||
('state', 'in', ['draft', 'confirmed'])
|
||||
]}"/>
|
||||
<field name="date_planned_finished" required="1"
|
||||
attrs="{'readonly': [
|
||||
'|',
|
||||
'&',
|
||||
('routing_id', '=', False),
|
||||
('state', 'in', ['done', 'cancel']),
|
||||
('routing_id', '!=', False)
|
||||
], 'invisible': [
|
||||
('routing_id', '!=', False),
|
||||
('state', 'in', ['draft', 'confirmed'])
|
||||
]}"/>
|
||||
attrs="{'readonly': ['|', ('is_partially_planned', '=', True), ('state', 'in', ['close', 'cancel'])]}"/>
|
||||
<field name="delay_alert_date" invisible="1"/>
|
||||
<field string=" " name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/>
|
||||
</div>
|
||||
@@ -182,31 +171,150 @@
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}" force_save="1"/>
|
||||
<field name="show_final_lots" invisible="1"/>
|
||||
<field name="production_location_id" invisible="1" readonly="1"/>
|
||||
<field name="move_finished_ids" invisible="1" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}">
|
||||
<tree editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom"/>
|
||||
<field name="operation_id"/>
|
||||
<field name="byproduct_id"/>
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="date_expected"/>
|
||||
<field name="picking_type_id"/>
|
||||
<field name="location_id"/>
|
||||
<field name="location_dest_id"/>
|
||||
<field name="company_id"/>
|
||||
<field name="warehouse_id"/>
|
||||
<field name="origin"/>
|
||||
<field name="group_id"/>
|
||||
<field name="propagate_date"/>
|
||||
<field name="propagate_cancel"/>
|
||||
<field name="delay_alert"/>
|
||||
<field name="propagate_date_minimum_delta"/>
|
||||
<field name="move_dest_ids"/>
|
||||
<field name="state"/>
|
||||
<!-- Useless as the editable in tree declaration -> For Form Test-->
|
||||
<field name="product_uom_category_id"/>
|
||||
<field name="allowed_operation_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</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': ['&', '&', ('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"/>
|
||||
<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'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}">
|
||||
<tree delete="0" default_order="is_done,sequence" editable="bottom">
|
||||
<field name="product_id" required="1" attrs="{'readonly': ['|', '|', ('has_move_lines', '=', True), ('state', '=', 'cancel'), '&', ('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="is_locked" invisible="1"/>
|
||||
<field name="has_move_lines" 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': ['&', ('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 < product_uom_qty and product_uom_qty - reserved_availability > 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 > 0.0001)"
|
||||
attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('show_details_visible', '=', True)]}"/>
|
||||
<field name="show_details_visible" invisible="1"/>
|
||||
<button name="action_show_details" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{"warn": 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="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', '=', 'cancel'), '&', ('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="is_locked" invisible="1"/>
|
||||
<field name="has_move_lines" 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': ['&', ('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="{"warn": true}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Operations" name="operations" groups="mrp.group_mrp_routings">
|
||||
<field name="workorder_ids" attrs="{'readonly': [('state', 'in', ['cancel', 'done'])]}" context="{'tree_view_ref': 'mrp.mrp_production_workorder_tree_editable_view', 'default_production_id': id, 'default_product_uom_id': product_uom_id, 'default_consumption': consumption, 'default_company_id': company_id}"/> </page>
|
||||
<page string="Miscellaneous" name="miscellaneous" groups="stock.group_stock_multi_locations">
|
||||
<group>
|
||||
<group>
|
||||
@@ -273,8 +381,7 @@
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<calendar date_start="date_planned_start" date_stop="date_planned_finished"
|
||||
string="Manufacturing Orders" color="routing_id" event_limit="5" quick_add="False">
|
||||
<field name="routing_id" filters="1"/>
|
||||
string="Manufacturing Orders" event_limit="5" quick_add="False">
|
||||
<field name="user_id" avatar_field="image_128"/>
|
||||
<field name="product_id"/>
|
||||
<field name="product_qty"/>
|
||||
@@ -286,7 +393,7 @@
|
||||
<field name="name">mrp.production.gantt</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="arch" type="xml">
|
||||
<gantt date_stop="date_finished" date_start="date_start" string="Productions" default_group_by="routing_id" create="0">
|
||||
<gantt date_stop="date_finished" date_start="date_start" string="Productions" create="0">
|
||||
</gantt>
|
||||
</field>
|
||||
</record>
|
||||
@@ -319,8 +426,7 @@
|
||||
<field name="name" string="Manufacturing Order" filter_domain="['|', ('name', 'ilike', self), ('origin', 'ilike', self)]"/>
|
||||
<field name="product_id"/>
|
||||
<field name="move_raw_ids" string="Component" filter_domain="[('move_raw_ids.product_id', 'ilike', self)]"/>
|
||||
<field name="name" string="Work Center" filter_domain="[('routing_id.operation_ids.workcenter_id', 'ilike', self)]"/>
|
||||
<field name="routing_id" groups="mrp.group_mrp_routings"/>
|
||||
<field name="name" string="Work Center" filter_domain="[('bom_id.operation_ids.workcenter_id', 'ilike', self)]"/>
|
||||
<field name="origin"/>
|
||||
<filter string="To Do" name="todo" domain="[('state', 'in', ('draft', 'confirmed', 'planned','progress', 'to_close'))]"
|
||||
help="Manufacturing Orders which are in confirmed state."/>
|
||||
@@ -352,7 +458,6 @@
|
||||
domain="[('activity_exception_decoration', '!=', False)]"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
|
||||
<filter string="Routing" name="routing" domain="[]" context="{'group_by': 'routing_id'}" groups="mrp.group_mrp_routings"/>
|
||||
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
|
||||
<filter string="Material Availability" name="groupby_reservation_state" domain="[]" context="{'group_by': 'reservation_state'}"/>
|
||||
<filter string="Scheduled Date" name="scheduled_date" domain="[]" context="{'group_by': 'date_planned_start'}" help="Scheduled Date by Month"/>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration"/>
|
||||
<field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration" width="1.5"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -21,7 +21,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Routing Work Centers">
|
||||
<sheet>
|
||||
<field name="routing_id" invisible="1" required="0"/>
|
||||
<group>
|
||||
<group name="description">
|
||||
<field name="name"/>
|
||||
@@ -63,109 +62,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Routings -->
|
||||
<record id="mrp_routing_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.routing.form</field>
|
||||
<field name="model">mrp.routing</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Routing">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Time<br/> Analysis</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="code"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Work Center Operations" name="work_center_operations">
|
||||
<field name="operation_ids" context="{'default_routing_id': id}"/>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="note"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_tree_view" model="ir.ui.view">
|
||||
<field name="name">mrp.routing.tree</field>
|
||||
<field name="model">mrp.routing</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Routing">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_kanban_view" model="ir.ui.view">
|
||||
<field name="name">mrp.routing.kanban</field>
|
||||
<field name="model">mrp.routing</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings mt4">
|
||||
<strong class="o_kanban_record_title"><span><t t-esc="record.name.value"/></span></strong>
|
||||
</div>
|
||||
<span class="badge badge-pill"><field name="code"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_search_view" model="ir.ui.view">
|
||||
<field name="name">mrp.routing.search</field>
|
||||
<field name="model">mrp.routing</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Routing">
|
||||
<field name="name" string="Routing" filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/>
|
||||
<filter name="inactive" string="Archived" domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_routing_action" model="ir.actions.act_window">
|
||||
<field name="name">Routings</field>
|
||||
<field name="name">Operations</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.routing</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="view_id" ref="mrp_routing_tree_view"/>
|
||||
<field name="search_view_id" ref="mrp_routing_search_view"/>
|
||||
<field name="res_model">mrp.routing.workcenter</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="mrp_routing_workcenter_tree_view"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new routing
|
||||
Create a new operation
|
||||
</p><p>
|
||||
Routings define the successive operations that need to be
|
||||
done to realize a Manufacturing Order. Each operation from
|
||||
a Routing is done at a specific Work Center and has a specific duration.
|
||||
Operation define that need to be done to realize a Work Order.
|
||||
Each operation is done at a specific Work Center and has a specific duration.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
@@ -174,6 +82,7 @@
|
||||
action="mrp_routing_action"
|
||||
parent="menu_mrp_configuration"
|
||||
groups="group_mrp_routings"
|
||||
sequence="50"/>
|
||||
sequence="100"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="name">Manufacturing Orders</field>
|
||||
<field name="res_model">mrp.production</field>
|
||||
<field name="view_mode">tree,kanban,form,gantt</field>
|
||||
<field name="domain">[('routing_id', '!=', False),('routing_id.operation_ids.workcenter_id','=', active_id)]</field>
|
||||
<field name="domain">[('bom_id', '!=', False), ('bom_id.operation_ids.workcenter_id', '=', active_id)]</field>
|
||||
</record>
|
||||
|
||||
<record id="action_work_orders" model="ir.actions.act_window">
|
||||
@@ -431,14 +431,6 @@
|
||||
parent="menu_mrp_configuration"
|
||||
sequence="90"/>
|
||||
|
||||
<menuitem id="menu_mrp_dashboard"
|
||||
name="Overview"
|
||||
action="mrp_workcenter_kanban_action"
|
||||
groups="group_mrp_routings"
|
||||
parent="menu_mrp_root"
|
||||
sequence="5"/>
|
||||
|
||||
|
||||
<record id="oee_loss_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.workcenter.productivity.loss.form</field>
|
||||
<field name="model">mrp.workcenter.productivity.loss</field>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<field name="view_mode">graph,pivot,tree,form,gantt,calendar</field>
|
||||
<field name="context">{'search_default_done': True}</field>
|
||||
<field name="search_view_id" ref="view_mrp_production_work_order_search"/>
|
||||
<field name="domain">[('production_id.routing_id', '=', active_id), ('state', '=', 'done')]</field>
|
||||
<field name="domain">[('operation_id.bom_id', '=', active_id), ('state', '=', 'done')]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data to display
|
||||
@@ -60,27 +60,65 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mrp_production_workorder_tree_view_inherit">
|
||||
<field name="name">mrp.production.work.order.tree</field>
|
||||
<record model="ir.ui.view" id="mrp_production_workorder_tree_editable_view">
|
||||
<field name="name">mrp.production.work.order.tree.editable</field>
|
||||
<field name="model">mrp.workorder</field>
|
||||
<field name="priority" eval="100"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Work Orders" delete="0" create="0">
|
||||
<field name="name"/>
|
||||
<field name="date_planned_start" decoration-danger="date_planned_start<current_date and state in ('ready')"/>
|
||||
<field name="workcenter_id" widget="selection"/>
|
||||
<field name="production_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="qty_production"/>
|
||||
<field name="product_uom_id"/>
|
||||
<field name="state"/>
|
||||
<field name="activity_exception_decoration" widget="activity_exception"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<tree editable="bottom">
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_produced" invisible="1"/>
|
||||
<field name="is_user_working" invisible="1"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="product_uom_id" invisible="1" readonly="0"/>
|
||||
<field name="production_state" invisible="1"/>
|
||||
<field name="production_bom_id" invisible="1"/>
|
||||
<field name="qty_producing" invisible="1"/>
|
||||
<field name="time_ids" invisible="1"/>
|
||||
<field name="working_state" invisible="1"/>
|
||||
<field name="operation_id" invisible="1" domain="['|', ('bom_id', '=', production_bom_id), ('bom_id', '=', False)]" context="{'default_workcenter_id': workcenter_id, 'default_company_id': company_id}"/>
|
||||
<field name="name" string="Operation"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="date_planned_start" optional="show"/>
|
||||
<field name="date_planned_finished" optional="hide"/>
|
||||
<field name="date_start" optional="hide" readonly="1"/>
|
||||
<field name="date_finished" optional="hide" readonly="1"/>
|
||||
<field name="duration_expected" widget="float_time"/>
|
||||
<field name="duration" widget="mrp_time_counter"
|
||||
attrs="{'invisible': ['|', ('production_state','=', 'draft'), ('time_ids', '=', [])]}"/>
|
||||
<button name="action_open_wizard" type="object" icon="fa-external-link" class="oe_edit_only"
|
||||
context="{'default_workcenter_id': workcenter_id}"/>
|
||||
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/>
|
||||
<button name="button_start" type="object" string="Start" class="btn-success"
|
||||
attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
|
||||
<button name="button_pending" type="object" string="Pause" class="btn-warning"
|
||||
attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_finish" type="object" string="Done" class="btn-success"
|
||||
attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="%(mrp.act_mrp_block_workcenter_wo)d" type="action" string="Block" context="{'default_workcenter_id': workcenter_id}" class="btn-danger"
|
||||
attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked')]}"/>
|
||||
<button name="button_unblock" type="object" string="Unblock" context="{'default_workcenter_id': workcenter_id}" class="btn-danger"
|
||||
attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done')), ('working_state', '!=', 'blocked')]}"/>
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
<field name="json_popover" widget="mrp_workorder_popover" string=" " attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_production_workorder_tree_view" model="ir.ui.view">
|
||||
<field name="name">mrp.production.work.order.tree</field>
|
||||
<field name="model">mrp.workorder</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_workorder_tree_editable_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="workcenter_id" position="after">
|
||||
<field name="production_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mrp_production_workorder_form_view_inherit">
|
||||
<field name="name">mrp.production.work.order.form</field>
|
||||
<field name="model">mrp.workorder</field>
|
||||
@@ -89,18 +127,7 @@
|
||||
<field name="is_user_working" invisible="1"/>
|
||||
<field name="working_state" invisible="1"/>
|
||||
<field name="production_state" invisible="1"/>
|
||||
<field name="allowed_lots_domain" invisible="1"/>
|
||||
<field name="is_finished_lines_editable" invisible="1"/>
|
||||
<header>
|
||||
<button name="button_finish" type="object" string="Finish Order" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('is_produced', '=', False)]}" class="btn-info"/>
|
||||
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'pending')]}"/>
|
||||
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'ready')]}" class="btn-success"/>
|
||||
<button name="record_production" type="object" string="Done" class="btn-success" attrs="{'invisible': ['|', '|', '|', ('is_produced', '=', True), ('working_state', '=', 'blocked'), ('state', '!=', 'progress'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_pending" type="object" string="Pause" class="btn-warning" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/>
|
||||
<button name="%(act_mrp_block_workcenter_wo)d" type="action" context="{'default_workcenter_id': workcenter_id}" string="Block" class="btn-danger" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_unblock" type="object" string="Unblock" class="btn-danger" attrs="{'invisible': [('working_state', '!=', 'blocked')]}"/>
|
||||
<button name="button_start" type="object" string="Continue Production" attrs="{'invisible': ['|', '|', '|', ('production_state', '=', 'done'), ('working_state', '=', 'blocked'), ('is_user_working', '=', True), ('state', '!=', 'progress')]}"/>
|
||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending,ready,progress,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
@@ -112,50 +139,25 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_id" string="To Produce"/>
|
||||
<label for="qty_produced" string="Quantity Produced"/>
|
||||
<div class="o_row">
|
||||
<field name="qty_produced"/> /
|
||||
<field name="qty_production"/>
|
||||
<field name="product_uom_id"/>
|
||||
<field name="production_availability" nolabel="1" widget="bullet_state" options="{'classes': {'assigned': 'success', 'waiting': 'danger'}}" attrs="{'invisible': [('state', '=', 'done')]}"/>
|
||||
</div>
|
||||
<field name="is_produced" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="production_id" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<field name="workcenter_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="production_id" invisible="1"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<field name="finished_lot_id" invisible="1"/>
|
||||
<field name="qty_producing" invisible="1"/>
|
||||
<group attrs="{'invisible': [('date_planned_start', '=', False)]}">
|
||||
<label for="date_planned_start" string="Planned Date"/>
|
||||
<div class="oe_inline">
|
||||
<field name="date_planned_start" class="mr8 oe_inline" required="True"/>
|
||||
<strong class="mr8 oe_inline">to</strong>
|
||||
<field name="date_planned_finished" class="oe_inline" required="True"/>
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
<field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
|
||||
</div>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Time Tracking" name="time_tracking" groups="mrp.group_mrp_manager">
|
||||
<group>
|
||||
<label for="date_planned_start" string="Planned Date"/>
|
||||
<div class="oe_inline">
|
||||
<field name="date_planned_start" class="mr8 oe_inline" required="True"/>
|
||||
<strong class="mr8 oe_inline">to</strong>
|
||||
<field name="date_planned_finished" class="oe_inline" required="True"/>
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
<field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
|
||||
</div>
|
||||
<label for="date_start" string="Effective Date" attrs="{'invisible': [('date_start', '=', False)]}"/>
|
||||
<div class="o_row" attrs="{'invisible': [('date_start', '=', False)]}">
|
||||
<field name="date_start" readonly="1"/>
|
||||
<div attrs="{'invisible': [('date_finished', '=', False)]}">
|
||||
<strong class="mr8">to</strong>
|
||||
<field name="date_finished" readonly="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<label for="duration_expected"/>
|
||||
<div>
|
||||
<field name="duration_expected" widget="float_time" class="oe_inline"/>
|
||||
minutes
|
||||
</div>
|
||||
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="time_ids" nolabel="1" context="{'default_workcenter_id': workcenter_id, 'default_workorder_id': id}">
|
||||
<tree>
|
||||
@@ -165,7 +167,7 @@
|
||||
<field name="user_id"/>
|
||||
<field name="workcenter_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="loss_id" string="Productivity"/>
|
||||
<field name="loss_id" string="Productivity" optional="hide"/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
@@ -186,44 +188,6 @@
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Current Production" name="current_production">
|
||||
<group>
|
||||
<group>
|
||||
<field name="qty_producing" string="Quantity in Production" attrs="{'readonly': ['|', ('product_tracking', '=', 'serial'), ('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="finished_lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', '=', 'none')]}" groups="stock.group_production_lot"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="consumption" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<h4 attrs="{'invisible': ['&', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])]}">Components</h4>
|
||||
<field name="raw_workorder_line_ids" attrs="{'invisible': ['&', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])], 'readonly': [('state', 'in', ('cancel', 'done'))]}" options="{'create': [('consumption', '=', 'flexible')]}">
|
||||
<tree editable="bottom" create="1" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id, 'active_mo_id': parent.production_id}"/>
|
||||
<field name="qty_to_consume" attrs="{'readonly': [('parent.consumption', '=', 'strict')]}" force_save="1"/>
|
||||
<field name="qty_reserved" readonly="1"/>
|
||||
<field name="qty_done" attrs="{'column_invisible': [('parent.state', 'not in', ('progress', 'done'))]}"/>
|
||||
<field name="product_uom_id" invisible="1"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<h4>Finished Products</h4>
|
||||
<field name="finished_workorder_line_ids" attrs="{'readonly': [('is_finished_lines_editable', '=', False)]}">
|
||||
<tree editable="bottom" create="1" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id}"/>
|
||||
<field name="qty_to_consume" readonly="1" string="To Produce" force_save="1"/>
|
||||
<field name="qty_done" string="Produced"/>
|
||||
<field name="product_uom_id" invisible="1"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Work Instruction" name="workorder_page_work_instruction" attrs="{'invisible': [('worksheet', '=', False), ('worksheet_google_slide', '=', False), ('operation_note', '=', False)]}">
|
||||
<field name="worksheet_type" invisible="1"/>
|
||||
<field name="worksheet" widget="pdf_viewer" attrs="{'invisible': [('worksheet_type', '!=', 'pdf')]}"/>
|
||||
@@ -485,6 +449,15 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mrp_workorder_mrp_production_form">
|
||||
<field name="name">Work Orders</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.workorder</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="mrp_production_workorder_form_view_inherit"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mrp_workorder_todo">
|
||||
<field name="name">Work Orders</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
||||
@@ -20,13 +20,10 @@
|
||||
<label for="group_mrp_routings" string="Work Orders"/>
|
||||
<a href="https://www.odoo.com/documentation/user/13.0/manufacturing/management/manufacturing_order.html#manage-manufacturing-with-routings-and-work-centers" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Process operations at specific work centers based on the routing
|
||||
Process operations at specific work centers
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('group_mrp_routings','=',False)]}">
|
||||
<div class="mt8">
|
||||
<div>
|
||||
<button name="%(mrp.mrp_routing_action)d" icon="fa-arrow-right" type="action" string="Routings" class="btn-link"/>
|
||||
</div>
|
||||
<div>
|
||||
<button name="%(mrp.mrp_workcenter_action)d" icon="fa-arrow-right" type="action" string="Work Centers" class="btn-link"/>
|
||||
</div>
|
||||
@@ -58,6 +55,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box" id="mrp_lock" title="Makes confirmed manufacturing orders locked rather than unlocked by default. This only applies to new manufacturing orders, not previously created ones.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_locked_by_default"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_locked_by_default"/>
|
||||
<div class="text-muted">
|
||||
Prevent manufacturing users to modify quantities to consume, unless a manager has unlocked the document
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-lg-6 col-12 o_setting_box" id="mrp_byproduct" title="Add by-products to bills of materials. This can be used to get several finished products as well. Without this option you only do: A + B = C. With the option: A + B = C + D.">
|
||||
|
||||
@@ -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': ['&', ('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': ['|', ('parent.routing_id', '=', False), ('id', '!=', False)]}" groups="mrp.group_mrp_routings"/>
|
||||
<field name="routing_id" 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 > 0) and (qty_done>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 > 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty < 0.0001" decoration-danger="not is_done and reserved_availability < product_uom_qty and product_uom_qty - reserved_availability > 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,6 +1,7 @@
|
||||
# -*- 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
|
||||
from . import mrp_consumption_warning
|
||||
|
||||
@@ -50,58 +50,40 @@ class ChangeProductionQty(models.TransientModel):
|
||||
format_qty = '%.{precision}f'.format(precision=precision)
|
||||
raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced))
|
||||
old_production_qty = production.product_qty
|
||||
production.write({'product_qty': wizard.product_qty})
|
||||
new_production_qty = wizard.product_qty
|
||||
done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id)
|
||||
qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id)
|
||||
factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty
|
||||
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
|
||||
|
||||
factor = (new_production_qty - qty_produced) / (old_production_qty - qty_produced)
|
||||
update_info = production._update_raw_moves(factor)
|
||||
documents = {}
|
||||
for line, line_data in lines:
|
||||
if line.child_bom_id and line.child_bom_id.type == 'phantom' or\
|
||||
line.product_id.type not in ['product', 'consu']:
|
||||
continue
|
||||
move = production.move_raw_ids.filtered(lambda x: x.bom_line_id.id == line.id and x.state not in ('done', 'cancel'))
|
||||
if move:
|
||||
move = move[0]
|
||||
old_qty = move.product_uom_qty
|
||||
else:
|
||||
old_qty = 0
|
||||
for move, old_qty, new_qty in update_info:
|
||||
iterate_key = production._get_document_iterate_key(move)
|
||||
if iterate_key:
|
||||
document = self.env['stock.picking']._log_activity_get_documents({move: (line_data['qty'], old_qty)}, iterate_key, 'UP')
|
||||
document = self.env['stock.picking']._log_activity_get_documents({move: (new_qty, old_qty)}, iterate_key, 'UP')
|
||||
for key, value in document.items():
|
||||
if documents.get(key):
|
||||
documents[key] += [value]
|
||||
else:
|
||||
documents[key] = [value]
|
||||
|
||||
production._update_raw_move(line, line_data)
|
||||
|
||||
production._log_manufacture_exception(documents)
|
||||
operation_bom_qty = {}
|
||||
for bom, bom_data in boms:
|
||||
for operation in bom.routing_id.operation_ids:
|
||||
operation_bom_qty[operation.id] = bom_data['qty']
|
||||
finished_moves_modification = self._update_finished_moves(production, production.product_qty - qty_produced, old_production_qty)
|
||||
production._log_downside_manufactured_quantity(finished_moves_modification)
|
||||
moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
moves._action_assign()
|
||||
finished_moves_modification = self._update_finished_moves(production, new_production_qty - qty_produced, old_production_qty - qty_produced)
|
||||
if finished_moves_modification:
|
||||
production._log_downside_manufactured_quantity(finished_moves_modification)
|
||||
production.write({'product_qty': new_production_qty})
|
||||
|
||||
for wo in production.workorder_ids:
|
||||
operation = wo.operation_id
|
||||
if operation_bom_qty.get(operation.id):
|
||||
cycle_number = float_round(operation_bom_qty[operation.id] / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
|
||||
wo.duration_expected = (operation.workcenter_id.time_start +
|
||||
operation.workcenter_id.time_stop +
|
||||
cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency)
|
||||
wo._onchange_expected_duration()
|
||||
quantity = wo.qty_production - wo.qty_produced
|
||||
if production.product_id.tracking == 'serial':
|
||||
quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0
|
||||
else:
|
||||
quantity = quantity if (quantity > 0) else 0
|
||||
if float_is_zero(quantity, precision_digits=precision):
|
||||
wo.finished_lot_id = False
|
||||
wo._workorder_line_ids().unlink()
|
||||
wo.qty_producing = quantity
|
||||
wo.check_ids.unlink()
|
||||
else:
|
||||
wo.qty_producing = quantity
|
||||
if wo.qty_produced < wo.qty_production and wo.state == 'done':
|
||||
wo.state = 'progress'
|
||||
if wo.qty_produced == wo.qty_production and wo.state == 'progress':
|
||||
@@ -117,11 +99,4 @@ class ChangeProductionQty(models.TransientModel):
|
||||
moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products?
|
||||
moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id})
|
||||
(moves_finished + moves_raw).write({'workorder_id': wo.id})
|
||||
if wo.state not in ('done', 'cancel'):
|
||||
line_values = wo._update_workorder_lines()
|
||||
wo._workorder_line_ids().create(line_values['to_create'])
|
||||
if line_values['to_delete']:
|
||||
line_values['to_delete'].unlink()
|
||||
for line, vals in line_values['to_update'].items():
|
||||
line.write(vals)
|
||||
return {}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class MrpConsumptionWarning(models.TransientModel):
|
||||
_name = 'mrp.consumption.warning'
|
||||
_description = "Wizard in case of consumption in warning/strict and more component has been used for a MO (related to the bom)"
|
||||
|
||||
mrp_production_ids = fields.Many2many('mrp.production')
|
||||
mrp_production_count = fields.Integer(compute="_compute_mrp_production_count")
|
||||
|
||||
consumption = fields.Selection([
|
||||
('flexible', 'Allowed'),
|
||||
('warning', 'Allowed with warning'),
|
||||
('strict', 'Blocked')], compute="_compute_consumption")
|
||||
mrp_consumption_warning_line_ids = fields.One2many('mrp.consumption.warning.line', 'mrp_consumption_warning_id')
|
||||
|
||||
@api.depends("mrp_production_ids")
|
||||
def _compute_mrp_production_count(self):
|
||||
for wizard in self:
|
||||
wizard.mrp_production_count = len(wizard.mrp_production_ids)
|
||||
|
||||
@api.depends("mrp_consumption_warning_line_ids.consumption")
|
||||
def _compute_consumption(self):
|
||||
for wizard in self:
|
||||
consumption_map = set(wizard.mrp_consumption_warning_line_ids.mapped("consumption"))
|
||||
wizard.consumption = "strict" in consumption_map and "strict" or "warning" in consumption_map and "warning" or "flexible"
|
||||
|
||||
def action_confirm(self):
|
||||
return self.mrp_production_ids.with_context(skip_consumption=True).button_mark_done()
|
||||
|
||||
def action_cancel(self):
|
||||
if self.env.context.get('from_workorder') and len(self.mrp_production_ids) == 1:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mrp.production',
|
||||
'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']],
|
||||
'res_id': self.mrp_production_ids.id,
|
||||
'target': 'main',
|
||||
}
|
||||
|
||||
class MrpConsumptionWarningLine(models.TransientModel):
|
||||
_name = 'mrp.consumption.warning.line'
|
||||
_description = "Line of issue consumption"
|
||||
|
||||
mrp_consumption_warning_id = fields.Many2one('mrp.consumption.warning', "Parent Wizard", readonly=True, required=True, ondelete="cascade")
|
||||
mrp_production_id = fields.Many2one('mrp.production', "Manufacturing Order", readonly=True, required=True, ondelete="cascade")
|
||||
consumption = fields.Selection(related="mrp_production_id.consumption")
|
||||
|
||||
product_id = fields.Many2one('product.product', "Product", readonly=True, required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', "Unit of Measure", related="product_id.uom_id", readonly=True)
|
||||
product_consumed_qty_uom = fields.Float("Consumed", readonly=True)
|
||||
product_expected_qty_uom = fields.Float("To Consume", readonly=True)
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- MO Consumption Warning -->
|
||||
<record id="view_mrp_consumption_warning_form" model="ir.ui.view">
|
||||
<field name="name">Consumption Warning</field>
|
||||
<field name="model">mrp.consumption.warning</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Consumption Warning">
|
||||
<field name="mrp_production_ids" invisible="1"/>
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="mrp_production_count" invisible="1"/>
|
||||
<div class="m-2">
|
||||
You consumed a different quantity than expected for the following products.
|
||||
<b attrs="{'invisible': [('consumption', '=', 'strict')]}">
|
||||
Please confirm it has been done on purpose.
|
||||
</b>
|
||||
<b attrs="{'invisible': [('consumption', '!=', 'strict')]}">
|
||||
Please review your component consumption or ask a manager to validate
|
||||
<span attrs="{'invisible':[('mrp_production_count', '!=', 1)]}">this manufacturing order</span>
|
||||
<span attrs="{'invisible':[('mrp_production_count', '=', 1)]}">these manufacturing orders</span>.
|
||||
</b>
|
||||
</div>
|
||||
<field name="mrp_consumption_warning_line_ids" nolabel="1">
|
||||
<tree create="0" delete="0" editable="top">
|
||||
<field name="mrp_production_id" attrs="{'column_invisible':[('parent.mrp_production_count', '=', 1)]}" force_save="1"/>
|
||||
<field name="consumption" invisible="1" force_save="1"/>
|
||||
<field name="product_id" force_save="1"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom" force_save="1"/>
|
||||
<field name="product_expected_qty_uom" force_save="1"/>
|
||||
<field name="product_consumed_qty_uom" force_save="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="action_confirm" string="Force"
|
||||
groups="mrp.group_mrp_manager" attrs="{'invisible': [('consumption', '!=', 'strict')]}"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button name="action_confirm" string="Confirm" attrs="{'invisible': [('consumption', '=', 'strict')]}"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button name="action_cancel" string="Review Consumption"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mrp_consumption_warning" model="ir.actions.act_window">
|
||||
<field name="name">Consumption Warning</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.consumption.warning</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -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 < 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 < 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>
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MrpProductionBackorderLine(models.TransientModel):
|
||||
_name = 'mrp.production.backorder.line'
|
||||
_description = "Backorder Confirmation Line"
|
||||
|
||||
mrp_production_backorder_id = fields.Many2one('mrp.production.backorder', 'MO Backorder', required=True, ondelete="cascade")
|
||||
mrp_production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, ondelete="cascade", readonly=True)
|
||||
to_backorder = fields.Boolean('To Backorder')
|
||||
|
||||
|
||||
class MrpProductionBackorder(models.TransientModel):
|
||||
_name = 'mrp.production.backorder'
|
||||
_description = "Wizard to mark as done or create back order"
|
||||
|
||||
mrp_production_ids = fields.Many2many('mrp.production')
|
||||
|
||||
mrp_production_backorder_line_ids = fields.One2many(
|
||||
'mrp.production.backorder.line',
|
||||
'mrp_production_backorder_id',
|
||||
string="Backorder Confirmation Lines")
|
||||
show_backorder_lines = fields.Boolean("Show backorder lines", compute="_compute_show_backorder_lines")
|
||||
|
||||
@api.depends('mrp_production_backorder_line_ids')
|
||||
def _compute_show_backorder_lines(self):
|
||||
for wizard in self:
|
||||
wizard.show_backorder_lines = len(wizard.mrp_production_backorder_line_ids) > 1
|
||||
|
||||
def action_close_mo(self):
|
||||
return self.mrp_production_ids.with_context(skip_backorder=True).button_mark_done()
|
||||
|
||||
def action_backorder(self):
|
||||
mo_ids_to_backorder = self.mrp_production_backorder_line_ids.filtered(lambda l: l.to_backorder).mrp_production_id.ids
|
||||
return self.mrp_production_ids.with_context(skip_backorder=True, mo_ids_to_backorder=mo_ids_to_backorder).button_mark_done()
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- MO Backorder -->
|
||||
<record id="view_mrp_production_backorder_form" model="ir.ui.view">
|
||||
<field name="name">Create Backorder</field>
|
||||
<field name="model">mrp.production.backorder</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create a Backorder">
|
||||
<group>
|
||||
<p>
|
||||
You produced less than the initial demand.
|
||||
</p><p class="text-muted">
|
||||
Create a backorder if you expect to process the remaining products later. Do not create a backorder if you will not process the remaining products.
|
||||
</p>
|
||||
</group>
|
||||
<field name="show_backorder_lines" invisible="1"/>
|
||||
<field name="mrp_production_backorder_line_ids" nolabel="1" attrs="{'invisible': [('show_backorder_lines', '=', False)]}">
|
||||
<tree create="0" delete="0" editable="top">
|
||||
<field name="mrp_production_id" force_save="1"/>
|
||||
<field name="to_backorder" widget="boolean_toggle"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="action_backorder" string="Create backorder"
|
||||
colspan="1" type="object" class="btn-primary" attrs="{'invisible': [('show_backorder_lines', '!=', False)]}"/>
|
||||
<button name="action_backorder" string="Validate"
|
||||
colspan="1" type="object" class="btn-primary" attrs="{'invisible': [('show_backorder_lines', '=', False)]}"/>
|
||||
<button name="action_close_mo" type="object" string="Mark as Done" attrs="{'invisible': [('show_backorder_lines', '!=', False)]}"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mrp_production_backorder" model="ir.actions.act_window">
|
||||
<field name="name">Backorder</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.production.backorder</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -71,9 +71,9 @@ class MrpProduction(models.Model):
|
||||
AccountAnalyticLine.create(vals)
|
||||
|
||||
def button_mark_done(self):
|
||||
self.ensure_one()
|
||||
res = super(MrpProduction, self).button_mark_done()
|
||||
self._costs_generate()
|
||||
for order in self:
|
||||
order._costs_generate()
|
||||
return res
|
||||
|
||||
def action_view_stock_valuation_layers(self):
|
||||
@@ -83,4 +83,5 @@ class MrpProduction(models.Model):
|
||||
context = literal_eval(action['context'])
|
||||
context.update(self.env.context)
|
||||
context['no_at_date'] = True
|
||||
context['search_default_group_by_product_id'] = False
|
||||
return dict(action, domain=domain, context=context)
|
||||
|
||||
@@ -47,7 +47,7 @@ class ProductProduct(models.Model):
|
||||
if not boms_to_recompute:
|
||||
boms_to_recompute = []
|
||||
total = 0
|
||||
for opt in bom.routing_id.operation_ids:
|
||||
for opt in bom.operation_ids:
|
||||
duration_expected = (
|
||||
opt.workcenter_id.time_start +
|
||||
opt.workcenter_id.time_stop +
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestBom(common.TransactionCase):
|
||||
super(TestBom, self).setUp()
|
||||
self.Product = self.env['product.product']
|
||||
self.Bom = self.env['mrp.bom']
|
||||
self.Routing = self.env['mrp.routing']
|
||||
#self.Routing = self.env['mrp.routing']
|
||||
self.operation = self.env['mrp.routing.workcenter']
|
||||
|
||||
# Products.
|
||||
@@ -117,37 +117,63 @@ class TestBom(common.TransactionCase):
|
||||
workcenter_from1.costs_hour = 100
|
||||
workcenter_1 = workcenter_from1.save()
|
||||
|
||||
routing_form1 = Form(self.Routing)
|
||||
routing_form1.name = 'Assembly Furniture'
|
||||
routing_1 = routing_form1.save()
|
||||
self.bom_1.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'batch': 'no',
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'batch': 'no',
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'batch': 'no',
|
||||
'sequence': 3,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
self.bom_2.write({
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'batch': 'no',
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'batch': 'no',
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'batch': 'no',
|
||||
'sequence': 3,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
operation_1 = self.operation.create({
|
||||
'name': 'Cutting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'routing_id': routing_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 20,
|
||||
'batch': 'no',
|
||||
'sequence': 1,
|
||||
})
|
||||
operation_2 = self.operation.create({
|
||||
'name': 'Drilling',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'routing_id': routing_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 25,
|
||||
'batch': 'no',
|
||||
'sequence': 2,
|
||||
})
|
||||
operation_3 = self.operation.create({
|
||||
'name': 'Fitting',
|
||||
'workcenter_id': workcenter_1.id,
|
||||
'routing_id': routing_1.id,
|
||||
'time_mode': 'manual',
|
||||
'time_cycle_manual': 30,
|
||||
'batch': 'no',
|
||||
'sequence': 3,
|
||||
})
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Dinning Table Operation Cost(1 Unit)
|
||||
@@ -160,7 +186,6 @@ class TestBom(common.TransactionCase):
|
||||
# Operation Cost 1 unit = 125
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
self.bom_1.routing_id = routing_1.id
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Table Head Operation Cost (1 Dozen)
|
||||
@@ -173,7 +198,6 @@ class TestBom(common.TransactionCase):
|
||||
# Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
self.bom_2.routing_id = routing_1.id
|
||||
|
||||
self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000")
|
||||
self.dining_table.button_bom_cost()
|
||||
|
||||
@@ -1,16 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mrp.tests.test_workorder_operation import TestWorkOrderProcessCommon
|
||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestMrpAccount(TestWorkOrderProcessCommon):
|
||||
class TestMrpAccount(TestMrpCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMrpAccount, cls).setUpClass()
|
||||
cls.source_location_id = cls.stock_location_14.id
|
||||
cls.warehouse = cls.env.ref('stock.warehouse0')
|
||||
# setting up alternative workcenters
|
||||
cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter bis',
|
||||
'capacity': 3,
|
||||
'time_start': 9,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Nuclear Workcenter ter',
|
||||
'capacity': 1,
|
||||
'time_start': 10,
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 85,
|
||||
})
|
||||
cls.product_4.uom_id = cls.uom_unit
|
||||
cls.planning_bom = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_4.id,
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
|
||||
(0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
|
||||
]})
|
||||
cls.dining_table = cls.env['product.product'].create({
|
||||
'name': 'Table (MTO)',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.product_table_sheet = cls.env['product.product'].create({
|
||||
'name': 'Table Top',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
cls.product_table_leg = cls.env['product.product'].create({
|
||||
'name': 'Table Leg',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
})
|
||||
cls.product_bolt = cls.env['product.product'].create({
|
||||
'name': 'Bolt',
|
||||
'type': 'product',
|
||||
})
|
||||
cls.product_screw = cls.env['product.product'].create({
|
||||
'name': 'Screw',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
cls.mrp_workcenter = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.mrp_bom_desk = cls.env['mrp.bom'].create({
|
||||
'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 3,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
|
||||
],
|
||||
})
|
||||
cls.mrp_bom_desk.write({
|
||||
'bom_line_ids': [
|
||||
(0, 0, {
|
||||
'product_id': cls.product_table_sheet.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 1,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_table_leg.id,
|
||||
'product_qty': 4,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 2,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_bolt.id,
|
||||
'product_qty': 4,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 3,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
(0, 0, {
|
||||
'product_id': cls.product_screw.id,
|
||||
'product_qty': 10,
|
||||
'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
|
||||
'sequence': 4,
|
||||
'operation_id': cls.mrp_bom_desk.operation_ids.id}),
|
||||
]
|
||||
})
|
||||
cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Drill Station 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
|
||||
'name': 'Assembly Line 1',
|
||||
'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
|
||||
})
|
||||
cls.categ_standard = cls.env['product.category'].create({
|
||||
'name': 'STANDARD',
|
||||
'property_cost_method': 'standard'
|
||||
@@ -66,24 +170,20 @@ class TestMrpAccount(TestWorkOrderProcessCommon):
|
||||
})
|
||||
inventory.action_validate
|
||||
bom = self.mrp_bom_desk.copy()
|
||||
bom.routing_id = False # TODO: extend the test later with the necessary operations
|
||||
bom.operation_ids = False
|
||||
production_table_form = Form(self.env['mrp.production'])
|
||||
production_table_form.product_id = self.dining_table
|
||||
production_table_form.bom_id = bom
|
||||
production_table_form.product_qty = 5.0
|
||||
production_table_form.product_qty = 1
|
||||
production_table = production_table_form.save()
|
||||
|
||||
production_table.extra_cost = 20
|
||||
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()
|
||||
production_table.post_inventory()
|
||||
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
|
||||
|
||||
# 1 table head at 20 + 4 table leg at 15 + 4 bolt at 10 + 10 screw at 10 + 1*20 (extra cost)
|
||||
|
||||
@@ -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):
|
||||
@@ -58,7 +55,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_backorder()
|
||||
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)
|
||||
@@ -94,7 +94,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_backorder()
|
||||
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)
|
||||
@@ -131,7 +134,7 @@ 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()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -169,7 +172,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -208,7 +211,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.value_svl, 7.2)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -246,7 +249,7 @@ 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()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 15)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<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">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button string="Valuation" type="object"
|
||||
name="action_view_stock_valuation_layers"
|
||||
class="oe_stat_button" icon="fa-dollar" groups="base.group_no_one"
|
||||
|
||||
@@ -37,16 +37,12 @@ class TestStockLandedCostsMrp(StockAccountTestCommon):
|
||||
'type': 'product',
|
||||
'categ_id': cls.categ_all.id
|
||||
})
|
||||
cls.routing_1 = cls.env['mrp.routing'].create({
|
||||
'name': 'Simple Line',
|
||||
})
|
||||
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
|
||||
cls.bom_refri = cls.env['mrp.bom'].create({
|
||||
'product_id': cls.product_refrigerator.id,
|
||||
'product_tmpl_id': cls.product_refrigerator.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'routing_id': cls.routing_1.id,
|
||||
'type': 'normal',
|
||||
})
|
||||
cls.bom_refri_line1 = cls.env['mrp.bom.line'].create({
|
||||
@@ -121,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,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_workorder
|
||||
from . import mrp_production
|
||||
|
||||
|
||||
+10
-3
@@ -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,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MrpWorkorder(models.Model):
|
||||
_inherit = 'mrp.workorder'
|
||||
|
||||
def record_production(self):
|
||||
confirm_expired_lots = self._check_expired_lots()
|
||||
if confirm_expired_lots:
|
||||
return confirm_expired_lots
|
||||
return super(MrpWorkorder, self).record_production()
|
||||
|
||||
def _get_expired_context(self, expired_lot_ids):
|
||||
context = super(MrpWorkorder, self)._get_expired_context(expired_lot_ids)
|
||||
context['default_workorder_id'] = self.id
|
||||
return context
|
||||
@@ -49,6 +49,7 @@ class TestStockProductionLot(TestStockCommon):
|
||||
'product_tmpl_id': cls.product_apple_pie.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_apple.id, 'product_qty': 3}),
|
||||
@@ -64,14 +65,6 @@ class TestStockProductionLot(TestStockCommon):
|
||||
'time_stop': 5,
|
||||
'time_efficiency': 80,
|
||||
})
|
||||
cls.routing = cls.env['mrp.routing'].create({'name': 'COOK'})
|
||||
cls.operation = cls.env['mrp.routing.workcenter'].create({
|
||||
'name': 'Bake in the oven',
|
||||
'workcenter_id': cls.workcenter.id,
|
||||
'routing_id': cls.routing.id,
|
||||
'time_cycle': 15,
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
def test_01_product_produce(self):
|
||||
""" Checks user doesn't get a confirmation wizard when they produces with
|
||||
@@ -84,16 +77,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
|
||||
@@ -106,72 +100,16 @@ 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')
|
||||
|
||||
def test_03_workorder_without_expired_lot(self):
|
||||
""" Checks user doesn't get a confirmation wizard when they makes a
|
||||
workorder without expired components. """
|
||||
# Set a routing on the BOM.
|
||||
self.bom_apple_pie.routing_id = self.routing
|
||||
# Creates the MO, starts it and plans the Work Order.
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_apple_pie
|
||||
mo_form.bom_id = self.bom_apple_pie
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
|
||||
wo = mo.workorder_ids[0]
|
||||
wo.button_start()
|
||||
# Set a non-expired lot.
|
||||
wo.raw_workorder_line_ids.write({
|
||||
'qty_done': 3,
|
||||
'lot_id': self.lot_good_apple,
|
||||
})
|
||||
|
||||
res = wo.record_production()
|
||||
# Try to record the production using non-expired lot must not return a wizard.
|
||||
self.assertEqual(res, True)
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_04_workorder_with_expired_lot(self):
|
||||
""" Checks user doesn't get a confirmation wizard when they makes a
|
||||
workorder without expired components. """
|
||||
# Set a routing on the BOM.
|
||||
self.bom_apple_pie.routing_id = self.routing
|
||||
# Creates the MO, starts it and plans the Work Order.
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = self.product_apple_pie
|
||||
mo_form.bom_id = self.bom_apple_pie
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
|
||||
wo = mo.workorder_ids[0]
|
||||
wo.button_start()
|
||||
# Set an expired lot.
|
||||
wo.raw_workorder_line_ids.write({
|
||||
'qty_done': 3,
|
||||
'lot_id': self.lot_expired_apple,
|
||||
})
|
||||
|
||||
res = wo.record_production()
|
||||
# Try to record the production using expired lot must return a
|
||||
# confirmation wizard.
|
||||
self.assertNotEqual(res, None)
|
||||
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
|
||||
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
|
||||
@@ -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,20 +61,23 @@ 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
|
||||
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
|
||||
productions |= production
|
||||
for subcontracted_production in productions:
|
||||
if subcontracted_production.state == 'progress':
|
||||
subcontracted_production.post_inventory()
|
||||
subcontracted_production._post_inventory()
|
||||
else:
|
||||
subcontracted_production.button_mark_done()
|
||||
# For concistency, set the date on production move before the date
|
||||
@@ -132,6 +138,7 @@ class StockPicking(models.Model):
|
||||
for move, bom in subcontract_details:
|
||||
mo = self.env['mrp.production'].with_company(move.company_id).create(self._prepare_subcontract_mo_vals(move, bom))
|
||||
self.env['stock.move'].create(mo._get_moves_raw_values())
|
||||
self.env['stock.move'].create(mo._get_moves_finished_values())
|
||||
mo.action_confirm()
|
||||
|
||||
# Link the finished to the receipt move.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,14 +12,15 @@ class PurchaseOrder(models.Model):
|
||||
compute='_compute_mrp_production_count',
|
||||
groups='mrp.group_mrp_user')
|
||||
|
||||
@api.depends('order_line.move_dest_ids.group_id.mrp_production_id')
|
||||
@api.depends('order_line.move_dest_ids.group_id.mrp_production_ids')
|
||||
def _compute_mrp_production_count(self):
|
||||
for purchase in self:
|
||||
purchase.mrp_production_count = len(purchase.order_line.move_dest_ids.group_id.mrp_production_id)
|
||||
purchase.mrp_production_count = len(purchase.order_line.move_dest_ids.group_id.mrp_production_ids |
|
||||
purchase.order_line.move_ids.move_dest_ids.group_id.mrp_production_ids)
|
||||
|
||||
def action_view_mrp_productions(self):
|
||||
self.ensure_one()
|
||||
mrp_production_ids = self.order_line.move_dest_ids.group_id.mrp_production_id.ids
|
||||
mrp_production_ids = (self.order_line.move_dest_ids.group_id.mrp_production_ids | self.order_line.move_ids.move_dest_ids.group_id.mrp_production_ids).ids
|
||||
action = {
|
||||
'res_model': 'mrp.production',
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
<record id="mrp_production_form_view_purchase" model="ir.ui.view">
|
||||
<field name="name">mrp.production.inherited.form.purchase</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="priority">32</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<xpath expr="//button[@name='action_view_mrp_production_childs']" position="before">
|
||||
<button class="oe_stat_button" name="action_view_purchase_orders" type="object" icon="fa-credit-card" groups="purchase.group_purchase_user" attrs="{'invisible': [('purchase_order_count', '=', 0)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="purchase_order_count"/></span>
|
||||
|
||||
@@ -12,14 +12,14 @@ class MrpProduction(models.Model):
|
||||
compute='_compute_sale_order_count',
|
||||
groups='sales_team.group_sale_salesman')
|
||||
|
||||
@api.depends('move_dest_ids.group_id.sale_id')
|
||||
@api.depends('procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id')
|
||||
def _compute_sale_order_count(self):
|
||||
for production in self:
|
||||
production.sale_order_count = len(production.move_dest_ids.group_id.sale_id)
|
||||
production.sale_order_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id)
|
||||
|
||||
def action_view_sale_orders(self):
|
||||
self.ensure_one()
|
||||
sale_order_ids = self.move_dest_ids.group_id.sale_id.ids
|
||||
sale_order_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id.ids
|
||||
action = {
|
||||
'res_model': 'sale.order',
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -12,14 +12,14 @@ class SaleOrder(models.Model):
|
||||
compute='_compute_mrp_production_count',
|
||||
groups='mrp.group_mrp_user')
|
||||
|
||||
@api.depends('procurement_group_id.stock_move_ids.created_production_id')
|
||||
@api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids')
|
||||
def _compute_mrp_production_count(self):
|
||||
for sale in self:
|
||||
sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id)
|
||||
sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids)
|
||||
|
||||
def action_view_mrp_production(self):
|
||||
self.ensure_one()
|
||||
mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.ids
|
||||
mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids
|
||||
action = {
|
||||
'res_model': 'mrp.production',
|
||||
'type': 'ir.actions.act_window',
|
||||
|
||||
@@ -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,15 +431,10 @@ 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()
|
||||
mnf_product_d.post_inventory()
|
||||
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.
|
||||
self.assertEqual(mnf_product_d.state, 'done', 'Manufacturing order should still be in progress state.')
|
||||
@@ -487,13 +481,10 @@ 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()
|
||||
mnf_product_a.post_inventory()
|
||||
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.')
|
||||
# Check product A avaialble quantity should be 120.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<record id="mrp_production_form_view_sale" model="ir.ui.view">
|
||||
<field name="name">mrp.production.inherited.form.sale</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="priority">64</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_mrp_production_childs']" position="before">
|
||||
|
||||
@@ -202,7 +202,7 @@ class StockMove(models.Model):
|
||||
else:
|
||||
move.is_locked = False
|
||||
|
||||
@api.depends('product_id', 'has_tracking')
|
||||
@api.depends('product_id', 'has_tracking', 'move_line_ids')
|
||||
def _compute_show_details_visible(self):
|
||||
""" According to this field, the button that calls `action_show_details` will be displayed
|
||||
to work on a move from its picking form view, or not.
|
||||
@@ -216,10 +216,12 @@ class StockMove(models.Model):
|
||||
for move in self:
|
||||
if not move.product_id:
|
||||
move.show_details_visible = False
|
||||
elif len(move.move_line_ids) > 1:
|
||||
move.show_details_visible = True
|
||||
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):
|
||||
@@ -280,9 +282,11 @@ class StockMove(models.Model):
|
||||
""" This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
|
||||
In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
|
||||
self.ensure_one()
|
||||
return self.move_line_ids or self.move_line_nosuggest_ids
|
||||
if self.picking_type_id.show_reserved is False:
|
||||
return self.move_line_nosuggest_ids
|
||||
return self.move_line_ids
|
||||
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done')
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done', 'picking_type_id')
|
||||
def _quantity_done_compute(self):
|
||||
""" This field represents the sum of the move lines `qty_done`. It allows the user to know
|
||||
if there is still work to do.
|
||||
@@ -292,29 +296,37 @@ class StockMove(models.Model):
|
||||
field will be used in `_action_done` in order to know if the move will need a backorder or
|
||||
an extra move.
|
||||
"""
|
||||
move_lines = self.env['stock.move.line']
|
||||
for move in self:
|
||||
move_lines |= move._get_move_lines()
|
||||
if not any(self._ids):
|
||||
# onchange
|
||||
for move in self:
|
||||
quantity_done = 0
|
||||
for move_line in move._get_move_lines():
|
||||
quantity_done += move_line.product_uom_id._compute_quantity(
|
||||
move_line.qty_done, move.product_uom, round=False)
|
||||
move.quantity_done = quantity_done
|
||||
else:
|
||||
# compute
|
||||
move_lines = self.env['stock.move.line']
|
||||
for move in self:
|
||||
move_lines |= move._get_move_lines()
|
||||
|
||||
data = self.env['stock.move.line'].read_group(
|
||||
[('id', 'in', move_lines.ids)],
|
||||
['move_id', 'product_uom_id', 'qty_done'], ['move_id', 'product_uom_id'],
|
||||
lazy=False
|
||||
)
|
||||
|
||||
rec = defaultdict(list)
|
||||
for d in data:
|
||||
rec[d['move_id'][0]] += [(d['product_uom_id'][0], d['qty_done'])]
|
||||
|
||||
# In case we are in an onchange, move.id is a NewId, not an integer. Therefore, there is no
|
||||
# match in the rec dictionary. By using move.ids[0] we get the correct integer value.
|
||||
for move in self:
|
||||
uom = move.product_uom
|
||||
move.quantity_done = sum(
|
||||
self.env['uom.uom'].browse(line_uom_id)._compute_quantity(qty, uom, round=False)
|
||||
for line_uom_id, qty in rec.get(move.ids[0] if move.ids else move.id, [])
|
||||
data = self.env['stock.move.line'].read_group(
|
||||
[('id', 'in', move_lines.ids)],
|
||||
['move_id', 'product_uom_id', 'qty_done'], ['move_id', 'product_uom_id'],
|
||||
lazy=False
|
||||
)
|
||||
|
||||
rec = defaultdict(list)
|
||||
for d in data:
|
||||
rec[d['move_id'][0]] += [(d['product_uom_id'][0], d['qty_done'])]
|
||||
|
||||
for move in self:
|
||||
uom = move.product_uom
|
||||
move.quantity_done = sum(
|
||||
self.env['uom.uom'].browse(line_uom_id)._compute_quantity(qty, uom, round=False)
|
||||
for line_uom_id, qty in rec.get(move.ids[0] if move.ids else move.id, [])
|
||||
)
|
||||
|
||||
def _quantity_done_set(self):
|
||||
quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done`
|
||||
for move in self:
|
||||
@@ -327,7 +339,12 @@ class StockMove(models.Model):
|
||||
elif len(move_lines) == 1:
|
||||
move_lines[0].qty_done = quantity_done
|
||||
else:
|
||||
raise UserError(_("Cannot set the done quantity from this stock move, work directly with the move lines."))
|
||||
# Bypass the error if we're trying to write the same value.
|
||||
ml_quantity_done = 0
|
||||
for move_line in move_lines:
|
||||
ml_quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom, round=False)
|
||||
if float_compare(quantity_done, ml_quantity_done, precision_rounding=move.product_uom.rounding) != 0:
|
||||
raise UserError(_("Cannot set the done quantity from this stock move, work directly with the move lines."))
|
||||
|
||||
def _set_product_qty(self):
|
||||
""" The meaning of product_qty field changed lately and is now a functional field computing the quantity
|
||||
@@ -342,10 +359,19 @@ class StockMove(models.Model):
|
||||
and is represented by the aggregated `product_qty` on the linked move lines. If the move
|
||||
is force assigned, the value will be 0.
|
||||
"""
|
||||
result = {data['move_id'][0]: data['product_qty'] for data in
|
||||
self.env['stock.move.line'].read_group([('move_id', 'in', self.ids)], ['move_id','product_qty'], ['move_id'])}
|
||||
for rec in self:
|
||||
rec.reserved_availability = rec.product_id.uom_id._compute_quantity(result.get(rec.id, 0.0), rec.product_uom, rounding_method='HALF-UP')
|
||||
if not any(self._ids):
|
||||
# onchange
|
||||
for move in self:
|
||||
reserved_availability = sum(move.move_line_ids.mapped('product_qty'))
|
||||
move.reserved_availability = move.product_id.uom_id._compute_quantity(
|
||||
reserved_availability, move.product_uom, rounding_method='HALF-UP')
|
||||
else:
|
||||
# compute
|
||||
result = {data['move_id'][0]: data['product_qty'] for data in
|
||||
self.env['stock.move.line'].read_group([('move_id', 'in', self.ids)], ['move_id', 'product_qty'], ['move_id'])}
|
||||
for move in self:
|
||||
move.reserved_availability = move.product_id.uom_id._compute_quantity(
|
||||
result.get(move.id, 0.0), move.product_uom, rounding_method='HALF-UP')
|
||||
|
||||
@api.depends('state', 'product_id', 'product_qty', 'location_id')
|
||||
def _compute_product_availability(self):
|
||||
@@ -989,6 +1015,8 @@ class StockMove(models.Model):
|
||||
|
||||
to_assign = {}
|
||||
for move in self:
|
||||
if move.state != 'draft':
|
||||
continue
|
||||
# if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available)
|
||||
if move.move_orig_ids:
|
||||
move_waiting |= move
|
||||
@@ -1525,13 +1553,8 @@ class StockMove(models.Model):
|
||||
else:
|
||||
return [(self.picking_id, self.product_id.responsible_id, visited)]
|
||||
|
||||
def _set_quantity_done(self, qty):
|
||||
"""
|
||||
Set the given quantity as quantity done on the move through the move lines. The method is
|
||||
able to handle move lines with a different UoM than the move (but honestly, this would be
|
||||
looking for trouble...).
|
||||
@param qty: quantity in the UoM of move.product_uom
|
||||
"""
|
||||
def _set_quantity_done_prepare_vals(self, qty):
|
||||
res = []
|
||||
for ml in self.move_line_ids:
|
||||
ml_qty = ml.product_uom_qty - ml.qty_done
|
||||
if float_compare(ml_qty, 0, precision_rounding=ml.product_uom_id.rounding) <= 0:
|
||||
@@ -1548,7 +1571,7 @@ class StockMove(models.Model):
|
||||
# Assign qty_done and explicitly round to make sure there is no inconsistency between
|
||||
# ml.qty_done and qty.
|
||||
taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding)
|
||||
ml.qty_done += taken_qty
|
||||
res.append((1, ml.id, {'qty_done': ml.qty_done + taken_qty}))
|
||||
if ml.product_uom_id != self.product_uom:
|
||||
taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)
|
||||
qty -= taken_qty
|
||||
@@ -1556,9 +1579,27 @@ class StockMove(models.Model):
|
||||
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0:
|
||||
break
|
||||
if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) > 0:
|
||||
vals = self._prepare_move_line_vals(quantity=0)
|
||||
vals['qty_done'] = qty
|
||||
ml = self.env['stock.move.line'].create(vals)
|
||||
if self.product_id.tracking != 'serial':
|
||||
vals = self._prepare_move_line_vals(quantity=0)
|
||||
vals['qty_done'] = qty
|
||||
res.append((0, 0, vals))
|
||||
else:
|
||||
uom_qty = self.product_uom._compute_quantity(qty, self.product_id.uom_id)
|
||||
for i in range(0, int(uom_qty)):
|
||||
vals = self._prepare_move_line_vals(quantity=0)
|
||||
vals['qty_done'] = 1
|
||||
vals['product_uom_id'] = self.product_id.uom_id.id
|
||||
res.append((0, 0, vals))
|
||||
return res
|
||||
|
||||
def _set_quantity_done(self, qty):
|
||||
"""
|
||||
Set the given quantity as quantity done on the move through the move lines. The method is
|
||||
able to handle move lines with a different UoM than the move (but honestly, this would be
|
||||
looking for trouble...).
|
||||
@param qty: quantity in the UoM of move.product_uom
|
||||
"""
|
||||
self.move_line_ids = self._set_quantity_done_prepare_vals(qty)
|
||||
|
||||
def _adjust_procure_method(self):
|
||||
""" This method will try to apply the procure method MTO on some moves if
|
||||
@@ -1610,3 +1651,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')
|
||||
|
||||
@@ -148,14 +148,15 @@ class StockMoveLine(models.Model):
|
||||
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||
return res
|
||||
|
||||
@api.onchange('qty_done')
|
||||
@api.onchange('qty_done', 'product_uom_id')
|
||||
def _onchange_qty_done(self):
|
||||
""" When the user is encoding a move line for a tracked product, we apply some logic to
|
||||
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
|
||||
"""
|
||||
res = {}
|
||||
if self.qty_done and self.product_id.tracking == 'serial':
|
||||
if float_compare(self.qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0:
|
||||
qty_done = self.product_uom_id._compute_quantity(self.qty_done, self.product_id.uom_id)
|
||||
if float_compare(qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0:
|
||||
message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
|
||||
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||
return res
|
||||
|
||||
@@ -4369,6 +4369,7 @@ class StockMove(SavepointCase):
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
@@ -4458,6 +4459,7 @@ class StockMove(SavepointCase):
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move2 = self.env['stock.move'].create({
|
||||
'name': 'test_transit_2',
|
||||
@@ -4467,6 +4469,7 @@ class StockMove(SavepointCase):
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'picking_id': picking.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
|
||||
@@ -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')"),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user