# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import re from collections import defaultdict, deque from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.tools import OrderedSet class MrpBatchProduct(models.TransientModel): _name = 'mrp.batch.produce' _description = 'Produce a batch of production order' production_id = fields.Many2one('mrp.production', 'Production') production_text_help = fields.Text('Explanation for batch production', compute='_compute_production_text_help') production_text = fields.Text('Batch Production') lot_name = fields.Char('First Lot/SN', compute="_compute_lot_name", store=True, readonly=False) lot_qty = fields.Integer('Number of SN', compute="_compute_lot_qty", store=True, readonly=False) component_separator = fields.Char('Component separator', default=',', required=True) lots_separator = fields.Char('Lot separator', default='|', required=True) lots_quantity_separator = fields.Char('Lot quantity separator', default=';', required=True) @api.depends('production_id') def _compute_lot_name(self): for wizard in self: if wizard.lot_name: continue wizard.lot_name = self.production_id.lot_producing_id.name if not wizard.lot_name: wizard.lot_name = self.env['stock.lot']._get_next_serial(self.production_id.company_id, self.production_id.product_id) @api.depends('production_id') def _compute_lot_qty(self): for wizard in self: wizard.lot_qty = wizard.production_id.product_qty @api.depends('production_id', 'component_separator') def _compute_production_text_help(self): basic_text = _("Write one line per finished product to produce, with serial numbers as follows:\n") for wizard in self: finished_product = wizard.production_id.product_id.display_name text = basic_text + finished_product for move_raw in wizard.production_id.move_raw_ids: if move_raw.product_id.tracking == "none": continue text += wizard.component_separator + move_raw.product_id.display_name wizard.production_text_help = text def action_prepare(self): return self._production_text_to_object(mark_done=False) def action_done(self): return self._production_text_to_object(mark_done=True) def action_generate_production_text(self): self.ensure_one() if not self.lot_name: raise UserError(_('Please specify the first serial number you would like to use.')) lots_name = self.env['stock.lot'].generate_lot_names(self.lot_name, self.lot_qty) self.production_text = '\n'.join([lot['lot_name'] for lot in lots_name]) action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_mrp_batch_produce") action['res_id'] = self.id return action def _production_text_to_object(self, mark_done=False): self.ensure_one() if not self.production_text: raise UserError(_('Please specify the serial number you would like to use.')) productions_amount = [] productions_lot_list = [] components_list = [] for production_line in deque(self.production_text.split("\n")): production_line = production_line.strip() if not production_line: continue components_line = deque(production_line.split(self.component_separator)) finished_line = components_line.popleft() finished_move = self.production_id.move_finished_ids.filtered(lambda m: m.product_id == self.production_id.product_id) finished_lot, finished_qty = self._get_lot_and_qty(finished_move, finished_line) productions_amount.append(finished_qty) productions_lot_list.append(finished_lot) components_list.append(components_line) productions = self.production_id._split_productions({self.production_id: productions_amount}) lots = self.env['stock.lot'].search( domain=[ ('company_id', 'in', [self.production_id.product_id.company_id.id, False]), ('name', 'in', productions_lot_list), ('product_id', '=', self.production_id.product_id.id) ] ) existing_lot_names = lots.mapped('name') raw_lots = [] for lot_name in productions_lot_list: if lot_name in existing_lot_names: continue raw_lots.append({ 'name': lot_name, 'product_id': productions.product_id.id }) lots = lots + self.env['stock.lot'].create(raw_lots) productions_to_set = OrderedSet() for production, finished_lot in zip(productions, lots): production.lot_producing_id = finished_lot self._process_components(production, components_list.pop(0)) productions_to_set.add(production.id) productions = self.env['mrp.production'].browse(productions_to_set) for production in reversed(productions): production.qty_producing = production.product_uom_qty production.set_qty_producing() production.move_raw_ids.picked = True if mark_done: return productions.with_context(from_wizard=True).button_mark_done() return def _process_components(self, production, components_line): lot_names = [] mls_to_unlink = set() moves_vals = defaultdict(list) for move_raw in production.move_raw_ids: if move_raw.product_id.tracking == "none" or not components_line: continue component_line = components_line.popleft().strip() mls_lines = component_line.split(self.lots_separator) for ml_line in mls_lines: lot_name, qty = self._get_lot_and_qty(move_raw, ml_line) moves_vals[move_raw].append((qty, lot_name)) lot_names.append(lot_name) lots = {(l.name, l.product_id): l for l in self.env['stock.lot'].search([('name', 'in', lot_names)])} mls_vals = [] for move, mls in moves_vals.items(): if mls: mls_to_unlink |= set(move.move_line_ids.ids) for qty, lot_name in mls: ml_vals = self._prepapre_move_line_vals(move, qty, lot_name, lots) mls_vals.append(ml_vals) self.env['stock.move.line'].browse(mls_to_unlink).unlink() self.env['stock.move.line'].create(mls_vals) def _prepapre_move_line_vals(self, move, qty, lot_name, lots): ml_vals = move._prepare_move_line_vals(qty) lot = lots.get((lot_name, move.product_id)) if not lot: if not move.picking_type_id.use_create_components_lots: raise UserError(_('Lot %s does not exist.', lot_name)) lot = self.env['stock.lot'].create({ 'name': lot_name, 'product_id': move.product_id.id }) lots[(lot_name, move.product_id)] = lot ml_vals['lot_id'] = lots[(lot_name, move.product_id)].id ml_vals['picked'] = True return ml_vals def _get_lot_and_qty(self, move, text): if self.lots_quantity_separator in text: lot_name, qty = re.match(r'(.+)%s(.+)' % (self.lots_quantity_separator), text).groups() return lot_name, float(qty) elif move.product_id.tracking == "none": return False, float(text) elif move.product_id.tracking == "serial": return text, 1 else: return text, move.product_uom_qty