Merge lp:~camptocamp/stock-logistic-warehouse/7.0-stock_reserve into lp:stock-logistic-warehouse
- 7.0-stock_reserve
- Merge into 7.0
Proposed by
Guewen Baconnier @ Camptocamp
Status: | Merged |
---|---|
Merged at revision: | 31 |
Proposed branch: | lp:~camptocamp/stock-logistic-warehouse/7.0-stock_reserve |
Merge into: | lp:stock-logistic-warehouse |
Diff against target: |
1495 lines (+1373/-0) 22 files modified
stock_reserve/__init__.py (+22/-0) stock_reserve/__openerp__.py (+58/-0) stock_reserve/data/stock_data.xml (+26/-0) stock_reserve/model/__init__.py (+23/-0) stock_reserve/model/product.py (+40/-0) stock_reserve/model/stock_reserve.py (+177/-0) stock_reserve/security/ir.model.access.csv (+3/-0) stock_reserve/test/stock_reserve.yml (+63/-0) stock_reserve/view/product.xml (+19/-0) stock_reserve/view/stock_reserve.xml (+140/-0) stock_reserve_sale/__init__.py (+23/-0) stock_reserve_sale/__openerp__.py (+65/-0) stock_reserve_sale/model/__init__.py (+23/-0) stock_reserve_sale/model/sale.py (+184/-0) stock_reserve_sale/model/stock_reserve.py (+50/-0) stock_reserve_sale/test/sale_line_reserve.yml (+118/-0) stock_reserve_sale/test/sale_reserve.yml (+65/-0) stock_reserve_sale/view/sale.xml (+67/-0) stock_reserve_sale/view/stock_reserve.xml (+30/-0) stock_reserve_sale/wizard/__init__.py (+22/-0) stock_reserve_sale/wizard/sale_stock_reserve.py (+111/-0) stock_reserve_sale/wizard/sale_stock_reserve_view.xml (+44/-0) |
To merge this branch: | bzr merge lp:~camptocamp/stock-logistic-warehouse/7.0-stock_reserve |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Joël Grand-Guillaume @ camptocamp | code review + tests | Approve | |
Review via email: mp+184731@code.launchpad.net |
Commit message
[ADD] stock_reserve, stock_reserve_sale: create stock reservation manually or from quotations.
Description of the change
Addition of 2 (related) modules:
stock_reserve: allows to manually create stock reservations, basically a stock move from a source location to a reservation location.
stock_reserve_sale: allows to create the reservations (the same than the 'stock_reserve' addon) directly from quotations or quotation lines. The reservations are released when a quotation is confirmed or cancelled.
To post a comment you must log in.
- 55. By Guewen Baconnier @ Camptocamp
-
[CHG] hide the stock reservation on the line's form
- 56. By Guewen Baconnier @ Camptocamp
-
[FIX] in form view of sales line, remove the update button (automatic) and remove the cancel button (already displayed on the header)
Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote : | # |
review:
Approve
(code review + tests)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'stock_reserve' |
2 | === added file 'stock_reserve/__init__.py' |
3 | --- stock_reserve/__init__.py 1970-01-01 00:00:00 +0000 |
4 | +++ stock_reserve/__init__.py 2013-09-10 07:53:22 +0000 |
5 | @@ -0,0 +1,22 @@ |
6 | +# -*- coding: utf-8 -*- |
7 | +############################################################################## |
8 | +# |
9 | +# Author: Guewen Baconnier |
10 | +# Copyright 2013 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 model |
28 | |
29 | === added file 'stock_reserve/__openerp__.py' |
30 | --- stock_reserve/__openerp__.py 1970-01-01 00:00:00 +0000 |
31 | +++ stock_reserve/__openerp__.py 2013-09-10 07:53:22 +0000 |
32 | @@ -0,0 +1,58 @@ |
33 | +# -*- coding: utf-8 -*- |
34 | +############################################################################## |
35 | +# |
36 | +# Author: Guewen Baconnier |
37 | +# Copyright 2013 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 | +{'name': 'Stock Reserve', |
55 | + 'version': '0.1', |
56 | + 'author': 'Camptocamp', |
57 | + 'category': 'Warehouse', |
58 | + 'license': 'AGPL-3', |
59 | + 'complexity': 'normal', |
60 | + 'images': [], |
61 | + 'website': "http://www.camptocamp.com", |
62 | + 'description': """ |
63 | +Stock Reserve |
64 | +============= |
65 | + |
66 | +Allows to create stock reservations on products. |
67 | + |
68 | +Each reservation can have a validity date, once passed, the reservation |
69 | +is automatically lifted. |
70 | + |
71 | +The reserved products are substracted from the virtual stock. It means |
72 | +that if you reserved a quantity of products which bring the virtual |
73 | +stock below the minimum, the orderpoint will be triggered and new |
74 | +purchase orders will be generated. It also implies that the max may be |
75 | +exceeded if the reservations are canceled. |
76 | + |
77 | +""", |
78 | + 'depends': ['stock', |
79 | + ], |
80 | + 'demo': [], |
81 | + 'data': ['view/stock_reserve.xml', |
82 | + 'view/product.xml', |
83 | + 'data/stock_data.xml', |
84 | + 'security/ir.model.access.csv', |
85 | + ], |
86 | + 'auto_install': False, |
87 | + 'test': ['test/stock_reserve.yml', |
88 | + ], |
89 | + 'installable': True, |
90 | + } |
91 | |
92 | === added directory 'stock_reserve/data' |
93 | === added file 'stock_reserve/data/stock_data.xml' |
94 | --- stock_reserve/data/stock_data.xml 1970-01-01 00:00:00 +0000 |
95 | +++ stock_reserve/data/stock_data.xml 2013-09-10 07:53:22 +0000 |
96 | @@ -0,0 +1,26 @@ |
97 | +<?xml version="1.0" encoding="utf-8"?> |
98 | +<openerp> |
99 | + <data noupdate="1"> |
100 | + <record id="stock_location_reservation" model="stock.location"> |
101 | + <field name="name">Reservation Stock</field> |
102 | + <field name="location_id" ref="stock.stock_location_company"/> |
103 | + </record> |
104 | + |
105 | + |
106 | + <!-- Release the stock.reservation when the validity date has |
107 | + passed --> |
108 | + <record forcecreate="True" id="ir_cron_release_stock_reservation" model="ir.cron"> |
109 | + <field name="name">Release the stock reservation having a passed validity date</field> |
110 | + <field eval="True" name="active" /> |
111 | + <field name="user_id" ref="base.user_root" /> |
112 | + <field name="interval_number">1</field> |
113 | + <field name="interval_type">days</field> |
114 | + <field name="numbercall">-1</field> |
115 | + <field eval="False" name="doall" /> |
116 | + <field name="model">stock.reservation</field> |
117 | + <field name="function">release_validity_exceeded</field> |
118 | + <field name="args">()</field> |
119 | + </record> |
120 | + |
121 | + </data> |
122 | +</openerp> |
123 | |
124 | === added directory 'stock_reserve/i18n' |
125 | === added directory 'stock_reserve/model' |
126 | === added file 'stock_reserve/model/__init__.py' |
127 | --- stock_reserve/model/__init__.py 1970-01-01 00:00:00 +0000 |
128 | +++ stock_reserve/model/__init__.py 2013-09-10 07:53:22 +0000 |
129 | @@ -0,0 +1,23 @@ |
130 | +# -*- coding: utf-8 -*- |
131 | +############################################################################## |
132 | +# |
133 | +# Author: Guewen Baconnier |
134 | +# Copyright 2013 Camptocamp SA |
135 | +# |
136 | +# This program is free software: you can redistribute it and/or modify |
137 | +# it under the terms of the GNU Affero General Public License as |
138 | +# published by the Free Software Foundation, either version 3 of the |
139 | +# License, or (at your option) any later version. |
140 | +# |
141 | +# This program is distributed in the hope that it will be useful, |
142 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
143 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
144 | +# GNU Affero General Public License for more details. |
145 | +# |
146 | +# You should have received a copy of the GNU Affero General Public License |
147 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
148 | +# |
149 | +############################################################################## |
150 | + |
151 | +from . import stock_reserve |
152 | +from . import product |
153 | |
154 | === added file 'stock_reserve/model/product.py' |
155 | --- stock_reserve/model/product.py 1970-01-01 00:00:00 +0000 |
156 | +++ stock_reserve/model/product.py 2013-09-10 07:53:22 +0000 |
157 | @@ -0,0 +1,40 @@ |
158 | +# -*- coding: utf-8 -*- |
159 | +############################################################################## |
160 | +# |
161 | +# Author: Guewen Baconnier |
162 | +# Copyright 2013 Camptocamp SA |
163 | +# |
164 | +# This program is free software: you can redistribute it and/or modify |
165 | +# it under the terms of the GNU Affero General Public License as |
166 | +# published by the Free Software Foundation, either version 3 of the |
167 | +# License, or (at your option) any later version. |
168 | +# |
169 | +# This program is distributed in the hope that it will be useful, |
170 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
171 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
172 | +# GNU Affero General Public License for more details. |
173 | +# |
174 | +# You should have received a copy of the GNU Affero General Public License |
175 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
176 | +# |
177 | +############################################################################## |
178 | + |
179 | +from openerp.osv import orm, fields |
180 | + |
181 | + |
182 | +class product_product(orm.Model): |
183 | + _inherit = 'product.product' |
184 | + |
185 | + def open_stock_reservation(self, cr, uid, ids, context=None): |
186 | + assert len(ids) == 1, "Expected 1 ID, got %r" % ids |
187 | + mod_obj = self.pool.get('ir.model.data') |
188 | + act_obj = self.pool.get('ir.actions.act_window') |
189 | + get_ref = mod_obj.get_object_reference |
190 | + __, action_id = get_ref(cr, uid, 'stock_reserve', |
191 | + 'action_stock_reservation') |
192 | + action = act_obj.read(cr, uid, action_id, context=context) |
193 | + action['context'] = {'search_default_draft': 1, |
194 | + 'search_default_reserved': 1, |
195 | + 'default_product_id': ids[0], |
196 | + 'search_default_product_id': ids[0]} |
197 | + return action |
198 | |
199 | === added file 'stock_reserve/model/stock_reserve.py' |
200 | --- stock_reserve/model/stock_reserve.py 1970-01-01 00:00:00 +0000 |
201 | +++ stock_reserve/model/stock_reserve.py 2013-09-10 07:53:22 +0000 |
202 | @@ -0,0 +1,177 @@ |
203 | +# -*- coding: utf-8 -*- |
204 | +############################################################################## |
205 | +# |
206 | +# Author: Guewen Baconnier |
207 | +# Copyright 2013 Camptocamp SA |
208 | +# |
209 | +# This program is free software: you can redistribute it and/or modify |
210 | +# it under the terms of the GNU Affero General Public License as |
211 | +# published by the Free Software Foundation, either version 3 of the |
212 | +# License, or (at your option) any later version. |
213 | +# |
214 | +# This program is distributed in the hope that it will be useful, |
215 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
216 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
217 | +# GNU Affero General Public License for more details. |
218 | +# |
219 | +# You should have received a copy of the GNU Affero General Public License |
220 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
221 | +# |
222 | +############################################################################## |
223 | + |
224 | +from openerp.osv import orm, fields |
225 | +from openerp.tools.translate import _ |
226 | + |
227 | + |
228 | +class stock_reservation(orm.Model): |
229 | + """ Allow to reserve products. |
230 | + |
231 | + The fields mandatory for the creation of a reservation are: |
232 | + |
233 | + * product_id |
234 | + * product_qty |
235 | + * product_uom |
236 | + * name |
237 | + |
238 | + The following fields are required but have default values that you may |
239 | + want to override: |
240 | + |
241 | + * company_id |
242 | + * location_id |
243 | + * dest_location_id |
244 | + |
245 | + Optionally, you may be interested to define: |
246 | + |
247 | + * date_validity (once passed, the reservation will be released) |
248 | + * note |
249 | + """ |
250 | + _name = 'stock.reservation' |
251 | + _description = 'Stock Reservation' |
252 | + _inherits = {'stock.move': 'move_id'} |
253 | + |
254 | + _columns = { |
255 | + 'move_id': fields.many2one('stock.move', |
256 | + 'Reservation Move', |
257 | + required=True, |
258 | + readonly=True, |
259 | + ondelete='cascade', |
260 | + select=1), |
261 | + 'date_validity': fields.date('Validity Date'), |
262 | + } |
263 | + |
264 | + def get_location_from_ref(self, cr, uid, ref, context=None): |
265 | + """ Get a location from a xmlid if allowed |
266 | + :param ref: tuple (module, xmlid) |
267 | + """ |
268 | + location_obj = self.pool.get('stock.location') |
269 | + data_obj = self.pool.get('ir.model.data') |
270 | + get_ref = data_obj.get_object_reference |
271 | + try: |
272 | + __, location_id = get_ref(cr, uid, *ref) |
273 | + location_obj.check_access_rule(cr, uid, [location_id], |
274 | + 'read', context=context) |
275 | + except (orm.except_orm, ValueError): |
276 | + location_id = False |
277 | + return location_id |
278 | + |
279 | + def _default_location_id(self, cr, uid, context=None): |
280 | + if context is None: |
281 | + context = {} |
282 | + move_obj = self.pool.get('stock.move') |
283 | + context['picking_type'] = 'internal' |
284 | + return move_obj._default_location_source(cr, uid, context=context) |
285 | + |
286 | + def _default_location_dest_id(self, cr, uid, context=None): |
287 | + ref = ('stock_reserve', 'stock_location_reservation') |
288 | + return self.get_location_from_ref(cr, uid, ref, context=context) |
289 | + |
290 | + _defaults = { |
291 | + 'type': 'internal', |
292 | + 'location_id': _default_location_id, |
293 | + 'location_dest_id': _default_location_dest_id, |
294 | + 'product_qty': 1.0, |
295 | + } |
296 | + |
297 | + def reserve(self, cr, uid, ids, context=None): |
298 | + """ Confirm a reservation |
299 | + |
300 | + The reservation is done using the default UOM of the product. |
301 | + A date until which the product is reserved can be specified. |
302 | + """ |
303 | + move_obj = self.pool.get('stock.move') |
304 | + reservations = self.browse(cr, uid, ids, context=context) |
305 | + move_ids = [reserv.move_id.id for reserv in reservations] |
306 | + move_obj.write(cr, uid, move_ids, |
307 | + {'date_expected': fields.datetime.now()}, |
308 | + context=context) |
309 | + move_obj.action_confirm(cr, uid, move_ids, context=context) |
310 | + move_obj.force_assign(cr, uid, move_ids, context=context) |
311 | + return True |
312 | + |
313 | + def release(self, cr, uid, ids, context=None): |
314 | + if isinstance(ids, (int, long)): |
315 | + ids = [ids] |
316 | + reservations = self.read(cr, uid, ids, ['move_id'], |
317 | + context=context, load='_classic_write') |
318 | + move_obj = self.pool.get('stock.move') |
319 | + move_ids = [reserv['move_id'] for reserv in reservations] |
320 | + move_obj.action_cancel(cr, uid, move_ids, context=context) |
321 | + return True |
322 | + |
323 | + def release_validity_exceeded(self, cr, uid, ids=None, context=None): |
324 | + """ Release all the reservation having an exceeded validity date """ |
325 | + domain = [('date_validity', '<', fields.date.today()), |
326 | + ('state', '=', 'assigned')] |
327 | + if ids: |
328 | + domain.append(('id', 'in', ids)) |
329 | + reserv_ids = self.search(cr, uid, domain, context=context) |
330 | + self.release(cr, uid, reserv_ids, context=context) |
331 | + return True |
332 | + |
333 | + def unlink(self, cr, uid, ids, context=None): |
334 | + """ Release the reservation before the unlink """ |
335 | + self.release(cr, uid, ids, context=context) |
336 | + return super(stock_reservation, self).unlink(cr, uid, ids, |
337 | + context=context) |
338 | + |
339 | + def onchange_product_id(self, cr, uid, ids, product_id=False, context=None): |
340 | + move_obj = self.pool.get('stock.move') |
341 | + if ids: |
342 | + reserv = self.read(cr, uid, ids, ['move_id'], context=context, |
343 | + load='_classic_write') |
344 | + move_ids = [rv['move_id'] for rv in reserv] |
345 | + else: |
346 | + move_ids = [] |
347 | + result = move_obj.onchange_product_id( |
348 | + cr, uid, move_ids, prod_id=product_id, loc_id=False, |
349 | + loc_dest_id=False, partner_id=False) |
350 | + if result.get('value'): |
351 | + vals = result['value'] |
352 | + # only keep the existing fields on the view |
353 | + keep = ('product_uom', 'name') |
354 | + result['value'] = dict((key, value) for key, value in |
355 | + result['value'].iteritems() if |
356 | + key in keep) |
357 | + return result |
358 | + |
359 | + def onchange_quantity(self, cr, uid, ids, product_id, product_qty, context=None): |
360 | + """ On change of product quantity avoid negative quantities """ |
361 | + if not product_id or product_qty <= 0.0: |
362 | + return {'value': {'product_qty': 0.0}} |
363 | + return {} |
364 | + |
365 | + def open_move(self, cr, uid, ids, context=None): |
366 | + assert len(ids) == 1, "1 ID expected, got %r" % ids |
367 | + reserv = self.read(cr, uid, ids[0], ['move_id'], context=context, |
368 | + load='_classic_write') |
369 | + mod_obj = self.pool.get('ir.model.data') |
370 | + act_obj = self.pool.get('ir.actions.act_window') |
371 | + get_ref = mod_obj.get_object_reference |
372 | + __, action_id = get_ref(cr, uid, 'stock', 'action_move_form2') |
373 | + action = act_obj.read(cr, uid, action_id, context=context) |
374 | + action['name'] = _('Reservation Move') |
375 | + # open directly in the form view |
376 | + __, view_id = get_ref(cr, uid, 'stock', 'view_move_form') |
377 | + action['views'] = [(view_id, 'form')] |
378 | + action['res_id'] = reserv['move_id'] |
379 | + return action |
380 | |
381 | === added directory 'stock_reserve/security' |
382 | === added file 'stock_reserve/security/ir.model.access.csv' |
383 | --- stock_reserve/security/ir.model.access.csv 1970-01-01 00:00:00 +0000 |
384 | +++ stock_reserve/security/ir.model.access.csv 2013-09-10 07:53:22 +0000 |
385 | @@ -0,0 +1,3 @@ |
386 | +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
387 | +access_stock_reservation_manager,stock.reservation manager,model_stock_reservation,stock.group_stock_manager,1,1,1,1 |
388 | +access_stock_reservation_user,stock.reservation user,model_stock_reservation,stock.group_stock_user,1,1,1,0 |
389 | |
390 | === added directory 'stock_reserve/test' |
391 | === added file 'stock_reserve/test/stock_reserve.yml' |
392 | --- stock_reserve/test/stock_reserve.yml 1970-01-01 00:00:00 +0000 |
393 | +++ stock_reserve/test/stock_reserve.yml 2013-09-10 07:53:22 +0000 |
394 | @@ -0,0 +1,63 @@ |
395 | +- |
396 | + I create a product to test the stock reservation |
397 | +- |
398 | + !record {model: product.product, id: product_sorbet}: |
399 | + default_code: 001SORBET |
400 | + name: Sorbet |
401 | + type: product |
402 | + categ_id: product.product_category_1 |
403 | + list_price: 100.0 |
404 | + standard_price: 70.0 |
405 | + uom_id: product.product_uom_kgm |
406 | + uom_po_id: product.product_uom_kgm |
407 | + procure_method: make_to_stock |
408 | + valuation: real_time |
409 | + cost_method: average |
410 | + property_stock_account_input: account.o_expense |
411 | + property_stock_account_output: account.o_income |
412 | +- |
413 | + I update the current stock of the Sorbet with 10 kgm |
414 | +- |
415 | + !record {model: stock.change.product.qty, id: change_qty}: |
416 | + new_quantity: 10 |
417 | + product_id: product_sorbet |
418 | +- |
419 | + !python {model: stock.change.product.qty}: | |
420 | + context['active_id'] = ref('stock_reserve.product_sorbet') |
421 | + self.change_product_qty(cr, uid, [ref('change_qty')], context=context) |
422 | +- |
423 | + I check Virtual stock of Sorbet after update stock. |
424 | +- |
425 | + !python {model: product.product}: | |
426 | + product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context) |
427 | + assert product.virtual_available == 10, "Stock is not updated." |
428 | +- |
429 | + I create a stock reservation for 5 kgm |
430 | +- |
431 | + !record {model: stock.reservation, id: reserv_sorbet1}: |
432 | + product_id: product_sorbet |
433 | + product_qty: 5.0 |
434 | + product_uom: product.product_uom_kgm |
435 | + name: reserve 5 kgm of sorbet for test |
436 | +- |
437 | + I confirm the reservation |
438 | +- |
439 | + !python {model: stock.reservation}: | |
440 | + self.reserve(cr, uid, [ref('reserv_sorbet1')], context=context) |
441 | +- |
442 | + I check Virtual stock of Sorbet after update reservation |
443 | +- |
444 | + !python {model: product.product}: | |
445 | + product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context) |
446 | + assert product.virtual_available == 5, "Stock is not updated." |
447 | +- |
448 | + I release the reservation |
449 | +- |
450 | + !python {model: stock.reservation}: | |
451 | + self.release(cr, uid, [ref('reserv_sorbet1')], context=context) |
452 | +- |
453 | + I check Virtual stock of Sorbet after update reservation |
454 | +- |
455 | + !python {model: product.product}: | |
456 | + product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context) |
457 | + assert product.virtual_available == 10, "Stock is not updated." |
458 | |
459 | === added directory 'stock_reserve/view' |
460 | === added file 'stock_reserve/view/product.xml' |
461 | --- stock_reserve/view/product.xml 1970-01-01 00:00:00 +0000 |
462 | +++ stock_reserve/view/product.xml 2013-09-10 07:53:22 +0000 |
463 | @@ -0,0 +1,19 @@ |
464 | +<?xml version="1.0" encoding="utf-8"?> |
465 | +<openerp> |
466 | + <data noupdate="0"> |
467 | + |
468 | + <record model="ir.ui.view" id="product_form_view"> |
469 | + <field name="name">product.product.form.reserve</field> |
470 | + <field name="model">product.product</field> |
471 | + <field name="inherit_id" ref="procurement.product_form_view_procurement_button"/> |
472 | + <field name="arch" type="xml"> |
473 | + <xpath expr="//div[@name='buttons']" position="inside"> |
474 | + <button string="Stock Reservations" |
475 | + name="open_stock_reservation" |
476 | + type="object"/> |
477 | + </xpath> |
478 | + </field> |
479 | + </record> |
480 | + |
481 | + </data> |
482 | +</openerp> |
483 | |
484 | === added file 'stock_reserve/view/stock_reserve.xml' |
485 | --- stock_reserve/view/stock_reserve.xml 1970-01-01 00:00:00 +0000 |
486 | +++ stock_reserve/view/stock_reserve.xml 2013-09-10 07:53:22 +0000 |
487 | @@ -0,0 +1,140 @@ |
488 | +<?xml version="1.0" encoding="utf-8"?> |
489 | +<openerp> |
490 | + <data noupdate="0"> |
491 | + <record id="view_stock_reservation_form" model="ir.ui.view"> |
492 | + <field name="name">stock.reservation.form</field> |
493 | + <field name="model">stock.reservation</field> |
494 | + <field name="arch" type="xml"> |
495 | + <form string="Stock Reservations" version="7.0"> |
496 | + <header> |
497 | + <button name="reserve" type="object" |
498 | + string="Reserve" |
499 | + class="oe_highlight" |
500 | + states="draft"/> |
501 | + <button name="release" type="object" |
502 | + string="Release" |
503 | + class="oe_highlight" |
504 | + states="assigned,confirmed,done"/> |
505 | + <button name="open_move" type="object" |
506 | + string="View Reservation Move"/> |
507 | + <field name="state" widget="statusbar" |
508 | + statusbar_visible="draft,assigned"/> |
509 | + </header> |
510 | + <sheet> |
511 | + <group> |
512 | + <group name="main_grp" string="Details"> |
513 | + <field name="product_id" |
514 | + on_change="onchange_product_id(product_id)" |
515 | + /> |
516 | + <label for="product_qty" /> |
517 | + <div> |
518 | + <field name="product_qty" |
519 | + on_change="onchange_quantity(product_id, product_qty)" |
520 | + class="oe_inline"/> |
521 | + <field name="product_uom" |
522 | + groups="product.group_uom" class="oe_inline"/> |
523 | + </div> |
524 | + <field name="name"/> |
525 | + <field name="date_validity" /> |
526 | + <field name="create_date" groups="base.group_no_one"/> |
527 | + <field name="company_id" |
528 | + groups="base.group_multi_company" |
529 | + widget="selection"/> |
530 | + </group> |
531 | + <group name="location" string="Locations" |
532 | + groups="stock.group_locations"> |
533 | + <field name="location_id"/> |
534 | + <field name="location_dest_id"/> |
535 | + </group> |
536 | + <group name="note" string="Notes"> |
537 | + <field name="note" nolabel="1"/> |
538 | + </group> |
539 | + </group> |
540 | + </sheet> |
541 | + </form> |
542 | + </field> |
543 | + </record> |
544 | + |
545 | + <record id="view_stock_reservation_tree" model="ir.ui.view"> |
546 | + <field name="name">stock.reservation.tree</field> |
547 | + <field name="model">stock.reservation</field> |
548 | + <field name="arch" type="xml"> |
549 | + <tree string="Stock Reservations" version="7.0" |
550 | + colors="blue:state == 'draft';grey:state == 'cancel'" > |
551 | + <field name="name" /> |
552 | + <field name="product_id" /> |
553 | + <field name="move_id" /> |
554 | + <field name="product_qty" sum="Total" /> |
555 | + <field name="product_uom" /> |
556 | + <field name="date_validity" /> |
557 | + <field name="state"/> |
558 | + <button name="reserve" type="object" |
559 | + string="Reserve" |
560 | + icon="terp-locked" |
561 | + states="draft"/> |
562 | + <button name="release" type="object" |
563 | + string="Release" |
564 | + icon="gtk-undo" |
565 | + states="assigned,confirmed,done"/> |
566 | + </tree> |
567 | + </field> |
568 | + </record> |
569 | + |
570 | + <record id="view_stock_reservation_search" model="ir.ui.view"> |
571 | + <field name="name">stock.reservation.search</field> |
572 | + <field name="model">stock.reservation</field> |
573 | + <field name="arch" type="xml"> |
574 | + <search string="Stock Reservations" version="7.0"> |
575 | + <filter name="draft" string="Draft" |
576 | + domain="[('state', '=', 'draft')]" |
577 | + help="Not already reserved"/> |
578 | + <filter name="reserved" string="Reserved" |
579 | + domain="[('state', '=', 'assigned')]" |
580 | + help="Moves are reserved."/> |
581 | + <filter name="cancel" string="Released" |
582 | + domain="[('state', '=', 'cancel')]" |
583 | + help="Reservations have been released."/> |
584 | + <field name="name" /> |
585 | + <field name="product_id" /> |
586 | + <field name="move_id" /> |
587 | + <group expand="0" string="Group By..."> |
588 | + <filter string="Status" |
589 | + name="groupby_state" |
590 | + domain="[]" context="{'group_by': 'state'}"/> |
591 | + <filter string="Product" domain="[]" |
592 | + name="groupby_product" |
593 | + context="{'group_by': 'product_id'}"/> |
594 | + <filter string="Product UoM" domain="[]" |
595 | + name="groupby_product_uom" |
596 | + context="{'group_by': 'product_uom'}"/> |
597 | + </group> |
598 | + </search> |
599 | + </field> |
600 | + </record> |
601 | + |
602 | + <record id="action_stock_reservation" model="ir.actions.act_window"> |
603 | + <field name="name">Stock Reservations</field> |
604 | + <field name="res_model">stock.reservation</field> |
605 | + <field name="type">ir.actions.act_window</field> |
606 | + <field name="view_type">form</field> |
607 | + <field name="view_id" ref="view_stock_reservation_tree"/> |
608 | + <field name="search_view_id" ref="view_stock_reservation_search"/> |
609 | + <field name="context">{'search_default_draft': 1, |
610 | + 'search_default_reserved': 1, |
611 | + 'search_default_groupby_product': 1}</field> |
612 | + <field name="help" type="html"> |
613 | + <p class="oe_view_nocontent_create"> |
614 | + Click to create a stock reservation. |
615 | + </p><p> |
616 | + This menu allow you to prepare and reserve some quantities |
617 | + of products. |
618 | + </p> |
619 | + </field> |
620 | + </record> |
621 | + |
622 | + <menuitem action="action_stock_reservation" |
623 | + id="menu_action_stock_reservation" |
624 | + parent="stock.menu_stock_inventory_control" |
625 | + sequence="30"/> |
626 | + </data> |
627 | +</openerp> |
628 | |
629 | === added directory 'stock_reserve_sale' |
630 | === added file 'stock_reserve_sale/__init__.py' |
631 | --- stock_reserve_sale/__init__.py 1970-01-01 00:00:00 +0000 |
632 | +++ stock_reserve_sale/__init__.py 2013-09-10 07:53:22 +0000 |
633 | @@ -0,0 +1,23 @@ |
634 | +# -*- coding: utf-8 -*- |
635 | +############################################################################## |
636 | +# |
637 | +# Author: Guewen Baconnier |
638 | +# Copyright 2013 Camptocamp SA |
639 | +# |
640 | +# This program is free software: you can redistribute it and/or modify |
641 | +# it under the terms of the GNU Affero General Public License as |
642 | +# published by the Free Software Foundation, either version 3 of the |
643 | +# License, or (at your option) any later version. |
644 | +# |
645 | +# This program is distributed in the hope that it will be useful, |
646 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
647 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
648 | +# GNU Affero General Public License for more details. |
649 | +# |
650 | +# You should have received a copy of the GNU Affero General Public License |
651 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
652 | +# |
653 | +############################################################################## |
654 | + |
655 | +from . import model |
656 | +from . import wizard |
657 | |
658 | === added file 'stock_reserve_sale/__openerp__.py' |
659 | --- stock_reserve_sale/__openerp__.py 1970-01-01 00:00:00 +0000 |
660 | +++ stock_reserve_sale/__openerp__.py 2013-09-10 07:53:22 +0000 |
661 | @@ -0,0 +1,65 @@ |
662 | +# -*- coding: utf-8 -*- |
663 | +############################################################################## |
664 | +# |
665 | +# Author: Guewen Baconnier |
666 | +# Copyright 2013 Camptocamp SA |
667 | +# |
668 | +# This program is free software: you can redistribute it and/or modify |
669 | +# it under the terms of the GNU Affero General Public License as |
670 | +# published by the Free Software Foundation, either version 3 of the |
671 | +# License, or (at your option) any later version. |
672 | +# |
673 | +# This program is distributed in the hope that it will be useful, |
674 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
675 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
676 | +# GNU Affero General Public License for more details. |
677 | +# |
678 | +# You should have received a copy of the GNU Affero General Public License |
679 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
680 | +# |
681 | +############################################################################## |
682 | + |
683 | +{'name': 'Stock Reserve Sales', |
684 | + 'version': '0.1', |
685 | + 'author': 'Camptocamp', |
686 | + 'category': 'Warehouse', |
687 | + 'license': 'AGPL-3', |
688 | + 'complexity': 'normal', |
689 | + 'images': [], |
690 | + 'website': "http://www.camptocamp.com", |
691 | + 'description': """ |
692 | +Stock Reserve Sales |
693 | +=================== |
694 | + |
695 | +Allows to create stock reservations for quotation lines before the |
696 | +confirmation of the quotation. The reservations might have a validity |
697 | +date and in any case they are lifted when the quotation is canceled or |
698 | +confirmed. |
699 | + |
700 | +Reservations can be done only on "make to stock" and stockable products. |
701 | + |
702 | +The reserved products are substracted from the virtual stock. It means |
703 | +that if you reserved a quantity of products which bring the virtual |
704 | +stock below the minimum, the orderpoint will be triggered and new |
705 | +purchase orders will be generated. It also implies that the max may be |
706 | +exceeded if the reservations are canceled. |
707 | + |
708 | +If you want to prevent sales orders to be confirmed when the stock is |
709 | +insufficient at the order date, you may want to install the |
710 | +`sale_exception_nostock` module. |
711 | + |
712 | +""", |
713 | + 'depends': ['sale_stock', |
714 | + 'stock_reserve', |
715 | + ], |
716 | + 'demo': [], |
717 | + 'data': ['wizard/sale_stock_reserve_view.xml', |
718 | + 'view/sale.xml', |
719 | + 'view/stock_reserve.xml', |
720 | + ], |
721 | + 'auto_install': False, |
722 | + 'test': ['test/sale_reserve.yml', |
723 | + 'test/sale_line_reserve.yml', |
724 | + ], |
725 | + 'installable': True, |
726 | + } |
727 | |
728 | === added directory 'stock_reserve_sale/i18n' |
729 | === added directory 'stock_reserve_sale/model' |
730 | === added file 'stock_reserve_sale/model/__init__.py' |
731 | --- stock_reserve_sale/model/__init__.py 1970-01-01 00:00:00 +0000 |
732 | +++ stock_reserve_sale/model/__init__.py 2013-09-10 07:53:22 +0000 |
733 | @@ -0,0 +1,23 @@ |
734 | +# -*- coding: utf-8 -*- |
735 | +############################################################################## |
736 | +# |
737 | +# Author: Guewen Baconnier |
738 | +# Copyright 2013 Camptocamp SA |
739 | +# |
740 | +# This program is free software: you can redistribute it and/or modify |
741 | +# it under the terms of the GNU Affero General Public License as |
742 | +# published by the Free Software Foundation, either version 3 of the |
743 | +# License, or (at your option) any later version. |
744 | +# |
745 | +# This program is distributed in the hope that it will be useful, |
746 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
747 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
748 | +# GNU Affero General Public License for more details. |
749 | +# |
750 | +# You should have received a copy of the GNU Affero General Public License |
751 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
752 | +# |
753 | +############################################################################## |
754 | + |
755 | +from . import sale |
756 | +from . import stock_reserve |
757 | |
758 | === added file 'stock_reserve_sale/model/sale.py' |
759 | --- stock_reserve_sale/model/sale.py 1970-01-01 00:00:00 +0000 |
760 | +++ stock_reserve_sale/model/sale.py 2013-09-10 07:53:22 +0000 |
761 | @@ -0,0 +1,184 @@ |
762 | +# -*- coding: utf-8 -*- |
763 | +############################################################################## |
764 | +# |
765 | +# Author: Guewen Baconnier |
766 | +# Copyright 2013 Camptocamp SA |
767 | +# |
768 | +# This program is free software: you can redistribute it and/or modify |
769 | +# it under the terms of the GNU Affero General Public License as |
770 | +# published by the Free Software Foundation, either version 3 of the |
771 | +# License, or (at your option) any later version. |
772 | +# |
773 | +# This program is distributed in the hope that it will be useful, |
774 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
775 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
776 | +# GNU Affero General Public License for more details. |
777 | +# |
778 | +# You should have received a copy of the GNU Affero General Public License |
779 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
780 | +# |
781 | +############################################################################## |
782 | + |
783 | +from openerp.osv import orm, fields |
784 | +from openerp.tools.translate import _ |
785 | + |
786 | + |
787 | +class sale_order(orm.Model): |
788 | + _inherit = 'sale.order' |
789 | + |
790 | + def _stock_reservation(self, cr, uid, ids, fields, args, context=None): |
791 | + result = {} |
792 | + for order_id in ids: |
793 | + result[order_id] = {'has_stock_reservation': False, |
794 | + 'is_stock_reservable': False} |
795 | + for sale in self.browse(cr, uid, ids, context=context): |
796 | + for line in sale.order_line: |
797 | + if line.reservation_ids: |
798 | + result[sale.id]['has_stock_reservation'] = True |
799 | + if line.is_stock_reservable: |
800 | + result[sale.id]['is_stock_reservable'] = True |
801 | + if sale.state not in ('draft', 'sent'): |
802 | + result[sale.id]['is_stock_reservable'] = False |
803 | + return result |
804 | + |
805 | + _columns = { |
806 | + 'has_stock_reservation': fields.function( |
807 | + _stock_reservation, |
808 | + type='boolean', |
809 | + readonly=True, |
810 | + multi='stock_reservation', |
811 | + string='Has Stock Reservations'), |
812 | + 'is_stock_reservable': fields.function( |
813 | + _stock_reservation, |
814 | + type='boolean', |
815 | + readonly=True, |
816 | + multi='stock_reservation', |
817 | + string='Can Have Stock Reservations'), |
818 | + } |
819 | + |
820 | + def release_all_stock_reservation(self, cr, uid, ids, context=None): |
821 | + sales = self.browse(cr, uid, ids, context=context) |
822 | + line_ids = [line.id for sale in sales for line in sale.order_line] |
823 | + line_obj = self.pool.get('sale.order.line') |
824 | + line_obj.release_stock_reservation(cr, uid, line_ids, context=context) |
825 | + return True |
826 | + |
827 | + def action_button_confirm(self, cr, uid, ids, context=None): |
828 | + self.release_all_stock_reservation(cr, uid, ids, context=context) |
829 | + return super(sale_order, self).action_button_confirm( |
830 | + cr, uid, ids, context=context) |
831 | + |
832 | + def action_cancel(self, cr, uid, ids, context=None): |
833 | + self.release_all_stock_reservation(cr, uid, ids, context=context) |
834 | + return super(sale_order, self).action_cancel( |
835 | + cr, uid, ids, context=context) |
836 | + |
837 | + |
838 | +class sale_order_line(orm.Model): |
839 | + _inherit = 'sale.order.line' |
840 | + |
841 | + def _is_stock_reservable(self, cr, uid, ids, fields, args, context=None): |
842 | + result = {}.fromkeys(ids, False) |
843 | + for line in self.browse(cr, uid, ids, context=context): |
844 | + if line.state != 'draft': |
845 | + continue |
846 | + if line.type == 'make_to_order': |
847 | + continue |
848 | + if (not line.product_id or line.product_id.type == 'service'): |
849 | + continue |
850 | + if not line.reservation_ids: |
851 | + result[line.id] = True |
852 | + return result |
853 | + |
854 | + _columns = { |
855 | + 'reservation_ids': fields.one2many( |
856 | + 'stock.reservation', |
857 | + 'sale_line_id', |
858 | + string='Stock Reservation'), |
859 | + 'is_stock_reservable': fields.function( |
860 | + _is_stock_reservable, |
861 | + type='boolean', |
862 | + readonly=True, |
863 | + string='Can be reserved'), |
864 | + } |
865 | + |
866 | + def copy_data(self, cr, uid, id, default=None, context=None): |
867 | + if default is None: |
868 | + default = {} |
869 | + default['reservation_ids'] = False |
870 | + return super(sale_order_line, self).copy_data( |
871 | + cr, uid, id, default=default, context=context) |
872 | + |
873 | + def release_stock_reservation(self, cr, uid, ids, context=None): |
874 | + lines = self.browse(cr, uid, ids, context=context) |
875 | + reserv_ids = [reserv.id for line in lines |
876 | + for reserv in line.reservation_ids] |
877 | + reserv_obj = self.pool.get('stock.reservation') |
878 | + reserv_obj.release(cr, uid, reserv_ids, context=context) |
879 | + return True |
880 | + |
881 | + def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, |
882 | + uom=False, qty_uos=0, uos=False, name='', partner_id=False, |
883 | + lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None): |
884 | + result = super(sale_order_line, self).product_id_change( |
885 | + cr, uid, ids, pricelist, product, qty=qty, uom=uom, |
886 | + qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, |
887 | + lang=lang, update_tax=update_tax, date_order=date_order, |
888 | + packaging=packaging, fiscal_position=fiscal_position, |
889 | + flag=flag, context=context) |
890 | + if not ids: # warn only if we change an existing line |
891 | + return result |
892 | + assert len(ids) == 1, "Expected 1 ID, got %r" % ids |
893 | + line = self.browse(cr, uid, ids[0], context=context) |
894 | + if qty != line.product_uom_qty and line.reservation_ids: |
895 | + msg = _("As you changed the quantity of the line, " |
896 | + "the quantity of the stock reservation will " |
897 | + "be automatically adjusted to %.2f.") % qty |
898 | + msg += "\n\n" |
899 | + result.setdefault('warning', {}) |
900 | + if result['warning'].get('message'): |
901 | + result['warning']['message'] += msg |
902 | + else: |
903 | + result['warning'] = { |
904 | + 'title': _('Configuration Error!'), |
905 | + 'message': msg, |
906 | + } |
907 | + return result |
908 | + |
909 | + def write(self, cr, uid, ids, vals, context=None): |
910 | + block_on_reserve = ('product_id', 'product_uom', 'product_uos', |
911 | + 'type') |
912 | + update_on_reserve = ('price_unit', 'product_uom_qty', 'product_uos_qty') |
913 | + keys = set(vals.keys()) |
914 | + test_block = keys.intersection(block_on_reserve) |
915 | + test_update = keys.intersection(update_on_reserve) |
916 | + if test_block: |
917 | + for line in self.browse(cr, uid, ids, context=context): |
918 | + if not line.reservation_ids: |
919 | + continue |
920 | + raise orm.except_orm( |
921 | + _('Error'), |
922 | + _('You cannot change the product or unit of measure ' |
923 | + 'of lines with a stock reservation. ' |
924 | + 'Release the reservation ' |
925 | + 'before changing the product.')) |
926 | + res = super(sale_order_line, self).write(cr, uid, ids, vals, context=context) |
927 | + if test_update: |
928 | + for line in self.browse(cr, uid, ids, context=context): |
929 | + if not line.reservation_ids: |
930 | + continue |
931 | + if len(line.reservation_ids) > 1: |
932 | + raise orm.except_orm( |
933 | + _('Error'), |
934 | + _('Several stock reservations are linked with the ' |
935 | + 'line. Impossible to adjust their quantity. ' |
936 | + 'Please release the reservation ' |
937 | + 'before changing the quantity.')) |
938 | + |
939 | + line.reservation_ids[0].write( |
940 | + {'price_unit': line.price_unit, |
941 | + 'product_qty': line.product_uom_qty, |
942 | + 'product_uos_qty': line.product_uos_qty, |
943 | + } |
944 | + ) |
945 | + return res |
946 | |
947 | === added file 'stock_reserve_sale/model/stock_reserve.py' |
948 | --- stock_reserve_sale/model/stock_reserve.py 1970-01-01 00:00:00 +0000 |
949 | +++ stock_reserve_sale/model/stock_reserve.py 2013-09-10 07:53:22 +0000 |
950 | @@ -0,0 +1,50 @@ |
951 | +# -*- coding: utf-8 -*- |
952 | +############################################################################## |
953 | +# |
954 | +# Author: Guewen Baconnier |
955 | +# Copyright 2013 Camptocamp SA |
956 | +# |
957 | +# This program is free software: you can redistribute it and/or modify |
958 | +# it under the terms of the GNU Affero General Public License as |
959 | +# published by the Free Software Foundation, either version 3 of the |
960 | +# License, or (at your option) any later version. |
961 | +# |
962 | +# This program is distributed in the hope that it will be useful, |
963 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
964 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
965 | +# GNU Affero General Public License for more details. |
966 | +# |
967 | +# You should have received a copy of the GNU Affero General Public License |
968 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
969 | +# |
970 | +############################################################################## |
971 | + |
972 | +from openerp.osv import orm, fields |
973 | + |
974 | + |
975 | +class stock_reservation(orm.Model): |
976 | + _inherit = 'stock.reservation' |
977 | + |
978 | + _columns = { |
979 | + 'sale_line_id': fields.many2one( |
980 | + 'sale.order.line', |
981 | + string='Sale Order Line', |
982 | + ondelete='cascade'), |
983 | + 'sale_id': fields.related( |
984 | + 'sale_line_id', 'order_id', |
985 | + type='many2one', |
986 | + relation='sale.order', |
987 | + string='Sale Order') |
988 | + } |
989 | + |
990 | + def release(self, cr, uid, ids, context=None): |
991 | + self.write(cr, uid, ids, {'sale_line_id': False}, context=context) |
992 | + return super(stock_reservation, self).release( |
993 | + cr, uid, ids, context=context) |
994 | + |
995 | + def copy_data(self, cr, uid, id, default=None, context=None): |
996 | + if default is None: |
997 | + default = {} |
998 | + default['sale_line_id'] = False |
999 | + return super(stock_reservation, self).copy_data( |
1000 | + cr, uid, id, default=default, context=context) |
1001 | |
1002 | === added directory 'stock_reserve_sale/test' |
1003 | === added file 'stock_reserve_sale/test/sale_line_reserve.yml' |
1004 | --- stock_reserve_sale/test/sale_line_reserve.yml 1970-01-01 00:00:00 +0000 |
1005 | +++ stock_reserve_sale/test/sale_line_reserve.yml 2013-09-10 07:53:22 +0000 |
1006 | @@ -0,0 +1,118 @@ |
1007 | +- |
1008 | + I create a product to test the stock reservation |
1009 | +- |
1010 | + !record {model: product.product, id: product_yogurt}: |
1011 | + default_code: 001yogurt |
1012 | + name: yogurt |
1013 | + type: product |
1014 | + categ_id: product.product_category_1 |
1015 | + list_price: 100.0 |
1016 | + standard_price: 70.0 |
1017 | + uom_id: product.product_uom_kgm |
1018 | + uom_po_id: product.product_uom_kgm |
1019 | + procure_method: make_to_stock |
1020 | + valuation: real_time |
1021 | + cost_method: average |
1022 | + property_stock_account_input: account.o_expense |
1023 | + property_stock_account_output: account.o_income |
1024 | +- |
1025 | + I update the current stock of the yogurt with 10 kgm |
1026 | +- |
1027 | + !record {model: stock.change.product.qty, id: change_qty}: |
1028 | + new_quantity: 10 |
1029 | + product_id: product_yogurt |
1030 | +- |
1031 | + !python {model: stock.change.product.qty}: | |
1032 | + context['active_id'] = ref('product_yogurt') |
1033 | + self.change_product_qty(cr, uid, [ref('change_qty')], context=context) |
1034 | +- |
1035 | + In order to test reservation of the sales order, I create a sales order |
1036 | +- |
1037 | + !record {model: sale.order, id: sale_reserve_02}: |
1038 | + partner_id: base.res_partner_2 |
1039 | + payment_term: account.account_payment_term |
1040 | +- |
1041 | + And I create a sales order line |
1042 | +- |
1043 | + !record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}: |
1044 | + name: Yogurt |
1045 | + product_id: product_yogurt |
1046 | + product_uom_qty: 4 |
1047 | + product_uom: product.product_uom_kgm |
1048 | + order_id: sale_reserve_02 |
1049 | +- |
1050 | + And I create a stock reserve for this line |
1051 | +- |
1052 | + !record {model: sale.stock.reserve, id: wizard_reserve_02_01}: |
1053 | + note: Reservation for the sales order line |
1054 | +- |
1055 | + I call the wizard to reserve the products of the sales order |
1056 | +- |
1057 | + !python {model: sale.stock.reserve}: | |
1058 | + active_id = ref('sale_line_reserve_02_01') |
1059 | + context['active_id'] = active_id |
1060 | + context['active_ids'] = [active_id] |
1061 | + context['active_model'] = 'sale.order.line' |
1062 | + self.button_reserve(cr, uid, [ref('wizard_reserve_02_01')], context=context) |
1063 | +- |
1064 | + I check Virtual stock of yogurt after update reservation |
1065 | +- |
1066 | + !python {model: product.product}: | |
1067 | + product = self.browse(cr, uid, ref('product_yogurt'), context=context) |
1068 | + assert product.virtual_available == 6, "Stock is not updated." |
1069 | +- |
1070 | + And I create a MTO sales order line |
1071 | +- |
1072 | + !record {model: sale.order.line, id: sale_line_reserve_02_02, view: sale.view_order_line_tree}: |
1073 | + order_id: sale_reserve_02 |
1074 | + name: Mouse, Wireless |
1075 | + product_id: product.product_product_12 |
1076 | + type: make_to_order |
1077 | + product_uom_qty: 4 |
1078 | + product_uom: product.product_uom_kgm |
1079 | +- |
1080 | + And I try to create a stock reserve for this MTO line |
1081 | +- |
1082 | + !record {model: sale.stock.reserve, id: wizard_reserve_02_02}: |
1083 | + note: Reservation for the sales order line |
1084 | +- |
1085 | + I call the wizard to reserve the products of the sales order |
1086 | +- |
1087 | + !python {model: sale.stock.reserve}: | |
1088 | + active_id = ref('sale_line_reserve_02_02') |
1089 | + context['active_id'] = active_id |
1090 | + context['active_ids'] = [active_id] |
1091 | + context['active_model'] = 'sale.order.line' |
1092 | + self.button_reserve(cr, uid, [ref('wizard_reserve_02_02')], context=context) |
1093 | +- |
1094 | + I should not have a stock reservation for a MTO line |
1095 | +- |
1096 | + !python {model: stock.reservation}: | |
1097 | + reserv_ids = self.search( |
1098 | + cr, uid, |
1099 | + [('sale_line_id', '=', ref('sale_line_reserve_02_02'))], |
1100 | + context=context) |
1101 | + assert not reserv_ids, "No stock reservation should be created for MTO lines" |
1102 | +- |
1103 | + And I change the quantity in the first line |
1104 | +- |
1105 | + !record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}: |
1106 | + product_uom_qty: 5 |
1107 | +- |
1108 | + |
1109 | + I check Virtual stock of yogurt after change of reservations |
1110 | +- |
1111 | + !python {model: product.product}: | |
1112 | + product = self.browse(cr, uid, ref('product_yogurt'), context=context) |
1113 | + assert product.virtual_available == 5, "Stock is not updated." |
1114 | +- |
1115 | + I release the sales order's reservations for the first line |
1116 | +- |
1117 | + !python {model: sale.order.line}: | |
1118 | + self.release_stock_reservation(cr, uid, [ref('sale_line_reserve_02_01')], context=context) |
1119 | +- |
1120 | + I check Virtual stock of yogurt after release of reservations |
1121 | +- |
1122 | + !python {model: product.product}: | |
1123 | + product = self.browse(cr, uid, ref('product_yogurt'), context=context) |
1124 | + assert product.virtual_available == 10, "Stock is not updated." |
1125 | |
1126 | === added file 'stock_reserve_sale/test/sale_reserve.yml' |
1127 | --- stock_reserve_sale/test/sale_reserve.yml 1970-01-01 00:00:00 +0000 |
1128 | +++ stock_reserve_sale/test/sale_reserve.yml 2013-09-10 07:53:22 +0000 |
1129 | @@ -0,0 +1,65 @@ |
1130 | +- |
1131 | + I create a product to test the stock reservation |
1132 | +- |
1133 | + !record {model: product.product, id: product_gelato}: |
1134 | + default_code: 001GELATO |
1135 | + name: Gelato |
1136 | + type: product |
1137 | + categ_id: product.product_category_1 |
1138 | + list_price: 100.0 |
1139 | + standard_price: 70.0 |
1140 | + uom_id: product.product_uom_kgm |
1141 | + uom_po_id: product.product_uom_kgm |
1142 | + procure_method: make_to_stock |
1143 | + valuation: real_time |
1144 | + cost_method: average |
1145 | + property_stock_account_input: account.o_expense |
1146 | + property_stock_account_output: account.o_income |
1147 | +- |
1148 | + I update the current stock of the Gelato with 10 kgm |
1149 | +- |
1150 | + !record {model: stock.change.product.qty, id: change_qty}: |
1151 | + new_quantity: 10 |
1152 | + product_id: product_gelato |
1153 | +- |
1154 | + !python {model: stock.change.product.qty}: | |
1155 | + context['active_id'] = ref('product_gelato') |
1156 | + self.change_product_qty(cr, uid, [ref('change_qty')], context=context) |
1157 | +- |
1158 | + In order to test reservation of the sales order, I create a sales order |
1159 | +- |
1160 | + !record {model: sale.order, id: sale_reserve_01}: |
1161 | + partner_id: base.res_partner_2 |
1162 | + payment_term: account.account_payment_term |
1163 | + order_line: |
1164 | + - product_id: product_gelato |
1165 | + product_uom_qty: 4 |
1166 | +- |
1167 | + I call the wizard to reserve the products of the sales order |
1168 | +- |
1169 | + !record {model: sale.stock.reserve, id: wizard_reserve_01}: |
1170 | + note: Reservation for the sales order |
1171 | +- |
1172 | + !python {model: sale.stock.reserve}: | |
1173 | + active_id = ref('sale_reserve_01') |
1174 | + context['active_id'] = active_id |
1175 | + context['active_ids'] = [active_id] |
1176 | + context['active_model'] = 'sale.order' |
1177 | + self.button_reserve(cr, uid, [ref('wizard_reserve_01')], context=context) |
1178 | +- |
1179 | + I check Virtual stock of Gelato after update reservation |
1180 | +- |
1181 | + !python {model: product.product}: | |
1182 | + product = self.browse(cr, uid, ref('product_gelato'), context=context) |
1183 | + assert product.virtual_available == 6, "Stock is not updated." |
1184 | +- |
1185 | + I release the sales order's reservations |
1186 | +- |
1187 | + !python {model: sale.order}: | |
1188 | + self.release_all_stock_reservation(cr, uid, [ref('sale_reserve_01')], context=context) |
1189 | +- |
1190 | + I check Virtual stock of Gelato after release of reservations |
1191 | +- |
1192 | + !python {model: product.product}: | |
1193 | + product = self.browse(cr, uid, ref('product_gelato'), context=context) |
1194 | + assert product.virtual_available == 10, "Stock is not updated." |
1195 | |
1196 | === added directory 'stock_reserve_sale/view' |
1197 | === added file 'stock_reserve_sale/view/sale.xml' |
1198 | --- stock_reserve_sale/view/sale.xml 1970-01-01 00:00:00 +0000 |
1199 | +++ stock_reserve_sale/view/sale.xml 2013-09-10 07:53:22 +0000 |
1200 | @@ -0,0 +1,67 @@ |
1201 | +<?xml version="1.0" encoding="utf-8"?> |
1202 | +<openerp> |
1203 | + <data noupdate="0"> |
1204 | + |
1205 | + <record id="view_order_form_reserve" model="ir.ui.view"> |
1206 | + <field name="name">sale.order.form.reserve</field> |
1207 | + <field name="model">sale.order</field> |
1208 | + <field name="inherit_id" ref="sale_stock.view_order_form_inherit"/> |
1209 | + <field name="arch" type="xml"> |
1210 | + <button name="action_quotation_send" position="before"> |
1211 | + <field name="is_stock_reservable" invisible="1"/> |
1212 | + <button name="%(action_sale_stock_reserve)d" |
1213 | + type="action" |
1214 | + string="Reserve Stock" |
1215 | + help="Pre-book products from stock" |
1216 | + attrs="{'invisible': [('is_stock_reservable', '=', False)]}" |
1217 | + /> |
1218 | + </button> |
1219 | + |
1220 | + <field name="order_line" position="attributes"> |
1221 | + <attribute name="options">{"reload_on_button": 1}</attribute> |
1222 | + </field> |
1223 | + |
1224 | + <xpath expr="//field[@name='order_line']/form//field[@name='state']" position="before"> |
1225 | + <field name="reservation_ids" invisible="1"/> |
1226 | + <button name="%(action_sale_stock_reserve)d" |
1227 | + type="action" |
1228 | + string="Reserve Stock" |
1229 | + attrs="{'invisible': ['|', ('reservation_ids', '!=', []), |
1230 | + ('state', '!=', 'draft')]}" /> |
1231 | + <button name="release_stock_reservation" |
1232 | + type="object" |
1233 | + string="Release Reservation" |
1234 | + attrs="{'invisible': ['|', ('reservation_ids', '=', []), |
1235 | + ('state', '!=', 'draft')]}" /> |
1236 | + </xpath> |
1237 | + |
1238 | + <xpath expr="//field[@name='order_line']/tree/field[@name='price_subtotal']" position="after"> |
1239 | + <field name="reservation_ids" invisible="1"/> |
1240 | + <field name="is_stock_reservable" invisible="1"/> |
1241 | + <button name="%(action_sale_stock_reserve)d" |
1242 | + type="action" |
1243 | + string="Reserve Stock" |
1244 | + icon="terp-locked" |
1245 | + attrs="{'invisible': [('is_stock_reservable', '=', False)]}" /> |
1246 | + <button name="release_stock_reservation" |
1247 | + type="object" |
1248 | + string="Release Reservation" |
1249 | + icon="gtk-undo" |
1250 | + attrs="{'invisible': [('reservation_ids', '=', [])]}" /> |
1251 | + </xpath> |
1252 | + |
1253 | + <field name="invoiced" position="before"> |
1254 | + <label for="has_stock_reservation"/> |
1255 | + <div> |
1256 | + <field name="has_stock_reservation"/> |
1257 | + <button name="release_all_stock_reservation" |
1258 | + string="cancel all" |
1259 | + type="object" class="oe_link" |
1260 | + attrs="{'invisible': [('has_stock_reservation', '=', False)]}"/> |
1261 | + </div> |
1262 | + </field> |
1263 | + </field> |
1264 | + </record> |
1265 | + |
1266 | + </data> |
1267 | +</openerp> |
1268 | |
1269 | === added file 'stock_reserve_sale/view/stock_reserve.xml' |
1270 | --- stock_reserve_sale/view/stock_reserve.xml 1970-01-01 00:00:00 +0000 |
1271 | +++ stock_reserve_sale/view/stock_reserve.xml 2013-09-10 07:53:22 +0000 |
1272 | @@ -0,0 +1,30 @@ |
1273 | +<?xml version="1.0" encoding="utf-8"?> |
1274 | +<openerp> |
1275 | + <data noupdate="0"> |
1276 | + <record id="view_stock_reservation_form" model="ir.ui.view"> |
1277 | + <field name="name">stock.reservation.form</field> |
1278 | + <field name="model">stock.reservation</field> |
1279 | + <field name="inherit_id" ref="stock_reserve.view_stock_reservation_form"/> |
1280 | + <field name="arch" type="xml"> |
1281 | + <group name="location" position="after"> |
1282 | + <group name="sale" string="Sales"> |
1283 | + <field name="sale_id"/> |
1284 | + <field name="sale_line_id"/> |
1285 | + </group> |
1286 | + </group> |
1287 | + </field> |
1288 | + </record> |
1289 | + |
1290 | + <record id="view_stock_reservation_tree" model="ir.ui.view"> |
1291 | + <field name="name">stock.reservation.tree</field> |
1292 | + <field name="model">stock.reservation</field> |
1293 | + <field name="inherit_id" ref="stock_reserve.view_stock_reservation_tree"/> |
1294 | + <field name="arch" type="xml"> |
1295 | + <field name="move_id" position="before"> |
1296 | + <field name="sale_id"/> |
1297 | + </field> |
1298 | + </field> |
1299 | + </record> |
1300 | + |
1301 | + </data> |
1302 | +</openerp> |
1303 | |
1304 | === added directory 'stock_reserve_sale/wizard' |
1305 | === added file 'stock_reserve_sale/wizard/__init__.py' |
1306 | --- stock_reserve_sale/wizard/__init__.py 1970-01-01 00:00:00 +0000 |
1307 | +++ stock_reserve_sale/wizard/__init__.py 2013-09-10 07:53:22 +0000 |
1308 | @@ -0,0 +1,22 @@ |
1309 | +# -*- coding: utf-8 -*- |
1310 | +############################################################################## |
1311 | +# |
1312 | +# Author: Guewen Baconnier |
1313 | +# Copyright 2013 Camptocamp SA |
1314 | +# |
1315 | +# This program is free software: you can redistribute it and/or modify |
1316 | +# it under the terms of the GNU Affero General Public License as |
1317 | +# published by the Free Software Foundation, either version 3 of the |
1318 | +# License, or (at your option) any later version. |
1319 | +# |
1320 | +# This program is distributed in the hope that it will be useful, |
1321 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1322 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1323 | +# GNU Affero General Public License for more details. |
1324 | +# |
1325 | +# You should have received a copy of the GNU Affero General Public License |
1326 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1327 | +# |
1328 | +############################################################################## |
1329 | + |
1330 | +from . import sale_stock_reserve |
1331 | |
1332 | === added file 'stock_reserve_sale/wizard/sale_stock_reserve.py' |
1333 | --- stock_reserve_sale/wizard/sale_stock_reserve.py 1970-01-01 00:00:00 +0000 |
1334 | +++ stock_reserve_sale/wizard/sale_stock_reserve.py 2013-09-10 07:53:22 +0000 |
1335 | @@ -0,0 +1,111 @@ |
1336 | +# -*- coding: utf-8 -*- |
1337 | +############################################################################## |
1338 | +# |
1339 | +# Author: Guewen Baconnier |
1340 | +# Copyright 2013 Camptocamp SA |
1341 | +# |
1342 | +# This program is free software: you can redistribute it and/or modify |
1343 | +# it under the terms of the GNU Affero General Public License as |
1344 | +# published by the Free Software Foundation, either version 3 of the |
1345 | +# License, or (at your option) any later version. |
1346 | +# |
1347 | +# This program is distributed in the hope that it will be useful, |
1348 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1349 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1350 | +# GNU Affero General Public License for more details. |
1351 | +# |
1352 | +# You should have received a copy of the GNU Affero General Public License |
1353 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1354 | +# |
1355 | +############################################################################## |
1356 | + |
1357 | +from openerp.osv import orm, fields |
1358 | + |
1359 | + |
1360 | +class sale_stock_reserve(orm.TransientModel): |
1361 | + _name = 'sale.stock.reserve' |
1362 | + |
1363 | + _columns = { |
1364 | + 'location_id': fields.many2one( |
1365 | + 'stock.location', |
1366 | + 'Source Location', |
1367 | + required=True), |
1368 | + 'location_dest_id': fields.many2one( |
1369 | + 'stock.location', |
1370 | + 'Reservation Location', |
1371 | + required=True, |
1372 | + help="Location where the system will reserve the " |
1373 | + "products."), |
1374 | + 'date_validity': fields.date( |
1375 | + "Validity Date", |
1376 | + help="If a date is given, the reservations will be released " |
1377 | + "at the end of the validity."), |
1378 | + 'note': fields.text('Notes'), |
1379 | + } |
1380 | + |
1381 | + def _default_location_id(self, cr, uid, context=None): |
1382 | + reserv_obj = self.pool.get('stock.reservation') |
1383 | + return reserv_obj._default_location_id(cr, uid, context=context) |
1384 | + |
1385 | + def _default_location_dest_id(self, cr, uid, context=None): |
1386 | + reserv_obj = self.pool.get('stock.reservation') |
1387 | + return reserv_obj._default_location_dest_id(cr, uid, context=context) |
1388 | + |
1389 | + _defaults = { |
1390 | + 'location_id': _default_location_id, |
1391 | + 'location_dest_id': _default_location_dest_id, |
1392 | + } |
1393 | + |
1394 | + def _prepare_stock_reservation(self, cr, uid, form, line, context=None): |
1395 | + product_uos = line.product_uos.id if line.product_uos else False |
1396 | + return {'product_id': line.product_id.id, |
1397 | + 'product_uom': line.product_uom.id, |
1398 | + 'product_qty': line.product_uom_qty, |
1399 | + 'date_validity': form.date_validity, |
1400 | + 'name': "{} ({})".format(line.order_id.name, line.name), |
1401 | + 'location_id': form.location_id.id, |
1402 | + 'location_dest_id': form.location_dest_id.id, |
1403 | + 'note': form.note, |
1404 | + 'product_uos_qty': line.product_uos_qty, |
1405 | + 'product_uos': product_uos, |
1406 | + 'price_unit': line.price_unit, |
1407 | + 'sale_line_id': line.id, |
1408 | + } |
1409 | + |
1410 | + def stock_reserve(self, cr, uid, ids, line_ids, context=None): |
1411 | + assert len(ids) == 1, "Expected 1 ID, got %r" % ids |
1412 | + reserv_obj = self.pool.get('stock.reservation') |
1413 | + line_obj = self.pool.get('sale.order.line') |
1414 | + |
1415 | + form = self.browse(cr, uid, ids[0], context=context) |
1416 | + lines = line_obj.browse(cr, uid, line_ids, context=context) |
1417 | + for line in lines: |
1418 | + if not line.is_stock_reservable: |
1419 | + continue |
1420 | + vals = self._prepare_stock_reservation(cr, uid, form, line, |
1421 | + context=context) |
1422 | + reserv_id = reserv_obj.create(cr, uid, vals, context=context) |
1423 | + reserv_obj.reserve(cr, uid, [reserv_id], context=context) |
1424 | + return True |
1425 | + |
1426 | + def button_reserve(self, cr, uid, ids, context=None): |
1427 | + assert len(ids) == 1, "Expected 1 ID, got %r" % ids |
1428 | + if context is None: |
1429 | + context = {} |
1430 | + close = {'type': 'ir.actions.act_window_close'} |
1431 | + active_model = context.get('active_model') |
1432 | + active_ids = context.get('active_ids') |
1433 | + if not (active_model and active_ids): |
1434 | + return close |
1435 | + |
1436 | + line_obj = self.pool.get('sale.order.line') |
1437 | + if active_model == 'sale.order': |
1438 | + sale_obj = self.pool.get('sale.order') |
1439 | + sales = sale_obj.browse(cr, uid, active_ids, context=context) |
1440 | + line_ids = [line.id for sale in sales for line in sale.order_line] |
1441 | + |
1442 | + if active_model == 'sale.order.line': |
1443 | + line_ids = active_ids |
1444 | + |
1445 | + self.stock_reserve(cr, uid, ids, line_ids, context=context) |
1446 | + return close |
1447 | |
1448 | === added file 'stock_reserve_sale/wizard/sale_stock_reserve_view.xml' |
1449 | --- stock_reserve_sale/wizard/sale_stock_reserve_view.xml 1970-01-01 00:00:00 +0000 |
1450 | +++ stock_reserve_sale/wizard/sale_stock_reserve_view.xml 2013-09-10 07:53:22 +0000 |
1451 | @@ -0,0 +1,44 @@ |
1452 | +<?xml version="1.0" encoding="utf-8"?> |
1453 | +<openerp> |
1454 | + <data noupdate="0"> |
1455 | + |
1456 | + <record id="view_sale_stock_reserve_form" model="ir.ui.view"> |
1457 | + <field name="name">sale.stock.reserve.form</field> |
1458 | + <field name="model">sale.stock.reserve</field> |
1459 | + <field name="arch" type="xml"> |
1460 | + <form string="Reserve Stock" version="7.0"> |
1461 | + <p class="oe_grey"> |
1462 | + A stock reservation will be created for the products |
1463 | + of the selected quotation lines. If a validity date is specified, |
1464 | + the reservation will be released once the date has passed. |
1465 | + </p> |
1466 | + <group> |
1467 | + <field name="location_id"/> |
1468 | + <field name="location_dest_id"/> |
1469 | + <field name="date_validity"/> |
1470 | + </group> |
1471 | + <group name="note" string="Notes"> |
1472 | + <field name="note" nolabel="1"/> |
1473 | + </group> |
1474 | + <footer> |
1475 | + <button string="Reserve" |
1476 | + name="button_reserve" |
1477 | + type="object" |
1478 | + class="oe_highlight" /> |
1479 | + or |
1480 | + <button special="cancel" class="oe_link" string="Cancel"/> |
1481 | + </footer> |
1482 | + </form> |
1483 | + </field> |
1484 | + </record> |
1485 | + |
1486 | + <record id="action_sale_stock_reserve" model="ir.actions.act_window"> |
1487 | + <field name="name">Reserve Stock for Quotation Lines</field> |
1488 | + <field name="type">ir.actions.act_window</field> |
1489 | + <field name="res_model">sale.stock.reserve</field> |
1490 | + <field name="view_type">form</field> |
1491 | + <field name="view_mode">form</field> |
1492 | + <field name="target">new</field> |
1493 | + </record> |
1494 | + </data> |
1495 | +</openerp> |
Hi Guewen,
Thanks for this contrib ! It LGTM.
Regards,
Joël