Merge lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde into lp:stock-logistic-flows

Proposed by Romain Deheele - Camptocamp
Status: Needs review
Proposed branch: lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde
Merge into: lp:stock-logistic-flows
Diff against target: 837 lines (+811/-0)
5 files modified
picking_dispatch_picking_oriented/__init__.py (+22/-0)
picking_dispatch_picking_oriented/__openerp__.py (+52/-0)
picking_dispatch_picking_oriented/dispatch.py (+581/-0)
picking_dispatch_picking_oriented/dispatch_view.xml (+59/-0)
picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml (+97/-0)
To merge this branch: bzr merge lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde
Reviewer Review Type Date Requested Status
Alexandre Fayolle - camptocamp Needs Resubmitting
Yannick Vaucher @ Camptocamp code review, no test Approve
Leonardo Pistone Approve
Review via email: mp+215147@code.launchpad.net

Description of the change

Hi,

It adds a picking dispatch extension, with a picking_oriented use.
Description:
    The picking_dispatch addon is stock move-oriented.
    This addon changes it for a picking-oriented use.
    On "Done" button, a wizard is displayed (same as picking_dispatch), but:
    - moves are not passed to "done" state, but splitted between picked quantity and remains.
    - unpicked moves are moved in a new backorder.
    Then, when the picking dispatch state is done :
    - the picking dispatch hides the "Stock Moves" tab, to deliver, the user uses "Related Picking" tab
    - the "Transfer Products" wizard ("Deliver" button) displays only moves linked to a done picking dispatch.

The process is the next:
  - A picking dispatch is created with several moves from several pickings.
  - When the user

I'm not necessarily satisfied about addon's name, I'm ok to change it if we find.

Regards,
Romain

To post a comment you must log in.
Revision history for this message
Leonardo Pistone (lepistone) wrote :

Thanks Romain

77 + "test": [],

review: Needs Fixing (code review)
Revision history for this message
Romain Deheele - Camptocamp (romaindeheele) wrote :

Hi Leonardo,

I added some tests.

Romain

Revision history for this message
Leonardo Pistone (lepistone) wrote :

- l282: syntax
- l177 typo
- can you please run flake8 on the module? there are some little issues here and there.
- (non-blocking) if some methods are copied to just override a small thing, it would help to have a precise reference to the original one.

- there are a couple of very long methods I'm not a big fan of. I'm definitely not blocking on that, because I imagine these come from the OpenERP core, and it's probably not worth it to refactor here leaving the original as it is.

Thanks!

64. By Romain Deheele - Camptocamp

[UPD] clean code according flake8 recommandations except lines length

Revision history for this message
Romain Deheele - Camptocamp (romaindeheele) wrote :

Thanks Leonardo,

I cleaned the code according to the flake8 recommandations, except lines length.
I prefer to leave lengths as it is to easily identify original code if necessary.
Very long methods come from OpenERP core, and are not (easily) hookables for the moment.

Revision history for this message
Leonardo Pistone (lepistone) wrote :

thanks romain

review: Approve
Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

(* l.188 + l.199 PEP8 80 chars max. I fear noone reads code displaying it on a dual screen.)

* few self.pool.get('...') to replace by self.pool['...']

* l.269 + l.599 could be written using []

* l.292 is it necessary to do a copy of the context?

* l.601 missing passing context

* l.626 active_id might be missing in context. This could raise a keyerror

* l.680 <field name="type">form</field> can be removed

* missing context on most of write method calls.
* do_partial and do_partial_via_dispatch is a bit too monolithic can you split it in operations?

To identify original superseeded code, you might add some strong comment saying which part has been modified where it start and where it ends.

review: Needs Fixing (code review, no test)
65. By Romain Deheele - Camptocamp

[UPD] add comment with more details

66. By Romain Deheele - Camptocamp

[UPD] small improvements

Revision history for this message
Romain Deheele - Camptocamp (romaindeheele) wrote :

Hi Yannick,

I updated code as you advised : very large lines, brackets instead of update, active_id checking, view type.

I'm agree with other tips, but:
- majority of methods are core stock addon overrides without call to super (because of no possibility to easily hook them)
There is here a dilemma between :
 - improve code to be consistent with OCA quality.
 - keep code visually close to core stock addon.

Could you check if you find that comments show now enough clearly the adds?
if yes, feel free to give your opinion about the exposed dilemma.

Romain

Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

Ok thanks for the changes

Looks better.

I agree we need to keep the code close to official one, in order to apply changes easilier.
I wonder if in case of overriding we should do that in a specific file and simply keep the code unchanged.

review: Approve (code review, no test)
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

The source code management for this project has been moved to https://github.com/OCA/stock-logistics-workflow

Could you resubmit this MP on the new site?

review: Needs Resubmitting

Unmerged revisions

66. By Romain Deheele - Camptocamp

[UPD] small improvements

65. By Romain Deheele - Camptocamp

[UPD] add comment with more details

64. By Romain Deheele - Camptocamp

[UPD] clean code according flake8 recommandations except lines length

63. By Romain Deheele - Camptocamp

[UPD] add comments

62. By Romain Deheele - Camptocamp

[UPD] add possibilty to set/change the carrier when the user is doing deliveries

61. By Romain Deheele - Camptocamp

[UPD] case of shortage context not anticipated (ex:broken product) between picking and shipping: separate staying availale move and dispatch

60. By Romain Deheele - Camptocamp

[UPD] manages dispatch_id info when there is a difference between picked quantities and shipped quantities

59. By Romain Deheele - Camptocamp

