Initial commit: Mrp packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 50d736b3bd
739 changed files with 538193 additions and 0 deletions

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import change_production_qty
from . import stock_warn_insufficient_qty
from . import mrp_production_backorder
from . import mrp_consumption_warning
from . import mrp_immediate_production
from . import stock_assign_serial_numbers
from . import mrp_production_split

View file

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero
class ChangeProductionQty(models.TransientModel):
_name = 'change.production.qty'
_description = 'Change Production Qty'
mo_id = fields.Many2one('mrp.production', 'Manufacturing Order',
required=True, ondelete='cascade')
product_qty = fields.Float(
'Quantity To Produce',
digits='Product Unit of Measure', required=True)
@api.model
def default_get(self, fields):
res = super(ChangeProductionQty, self).default_get(fields)
if 'mo_id' in fields and not res.get('mo_id') and self._context.get('active_model') == 'mrp.production' and self._context.get('active_id'):
res['mo_id'] = self._context['active_id']
if 'product_qty' in fields and not res.get('product_qty') and res.get('mo_id'):
res['product_qty'] = self.env['mrp.production'].browse(res['mo_id']).product_qty
return res
@api.model
def _update_finished_moves(self, production, new_qty, old_qty):
""" Update finished product and its byproducts. This method only update
the finished moves not done or cancel and just increase or decrease
their quantity according the unit_ratio. It does not use the BoM, BoM
modification during production would not be taken into consideration.
"""
modification = {}
push_moves = self.env['stock.move']
for move in production.move_finished_ids:
if move.state in ('done', 'cancel'):
continue
qty = (new_qty - old_qty) * move.unit_factor
modification[move] = (move.product_uom_qty + qty, move.product_uom_qty)
if self._need_quantity_propagation(move, qty):
push_moves |= move.copy({'product_uom_qty': qty})
else:
self._update_product_qty(move, qty)
if push_moves:
push_moves._action_confirm()._action_assign()
return modification
@api.model
def _need_quantity_propagation(self, move, qty):
return move.move_dest_ids and not float_is_zero(qty, precision_rounding=move.product_uom.rounding)
@api.model
def _update_product_qty(self, move, qty):
move.write({'product_uom_qty': move.product_uom_qty + qty})
def change_prod_qty(self):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for wizard in self:
production = wizard.mo_id
produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done'))
if wizard.product_qty < produced:
format_qty = '%.{precision}f'.format(precision=precision)
raise UserError(_(
"You have already processed %(quantity)s. Please input a quantity higher than %(minimum)s ",
quantity=format_qty % produced,
minimum=format_qty % produced
))
old_production_qty = production.product_qty
new_production_qty = wizard.product_qty
factor = new_production_qty / old_production_qty
update_info = production._update_raw_moves(factor)
documents = {}
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: (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._log_manufacture_exception(documents)
self._update_finished_moves(production, new_production_qty, old_production_qty)
production.write({'product_qty': new_production_qty})
if not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding) and\
not production.workorder_ids:
production.qty_producing = new_production_qty
production._set_qty_producing()
for wo in production.workorder_ids:
operation = wo.operation_id
wo.duration_expected = wo._get_duration_expected(ratio=new_production_qty / old_production_qty)
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 and not float_is_zero(quantity, precision_digits=precision)) else 0
wo._update_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':
wo.state = 'done'
# assign moves; last operation receive all unassigned moves
# TODO: following could be put in a function as it is similar as code in _workorders_create
# TODO: only needed when creating new moves
moves_raw = production.move_raw_ids.filtered(lambda move: move.operation_id == operation and move.state not in ('done', 'cancel'))
if wo == production.workorder_ids[-1]:
moves_raw |= production.move_raw_ids.filtered(lambda move: not move.operation_id)
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})
# run scheduler for moves forecasted to not have enough in stock
self.mo_id.filtered(lambda mo: mo.state in ['confirmed', 'progress']).move_raw_ids._trigger_scheduler()
return {}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Change Product Quantity -->
<record id="view_change_production_qty_wizard" model="ir.ui.view">
<field name="name">Change Quantity To Produce</field>
<field name="model">change.production.qty</field>
<field name="arch" type="xml">
<form string="Change Product Qty">
<group>
<field name="product_qty"/>
<field name="mo_id" invisible="1"/>
</group>
<footer>
<button name="change_prod_qty" string="Approve" data-hotkey="q"
colspan="1" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="action_change_production_qty" model="ir.actions.act_window">
<field name="name">Change Quantity To Produce</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">change.production.qty</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, models, api
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero
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):
ctx = dict(self.env.context)
ctx.pop('default_mrp_production_ids', None)
return self.mrp_production_ids.with_context(ctx, skip_consumption=True).button_mark_done()
def action_set_qty(self):
missing_move_vals = []
problem_tracked_products = self.env['product.product']
for production in self.mrp_production_ids:
for line in self.mrp_consumption_warning_line_ids:
if line.mrp_production_id != production:
continue
for move in production.move_raw_ids:
if line.product_id != move.product_id:
continue
qty_expected = line.product_uom_id._compute_quantity(line.product_expected_qty_uom, move.product_uom)
qty_compare_result = float_compare(qty_expected, move.quantity_done, precision_rounding=move.product_uom.rounding)
if qty_compare_result != 0:
if (move.has_tracking in ('lot', 'serial')
and not production.use_auto_consume_components_lots
and qty_compare_result > 0):
problem_tracked_products |= line.product_id
break
move.quantity_done = qty_expected
# in case multiple lines with same product => set others to 0 since we have no way to know how to distribute the qty done
line.product_expected_qty_uom = 0
# move was deleted before confirming MO or force deleted somehow
if not float_is_zero(line.product_expected_qty_uom, precision_rounding=line.product_uom_id.rounding):
if line.product_id.tracking in ('lot', 'serial') and not line.mrp_production_id.use_auto_consume_components_lots:
problem_tracked_products |= line.product_id
continue
missing_move_vals.append({
'product_id': line.product_id.id,
'product_uom': line.product_uom_id.id,
'product_uom_qty': line.product_expected_qty_uom,
'quantity_done': line.product_expected_qty_uom,
'raw_material_production_id': line.mrp_production_id.id,
'additional': True,
})
if problem_tracked_products:
raise UserError(
_("Values cannot be set and validated because a Lot/Serial Number needs to be specified for a tracked product that is having its consumed amount increased:\n- ") +
"\n- ".join(problem_tracked_products.mapped('name'))
)
if missing_move_vals:
self.env['stock.move'].create(missing_move_vals)
return self.action_confirm()
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)

