[IMP](sale_)stock,mrp: smart putaway rules

We introduced new Smart Putaway Rules.
Locations now can have a storage category, on each storage category,
we can specify the amount of products/packages(with certian package
type) that can be stored in the location.
On putaway rules, we can also set a storage category. Now when apply a
putaway rule, we will find a suitable child location of the out
location according to quantity/weight setting on the storage category.

Task 2341820
PR #63516
ENT PR odoo/enterprise#15363
UPG PR odoo/upgrade#2040
This commit is contained in:
yhu-odoo
2021-03-31 09:00:40 +00:00
parent 1cd0a138cf
commit 92e577d106
25 changed files with 1196 additions and 48 deletions
+3 -2
View File
@@ -766,7 +766,8 @@ class MrpWorkorder(models.Model):
move_line.product_uom_qty += self.qty_producing
move_line.qty_done += self.qty_producing
else:
location_dest_id = production_move.location_dest_id._get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id
quantity = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
putaway_location = production_move.location_dest_id._get_putaway_strategy(self.product_id, quantity)
move_line.create({
'move_id': production_move.id,
'product_id': production_move.product_id.id,
@@ -775,7 +776,7 @@ class MrpWorkorder(models.Model):
'product_uom_id': self.product_uom_id.id,
'qty_done': self.qty_producing,
'location_id': production_move.location_id.id,
'location_dest_id': location_dest_id,
'location_dest_id': putaway_location.id,
})
else:
rounding = production_move.product_uom.rounding
@@ -16,3 +16,4 @@ access_account_journal,account.journal,account.model_account_journal,stock.group
access_stock_location_sale_manager,stock.location sale manager,stock.model_stock_location,sales_team.group_sale_manager,1,0,0,0
access_stock_rule_salemanager,stock_rule salemanager,stock.model_stock_rule,sales_team.group_sale_manager,1,1,1,1
access_stock_rule,stock.rule.flow,stock.model_stock_rule,sales_team.group_sale_salesman,1,0,0,0
access_stock_package_type_salesman,stock_package_type salesman,stock.model_stock_package_type,sales_team.group_sale_salesman,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
16 access_stock_location_sale_manager stock.location sale manager stock.model_stock_location sales_team.group_sale_manager 1 0 0 0
17 access_stock_rule_salemanager stock_rule salemanager stock.model_stock_rule sales_team.group_sale_manager 1 1 1 1
18 access_stock_rule stock.rule.flow stock.model_stock_rule sales_team.group_sale_salesman 1 0 0 0
19 access_stock_package_type_salesman stock_package_type salesman stock.model_stock_package_type sales_team.group_sale_salesman 1 0 0 0
+2
View File
@@ -15,6 +15,7 @@
'data/stock_demo.xml',
'data/stock_demo2.xml',
'data/stock_orderpoint_demo.xml',
'data/stock_storage_category_demo.xml',
],
'data': [
'security/stock_security.xml',
@@ -71,6 +72,7 @@
'views/stock_move_line_views.xml',
'views/stock_move_views.xml',
'views/stock_orderpoint_views.xml',
'views/stock_storage_category_views.xml',
'views/res_config_settings_views.xml',
'views/report_stock_traceability.xml',
'views/stock_template.xml',
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="stock_storage_category_high_small" model="stock.storage.category">
<field name="name">High frequency - Small</field>
<field name="max_weight">200</field>
</record>
<record id="stock_storage_category_high_big" model="stock.storage.category">
<field name="name">High frequency - Big</field>
<field name="max_weight">3000</field>
</record>
<record id="stock_storage_category_medium_small" model="stock.storage.category">
<field name="name">Medium frequency - Small</field>
<field name="max_weight">200</field>
</record>
<record id="stock_storage_category_medium_big" model="stock.storage.category">
<field name="name">Medium frequency - Big</field>
<field name="max_weight">3000</field>
</record>
<record id="stock_storage_category_low_small" model="stock.storage.category">
<field name="name">Low frequency - Small</field>
<field name="max_weight">200</field>
</record>
<record id="stock_storage_category_low_big" model="stock.storage.category">
<field name="name">Low frequency - Big</field>
<field name="max_weight">3000</field>
</record>
</data>
</odoo>
+1
View File
@@ -20,3 +20,4 @@ from . import stock_scrap
from . import product
from . import stock_package_level
from . import stock_package_type
from . import stock_storage_category
+33
View File
@@ -96,6 +96,7 @@ class Product(models.Model):
reordering_max_qty = fields.Float(
compute='_compute_nbr_reordering_rules', compute_sudo=False)
putaway_rule_ids = fields.One2many('stock.putaway.rule', 'product_id', 'Putaway Rules')
storage_category_capacity_ids = fields.One2many('stock.storage.category.capacity', 'product_id', 'Storage Category Capacity')
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state')
@api.depends_context(
@@ -486,6 +487,18 @@ class Product(models.Model):
]
return self.env['product.template']._get_action_view_related_putaway_rules(domain)
def action_view_storage_category_capacity(self):
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_storage_category_capacity")
action['context'] = {
'hide_package_type': True,
}
if len(self) == 1:
action['context'].update({
'default_product_id': self.id,
})
action['domain'] = [('product_id', 'in', self.ids)]
return action
def action_open_product_lot(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_production_lot_form")
@@ -811,6 +824,22 @@ class ProductTemplate(models.Model):
raise UserError(_("Available quantity should be set to zero before changing type"))
return super(ProductTemplate, self).write(vals)
def copy(self, default=None):
res = super().copy(default=default)
# Since we don't copy product variants directly, we need to match the newly
# created product variants with the old one, and copy the storage category
# capacity from them.
new_product_dict = {}
for product in res.product_variant_ids:
product_attribute_value = product.product_template_attribute_value_ids.product_attribute_value_id
new_product_dict[product_attribute_value] = product.id
storage_category_capacity_vals = []
for storage_category_capacity in self.product_variant_ids.storage_category_capacity_ids:
product_attribute_value = storage_category_capacity.product_id.product_template_attribute_value_ids.product_attribute_value_id
storage_category_capacity_vals.append(storage_category_capacity.copy_data({'product_id': new_product_dict[product_attribute_value]})[0])
self.env['stock.storage.category.capacity'].create(storage_category_capacity_vals)
return res
# Be aware that the exact same function exists in product.product
def action_open_quants(self):
return self.with_context(active_test=False).product_variant_ids.filtered(lambda p: p.active or p.qty_available != 0).action_open_quants()
@@ -843,6 +872,10 @@ class ProductTemplate(models.Model):
]
return self._get_action_view_related_putaway_rules(domain)
def action_view_storage_category_capacity(self):
self.ensure_one()
return self.product_variant_ids.action_view_storage_category_capacity()
def action_view_orderpoints(self):
return self.product_variant_ids.action_view_orderpoints()
+47 -2
View File
@@ -3,6 +3,7 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare
class RemovalStrategy(models.Model):
@@ -59,15 +60,18 @@ class StockPutawayRule(models.Model):
location_in_id = fields.Many2one(
'stock.location', 'When product arrives in', check_company=True,
domain="[('child_ids', '!=', False), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
default=_default_location_id, required=True, ondelete='cascade')
default=_default_location_id, required=True, ondelete='cascade', index=True)
location_out_id = fields.Many2one(
'stock.location', 'Store to', check_company=True,
domain="[('id', 'child_of', location_in_id), ('id', '!=', location_in_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
domain="[('id', 'child_of', location_in_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
required=True, ondelete='cascade')
sequence = fields.Integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list.")
company_id = fields.Many2one(
'res.company', 'Company', required=True,
default=lambda s: s.env.company.id, index=True)
package_type_ids = fields.Many2many('stock.package.type', string='Package Type', check_company=True)
storage_category_id = fields.Many2one('stock.storage.category', 'Storage Category', ondelete='cascade', check_company=True)
active = fields.Boolean('Active', default=True)
@api.onchange('location_in_id')
def _onchange_location_in(self):
@@ -86,3 +90,44 @@ class StockPutawayRule(models.Model):
if rule.company_id.id != vals['company_id']:
raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one."))
return super(StockPutawayRule, self).write(vals)
def _get_putaway_location(self, product, quantity=0, package=None, qty_by_location=None):
package_type = package and package.package_type_id or None
checked_locations = set()
for putaway_rule in self:
location_out = putaway_rule.location_out_id
if not putaway_rule.storage_category_id:
if location_out in checked_locations:
continue
if location_out._check_can_be_used(product, quantity, package, qty_by_location[location_out.id]):
return location_out
continue
child_locations = self.env['stock.location'].search([('id', 'child_of', location_out.id), ('usage', '=', 'internal')])
# check if already have the product/package type stored
for location in child_locations:
if location in checked_locations:
continue
if package_type:
if location.quant_ids.filtered(lambda q: q.product_id == product and q.package_id and q.package_id.package_type_id == package_type):
if location._check_can_be_used(product, package=package, location_qty=qty_by_location[location.id]):
return location
else:
checked_locations.add(location)
elif float_compare(qty_by_location[location.id], 0, precision_rounding=product.uom_id.rounding) > 0:
if location._check_can_be_used(product, quantity, location_qty=qty_by_location[location.id]):
return location
else:
checked_locations.add(location)
# check locations with matched storage category
for location in child_locations.filtered(lambda l: l.storage_category_id == putaway_rule.storage_category_id):
if location in checked_locations:
continue
if location._check_can_be_used(product, quantity, package, qty_by_location[location.id]):
return location
checked_locations.add(location)
return None
@@ -39,11 +39,13 @@ class ResConfigSettings(models.TransientModel):
module_quality_control_worksheet = fields.Boolean("Quality Worksheet")
group_stock_multi_locations = fields.Boolean('Storage Locations', implied_group='stock.group_stock_multi_locations',
help="Store products in specific locations of your warehouse (e.g. bins, racks) and to track inventory accordingly.")
group_stock_storage_categories = fields.Boolean('Storage Categories', implied_group='stock.group_stock_storage_categories')
@api.onchange('group_stock_multi_locations')
def _onchange_group_stock_multi_locations(self):
if not self.group_stock_multi_locations:
self.group_stock_adv_location = False
self.group_stock_storage_categories = False
@api.onchange('group_stock_production_lot')
def _onchange_group_stock_production_lot(self):
@@ -62,6 +64,20 @@ class ResConfigSettings(models.TransientModel):
if not self.group_stock_multi_locations and location_grp in base_user.implied_ids and warehouse_grp in base_user.implied_ids:
raise UserError(_("You can't desactivate the multi-location if you have more than once warehouse by company"))
# Deactivate putaway rules with storage category when not in storage category
# group. Otherwise, active them.
storage_cate_grp = self.env.ref('stock.group_stock_storage_categories')
PutawayRule = self.env['stock.putaway.rule']
if self.group_stock_storage_categories and storage_cate_grp not in base_user.implied_ids:
putaway_rules = PutawayRule.search([
('active', '=', False),
('storage_category_id', '!=', False)
])
putaway_rules.write({'active': True})
elif not self.group_stock_storage_categories and storage_cate_grp in base_user.implied_ids:
putaway_rules = PutawayRule.search([('storage_category_id', '!=', False)])
putaway_rules.write({'active': False})
res = super(ResConfigSettings, self).set_values()
if not self.user_has_groups('stock.group_stock_manager'):
@@ -89,4 +105,5 @@ class ResConfigSettings(models.TransientModel):
('show_operations', '=', False)
])
picking_types.sudo().write({'show_operations': True})
return res
+112 -19
View File
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict, OrderedDict
from datetime import timedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.osv import expression
from collections import OrderedDict
from odoo.tools.float_utils import float_compare
class Location(models.Model):
@@ -68,10 +68,33 @@ class Location(models.Model):
next_inventory_date = fields.Date("Next Expected Inventory", compute="_compute_next_inventory_date", store=True, help="Date for next planned inventory based on cyclic schedule.")
warehouse_view_ids = fields.One2many('stock.warehouse', 'view_location_id', readonly=True)
warehouse_id = fields.Many2one('stock.warehouse', compute='_compute_warehouse_id')
storage_category_id = fields.Many2one('stock.storage.category', string='Storage Category')
outgoing_move_line_ids = fields.One2many('stock.move.line', 'location_id', help='Technical: used to compute weight.')
incoming_move_line_ids = fields.One2many('stock.move.line', 'location_dest_id', help='Technical: used to compute weight.')
net_weight = fields.Float('Net Weight', compute="_compute_weight")
forecast_weight = fields.Float('Forecasted Weight', compute="_compute_weight")
_sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !'),
('inventory_freq_nonneg', 'check(cyclic_inventory_frequency >= 0)', 'The inventory frequency (days) for a location must be non-negative')]
@api.depends('outgoing_move_line_ids.product_qty', 'incoming_move_line_ids.product_qty',
'outgoing_move_line_ids.state', 'incoming_move_line_ids.state',
'outgoing_move_line_ids.product_id.weight', 'outgoing_move_line_ids.product_id.weight',
'quant_ids.quantity', 'quant_ids.product_id.weight')
def _compute_weight(self):
for location in self:
location.net_weight = 0
quants = location.quant_ids.filtered(lambda q: q.product_id.type != 'service')
incoming_move_lines = location.incoming_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel'])
outgoing_move_lines = location.outgoing_move_line_ids.filtered(lambda ml: ml.product_id.type != 'service' and ml.state not in ['draft', 'done', 'cancel'])
for quant in quants:
location.net_weight += quant.product_id.weight * quant.quantity
location.forecast_weight = location.net_weight
for line in incoming_move_lines:
location.forecast_weight += line.product_id.weight * line.product_qty
for line in outgoing_move_lines:
location.forecast_weight -= line.product_id.weight * line.product_qty
@api.depends('name', 'location_id.complete_name')
def _compute_complete_name(self):
for location in self:
@@ -171,28 +194,98 @@ class Location(models.Model):
domain = ['|', ('barcode', operator, name), ('complete_name', operator, name)]
return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
def _get_putaway_strategy(self, product):
''' Returns the location where the product has to be put, if any compliant putaway strategy is found. Otherwise returns None.'''
putaway_location = self.env['stock.location']
# Looking for a putaway about the product.
putaway_rules = self.putaway_rule_ids.filtered(lambda x: x.product_id == product)
if putaway_rules:
putaway_location = putaway_rules[0].location_out_id
# If not product putaway found, we're looking with category so.
else:
categ = product.categ_id
while categ:
putaway_rules = self.putaway_rule_ids.filtered(lambda x: x.category_id == categ)
if putaway_rules:
putaway_location = putaway_rules[0].location_out_id
break
categ = categ.parent_id
return putaway_location
def _get_putaway_strategy(self, product, quantity=0, package=None):
"""Returns the location where the product has to be put, if any compliant
putaway strategy is found. Otherwise returns self.
The quantity should be in the default UOM of the product, it is used when
no package is specified.
"""
package_type = package and package.package_type_id or self.env['stock.package.type']
putaway_rules = self.env['stock.putaway.rule']
putaway_rules |= self.putaway_rule_ids.filtered(lambda x: x.product_id == product and (package_type in x.package_type_ids or package_type == x.package_type_ids))
categ = product.categ_id
while categ:
putaway_rules |= self.putaway_rule_ids.filtered(lambda x: x.category_id == categ and (package_type in x.package_type_ids or package_type == x.package_type_ids))
categ = categ.parent_id
if package_type:
putaway_rules |= self.putaway_rule_ids.filtered(lambda x: not x.product_id and (package_type in x.package_type_ids or package_type == x.package_type_ids))
# get current product qty (qty in current quants and future qty on assigned ml) of all child locations
qty_by_location = defaultdict(lambda: 0)
locations = self.env['stock.location'].search([('id', 'child_of', self.id), ('usage', '=', 'internal')])
if locations.storage_category_id:
move_line_data = self.env['stock.move.line'].read_group([
('product_id', '=', product.id),
('location_dest_id', 'in', locations.ids),
('state', 'not in', ['draft', 'done', 'cancel'])
], ['location_dest_id', 'product_id', 'product_qty:sum'], ['location_dest_id'])
quant_data = self.env['stock.quant'].read_group([
('product_id', '=', product.id),
('location_id', 'in', locations.ids),
], ['location_id', 'product_id', 'quantity:sum'], ['location_id'])
for values in move_line_data:
qty_by_location[values['location_dest_id'][0]] = values['product_qty']
for values in quant_data:
qty_by_location[values['location_id'][0]] += values['quantity']
return putaway_rules._get_putaway_location(product, quantity, package, qty_by_location) or self
def should_bypass_reservation(self):
self.ensure_one()
return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location or (self.usage == 'transit' and not self.company_id)
def _check_can_be_used(self, product, quantity=0, package=None, location_qty=0):
"""Check if product/package can be stored in the location. Quantity
should in the default uom of product, it's only used when no package is
specified."""
self.ensure_one()
if package and package.package_type_id:
return self._check_package_storage(product, package)
return self._check_product_storage(product, quantity, location_qty)
def _check_product_storage(self, product, quantity, location_qty):
"""Check if a number of product can be stored in the location. Quantity
should in the default uom of product."""
self.ensure_one()
if self.storage_category_id:
# check weight
if self.storage_category_id.max_weight < self.forecast_weight + product.weight * quantity:
return False
# check if only allow new product when empty
if self.storage_category_id.allow_new_product == "empty" and any(float_compare(q.quantity, 0, precision_rounding=q.product_id.uom_id.rounding) > 0 for q in self.quant_ids):
return False
# check if only allow same product
if self.storage_category_id.allow_new_product == "same" and self.quant_ids and self.quant_ids.product_id != product:
return False
# check if enough space
product_capacity = self.storage_category_id.product_capacity_ids.filtered(lambda pc: pc.product_id == product)
if product_capacity and quantity + location_qty > product_capacity.quantity:
return False
return True
def _check_package_storage(self, product, package):
"""Check if the given package can be stored in the location."""
self.ensure_one()
if self.storage_category_id:
# check weight
if self.storage_category_id.max_weight < self.forecast_weight + package.package_type_id.max_weight:
return False
# check if only allow new product when empty
if self.storage_category_id.allow_new_product == "empty" and any(float_compare(q.quantity, 0, precision_rounding=q.product_id.uom_id.rounding) > 0 for q in self.quant_ids):
return False
# check if only allow same product
if self.storage_category_id.allow_new_product == "same" and self.quant_ids and self.quant_ids.product_id != product:
return False
# check if enough space
package_capacity = self.storage_category_id.package_capacity_ids.filtered(lambda pc: pc.package_type_id == package.package_type_id)
if package_capacity:
package_number = len(self.quant_ids.package_id.filtered(lambda q: q.package_type_id == package.package_type_id))
if package_number >= package_capacity.quantity:
return False
return True
class Route(models.Model):
_name = 'stock.location.route'
+3 -3
View File
@@ -1045,10 +1045,10 @@ class StockMove(models.Model):
if origin_move_line:
location_dest = origin_move_line.location_dest_id
else:
location_dest = self.location_dest_id._get_putaway_strategy(self.product_id)
location_dest = self.location_dest_id._get_putaway_strategy(self.product_id, quantity=1)
move_line_vals = {
'picking_id': self.picking_id.id,
'location_dest_id': location_dest.id or self.location_dest_id.id,
'location_dest_id': location_dest.id,
'location_id': self.location_id.id,
'product_id': self.product_id.id,
'product_uom_id': self.product_id.uom_id.id,
@@ -1201,7 +1201,7 @@ class StockMove(models.Model):
def _prepare_move_line_vals(self, quantity=None, reserved_quant=None):
self.ensure_one()
# apply putaway
location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id).id or self.location_dest_id.id
location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id, quantity=quantity or 0).id
vals = {
'move_id': self.id,
'product_id': self.product_id.id,
+17 -6
View File
@@ -112,11 +112,18 @@ class StockMoveLine(models.Model):
if any(ml.qty_done < 0 for ml in self):
raise ValidationError(_('You can not enter negative quantities.'))
@api.onchange('result_package_id')
def _onchange_result_package_id(self):
if self.result_package_id:
if not self.id and self.user_has_groups('stock.group_stock_multi_locations'):
self.location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id, package=self.result_package_id)
@api.onchange('product_id', 'product_uom_id')
def _onchange_product_id(self):
if self.product_id:
if not self.id and self.user_has_groups('stock.group_stock_multi_locations'):
self.location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id) or self.location_dest_id
if not self.id and self.user_has_groups('stock.group_stock_multi_locations') and not self.result_package_id:
qty_done = self.product_uom_id._compute_quantity(self.qty_done, self.product_id.uom_id)
self.location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id, qty_done)
if self.picking_id:
product = self.product_id.with_context(lang=self.picking_id.partner_id.lang or self.env.user.lang)
self.description_picking = product._get_description(self.picking_id.picking_type_id)
@@ -177,11 +184,15 @@ class StockMoveLine(models.Model):
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
"""
res = {}
if self.qty_done and self.product_id.tracking == 'serial':
if self.qty_done:
qty_done = self.product_uom_id._compute_quantity(self.qty_done, self.product_id.uom_id)
if float_compare(qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0:
message = _('You can only process 1.0 %s of products with unique serial number.', self.product_id.uom_id.name)
res['warning'] = {'title': _('Warning'), 'message': message}
if not self.id and self.user_has_groups('stock.group_stock_multi_locations') and not self.result_package_id:
self.location_dest_id = self.location_dest_id._get_putaway_strategy(self.product_id, qty_done)
if self.product_id.tracking == 'serial':
qty_done = self.product_uom_id._compute_quantity(self.qty_done, self.product_id.uom_id)
if float_compare(qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0:
message = _('You can only process 1.0 %s of products with unique serial number.', self.product_id.uom_id.name)
res['warning'] = {'title': _('Warning'), 'message': message}
return res
def init(self):
+8 -2
View File
@@ -24,6 +24,7 @@ class PackageType(models.Model):
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', default=_get_default_weight_uom)
length_uom_name = fields.Char(string='Length unit of measure label', compute='_compute_length_uom_name', default=_get_default_length_uom)
company_id = fields.Many2one('res.company', 'Company', index=True)
storage_category_capacity_ids = fields.One2many('stock.storage.category.capacity', 'package_type_id', 'Storage Category Capacity', copy=True)
_sql_constraints = [
('positive_height', 'CHECK(height>=0)', 'Height must be positive'),
@@ -37,5 +38,10 @@ class PackageType(models.Model):
package_type.length_uom_name = self.env['product.template']._get_length_uom_name_from_ir_config_parameter()
def _compute_weight_uom_name(self):
for packaging in self:
packaging.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
for package_type in self:
package_type.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
def copy(self, default=None):
default = dict(default or {})
default.update(name=_("%s (copy)") % self.name)
return super().copy(default)
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class StorageCategory(models.Model):
_name = 'stock.storage.category'
_description = "Storage Category"
_order = "name"
name = fields.Char('Storage Category', required=True)
max_weight = fields.Float('Max Weight', digits='Stock Weight')
capacity_ids = fields.One2many('stock.storage.category.capacity', 'storage_category_id', copy=True)
product_capacity_ids = fields.One2many('stock.storage.category.capacity', compute="_compute_storage_capacity_ids", inverse="_set_storage_capacity_ids")
package_capacity_ids = fields.One2many('stock.storage.category.capacity', compute="_compute_storage_capacity_ids", inverse="_set_storage_capacity_ids")
allow_new_product = fields.Selection([
('empty', 'If the location is empty'),
('same', 'If all products are same'),
('mixed', 'Allow mixed products')], default='mixed', required=True)
location_ids = fields.One2many('stock.location', 'storage_category_id')
company_id = fields.Many2one('res.company', 'Company')
_sql_constraints = [
('positive_max_weight', 'CHECK(max_weight >= 0)', 'Max weight should be a positive number.'),
]
@api.depends('capacity_ids')
def _compute_storage_capacity_ids(self):
for storage_category in self:
storage_category.product_capacity_ids = storage_category.capacity_ids.filtered(lambda c: c.product_id)
storage_category.package_capacity_ids = storage_category.capacity_ids.filtered(lambda c: c.package_type_id)
def _set_storage_capacity_ids(self):
for storage_category in self:
storage_category.capacity_ids = storage_category.product_capacity_ids | storage_category.package_capacity_ids
def copy(self, default=None):
default = dict(default or {})
default.update(name=_("%s (copy)") % self.name)
return super().copy(default)
class StorageCategoryProductCapacity(models.Model):
_name = 'stock.storage.category.capacity'
_description = "Storage Category Capacity"
_check_company_auto = True
_order = "storage_category_id"
@api.model
def _domain_product_id(self):
domain = "('type', '=', 'product')"
if self.env.context.get('active_model') == 'product.template':
product_template_id = self.env.context.get('active_id', False)
domain = f"('product_tmpl_id', '=', {product_template_id})"
elif self.env.context.get('default_product_id', False):
product_id = self.env.context.get('default_product_id', False)
domain = f"('id', '=', {product_id})"
return f"[{domain}, '|', ('company_id', '=', False), ('company_id', '=', company_id)]"
storage_category_id = fields.Many2one('stock.storage.category', ondelete='cascade', required=True, index=True)
product_id = fields.Many2one('product.product', 'Product', domain=lambda self: self._domain_product_id(), ondelete='cascade', check_company=True)
package_type_id = fields.Many2one('stock.package.type', 'Package Type', ondelete='cascade', check_company=True)
quantity = fields.Float('Quantity', required=True)
product_uom_id = fields.Many2one(related='product_id.uom_id')
company_id = fields.Many2one('res.company', 'Company', related="storage_category_id.company_id")
_sql_constraints = [
('positive_quantity', 'CHECK(quantity > 0)', 'Quantity should be a positive number.'),
('unique_product', 'UNIQUE(product_id, storage_category_id)', 'Multiple capacity rules for one product.'),
('unique_package_type', 'UNIQUE(package_type_id, storage_category_id)', 'Multiple capacity rules for one package type.'),
]
@@ -83,3 +83,7 @@ access_stock_package_destination,access.stock.package.destination,model_stock_pa
access_stock_orderpoint_snooze,access_stock_orderpoint_snooze,model_stock_orderpoint_snooze,stock.group_stock_user,1,1,1,1
access_stock_package_type_user,access_stock_package_type_user,model_stock_package_type,stock.group_stock_user,1,0,0,0
access_stock_package_type_manager,access_stock_package_type_manager,model_stock_package_type,stock.group_stock_manager,1,1,1,1
access_stock_storage_category_user,stock.storage.category.user,model_stock_storage_category,base.group_user,1,0,0,0
access_stock_storage_category_manager,stock.storage.category.manager,model_stock_storage_category,stock.group_stock_manager,1,1,1,1
access_stock_storage_category_capacity_user,stock.storage.category.capacity.user,model_stock_storage_category_capacity,base.group_user,1,0,0,0
access_stock_storage_category_capacity_manager,stock.storage.category.capacity.manager,model_stock_storage_category_capacity,stock.group_stock_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
83 access_stock_orderpoint_snooze access_stock_orderpoint_snooze model_stock_orderpoint_snooze stock.group_stock_user 1 1 1 1
84 access_stock_package_type_user access_stock_package_type_user model_stock_package_type stock.group_stock_user 1 0 0 0
85 access_stock_package_type_manager access_stock_package_type_manager model_stock_package_type stock.group_stock_manager 1 1 1 1
86 access_stock_storage_category_user stock.storage.category.user model_stock_storage_category base.group_user 1 0 0 0
87 access_stock_storage_category_manager stock.storage.category.manager model_stock_storage_category stock.group_stock_manager 1 1 1 1
88 access_stock_storage_category_capacity_user stock.storage.category.capacity.user model_stock_storage_category_capacity base.group_user 1 0 0 0
89 access_stock_storage_category_capacity_manager stock.storage.category.capacity.manager model_stock_storage_category_capacity stock.group_stock_manager 1 1 1 1
+5
View File
@@ -63,6 +63,11 @@
<field name="name">Require a signature on your delivery orders</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="group_stock_storage_categories" model="res.groups">
<field name="name">Manage Storage Categories</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
<data noupdate="1">
<record id="base.default_user" model="res.users">
+667
View File
@@ -10,6 +10,8 @@ class StockMove(TransactionCase):
@classmethod
def setUpClass(cls):
super(StockMove, cls).setUpClass()
group_stock_multi_locations = cls.env.ref('stock.group_stock_multi_locations')
cls.env.user.write({'groups_id': [(4, group_stock_multi_locations.id, 0)]})
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.customer_location = cls.env.ref('stock.stock_location_customers')
cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
@@ -937,6 +939,671 @@ class StockMove(TransactionCase):
# check if the putaway was rightly applied
self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2_location.id)
def test_putaway_with_storage_category_1(self):
"""Receive a product. Test the product will be move to a child location
with correct storage category.
"""
# storage category
storage_category = self.env['stock.storage.category'].create({
'name': "storage category"
})
self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
})
shelf2_location = self.env['stock.location'].create({
'name': 'shelf2',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
# check if the putaway was rightly applied
self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf2_location.id)
def test_putaway_with_storage_category_2(self):
"""Receive a product twice. Test first time the putaway applied since we
have enough space, and second time it is not since the location is full.
"""
storage_category = self.env['stock.storage.category'].create({
'name': "storage category"
})
# set the capacity for the product in this storage category to be 100
storage_category_form = Form(storage_category, view='stock.stock_storage_category_form')
with storage_category_form.product_capacity_ids.new() as line:
line.product_id = self.product
line.quantity = 100
storage_category = storage_category_form.save()
shelf1_location = self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# first move
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
# check if the putaway was rightly applied
self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
# second move
move2 = self.env['stock.move'].create({
'name': 'test_move_2',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move2._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move2.move_line_ids), 1)
# check if the putaway wasn't applied
self.assertEqual(move2.move_line_ids.location_dest_id.id, self.stock_location.id)
def test_putaway_with_storage_category_3(self):
"""Received products twice, set storage category to only accept new
product when empty. Check the first time putaway rule applied and second
time not.
"""
storage_category = self.env['stock.storage.category'].create({
'name': "storage category",
'allow_new_product': "empty",
})
shelf1_location = self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# first move
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
move_line = move1.move_line_ids[0]
move_line.qty_done = 100
move1._action_done()
self.assertEqual(move1.state, 'done')
# check if the putaway was rightly applied
self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
# second move
move2 = self.env['stock.move'].create({
'name': 'test_move_2',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move2._action_confirm()
self.assertEqual(move2.state, 'assigned')
self.assertEqual(len(move2.move_line_ids), 1)
# check if the putaway wasn't applied
self.assertEqual(move2.move_line_ids.location_dest_id.id, self.stock_location.id)
def test_putaway_with_storage_category_4(self):
"""Received products, set storage category to only accept same product.
Check the putaway rule can't be applied when the location has different
products.
"""
storage_category = self.env['stock.storage.category'].create({
'name': "storage category",
'allow_new_product': "same",
})
shelf1_location = self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# create a different product and its quant
product2 = self.env['product.product'].create({
'name': 'Product 2',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
self.env['stock.quant'].create({
'product_id': product2.id,
'product_uom_id': self.uom_unit.id,
'location_id': shelf1_location.id,
'quantity': 1,
'reserved_quantity': 0,
})
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
move_line = move1.move_line_ids[0]
move_line.qty_done = 100
move1._action_done()
self.assertEqual(move1.state, 'done')
# check if the putaway can't be applied
self.assertEqual(move1.move_line_ids.location_dest_id.id, self.stock_location.id)
def test_putaway_with_storage_category_5(self):
"""Receive a package. Test the package will be move to a child location
with correct storage category.
"""
# storage category
storage_category = self.env['stock.storage.category'].create({
'name': "storage category"
})
package_type = self.env['stock.package.type'].create({
'name': "package type",
})
self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
})
shelf2_location = self.env['stock.location'].create({
'name': 'shelf2',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
'package_type_ids': [(4, package_type.id, 0)],
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
package = self.env['stock.quant.package'].create({
'name': 'package',
'package_type_id': package_type.id,
})
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
move_form = Form(move1, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package
line.qty_done = 100
move1 = move_form.save()
move1._action_done()
# check if the putaway was rightly applied
self.assertEqual(package.location_id.id, shelf2_location.id)
def test_putaway_with_storage_category_6(self):
"""Receive package with same package type twice. Check putaway rule can
be applied on the first one but not the second one due to no space.
"""
# storage category
storage_category = self.env['stock.storage.category'].create({
'name': "storage category"
})
package_type = self.env['stock.package.type'].create({
'name': "package type",
})
# set the capacity for the package type in this storage category to be 1
storage_category_form = Form(storage_category, view='stock.stock_storage_category_form')
with storage_category_form.package_capacity_ids.new() as line:
line.package_type_id = package_type
line.quantity = 1
storage_category = storage_category_form.save()
self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
})
shelf2_location = self.env['stock.location'].create({
'name': 'shelf2',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
'package_type_ids': [(4, package_type.id, 0)],
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# first package
package1 = self.env['stock.quant.package'].create({
'name': 'package 1',
'package_type_id': package_type.id,
})
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
move_form = Form(move1, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package1
line.qty_done = 100
move1 = move_form.save()
move1._action_done()
# check if the putaway was rightly applied
self.assertEqual(package1.location_id.id, shelf2_location.id)
# second package
package2 = self.env['stock.quant.package'].create({
'name': 'package 2',
'package_type_id': package_type.id,
})
move2 = self.env['stock.move'].create({
'name': 'test_move_2',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move2._action_confirm()
self.assertEqual(move2.state, 'assigned')
self.assertEqual(len(move2.move_line_ids), 1)
move_form = Form(move2, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package2
line.qty_done = 100
move2 = move_form.save()
move2._action_done()
# check if the putaway wasn't applied
self.assertEqual(package2.location_id.id, self.stock_location.id)
def test_putaway_with_storage_category_7(self):
"""Receive package with same package type twice, set storage category to
only accept new product when empty. Check putaway rule can be applied on
the first one but not the second one.
"""
# storage category
storage_category = self.env['stock.storage.category'].create({
'name': "storage category",
'allow_new_product': "empty",
})
package_type = self.env['stock.package.type'].create({
'name': "package type",
})
# set the capacity for the package type in this storage category to be 100
storage_category_form = Form(storage_category, view='stock.stock_storage_category_form')
with storage_category_form.package_capacity_ids.new() as line:
line.package_type_id = package_type
line.quantity = 100
storage_category = storage_category_form.save()
self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
})
shelf2_location = self.env['stock.location'].create({
'name': 'shelf2',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
'package_type_ids': [(4, package_type.id, 0)],
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# first package
package1 = self.env['stock.quant.package'].create({
'name': 'package 1',
'package_type_id': package_type.id,
})
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
move_form = Form(move1, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package1
line.qty_done = 100
move1 = move_form.save()
move1._action_done()
# check if the putaway was rightly applied
self.assertEqual(package1.location_id.id, shelf2_location.id)
# second package
package2 = self.env['stock.quant.package'].create({
'name': 'package 2',
'package_type_id': package_type.id,
})
move2 = self.env['stock.move'].create({
'name': 'test_move_2',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move2._action_confirm()
self.assertEqual(move2.state, 'assigned')
self.assertEqual(len(move2.move_line_ids), 1)
move_form = Form(move2, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package2
line.qty_done = 100
move2 = move_form.save()
move2._action_done()
# check if the putaway wasn't applied
self.assertEqual(package2.location_id.id, self.stock_location.id)
def test_putaway_with_storage_category_8(self):
"""Receive package withs different products, set storage category to only
accept same product. Check putaway rule can be applied on the first one
but not the second one.
"""
# storage category
storage_category = self.env['stock.storage.category'].create({
'name': "storage category",
'allow_new_product': "same",
})
package_type = self.env['stock.package.type'].create({
'name': "package type",
})
# set the capacity for the package type in this storage category to be 100
storage_category_form = Form(storage_category, view='stock.stock_storage_category_form')
with storage_category_form.package_capacity_ids.new() as line:
line.package_type_id = package_type
line.quantity = 100
storage_category = storage_category_form.save()
self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
})
shelf2_location = self.env['stock.location'].create({
'name': 'shelf2',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location for package type
putaway = self.env['stock.putaway.rule'].create({
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
'package_type_ids': [(4, package_type.id, 0)],
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# first package
package1 = self.env['stock.quant.package'].create({
'name': 'package 1',
'package_type_id': package_type.id,
})
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
move_form = Form(move1, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package1
line.qty_done = 100
move1 = move_form.save()
move1._action_done()
# check if the putaway was rightly applied
self.assertEqual(package1.location_id.id, shelf2_location.id)
# second package
package2 = self.env['stock.quant.package'].create({
'name': 'package 2',
'package_type_id': package_type.id,
})
product2 = self.env['product.product'].create({
'name': 'Product 2',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
move2 = self.env['stock.move'].create({
'name': 'test_move_2',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move2._action_confirm()
self.assertEqual(move2.state, 'assigned')
self.assertEqual(len(move2.move_line_ids), 1)
move_form = Form(move2, view='stock.view_stock_move_nosuggest_operations')
with move_form.move_line_nosuggest_ids.new() as line:
line.result_package_id = package2
line.qty_done = 100
move2 = move_form.save()
move2._action_done()
# check if the putaway wasn't applied
self.assertEqual(package2.location_id.id, self.stock_location.id)
def test_putaway_with_storage_category_9(self):
"""Receive a product twice. Test first time the putaway applied, and second
time it is not since the products violate the max_weight limitaion.
"""
self.product.weight = 1
storage_category = self.env['stock.storage.category'].create({
'name': "storage category",
'max_weight': 100,
})
shelf1_location = self.env['stock.location'].create({
'name': 'shelf1',
'usage': 'internal',
'location_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
# putaway from stock to child location with storage_category
putaway = self.env['stock.putaway.rule'].create({
'product_id': self.product.id,
'location_in_id': self.stock_location.id,
'location_out_id': self.stock_location.id,
'storage_category_id': storage_category.id,
})
self.stock_location.write({
'putaway_rule_ids': [(4, putaway.id, 0)],
})
# first move
move1 = self.env['stock.move'].create({
'name': 'test_move_1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move1._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move1.move_line_ids), 1)
# check if the putaway was rightly applied
self.assertEqual(move1.move_line_ids.location_dest_id.id, shelf1_location.id)
# second move
move2 = self.env['stock.move'].create({
'name': 'test_move_2',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 100.0,
})
move2._action_confirm()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(len(move2.move_line_ids), 1)
# check if the putaway wasn't applied since there are already 100kg products in the location
self.assertEqual(move2.move_line_ids.location_dest_id.id, self.stock_location.id)
def test_availability_1(self):
""" Check that the `availability` field on a move is correctly computed when there is
more than enough products in stock.
+16 -9
View File
@@ -7,20 +7,27 @@
<tree string="Putaway Rules" editable="bottom" sample='1'>
<field name="sequence" widget="handle"
invisible="context.get('invisible_handle', False)"/>
<field name="product_id"
attrs="{'readonly': [('category_id', '!=', False)], 'required': [('category_id', '=', False)]}"
<field name="location_in_id" string="When product arrives in"
options="{'no_create': True}"
readonly="context.get('fixed_location', False)"/>
<field name="product_id" string="Product"
attrs="{'readonly': [('category_id', '!=', False)], 'required': [('category_id', '=', False), ('package_type_ids', '=', False)]}"
options="{'no_create': True, 'no_open': True}"
readonly="context.get('single_product', False)"
force_save="1"/>
<field name="category_id"
attrs="{'readonly': [('product_id', '!=', False)], 'required': [('product_id', '=', False)]}"
<field name="category_id" string="Product Category"
attrs="{'readonly': [('product_id', '!=', False)], 'required': [('product_id', '=', False), ('package_type_ids', '=', False)]}"
options="{'no_create': True, 'no_open': True}"
readonly="context.get('fixed_category', False)"
force_save="1"/>
<field name="location_in_id"
options="{'no_create': True}"
readonly="context.get('fixed_location', False)"/>
<field name="location_out_id" attrs="{'readonly': [('location_in_id', '=', False)]}"
<field name="package_type_ids" string="Package type"
options="{'no_create': True, 'no_open': True}"
groups="stock.group_tracking_lot" widget="many2many_tags"/>
<field name="location_out_id" string="Store to"
attrs="{'readonly': [('location_in_id', '=', False)]}"
options="{'no_create': True}"/>
<field name="storage_category_id" string="Storage Category"
groups="stock.group_stock_storage_categories"
options="{'no_create': True}"/>
<field name="company_id" groups="stock.group_stock_multi_locations" force_save="1" readonly="context.get('fixed_location', False)" options="{'no_create': True}"/>
</tree>
@@ -96,5 +103,5 @@
</record>
<menuitem id="menu_putaway" name="Putaway Rules" parent="stock.menu_warehouse_config"
action="action_putaway_tree" sequence="5" groups="stock.group_stock_multi_locations"/>
action="action_putaway_tree" sequence="8" groups="stock.group_stock_multi_locations"/>
</odoo>
+12
View File
@@ -287,6 +287,12 @@
class="oe_stat_button" icon="fa-random" groups="stock.group_stock_multi_locations"
attrs="{'invisible': [('type', '=', 'service')]}"
context="{'invisible_handle': True, 'single_product': True}"/>
<button type="object" string="Storage Capacities"
name="action_view_storage_category_capacity"
groups="stock.group_stock_storage_categories"
attrs="{'invisible':[('type', '=', 'service')]}"
class="oe_stat_button"
icon="fa-cubes"/>
</div>
<xpath expr="//button[@name='%(action_open_routes)d']" position="attributes">
<attribute name="context">
@@ -388,6 +394,12 @@
'invisible_handle': True,
'single_product': product_variant_count == 1,
}"/>
<button type="object" string="Storage Capacities"
name="action_view_storage_category_capacity"
groups="stock.group_stock_storage_categories"
attrs="{'invisible':[('type', '=', 'service')]}"
class="oe_stat_button"
icon="fa-cubes"/>
</div>
<!-- change attrs of fields added in view_template_property_form
@@ -386,6 +386,24 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
id="categorize_locations"
attrs="{'invisible': [('group_stock_multi_locations', '=', False)]}">
<div class="o_setting_left_pane">
<field name="group_stock_storage_categories"/>
</div>
<div class="o_setting_right_pane">
<label for="group_stock_storage_categories"/>
<div class="text-muted">
Categorize your locations for smarter putaway rules
</div>
<div class="content-group">
<div class="mt8" attrs="{'invisible': [('group_stock_storage_categories', '=', False)]}">
<button name="%(stock.action_storage_category)d" icon="fa-arrow-right" type="action" string="Storage Categories" class="btn-link"/>
</div>
</div>
</div>
</div>
</div>
<h2 id="schedule_info" invisible="1">Advanced Scheduling</h2>
<div class="row mt16 o_settings_container">
+3 -1
View File
@@ -35,6 +35,7 @@
<group string="Additional Information" name="additional_info">
<field name="active" invisible="1"/>
<field name="usage"/>
<field name="storage_category_id" attrs="{'invisible': [('usage', '!=', 'internal')]}" groups="stock.group_stock_storage_categories"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="scrap_location" attrs="{'invisible': [('usage', 'not in', ('inventory', 'internal'))]}"/>
<field name="return_location"/>
@@ -88,10 +89,11 @@
<field name="model">stock.location</field>
<field name="priority" eval="2"/>
<field name="arch" type="xml">
<tree string="Stock Location" decoration-info="usage=='view'" decoration-danger="usage=='internal'" create="false">
<tree string="Stock Location" decoration-info="usage=='view'" decoration-danger="usage=='internal'" create="false" multi_edit="1">
<field name="active" invisible="1"/>
<field name="complete_name" string="Location"/>
<field name="usage"/>
<field name="storage_category_id" attrs="{'readonly': [('usage', '!=', 'internal')]}" groups="stock.group_stock_storage_categories"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
+11 -1
View File
@@ -30,6 +30,16 @@
<field name="barcode"/>
</group>
</group>
<group name="storage_categories" groups="stock.group_stock_storage_categories">
<group>
<field name="storage_category_capacity_ids" context="{'default_package_type_id': id}">
<tree editable="bottom">
<field name="storage_category_id"/>
<field name="quantity"/>
</tree>
</field>
</group>
</group>
</sheet>
</form>
</field>
@@ -46,7 +56,7 @@
<field name="width"/>
<field name="packaging_length"/>
<field name="max_weight"/>
<field name="barcode" invisible="1"/>
<field name="barcode" optional="hide"/>
</tree>
</field>
</record>
+1 -1
View File
@@ -118,7 +118,7 @@
parent="menu_stock_root" sequence="0"
name="Overview"/>
<menuitem id="menu_pickingtype" name="Operations Types" parent="stock.menu_warehouse_config" action="action_picking_type_list" sequence="4"/>
<menuitem id="menu_pickingtype" name="Operations Types" parent="stock.menu_warehouse_config" action="action_picking_type_list" sequence="5"/>
<record model="ir.ui.view" id="stock_picking_calendar">
<field name="name">stock.picking.calendar</field>
+6 -1
View File
@@ -301,7 +301,12 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_record_top mb0">
<strong class="o_kanban_record_title"><field name="name"/></strong>
<div class="col-6">
<strong class="o_kanban_record_title"><field name="name"/></strong>
</div>
<div class="col-6">
<field name="package_type_id"/>
</div>
</div>
</div>
</t>
+1 -1
View File
@@ -135,7 +135,7 @@
</record>
<menuitem action="action_rules_form" id="menu_action_rules_form"
parent="menu_warehouse_config" sequence="2" groups="stock.group_adv_location"/>
parent="menu_warehouse_config" sequence="4" groups="stock.group_adv_location"/>
</data>
</odoo>
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="stock_storage_category_form" model="ir.ui.view">
<field name="name">stock.storage.category.form</field>
<field name="model">stock.storage.category</field>
<field name="arch" type="xml">
<form string="Storage Category">
<sheet>
<group>
<group>
<field name="name"/>
<field name="allow_new_product"/>
</group>
<group>
<label for="max_weight"/>
<div class="o_row">
<field name="max_weight"/>
<span>kg</span>
</div>
<field name="company_id"/>
</group>
</group>
<notebook>
<page string="Product Capacity" name="product_capacity">
<field name="product_capacity_ids" context="{'default_storage_category_id': id}">
<tree editable="bottom">
<field name="product_id" required="1"/>
<field name="quantity"/>
<field name="product_uom_id" options="{'no_create': True, 'no_open': True}"/>
<field name="company_id" invisible="1"/>
</tree>
</field>
</page>
<page string="Package Capacity" name="package_capacity" groups="stock.group_tracking_lot">
<field name="package_capacity_ids" context="{'default_storage_category_id': id}">
<tree editable="bottom">
<field name="package_type_id" required="1"/>
<field name="quantity"/>
<field name="company_id" invisible="1"/>
</tree>
</field>
</page>
<page string="Locations" name="locations">
<field name="location_ids">
<tree create="false" delete="false">
<field name="complete_name" string="Location"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="stock_storage_category_tree" model="ir.ui.view">
<field name="name">stock.storage.category.tree</field>
<field name="model">stock.storage.category</field>
<field name="arch" type="xml">
<tree string="Storage Categories" multi_edit="1">
<field name="name"/>
<field name="max_weight" string="Max Weight (kg)"/>
<field name="allow_new_product"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="action_storage_category" model="ir.actions.act_window">
<field name="name">Storage Categories</field>
<field name="res_model">stock.storage.category</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="stock_storage_category_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new storage category
</p>
</field>
</record>
<record id="stock_storage_category_capacity_tree" model="ir.ui.view">
<field name="name">stock.storage.category.capacity.tree</field>
<field name="model">stock.storage.category.capacity</field>
<field name="arch" type="xml">
<tree string="Storage Category Capacity" multi_edit="1" editable="bottom">
<field name="storage_category_id"/>
<field name="product_id" invisible="context.get('default_product_id')" attrs="{'required': [('package_type_id', '=', False)], 'readonly': [('package_type_id', '!=', False)]}"/>
<field name="package_type_id" attrs="{'required': [('product_id', '=', False)], 'readonly': [('product_id', '!=', False)]}" invisible="context.get('hide_package_type', False)" groups="stock.group_tracking_lot"/>
<field name="quantity"/>
<field name="product_uom_id" options="{'no_create': True, 'no_open': True}"/>
<field name="company_id" invisible="1"/>
</tree>
</field>
</record>
<record id="action_storage_category_capacity" model="ir.actions.act_window">
<field name="name">Storage Category Capacity</field>
<field name="res_model">stock.storage.category.capacity</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">tree</field>
</record>
<menuitem id="menu_storage_categoty_config" action="action_storage_category" name="Storage Categories" parent="menu_warehouse_config" sequence="6" groups="stock.group_stock_storage_categories"/>
<menuitem id="menu_storage_categoty_capacity_config" action="action_storage_category_capacity" name="Storage Category Capacity" parent="menu_warehouse_config" sequence="7" groups="base.group_no_one"/>
</odoo>