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