View file

@ -0,0 +1,58 @@
<?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" data-hotkey="q"
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')]}" data-hotkey="q"
colspan="1" type="object" class="btn-primary"/>
<button name="action_set_qty" string="Set Quantities &amp; Validate" colspan="1" type="object" class="btn-secondary"/>
<button name="action_cancel" string="Discard" data-hotkey="w"
colspan="1" type="object" class="btn-secondary"/>
</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>

View file

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero
class MrpImmediateProductionLine(models.TransientModel):
_name = 'mrp.immediate.production.line'
_description = 'Immediate Production Line'
immediate_production_id = fields.Many2one('mrp.immediate.production', 'Immediate Production', required=True)
production_id = fields.Many2one('mrp.production', 'Production', required=True)
to_immediate = fields.Boolean('To Process')
class MrpImmediateProduction(models.TransientModel):
_name = 'mrp.immediate.production'
_description = 'Immediate Production'
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if 'immediate_production_line_ids' in fields:
if self.env.context.get('default_mo_ids'):
res['mo_ids'] = self.env.context['default_mo_ids']
res['immediate_production_line_ids'] = [(0, 0, {'to_immediate': True, 'production_id': mo_id[1]}) for mo_id in res['mo_ids']]
return res
mo_ids = fields.Many2many('mrp.production', 'mrp_production_production_rel')
show_productions = fields.Boolean(compute='_compute_show_production')
immediate_production_line_ids = fields.One2many(
'mrp.immediate.production.line',
'immediate_production_id',
string="Immediate Production Lines")
@api.depends('immediate_production_line_ids')
def _compute_show_production(self):
for wizard in self:
wizard.show_productions = len(wizard.immediate_production_line_ids.production_id) > 1
def process(self):
productions_to_do = self.env['mrp.production']
productions_not_to_do = self.env['mrp.production']
for line in self.immediate_production_line_ids:
if line.to_immediate is True:
productions_to_do |= line.production_id
else:
productions_not_to_do |= line.production_id
for production in productions_to_do:
error_msg = ""
if production.product_tracking in ('lot', 'serial') and not production.lot_producing_id:
production.action_generate_serial()
if production.product_tracking == 'serial' and float_compare(production.qty_producing, 1, precision_rounding=production.product_uom_id.rounding) == 1:
production.qty_producing = 1
else:
production.qty_producing = production.product_qty - production.qty_produced
production._set_qty_producing()
for move in production.move_raw_ids:
if move.state in ('done', 'cancel') or not move.product_uom_qty:
continue
rounding = move.product_uom.rounding
if move.has_tracking in ('serial', 'lot') and float_is_zero(move.quantity_done, precision_rounding=rounding):
error_msg += "\n - %s" % move.product_id.display_name
if error_msg:
error_msg = _('You need to supply Lot/Serial Number for products:') + error_msg
raise UserError(error_msg)
productions_to_validate = self.env.context.get('button_mark_done_production_ids')
if productions_to_validate:
productions_to_validate = self.env['mrp.production'].browse(productions_to_validate)
productions_to_validate = productions_to_validate - productions_not_to_do
consumption_issues = productions_to_validate._get_consumption_issues()
if consumption_issues:
return productions_to_validate._action_generate_consumption_wizard(consumption_issues)
return productions_to_validate.with_context(skip_immediate=True).button_mark_done()
return True

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_immediate_production" model="ir.ui.view">
<field name="name">mrp.immediate.production.view.form</field>
<field name="model">mrp.immediate.production</field>
<field name="arch" type="xml">
<form string="Immediate production?">
<group>
<p colspan="2">You have not recorded <i>produced</i> quantities yet, by clicking on <i>apply</i> Odoo will produce all the finished products and consume all components.</p>
</group>
<field name="show_productions" invisible="1"/>
<field name="immediate_production_line_ids" nolabel="1" attrs="{'invisible': [('show_productions', '=', False)]}">
<tree create="0" delete="0" editable="top">
<field name="production_id"/>
<field name="to_immediate" widget="boolean_toggle"/>
</tree>
</field>
<footer>
<button name="process" string="Apply" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,40 @@
# -*- 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):
ctx = dict(self.env.context)
ctx.pop('default_mrp_production_ids', None)
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(ctx, skip_backorder=True, mo_ids_to_backorder=mo_ids_to_backorder).button_mark_done()

