diff --git a/CHANGELOG b/CHANGELOG
index 58b5c1ddf8..319442d101 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+* Split moves into supplier and customer moves
+
Version 3.6.0 - 2015-04-20
* Bug fixes (see mercurial logs for details)
* Add support for PyPy
diff --git a/__init__.py b/__init__.py
index 94aaef5fa5..59e356c638 100644
--- a/__init__.py
+++ b/__init__.py
@@ -12,9 +12,11 @@ def register():
Configuration,
ShipmentDrop,
Move,
+ SaleConfig,
Sale,
SaleLine,
PurchaseRequest,
+ PurchaseConfig,
Purchase,
PurchaseLine,
ProductSupplier,
@@ -22,5 +24,4 @@ def register():
Pool.register(
CreatePurchase,
PurchaseHandleShipmentException,
- SaleHandleShipmentException,
module='sale_supply_drop_shipment', type_='wizard')
diff --git a/doc/index.rst b/doc/index.rst
index b88091e529..833332796a 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -22,7 +22,12 @@ The drop shipment can be in one of this states:
* Waiting
- All moves are in draft but the shipment is in readonly.
+ All moves are in draft, the synchronization between the moves of the supplier
+ and the moves to the customer has occured.
+
+* Shipped
+
+ All moves from the supplier are done.
* Done
diff --git a/purchase.py b/purchase.py
index c95fd52ed6..6092e138a3 100644
--- a/purchase.py
+++ b/purchase.py
@@ -1,13 +1,14 @@
# 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 fields
+from trytond.model import fields, ModelView, Workflow
from trytond.pyson import Eval
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
+from trytond.tools import grouped_slice
-__all__ = ['PurchaseRequest', 'Purchase', 'PurchaseLine', 'ProductSupplier',
- 'CreatePurchase', 'PurchaseHandleShipmentException']
+__all__ = ['PurchaseRequest', 'PurchaseConfig', 'Purchase', 'PurchaseLine',
+ 'ProductSupplier', 'CreatePurchase', 'PurchaseHandleShipmentException']
__metaclass__ = PoolMeta
@@ -27,6 +28,14 @@ class PurchaseRequest:
depends=['customer', 'state'])
+class PurchaseConfig:
+ __name__ = 'purchase.configuration'
+
+ purchase_drop_location = fields.Property(
+ fields.Many2One('stock.location', 'Purchase Drop Location',
+ domain=[('type', '=', 'drop')]))
+
+
class Purchase:
__name__ = 'purchase.purchase'
@@ -48,6 +57,13 @@ class Purchase:
},
depends=['customer']),
'get_drop_shipments')
+ drop_location = fields.Many2One('stock.location', 'Drop Location',
+ domain=[('type', '=', 'drop')],
+ states={
+ 'invisible': ~Eval('customer', False),
+ 'required': Eval('customer', False),
+ },
+ depends=['customer'])
@classmethod
def __setup__(cls):
@@ -57,6 +73,14 @@ class Purchase:
'defined for quotation of purchase "%s".'),
})
+ @staticmethod
+ def default_drop_location():
+ pool = Pool()
+ PurchaseConfig = pool.get('purchase.configuration')
+
+ config = PurchaseConfig(1)
+ return config.purchase_drop_location.id
+
@fields.depends('customer', 'delivery_address')
def on_change_customer(self):
self.delivery_address = None
@@ -73,6 +97,49 @@ class Purchase:
if self.customer and not self.delivery_address:
self.raise_user_error('delivery_address_required', self.rec_name)
+ def create_move(self, move_type):
+ pool = Pool()
+ DropShipment = pool.get('stock.shipment.drop')
+
+ moves = super(Purchase, self).create_move(move_type)
+ if moves and self.customer and move_type == 'in':
+ drop_shipment = self.create_drop_shipment()
+ drop_shipment.supplier_moves = moves
+ drop_shipment.save()
+ DropShipment.wait([drop_shipment])
+
+ return moves
+
+ def create_drop_shipment(self):
+ pool = Pool()
+ DropShipment = pool.get('stock.shipment.drop')
+
+ return DropShipment(
+ company=self.company,
+ supplier=self.party,
+ contact_address=self.party.address_get(),
+ customer=self.customer,
+ delivery_address=self.delivery_address,
+ )
+
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('cancel')
+ def cancel(cls, purchases):
+ pool = Pool()
+ SaleLine = pool.get('sale.line')
+ Move = pool.get('stock.move')
+
+ super(Purchase, cls).cancel(purchases)
+ sale_lines = []
+ for sub_purchases in grouped_slice(purchases):
+ sale_lines += SaleLine.search([
+ ('purchase_request.purchase_line.purchase', 'in',
+ [p.id for p in sub_purchases]),
+ ])
+ Move.cancel([m for sl in sale_lines for m in sl.moves
+ if m.state != 'done' and m.from_location.type == 'drop'])
+
class PurchaseLine:
__name__ = 'purchase.line'
@@ -80,7 +147,7 @@ class PurchaseLine:
def get_to_location(self, name):
result = super(PurchaseLine, self).get_to_location(name)
if self.purchase.customer:
- return self.purchase.customer.customer_location.id
+ return self.purchase.drop_location.id
return result
def get_move(self):
@@ -122,36 +189,44 @@ class PurchaseHandleShipmentException:
pool = Pool()
Sale = pool.get('sale.sale')
SaleLine = pool.get('sale.line')
+ Purchase = pool.get('purchase.purchase')
+ PurchaseRequest = pool.get('purchase.request')
+ Move = pool.get('stock.move')
super(PurchaseHandleShipmentException, self).transition_handle()
- to_recreate = self.ask.recreate_moves
- domain_moves = self.ask.domain_moves
- sales, saleline_write = set(), []
- sale_lines = SaleLine.search([
- ('purchase_request.purchase_line.purchase', '=',
- Transaction().context['active_id']),
- ])
- saleline_write = []
- for sale_line in sale_lines:
- moves_ignored = []
- moves_recreated = []
- skip = set(sale_line.moves_ignored)
- skip.update(sale_line.moves_recreated)
- for move in sale_line.moves:
- if move not in domain_moves or move in skip:
- continue
- if move in to_recreate:
- moves_recreated.append(move.id)
- else:
- moves_ignored.append(move.id)
- sales.add(sale_line.sale)
- saleline_write.append([sale_line])
- saleline_write.append({
- 'moves_ignored': [('add', moves_ignored)],
- 'moves_recreated': [('add', moves_recreated)],
- })
+ sales = set()
+ moves = set()
+ to_recreate = set(self.ask.recreate_moves)
+ domain_moves = set(self.ask.domain_moves)
+ purchase = Purchase(Transaction().context['active_id'])
- SaleLine.write(*saleline_write)
+ requests = []
+ for sub_lines in grouped_slice([pl.id for pl in purchase.lines]):
+ requests += PurchaseRequest.search([
+ ('purchase_line', 'in', list(sub_lines)),
+ ])
+ pline2request = {r.purchase_line: r for r in requests}
+ request2sline = {}
+ for sub_requests in grouped_slice(requests):
+ sale_lines = SaleLine.search([
+ ('purchase_request', 'in', [r.id for r in sub_requests]),
+ ])
+ request2sline.update({sl.purchase_request: sl
+ for sl in sale_lines})
+
+ for line in purchase.lines:
+ if not set(line.moves) & domain_moves:
+ continue
+ if not any(m in to_recreate for m in line.moves):
+ sale_line = request2sline[pline2request[line]]
+ moves.update({m for m in sale_line.moves
+ if (m.state != 'done'
+ and m.from_location.type == 'drop')})
+ sales.add(sale_line.sale)
+
+ if moves:
+ Move.cancel(list(moves))
+ if sales:
Sale.process(list(sales))
return 'end'
diff --git a/purchase.xml b/purchase.xml
index 7f8eda9c6a..0c6c05e60b 100644
--- a/purchase.xml
+++ b/purchase.xml
@@ -8,6 +8,12 @@ this repository contains the full copyright notices and license terms. -->
purchase_form
+
+ purchase.configuration
+
+ purchase_configuration_form
+
Drop Shipments
diff --git a/sale.py b/sale.py
index 276a0adf4b..4fad8a1126 100644
--- a/sale.py
+++ b/sale.py
@@ -1,17 +1,22 @@
# 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 itertools import groupby, chain
-from functools import partial
from trytond.model import fields
from trytond.pyson import Eval
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
-from trytond.const import OPERATORS
-__all__ = ['Sale', 'SaleLine', 'SaleHandleShipmentException']
+__all__ = ['SaleConfig', 'Sale', 'SaleLine']
__metaclass__ = PoolMeta
+class SaleConfig:
+ __name__ = 'sale.configuration'
+
+ sale_drop_location = fields.Property(
+ fields.Many2One('stock.location', 'Sale Drop Location',
+ domain=[('type', '=', 'drop')]))
+
+
class Sale:
__name__ = 'sale.sale'
@@ -21,6 +26,16 @@ class Sale:
'invisible': ~Eval('drop_shipments'),
}),
'get_drop_shipments')
+ drop_location = fields.Many2One('stock.location', 'Drop Location',
+ domain=[('type', '=', 'drop')])
+
+ @staticmethod
+ def default_drop_location():
+ pool = Pool()
+ Config = pool.get('sale.configuration')
+
+ config = Config(1)
+ return config.sale_drop_location.id
def get_drop_shipments(self, name):
DropShipment = Pool().get('stock.shipment.drop')
@@ -30,64 +45,34 @@ class Sale:
def create_shipment(self, shipment_type):
shipments = super(Sale, self).create_shipment(shipment_type)
if shipment_type == 'out':
- self.create_drop_shipment()
+ with Transaction().set_context(_drop_shipment=True):
+ self.create_drop_shipment_moves()
return shipments
- def _get_drop_move_sale_line(self):
- '''
- Return a line of moves
- '''
+ def create_drop_shipment_moves(self):
+ pool = Pool()
+ Move = pool.get('stock.move')
+
moves = []
for line in self.lines:
moves += line.get_drop_moves()
- return moves
-
- def _group_drop_shipment_key(self, moves, move):
- '''
- The key to group moves by shipments
- '''
- planned_date = max(m.planned_date for m in moves)
- return (
- ('planned_date', planned_date),
- ('supplier', move.purchase.party),
- )
-
- def create_drop_shipment(self):
- '''
- Create a drop shipment for the sale
- '''
- pool = Pool()
- Shipment = pool.get('stock.shipment.drop')
-
- moves = self._get_drop_move_sale_line()
- if not moves:
- return []
- keyfunc = partial(self._group_drop_shipment_key, moves)
- moves = sorted(moves, key=keyfunc)
-
- shipments = []
- for key, grouped_moves in groupby(moves, key=keyfunc):
- values = {
- 'customer': self.party.id,
- 'delivery_address': self.shipment_address.id,
- 'reference': self.reference,
- 'company': self.company.id,
- }
- values.update(dict(key))
- shipment = Shipment(**values)
- shipment.moves = [m for m in grouped_moves]
- shipment.save()
- shipments.append(shipment)
- Shipment.wait(shipments)
- return shipments
+ Move.save(moves)
class SaleLine:
__name__ = 'sale.line'
+ @property
+ def supply_on_sale(self):
+ supply_on_sale = super(SaleLine, self).supply_on_sale
+ return (supply_on_sale
+ or all(m.from_location.type == 'drop'
+ for m in self.moves))
+
def get_move(self, shipment_type):
result = super(SaleLine, self).get_move(shipment_type)
if (shipment_type == 'out'
+ and not Transaction().context.get('_drop_shipment')
and self.supply_on_sale):
if (self.purchase_request and self.purchase_request.customer
and self.purchase_request_state != 'cancel'):
@@ -116,112 +101,9 @@ class SaleLine:
return []
moves = []
if self.purchase_request and self.purchase_request.customer:
- if self.purchase_request.purchase_line:
- moves = [m
- for m in self.purchase_request.purchase_line.moves
- if m.state == 'draft' and not m.shipment]
+ move = self.get_move('out')
+ if move is not None:
+ move.from_location = self.sale.drop_location
+ move.cost_price = move.unit_price
+ moves.append(move)
return moves
-
- @classmethod
- def read(cls, ids, fields_names=None):
- # Add moves from purchase_request as they can have only one origin
- PurchaseRequest = Pool().get('purchase.request')
- added = False
- if 'moves' in fields_names or []:
- if 'purchase_request' not in fields_names:
- fields_names = fields_names[:]
- fields_names.append('purchase_request')
- added = True
- values = super(SaleLine, cls).read(ids, fields_names=fields_names)
- if 'moves' in fields_names or []:
- with Transaction().set_context(_check_access=False):
- purchase_requests = PurchaseRequest.browse(
- list(set(v['purchase_request']
- for v in values if v['purchase_request'])))
- id2purchase_requests = dict((p.id, p)
- for p in purchase_requests)
- for value in values:
- if value['purchase_request']:
- purchase_request = id2purchase_requests[
- value['purchase_request']]
- if (purchase_request.customer
- and purchase_request.purchase_line):
- move_ids = tuple(m.id
- for m in purchase_request.purchase_line.moves)
- if value['moves'] is None:
- value['moves'] = move_ids
- else:
- value['moves'] += move_ids
- if added:
- del value['purchase_request']
- return values
-
- @classmethod
- def search(cls, domain, *args, **kwargs):
- def process(domain):
- domain = domain[:]
- i = 0
- while i < len(domain):
- arg = domain[i]
- if (isinstance(arg, tuple)
- or (isinstance(arg, list)
- and len(arg) > 2
- and arg[1] in OPERATORS)):
- if arg[0] == 'moves':
- domain[i] = ['OR',
- arg,
- [
- ('purchase_request.purchase_line.moves',) +
- tuple(arg[1:]),
- ('purchase_request.customer', '!=', None),
- ]
- ]
- elif isinstance(arg, list):
- domain[i] = process(arg)
- i += 1
- return domain
- return super(SaleLine, cls).search(process(domain), *args, **kwargs)
-
-
-class SaleHandleShipmentException:
- __name__ = 'sale.handle.shipment.exception'
-
- def transition_handle(self):
- pool = Pool()
- Purchase = pool.get('purchase.purchase')
- PurchaseLine = pool.get('purchase.line')
- Sale = pool.get('sale.sale')
-
- super(SaleHandleShipmentException, self).transition_handle()
-
- to_recreate = self.ask.recreate_moves
- domain_moves = self.ask.domain_moves
- purchases, purchaseline_write = set(), []
- sale = Sale(Transaction().context['active_id'])
- for line in sale.lines:
- if not line.purchase_request:
- continue
- purchase_line = line.purchase_request.purchase_line
- if not purchase_line:
- continue
-
- moves_ignored = []
- moves_recreated = []
- skip = set(purchase_line.moves_ignored)
- skip.update(purchase_line.moves_recreated)
- for move in purchase_line.moves:
- if move not in domain_moves or move in skip:
- continue
- if move in to_recreate:
- moves_recreated.append(move.id)
- else:
- moves_ignored.append(move.id)
- purchases.add(purchase_line.purchase)
- purchaseline_write.append(([purchase_line], {
- 'moves_ignored': [('add', moves_ignored)],
- 'moves_recreated': [('add', moves_recreated)],
- }))
-
- PurchaseLine.write(*chain(*purchaseline_write))
- Purchase.process(list(purchases))
- return 'end'
diff --git a/sale.xml b/sale.xml
index 8728d0b373..9a01f2298b 100644
--- a/sale.xml
+++ b/sale.xml
@@ -9,6 +9,12 @@ this repository contains the full copyright notices and license terms. -->
sale_form
+
+ sale.configuration
+
+ sale_configuration_form
+
+
Drop Shipments
stock.shipment.drop
diff --git a/stock.py b/stock.py
index 4dde6fb656..35f2d5305a 100644
--- a/stock.py
+++ b/stock.py
@@ -1,9 +1,15 @@
# 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 collections import defaultdict
+
+from sql.operators import Concat
+from sql.aggregate import Count
+
from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.pyson import Eval, If
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
+from trytond.tools import grouped_slice
__all__ = ['Configuration', 'ShipmentDrop', 'Move']
@@ -44,7 +50,8 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
}, depends=['state'])
supplier = fields.Many2One('party.party', 'Supplier', required=True,
states={
- 'readonly': (((Eval('state') != 'draft') | Eval('moves', [0]))
+ 'readonly': (((Eval('state') != 'draft')
+ | Eval('supplier_moves', [0]))
& Eval('supplier')),
},
depends=['state', 'supplier'])
@@ -56,7 +63,8 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
depends=['state', 'supplier'])
customer = fields.Many2One('party.party', 'Customer', required=True,
states={
- 'readonly': (((Eval('state') != 'draft') | Eval('moves', [0]))
+ 'readonly': (((Eval('state') != 'draft')
+ | Eval('customer_moves', [0]))
& Eval('customer')),
},
depends=['state'])
@@ -68,29 +76,101 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
domain=[('party', '=', Eval('customer'))],
depends=['state', 'customer'])
moves = fields.One2Many('stock.move', 'shipment', 'Moves',
- add_remove=[
- ('shipment', '=', None),
- ('state', '=', 'draft'),
- ('supplier', '=', Eval('supplier')),
- ('customer_drop', '=', Eval('customer')),
- ],
domain=[
('company', '=', Eval('company')),
- ('from_location.type', '=', 'supplier'),
- ('to_location.type', '=', 'customer'),
+ ['OR',
+ [
+ ('from_location.type', '=', 'supplier'),
+ ('to_location.type', '=', 'drop'),
+ ],
+ [
+ ('from_location.type', '=', 'drop'),
+ ('to_location.type', '=', 'customer'),
+ ],
+ ],
],
- states={
- 'readonly': Eval('state').in_(['waiting', 'done', 'cancel']),
- },
- depends=['state', 'company', 'supplier', 'customer'])
+ depends=['company'], readonly=True)
+ supplier_moves = fields.Function(fields.One2Many('stock.move', None,
+ 'Supplier Moves',
+ states={
+ 'readonly': Eval('state').in_(['shipped', 'done', 'cancel']),
+ },
+ depends=['state', 'supplier']), 'get_moves', 'set_moves')
+ customer_moves = fields.Function(fields.One2Many('stock.move', None,
+ 'Customer Moves',
+ states={
+ 'readonly': Eval('state') != 'shipped',
+ },
+ depends=['state', 'customer']), 'get_moves', 'set_moves')
code = fields.Char('Code', select=1, readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('waiting', 'Waiting'),
+ ('shipped', 'Shipped'),
('done', 'Done'),
('cancel', 'Canceled'),
], 'State', readonly=True)
+ @classmethod
+ def __register__(cls, module_name):
+ pool = Pool()
+ Move = pool.get('stock.move')
+ PurchaseLine = pool.get('purchase.line')
+ PurchaseRequest = pool.get('purchase.request')
+ SaleLine = pool.get('sale.line')
+ Location = pool.get('stock.location')
+ move = Move.__table__()
+ purchase_line = PurchaseLine.__table__()
+ purchase_request = PurchaseRequest.__table__()
+ sale_line = SaleLine.__table__()
+ location = Location.__table__()
+ cursor = Transaction().cursor
+
+ super(ShipmentDrop, cls).__register__(module_name)
+
+ # Migration from 3.6
+ cursor.execute(*location.select(Count(location.id),
+ where=(location.type == 'drop')))
+ has_drop_shipment, = cursor.fetchone()
+
+ if not has_drop_shipment:
+ drop_shipment = Location(name='Migration Drop Shipment',
+ type='drop', active=False)
+ drop_shipment.save()
+ drop_shipment_location = drop_shipment.id
+
+ move_sale_query = move.join(purchase_line,
+ condition=move.origin == Concat('purchase.line,',
+ purchase_line.id)
+ ).join(purchase_request,
+ condition=purchase_request.purchase_line == purchase_line.id
+ ).join(sale_line,
+ condition=sale_line.purchase_request == purchase_request.id
+ ).select(
+ move.id, move.to_location, sale_line.id,
+ where=move.shipment.like('stock.shipment.drop,%'))
+ cursor.execute(*move_sale_query)
+ move_sales = cursor.fetchall()
+
+ for sub_move in grouped_slice(move_sales):
+ sub_ids = [s[0] for s in sub_move]
+ cursor.execute(*move.update(
+ columns=[move.to_location],
+ values=[drop_shipment_location],
+ where=move.id.in_(sub_ids)))
+
+ create_move = move.insert(values=move.select(
+ where=move.shipment.like('stock.shipment.drop,%')))
+ cursor.execute(*create_move)
+
+ for move_id, customer_location, line_id in move_sales:
+ cursor.execute(move.update(
+ columns=[move.origin, move.from_location,
+ move.to_location],
+ values=[Concat('sale.line,', str(line_id)),
+ drop_shipment_location, customer_location],
+ where=(move.id == move_id)))
+
@classmethod
def __setup__(cls):
super(ShipmentDrop, cls).__setup__()
@@ -99,11 +179,13 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
})
cls._transitions |= set((
('draft', 'waiting'),
- ('waiting', 'done'),
+ ('waiting', 'shipped'),
('draft', 'cancel'),
('waiting', 'cancel'),
('waiting', 'draft'),
('cancel', 'draft'),
+ ('shipped', 'done'),
+ ('shipped', 'cancel'),
))
cls._buttons.update({
'cancel': {
@@ -118,9 +200,12 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
'wait': {
'invisible': Eval('state') != 'draft',
},
- 'done': {
+ 'ship': {
'invisible': Eval('state') != 'waiting',
},
+ 'done': {
+ 'invisible': Eval('state') != 'shipped',
+ },
})
cls._error_messages.update({
'reset_move': ('You cannot reset to draft move "%s" which was '
@@ -168,6 +253,18 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
'planned_date': planned_date,
})
+ def get_moves(self, name):
+ if name == 'supplier_moves':
+ return [m.id for m in self.moves if m.to_location.type == 'drop']
+ elif name == 'customer_moves':
+ return [m.id for m in self.moves if m.from_location.type == 'drop']
+
+ @classmethod
+ def set_moves(cls, shipments, name, values):
+ if not values:
+ return
+ cls.write(shipments, {'moves': values})
+
@classmethod
def create(cls, vlist):
pool = Pool()
@@ -198,20 +295,24 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
if values.get('state', '') not in ('done', 'cancel'):
continue
with Transaction().set_context(_check_access=False):
- purchases = set()
- move_ids = [m.id for s in shipments for m in s.moves]
- purchase_lines = PurchaseLine.search([
- ('moves', 'in', move_ids),
- ])
- purchases = list(set(l.purchase for l in purchase_lines or []))
- Purchase.process(purchases)
-
- sale_lines = SaleLine.search([
- ('moves', 'in', move_ids),
- ])
+ move_ids = [m.id for s in shipments for m in s.customer_moves]
+ sale_lines = []
+ for sub_moves in grouped_slice(move_ids):
+ sale_lines += SaleLine.search([
+ ('moves', 'in', list(sub_moves)),
+ ])
sales = list(set(l.sale for l in sale_lines or []))
Sale.process(sales)
+ move_ids = [m.id for s in shipments for m in s.supplier_moves]
+ purchase_lines = []
+ for sub_moves in grouped_slice(move_ids):
+ purchase_lines += PurchaseLine.search([
+ ('moves', 'in', move_ids),
+ ])
+ purchases = list(set(l.purchase for l in purchase_lines or []))
+ Purchase.process(purchases)
+
@classmethod
def copy(cls, shipments, default=None):
if default is None:
@@ -225,7 +326,9 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
@Workflow.transition('cancel')
def cancel(cls, shipments):
Move = Pool().get('stock.move')
- Move.cancel([m for s in shipments for m in s.moves])
+ Move.cancel([m for s in shipments for m in s.supplier_moves])
+ Move.write([m for s in shipments for m in s.customer_moves],
+ {'shipment': None})
@classmethod
@ModelView.button
@@ -242,11 +345,72 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
cls.raise_user_error('reset_move', (move.rec_name,))
Move.draft([m for s in shipments for m in s.moves])
+ @classmethod
+ def _synchronize_moves(cls, shipments):
+ pool = Pool()
+ UoM = pool.get('product.uom')
+ Move = pool.get('stock.move')
+
+ to_save = []
+ for shipment in shipments:
+ product_qty = defaultdict(lambda: 0)
+ for s_move in shipment.supplier_moves:
+ product_qty[s_move.product] += UoM.compute_qty(s_move.uom,
+ s_move.quantity, s_move.product.default_uom)
+ for c_move in shipment.customer_moves:
+ if product_qty[c_move.product] <= 0:
+ continue
+ move_qty = UoM.compute_qty(c_move.uom, c_move.quantity,
+ c_move.product.default_uom)
+ qty = min(product_qty[c_move.product], move_qty)
+ c_move.quantity = UoM.compute_qty(c_move.product.default_uom,
+ qty, c_move.uom)
+ product_qty[c_move.product] -= qty
+ to_save.append(c_move)
+ if to_save:
+ Move.save(to_save)
+
@classmethod
@ModelView.button
@Workflow.transition('waiting')
def wait(cls, shipments):
- pass
+ pool = Pool()
+ PurchaseRequest = pool.get('purchase.request')
+ SaleLine = pool.get('sale.line')
+ Move = pool.get('stock.move')
+
+ requests = []
+ for sub_lines in grouped_slice([m.origin.id for s in shipments
+ for m in s.supplier_moves]):
+ requests += PurchaseRequest.search([
+ ('purchase_line', 'in', list(sub_lines)),
+ ])
+ pline2request = {r.purchase_line: r for r in requests}
+ sale_lines = SaleLine.search([
+ ('purchase_request', 'in', [r.id for r in requests]),
+ ])
+ request2sline = {sl.purchase_request: sl for sl in sale_lines}
+
+ to_save = []
+ for shipment in shipments:
+ for move in shipment.supplier_moves:
+ sale_line = request2sline[pline2request[move.origin]]
+ for move in sale_line.moves:
+ if (move.state not in ('cancel', 'done')
+ and move.from_location.type == 'drop'):
+ move.shipment = shipment
+ to_save.append(move)
+ Move.save(to_save)
+ cls._synchronize_moves(shipments)
+
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('shipped')
+ def ship(cls, shipments):
+ pool = Pool()
+ Move = pool.get('stock.move')
+ Move.do([m for s in shipments for m in s.supplier_moves])
+ cls._synchronize_moves(shipments)
@classmethod
@ModelView.button
@@ -255,7 +419,7 @@ class ShipmentDrop(Workflow, ModelSQL, ModelView):
pool = Pool()
Move = pool.get('stock.move')
Date = pool.get('ir.date')
- Move.do([m for s in shipments for m in s.moves])
+ Move.do([m for s in shipments for m in s.customer_moves])
cls.write(shipments, {
'effective_date': Date.today(),
})
@@ -275,12 +439,19 @@ class Move:
return models
def get_customer_drop(self, name):
- PurchaseLine = Pool().get('purchase.line')
- if (isinstance(self.origin, PurchaseLine)
- and self.origin.purchase.customer):
+ pool = Pool()
+ SaleLine = pool.get('sale.line')
+ PurchaseLine = pool.get('purchase.line')
+
+ if isinstance(self.origin, SaleLine):
+ return self.origin.sale.party.id
+ elif isinstance(self.origin, PurchaseLine):
return self.origin.purchase.customer.id
@classmethod
def search_customer_drop(cls, name, clause):
- return [('origin.purchase.customer',) + tuple(clause[1:])
- + ('purchase.line',)]
+ return ['OR',
+ ('origin.sale.party',) + tuple(clause[1:]) + ('sale.line',),
+ (('origin.purchase.customer',) + tuple(clause[1:])
+ + ('purchase.line',)),
+ ]
diff --git a/stock.xml b/stock.xml
index 1bfdb64701..4f2c277a72 100644
--- a/stock.xml
+++ b/stock.xml
@@ -77,6 +77,13 @@ this repository contains the full copyright notices and license terms. -->
+
+ Shipped
+
+
+
+
All
@@ -188,4 +195,28 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
+
+ Drop
+ DROP
+ drop
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/scenario_sale_supply_drop_shipment.rst b/tests/scenario_sale_supply_drop_shipment.rst
index 43d7f94033..5ed8aac818 100644
--- a/tests/scenario_sale_supply_drop_shipment.rst
+++ b/tests/scenario_sale_supply_drop_shipment.rst
@@ -183,19 +183,28 @@ Create Purchase from Request::
>>> purchase.click('process')
>>> purchase.state
u'processing'
+
>>> config.user = sale_user.id
>>> sale.reload()
>>> sale.shipments
[]
>>> shipment, = sale.drop_shipments
-Receive 100 products::
+Receiving only 100 products::
>>> config.user = stock_user.id
- >>> move, = shipment.moves
+ >>> move, = shipment.supplier_moves
>>> move.unit_price == move.cost_price
True
>>> move.quantity = 100
+ >>> shipment.click('ship')
+ >>> config.user = sale_user.id
+ >>> sale.reload()
+ >>> sale.shipments
+ []
+ >>> shipment, = sale.drop_shipments
+
+ >>> config.user = stock_user.id
>>> shipment.click('done')
>>> shipment.state
u'done'
@@ -203,22 +212,28 @@ Receive 100 products::
>>> sale.reload()
>>> sale.shipments
[]
- >>> _, shipment = sale.drop_shipments
- >>> move, = shipment.moves
- >>> move.quantity
- 150.0
- >>> move.unit_price == move.cost_price
- True
-The purchase is now waiting for this new drop shipment::
+The purchase is now waiting for his new drop shipment::
>>> config.user = purchase_user.id
>>> purchase.reload()
>>> purchase.shipment_state
u'waiting'
+ >>> shipment, = [s for s in purchase.drop_shipments
+ ... if s.state == 'waiting']
+ >>> move, = shipment.customer_moves
+ >>> move.quantity
+ 150.0
+ >>> move.unit_price == move.cost_price
+ True
+ >>> move, = shipment.supplier_moves
+ >>> move.quantity
+ 150.0
+ >>> move.unit_price == move.cost_price
+ True
-Let's cancel the shipment and handle the issue from the purchase.
-As a consequence the sale order is over::
+Let's cancel the shipment and handle the issue on the purchase.
+As a consequence the sale order is now in exception::
>>> config.user = stock_user.id
>>> shipment.click('cancel')
@@ -238,7 +253,7 @@ As a consequence the sale order is over::
>>> config.user = sale_user.id
>>> sale.reload()
>>> sale.shipment_state
- u'sent'
+ u'exception'
Cancelling the workflow on the purchase step::
@@ -268,49 +283,21 @@ Cancelling the workflow on the purchase step::
>>> purchase, = Purchase.find([('state', '=', 'draft')])
>>> purchase.click('cancel')
-The sale just created a new outgoing shipment for the sale::
+The sale is then in exception::
>>> config.user = sale_user.id
>>> sale.reload()
- >>> shipment, = sale.shipments
-
-If the shipment is cancelled then the shipement state of the sale will be
-considered as 'sent'::
-
- >>> config.user = stock_user.id
- >>> shipment.click('cancel')
- >>> config.user = sale_user.id
>>> sale.shipment_state
u'exception'
-
>>> handle_exception = Wizard('sale.handle.shipment.exception', [sale])
- >>> _ = handle_exception.form.recreate_moves.pop()
>>> handle_exception.execute('handle')
>>> sale.reload()
>>> sale.shipment_state
- u'sent'
+ u'waiting'
-If we opt not to cancel the shipment and deliver from stock::
+The sale just created a new outgoing shipment for the sale and we can deliver
+from stock::
- >>> config.user = sale_user.id
- >>> sale = Sale()
- >>> sale.party = customer
- >>> sale.payment_term = payment_term
- >>> sale_line = sale.lines.new()
- >>> sale_line.product = product
- >>> sale_line.quantity = 125
- >>> sale.click('quote')
- >>> sale.click('confirm')
- >>> sale.click('process')
- >>> config.user = purchase_user.id
- >>> purchase_request, = PurchaseRequest.find([('purchase_line', '=', None)])
- >>> create_purchase = Wizard('purchase.request.create_purchase',
- ... [purchase_request])
- >>> purchase, = Purchase.find([('state', '=', 'draft')])
- >>> purchase.click('cancel')
-
- >>> config.user = sale_user.id
- >>> sale.reload()
>>> shipment, = sale.shipments
>>> config.user = stock_user.id
@@ -319,53 +306,6 @@ If we opt not to cancel the shipment and deliver from stock::
>>> shipment.click('done')
>>> config.user = sale_user.id
- >>> sale.shipment_state
- u'sent'
-
-Now let's handle the exception on the sale side::
-
- >>> config.user = sale_user.id
- >>> sale = Sale()
- >>> sale.party = customer
- >>> sale.payment_term = payment_term
- >>> sale_line = sale.lines.new()
- >>> sale_line.product = product
- >>> sale_line.quantity = 375
- >>> sale.save()
- >>> sale.click('quote')
- >>> sale.click('confirm')
- >>> sale.click('process')
-
- >>> config.user = purchase_user.id
- >>> purchase_request, = PurchaseRequest.find([('purchase_line', '=', None)])
- >>> purchase_request.quantity
- 375.0
- >>> create_purchase = Wizard('purchase.request.create_purchase',
- ... [purchase_request])
- >>> purchase, = Purchase.find([('state', '=', 'draft')])
- >>> purchase.payment_term = payment_term
- >>> purchase.click('quote')
- >>> purchase.click('confirm')
- >>> purchase.click('process')
- >>> purchase.shipment_state
- u'waiting'
- >>> config.user = sale_user.id
- >>> sale.reload()
- >>> shipment, = sale.drop_shipments
- >>> config.user = stock_user.id
- >>> shipment.click('cancel')
-
- >>> config.user = sale_user.id
- >>> sale.shipment_state
- u'exception'
- >>> handle_exception = Wizard('sale.handle.shipment.exception', [sale])
- >>> _ = handle_exception.form.recreate_moves.pop()
- >>> handle_exception.execute('handle')
>>> sale.reload()
>>> sale.shipment_state
u'sent'
-
- >>> config.user = purchase_user.id
- >>> purchase.reload()
- >>> purchase.shipment_state
- u'received'
diff --git a/view/purchase_configuration_form.xml b/view/purchase_configuration_form.xml
new file mode 100644
index 0000000000..8763dc5a03
--- /dev/null
+++ b/view/purchase_configuration_form.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/purchase_form.xml b/view/purchase_form.xml
index eb0f48c059..4fa2dc8539 100644
--- a/view/purchase_form.xml
+++ b/view/purchase_form.xml
@@ -9,6 +9,11 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
diff --git a/view/sale_configuration_form.xml b/view/sale_configuration_form.xml
new file mode 100644
index 0000000000..214e7f8123
--- /dev/null
+++ b/view/sale_configuration_form.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/view/sale_form.xml b/view/sale_form.xml
index 45237edde3..6d24cdf268 100644
--- a/view/sale_form.xml
+++ b/view/sale_form.xml
@@ -7,4 +7,9 @@ this repository contains the full copyright notices and license terms. -->
position="after">
+
+
+
+
diff --git a/view/shipment_drop_form.xml b/view/shipment_drop_form.xml
index 2df0d250af..22319711ec 100644
--- a/view/shipment_drop_form.xml
+++ b/view/shipment_drop_form.xml
@@ -20,13 +20,21 @@ this repository contains the full copyright notices and license terms. -->
-
+
+
+
+
+
+
+
+
+