Files
2025-12-26 13:11:43 +00:00

360 lines
11 KiB
Python
Executable File

# 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 mimetypes
from sql import Column
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import ModelView, Workflow, fields
from trytond.model.exceptions import AccessError
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.report import Report
from trytond.transaction import Transaction
from trytond.wizard import StateAction, StateTransition, Wizard
from .exceptions import PackWarning
if config.getboolean('stock_package_shipping', 'filestore', default=False):
file_id = 'shipping_label_id'
store_prefix = config.get(
'stock_package_shipping', 'store_prefix', default=None)
else:
file_id = store_prefix = None
class Package(metaclass=PoolMeta):
__name__ = 'stock.package'
shipping_reference = fields.Char('Shipping Reference',
states={
'readonly': Eval('has_shipping_service', False),
})
shipping_label = fields.Binary(
"Shipping Label", readonly=True,
file_id=file_id, store_prefix=store_prefix)
shipping_label_id = fields.Char(
"Shipping Label ID", readonly=True, strip=False)
shipping_label_mimetype = fields.Char(
"Shipping Label MIME Type", readonly=True)
shipping_tracking_url = fields.Function(
fields.Char(
"Shipping Tracking URL",
states={
'invisible': ~Eval('shipping_tracking_url'),
}),
'get_shipping_tracking_url')
has_shipping_service = fields.Function(
fields.Boolean("Has Shipping Service"),
'on_change_with_has_shipping_service')
def get_shipping_tracking_url(self, name):
return
@classmethod
def __setup__(cls):
super().__setup__()
cls.shipping_reference.search_unaccented = False
cls._buttons.update(
print_shipping_label={
'invisible': ~Eval('shipping_label'),
'depends': ['shipping_label'],
})
@classmethod
def __register__(cls, module_name):
pool = Pool()
PackageType = pool.get('stock.package.type')
cursor = Transaction().connection.cursor()
table = cls.__table__()
package_type = PackageType.__table__()
table_h = cls.__table_handler__(module_name)
dimension_columns = [
'length', 'length_uom',
'height', 'height_uom',
'width', 'width_uom']
dimension_exists = any(
table_h.column_exist(c) for c in dimension_columns)
super().__register__(module_name)
# Migration from 5.8: Update dimensions on package from package_type
if not dimension_exists:
columns = []
values = []
for c in dimension_columns:
columns.append(Column(table, c))
values.append(package_type.select(
Column(package_type, c),
where=package_type.id == table.type))
cursor.execute(*table.update(
columns=columns,
values=values))
@fields.depends('shipment')
def on_change_with_has_shipping_service(self, name=None):
return bool(
self.shipment
and getattr(self.shipment, 'carrier', None)
and getattr(self.shipment.carrier, 'shipping_service', None))
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
domain = super(Package, cls).search_rec_name(name, clause)
return [bool_op,
domain,
('shipping_reference', *clause[1:]),
]
@classmethod
def copy(cls, packages, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('shipping_reference', None)
default.setdefault('shipping_label', None)
return super(Package, cls).copy(packages, default=default)
@classmethod
@ModelView.button_action('stock_package_shipping.report_shipping_label')
def print_shipping_label(cls, packages):
pass
class ShippingMixin:
__slots__ = ()
shipping_reference = fields.Char(
"Shipping Reference",
states={
'readonly': Eval('has_shipping_service', False),
})
shipping_description = fields.Char('Shipping Description',
states={
'readonly': Eval('state').in_(['done', 'packed'])
})
has_shipping_service = fields.Function(
fields.Boolean("Has Shipping Service"),
'on_change_with_has_shipping_service')
@classmethod
def __setup__(cls):
super().__setup__()
cls.shipping_reference.search_unaccented = False
cls._buttons.update({
'create_shipping': {
'invisible': (Eval('shipping_reference', False)
| ~Eval('carrier', False)),
'readonly': (Eval('shipping_reference', False)
| ~Eval('root_packages', False)
| ~Eval('carrier', False)
| ~Eval('state').in_(['packed', 'done'])),
'depends': [
'state', 'carrier', 'shipping_reference',
'root_packages'],
},
})
@classmethod
def __register__(cls, module):
table = cls.__table__()
table_h = cls.__table_handler__(module)
cursor = Transaction().connection.cursor()
fill_shiping_reference = (
table_h.column_exist('reference')
and not table_h.column_exist('shipping_reference'))
super().__register__(module)
# Migration from 6.8: fill shipping_reference
if fill_shiping_reference:
cursor.execute(*table.update(
[table.shipping_reference],
[table.reference]))
@fields.depends('carrier')
def on_change_with_has_shipping_service(self, name=None):
return bool(self.carrier and self.carrier.shipping_service)
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
domain = super().search_rec_name(name, clause)
return [bool_op,
domain,
('shipping_reference', *clause[1:]),
]
@classmethod
def validate(cls, shipments):
super().validate(shipments)
for shipment in shipments:
if shipment.carrier and shipment.carrier.shipping_service:
method_name = ('validate_packing_%s'
% shipment.carrier.shipping_service)
validator = getattr(shipment, method_name)
validator()
@classmethod
def check_no_carrier(cls, shipments):
pool = Pool()
Warning = pool.get('res.user.warning')
for shipment in shipments:
if (not shipment.carrier
and shipment.delivery_address
and shipment.warehouse not in
shipment.delivery_address.warehouses):
name = 'shipment_out_no_carrier_%s' % shipment
if Warning.check(name):
raise PackWarning(name,
gettext('stock_package_shipping'
'.msg_shipment_without_carrier',
shipment=shipment.rec_name))
@classmethod
@ModelView.button_action(
'stock_package_shipping.act_create_shipping_wizard')
def create_shipping(cls, shipments):
for shipment in shipments:
if shipment.state not in shipment.shipping_allowed:
raise AccessError(
gettext('stock_package_shipping.msg_shipment_not_packed',
shipment=shipment.rec_name))
@property
def shipping_allowed(self):
raise NotImplementedError
@property
def shipping_warehouse(self):
raise NotImplementedError
@property
def shipping_to(self):
raise NotImplementedError
@property
def shipping_to_address(self):
raise NotImplementedError
class ShipmentOut(ShippingMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.out'
@classmethod
@ModelView.button
@Workflow.transition('packed')
def pack(cls, shipments):
super().pack(shipments)
cls.check_no_carrier(shipments)
@property
def shipping_allowed(self):
return {'packed', 'done'}
@property
def shipping_warehouse(self):
return self.warehouse
@property
def shipping_to(self):
return self.customer
@property
def shipping_to_address(self):
return self.delivery_address
class ShipmentInReturn(ShippingMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.in.return'
carrier = fields.Many2One(
'carrier', "Carrier",
states={
'readonly': ~Eval('state').in_(['draft', 'waiting', 'assigned']),
})
@classmethod
@ModelView.button
@Workflow.transition('done')
def do(cls, shipments):
super().do(shipments)
cls.check_no_carrier(shipments)
@property
def shipping_allowed(self):
return {'assigned', 'done'}
@property
def shipping_warehouse(self):
return self.from_location.warehouse
@property
def shipping_to(self):
return self.supplier
@property
def shipping_to_address(self):
return self.delivery_address
class CreateShipping(Wizard):
'Create Shipping'
__name__ = 'stock.shipment.create_shipping'
start = StateTransition()
def transition_start(self):
shipping_service = self.record.carrier.shipping_service
method_name = 'validate_packing_%s' % shipping_service
getattr(self.record, method_name)()
return 'end'
class ShippingLabel(Report):
__name__ = 'stock.package.shipping_label'
@classmethod
def render(cls, report, report_context):
package = report_context['record']
if not package:
return '.bin', b''
extension = mimetypes.guess_extension(
package.shipping_label_mimetype or 'application/octet-stream')
# Return with extension so convert has it
return extension, package.shipping_label or b''
@classmethod
def convert(cls, report, data, **kwargs):
return data
class PrintShippingLabel(Wizard):
"Print Shipping Label"
__name__ = 'stock.shipment.print_shipping_label'
start_state = 'print_'
print_ = StateAction('stock_package_shipping.report_shipping_label')
def do_print_(self, action):
package_ids = []
labels = set()
for shipment in self.records:
for package in shipment.packages:
if (package.shipping_label
and package.shipping_label not in labels):
package_ids.append(package.id)
labels.add(package.shipping_label)
return action, {'ids': package_ids}