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

Subscribers

People subscribed via source and target branches