360 lines
11 KiB
Python
Executable File
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}
|