675 lines
24 KiB
Python
675 lines
24 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import datetime
|
|
import operator
|
|
from decimal import Decimal
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
ModelView, ModelSQL, MatchMixin, ValueMixin, DeactivableMixin, fields,
|
|
sequence_ordered, tree)
|
|
from trytond import backend
|
|
from trytond.cache import Cache
|
|
from trytond.pyson import Eval, If
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.tools import grouped_slice
|
|
from trytond.tools.multivalue import migrate_property
|
|
|
|
from .exceptions import LocationValidationError
|
|
|
|
__all__ = ['Location', 'Party', 'PartyLocation', 'ProductsByLocationsContext',
|
|
'LocationLeadTime']
|
|
|
|
STATES = {
|
|
'readonly': ~Eval('active'),
|
|
}
|
|
DEPENDS = ['active']
|
|
|
|
|
|
class Location(DeactivableMixin, tree(), ModelSQL, ModelView):
|
|
"Stock Location"
|
|
__name__ = 'stock.location'
|
|
_default_warehouse_cache = Cache('stock.location.default_warehouse',
|
|
context=False)
|
|
|
|
name = fields.Char("Name", size=None, required=True, states=STATES,
|
|
depends=DEPENDS, translate=True)
|
|
code = fields.Char("Code", size=None, states=STATES, depends=DEPENDS,
|
|
select=True,
|
|
help="The internal identifier used for the location.")
|
|
address = fields.Many2One("party.address", "Address",
|
|
states={
|
|
'invisible': Eval('type') != 'warehouse',
|
|
'readonly': ~Eval('active'),
|
|
},
|
|
depends=['type', 'active'])
|
|
type = fields.Selection([
|
|
('supplier', 'Supplier'),
|
|
('customer', 'Customer'),
|
|
('lost_found', 'Lost and Found'),
|
|
('warehouse', 'Warehouse'),
|
|
('storage', 'Storage'),
|
|
('production', 'Production'),
|
|
('drop', 'Drop'),
|
|
('view', 'View'),
|
|
], 'Location type', states=STATES, depends=DEPENDS)
|
|
type_string = type.translated('type')
|
|
parent = fields.Many2One("stock.location", "Parent", select=True,
|
|
left="left", right="right",
|
|
states={
|
|
'invisible': Eval('type') == 'warehouse',
|
|
},
|
|
depends=['type'],
|
|
help="Used to add structure above the location.")
|
|
left = fields.Integer('Left', required=True, select=True)
|
|
right = fields.Integer('Right', required=True, select=True)
|
|
childs = fields.One2Many("stock.location", "parent", "Children",
|
|
help="Used to add structure below the location.")
|
|
flat_childs = fields.Boolean(
|
|
"Flat Children",
|
|
help="Check to enforce a single level of children with no "
|
|
"grandchildren.")
|
|
warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'),
|
|
'get_warehouse')
|
|
input_location = fields.Many2One(
|
|
"stock.location", "Input", states={
|
|
'invisible': Eval('type') != 'warehouse',
|
|
'readonly': ~Eval('active'),
|
|
'required': Eval('type') == 'warehouse',
|
|
},
|
|
domain=[
|
|
('type', '=', 'storage'),
|
|
['OR',
|
|
('parent', 'child_of', [Eval('id')]),
|
|
('parent', '=', None),
|
|
],
|
|
],
|
|
depends=['type', 'active', 'id'],
|
|
help="Where incoming stock is received.")
|
|
output_location = fields.Many2One(
|
|
"stock.location", "Output", states={
|
|
'invisible': Eval('type') != 'warehouse',
|
|
'readonly': ~Eval('active'),
|
|
'required': Eval('type') == 'warehouse',
|
|
},
|
|
domain=[
|
|
('type', '=', 'storage'),
|
|
['OR',
|
|
('parent', 'child_of', [Eval('id')]),
|
|
('parent', '=', None)]],
|
|
depends=['type', 'active', 'id'],
|
|
help="Where outgoing stock is sent from.")
|
|
storage_location = fields.Many2One(
|
|
"stock.location", "Storage", states={
|
|
'invisible': Eval('type') != 'warehouse',
|
|
'readonly': ~Eval('active'),
|
|
'required': Eval('type') == 'warehouse',
|
|
},
|
|
domain=[
|
|
('type', 'in', ['storage', 'view']),
|
|
['OR',
|
|
('parent', 'child_of', [Eval('id')]),
|
|
('parent', '=', None)]],
|
|
depends=['type', 'active', 'id'],
|
|
help="The top level location where stock is stored.")
|
|
picking_location = fields.Many2One(
|
|
'stock.location', 'Picking', states={
|
|
'invisible': Eval('type') != 'warehouse',
|
|
'readonly': ~Eval('active'),
|
|
},
|
|
domain=[
|
|
('type', '=', 'storage'),
|
|
('parent', 'child_of', [Eval('storage_location', -1)]),
|
|
],
|
|
depends=['type', 'active', 'storage_location'],
|
|
help="Where stock is picked from.\n"
|
|
"Leave empty to use the storage location.")
|
|
quantity = fields.Function(
|
|
fields.Float('Quantity',
|
|
help="The amount of stock in the location."),
|
|
'get_quantity', searcher='search_quantity')
|
|
forecast_quantity = fields.Function(
|
|
fields.Float('Forecast Quantity',
|
|
help="The amount of stock expected to be in the location."),
|
|
'get_quantity', searcher='search_quantity')
|
|
cost_value = fields.Function(fields.Numeric('Cost Value',
|
|
help="The value of the stock in the location."),
|
|
'get_cost_value')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Location, cls).__setup__()
|
|
cls._order.insert(0, ('name', 'ASC'))
|
|
|
|
parent_domain = [
|
|
['OR',
|
|
('parent.flat_childs', '=', False),
|
|
('parent', '=', None),
|
|
]
|
|
]
|
|
childs_domain = [
|
|
If(Eval('flat_childs', False),
|
|
('childs', '=', None),
|
|
()),
|
|
]
|
|
childs_mapping = cls._childs_domain()
|
|
for type_, allowed_parents in cls._parent_domain().items():
|
|
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
|
|
cls.childs.depends.extend(['flat_childs', 'type'])
|
|
|
|
@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'],
|
|
'warehouse': [''],
|
|
}
|
|
|
|
@classmethod
|
|
def _childs_domain(cls):
|
|
childs_domain = {}
|
|
for type_, allowed_parents in cls._parent_domain().items():
|
|
for parent in allowed_parents:
|
|
childs_domain.setdefault(parent, [])
|
|
childs_domain[parent].append(type_)
|
|
return childs_domain
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
super(Location, cls).__register__(module_name)
|
|
|
|
table = cls.__table_handler__(module_name)
|
|
table.index_action(['left', 'right'], 'add')
|
|
|
|
@classmethod
|
|
def validate(cls, locations):
|
|
super(Location, cls).validate(locations)
|
|
inactives = []
|
|
for location in locations:
|
|
location.check_type_for_moves()
|
|
if not location.active:
|
|
inactives.append(location)
|
|
cls.check_inactive(inactives)
|
|
|
|
def check_type_for_moves(self):
|
|
""" Check locations with moves have types compatible with moves. """
|
|
invalid_move_types = ['warehouse', 'view']
|
|
Move = Pool().get('stock.move')
|
|
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']),
|
|
])
|
|
if moves:
|
|
raise LocationValidationError(
|
|
gettext('stock.msg_location_invalid_type_for_moves',
|
|
location=self.rec_name,
|
|
type=self.type_string))
|
|
|
|
@classmethod
|
|
def check_inactive(cls, locations):
|
|
"Check inactive location are empty"
|
|
assert all(not l.active for l in locations)
|
|
empty = cls.get_empty_locations(locations)
|
|
non_empty = set(locations) - set(empty)
|
|
if non_empty:
|
|
raise LocationValidationError(
|
|
gettext('stock.msg_location_inactive_not_empty',
|
|
location=next(iter(non_empty)).rec_name))
|
|
|
|
@classmethod
|
|
def get_empty_locations(cls, locations=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
if locations is None:
|
|
locations = cls.search([])
|
|
if not locations:
|
|
return []
|
|
location_ids = list(map(int, locations))
|
|
# Use root to compute for all companies
|
|
# and ensures inactive locations are in the query
|
|
with Transaction().set_user(0), \
|
|
Transaction().set_context(active_test=False):
|
|
query = Move.compute_quantities_query(
|
|
location_ids, with_childs=True)
|
|
quantities = Move.compute_quantities(
|
|
query, location_ids, with_childs=True)
|
|
empty = set(location_ids)
|
|
for (location_id, product), quantity in quantities.items():
|
|
if quantity:
|
|
empty.discard(location_id)
|
|
for sub_ids in grouped_slice(list(empty)):
|
|
sub_ids = list(sub_ids)
|
|
moves = Move.search([
|
|
('state', 'not in', ['done', 'cancel']),
|
|
['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)
|
|
|
|
@staticmethod
|
|
def default_left():
|
|
return 0
|
|
|
|
@staticmethod
|
|
def default_right():
|
|
return 0
|
|
|
|
@classmethod
|
|
def default_flat_childs(cls):
|
|
return False
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'storage'
|
|
|
|
@classmethod
|
|
def check_xml_record(cls, records, values):
|
|
return True
|
|
|
|
def get_warehouse(self, name):
|
|
# Order by descending left to get the first one in the tree
|
|
with Transaction().set_context(active_test=False):
|
|
locations = self.search([
|
|
('parent', 'parent_of', [self.id]),
|
|
('type', '=', 'warehouse'),
|
|
], order=[('left', 'DESC')])
|
|
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
|
|
|
|
@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:]),
|
|
]
|
|
|
|
@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
|
|
and isinstance(trans_context[name], int))
|
|
|
|
if not any(map(valid_context, ['product', 'product_template'])):
|
|
return {l.id: None for l in locations}
|
|
|
|
context = {}
|
|
if (name == 'quantity'
|
|
and (trans_context.get('stock_date_end', datetime.date.max)
|
|
> Date_.today())):
|
|
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
|
|
|
|
if trans_context.get('product') is not None:
|
|
grouping = ('product',)
|
|
grouping_filter = ([trans_context['product']],)
|
|
key = trans_context['product']
|
|
else:
|
|
grouping = ('product.template',)
|
|
grouping_filter = ([trans_context['product_template']],)
|
|
key = trans_context['product_template']
|
|
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,
|
|
with_childs=trans_context.get('with_childs', True)))
|
|
|
|
return dict((loc.id, pbl.get((loc.id, key), 0)) for loc in locations)
|
|
|
|
@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)]
|
|
|
|
@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
|
|
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 'stock_date_end' in trans_context:
|
|
# 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:
|
|
for location in locations:
|
|
cost_values[location.id] = (
|
|
Decimal(str(location.quantity)) * cost_price)
|
|
return cost_values
|
|
|
|
@classmethod
|
|
def _set_warehouse_parent(cls, locations):
|
|
'''
|
|
Set the parent of child location of warehouse if not set
|
|
'''
|
|
to_update = set()
|
|
to_save = []
|
|
for location in locations:
|
|
if location.type == 'warehouse':
|
|
if not location.input_location.parent:
|
|
to_update.add(location.input_location)
|
|
if not location.output_location.parent:
|
|
to_update.add(location.output_location)
|
|
if not location.storage_location.parent:
|
|
to_update.add(location.storage_location)
|
|
if to_update:
|
|
for child_location in to_update:
|
|
child_location.parent = location
|
|
to_save.append(child_location)
|
|
to_update.clear()
|
|
cls.save(to_save)
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
locations = super(Location, cls).create(vlist)
|
|
cls._set_warehouse_parent(locations)
|
|
cls._default_warehouse_cache.clear()
|
|
return locations
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
super(Location, cls).write(*args)
|
|
locations = sum(args[::2], [])
|
|
cls._set_warehouse_parent(locations)
|
|
cls._default_warehouse_cache.clear()
|
|
|
|
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:
|
|
in_out_sto = (getattr(warehouse, f).id for f in fields)
|
|
for location in locations:
|
|
if location.id not in in_out_sto:
|
|
continue
|
|
childs = wh2childs.setdefault(warehouse.id, cls.search([
|
|
('parent', 'child_of', warehouse.id),
|
|
]))
|
|
if location not in childs:
|
|
raise LocationValidationError(
|
|
gettext('stock.msg_location_child_of_warehouse',
|
|
location=location.rec_name,
|
|
warehouse=warehouse.rec_name))
|
|
|
|
@classmethod
|
|
def delete(cls, *args):
|
|
super().delete(*args)
|
|
cls._default_warehouse_cache.clear()
|
|
|
|
@classmethod
|
|
def copy(cls, locations, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
|
|
res = []
|
|
for location in locations:
|
|
if location.type == 'warehouse':
|
|
|
|
wh_default = default.copy()
|
|
wh_default['type'] = 'view'
|
|
wh_default['input_location'] = None
|
|
wh_default['output_location'] = None
|
|
wh_default['storage_location'] = None
|
|
wh_default['childs'] = None
|
|
|
|
new_location, = super(Location, cls).copy([location],
|
|
default=wh_default)
|
|
|
|
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,
|
|
},
|
|
cp_warehouse_id=new_location.id):
|
|
cls.copy(location.childs,
|
|
default={'parent': new_location.id})
|
|
cls.write([new_location], {
|
|
'type': 'warehouse',
|
|
})
|
|
else:
|
|
new_location, = super(Location, cls).copy([location],
|
|
default=default)
|
|
warehouse_locations = Transaction().context.get(
|
|
'cp_warehouse_locations') or {}
|
|
if location.id in warehouse_locations.values():
|
|
cp_warehouse = cls(
|
|
Transaction().context['cp_warehouse_id'])
|
|
for field, loc_id in warehouse_locations.items():
|
|
if loc_id == location.id:
|
|
cls.write([cp_warehouse], {
|
|
field: new_location.id,
|
|
})
|
|
|
|
res.append(new_location)
|
|
return res
|
|
|
|
|
|
supplier_location = fields.Many2One(
|
|
'stock.location', "Supplier Location", domain=[('type', '=', 'supplier')],
|
|
help="The default source location for stock received from the party.")
|
|
customer_location = fields.Many2One(
|
|
'stock.location', "Customer Location", domain=[('type', '=', 'customer')],
|
|
help="The default destination location for stock sent to the party.")
|
|
|
|
|
|
class Party(metaclass=PoolMeta):
|
|
__name__ = 'party.party'
|
|
supplier_location = fields.MultiValue(supplier_location)
|
|
customer_location = fields.MultiValue(customer_location)
|
|
locations = fields.One2Many(
|
|
'party.party.location', 'party', "Locations")
|
|
|
|
@classmethod
|
|
def multivalue_model(cls, field):
|
|
pool = Pool()
|
|
if field in {'supplier_location', 'customer_location'}:
|
|
return pool.get('party.party.location')
|
|
return super(Party, cls).multivalue_model(field)
|
|
|
|
@classmethod
|
|
def default_supplier_location(cls, **pattern):
|
|
return cls.multivalue_model(
|
|
'supplier_location').default_supplier_location()
|
|
|
|
@classmethod
|
|
def default_customer_location(cls, **pattern):
|
|
return cls.multivalue_model(
|
|
'customer_location').default_customer_location()
|
|
|
|
|
|
class PartyLocation(ModelSQL, ValueMixin):
|
|
"Party Location"
|
|
__name__ = 'party.party.location'
|
|
party = fields.Many2One(
|
|
'party.party', "Party", ondelete='CASCADE', select=True)
|
|
supplier_location = supplier_location
|
|
customer_location = customer_location
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
exist = TableHandler.table_exist(cls._table)
|
|
|
|
super(PartyLocation, cls).__register__(module_name)
|
|
|
|
if not exist:
|
|
cls._migrate_property([], [], [])
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.extend(['supplier_location', 'customer_location'])
|
|
value_names.extend(['supplier_location', 'customer_location'])
|
|
migrate_property(
|
|
'party.party', field_names, cls, value_names,
|
|
parent='party', fields=fields)
|
|
|
|
@classmethod
|
|
def default_supplier_location(cls):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
try:
|
|
return ModelData.get_id('stock', 'location_supplier')
|
|
except KeyError:
|
|
return None
|
|
|
|
@classmethod
|
|
def default_customer_location(cls):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
try:
|
|
return ModelData.get_id('stock', 'location_customer')
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
class ProductsByLocationsContext(ModelView):
|
|
'Products by Locations'
|
|
__name__ = 'stock.products_by_locations.context'
|
|
forecast_date = fields.Date(
|
|
'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')
|
|
|
|
@staticmethod
|
|
def default_forecast_date():
|
|
Date_ = Pool().get('ir.date')
|
|
return Date_.today()
|
|
|
|
@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 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'),
|
|
])
|
|
warehouse_to = fields.Many2One('stock.location', 'Warehouse To',
|
|
ondelete='CASCADE',
|
|
domain=[
|
|
('type', '=', 'warehouse'),
|
|
])
|
|
lead_time = fields.TimeDelta('Lead Time',
|
|
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
|