Split moves into supplier and customer moves

issue4371
review15641002
This commit is contained in:
Nicolas Évrard
2015-10-04 17:55:39 +02:00
parent 106b86275f
commit b7887bdd87
15 changed files with 474 additions and 317 deletions
+2
View File
@@ -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
+2 -1
View File
@@ -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')
+6 -1
View File
@@ -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
+106 -31
View File
@@ -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'
+6
View File
@@ -8,6 +8,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="inherit" ref="purchase.purchase_view_form"/>
<field name="name">purchase_form</field>
</record>
<record model="ir.ui.view" id="purchase_configuration_view_form">
<field name="model">purchase.configuration</field>
<field name="inherit"
ref="purchase.purchase_configuration_view_form"/>
<field name="name">purchase_configuration_form</field>
</record>
<record model="ir.action.act_window" id="act_purchase_drop_shipment_form">
<field name="name">Drop Shipments</field>
+39 -157
View File
@@ -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'
+6
View File
@@ -9,6 +9,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">sale_form</field>
</record>
<record model="ir.ui.view" id="sale_configuration_view_form">
<field name="model">sale.configuration</field>
<field name="inherit" ref="sale.sale_configuration_view_form"/>
<field name="name">sale_configuration_form</field>
</record>
<record model="ir.action.act_window" id="act_sale_drop_shipment_form">
<field name="name">Drop Shipments</field>
<field name="res_model">stock.shipment.drop</field>
+206 -35
View File
@@ -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',)),
]
+31
View File
@@ -77,6 +77,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="domain" eval="[('state', '=', 'waiting')]" pyson="1"/>
<field name="act_window" ref="act_shipment_drop_form"/>
</record>
<record model="ir.action.act_window.domain"
id="act_shipment_drop_form_domain_shipped">
<field name="name">Shipped</field>
<field name="sequence" eval="30"/>
<field name="domain" eval="[('state', '=', 'shipped')]" pyson="1"/>
<field name="act_window" ref="act_shipment_drop_form"/>
</record>
<record model="ir.action.act_window.domain" id="act_shipment_drop_form_domain_all">
<field name="name">All</field>
<field name="sequence" eval="9999"/>
@@ -188,4 +195,28 @@ this repository contains the full copyright notices and license terms. -->
</record>
</data>
<data noupdate="1">
<!-- Default locations -->
<record model="stock.location" id="location_drop">
<field name="name">Drop</field>
<field name="code">DROP</field>
<field name="type">drop</field>
</record>
<record model="ir.property" id="default_sale_location_drop">
<field name="field"
search="[('model.model', '=', 'sale.configuration'), ('name', '=', 'sale_drop_location')]"/>
<field name="value"
eval="'stock.location,' + str(ref('location_drop'))"/>
</record>
<record model="ir.property" id="default_purchase_location_drop">
<field name="field"
search="[('model.model', '=', 'purchase.configuration'), ('name', '=', 'purchase_drop_location')]"/>
<field name="value"
eval="'stock.location,' + str(ref('location_drop'))"/>
</record>
</data>
</tryton>
+31 -91
View File
@@ -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'
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='purchase_invoice_method']"
position="after">
<newline/>
<label name="purchase_drop_location"/>
<field name="purchase_drop_location"/>
</xpath>
</data>
+5
View File
@@ -9,6 +9,11 @@ this repository contains the full copyright notices and license terms. -->
<field name="delivery_address"/>
<newline/>
</xpath>
<xpath expr="/form/notebook/page[@id='info']/field[@name='invoice_method']"
position="after">
<label name="drop_location"/>
<field name="drop_location"/>
</xpath>
<xpath
expr="/form/notebook/page[@name='shipments']/field[@name='shipments']"
position="after">
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='sale_shipment_method']" position="after">
<label name="sale_drop_location"/>
<field name="sale_drop_location"/>
</xpath>
</data>
+5
View File
@@ -7,4 +7,9 @@ this repository contains the full copyright notices and license terms. -->
position="after">
<field name="drop_shipments" colspan="4"/>
</xpath>
<xpath expr="/form/notebook/page[@id='other']/field[@name='shipment_method']"
position="after">
<label name="drop_location"/>
<field name="drop_location"/>
</xpath>
</data>
+9 -1
View File
@@ -20,13 +20,21 @@ this repository contains the full copyright notices and license terms. -->
<field name="effective_date"/>
<label name="company"/>
<field name="company"/>
<field name="moves" colspan="4"/>
<notebook>
<page name="supplier_moves">
<field name="supplier_moves" colspan="4"/>
</page>
<page name="customer_moves">
<field name="customer_moves" colspan="4"/>
</page>
</notebook>
<label name="state"/>
<field name="state"/>
<group col="5" colspan="2" id="buttons">
<button string="Cancel" name="cancel" icon="tryton-cancel"/>
<button string="Draft" name="draft"/>
<button string="Wait" name="wait" icon="tryton-go-next"/>
<button string="Ship" name="ship" icon="tryton-go-next"/>
<button string="Done" name="done" icon="tryton-ok"/>
</group>
</form>