mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-22 17:12:00 +02:00
Initial commit: Mrp packages
This commit is contained in:
commit
50d736b3bd
739 changed files with 538193 additions and 0 deletions
10
odoo-bringout-oca-ocb-mrp/mrp/wizard/__init__.py
Normal file
10
odoo-bringout-oca-ocb-mrp/mrp/wizard/__init__.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
121
odoo-bringout-oca-ocb-mrp/mrp/wizard/change_production_qty.py
Normal file
121
odoo-bringout-oca-ocb-mrp/mrp/wizard/change_production_qty.py
Normal 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 {}
|
||||
|
|
@ -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>
|
||||
102
odoo-bringout-oca-ocb-mrp/mrp/wizard/mrp_consumption_warning.py
Normal file
102
odoo-bringout-oca-ocb-mrp/mrp/wizard/mrp_consumption_warning.py
Normal 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)
|
||||
|
|
@ -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 & 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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
103
odoo-bringout-oca-ocb-mrp/mrp/wizard/mrp_production_split.py
Normal file
103
odoo-bringout-oca-ocb-mrp/mrp/wizard/mrp_production_split.py
Normal 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')
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue