diff --git a/CHANGELOG b/CHANGELOG
index 4fa3ac62bd..a8705536fd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Support warehouse pickup
* Allow resetting packed customer shipments
* Allow grouping by date products by location
* Ignore consumable products when inactivating locations
diff --git a/__init__.py b/__init__.py
index 29d141aea9..32260e7c5b 100644
--- a/__init__.py
+++ b/__init__.py
@@ -15,8 +15,6 @@ def register():
Pool.register(
location.Location,
location.WarehouseWasteLocation,
- location.Party,
- location.PartyLocation,
location.ProductsByLocationsContext,
location.ProductsByLocations,
location.LocationLeadTime,
@@ -27,6 +25,9 @@ def register():
shipment.ShipmentOutReturn,
shipment.ShipmentInternal,
shipment.AssignPartial,
+ party.Party,
+ party.PartyLocation,
+ party.PartyDeliveredToWarehouse,
party.Address,
party.ContactMechanism,
period.Period,
diff --git a/delivery_note.fodt b/delivery_note.fodt
index 518a683a7b..cb232e162e 100644
--- a/delivery_note.fodt
+++ b/delivery_note.fodt
@@ -1,128 +1,137 @@
-
- LibreOffice/6.4.6.2$Linux_X86_64 LibreOffice_project/40$Build-22008-06-07T15:29:022009-03-26T18:33:131PT0S
+
+ LibreOffice/7.3.3.2$Linux_X86_64 LibreOffice_project/30$Build-22008-06-07T15:29:022009-03-26T18:33:131PT0S
- 28998
+ 0
0
- 26301
- 24026
+ 48129
+ 23021
true
false
view2
- 4651
- 35512
+ 37596
+ 8964
0
- 28998
- 26300
- 53023
+ 0
+ 48128
+ 23019
0
0
false
100
false
+ false
false
false
- true
- true
- true
- 0
- true
- true
-
- false
- false
- false
- false
- false
- false
false
- false
+ false
+ true
+ true
+ true
+ true
+ true
+ false
+ 0
+ false
+ false
+ false
+ false
+ false
+ true
+ false
+ false
+ false
+ false
+ false
+ true
true
true
- false
false
+ false
false
- false
- false
false
false
false
- false
- false
- true
- false
- true
- true
- 1813180
- false
-
- false
- true
- false
- false
- false
- true
+ false
+ true
false
- false
- false
+ false
true
- 2809848
-
+ false
+ false
+ false
+ true
+ 0
+ 1
+ true
+ false
+
+ high-resolution
+ false
+
+
+ false
+ false
true
+ false
+ true
+ true
+ false
+ true
+
+ true
+ 2929175
+
+ true
+ false
+ true
+ true
+ 0
+
+ false
+ false
+ false
+ false
+ false
+ true
+ false
+ false
+ false
+ false
+ true
+ false
+ false
+
+ false
false
true
- false
- false
- true
- true
- false
- true
- false
- false
- false
- 0
- false
- true
- high-resolution
- false
- false
- false
- false
- false
- false
- true
- true
- true
-
- true
- false
- false
- true
- false
- false
- false
-
- false
- false
- false
- 1
- true
- false
false
- 0
+ false
+ false
false
false
-
-
- false
+ false
+ false
+ false
+ false
+ 1813180
+ false
+ false
+ false
+ false
+ false
+ false
+ true
+ true
+ false
@@ -131,15 +140,15 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -147,11 +156,11 @@
-
+
-
+
@@ -160,21 +169,21 @@
-
+
-
-
+
+
-
-
+
+
-
+
@@ -187,7 +196,7 @@
-
+
@@ -229,51 +238,51 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -332,67 +341,63 @@
-
-
-
-
-
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
+
-
+
+
+
+
+
-
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
@@ -401,7 +406,7 @@
-
+
@@ -411,16 +416,19 @@
+
+
+
-
+
<if test="company and company.header">
<for each="line in company.header.split('\n')">
<line>
</for>
</if>
- <company.rec_name if company else ''>
+ <company.rec_name if company else ''>
<if test="company and company.footer">
@@ -443,19 +451,20 @@
<for each="shipment in records">
-
- <replace text:p="set_lang(shipment.delivery_address.party.lang)">
- <replace text:p="shipment.set_lang(shipment.delivery_address.party.lang)">
- <for each="line in shipment.delivery_address.full_address.split('\n')">
+
+ <replace text:p="set_lang(shipment.delivery_address.party.lang)">
+ <replace text:p="shipment.set_lang(shipment.delivery_address.party.lang)">
+ <shipment.customer.full_name>
+ <for each="line in shipment.delivery_address.full_address.split('\n')">
<line>
</for>
- Delivery Note
- Shipment Number: <shipment.number>Customer Code: <shipment.customer.code>Reference: <shipment.origins or ''><', ' if (shipment.origins and shipment.reference) else ''><shipment.reference or ''>Date: <format_date(shipment.effective_date and shipment.effective_date or datetime.datetime.today(), shipment.delivery_address.party.lang)>
+ Delivery Note
+ Shipment Number: <shipment.number>Customer Code: <shipment.customer.code>Reference: <shipment.origins or ''><', ' if (shipment.origins and shipment.reference) else ''><shipment.reference or ''>Date: <format_date(shipment.effective_date and shipment.effective_date or datetime.datetime.today(), shipment.delivery_address.party.lang)>
-
+
Product
@@ -464,13 +473,13 @@
-
+
<for each="move in shipment.outgoing_moves">
-
+
<move.product.rec_name>
@@ -478,7 +487,7 @@
<format_number_symbol(move.quantity, shipment.delivery_address.party.lang, move.uom, digits=move.uom.digits)>
-
+
</for>
@@ -488,4 +497,4 @@
</for>
-
+
\ No newline at end of file
diff --git a/location.py b/location.py
index f92b9ee318..d372e66d25 100644
--- a/location.py
+++ b/location.py
@@ -6,17 +6,15 @@ from decimal import Decimal
from sql import Column
-from trytond import backend
from trytond.cache import Cache
from trytond.i18n import gettext
from trytond.model import (
- DeactivableMixin, MatchMixin, Model, ModelSQL, ModelView, ValueMixin,
- fields, sequence_ordered, tree)
+ DeactivableMixin, MatchMixin, Model, ModelSQL, ModelView, fields,
+ sequence_ordered, tree)
from trytond.modules.product import price_digits, round_price
-from trytond.pool import Pool, PoolMeta
+from trytond.pool import Pool
from trytond.pyson import Eval, If
from trytond.tools import grouped_slice
-from trytond.tools.multivalue import migrate_property
from trytond.transaction import Transaction
from .exceptions import LocationValidationError
@@ -148,6 +146,14 @@ class Location(DeactivableMixin, tree(), ModelSQL, ModelView):
],
help="The warehouses that use the location for waste products.")
+ allow_pickup = fields.Boolean(
+ "Allow Pickup",
+ states={
+ 'invisible': (
+ (Eval('type') != 'warehouse')
+ & ~Eval('address')),
+ })
+
quantity = fields.Function(
fields.Float(
"Quantity", digits=(16, Eval('quantity_uom_digits', 2)),
@@ -636,83 +642,6 @@ class Location(DeactivableMixin, tree(), ModelSQL, ModelView):
]
-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):
- exist = backend.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'
diff --git a/location.xml b/location.xml
index 5c91efe40c..92b8272ed6 100644
--- a/location.xml
+++ b/location.xml
@@ -126,13 +126,6 @@ this repository contains the full copyright notices and license terms. -->
-
- party.party
-
- party_form
-
-
-
stock.location.lead_time
tree
diff --git a/party.py b/party.py
index 71d05602aa..74c1164aef 100644
--- a/party.py
+++ b/party.py
@@ -1,9 +1,122 @@
# 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 import backend
from trytond.i18n import gettext
-from trytond.model import fields
+from trytond.model import ModelSQL, ValueMixin, fields
from trytond.modules.party.exceptions import EraseError
from trytond.pool import Pool, PoolMeta
+from trytond.tools.multivalue import migrate_property
+from trytond.transaction import Transaction
+
+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")
+ delivered_to_warehouses = fields.Many2Many(
+ 'party.party-delivered_to-stock.location', 'party', 'location',
+ "Delivered to Warehouses",
+ domain=[
+ ('type', '=', 'warehouse'),
+ ],
+ filter=[
+ ('allow_pickup', '=', True),
+ ])
+
+ @classmethod
+ def multivalue_model(cls, field):
+ pool = Pool()
+ if field in {'supplier_location', 'customer_location'}:
+ return pool.get('party.party.location')
+ return super().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()
+
+ def address_get(self, type=None):
+ pool = Pool()
+ Location = pool.get('stock.location')
+ context = Transaction().context
+ address = super().address_get(type=type)
+ if (type == 'delivery'
+ and context.get('warehouse')):
+ warehouse = Location(context['warehouse'])
+ if warehouse in self.delivered_to_warehouses:
+ address = warehouse.address
+ return address
+
+
+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):
+ exist = backend.TableHandler.table_exist(cls._table)
+
+ super().__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 PartyDeliveredToWarehouse(ModelSQL):
+ "Party Delivered to Warehouse"
+ __name__ = 'party.party-delivered_to-stock.location'
+
+ party = fields.Many2One('party.party', "Party", required=True)
+ location = fields.Many2One(
+ 'stock.location', "Location", required=True, ondelete='CASCADE',
+ domain=[
+ ('type', '=', 'warehouse'),
+ ])
class Address(metaclass=PoolMeta):
@@ -11,6 +124,8 @@ class Address(metaclass=PoolMeta):
delivery = fields.Boolean(
'Delivery',
help="Check to send deliveries to the address.")
+ warehouses = fields.One2Many(
+ 'stock.location', 'address', "Warehouses", readonly=True)
class ContactMechanism(metaclass=PoolMeta):
diff --git a/party.xml b/party.xml
index db51349d02..bb6ea67584 100644
--- a/party.xml
+++ b/party.xml
@@ -3,6 +3,12 @@
this repository contains the full copyright notices and license terms. -->
+
+ party.party
+
+ party_form
+
+
party.address
diff --git a/shipment.py b/shipment.py
index 98171a351f..4f17c33e9a 100644
--- a/shipment.py
+++ b/shipment.py
@@ -601,8 +601,14 @@ class ShipmentInReturn(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
states={
'readonly': Eval('state') != 'draft',
},
- domain=[
- ('party', '=', Eval('supplier'))
+ domain=['OR',
+ ('party', '=', Eval('supplier')),
+ ('warehouses', 'where', [
+ ('id', '=', Eval('warehouse', -1)),
+ If(Eval('state') == 'draft',
+ ('allow_pickup', '=', True),
+ ()),
+ ]),
],
help="Where the stock is sent to.")
from_location = fields.Many2One('stock.location', "From Location",
@@ -615,6 +621,9 @@ class ShipmentInReturn(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
'readonly': (Eval('state') != 'draft') | Eval('moves', [0]),
}, domain=[('type', '=', 'supplier')],
help="Where the stock is moved to.")
+ warehouse = fields.Function(
+ fields.Many2One('stock.location', "Warehouse"),
+ 'on_change_with_warehouse')
moves = fields.One2Many('stock.move', 'shipment', 'Moves',
states={
'readonly': (((Eval('state') != 'draft') | ~Eval('from_location'))
@@ -734,6 +743,11 @@ class ShipmentInReturn(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
self.delivery_address = self.supplier.address_get('delivery')
self.to_location = self.supplier.supplier_location
+ @fields.depends('from_location')
+ def on_change_with_warehouse(self, name=None):
+ if self.from_location and self.from_location.warehouse:
+ return self.from_location.warehouse.id
+
@property
def _move_planned_date(self):
'''
@@ -955,7 +969,16 @@ class ShipmentOut(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
'Delivery Address', required=True,
states={
'readonly': Eval('state') != 'draft',
- }, domain=[('party', '=', Eval('customer'))],
+ },
+ domain=['OR',
+ ('party', '=', Eval('customer')),
+ ('warehouses', 'where', [
+ ('id', '=', Eval('warehouse', -1)),
+ If(Eval('state') == 'draft',
+ ('allow_pickup', '=', True),
+ ()),
+ ]),
+ ],
help="Where the stock is sent to.")
reference = fields.Char("Reference", size=None, select=True,
states={
@@ -1153,11 +1176,14 @@ class ShipmentOut(ShipmentAssignMixin, Workflow, ModelSQL, ModelView):
def default_company():
return Transaction().context.get('company')
- @fields.depends('customer')
+ @fields.depends('customer', 'warehouse')
def on_change_customer(self):
self.delivery_address = None
if self.customer:
- self.delivery_address = self.customer.address_get(type='delivery')
+ with Transaction().set_context(
+ warehouse=self.warehouse.id if self.warehouse else None):
+ self.delivery_address = self.customer.address_get(
+ type='delivery')
@fields.depends('customer')
def on_change_with_customer_location(self, name=None):
@@ -1611,8 +1637,8 @@ class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
help="The party that purchased the stock.")
customer_location = fields.Function(fields.Many2One('stock.location',
'Customer Location'), 'on_change_with_customer_location')
- delivery_address = fields.Many2One('party.address',
- 'Delivery Address', required=True,
+ contact_address = fields.Many2One(
+ 'party.address', "Contact Address",
states={
'readonly': Eval('state') != 'draft',
}, domain=[('party', '=', Eval('customer'))],
@@ -1729,6 +1755,9 @@ class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
if table.column_exist('code'):
table.column_rename('code', 'number')
+ # Migration from 6.4: rename delivery_address to contact_address
+ table.column_rename('delivery_address', 'contact_address')
+
super(ShipmentOutReturn, cls).__register__(module_name)
# Migration from 5.6: rename state cancel to cancelled
@@ -1736,6 +1765,9 @@ class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
[sql_table.state], ['cancelled'],
where=sql_table.state == 'cancel'))
+ # Migration from 6.4: remove required on contact_address
+ table.not_null_action('contact_address', 'remove')
+
@classmethod
def order_effective_date(cls, tables):
table, _ = tables[None]
@@ -1756,9 +1788,9 @@ class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView):
@fields.depends('customer')
def on_change_customer(self):
- self.delivery_address = None
+ self.contact_address = None
if self.customer:
- self.delivery_address = self.customer.address_get(type='delivery')
+ self.contact_address = self.customer.address_get()
@fields.depends('customer')
def on_change_with_customer_location(self, name=None):
@@ -2736,7 +2768,7 @@ class DeliveryNote(CompanyReport):
@classmethod
def execute(cls, ids, data):
- with Transaction().set_context(address_with_party=True):
+ with Transaction().set_context(address_with_party=False):
return super(DeliveryNote, cls).execute(ids, data)
diff --git a/view/location_form.xml b/view/location_form.xml
index 16685ac0a7..7fe2d77209 100644
--- a/view/location_form.xml
+++ b/view/location_form.xml
@@ -17,7 +17,9 @@ this repository contains the full copyright notices and license terms. -->
-
+
+
+
diff --git a/view/party_form.xml b/view/party_form.xml
index a1c7922ee5..104b7bf230 100644
--- a/view/party_form.xml
+++ b/view/party_form.xml
@@ -12,6 +12,9 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
diff --git a/view/shipment_out_return_form.xml b/view/shipment_out_return_form.xml
index 65caef688d..6dadcfb7bd 100644
--- a/view/shipment_out_return_form.xml
+++ b/view/shipment_out_return_form.xml
@@ -8,8 +8,8 @@ this repository contains the full copyright notices and license terms. -->
-
-
+
+
diff --git a/view/shipment_out_return_tree.xml b/view/shipment_out_return_tree.xml
index ca964f77be..398af675bf 100644
--- a/view/shipment_out_return_tree.xml
+++ b/view/shipment_out_return_tree.xml
@@ -9,6 +9,6 @@ this repository contains the full copyright notices and license terms. -->
-
+