Merge lp:~camptocamp/stock-logistic-warehouse/7.0-stock_reserve into lp:stock-logistic-warehouse

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
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 :

Hi Guewen,

Thanks for this contrib ! It LGTM.

Regards,

Joël

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>

Subscribers

People subscribed via source and target branches