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/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. -->