[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:
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user