Files
tryton/modules/stock/location.py
T

775 lines
28 KiB
Python
Raw Normal View History

2014-12-02 12:56:04 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2010-08-19 22:22:44 +02:00
import datetime
2018-03-09 19:17:58 +01:00
import operator
from decimal import Decimal
from sql import Column
from trytond.cache import Cache
2021-12-11 17:58:44 +01:00
from trytond.i18n import gettext
from trytond.model import (
DeactivableMixin, Index, MatchMixin, Model, ModelSQL, ModelView, fields,
2022-09-10 18:34:29 +02:00
sequence_ordered, tree)
2021-12-11 17:58:44 +01:00
from trytond.modules.product import price_digits, round_price
2022-09-10 18:34:29 +02:00
from trytond.pool import Pool
2022-11-24 19:16:51 +01:00
from trytond.pyson import Eval, If, TimeDelta
from trytond.tools import grouped_slice
from trytond.transaction import Transaction, inactive_records
2020-12-05 11:51:29 +01:00
2018-12-29 14:20:29 +01:00
from .exceptions import LocationValidationError
2020-10-11 23:46:10 +02:00
class WarehouseWasteLocation(ModelSQL):
"Warehouse Waste Location"
__name__ = 'stock.location.waste'
warehouse = fields.Many2One(
'stock.location', "Warehouse", required=True, ondelete='CASCADE',
domain=[('type', '=', 'warehouse')])
location = fields.Many2One(
'stock.location', "Waste Location", required=True, ondelete='CASCADE',
domain=[('type', '=', 'lost_found')])
2018-05-12 00:51:33 +02:00
class Location(DeactivableMixin, tree(), ModelSQL, ModelView):
2008-04-08 14:27:53 +02:00
"Stock Location"
2012-09-24 22:18:17 +02:00
__name__ = 'stock.location'
_default_warehouse_cache = Cache('stock.location.default_warehouse',
context=False)
name = fields.Char("Name", size=None, required=True, translate=True)
code = fields.Char(
"Code",
2018-12-04 12:25:45 +01:00
help="The internal identifier used for the location.")
address = fields.Many2One(
'party.address', "Address",
2011-08-04 16:57:42 +02:00
states={
2015-06-28 23:45:31 +02:00
'invisible': Eval('type') != 'warehouse',
})
type = fields.Selection([
('supplier', 'Supplier'),
('customer', 'Customer'),
2008-05-14 11:45:08 +02:00
('lost_found', 'Lost and Found'),
('warehouse', 'Warehouse'),
('storage', 'Storage'),
('production', 'Production'),
2015-10-04 17:54:19 +02:00
('drop', 'Drop'),
('view', 'View'),
], "Type")
2018-12-29 14:20:29 +01:00
type_string = type.translated('type')
parent = fields.Many2One(
2023-04-22 18:43:45 +02:00
"stock.location", "Parent", ondelete='CASCADE',
2014-12-05 18:43:36 +01:00
left="left", right="right",
2018-12-04 12:25:45 +01:00
help="Used to add structure above the location.")
left = fields.Integer('Left', required=True)
right = fields.Integer('Right', required=True)
2018-12-04 12:25:45 +01:00
childs = fields.One2Many("stock.location", "parent", "Children",
help="Used to add structure below the location.")
2017-09-28 23:42:22 +02:00
flat_childs = fields.Boolean(
"Flat Children",
2018-12-04 12:25:45 +01:00
help="Check to enforce a single level of children with no "
"grandchildren.")
2015-11-05 23:26:24 +01:00
warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'),
'get_warehouse')
2008-04-24 00:49:17 +02:00
input_location = fields.Many2One(
2008-07-23 20:33:17 +02:00
"stock.location", "Input", states={
2015-06-28 23:45:31 +02:00
'invisible': Eval('type') != 'warehouse',
'required': Eval('type') == 'warehouse',
2011-08-04 16:57:42 +02:00
},
2010-01-20 11:04:28 +01:00
domain=[
['OR',
('type', '=', 'storage'),
('id', '=', Eval('storage_location', -1)),
],
2009-06-23 18:49:55 +02:00
['OR',
2021-10-03 13:45:32 +02:00
('parent', 'child_of', [Eval('id', -1)]),
2012-03-27 10:44:04 +02:00
('parent', '=', None),
2011-08-04 16:57:42 +02:00
],
2010-01-20 11:04:28 +01:00
],
2018-12-04 12:25:45 +01:00
help="Where incoming stock is received.")
2008-04-24 00:49:17 +02:00
output_location = fields.Many2One(
2008-07-23 20:33:17 +02:00
"stock.location", "Output", states={
2015-06-28 23:45:31 +02:00
'invisible': Eval('type') != 'warehouse',
'required': Eval('type') == 'warehouse',
2008-07-23 20:33:17 +02:00
},
2012-04-03 20:07:43 +02:00
domain=[
['OR',
('type', '=', 'storage'),
('id', '=', Eval('storage_location', -1)),
],
2009-06-23 18:49:55 +02:00
['OR',
2021-10-03 13:45:32 +02:00
('parent', 'child_of', [Eval('id', -1)]),
2012-03-27 10:44:04 +02:00
('parent', '=', None)]],
2018-12-04 12:25:45 +01:00
help="Where outgoing stock is sent from.")
2008-04-24 00:49:17 +02:00
storage_location = fields.Many2One(
2008-07-23 20:33:17 +02:00
"stock.location", "Storage", states={
2015-06-28 23:45:31 +02:00
'invisible': Eval('type') != 'warehouse',
'required': Eval('type') == 'warehouse',
2008-07-23 20:33:17 +02:00
},
2012-04-03 20:07:43 +02:00
domain=[
('type', 'in', ['storage', 'view']),
2009-06-23 18:49:55 +02:00
['OR',
2021-10-03 13:45:32 +02:00
('parent', 'child_of', [Eval('id', -1)]),
2012-03-27 10:44:04 +02:00
('parent', '=', None)]],
2018-12-04 12:25:45 +01:00
help="The top level location where stock is stored.")
2015-09-14 10:17:04 +02:00
picking_location = fields.Many2One(
'stock.location', 'Picking', states={
'invisible': Eval('type') != 'warehouse',
},
domain=[
('type', '=', 'storage'),
('parent', 'child_of', [Eval('storage_location', -1)]),
],
2018-12-04 12:25:45 +01:00
help="Where stock is picked from.\n"
"Leave empty to use the storage location.")
lost_found_location = fields.Many2One(
'stock.location', "Lost and Found",
states={
'invisible': Eval('type') != 'warehouse',
},
domain=[
('type', '=', 'lost_found'),
],
help="Used, by inventories, when correcting stock levels "
"in the warehouse.")
2020-10-11 23:46:10 +02:00
waste_locations = fields.Many2Many(
'stock.location.waste', 'warehouse', 'location', "Waste Locations",
states={
'invisible': Eval('type') != 'warehouse',
},
domain=[
('type', '=', 'lost_found'),
],
help="The locations used for waste products from the warehouse.")
waste_warehouses = fields.Many2Many(
'stock.location.waste', 'location', 'warehouse', "Waste Warehouses",
states={
'invisible': Eval('type') != 'lost_found',
},
domain=[
('type', '=', 'warehouse'),
],
help="The warehouses that use the location for waste products.")
2021-01-17 00:41:23 +01:00
2022-09-10 18:34:29 +02:00
allow_pickup = fields.Boolean(
"Allow Pickup",
states={
'invisible': (
(Eval('type') != 'warehouse')
& ~Eval('address')),
})
2018-03-09 19:17:58 +01:00
quantity = fields.Function(
2021-01-17 00:41:23 +01:00
fields.Float(
"Quantity", digits='quantity_uom',
2021-01-17 00:41:23 +01:00
help="The amount of stock in the location."),
2018-12-04 12:25:45 +01:00
'get_quantity', searcher='search_quantity')
2018-03-09 19:17:58 +01:00
forecast_quantity = fields.Function(
2021-01-17 00:41:23 +01:00
fields.Float(
"Forecast Quantity", digits='quantity_uom',
2021-01-17 00:41:23 +01:00
help="The amount of stock expected to be in the location."),
2018-12-04 12:25:45 +01:00
'get_quantity', searcher='search_quantity')
2021-01-17 00:41:23 +01:00
quantity_uom = fields.Function(fields.Many2One(
2023-08-20 12:17:27 +02:00
'product.uom', "Quantity UoM",
help="The Unit of Measure for the quantities."),
2021-01-17 00:41:23 +01:00
'get_quantity_uom')
2020-12-05 11:51:29 +01:00
cost_value = fields.Function(fields.Numeric(
"Cost Value", digits=price_digits,
help="The value of the stock in the location."),
'get_cost_value')
2008-04-08 14:27:53 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
2012-09-24 22:18:17 +02:00
super(Location, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.code, Index.Similarity())),
Index(
t,
(t.left, Index.Range()),
(t.right, Index.Range())),
})
2012-09-24 22:18:17 +02:00
cls._order.insert(0, ('name', 'ASC'))
2008-04-22 17:49:50 +02:00
2017-09-28 23:42:22 +02:00
parent_domain = [
['OR',
('parent.flat_childs', '=', False),
('parent', '=', None),
]
]
childs_domain = [
If(Eval('flat_childs', False),
('childs', '=', None),
()),
]
2014-12-05 18:43:36 +01:00
childs_mapping = cls._childs_domain()
2018-06-26 19:11:55 +02:00
for type_, allowed_parents in cls._parent_domain().items():
2014-12-05 18:43:36 +01:00
parent_domain.append(If(Eval('type') == type_,
('type', 'in', allowed_parents), ()))
childs_domain.append(If(Eval('type') == type_,
('type', 'in', childs_mapping[type_]), ()))
cls.parent.domain = parent_domain
cls.childs.domain = childs_domain
@classmethod
def _parent_domain(cls):
'''Returns a dict with location types as keys and a list of allowed
parent location types as values'''
return {
'customer': ['customer'],
'supplier': ['supplier'],
'production': ['production'],
'lost_found': ['lost_found'],
'view': ['warehouse', 'view', 'storage'],
'storage': ['warehouse', 'view', 'storage'],
2021-06-09 18:03:23 +02:00
'warehouse': ['view'],
2014-12-05 18:43:36 +01:00
}
@classmethod
def _childs_domain(cls):
childs_domain = {}
2018-06-26 19:11:55 +02:00
for type_, allowed_parents in cls._parent_domain().items():
2014-12-05 18:43:36 +01:00
for parent in allowed_parents:
childs_domain.setdefault(parent, [])
childs_domain[parent].append(type_)
return childs_domain
@classmethod
2022-04-11 23:24:21 +02:00
def validate_fields(cls, locations, field_names):
super().validate_fields(locations, field_names)
2017-07-12 19:55:02 +02:00
inactives = []
for location in locations:
2022-04-11 23:24:21 +02:00
location.check_type_for_moves(field_names)
if 'active' in field_names and not location.active:
2017-07-12 19:55:02 +02:00
inactives.append(location)
cls.check_inactive(inactives)
2022-04-11 23:24:21 +02:00
def check_type_for_moves(self, field_names=None):
2009-02-03 21:11:13 -08:00
""" Check locations with moves have types compatible with moves. """
2022-04-11 23:24:21 +02:00
pool = Pool()
Move = pool.get('stock.move')
if field_names and 'type' not in field_names:
return
invalid_move_types = ['warehouse', 'view']
if self.type in invalid_move_types:
# Use root to compute for all companies
with Transaction().set_user(0):
moves = Move.search([
['OR',
('to_location', '=', self.id),
('from_location', '=', self.id),
],
('state', 'not in', ['staging', 'draft']),
2022-04-11 23:24:21 +02:00
],
order=[], limit=1)
if moves:
2018-12-29 14:20:29 +01:00
raise LocationValidationError(
gettext('stock.msg_location_invalid_type_for_moves',
location=self.rec_name,
type=self.type_string))
2009-02-03 21:11:13 -08:00
2017-07-12 19:55:02 +02:00
@classmethod
def check_inactive(cls, locations):
"Check inactive location are empty"
assert all(not l.active for l in locations)
2017-07-12 19:55:02 +02:00
empty = cls.get_empty_locations(locations)
non_empty = set(locations) - set(empty)
if non_empty:
2018-12-29 14:20:29 +01:00
raise LocationValidationError(
gettext('stock.msg_location_inactive_not_empty',
location=next(iter(non_empty)).rec_name))
2017-07-12 19:55:02 +02:00
@classmethod
def get_empty_locations(cls, locations=None):
pool = Pool()
Move = pool.get('stock.move')
Product = pool.get('product.product')
2017-07-12 19:55:02 +02:00
if locations is None:
locations = cls.search([])
if not locations:
return []
2018-06-26 19:11:55 +02:00
location_ids = list(map(int, locations))
2017-07-12 19:55:02 +02:00
# Use root to compute for all companies
# and ensures inactive locations are in the query
with Transaction().set_user(0), inactive_records():
2017-07-12 19:55:02 +02:00
query = Move.compute_quantities_query(
location_ids, with_childs=True)
quantities = Move.compute_quantities(
query, location_ids, with_childs=True)
empty = set(location_ids)
product_ids = [q[1] for q in quantities.keys()]
consumables = {
p.id for p in Product.browse(product_ids) if p.consumable}
2018-06-26 19:11:55 +02:00
for (location_id, product), quantity in quantities.items():
if quantity and product not in consumables:
2017-07-12 19:55:02 +02:00
empty.discard(location_id)
for sub_ids in grouped_slice(list(empty)):
sub_ids = list(sub_ids)
moves = Move.search([
2020-06-06 10:30:11 +01:00
('state', 'not in', ['done', 'cancelled']),
2017-07-12 19:55:02 +02:00
['OR',
('from_location', 'in', sub_ids),
('to_location', 'in', sub_ids),
],
])
for move in moves:
for location in [move.from_location, move.to_location]:
empty.discard(location.id)
return cls.browse(empty)
2012-09-24 22:18:17 +02:00
@staticmethod
def default_left():
return 0
2012-09-24 22:18:17 +02:00
@staticmethod
def default_right():
return 0
2017-09-28 23:42:22 +02:00
@classmethod
def default_flat_childs(cls):
return False
2012-09-24 22:18:17 +02:00
@staticmethod
def default_type():
return 'storage'
2008-04-24 00:49:17 +02:00
2012-09-24 22:18:17 +02:00
@classmethod
2018-09-19 00:15:59 +02:00
def check_xml_record(cls, records, values):
pass
2008-07-23 20:37:32 +02:00
2015-11-05 23:26:24 +01:00
def get_warehouse(self, name):
# Order by descending left to get the first one in the tree
with inactive_records():
locations = self.search([
('parent', 'parent_of', [self.id]),
('type', '=', 'warehouse'),
], order=[('left', 'DESC')])
2015-11-05 23:26:24 +01:00
if locations:
return locations[0].id
@classmethod
def get_default_warehouse(cls):
warehouse = Transaction().context.get('warehouse')
if warehouse:
return warehouse
warehouse = cls._default_warehouse_cache.get(None, -1)
if warehouse == -1:
warehouses = cls.search([
('type', '=', 'warehouse'),
], limit=2)
if len(warehouses) == 1:
warehouse = warehouses[0].id
else:
warehouse = None
cls._default_warehouse_cache.set(None, warehouse)
return warehouse
@property
def lost_found_used(self):
if self.warehouse:
return self.warehouse.lost_found_location
2012-09-24 22:18:17 +02:00
@classmethod
def search_rec_name(cls, name, clause):
if clause[1].startswith('!') or clause[1].startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
return [bool_op,
(cls._rec_name,) + tuple(clause[1:]),
('code',) + tuple(clause[1:]),
]
2008-04-08 14:27:53 +02:00
2020-10-11 16:05:16 +02:00
@classmethod
def _get_quantity_grouping(cls):
context = Transaction().context
grouping, grouping_filter, key = (), (), []
if context.get('product') is not None:
grouping = ('product',)
grouping_filter = ([context['product']],)
key = (context['product'],)
elif context.get('product_template') is not None:
grouping = ('product.template',)
grouping_filter = ([context['product_template']],)
key = (context['product_template'],)
return grouping, grouping_filter, key
2012-09-24 22:18:17 +02:00
@classmethod
def get_quantity(cls, locations, name):
pool = Pool()
Product = pool.get('product.product')
Date_ = pool.get('ir.date')
trans_context = Transaction().context
def valid_context(name):
return (trans_context.get(name) is not None
2018-06-26 19:11:55 +02:00
and isinstance(trans_context[name], int))
2010-08-19 22:22:44 +02:00
context = {}
if (name == 'quantity'
and ((trans_context.get('stock_date_end') or datetime.date.max)
> Date_.today())):
2012-09-24 22:18:17 +02:00
context['stock_date_end'] = Date_.today()
if name == 'forecast_quantity':
context['forecast'] = True
if not trans_context.get('stock_date_end'):
context['stock_date_end'] = datetime.date.max
2020-10-11 16:05:16 +02:00
grouping, grouping_filter, key = cls._get_quantity_grouping()
if not grouping:
return {loc.id: None for loc in locations}
pbl = {}
for sub_locations in grouped_slice(locations):
location_ids = [l.id for l in sub_locations]
with Transaction().set_context(context):
pbl.update(Product.products_by_location(
location_ids,
grouping=grouping,
grouping_filter=grouping_filter,
2018-03-09 19:17:58 +01:00
with_childs=trans_context.get('with_childs', True)))
2020-10-11 16:05:16 +02:00
return dict((loc.id, pbl.get((loc.id,) + key, 0)) for loc in locations)
2018-03-09 19:17:58 +01:00
@classmethod
def search_quantity(cls, name, domain):
_, operator_, operand = domain
operator_ = {
'=': operator.eq,
'>=': operator.ge,
'>': operator.gt,
'<=': operator.le,
'<': operator.lt,
'!=': operator.ne,
'in': lambda v, l: v in l,
'not in': lambda v, l: v not in l,
}.get(operator_, lambda v, l: False)
ids = []
for location in cls.search([]):
if operator_(getattr(location, name), operand):
ids.append(location.id)
return [('id', 'in', ids)]
2021-01-17 00:41:23 +01:00
@classmethod
def get_quantity_uom(cls, locations, name):
pool = Pool()
Product = pool.get('product.product')
Template = pool.get('product.template')
context = Transaction().context
value = None
uom = None
if context.get('product') is not None:
product = Product(context['product'])
uom = product.default_uom
elif context.get('product_template') is not None:
template = Template(context['product_template'])
uom = template.default_uom
if uom:
value = uom.id
2021-01-17 00:41:23 +01:00
return {l.id: value for l in locations}
2012-09-24 22:18:17 +02:00
@classmethod
def get_cost_value(cls, locations, name):
pool = Pool()
Product = pool.get('product.product')
Template = pool.get('product.template')
trans_context = Transaction().context
cost_values = {l.id: None for l in locations}
def valid_context(name):
return (trans_context.get(name) is not None
2018-06-26 19:11:55 +02:00
and isinstance(trans_context[name], int))
if not any(map(valid_context, ['product', 'product_template'])):
return cost_values
def get_record():
if trans_context.get('product') is not None:
return Product(trans_context['product'])
else:
return Template(trans_context['product_template'])
context = {}
if trans_context.get('stock_date_end') is not None:
# Use the last cost_price of the day
context['_datetime'] = datetime.datetime.combine(
trans_context['stock_date_end'], datetime.time.max)
# The date could be before the product creation
record = get_record()
if record.create_date > context['_datetime']:
return cost_values
with Transaction().set_context(context):
cost_price = get_record().cost_price
# The template may have more than one product
if cost_price is not None:
2012-09-24 22:18:17 +02:00
for location in locations:
2020-12-05 11:51:29 +01:00
cost_values[location.id] = round_price(
Decimal(str(location.quantity)) * cost_price)
return cost_values
2012-09-24 22:18:17 +02:00
@classmethod
def _set_warehouse_parent(cls, locations):
2008-12-12 17:09:08 +01:00
'''
Set the parent of child location of warehouse if not set
'''
2012-09-24 22:18:17 +02:00
to_update = set()
2016-06-23 09:40:00 +02:00
to_save = []
2008-12-12 17:09:08 +01:00
for location in locations:
if location.type == 'warehouse':
if not location.input_location.parent:
2012-09-24 22:18:17 +02:00
to_update.add(location.input_location)
2008-12-12 17:09:08 +01:00
if not location.output_location.parent:
2012-09-24 22:18:17 +02:00
to_update.add(location.output_location)
2008-12-12 17:09:08 +01:00
if not location.storage_location.parent:
2012-09-24 22:18:17 +02:00
to_update.add(location.storage_location)
if to_update:
2016-06-23 09:40:00 +02:00
for child_location in to_update:
child_location.parent = location
to_save.append(child_location)
to_update.clear()
2016-06-23 09:40:00 +02:00
cls.save(to_save)
2008-12-12 17:09:08 +01:00
2012-09-24 22:18:17 +02:00
@classmethod
2013-01-06 22:18:13 +01:00
def create(cls, vlist):
locations = super(Location, cls).create(vlist)
cls._set_warehouse_parent(locations)
cls._default_warehouse_cache.clear()
2013-01-06 22:18:13 +01:00
return locations
2008-12-12 17:09:08 +01:00
2012-09-24 22:18:17 +02:00
@classmethod
2013-12-19 14:45:31 +01:00
def write(cls, *args):
super(Location, cls).write(*args)
locations = sum(args[::2], [])
2012-09-24 22:18:17 +02:00
cls._set_warehouse_parent(locations)
cls._default_warehouse_cache.clear()
2012-09-24 22:18:17 +02:00
ids = [l.id for l in locations]
warehouses = cls.search([
('type', '=', 'warehouse'),
['OR',
('storage_location', 'in', ids),
('input_location', 'in', ids),
('output_location', 'in', ids),
]])
fields = ('storage_location', 'input_location', 'output_location')
wh2childs = {}
for warehouse in warehouses:
2012-09-24 22:18:17 +02:00
in_out_sto = (getattr(warehouse, f).id for f in fields)
for location in locations:
if location.id not in in_out_sto:
continue
2012-09-24 22:18:17 +02:00
childs = wh2childs.setdefault(warehouse.id, cls.search([
('parent', 'child_of', warehouse.id),
]))
if location not in childs:
2018-12-29 14:20:29 +01:00
raise LocationValidationError(
gettext('stock.msg_location_child_of_warehouse',
location=location.rec_name,
warehouse=warehouse.rec_name))
@classmethod
2023-04-22 18:43:45 +02:00
def delete(cls, locations):
# Delete also required children as CASCADING is done separately
extra_locations = []
for location in locations:
extra_locations.extend(filter(None, [
location.input_location,
location.output_location,
location.storage_location,
]))
super().delete(locations + extra_locations)
cls._default_warehouse_cache.clear()
2012-09-24 22:18:17 +02:00
@classmethod
def copy(cls, locations, default=None):
if default is None:
default = {}
2018-09-01 01:02:04 +02:00
else:
default = default.copy()
2009-05-11 10:55:41 +02:00
res = []
for location in locations:
if location.type == 'warehouse':
wh_default = default.copy()
wh_default['type'] = 'view'
2012-03-27 10:44:04 +02:00
wh_default['input_location'] = None
wh_default['output_location'] = None
wh_default['storage_location'] = None
wh_default['childs'] = None
2009-05-11 10:55:41 +02:00
2012-09-24 22:18:17 +02:00
new_location, = super(Location, cls).copy([location],
default=wh_default)
2009-05-11 10:55:41 +02:00
2010-08-19 22:22:44 +02:00
with Transaction().set_context(
cp_warehouse_locations={
'input_location': location.input_location.id,
'output_location': location.output_location.id,
'storage_location': location.storage_location.id,
},
2012-09-24 22:18:17 +02:00
cp_warehouse_id=new_location.id):
cls.copy(location.childs,
default={'parent': new_location.id})
cls.write([new_location], {
'type': 'warehouse',
})
2009-05-11 10:55:41 +02:00
else:
2012-09-24 22:18:17 +02:00
new_location, = super(Location, cls).copy([location],
default=default)
2010-08-19 22:22:44 +02:00
warehouse_locations = Transaction().context.get(
2012-09-24 22:18:17 +02:00
'cp_warehouse_locations') or {}
2009-05-11 10:55:41 +02:00
if location.id in warehouse_locations.values():
cp_warehouse = cls(
Transaction().context['cp_warehouse_id'])
2018-06-26 19:11:55 +02:00
for field, loc_id in warehouse_locations.items():
2009-05-11 10:55:41 +02:00
if loc_id == location.id:
2012-09-24 22:18:17 +02:00
cls.write([cp_warehouse], {
field: new_location.id,
2012-04-03 20:07:43 +02:00
})
2009-05-11 10:55:41 +02:00
2012-09-24 22:18:17 +02:00
res.append(new_location)
return res
@classmethod
def view_attributes(cls):
storage_types = Eval('type').in_(['storage', 'warehouse', 'view'])
return super().view_attributes() + [
('/tree/field[@name="quantity"]',
'visual', If(
storage_types & (Eval('quantity', 0) < 0), 'danger', ''),
['type']),
('/tree/field[@name="forecast_quantity"]',
'visual', If(
storage_types & (Eval('forecast_quantity', 0) < 0),
'warning', ''),
['type']),
]
class ProductsByLocationsContext(ModelView):
2012-01-02 18:07:56 +01:00
'Products by Locations'
__name__ = 'stock.products_by_locations.context'
company = fields.Many2One('company.company', "Company", required=True)
2008-06-05 01:00:26 +02:00
forecast_date = fields.Date(
2018-12-04 12:25:45 +01:00
'At Date',
help="The date for which the stock quantity is calculated.\n"
"* An empty value calculates as far ahead as possible.\n"
"* A date in the past will provide historical values.")
stock_date_end = fields.Function(fields.Date('At Date'),
'on_change_with_stock_date_end')
2008-10-06 12:11:57 +02:00
@classmethod
def default_company(cls):
return Transaction().context.get('company')
2012-09-24 22:18:17 +02:00
@staticmethod
def default_forecast_date():
Date_ = Pool().get('ir.date')
return Date_.today()
2008-06-05 01:00:26 +02:00
@fields.depends('forecast_date')
def on_change_with_stock_date_end(self, name=None):
if self.forecast_date is None:
return datetime.date.max
return self.forecast_date
class ProductsByLocations(DeactivableMixin, ModelSQL, ModelView):
"Products by Locations"
__name__ = 'stock.products_by_locations'
product = fields.Many2One('product.product', "Product")
quantity = fields.Function(
fields.Float("Quantity", digits='default_uom'),
'get_product', searcher='search_product')
forecast_quantity = fields.Function(
fields.Float("Forecast Quantity", digits='default_uom'),
'get_product', searcher='search_product')
default_uom = fields.Function(
2023-08-20 12:17:27 +02:00
fields.Many2One(
'product.uom', "Default UoM",
help="The default Unit of Measure."),
'get_product', searcher='search_product')
cost_value = fields.Function(
fields.Numeric("Cost Value"), 'get_product')
consumable = fields.Function(
fields.Boolean("Consumable"), 'get_product',
searcher='search_product')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('product', 'ASC'))
@classmethod
def table_query(cls):
pool = Pool()
Product = pool.get('product.product')
product = Product.__table__()
columns = []
for fname, field in cls._fields.items():
if not hasattr(field, 'set'):
if (isinstance(field, fields.Many2One)
and field.get_target() == Product):
column = Column(product, 'id')
else:
column = Column(product, fname)
columns.append(column.as_(fname))
return product.select(*columns)
def get_rec_name(self, name):
return self.product.rec_name
@classmethod
def search_rec_name(cls, name, clause):
return [('product.rec_name',) + tuple(clause[1:])]
def get_product(self, name):
value = getattr(self.product, name)
if isinstance(value, Model):
value = value.id
return value
@classmethod
def search_product(cls, name, clause):
nested = clause[0][len(name):]
return [('product.' + name + nested, *clause[1:])]
2016-09-13 14:10:03 +02:00
class LocationLeadTime(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
'Location Lead Time'
__name__ = 'stock.location.lead_time'
warehouse_from = fields.Many2One('stock.location', 'Warehouse From',
ondelete='CASCADE',
domain=[
('type', '=', 'warehouse'),
('id', '!=', Eval('warehouse_to', -1)),
])
warehouse_to = fields.Many2One('stock.location', 'Warehouse To',
ondelete='CASCADE',
domain=[
('type', '=', 'warehouse'),
('id', '!=', Eval('warehouse_from', -1)),
])
2022-11-24 19:16:51 +01:00
lead_time = fields.TimeDelta(
"Lead Time",
domain=['OR',
('lead_time', '=', None),
('lead_time', '>=', TimeDelta()),
],
2018-12-04 12:25:45 +01:00
help="The time it takes to move stock between the warehouses.")
@classmethod
def get_lead_time(cls, pattern):
for record in cls.search([]):
if record.match(pattern):
return record.lead_time