Files
tryton/modules/stock_package/stock.py
T
2022-12-16 15:21:11 +01:00

420 lines
14 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.
from sql import Cast, Literal
from sql.functions import Substring, Position
from trytond import backend
from trytond.i18n import gettext
from trytond.model import ModelSQL, ModelView, Workflow, fields, tree
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Bool, Id, If
from trytond.report import Report
from trytond.transaction import Transaction
from .exceptions import PackageError
class Configuration(metaclass=PoolMeta):
__name__ = 'stock.configuration'
package_sequence = fields.MultiValue(fields.Many2One(
'ir.sequence', "Package Sequence", required=True,
domain=[
('company', 'in', [Eval('context', {}).get('company'), None]),
('sequence_type', '=',
Id('stock_package', 'sequence_type_package')),
]))
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'package_sequence':
return pool.get('stock.configuration.sequence')
return super(Configuration, cls).multivalue_model(field)
@classmethod
def default_package_sequence(cls, **pattern):
return cls.multivalue_model(
'package_sequence').default_package_sequence()
class ConfigurationSequence(metaclass=PoolMeta):
__name__ = 'stock.configuration.sequence'
package_sequence = fields.Many2One(
'ir.sequence', "Package Sequence", required=True,
domain=[
('company', 'in', [Eval('company', -1), None]),
('sequence_type', '=',
Id('stock_package', 'sequence_type_package')),
],
depends=['company'])
@classmethod
def __register__(cls, module_name):
exist = backend.TableHandler.table_exist(cls._table)
if exist:
table = cls.__table_handler__(module_name)
exist &= table.column_exist('package_sequence')
super(ConfigurationSequence, cls).__register__(module_name)
if not exist:
cls._migrate_property([], [], [])
@classmethod
def _migrate_property(cls, field_names, value_names, fields):
field_names.append('package_sequence')
value_names.append('package_sequence')
super(ConfigurationSequence, cls)._migrate_property(
field_names, value_names, fields)
@classmethod
def default_package_sequence(cls):
pool = Pool()
ModelData = pool.get('ir.model.data')
try:
return ModelData.get_id('stock_package', 'sequence_package')
except KeyError:
return None
class WeightMixin:
__slots__ = ()
packaging_weight = fields.Float(
"Packaging Weight", digits=(16, Eval('packaging_weight_digits', 2)),
depends=['packaging_weight_digits'],
help="The weight of the package when empty.")
packaging_weight_uom = fields.Many2One(
'product.uom', "Packaging Weight Uom",
domain=[('category', '=', Id('product', 'uom_cat_weight'))],
states={
'required': Bool(Eval('packaging_weight')),
},
depends=['packaging_weight'])
packaging_weight_digits = fields.Function(
fields.Integer("Packaging Weight Digits"),
'on_change_with_packaging_weight_digits')
@fields.depends('packaging_weight_uom')
def on_change_with_packaging_weight_digits(self, name=None):
return (
self.packaging_weight_uom.digits if self.packaging_weight_uom
else None)
class Package(tree(), WeightMixin, ModelSQL, ModelView):
'Stock Package'
__name__ = 'stock.package'
_rec_name = 'code'
code = fields.Char('Code', select=True, readonly=True, required=True)
company = fields.Many2One('company.company', "Company", required=True)
type = fields.Many2One(
'stock.package.type', "Type", required=True,
states={
'readonly': Eval('state') == 'closed',
},
depends=['state'])
shipment = fields.Reference(
"Shipment", selection='get_shipment', select=True,
states={
'readonly': Eval('state') == 'closed',
},
domain=[
('company', '=', Eval('company', -1)),
],
depends=['state', 'company'])
moves = fields.One2Many('stock.move', 'package', 'Moves',
domain=[
('company', '=', Eval('company', -1)),
('shipment', '=', Eval('shipment')),
('to_location.type', 'in', ['customer', 'supplier']),
('state', '!=', 'cancelled'),
],
add_remove=[
('package', '=', None),
],
states={
'readonly': Eval('state') == 'closed',
},
depends=['company', 'shipment', 'state'])
parent = fields.Many2One(
'stock.package', "Parent", select=True, ondelete='CASCADE',
domain=[
('company', '=', Eval('company', -1)),
('shipment', '=', Eval('shipment')),
],
states={
'readonly': Eval('state') == 'closed',
},
depends=['company', 'shipment', 'state'])
children = fields.One2Many(
'stock.package', 'parent', 'Children',
domain=[
('company', '=', Eval('company', -1)),
('shipment', '=', Eval('shipment')),
],
states={
'readonly': Eval('state') == 'closed',
},
depends=['company', 'shipment', 'state'])
state = fields.Function(fields.Selection([
('open', "Open"),
('closed', "Closed"),
], "State"), 'on_change_with_state')
@classmethod
def __register__(cls, module):
pool = Pool()
table_h = cls.__table_handler__(module)
table = cls.__table__()
company_exist = table_h.column_exist('company')
cursor = Transaction().connection.cursor()
super().__register__(module)
# Migration from 5.8: add company
if not company_exist:
shipment_id = Cast(Substring(
table.shipment,
Position(',', table.shipment) + Literal(1)),
cls.id.sql_type().base)
for name in cls._get_shipment():
Shipment = pool.get(name)
shipment = Shipment.__table__()
value = (shipment
.select(shipment.company,
where=shipment.id == shipment_id))
cursor.execute(*table.update(
[table.company], [value],
where=table.shipment.like(name + ',%')))
@classmethod
def default_company(cls):
return Transaction().context.get('company')
@staticmethod
def _get_shipment():
'Return list of Model names for shipment Reference'
return [
'stock.shipment.out',
'stock.shipment.in.return',
]
@classmethod
def get_shipment(cls):
pool = Pool()
Model = pool.get('ir.model')
get_name = Model.get_name
models = cls._get_shipment()
return [(None, '')] + [(m, get_name(m)) for m in models]
@fields.depends('shipment')
def on_change_with_state(self, name=None):
if (self.shipment
and self.shipment.state in {'packed', 'done', 'cancelled'}):
return 'closed'
return 'open'
@fields.depends('type')
def on_change_type(self):
if self.type:
self.packaging_weight = self.type.packaging_weight
self.packaging_weight_uom = self.type.packaging_weight_uom
@classmethod
def create(cls, vlist):
pool = Pool()
Config = pool.get('stock.configuration')
vlist = [v.copy() for v in vlist]
config = Config(1)
default_company = cls.default_company()
for values in vlist:
values['code'] = config.get_multivalue(
'package_sequence',
company=values.get('company', default_company)).get()
return super(Package, cls).create(vlist)
@classmethod
def copy(cls, packages, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('moves')
return super().copy(packages, default=default)
class Type(WeightMixin, ModelSQL, ModelView):
'Stock Package Type'
__name__ = 'stock.package.type'
name = fields.Char('Name', required=True)
class Move(metaclass=PoolMeta):
__name__ = 'stock.move'
package = fields.Many2One(
'stock.package', "Package", select=True,
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('state') == 'cancelled',
},
depends=['company', 'state'])
@property
def package_path(self):
path = []
package = self.package
while package:
path.append(package)
package = package.parent
path.reverse()
return path
@classmethod
def copy(cls, moves, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('package')
return super().copy(moves, default=default)
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, moves):
cls.write([m for m in moves if m.package], {'package': None})
super().cancel(moves)
class PackageMixin(object):
__slots__ = ()
packages = fields.One2Many('stock.package', 'shipment', 'Packages',
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
root_packages = fields.Function(fields.One2Many('stock.package',
'shipment', 'Packages',
domain=[
('company', '=', Eval('company', -1)),
('parent', '=', None),
],
depends=['company']),
'get_root_packages', setter='set_root_packages')
def get_root_packages(self, name):
return [p.id for p in self.packages if not p.parent]
@classmethod
def set_root_packages(cls, shipments, name, value):
if not value:
return
cls.write(shipments, {
'packages': value,
})
@classmethod
def check_packages(cls, shipments):
for shipment in shipments:
if not shipment.packages:
continue
length = sum(len(p.moves) for p in shipment.packages)
if len(list(shipment.packages_moves)) != length:
raise PackageError(
gettext('stock_package.msg_package_mismatch',
shipment=shipment.rec_name))
@property
def packages_moves(self):
raise NotImplementedError
class ShipmentOut(PackageMixin, object, metaclass=PoolMeta):
__name__ = 'stock.shipment.out'
@classmethod
def __setup__(cls):
super().__setup__()
packages_readonly = If(
Eval('warehouse_storage') == Eval('warehouse_output'),
Eval('state') != 'waiting',
Eval('state') != 'picked')
packages_depends = ['warehouse_storage', 'warehouse_output', 'state']
for field in [cls.packages, cls.root_packages]:
field.states['readonly'] = packages_readonly
field.depends.extend(packages_depends)
@classmethod
@ModelView.button
@Workflow.transition('packed')
def pack(cls, shipments):
super(ShipmentOut, cls).pack(shipments)
cls.check_packages(shipments)
@classmethod
@ModelView.button
@Workflow.transition('done')
def done(cls, shipments):
super(ShipmentOut, cls).done(shipments)
cls.check_packages(shipments)
@property
def packages_moves(self):
return (m for m in self.outgoing_moves if m.state != 'cancelled')
def _group_parcel_key(self, lines, line):
try:
root_package = line.package_path[0]
except IndexError:
root_package = None
return super()._group_parcel_key(lines, line) + (
('root_package', root_package),)
@fields.depends('carrier')
def _parcel_weight(self, parcel):
pool = Pool()
Uom = pool.get('product.uom')
weight = super()._parcel_weight(parcel)
if self.carrier:
carrier_uom = self.carrier.weight_uom
packages = {p for l in parcel for p in l.package_path}
for package in packages:
if package.packaging_weight:
weight += Uom.compute_qty(
package.packaging_weight_uom, package.packaging_weight,
carrier_uom, round=False)
return weight
class ShipmentInReturn(PackageMixin, object, metaclass=PoolMeta):
__name__ = 'stock.shipment.in.return'
@classmethod
def __setup__(cls):
super().__setup__()
packages_readonly = ~Eval('state').in_(['waiting', 'assigned'])
packages_depends = ['state']
for field in [cls.packages, cls.root_packages]:
field.states['readonly'] = packages_readonly
field.depends.extend(packages_depends)
@classmethod
@ModelView.button
@Workflow.transition('done')
def done(cls, shipments):
super(ShipmentInReturn, cls).done(shipments)
cls.check_packages(shipments)
@property
def packages_moves(self):
return (m for m in self.moves if m.state != 'cancelled')
class PackageLabel(Report):
'Package Label'
__name__ = 'stock.package.label'