[ADD] add tests

58. By Romain Deheele - Camptocamp

changes on __opener__.py

57. By Romain Deheele - Camptocamp

[ADD] add picking dispatch extension with picking-oriented use

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'picking_dispatch_picking_oriented'
2=== added file 'picking_dispatch_picking_oriented/__init__.py'
3--- picking_dispatch_picking_oriented/__init__.py 1970-01-01 00:00:00 +0000
4+++ picking_dispatch_picking_oriented/__init__.py 2014-05-14 15:41:52 +0000
5@@ -0,0 +1,22 @@
6+# -*- coding: utf-8 -*-
7+##############################################################################
8+#
9+# Author: Alexandre Fayolle, Romain Deheele
10+# Copyright 2014 Camptocamp SA
11+#
12+# This program is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Affero General Public License as
14+# published by the Free Software Foundation, either version 3 of the
15+# License, or (at your option) any later version.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU Affero General Public License for more details.
21+#
22+# You should have received a copy of the GNU Affero General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+#
25+##############################################################################
26+
27+from . import dispatch # noqa
28
29=== added file 'picking_dispatch_picking_oriented/__openerp__.py'
30--- picking_dispatch_picking_oriented/__openerp__.py 1970-01-01 00:00:00 +0000
31+++ picking_dispatch_picking_oriented/__openerp__.py 2014-05-14 15:41:52 +0000
32@@ -0,0 +1,52 @@
33+# -*- coding: utf-8 -*-
34+##############################################################################
35+#
36+# Author: Alexandre Fayolle, Romain Deheele
37+# Copyright 2014 Camptocamp SA
38+#
39+# This program is free software: you can redistribute it and/or modify
40+# it under the terms of the GNU Affero General Public License as
41+# published by the Free Software Foundation, either version 3 of the
42+# License, or (at your option) any later version.
43+#
44+# This program is distributed in the hope that it will be useful,
45+# but WITHOUT ANY WARRANTY; without even the implied warranty of
46+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47+# GNU Affero General Public License for more details.
48+#
49+# You should have received a copy of the GNU Affero General Public License
50+# along with this program. If not, see <http://www.gnu.org/licenses/>.
51+#
52+##############################################################################
53+
54+
55+{
56+ "name": "Picking Dispatch picking-oriented",
57+ "version": "0.1",
58+ "depends": ['picking_dispatch','delivery'],
59+ "author": "Camptocamp",
60+ "license": "AGPL-3",
61+ "description": """picking_dispatch addon is stock move-oriented.
62+This addon changes it for a picking-oriented use.
63+
64+On "Done" button, a wizard is displayed (same as picking_dispatch), but:
65+
66+* moves are not passed to "done" state, but split between picked quantity and remains.
67+
68+* unpicked moves are moved in a new backorder.
69+
70+Then, when the picking dispatch state is done:
71+
72+* the picking dispatch hides the "Stock Moves" tab, the user uses "Related Picking" tab to deliver pickings one after the other.
73+
74+* the "Transfer Products" wizard ("Deliver" button) displays only moves linked to a done picking dispatch.
75+
76+* on "Transfer Products" wizard, a carrier field is displayed to give possility to check and change it if it's necessary.
77+ """,
78+ "website": "http://www.camptocamp.com",
79+ "category": "Warehouse Management",
80+ "demo": [],
81+ "data": ['dispatch_view.xml'],
82+ "test": ['test/dispatch_picking_oriented.yml'],
83+ "installable": True,
84+}
85
86=== added file 'picking_dispatch_picking_oriented/dispatch.py'
87--- picking_dispatch_picking_oriented/dispatch.py 1970-01-01 00:00:00 +0000
88+++ picking_dispatch_picking_oriented/dispatch.py 2014-05-14 15:41:52 +0000
89@@ -0,0 +1,581 @@
90+# -*- coding: utf-8 -*-
91+##############################################################################
92+#
93+# Author: Alexandre Fayolle, Romain Deheele
94+# Copyright 2014 Camptocamp SA
95+#
96+# This program is free software: you can redistribute it and/or modify
97+# it under the terms of the GNU Affero General Public License as
98+# published by the Free Software Foundation, either version 3 of the
99+# License, or (at your option) any later version.
100+#
101+# This program is distributed in the hope that it will be useful,
102+# but WITHOUT ANY WARRANTY; without even the implied warranty of
103+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
104+# GNU Affero General Public License for more details.
105+#
106+# You should have received a copy of the GNU Affero General Public License
107+# along with this program. If not, see <http://www.gnu.org/licenses/>.
108+#
109+#############################################################################
110+import logging
111+import time
112+from openerp import netsvc
113+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
114+from openerp.tools.float_utils import float_compare
115+from openerp.osv import orm, fields, osv
116+from openerp.tools.translate import _
117+_logger = logging.getLogger(__name__)
118+
119+
120+class stock_partial_picking(orm.TransientModel):
121+ _inherit = "stock.partial.picking"
122+
123+ _columns = {
124+ 'carrier_id': fields.many2one('delivery.carrier', 'Carrier'),
125+ }
126+
127+ def default_get(self, cr, uid, fields, context=None):
128+ """override from stock/wizard/stock_partial_picking.py (no call to super):
129+ - to update filter on showed moves
130+ - to fill carrier_id"""
131+ if context is None:
132+ context = {}
133+ res = super(stock_partial_picking, self).default_get(cr, uid, fields,
134+ context=context)
135+ picking_ids = context.get('active_ids', [])
136+ active_model = context.get('active_model')
137+ picking_obj = self.pool.get('stock.picking')
138+
139+ if not picking_ids or len(picking_ids) != 1:
140+ """ Partial Picking Processing may only be done
141+ for one picking at a time"""
142+ return res
143+ assert active_model in ('stock.picking', 'stock.picking.in', 'stock.picking.out'), 'Bad context propagation'
144+ picking_id, = picking_ids
145+ if 'picking_id' in fields:
146+ res.update(picking_id=picking_id)
147+ # add one condition on move_ids filling : only not linked to a not finished dispatch moves are showed
148+ if 'move_ids' in fields:
149+ picking = picking_obj.browse(cr, uid, picking_id, context=context)
150+ moves = [self._partial_move_for(cr, uid, m)
151+ for m in picking.move_lines
152+ if (m.state not in ('done', 'cancel')
153+ and not (m.dispatch_id and
154+ m.dispatch_id.state != 'done'))]
155+ res.update(move_ids=moves)
156+ if 'date' in fields:
157+ res.update(date=time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
158+ # fill carrier_id info
159+ if 'carrier_id' in fields:
160+ picking = picking_obj.browse(cr, uid, picking_id, context=context)
161+ res.update(carrier_id=picking.carrier_id.id)
162+ return res
163+
164+ def do_partial(self, cr, uid, ids, context=None):
165+ """override from stock/wizard/stock_partial_picking.py (no call to super):
166+ - to just add carrier_id in partial_data (1) """
167+ assert len(ids) == 1, 'Partial picking processing may only be done one at a time.'
168+ stock_picking = self.pool.get('stock.picking')
169+ stock_move = self.pool.get('stock.move')
170+ uom_obj = self.pool.get('product.uom')
171+ partial = self.browse(cr, uid, ids[0], context=context)
172+ # (1) add carrier_id in partial_data dict
173+ partial_data = {
174+ 'delivery_date': partial.date,
175+ 'carrier_id': partial.carrier_id and partial.carrier_id.id or False
176+ }
177+ picking_type = partial.picking_id.type
178+
179+ for wizard_line in partial.move_ids:
180+ line_uom = wizard_line.product_uom
181+ move_id = wizard_line.move_id.id
182+
183+ #Quantity must be Positive
184+ if wizard_line.quantity < 0:
185+ raise osv.except_osv(_('Warning!'), _('Please provide proper Quantity.'))
186+
187+ #Compute the quantity for respective wizard_line in the line uom (this jsut do the rounding if necessary)
188+ qty_in_line_uom = uom_obj._compute_qty(cr, uid, line_uom.id, wizard_line.quantity, line_uom.id)
189+
190+ if line_uom.factor and line_uom.factor != 0:
191+ if float_compare(qty_in_line_uom, wizard_line.quantity, precision_rounding=line_uom.rounding) != 0:
192+ raise osv.except_osv(_('Warning!'),
193+ _('The unit of measure rounding does not allow you to ship "%s %s", '
194+ 'only rounding of "%s %s" is accepted by the Unit of Measure.')
195+ % (wizard_line.quantity, line_uom.name, line_uom.rounding, line_uom.name))
196+ if move_id:
197+ #Check rounding Quantity.ex.
198+ #picking: 1kg, uom kg rounding = 0.01 (rounding to 10g),
199+ #partial delivery: 253g
200+ #=> result= refused, as the qty left on picking would be 0.747kg and only 0.75 is accepted by the uom.
201+ initial_uom = wizard_line.move_id.product_uom
202+ #Compute the quantity for respective wizard_line in the initial uom
203+ qty_in_initial_uom = uom_obj._compute_qty(cr, uid, line_uom.id, wizard_line.quantity, initial_uom.id)
204+ without_rounding_qty = (wizard_line.quantity / line_uom.factor) * initial_uom.factor
205+ if float_compare(qty_in_initial_uom, without_rounding_qty, precision_rounding=initial_uom.rounding) != 0:
206+ raise osv.except_osv(_('Warning!'),
207+ _('The rounding of the initial uom does not allow you to ship "%s %s", '
208+ 'as it would let a quantity of "%s %s" to ship and only rounding of "%s %s" is accepted by the uom.')
209+ % (wizard_line.quantity, line_uom.name, wizard_line.move_id.product_qty - without_rounding_qty,
210+ initial_uom.name, initial_uom.rounding, initial_uom.name))
211+ else:
212+ seq_obj_name = 'stock.picking.' + picking_type
213+ move_id = stock_move.create(cr, uid, {'name': self.pool.get('ir.sequence').get(cr, uid, seq_obj_name),
214+ 'product_id': wizard_line.product_id.id,
215+ 'product_qty': wizard_line.quantity,
216+ 'product_uom': wizard_line.product_uom.id,
217+ 'prodlot_id': wizard_line.prodlot_id.id,
218+ 'location_id': wizard_line.location_id.id,
219+ 'location_dest_id': wizard_line.location_dest_id.id,
220+ 'picking_id': partial.picking_id.id
221+ }, context=context)
222+ stock_move.action_confirm(cr, uid, [move_id], context)
223+ partial_data['move%s' % (move_id)] = {
224+ 'product_id': wizard_line.product_id.id,
225+ 'product_qty': wizard_line.quantity,
226+ 'product_uom': wizard_line.product_uom.id,
227+ 'prodlot_id': wizard_line.prodlot_id.id,
228+ }
229+ if (picking_type == 'in') and (wizard_line.product_id.cost_method == 'average'):
230+ partial_data['move%s' % (wizard_line.move_id.id)].update(product_price=wizard_line.cost,
231+ product_currency=wizard_line.currency.id)
232+ stock_picking.do_partial(cr, uid, [partial.picking_id.id], partial_data, context=context)
233+ return {'type': 'ir.actions.act_window_close'}
234+
235+
236+class stock_partial_move(orm.TransientModel):
237+ _inherit = 'stock.partial.move'
238+
239+ def do_partial(self, cr, uid, ids, context=None):
240+ """override from stock/wizard/stock_partial_move.py (no call to super):
241+ - to do not close window if the action is launched from a picking dispatch """
242+ assert len(ids) == 1, 'Partial move processing may only be done one form at a time.'
243+ partial = self.browse(cr, uid, ids[0], context=context)
244+ partial_data = {
245+ 'delivery_date': partial.date
246+ }
247+ moves_ids = []
248+ for move in partial.move_ids:
249+ if not move.move_id:
250+ raise orm.except_orm(_('Warning !'), _("You have manually created product lines, please delete them to proceed"))
251+ move_id = move.move_id.id
252+ partial_data['move%s' % (move_id)] = {
253+ 'product_id': move.product_id.id,
254+ 'product_qty': move.quantity,
255+ 'product_uom': move.product_uom.id,
256+ 'prodlot_id': move.prodlot_id.id,
257+ }
258+ moves_ids.append(move_id)
259+ if (move.move_id.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
260+ partial_data['move%s' % (move_id)].update(product_price=move.cost,
261+ product_currency=move.currency.id)
262+ #in classic context, we close wizard pop-up.
263+ #in picking dispatch context, we need to display the new created dispatch
264+ res = self.pool.get('stock.move').do_partial(cr, uid, moves_ids, partial_data, context=context)
265+ if context and 'partial_via_dispatch' in context:
266+ return res
267+ return {'type': 'ir.actions.act_window_close'}
268+
269+
270+class PickingDispatch(orm.Model):
271+ _inherit = 'picking.dispatch'
272+
273+ def action_done(self, cr, uid, ids, context=None):
274+ """Override from picking_dispatch/picking_dispatch.py (no call to super):
275+ - to indicate 'partial_via_dispatch' context"""
276+ if not ids:
277+ return True
278+ if context is None:
279+ context = {}
280+ ctx = context.copy()
281+ ctx['partial_via_dispatch'] = True
282+ move_obj = self.pool['stock.move']
283+ move_ids = move_obj.search(cr, uid, [('dispatch_id', 'in', ids)], context=ctx)
284+ return move_obj.action_partial_move(cr, uid, move_ids, context=ctx)
285+
286+
287+class StockPicking(orm.Model):
288+ _inherit = 'stock.picking'
289+
290+ def do_partial(self, cr, uid, ids, partial_datas, context=None):
291+ """ Override from stock/stock.py (no call to super) to:
292+ - (###1) pass carrier_id information on done picking
293+ - in case of shortage :
294+ - (###2) copies are the moves in state 'done', we need to pass them the current dispatch id
295+ - (###3) manage dispatch backorder link (reminder : undone moves are the original moves)
296+
297+ """
298+ if context is None:
299+ context = {}
300+ else:
301+ context = dict(context)
302+ res = {}
303+ move_obj = self.pool.get('stock.move')
304+ product_obj = self.pool.get('product.product')
305+ currency_obj = self.pool.get('res.currency')
306+ uom_obj = self.pool.get('product.uom')
307+ pricetype_obj = self.pool.get('product.price.type')
308+ sequence_obj = self.pool.get('ir.sequence')
309+ wf_service = netsvc.LocalService("workflow")
310+ for pick in self.browse(cr, uid, ids, context=context):
311+ new_picking = None
312+ complete, too_many, too_few = [], [], []
313+ move_product_qty, prodlot_ids, product_avail, partial_qty, product_uoms = {}, {}, {}, {}, {}
314+ for move in pick.move_lines:
315+ if move.state in ('done', 'cancel'):
316+ continue
317+ partial_data = partial_datas.get('move%s' % (move.id), {})
318+ product_qty = partial_data.get('product_qty', 0.0)
319+ move_product_qty[move.id] = product_qty
320+ product_uom = partial_data.get('product_uom', False)
321+ product_price = partial_data.get('product_price', 0.0)
322+ product_currency = partial_data.get('product_currency', False)
323+ prodlot_id = partial_data.get('prodlot_id')
324+ prodlot_ids[move.id] = prodlot_id
325+ product_uoms[move.id] = product_uom
326+ partial_qty[move.id] = uom_obj._compute_qty(cr, uid, product_uoms[move.id], product_qty, move.product_uom.id)
327+ if move.product_qty == partial_qty[move.id]:
328+ complete.append(move)
329+ elif move.product_qty > partial_qty[move.id]:
330+ too_few.append(move)
331+ else:
332+ too_many.append(move)
333+
334+ # Average price computation
335+ if (pick.type == 'in') and (move.product_id.cost_method == 'average'):
336+ product = product_obj.browse(cr, uid, move.product_id.id)
337+ move_currency_id = move.company_id.currency_id.id
338+ context['currency_id'] = move_currency_id
339+ qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
340+ price_type_id = pricetype_obj.search(cr, uid,
341+ [('field', '=', 'standard_price')],
342+ context=context)[0]
343+ price_type = pricetype_obj.browse(cr, uid, price_type_id, context=context)
344+ price_type_currency_id = price_type.currency_id.id
345+
346+ if product.id not in product_avail:
347+ # keep track of stock on hand including processed lines not yet marked as done
348+ product_avail[product.id] = product.qty_available
349+
350+ if qty > 0:
351+ # New price in company currency
352+ new_price = currency_obj.compute(cr, uid, product_currency,
353+ move_currency_id, product_price, round=False)
354+ new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
355+ product.uom_id.id)
356+ if product_avail[product.id] <= 0:
357+ product_avail[product.id] = 0
358+ new_std_price = new_price
359+ else:
360+ # Get the standard price
361+ amount_unit = product.price_get('standard_price', context=context)[product.id]
362+ # Here we must convert the new price computed in the currency of the price_type
363+ # of the product (e.g. company currency: EUR, price_type: USD)
364+ # The current value is still in company currency at this stage
365+ new_std_price = ((amount_unit * product_avail[product.id])
366+ + (new_price * qty))/(product_avail[product.id] + qty)
367+ # Convert the price in price_type currency
368+ new_std_price = currency_obj.compute(
369+ cr, uid, move_currency_id,
370+ price_type_currency_id, new_std_price)
371+ # Write the field according to price type field
372+ product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
373+
374+ # Record the values that were chosen in the wizard, so they can be
375+ # used for inventory valuation if real-time valuation is enabled.
376+ move_obj.write(cr, uid, [move.id], {
377+ 'price_unit': product_price,
378+ 'price_currency_id': product_currency
379+ })
380+ product_avail[product.id] += qty
381+
382+ for move in too_few:
383+ product_qty = move_product_qty[move.id]
384+ if not new_picking:
385+ new_picking_name = pick.name
386+ self.write(cr, uid, [pick.id], {
387+ 'name': sequence_obj.get(cr, uid, 'stock.picking.%s' % (pick.type)),
388+ })
389+ # (###1) pass carrier_id information on done picking
390+ new_picking = self.copy(cr, uid, pick.id, {
391+ 'name': new_picking_name,
392+ 'move_lines': [],
393+ 'state': 'draft',
394+ 'carrier_id': 'carrier_id' in partial_datas and partial_datas['carrier_id'],
395+ })
396+ if product_qty != 0:
397+ # (###2) copies are the moves in state 'done', we need to pass them the current dispatch id
398+ defaults = {
399+ 'product_qty': product_qty,
400+ 'product_uos_qty': product_qty, # TODO: put correct uos_qty
401+ 'picking_id': new_picking,
402+ 'state': 'assigned',
403+ 'move_dest_id': move.move_dest_id.id,
404+ 'price_unit': move.price_unit,
405+ 'product_uom': product_uoms[move.id],
406+ 'dispatch_id': move.dispatch_id and move.dispatch_id.id
407+ }
408+ prodlot_id = prodlot_ids[move.id]
409+ if prodlot_id:
410+ defaults.update(prodlot_id=prodlot_id)
411+ # (###2) the copy will be a done move, it has to attached to the current dispatch
412+ move_obj.copy(cr, uid, move.id, defaults)
413+ move_vals = {
414+ 'product_qty': move.product_qty - partial_qty[move.id],
415+ 'product_uos_qty': move.product_qty - partial_qty[move.id], # TODO: put correct uos_qty
416+ 'prodlot_id': False,
417+ 'tracking_id': False,
418+ }
419+ # (###3) manage dispatch backorder link (2 cases)
420+ if move.dispatch_id:
421+ dispatch_obj = self.pool['picking.dispatch']
422+ new_dispatch_id = dispatch_obj.search(cr, uid, [('backorder_id', '=', move.dispatch_id.id),
423+ ('state', '=', 'draft')], context=context)
424+ # 1. we have anticipated the shortage, a backorder dispatch exists, we can attach the move on it
425+ if new_dispatch_id:
426+ move_vals['dispatch_id'] = new_dispatch_id[0]
427+ # 2. we have not anticipated (broken products,...), the move will be not attached to a dispatch
428+ else:
429+ move_vals['dispatch_id'] = False
430+ move_obj.write(cr, uid, [move.id], move_vals)
431+
432+ if new_picking:
433+ move_obj.write(cr, uid, [c.id for c in complete], {'picking_id': new_picking})
434+ for move in complete:
435+ defaults = {'product_uom': product_uoms[move.id], 'product_qty': move_product_qty[move.id]}
436+ if prodlot_ids.get(move.id):
437+ defaults.update({'prodlot_id': prodlot_ids[move.id]})
438+ move_obj.write(cr, uid, [move.id], defaults)
439+ for move in too_many:
440+ product_qty = move_product_qty[move.id]
441+ defaults = {
442+ 'product_qty': product_qty,
443+ 'product_uos_qty': product_qty, # TODO: put correct uos_qty
444+ 'product_uom': product_uoms[move.id]
445+ }
446+ prodlot_id = prodlot_ids.get(move.id)
447+ if prodlot_ids.get(move.id):
448+ defaults.update(prodlot_id=prodlot_id)
449+ if new_picking:
450+ defaults.update(picking_id=new_picking)
451+ move_obj.write(cr, uid, [move.id], defaults)
452+
453+ # At first we confirm the new picking (if necessary)
454+ if new_picking:
455+ wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_confirm', cr)
456+ # Then we finish the good picking
457+ self.write(cr, uid, [pick.id], {'backorder_id': new_picking})
458+ self.action_move(cr, uid, [new_picking], context=context)
459+ wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_done', cr)
460+ wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
461+ delivered_pack_id = pick.id
462+ back_order_name = self.browse(cr, uid, delivered_pack_id, context=context).name
463+ self.message_post(cr, uid, new_picking, body=_("Back order <em>%s</em> has been <b>created</b>.") % (back_order_name), context=context)
464+ else:
465+ # (###1) pass carrier_id information on done picking
466+ if 'carrier_id' in partial_datas and partial_datas['carrier_id']:
467+ self.write(cr, uid, [pick.id], {'carrier_id': partial_datas['carrier_id']}, context=context)
468+ self.action_move(cr, uid, [pick.id], context=context)
469+ wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
470+ delivered_pack_id = pick.id
471+
472+ delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context)
473+ res[pick.id] = {'delivered_picking': delivered_pack.id or False}
474+
475+ return res
476+
477+
478+class StockMove(orm.Model):
479+ _inherit = 'stock.move'
480+
481+ _columns = {
482+ 'dispatch_state': fields.related('dispatch_id', 'state',
483+ type='char',
484+ relation='picking.dispatch',
485+ string='Dispatch State',
486+ readonly=True),
487+ }
488+
489+ def copy_data(self, cr, uid, id, default=None, context=None):
490+ """Override because in shortage context done moves are created from copies of undone moves
491+ We need dispatch_id information on done move"""
492+ if default is None:
493+ default = {}
494+ dispatch_id = False
495+ if 'dispatch_id' in default and default['dispatch_id']:
496+ dispatch_id = default['dispatch_id']
497+ default = default.copy()
498+ res = super(StockMove, self).\
499+ copy_data(cr, uid, id, default=default, context=context)
500+ if dispatch_id:
501+ res.update({'dispatch_id': dispatch_id})
502+ return res
503+
504+ def do_partial(self, cr, uid, ids, partial_datas, context=None):
505+ """ Inherited to allow the use of do_partial_via_dispatch()
506+ instead of do_partial(), switch is done with
507+ a 'partial_via_dispatch' key in the context.
508+
509+ @param partial_datas : Dictionary containing details of partial picking
510+ like partner_id, address_id, delivery_date,
511+ delivery moves with product_id, product_qty, uom
512+ @return: Dictionary of values
513+ """
514+ if context is None:
515+ context = {}
516+ if context.get('partial_via_dispatch'):
517+ return self.do_partial_via_dispatch(
518+ cr, uid, ids, partial_datas, context=context)
519+ else:
520+ return super(StockMove, self).do_partial(
521+ cr, uid, ids, partial_datas, context=context)
522+
523+ def do_partial_via_dispatch(self, cr, uid, ids, partial_datas, context=None):
524+ """ Copy of do_partial on stock.move in stock/stock.py (no call to super)
525+ the behaviour changes from interaction with dispatch (l.552)
526+ Makes picking dispatch done, split moves between ok and other in backorders.
527+ @param partial_datas: Dictionary containing details of partial picking
528+ like partner_id, delivery_date, delivery
529+ moves with product_id, product_qty, uom
530+ """
531+ product_obj = self.pool.get('product.product')
532+ currency_obj = self.pool.get('res.currency')
533+ pricetype_obj = self.pool.get('product.price.type')
534+ uom_obj = self.pool.get('product.uom')
535+ dispatch_obj = self.pool.get('picking.dispatch')
536+
537+ if context is None:
538+ context = {}
539+
540+ complete, too_many, too_few = [], [], []
541+ move_product_qty = {}
542+ prodlot_ids = {}
543+ for move in self.browse(cr, uid, ids, context=context):
544+ if move.state in ('done', 'cancel'):
545+ continue
546+ partial_data = partial_datas.get('move%s' % (move.id), False)
547+ assert partial_data, _('Missing partial picking data for move #%s.') % (move.id)
548+ product_qty = partial_data.get('product_qty', 0.0)
549+ move_product_qty[move.id] = product_qty
550+ product_uom = partial_data.get('product_uom', False)
551+ product_price = partial_data.get('product_price', 0.0)
552+ product_currency = partial_data.get('product_currency', False)
553+ prodlot_ids[move.id] = partial_data.get('prodlot_id')
554+ if move.product_qty == product_qty:
555+ complete.append(move)
556+ elif move.product_qty > product_qty:
557+ too_few.append(move)
558+ else:
559+ too_many.append(move)
560+
561+ # Average price computation
562+ if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
563+ product = product_obj.browse(cr, uid, move.product_id.id)
564+ move_currency_id = move.company_id.currency_id.id
565+ context['currency_id'] = move_currency_id
566+ qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
567+ price_type_id = pricetype_obj.search(cr, uid,
568+ [('field', '=', 'standard_price')],
569+ context=context)[0]
570+ price_type = pricetype_obj.browse(cr, uid, price_type_id, context=context)
571+ price_type_currency_id = price_type.currency_id.id
572+ if qty > 0:
573+ new_price = currency_obj.compute(cr, uid, product_currency,
574+ move_currency_id, product_price, round=False)
575+ new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
576+ product.uom_id.id)
577+ if product.qty_available <= 0:
578+ new_std_price = new_price
579+ else:
580+ # Get the standard price
581+ amount_unit = product.price_get('standard_price', context=context)[product.id]
582+ # Here we must convert the new price computed in the currency of the price_type
583+ # of the product (e.g. company currency: EUR, price_type: USD)
584+ # The current value is still in company currency at this stage
585+ new_std_price = ((amount_unit * product.qty_available)
586+ + (new_price * qty))/(product.qty_available + qty)
587+ # Convert the price in price_type currency
588+ new_std_price = currency_obj.compute(
589+ cr, uid, move_currency_id,
590+ price_type_currency_id, new_std_price)
591+ # Write the field according to price type field
592+ product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
593+
594+ # Record the values that were chosen in the wizard, so they can be
595+ # used for inventory valuation if real-time valuation is enabled.
596+ self.write(cr, uid, [move.id],
597+ {'price_unit': product_price,
598+ 'price_currency_id': product_currency,
599+ })
600+
601+ for move in too_few:
602+ product_qty = move_product_qty[move.id]
603+ if product_qty != 0:
604+ defaults = {
605+ 'product_qty': product_qty,
606+ 'product_uos_qty': product_qty,
607+ 'picking_id': move.picking_id.id,
608+ 'state': 'assigned',
609+ 'move_dest_id': move.move_dest_id.id,
610+ 'price_unit': move.price_unit,
611+ }
612+ prodlot_id = prodlot_ids[move.id]
613+ if prodlot_id:
614+ defaults['prodlot_id'] = prodlot_id
615+ new_move = self.copy(cr, uid, move.id, defaults)
616+ complete.append(self.browse(cr, uid, new_move))
617+ self.write(cr, uid, [move.id],
618+ {'product_qty': move.product_qty - product_qty,
619+ 'product_uos_qty': move.product_qty - product_qty,
620+ 'prodlot_id': False,
621+ 'tracking_id': False,
622+ })
623+
624+ for move in too_many:
625+ self.write(cr, uid, [move.id],
626+ {'product_qty': move.product_qty,
627+ 'product_uos_qty': move.product_qty,
628+ })
629+ complete.append(move)
630+
631+ for move in complete:
632+ if prodlot_ids.get(move.id):
633+ self.write(cr, uid, [move.id], {'prodlot_id': prodlot_ids.get(move.id)})
634+
635+ '''in complete_move_ids, we have:
636+ * moves that were fully processed
637+ * newly created moves belonging
638+ to the same dispatch as the original move
639+ so the difference between the original set of moves
640+ and the complete_moves is the set of unprocessed moves'''
641+ dispatch_id = 'active_id' in context and context['active_id']
642+ ids = self.search(cr, uid,
643+ [('dispatch_id', '=', dispatch_id)],
644+ context=context)
645+ complete_move_ids = [x.id for x in complete]
646+ unprocessed_move_ids = set(ids) - set(complete_move_ids)
647+ # if there are still unprocessed_moves, we need to create a dispatch backorder.
648+ if unprocessed_move_ids:
649+ new_dispatch_id = dispatch_obj.copy(cr, uid, dispatch_id,
650+ {'backorder_id': dispatch_id})
651+ self.write(cr, uid, complete_move_ids,
652+ {'dispatch_id': dispatch_id})
653+ self.write(cr, uid, list(unprocessed_move_ids),
654+ {'dispatch_id': new_dispatch_id})
655+ dispatch_obj.write(cr, uid, [dispatch_id], {'state': 'done'},
656+ context=context)
657+ # to display the correct dispatch, we need to focus explicitly on (cause switch ids)
658+ return {
659+ 'domain': str([('id', '=', dispatch_id)]),
660+ 'view_type': 'form',
661+ 'view_mode': 'form',
662+ 'res_model': 'picking.dispatch',
663+ 'type': 'ir.actions.act_window',
664+ 'context': context,
665+ 'res_id': dispatch_id,
666+ }
667+ else:
668+ dispatch_obj.write(cr, uid, [dispatch_id], {'state': 'done'},
669+ context=context)
670+ return {'type': 'ir.actions.act_window_close'}
671
672=== added file 'picking_dispatch_picking_oriented/dispatch_view.xml'
673--- picking_dispatch_picking_oriented/dispatch_view.xml 1970-01-01 00:00:00 +0000
674+++ picking_dispatch_picking_oriented/dispatch_view.xml 2014-05-14 15:41:52 +0000
675@@ -0,0 +1,59 @@
676+<?xml version="1.0" encoding="utf-8"?>
677+<openerp>
678+ <data>
679+
680+ <!-- picking dispatch : hides stock moves tab if dispatch is done -->
681+ <record id="view_dispatch_picking_oriented_form" model="ir.ui.view">
682+ <field name="name">picking.dispatch.form.picking.oriented</field>
683+ <field name="model">picking.dispatch</field>
684+ <field name="inherit_id" ref="picking_dispatch.picking_dispatch_form"/>
685+ <field name="arch" type="xml">
686+ <page string="Stock Moves" position="attributes">
687+ <attribute name="attrs">{'invisible':[('state','=','done')]}</attribute>
688+ </page>
689+ </field>
690+ </record>
691+
692+ <!-- stock picking : up dispatch infos -->
693+ <record id="view_picking_form_dispatch_picking_oriented" model="ir.ui.view">
694+ <field name="name">stock.picking.form.dispatch.int.picking.oriented</field>
695+ <field name="model">stock.picking</field>
696+ <field name="inherit_id" ref="picking_dispatch.view_picking_form_int"/>
697+ <field name="arch" type="xml">
698+ <xpath expr="//form/sheet/group" position="after">
699+ <field name="related_dispatch_ids"/>
700+ </xpath>
701+ <page string="Related Dispatch" position="replace">
702+ </page>
703+ </field>
704+ </record>
705+
706+ <!-- stock move : add dispatch state on tree view -->
707+ <record id="view_move_picking_tree_dispatch_picking_oriented" model="ir.ui.view">
708+ <field name="name">stock.move.picking.tree.picking.oriented</field>
709+ <field name="model">stock.move</field>
710+ <field name="inherit_id" ref="stock.view_move_picking_tree"/>
711+ <field name="arch" type="xml">
712+ <field name="product_qty" position="after">
713+ <field name="dispatch_id"/>
714+ <field name="dispatch_state"/>
715+ </field>
716+ </field>
717+ </record>
718+
719+ <!-- stock partial picking : add carrier_id on stock partial picking wizard -->
720+ <record id="add_carrier_id_on_stock_partial_picking_form" model="ir.ui.view">
721+ <field name="name">add.carrier.id.on.stock.partial.picking.form</field>
722+ <field name="model">stock.partial.picking</field>
723+ <field name="inherit_id" ref="stock.stock_partial_picking_form"/>
724+ <field name="arch" type="xml">
725+ <field name="move_ids" position="before">
726+ <group>
727+ <field name="carrier_id"/>
728+ </group>
729+ </field>
730+ </field>
731+ </record>
732+
733+ </data>
734+</openerp>
735
736=== added directory 'picking_dispatch_picking_oriented/test'
737=== added file 'picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml'
738--- picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml 1970-01-01 00:00:00 +0000
739+++ picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml 2014-05-14 15:41:52 +0000
740@@ -0,0 +1,97 @@
741+-
742+ I create an outgoing picking with 2 moves.
743+-
744+ !record {model: stock.picking.out, id: ship_out_1}:
745+ name: OUT_001
746+-
747+ !record {model: stock.move, id: move_out_a}:
748+ product_id: product.product_product_11
749+ product_qty: 4
750+ product_uom: product.product_uom_unit
751+ location_id: stock.stock_location_components
752+ location_dest_id: stock.stock_location_output
753+ picking_id: ship_out_1
754+-
755+ !record {model: stock.move, id: move_out_b}:
756+ product_id: product.product_product_10
757+ product_qty: 4
758+ product_uom: product.product_uom_unit
759+ location_id: stock.stock_location_components
760+ location_dest_id: stock.stock_location_output
761+ picking_id: ship_out_1
762+-
763+ I confirm the outgoing picking and force assign it.
764+-
765+ !workflow {model: stock.picking, action: button_confirm, ref: ship_out_1}
766+-
767+ !python {model: stock.picking}: |
768+ self.force_assign(cr, uid, [ref("ship_out_1")])
769+-
770+ I create a dispatch and I link it with the 2 moves.
771+-
772+ !record {model: picking.dispatch, id: dispatch_1}:
773+ name: Dispatch_1
774+ picker_id: base.user_demo
775+-
776+ !python {model: stock.move}: |
777+ self.write(cr, uid, [ref("move_out_a"),ref("move_out_b")], {'dispatch_id':ref("dispatch_1")})
778+-
779+ I assign the dispatch
780+-
781+ !python {model: picking.dispatch}: |
782+ self.action_assign(cr, uid, [ref("dispatch_1")])
783+-
784+ I confirm the dispatch
785+-
786+ !python {model: picking.dispatch}: |
787+ self.action_progress(cr, uid, [ref("dispatch_1")])
788+-
789+ I process the dispatch, it displays a wizard where I choose quantities that I pick.
790+-
791+ !python {model: stock.partial.move}: |
792+ context.update({'active_model': 'stock.move', 'active_id': ref('dispatch_1'), 'active_ids': [ref('move_out_a'),ref('move_out_b')], 'partial_via_dispatch': True})
793+-
794+ !record {model: stock.partial.move, id: partial_move_dispatch}:
795+ move_ids:
796+ - quantity: 1
797+ product_id: product.product_product_11
798+ product_uom: product.product_uom_unit
799+ move_id: move_out_a
800+ location_id: stock.stock_location_components
801+ location_dest_id: stock.stock_location_output
802+ - quantity: 3
803+ product_id: product.product_product_10
804+ product_uom: product.product_uom_unit
805+ move_id: move_out_b
806+ location_id: stock.stock_location_components
807+ location_dest_id: stock.stock_location_output
808+-
809+ !python {model: stock.partial.move }: |
810+ self.do_partial(cr, uid, [ref('partial_move_dispatch')], context=context)
811+-
812+ I deliver outgoing shipment linked to the dispatch, only moves with
813+-
814+ !python {model: stock.partial.picking}: |
815+ context.update({'active_model': 'stock.picking', 'active_id': ref('ship_out_1'), 'active_ids': [ref('ship_out_1')]})
816+-
817+ !record {model: stock.partial.picking, id: partial_outgoing}:
818+ picking_id: ship_out_1
819+-
820+ !python {model: stock.partial.picking }: |
821+ self.do_partial(cr, uid, [ref('partial_outgoing')], context=context)
822+-
823+ I check outgoing shipment backorder.
824+-
825+ !python {model: stock.picking}: |
826+ shipment = self.browse(cr, uid, ref("ship_out_1"), context=context)
827+ for move_line in shipment.move_lines:
828+ if move_line.id == ref("move_out_a"):
829+ assert move_line.product_qty == 3.0, "Move Quantity from a should be 3"
830+ if move_line.id == ref("move_out_b"):
831+ assert move_line.product_qty == 1.0, "Move Quantity from b should be 1"
832+-
833+ I check if the picking dispatch backorder exists
834+-
835+ !python {model: picking.dispatch}: |
836+ backorder = self.search(cr, uid, [('backorder_id','=',ref("dispatch_1"))], context=context)
837+ assert backorder, "the backorder exists"

Subscribers

People subscribed via source and target branches