Merge lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde into lp:stock-logistic-flows
- 7.0-picking_dispatch_picking_oriented_use-rde
- Merge into 7.0
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 |
Related bugs: |
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 |
Commit message
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
Leonardo Pistone (lepistone) wrote : | # |
Romain Deheele - Camptocamp (romaindeheele) wrote : | # |
Hi Leonardo,
I added some tests.
Romain
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
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.
Leonardo Pistone (lepistone) wrote : | # |
thanks romain
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.
* 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"
* missing context on most of write method calls.
* do_partial and do_partial_
To identify original superseeded code, you might add some strong comment saying which part has been modified where it start and where it ends.
- 65. By Romain Deheele - Camptocamp
-
[UPD] add comment with more details
- 66. By Romain Deheele - Camptocamp
-
[UPD] small improvements
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
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.
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote : | # |
The source code management for this project has been moved to https:/
Could you resubmit this MP on the new site?
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
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" |
Thanks Romain
77 + "test": [],