View file

@ -0,0 +1,44 @@
<?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 colspan="2">
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" data-hotkey="q"
colspan="1" type="object" class="btn-primary" attrs="{'invisible': [('show_backorder_lines', '!=', False)]}"/>
<button name="action_backorder" string="Validate" data-hotkey="q"
colspan="1" type="object" class="btn-primary" attrs="{'invisible': [('show_backorder_lines', '=', False)]}"/>
<button name="action_close_mo" type="object" string="No Backorder" attrs="{'invisible': [('show_backorder_lines', '!=', False)]}" data-hotkey="x"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="action_mrp_production_backorder" model="ir.actions.act_window">
<field name="name">You produced less than initial demand</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>

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, Command
from odoo.tools import float_round, float_compare
class MrpProductionSplitMulti(models.TransientModel):
_name = 'mrp.production.split.multi'
_description = "Wizard to Split Multiple Productions"
production_ids = fields.One2many('mrp.production.split', 'production_split_multi_id', 'Productions To Split')
class MrpProductionSplit(models.TransientModel):
_name = 'mrp.production.split'
_description = "Wizard to Split a Production"
production_split_multi_id = fields.Many2one('mrp.production.split.multi', 'Split Productions')
production_id = fields.Many2one('mrp.production', 'Manufacturing Order', readonly=True)
product_id = fields.Many2one(related='production_id.product_id')
product_qty = fields.Float(related='production_id.product_qty')
product_uom_id = fields.Many2one(related='production_id.product_uom_id')
production_capacity = fields.Float(related='production_id.production_capacity')
counter = fields.Integer(
"Split #", default=0, compute="_compute_counter",
store=True, readonly=False)
production_detailed_vals_ids = fields.One2many(
'mrp.production.split.line', 'mrp_production_split_id',
'Split Details', compute="_compute_details", store=True, readonly=False)
valid_details = fields.Boolean("Valid", compute="_compute_valid_details")
@api.depends('production_detailed_vals_ids')
def _compute_counter(self):
for wizard in self:
wizard.counter = len(wizard.production_detailed_vals_ids)
@api.depends('counter')
def _compute_details(self):
for wizard in self:
commands = [Command.clear()]
if wizard.counter < 1 or not wizard.production_id:
wizard.production_detailed_vals_ids = commands
continue
quantity = float_round(wizard.product_qty / wizard.counter, precision_rounding=wizard.product_uom_id.rounding)
remaining_quantity = wizard.product_qty
for _ in range(wizard.counter - 1):
commands.append(Command.create({
'quantity': quantity,
'user_id': wizard.production_id.user_id,
'date': wizard.production_id.date_planned_start,
}))
remaining_quantity = float_round(remaining_quantity - quantity, precision_rounding=wizard.product_uom_id.rounding)
commands.append(Command.create({
'quantity': remaining_quantity,
'user_id': wizard.production_id.user_id,
'date': wizard.production_id.date_planned_start,
}))
wizard.production_detailed_vals_ids = commands
@api.depends('production_detailed_vals_ids')
def _compute_valid_details(self):
self.valid_details = False
for wizard in self:
if wizard.production_detailed_vals_ids:
wizard.valid_details = float_compare(wizard.product_qty, sum(wizard.production_detailed_vals_ids.mapped('quantity')), precision_rounding=wizard.product_uom_id.rounding) == 0
def action_split(self):
productions = self.production_id._split_productions({self.production_id: [detail.quantity for detail in self.production_detailed_vals_ids]})
for production, detail in zip(productions, self.production_detailed_vals_ids):
production.user_id = detail.user_id
production.date_planned_start = detail.date
if self.production_split_multi_id:
saved_production_split_multi_id = self.production_split_multi_id.id
self.production_split_multi_id.production_ids = [Command.unlink(self.id)]
action = self.env['ir.actions.actions']._for_xml_id('mrp.action_mrp_production_split_multi')
action['res_id'] = saved_production_split_multi_id
return action
def action_prepare_split(self):
action = self.env['ir.actions.actions']._for_xml_id('mrp.action_mrp_production_split')
action['res_id'] = self.id
return action
def action_return_to_list(self):
self.production_detailed_vals_ids = [Command.clear()]
self.counter = 0
action = self.env['ir.actions.actions']._for_xml_id('mrp.action_mrp_production_split_multi')
action['res_id'] = self.production_split_multi_id.id
return action
class MrpProductionSplitLine(models.TransientModel):
_name = 'mrp.production.split.line'
_description = "Split Production Detail"
mrp_production_split_id = fields.Many2one(
'mrp.production.split', 'Split Production', required=True, ondelete="cascade")
quantity = fields.Float('Quantity To Produce', digits='Product Unit of Measure', required=True)
user_id = fields.Many2one(
'res.users', 'Responsible',
domain=lambda self: [('groups_id', 'in', self.env.ref('mrp.group_mrp_user').id)])
date = fields.Datetime('Schedule Date')

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_mrp_production_split_multi_form" model="ir.ui.view">
<field name="name">mrp.production.split.multi.form</field>
<field name="model">mrp.production.split.multi</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Split Productions">
<field name="production_ids">
<tree create="0" editable="top">
<field name="production_id"/>
<field name="product_id"/>
<field name="product_qty"/>
<field name="production_capacity"/>
<field name="product_uom_id" groups="uom.group_uom"/>
<button name="action_prepare_split" type="object" icon="fa-scissors" width="0.1" title="Split Production"/>
</tree>
</field>
<footer>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="view_mrp_production_split_form" model="ir.ui.view">
<field name="name">Split Production</field>
<field name="model">mrp.production.split</field>
<field name="arch" type="xml">
<form string="Split Production">
<group>
<group>
<field name="production_id" readonly="1"/>
</group>
<group>
<field name="product_id"/>
<label for="product_qty"/>
<div class="o_row">
<span><field name="product_qty"/></span>
<span><field name="product_uom_id" groups="uom.group_uom"/></span>
</div>
</group>
<group>
<field name="counter"/>
</group>
<group>
<label for="production_capacity"/>
<div class="o_row">
<span><field name="production_capacity"/></span>
<span><field name="product_uom_id" groups="uom.group_uom"/></span>
</div>
</group>
</group>
<field name="production_detailed_vals_ids" attrs="{'invisible': [('counter', '=', 0)]}">
<tree editable="top">
<field name="date"/>
<field name="user_id"/>
<field name="quantity"/>
</tree>
</field>
<field name="production_split_multi_id" invisible="1"/>
<field name="valid_details" invisible="1"/>
<footer>
<button string="Split" class="btn-primary" type="object" name="action_split" data-hotkey="q" attrs="{'invisible': [('valid_details', '=', False)]}"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z" attrs="{'invisible': [('production_split_multi_id', '!=', False)]}"/>
<button string="Discard" class="btn-secondary" type="object" name="action_return_to_list" data-hotkey="z" attrs="{'invisible': [('production_split_multi_id', '=', False)]}"/>
</footer>
</form>
</field>
</record>
<record id="action_mrp_production_split_multi" model="ir.actions.act_window">
<field name="name">Split productions</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.production.split.multi</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="action_mrp_production_split" model="ir.actions.act_window">
<field name="name">Split production</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.production.split</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Workcenter Block Dialog -->
<record id="mrp_workcenter_block_wizard_form" model="ir.ui.view">
<field name="name">mrp.workcenter.productivity.form</field>
<field name="model">mrp.workcenter.productivity</field>
<field name="arch" type="xml">
<form string="Block Workcenter">
<group>
<field name="loss_id" class="oe_inline" domain="[('manual','=',True)]"/>
<field name="description" placeholder="Add a description..."/>
<field name="workcenter_id" invisible="1"/>
<field name="company_id" invisible="1"/>
</group>
<footer>
<button name="button_block" string="Block" type="object" class="btn-danger text-uppercase" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="act_mrp_block_workcenter" model="ir.actions.act_window">
<field name="name">Block Workcenter</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.workcenter.productivity</field>
<field name="view_mode">form</field>
<field name="context">{'default_workcenter_id': active_id}</field>
<field name="view_id" eval="mrp_workcenter_block_wizard_form"/>
<field name="target">new</field>
</record>
<record id="act_mrp_block_workcenter_wo" model="ir.actions.act_window">
<field name="name">Block Workcenter</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.workcenter.productivity</field>
<field name="view_mode">form</field>
<field name="view_id" eval="mrp_workcenter_block_wizard_form"/>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import Counter
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare
class StockAssignSerialNumbers(models.TransientModel):
_inherit = 'stock.assign.serial'
production_id = fields.Many2one('mrp.production', 'Production')
expected_qty = fields.Float('Expected Quantity', digits='Product Unit of Measure')
serial_numbers = fields.Text('Produced Serial Numbers')
produced_qty = fields.Float('Produced Quantity', digits='Product Unit of Measure')
show_apply = fields.Boolean() # Technical field to show the Apply button
show_backorders = fields.Boolean() # Technical field to show the Create Backorder and No Backorder buttons
multiple_lot_components_names = fields.Text() # Names of components with multiple lots, used to show warning
def generate_serial_numbers_production(self):
if self.next_serial_number and self.next_serial_count:
generated_serial_numbers = "\n".join(self.env['stock.lot'].generate_lot_names(self.next_serial_number, self.next_serial_count))
self.serial_numbers = "\n".join([self.serial_numbers, generated_serial_numbers]) if self.serial_numbers else generated_serial_numbers
self._onchange_serial_numbers()
action = self.env["ir.actions.actions"]._for_xml_id("mrp.act_assign_serial_numbers_production")
action['res_id'] = self.id
return action
def _get_serial_numbers(self):
if self.serial_numbers:
return list(filter(lambda serial_number: len(serial_number.strip()) > 0, self.serial_numbers.split('\n')))
return []
@api.onchange('serial_numbers')
def _onchange_serial_numbers(self):
self.show_apply = False
self.show_backorders = False
serial_numbers = self._get_serial_numbers()
duplicate_serial_numbers = [serial_number for serial_number, counter in Counter(serial_numbers).items() if counter > 1]
if duplicate_serial_numbers:
self.serial_numbers = ""
self.produced_qty = 0
raise UserError(_('Duplicate Serial Numbers (%s)') % ','.join(duplicate_serial_numbers))
existing_serial_numbers = self.env['stock.lot'].search([
('company_id', '=', self.production_id.company_id.id),
('product_id', '=', self.production_id.product_id.id),
('name', 'in', serial_numbers),
])
if existing_serial_numbers:
self.serial_numbers = ""
self.produced_qty = 0
raise UserError(_('Existing Serial Numbers (%s)') % ','.join(existing_serial_numbers.mapped('display_name')))
if len(serial_numbers) > self.expected_qty:
self.serial_numbers = ""
self.produced_qty = 0
raise UserError(_('There are more Serial Numbers than the Quantity to Produce'))
self.produced_qty = len(serial_numbers)
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
self.show_apply = float_compare(self.produced_qty, self.expected_qty, precision_digits=precision) == 0
self.show_backorders = self.produced_qty > 0 and self.produced_qty < self.expected_qty
def _assign_serial_numbers(self, cancel_remaining_quantity=False):
serial_numbers = self._get_serial_numbers()
productions = self.production_id._split_productions(
{self.production_id: [1] * len(serial_numbers)}, cancel_remaining_quantity, set_consumed_qty=True)
production_lots_vals = []
for serial_name in serial_numbers:
production_lots_vals.append({
'product_id': self.production_id.product_id.id,
'company_id': self.production_id.company_id.id,
'name': serial_name,
})
production_lots = self.env['stock.lot'].create(production_lots_vals)
for production, production_lot in zip(productions, production_lots):
production.lot_producing_id = production_lot.id
production.qty_producing = production.product_qty
for workorder in production.workorder_ids:
workorder.qty_produced = workorder.qty_producing
if productions and len(production_lots) < len(productions):
productions[-1].move_raw_ids.move_line_ids.write({'qty_done': 0})
productions[-1].state = "confirmed"
def apply(self):
self._assign_serial_numbers()
def create_backorder(self):
self._assign_serial_numbers(False)
def no_backorder(self):
self._assign_serial_numbers(True)

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_assign_serial_numbers_production" model="ir.ui.view">
<field name="name">mrp_assign_serial_numbers</field>
<field name="model">stock.assign.serial</field>
<field name="arch" type="xml">
<form string="Serial Mass Produce">
<group>
<field name="production_id" readonly="True"/>
</group>
<group>
<group>
<field name="next_serial_number"/>
</group>
<group>
<label for="next_serial_count"/>
<div class="o_row">
<span><field name="next_serial_count"/></span>
<button name="generate_serial_numbers_production" type="object" class="btn btn-secondary" title="Generate Serial Numbers">
<span>Generate</span>
</button>
</div>
</group>
</group>
<group>
<field name="serial_numbers" placeholder="copy paste a list and/or use Generate"/>
</group>
<field name="multiple_lot_components_names" invisible="1"/>
<group col="1">
<p class="o_form_label oe_inline text-danger" attrs="{'invisible': [('multiple_lot_components_names', '=', False)]}">
Note that components <field name="multiple_lot_components_names" readonly="True"/> have multiple lot reservations.<br/>
Do you want to confirm anyway ?
</p>
</group>
<field name="show_apply" invisible="1" />
<field name="show_backorders" invisible="1" />
<group>
<group>
<field name="produced_qty" readonly="True" force_save="True"/>
</group>
<group>
<field name="expected_qty" readonly="True"/>
</group>
<p col="1" class="o_form_label oe_inline" attrs="{'invisible': [('show_backorders', '=', False)]}">
You have entered less serial numbers than the quantity to produce.<br/>
Create a backorder if you expect to process the remaining quantities later.<br/>
Do not create a backorder if you will not process the remaining products.
</p>
</group>
<footer>
<button name="apply" string="Apply" type="object" class="btn-primary" attrs="{'invisible': [('show_apply', '=', False)]}"/>
<button name="create_backorder" string="Create Backorder" type="object" class="btn-primary" attrs="{'invisible': [('show_backorders', '=', False)]}"/>
<button name="no_backorder" string="No Backorder" type="object" class="btn-primary" attrs="{'invisible': [('show_backorders', '=', False)]}"/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="act_assign_serial_numbers_production" model="ir.actions.act_window">
<field name="name">Assign Serial Numbers</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.assign.serial</field>
<field name="view_id" ref="view_assign_serial_numbers_production"/>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class StockWarnInsufficientQtyUnbuild(models.TransientModel):
_name = 'stock.warn.insufficient.qty.unbuild'
_inherit = 'stock.warn.insufficient.qty'
_description = 'Warn Insufficient Unbuild Quantity'
unbuild_id = fields.Many2one('mrp.unbuild', 'Unbuild')
def _get_reference_document_company_id(self):
return self.unbuild_id.company_id
def action_done(self):
self.ensure_one()
return self.unbuild_id.action_unbuild()

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_warn_insufficient_qty_unbuild_form_view" model="ir.ui.view">
<field name="name">stock.warn.insufficient.qty.unbuild</field>
<field name="model">stock.warn.insufficient.qty.unbuild</field>
<field name="inherit_id" ref="stock.stock_warn_insufficient_qty_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='description']" position="inside">
Do you confirm you want to unbuild <strong><field name="quantity" readonly="True"/></strong><field name="product_uom_name" readonly="True" class="mx-1"/>from location <strong><field name="location_id" readonly="True"/></strong>? This may lead to inconsistencies in your inventory.
</xpath>
</field>
</record>
</odoo>