Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addons/mrp/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
'wizard/mrp_consumption_warning_views.xml',
'wizard/mrp_batch_produce.xml',
'wizard/mrp_production_split.xml',
'wizard/mrp_generate_serials_views.xml',
'views/mrp_views_menus.xml',
'views/stock_move_views.xml',
'views/mrp_workorder_views.xml',
Expand Down
37 changes: 37 additions & 0 deletions addons/mrp/models/mrp_production.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,20 @@ def _get_default_is_locked(self):
string='Date Category', store=False,
search='_search_date_category', readonly=True
)
lot_ids = fields.Many2many(
'stock.lot',
'mrp_production_lot_rel',
'production_id',
'lot_id',
string='Pre-generated Serials',
copy=False,
help="Serial/Lot numbers pre-generated for this MO. Used to prefill the Batch Produce wizard.",
)

lot_serial_count = fields.Integer(
string="Generated Serials",
compute="_compute_lot_serial_count",
)

_sql_constraints = [
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
Expand Down Expand Up @@ -339,6 +353,11 @@ def _compute_locations(self):
fallback_loc = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id
production.location_src_id = production.picking_type_id.default_location_src_id.id or fallback_loc.id
production.location_dest_id = production.picking_type_id.default_location_dest_id.id or fallback_loc.id

@api.depends("lot_ids")
def _compute_lot_serial_count(self):
for mo in self:
mo.lot_serial_count = len(mo.lot_ids)

@api.model
def _search_components_availability_state(self, operator, value):
Expand Down Expand Up @@ -1142,6 +1161,24 @@ def action_update_bom(self):
if production.bom_id:
production._link_bom(production.bom_id)
self.is_outdated_bom = False

def action_view_pregenerated_serials(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_production_lot_form")
action["domain"] = [("id", "in", self.lot_ids.ids)]
action["context"] = {}
return action

def action_open_generate_serials_wizard(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Generate Serials"),
"res_model": "mrp.generate.serials",
"view_mode": "form",
"target": "new",
"context": {"active_id": self.id},
}

def _get_bom_values(self, ratio=1):
""" Returns the BoM lines, by-products and operations values needed to
Expand Down
1 change: 1 addition & 0 deletions addons/mrp/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ access_mrp_workcenter_capacity_manager,mrp.workcenter.capacity.manager,model_mrp
access_mrp_workcenter_capacity_group_user,mrp.workcenter.capacity,model_mrp_workcenter_capacity,mrp.group_mrp_user,1,0,0,0
access_mrp_batch_produce,access.mrp_batch_produce,model_mrp_batch_produce,mrp.group_mrp_user,1,1,1,0
access_stock_move_mrp_user,stock.move mrp_user,stock.model_stock_move,mrp.group_mrp_user,1,1,1,1
access_mrp_generate_serials,access.mrp.generate.serials,model_mrp_generate_serials,mrp.group_mrp_user,1,1,1,0
20 changes: 20 additions & 0 deletions addons/mrp/views/mrp_production_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,31 @@
<field name="is_planned" invisible="1"/>
<field name="show_allocation" invisible="1"/>
<field name="workorder_ids" invisible="1"/>
<field name="lot_serial_count" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button name="action_view_reception_report" string="Allocation" type="object"
class="oe_stat_button" icon="fa-list"
invisible="not show_allocation"
groups="mrp.group_mrp_reception_report"/>
<button class="oe_stat_button"
name="action_open_generate_serials_wizard"
type="object"
icon="fa-hashtag"
invisible="product_tracking != 'serial' or not product_qty or state in ('done','cancel')">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Generate Serials</span>
</div>
</button>
<button class="oe_stat_button"
name="action_view_pregenerated_serials"
type="object"
icon="fa-barcode"
invisible="lot_serial_count == 0">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="lot_serial_count"/></span>
<span class="o_stat_text">Serials</span>
</div>
</button>
<button class="oe_stat_button" name="action_view_mrp_production_childs" type="object" icon="fa-wrench" 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>
Expand Down
1 change: 1 addition & 0 deletions addons/mrp/wizard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from . import mrp_batch_produce
from . import mrp_production_split
from . import stock_label_type
from . import mrp_generate_serials
30 changes: 30 additions & 0 deletions addons/mrp/wizard/mrp_batch_produce.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,36 @@ class MrpBatchProduct(models.TransientModel):
_name = 'mrp.batch.produce'
_description = 'Produce a batch of production order'

@api.model
def default_get(self, fields_list):
# fill pre-generated serials
vals = super().default_get(fields_list)
active_id = self.env.context.get("active_id")
if not active_id:
active_ids = self.env.context.get("active_ids") or []
active_id = active_ids[0] if active_ids else False
if not active_id:
return vals
mo = self.env["mrp.production"].browse(active_id)
if not mo.exists():
return vals
if mo.product_id.tracking != "serial":
return vals
if mo.lot_ids:
lots = mo.lot_ids.sorted(key=lambda l: l.name)
lot_names = lots.mapped("name")
# only full order
if len(lot_names) != int(mo.product_qty):
raise UserError(
_("This MO has %(n)s pre-generated serials but quantity is %(q)s. "
"Generate exactly one serial per unit (or adjust the MO quantity).")
% {"n": len(lot_names), "q": mo.product_qty}
)
vals["production_text"] = "\n".join(lot_names)
vals["lot_qty"] = len(lot_names)
vals["lot_name"] = lot_names[0]
return vals

production_id = fields.Many2one('mrp.production', 'Production')

production_text_help = fields.Text('Explanation for batch production', compute='_compute_production_text_help')
Expand Down
49 changes: 49 additions & 0 deletions addons/mrp/wizard/mrp_generate_serials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError


class MrpGenerateSerials(models.TransientModel):
_name = "mrp.generate.serials"
_description = "Pregenerate serial numbers for a Mass MO"

production_id = fields.Many2one("mrp.production", required=True, readonly=True)
first_lot_sn = fields.Char(string="First Serial")
qty = fields.Integer(string="Number of Serials", required=True, default=1)

@api.model
def default_get(self, fields_list):
vals = super().default_get(fields_list)
active_id = self.env.context.get("active_id")
if not active_id:
raise UserError(_("Open this wizard from a Manufacturing Order."))
mo = self.env["mrp.production"].browse(active_id)
vals["production_id"] = mo.id
# guess this if possible
vals["first_lot_sn"] = self.env["stock.lot"]._get_next_serial(mo.company_id, mo.product_id)
vals["qty"] = max(int(mo.product_qty) - len(mo.lot_ids), 1)
return vals

def action_generate(self):
self.ensure_one()
mo = self.production_id
if mo.product_tracking != "serial":
raise UserError(_("The product is not tracked by unique serial number."))
if self.qty <= 0:
raise UserError(_("Please enter a number of serials to generate."))
if not self.first_lot_sn:
raise UserError(_("Please specify the first serial number."))
lots_name = self.env["stock.lot"].generate_lot_names(self.first_lot_sn, self.qty)
names = [x["lot_name"] for x in lots_name]
existing = self.env["stock.lot"].search([
("company_id", "in", [mo.company_id.id, False]),
("product_id", "=", mo.product_id.id),
("name", "in", names),
])
existing_names = set(existing.mapped("name"))
to_create = [{"name": n, "product_id": mo.product_id.id} for n in names if n not in existing_names]
created = self.env["stock.lot"].create(to_create) if to_create else self.env["stock.lot"]
lots = existing | created
lots_by_name = {l.name: l.id for l in lots}
ordered_ids = [lots_by_name[n] for n in names if n in lots_by_name]
mo.lot_ids = [(6, 0, ordered_ids)]
return {"type": "ir.actions.act_window_close"}
20 changes: 20 additions & 0 deletions addons/mrp/wizard/mrp_generate_serials_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_mrp_generate_serials_form" model="ir.ui.view">
<field name="name">mrp.generate.serials.form</field>
<field name="model">mrp.generate.serials</field>
<field name="arch" type="xml">
<form string="Generate Serials">
<group>
<field name="production_id" invisible="1"/>
<field name="first_lot_sn"/>
<field name="qty"/>
</group>
<footer>
<button name="action_generate" type="object" string="Generate" class="btn-primary"/>
<button string="Cancel" special="cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
</odoo>