[IMP] mrp: Mass assignation of SN/Lot in MRP

Allow to produce several serial numbers at once.
Only when BoM do not contain any component tracked by unique serial number
and lot ones are from 1 lot.
Also allow to copy/paste serial numbers.
This applies to manufacturing orders.

Task: 2444742
Part-of: odoo/odoo#71732
This commit is contained in:
Arnold Moyaux
2021-09-02 10:41:46 +00:00
parent 35fb293c05
commit 174c3b7951
15 changed files with 406 additions and 31 deletions
+1
View File
@@ -23,6 +23,7 @@
'wizard/mrp_production_backorder.xml',
'wizard/mrp_consumption_warning_views.xml',
'wizard/mrp_immediate_production_views.xml',
'wizard/stock_assign_serial_numbers.xml',
'views/mrp_views_menus.xml',
'views/stock_move_views.xml',
'views/mrp_workorder_views.xml',
+136
View File
@@ -265,6 +265,7 @@ class MrpProduction(models.Model):
('late', 'Late')], compute='_compute_components_availability')
show_lot_ids = fields.Boolean('Display the serial number shortcut on the moves', compute='_compute_show_lot_ids')
forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue')
show_serial_mass_produce = fields.Boolean('Display the serial mass product wizard action moves', compute='_compute_show_serial_mass_produce')
@api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids')
def _compute_mrp_production_child_count(self):
@@ -521,6 +522,17 @@ class MrpProduction(models.Model):
for order in self:
order.show_lot_ids = order.state != 'draft' and any(m.product_id.tracking == 'serial' for m in order.move_raw_ids)
@api.depends('state', 'move_raw_ids')
def _compute_show_serial_mass_produce(self):
self.show_serial_mass_produce = False
for order in self:
if order.state == 'confirmed' and order.product_id.tracking == 'serial' and \
float_compare(order.product_qty, 1, precision_rounding=order.product_uom_id.rounding) > 0 and \
float_compare(order.qty_producing, order.product_qty, precision_rounding=order.product_uom_id.rounding) < 0:
have_serial_components, dummy, dummy, dummy = order._check_serial_mass_produce_components()
if not have_serial_components:
order.show_serial_mass_produce = True
_sql_constraints = [
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
@@ -1708,6 +1720,34 @@ class MrpProduction(models.Model):
'target': 'new',
}
def action_serial_mass_produce_wizard(self):
self.ensure_one()
self._check_company()
if self.state != 'confirmed':
return
if self.product_id.tracking != 'serial':
return
dummy, dummy, missing_components, multiple_lot_components = self._check_serial_mass_produce_components()
message = ""
if missing_components:
message += _("Make sure enough quantities of these components are reserved to carry on production:\n")
message += "\n".join(component.name for component in missing_components)
if multiple_lot_components:
if message:
message += "\n"
message += _("Component Lots must be unique for mass production. Please review consumption for:\n")
message += "\n".join(component.name for component in multiple_lot_components)
if message:
raise UserError(message)
next_serial = self.env['stock.production.lot'].get_next_serial(self.company_id, self.product_id)
action = self.env["ir.actions.actions"]._for_xml_id("mrp.act_assign_serial_numbers_production")
action['context'] = {
'default_production_id': self.id,
'default_expected_qty': self.product_qty,
'default_next_serial_number': next_serial,
}
return action
@api.model
def _prepare_procurement_group_vals(self, values):
return {'name': values['name']}
@@ -1809,3 +1849,99 @@ class MrpProduction(models.Model):
) and float_is_zero(production.qty_producing, precision_digits=pd):
immediate_productions |= production
return immediate_productions
def _check_serial_mass_produce_components(self):
have_serial_components = False
have_lot_components = False
missing_components = set()
multiple_lot_components = set()
for production in self:
have_serial_components |= any(move.product_id.tracking == 'serial' for move in production.move_raw_ids)
have_lot_components |= any(move.product_id.tracking == 'lot' for move in production.move_raw_ids)
missing_components.update([move.product_id for move in production.move_raw_ids if float_compare(move.reserved_availability, move.product_uom_qty, precision_rounding=move.product_uom.rounding) < 0])
components = {}
for move in production.move_raw_ids:
if move.product_id.tracking != 'lot':
continue
lot_ids = move.mapped('move_line_ids.lot_id.id')
if not lot_ids:
continue
components.setdefault(move.product_id, set()).update(lot_ids)
multiple_lot_components.update([p for p, l in components.items() if len(l) != 1])
return (have_serial_components, have_lot_components, missing_components, multiple_lot_components)
def _generate_backorder_productions_multi(self, serial_numbers, cancel_remaining_quantities=False):
self.ensure_one()
if self.product_id.tracking != 'serial':
raise UserError(_('Expected product tracked by Serial Number.'))
if self.backorder_sequence == 0:
self.backorder_sequence = 1
result = []
production = self
for serial_number in serial_numbers:
production.qty_producing = 1
if production.product_qty > 1 and (serial_number != serial_numbers[-1] or not cancel_remaining_quantities):
backorder = production.copy(default=production._get_backorder_mo_vals())
else:
backorder = None
new_moves_origin = []
new_moves_quantity_to_split = []
new_moves_vals = []
for move in production.move_raw_ids | production.move_finished_ids:
if not move.additional:
uom_qty_to_split = move.product_uom_qty - move.unit_factor * production.qty_producing
qty_to_split = move.product_uom._compute_quantity(uom_qty_to_split, move.product_id.uom_id, rounding_method='HALF-UP')
uom_qty_to_split = float_round(uom_qty_to_split, precision_rounding=move.product_uom.rounding, rounding_method='HALF-UP')
move_vals = move._split(qty_to_split)
if backorder:
if not move_vals:
continue
if move.raw_material_production_id:
move_vals[0]['raw_material_production_id'] = backorder.id
else:
move_vals[0]['production_id'] = backorder.id
new_moves_origin.append(move)
new_moves_vals.append(move_vals[0])
new_moves_quantity_to_split.append(uom_qty_to_split)
elif move.raw_material_production_id:
move.move_line_ids.product_uom_qty = 0
move.move_line_ids[0].product_uom_qty = move.product_uom_qty
if backorder:
new_moves = self.env['stock.move'].create(new_moves_vals)
for move, quantity_to_split, new_move in zip(new_moves_origin, new_moves_quantity_to_split, new_moves):
if move.raw_material_production_id:
move.move_line_ids[0].copy({
'move_id': new_move.id,
'product_uom_qty': quantity_to_split,
'lot_id': move.move_line_ids[0].lot_id.id if move.move_line_ids[0].lot_id else 0,
})
move.with_context(bypass_reservation_update=True).move_line_ids.product_uom_qty = 0
move.with_context(bypass_reservation_update=True).move_line_ids[0].product_uom_qty = move.product_uom_qty
for old_wo, wo in zip(production.workorder_ids, backorder.workorder_ids):
wo.qty_produced = max(old_wo.qty_produced - old_wo.qty_producing, 0)
wo.qty_producing = 1
ratio = production.qty_producing / production.product_qty
for workorder in production.workorder_ids:
workorder.duration_expected = workorder.duration_expected * ratio
for workorder in backorder.workorder_ids:
workorder.duration_expected = workorder.duration_expected * (1 - ratio)
backorder.action_confirm()
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
production.product_qty = 1
production.lot_producing_id = self.env['stock.production.lot'].create({
'product_id': production.product_id.id,
'company_id': production.company_id.id,
'name': serial_number,
})
qty_producing_uom = production.product_uom_id._compute_quantity(production.qty_producing, production.product_id.uom_id, rounding_method='HALF-UP')
if qty_producing_uom != 1:
production.qty_producing = production.product_id.uom_id._compute_quantity(1, production.product_uom_id, rounding_method='HALF-UP')
for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)):
if not move.product_uom:
continue
new_qty = float_round((production.qty_producing - production.qty_produced) * move.unit_factor, precision_rounding=move.product_uom.rounding)
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
result.append((production.lot_producing_id.id, production.id))
production = backorder
return result
+1
View File
@@ -13,3 +13,4 @@ from . import test_oee
from . import test_traceability
from . import test_multicompany
from . import test_backorder
from . import test_smp
+82
View File
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import Form
from odoo.exceptions import UserError
class TestMrpSerialMassProduce(TestMrpCommon):
def test_smp_serial(self):
"""Create a MO for a product not tracked by serial number.
The smp wizard should not open.
"""
mo = self.generate_mo()[0]
self.assertEqual(mo.state, 'confirmed')
res = mo.action_serial_mass_produce_wizard()
self.assertFalse(res)
def test_smp_no_serial_component(self):
"""Create a MO for a product tracked by serial number with a component also tracked by serial number.
An error should be throwed.
"""
mo = self.generate_mo(tracking_final='serial', tracking_base_2='serial')[0]
with self.assertRaises(UserError):
mo.action_serial_mass_produce_wizard()
def test_smp_produce_all(self):
"""Create a MO for a product tracked by serial number.
Open the smp wizard, generate all serial numbers to produce all quantitites.
"""
mo = self.generate_mo(tracking_final='serial')[0]
count = mo.product_qty
# Make some stock and reserve
for product in mo.move_raw_ids.product_id:
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'inventory_quantity': 100,
'location_id': mo.location_src_id.id,
})._apply_inventory()
mo.action_assign()
# Open the wizard
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
# Let the wizard generate all serial numbers
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = count
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to apply generated serial numbers
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().apply()
# Initial MO should have a backorder-sequenced name and be in to_close state
self.assertTrue("-001" in mo.name)
self.assertEqual(mo.state, "to_close")
# Each generated serial number should have its own mo
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), count)
def test_smp_produce_all_but_one(self):
"""Create a MO for a product tracked by serial number.
Open the smp wizard, generate all but one serial numbers and create a back order.
"""
mo = self.generate_mo(tracking_final='serial')[0]
count = mo.product_qty
# Make some stock and reserve
for product in mo.move_raw_ids.product_id:
self.env['stock.quant'].with_context(inventory_mode=True).create({
'product_id': product.id,
'inventory_quantity': 100,
'location_id': mo.location_src_id.id,
})._apply_inventory()
mo.action_assign()
action = mo.action_serial_mass_produce_wizard()
wizard = Form(self.env['stock.assign.serial'].with_context(**action['context']))
wizard.next_serial_number = "sn#1"
wizard.next_serial_count = count - 1
action = wizard.save().generate_serial_numbers_production()
# Reload the wizard to create backorder (applying generated serial numbers)
wizard = Form(self.env['stock.assign.serial'].browse(action['res_id']))
wizard.save().create_backorder()
# Last MO in sequence is the backorder
bo = mo.procurement_group_id.mrp_production_ids[-1]
self.assertEqual(bo.backorder_sequence, count)
self.assertEqual(bo.state, "confirmed")
@@ -101,6 +101,8 @@
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,progress,done"/>
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('show_lock', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to adjust what has been consumed or produced." data-hotkey="l"/>
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('show_lock', '=', False), ('is_locked', '=', True)]}" string="Lock" groups="mrp.group_mrp_manager" type="object" help="Lock the manufacturing order to prevent changes to what has been consumed or produced." data-hotkey="l"/>
<field name="show_serial_mass_produce" invisible="1"/>
<button name="action_serial_mass_produce_wizard" attrs="{'invisible': [('show_serial_mass_produce', '=', False)]}" string="Mass Produce" type="object"/>
<button name="action_cancel" type="object" string="Cancel" data-hotkey="z"
attrs="{'invisible': ['|', '|', ('id', '=', False), ('state', 'in', ('done', 'cancel')), ('confirm_cancel', '=', True)]}"/>
<button name="action_cancel" type="object" string="Cancel" data-hotkey="z"
+1
View File
@@ -6,3 +6,4 @@ 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
@@ -0,0 +1,68 @@
# -*- 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
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(help="Technical field to show the Apply button")
show_backorders = fields.Boolean(help="Technical field to show the Create Backorder and No Backorder buttons")
def generate_serial_numbers_production(self):
if self.next_serial_number and self.next_serial_count:
generated_serial_numbers = "\n".join(self.env['stock.production.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.production.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)
self.show_apply = self.produced_qty == self.expected_qty
self.show_backorders = self.produced_qty > 0 and self.produced_qty < self.expected_qty
def apply(self):
self.production_id._generate_backorder_productions_multi(self._get_serial_numbers())
def create_backorder(self):
self.production_id._generate_backorder_productions_multi(self._get_serial_numbers(), False)
def no_backorder(self):
self.production_id._generate_backorder_productions_multi(self._get_serial_numbers(), True)
@@ -0,0 +1,63 @@
<?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="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 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>
@@ -7,7 +7,6 @@
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='origin']" position="after">
<field name="picking_type_id"/>
<field name="group_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree//field[@name='price_subtotal']" position="after">
@@ -54,11 +54,13 @@
<attribute name="attrs">{'column_invisible': [('parent.state', 'not in', ('purchase', 'done'))], 'readonly': [('product_type', 'in', ('consu', 'product'))]}</attribute>
</xpath>
<xpath expr="//page[@name='purchase_delivery_invoice']/group/group" position="inside">
<field name="picking_type_id" domain="[('code','=','incoming'), '|', ('warehouse_id', '=', False), ('warehouse_id.company_id', '=', company_id)]" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="dest_address_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
<field name="default_location_dest_id_usage" invisible="1"/>
<field name="incoterm_id"/>
</xpath>
<xpath expr="//field[@name='origin']" position="after">
<field name="picking_type_id" domain="[('code','=','incoming'), '|', ('warehouse_id', '=', False), ('warehouse_id.company_id', '=', company_id)]" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="dest_address_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
</xpath>
</field>
</record>
+4 -26
View File
@@ -6,8 +6,6 @@ from collections import defaultdict
from datetime import timedelta
from itertools import groupby
from operator import itemgetter
from re import findall as regex_findall
from re import split as regex_split
from odoo import _, api, fields, models
from odoo.exceptions import UserError
@@ -636,6 +634,9 @@ class StockMove(models.Model):
else:
view = self.env.ref('stock.view_stock_move_nosuggest_operations')
if self.product_id.tracking == "serial" and self.state == "assigned":
self.next_serial = self.env['stock.production.lot'].get_next_serial(self.company_id, self.product_id)
return {
'name': _('Detailed Operations'),
'type': 'ir.actions.act_window',
@@ -743,30 +744,7 @@ class StockMove(models.Model):
`next_serial`) and create a move line for each generated `lot_name`.
"""
self.ensure_one()
if not next_serial_count:
next_serial_count = self.next_serial_count
# We look if the serial number contains at least one digit.
caught_initial_number = regex_findall("\d+", self.next_serial)
if not caught_initial_number:
raise UserError(_('The serial number must contain at least one digit.'))
# We base the serie on the last number find in the base serial number.
initial_number = caught_initial_number[-1]
padding = len(initial_number)
# We split the serial number to get the prefix and suffix.
splitted = regex_split(initial_number, self.next_serial)
# initial_number could appear several times in the SN, e.g. BAV023B00001S00001
prefix = initial_number.join(splitted[:-1])
suffix = splitted[-1]
initial_number = int(initial_number)
lot_names = []
for i in range(0, next_serial_count):
lot_names.append('%s%s%s' % (
prefix,
str(initial_number + i).zfill(padding),
suffix
))
lot_names = self.env['stock.production.lot'].generate_lot_names(self.next_serial, next_serial_count or self.next_serial_count)
move_lines_commands = self._generate_serial_move_line_commands(lot_names)
self.write({'move_line_ids': move_lines_commands})
return True
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from re import findall as regex_findall
from re import split as regex_split
from odoo.tools.misc import attrgetter
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
@@ -31,6 +34,43 @@ class ProductionLot(models.Model):
delivery_count = fields.Integer('Delivery order count', compute='_compute_delivery_ids')
last_delivery_partner_id = fields.Many2one('res.partner', compute='_compute_delivery_ids')
@api.model
def generate_lot_names(self, first_lot, count):
"""Generate `lot_names` from a string."""
# We look if the first lot contains at least one digit.
caught_initial_number = regex_findall(r"\d+", first_lot)
if not caught_initial_number:
raise UserError(_('The lot name must contain at least one digit.'))
# We base the serie on the last number found in the base lot.
initial_number = caught_initial_number[-1]
padding = len(initial_number)
# We split the lot name to get the prefix and suffix.
splitted = regex_split(initial_number, first_lot)
# initial_number could appear several times, e.g. BAV023B00001S00001
prefix = initial_number.join(splitted[:-1])
suffix = splitted[-1]
initial_number = int(initial_number)
lot_names = []
for i in range(0, count):
lot_names.append('%s%s%s' % (
prefix,
str(initial_number + i).zfill(padding),
suffix
))
return lot_names
@api.model
def get_next_serial(self, company, product):
"""Return the next serial number to be attributed to the product."""
if product.tracking == "serial":
last_serial = self.env['stock.production.lot'].search(
[('company_id', '=', company.id), ('product_id', '=', product.id)],
limit=1, order='id DESC')
if last_serial:
return self.env['stock.production.lot'].generate_lot_names(last_serial.name, 2)[1]
return False
@api.constrains('name', 'product_id', 'company_id')
def _check_unique_lot(self):
domain = [('product_id', 'in', self.product_id.ids),
@@ -16,8 +16,8 @@ class StockAssignSerialNumbers(models.TransientModel):
return len(filtered_move_lines)
product_id = fields.Many2one('product.product', 'Product',
related='move_id.product_id', required=True)
move_id = fields.Many2one('stock.move', required=True)
related='move_id.product_id')
move_id = fields.Many2one('stock.move')
next_serial_number = fields.Char('First SN', required=True)
next_serial_count = fields.Integer('Number of SN',
default=_default_next_serial_count, required=True)
@@ -24,6 +24,7 @@
<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"/>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="target">new</field>
@@ -53,6 +53,7 @@ class ResCompany(models.Model):
'default_location_src_id': self.env.ref('stock.stock_location_suppliers').id,
'default_location_dest_id': self.env.ref('stock.stock_location_customers').id,
'sequence_code': 'DS',
'use_existing_lots': False,
})
if dropship_vals:
self.env['stock.picking.type'].create(dropship_vals)