first commit
This commit is contained in:
331
modules/stock_shipping_point/stock.py
Normal file
331
modules/stock_shipping_point/stock.py
Normal file
@@ -0,0 +1,331 @@
|
||||
# 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 trytond.model import (
|
||||
DeactivableMixin, MatchMixin, ModelSQL, ModelView, Workflow, fields,
|
||||
sequence_ordered)
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval, Id, If
|
||||
|
||||
|
||||
class Location(metaclass=PoolMeta):
|
||||
__name__ = 'stock.location'
|
||||
|
||||
shipping_points = fields.One2Many(
|
||||
'stock.shipping.point', 'warehouse', "Shipping Points",
|
||||
states={
|
||||
'invisible': Eval('type') != 'warehouse',
|
||||
})
|
||||
|
||||
|
||||
class ShippingPoint(DeactivableMixin, ModelSQL, ModelView):
|
||||
__name__ = 'stock.shipping.point'
|
||||
|
||||
name = fields.Char("Name", required=True)
|
||||
warehouse = fields.Many2One(
|
||||
'stock.location', "Warehouse", required=True,
|
||||
domain=[
|
||||
('type', '=', 'warehouse'),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def default_warehouse(cls):
|
||||
Location = Pool().get('stock.location')
|
||||
return Location.get_default_warehouse()
|
||||
|
||||
|
||||
class ShippingPointSelection(
|
||||
sequence_ordered(), MatchMixin, ModelSQL, ModelView):
|
||||
__name__ = 'stock.shipping.point.selection'
|
||||
|
||||
warehouse = fields.Many2One(
|
||||
'stock.location', "Warehouse", required=True,
|
||||
domain=[
|
||||
('type', '=', 'warehouse'),
|
||||
])
|
||||
shipping_point = fields.Many2One(
|
||||
'stock.shipping.point', "Shipping Point", required=True,
|
||||
domain=[
|
||||
('warehouse', '=', Eval('warehouse', -1)),
|
||||
])
|
||||
|
||||
delivery_country = fields.Many2One(
|
||||
'country.country', "Country", ondelete='CASCADE',
|
||||
help="Apply only when delivering to this country.\n"
|
||||
"Leave empty for any country.")
|
||||
contains_product_categories = fields.Many2Many(
|
||||
'stock.shipping.point.selection-contains-product.category',
|
||||
'selection', 'category', "Contains Product Categories",
|
||||
help="Apply only when at least one product shipped "
|
||||
"is in one of these categories.\n"
|
||||
"Leave empty for any product category.")
|
||||
|
||||
@classmethod
|
||||
def default_warehouse(cls):
|
||||
Location = Pool().get('stock.location')
|
||||
return Location.get_default_warehouse()
|
||||
|
||||
@classmethod
|
||||
def get_shipping_point(cls, shipment, pattern=None):
|
||||
pattern = pattern.copy() if pattern else {}
|
||||
pattern.update(shipment.shipping_point_pattern())
|
||||
selections = cls.search([
|
||||
('warehouse', '=', shipment.warehouse),
|
||||
])
|
||||
for selection in selections:
|
||||
if selection.match(pattern):
|
||||
return selection.shipping_point
|
||||
|
||||
def match(self, pattern, match_none=False):
|
||||
def parents(categories):
|
||||
for category in categories:
|
||||
while category:
|
||||
yield category
|
||||
category = category.parent
|
||||
|
||||
pattern = pattern.copy()
|
||||
products = pattern.pop('products', [])
|
||||
if self.contains_product_categories:
|
||||
categories = set()
|
||||
for product in set(products):
|
||||
categories.update(parents(product.categories_all))
|
||||
if not categories & set(self.contains_product_categories):
|
||||
return False
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
|
||||
class ShippingPointSelection_Contains_ProductCategory(ModelSQL):
|
||||
__name__ = 'stock.shipping.point.selection-contains-product.category'
|
||||
|
||||
selection = fields.Many2One(
|
||||
'stock.shipping.point.selection', "Selection",
|
||||
required=True, ondelete='CASCADE')
|
||||
category = fields.Many2One(
|
||||
'product.category', "Category", required=True, ondelete='CASCADE')
|
||||
|
||||
|
||||
class ShippingPointSelection_ProductClassification(metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipping.point.selection'
|
||||
|
||||
contains_product_classification = fields.Reference(
|
||||
"Contains Product Classification",
|
||||
selection='get_product_classifications',
|
||||
help="Apply only when at least one product shipped "
|
||||
"belongs to this classification.\n"
|
||||
"Leave empty for any product classification.")
|
||||
|
||||
@classmethod
|
||||
def get_product_classifications(cls):
|
||||
pool = Pool()
|
||||
Template = pool.get('product.template')
|
||||
return Template.get_classification()
|
||||
|
||||
def match(self, pattern, match_none=False):
|
||||
def parents(classification):
|
||||
while classification:
|
||||
yield classification
|
||||
classification = classification.parent
|
||||
|
||||
products = pattern.get('products', [])
|
||||
if self.contains_product_classification:
|
||||
classifications = set()
|
||||
for product in set(products):
|
||||
if hasattr(product.classification, 'parent'):
|
||||
classifications.update(parents(product.classification))
|
||||
else:
|
||||
classifications.add(product.classification)
|
||||
if self.contains_product_classification not in classifications:
|
||||
return False
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
|
||||
class ShippingPointSelection_ShipmentMeasurements(metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipping.point.selection'
|
||||
|
||||
min_weight = fields.Float(
|
||||
"Minimal Weight", digits='weight_uom',
|
||||
domain=[
|
||||
If(Eval('max_weight'),
|
||||
['OR',
|
||||
('min_weight', '=', None),
|
||||
('min_weight', '<=', Eval('max_weight', 0)),
|
||||
],
|
||||
[]),
|
||||
])
|
||||
max_weight = fields.Float(
|
||||
"Maximum Weight", digits='weight_uom',
|
||||
domain=[
|
||||
If(Eval('min_weight'),
|
||||
['OR',
|
||||
('max_weight', '=', None),
|
||||
('max_weight', '>=', Eval('min_weight', 0)),
|
||||
],
|
||||
[]),
|
||||
])
|
||||
weight_uom = fields.Many2One(
|
||||
'product.uom', "Weight UoM",
|
||||
domain=[('category', '=', Id('product', 'uom_cat_weight'))],
|
||||
states={
|
||||
'required': Eval('min_weight') | Eval('max_weight'),
|
||||
})
|
||||
|
||||
min_volume = fields.Float(
|
||||
"Minimal Volume", digits='volume_uom',
|
||||
domain=[
|
||||
If(Eval('max_volume'),
|
||||
['OR',
|
||||
('min_volume', '=', None),
|
||||
('min_volume', '<=', Eval('max_volume', 0)),
|
||||
],
|
||||
[]),
|
||||
])
|
||||
max_volume = fields.Float(
|
||||
"Maximal Volume", digits='volume_uom',
|
||||
domain=[
|
||||
If(Eval('min_volume'),
|
||||
['OR',
|
||||
('max_volume', '=', None),
|
||||
('max_volume', '>=', Eval('min_volume', 0)),
|
||||
],
|
||||
[]),
|
||||
])
|
||||
volume_uom = fields.Many2One(
|
||||
'product.uom', "Volume UoM",
|
||||
domain=[('category', '=', Id('product', 'uom_cat_volume'))],
|
||||
states={
|
||||
'required': Eval('min_volume') | Eval('max_volume'),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def get_shipping_point(cls, shipment, pattern=None):
|
||||
pool = Pool()
|
||||
UoM = pool.get('product.uom')
|
||||
ModelData = pool.get('ir.model.data')
|
||||
|
||||
kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
|
||||
liter = UoM(ModelData.get_id('product', 'uom_liter'))
|
||||
|
||||
pattern = pattern.copy() if pattern else {}
|
||||
pattern['weight'] = UoM.compute_qty(
|
||||
shipment.weight_uom, shipment.weight, kg, round=False)
|
||||
pattern['volume'] = UoM.compute_qty(
|
||||
shipment.volume_uom, shipment.volume, liter, round=False)
|
||||
return super().get_shipping_point(shipment, pattern=pattern)
|
||||
|
||||
def match(self, pattern, match_none=False):
|
||||
pool = Pool()
|
||||
UoM = pool.get('product.uom')
|
||||
ModelData = pool.get('ir.model.data')
|
||||
|
||||
kg = UoM(ModelData.get_id('product', 'uom_kilogram'))
|
||||
liter = UoM(ModelData.get_id('product', 'uom_liter'))
|
||||
|
||||
pattern = pattern.copy()
|
||||
|
||||
weight = pattern.pop('weight', 0)
|
||||
if self.weight_uom:
|
||||
weight = UoM.compute_qty(
|
||||
kg, weight, self.weight_uom, round=False)
|
||||
if self.min_weight and weight < self.min_weight:
|
||||
return False
|
||||
if self.max_weight and weight > self.max_weight:
|
||||
return False
|
||||
|
||||
volume = pattern.pop('volume', 0)
|
||||
if self.volume_uom:
|
||||
volume = UoM.compute_qty(
|
||||
liter, volume, self.volume_uom, round=False)
|
||||
if self.min_volume and volume < self.min_volume:
|
||||
return False
|
||||
if self.max_volume and volume > self.max_volume:
|
||||
return False
|
||||
|
||||
return super().match(pattern, match_none=match_none)
|
||||
|
||||
|
||||
class ShippingPointMixin:
|
||||
__slots__ = ()
|
||||
|
||||
shipping_point = fields.Many2One(
|
||||
'stock.shipping.point', "Shipping Point",
|
||||
domain=[
|
||||
('warehouse', '=', Eval('warehouse', -1)),
|
||||
],
|
||||
states={
|
||||
'readonly': Eval('state') != 'draft',
|
||||
})
|
||||
|
||||
|
||||
class ShippingPointAssignMixin(ShippingPointMixin):
|
||||
__slots__ = ()
|
||||
|
||||
def shipping_point_pattern(self):
|
||||
pattern = {}
|
||||
if getattr(self, 'delivery_address', None):
|
||||
pattern['delivery_country'] = self.delivery_address.country
|
||||
pattern['products'] = {m.product for m in self.moves}
|
||||
return pattern
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('draft')
|
||||
def draft(cls, shipments):
|
||||
super().draft(shipments)
|
||||
cls.write(shipments, {'shipping_point': None})
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('waiting')
|
||||
def wait(cls, shipments, moves=None):
|
||||
pool = Pool()
|
||||
Selection = pool.get('stock.shipping.point.selection')
|
||||
|
||||
super().wait(shipments, moves=moves)
|
||||
|
||||
for shipment in shipments:
|
||||
if not shipment.shipping_point:
|
||||
shipment.shipping_point = Selection.get_shipping_point(
|
||||
shipment)
|
||||
cls.save(shipments)
|
||||
|
||||
|
||||
class ShipmentIn(ShippingPointMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.in'
|
||||
|
||||
|
||||
class ShipmentInReturn(ShippingPointAssignMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.in.return'
|
||||
|
||||
|
||||
class ShipmentOut(ShippingPointAssignMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.out'
|
||||
|
||||
|
||||
class ShipmentOutReturn(ShippingPointMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.out.return'
|
||||
|
||||
|
||||
class ShipmentInternal(ShippingPointAssignMixin, metaclass=PoolMeta):
|
||||
__name__ = 'stock.shipment.internal'
|
||||
|
||||
incoming_shipping_point = fields.Many2One(
|
||||
'stock.shipping.point', "Incoming Shipping Point",
|
||||
domain=[
|
||||
('warehouse', '=', Eval('to_warehouse', -1)),
|
||||
],
|
||||
states={
|
||||
'readonly': Eval('state') != 'shipped',
|
||||
'invisible': ~Eval('transit_location'),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls.shipping_point.string = "Outgoing Shipping Point"
|
||||
cls.shipping_point.states['invisible'] = ~Eval('transit_location')
|
||||
|
||||
def shipping_point_pattern(self):
|
||||
pattern = super().shipping_point_pattern()
|
||||
if self.to_warehouse and self.to_warehouse.address:
|
||||
pattern['delivery_country'] = self.to_warehouse.address.country
|
||||
return pattern
|
||||
Reference in New Issue
Block a user