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
=== added directory 'stock_reserve'
=== added file 'stock_reserve/__init__.py'
--- stock_reserve/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve/__init__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import model
023
=== added file 'stock_reserve/__openerp__.py'
--- stock_reserve/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve/__openerp__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,58 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22{'name': 'Stock Reserve',
23 'version': '0.1',
24 'author': 'Camptocamp',
25 'category': 'Warehouse',
26 'license': 'AGPL-3',
27 'complexity': 'normal',
28 'images': [],
29 'website': "http://www.camptocamp.com",
30 'description': """
31Stock Reserve
32=============
33
34Allows to create stock reservations on products.
35
36Each reservation can have a validity date, once passed, the reservation
37is automatically lifted.
38
39The reserved products are substracted from the virtual stock. It means
40that if you reserved a quantity of products which bring the virtual
41stock below the minimum, the orderpoint will be triggered and new
42purchase orders will be generated. It also implies that the max may be
43exceeded if the reservations are canceled.
44
45""",
46 'depends': ['stock',
47 ],
48 'demo': [],
49 'data': ['view/stock_reserve.xml',
50 'view/product.xml',
51 'data/stock_data.xml',
52 'security/ir.model.access.csv',
53 ],
54 'auto_install': False,
55 'test': ['test/stock_reserve.yml',
56 ],
57 'installable': True,
58 }
059
=== added directory 'stock_reserve/data'
=== added file 'stock_reserve/data/stock_data.xml'
--- stock_reserve/data/stock_data.xml 1970-01-01 00:00:00 +0000
+++ stock_reserve/data/stock_data.xml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="1">
4 <record id="stock_location_reservation" model="stock.location">
5 <field name="name">Reservation Stock</field>
6 <field name="location_id" ref="stock.stock_location_company"/>
7 </record>
8
9
10 <!-- Release the stock.reservation when the validity date has
11 passed -->
12 <record forcecreate="True" id="ir_cron_release_stock_reservation" model="ir.cron">
13 <field name="name">Release the stock reservation having a passed validity date</field>
14 <field eval="True" name="active" />
15 <field name="user_id" ref="base.user_root" />
16 <field name="interval_number">1</field>
17 <field name="interval_type">days</field>
18 <field name="numbercall">-1</field>
19 <field eval="False" name="doall" />
20 <field name="model">stock.reservation</field>
21 <field name="function">release_validity_exceeded</field>
22 <field name="args">()</field>
23 </record>
24
25 </data>
26</openerp>
027
=== added directory 'stock_reserve/i18n'
=== added directory 'stock_reserve/model'
=== added file 'stock_reserve/model/__init__.py'
--- stock_reserve/model/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve/model/__init__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import stock_reserve
23from . import product
024
=== added file 'stock_reserve/model/product.py'
--- stock_reserve/model/product.py 1970-01-01 00:00:00 +0000
+++ stock_reserve/model/product.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,40 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.osv import orm, fields
23
24
25class product_product(orm.Model):
26 _inherit = 'product.product'
27
28 def open_stock_reservation(self, cr, uid, ids, context=None):
29 assert len(ids) == 1, "Expected 1 ID, got %r" % ids
30 mod_obj = self.pool.get('ir.model.data')
31 act_obj = self.pool.get('ir.actions.act_window')
32 get_ref = mod_obj.get_object_reference
33 __, action_id = get_ref(cr, uid, 'stock_reserve',
34 'action_stock_reservation')
35 action = act_obj.read(cr, uid, action_id, context=context)
36 action['context'] = {'search_default_draft': 1,
37 'search_default_reserved': 1,
38 'default_product_id': ids[0],
39 'search_default_product_id': ids[0]}
40 return action
041
=== added file 'stock_reserve/model/stock_reserve.py'
--- stock_reserve/model/stock_reserve.py 1970-01-01 00:00:00 +0000
+++ stock_reserve/model/stock_reserve.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,177 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.osv import orm, fields
23from openerp.tools.translate import _
24
25
26class stock_reservation(orm.Model):
27 """ Allow to reserve products.
28
29 The fields mandatory for the creation of a reservation are:
30
31 * product_id
32 * product_qty
33 * product_uom
34 * name
35
36 The following fields are required but have default values that you may
37 want to override:
38
39 * company_id
40 * location_id
41 * dest_location_id
42
43 Optionally, you may be interested to define:
44
45 * date_validity (once passed, the reservation will be released)
46 * note
47 """
48 _name = 'stock.reservation'
49 _description = 'Stock Reservation'
50 _inherits = {'stock.move': 'move_id'}
51
52 _columns = {
53 'move_id': fields.many2one('stock.move',
54 'Reservation Move',
55 required=True,
56 readonly=True,
57 ondelete='cascade',
58 select=1),
59 'date_validity': fields.date('Validity Date'),
60 }
61
62 def get_location_from_ref(self, cr, uid, ref, context=None):
63 """ Get a location from a xmlid if allowed
64 :param ref: tuple (module, xmlid)
65 """
66 location_obj = self.pool.get('stock.location')
67 data_obj = self.pool.get('ir.model.data')
68 get_ref = data_obj.get_object_reference
69 try:
70 __, location_id = get_ref(cr, uid, *ref)
71 location_obj.check_access_rule(cr, uid, [location_id],
72 'read', context=context)
73 except (orm.except_orm, ValueError):
74 location_id = False
75 return location_id
76
77 def _default_location_id(self, cr, uid, context=None):
78 if context is None:
79 context = {}
80 move_obj = self.pool.get('stock.move')
81 context['picking_type'] = 'internal'
82 return move_obj._default_location_source(cr, uid, context=context)
83
84 def _default_location_dest_id(self, cr, uid, context=None):
85 ref = ('stock_reserve', 'stock_location_reservation')
86 return self.get_location_from_ref(cr, uid, ref, context=context)
87
88 _defaults = {
89 'type': 'internal',
90 'location_id': _default_location_id,
91 'location_dest_id': _default_location_dest_id,
92 'product_qty': 1.0,
93 }
94
95 def reserve(self, cr, uid, ids, context=None):
96 """ Confirm a reservation
97
98 The reservation is done using the default UOM of the product.
99 A date until which the product is reserved can be specified.
100 """
101 move_obj = self.pool.get('stock.move')
102 reservations = self.browse(cr, uid, ids, context=context)
103 move_ids = [reserv.move_id.id for reserv in reservations]
104 move_obj.write(cr, uid, move_ids,
105 {'date_expected': fields.datetime.now()},
106 context=context)
107 move_obj.action_confirm(cr, uid, move_ids, context=context)
108 move_obj.force_assign(cr, uid, move_ids, context=context)
109 return True
110
111 def release(self, cr, uid, ids, context=None):
112 if isinstance(ids, (int, long)):
113 ids = [ids]
114 reservations = self.read(cr, uid, ids, ['move_id'],
115 context=context, load='_classic_write')
116 move_obj = self.pool.get('stock.move')
117 move_ids = [reserv['move_id'] for reserv in reservations]
118 move_obj.action_cancel(cr, uid, move_ids, context=context)
119 return True
120
121 def release_validity_exceeded(self, cr, uid, ids=None, context=None):
122 """ Release all the reservation having an exceeded validity date """
123 domain = [('date_validity', '<', fields.date.today()),
124 ('state', '=', 'assigned')]
125 if ids:
126 domain.append(('id', 'in', ids))
127 reserv_ids = self.search(cr, uid, domain, context=context)
128 self.release(cr, uid, reserv_ids, context=context)
129 return True
130
131 def unlink(self, cr, uid, ids, context=None):
132 """ Release the reservation before the unlink """
133 self.release(cr, uid, ids, context=context)
134 return super(stock_reservation, self).unlink(cr, uid, ids,
135 context=context)
136
137 def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
138 move_obj = self.pool.get('stock.move')
139 if ids:
140 reserv = self.read(cr, uid, ids, ['move_id'], context=context,
141 load='_classic_write')
142 move_ids = [rv['move_id'] for rv in reserv]
143 else:
144 move_ids = []
145 result = move_obj.onchange_product_id(
146 cr, uid, move_ids, prod_id=product_id, loc_id=False,
147 loc_dest_id=False, partner_id=False)
148 if result.get('value'):
149 vals = result['value']
150 # only keep the existing fields on the view
151 keep = ('product_uom', 'name')
152 result['value'] = dict((key, value) for key, value in
153 result['value'].iteritems() if
154 key in keep)
155 return result
156
157 def onchange_quantity(self, cr, uid, ids, product_id, product_qty, context=None):
158 """ On change of product quantity avoid negative quantities """
159 if not product_id or product_qty <= 0.0:
160 return {'value': {'product_qty': 0.0}}
161 return {}
162
163 def open_move(self, cr, uid, ids, context=None):
164 assert len(ids) == 1, "1 ID expected, got %r" % ids
165 reserv = self.read(cr, uid, ids[0], ['move_id'], context=context,
166 load='_classic_write')
167 mod_obj = self.pool.get('ir.model.data')
168 act_obj = self.pool.get('ir.actions.act_window')
169 get_ref = mod_obj.get_object_reference
170 __, action_id = get_ref(cr, uid, 'stock', 'action_move_form2')
171 action = act_obj.read(cr, uid, action_id, context=context)
172 action['name'] = _('Reservation Move')
173 # open directly in the form view
174 __, view_id = get_ref(cr, uid, 'stock', 'view_move_form')
175 action['views'] = [(view_id, 'form')]
176 action['res_id'] = reserv['move_id']
177 return action
0178
=== added directory 'stock_reserve/security'
=== added file 'stock_reserve/security/ir.model.access.csv'
--- stock_reserve/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
+++ stock_reserve/security/ir.model.access.csv 2013-09-10 07:53:22 +0000
@@ -0,0 +1,3 @@
1id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2access_stock_reservation_manager,stock.reservation manager,model_stock_reservation,stock.group_stock_manager,1,1,1,1
3access_stock_reservation_user,stock.reservation user,model_stock_reservation,stock.group_stock_user,1,1,1,0
04
=== added directory 'stock_reserve/test'
=== added file 'stock_reserve/test/stock_reserve.yml'
--- stock_reserve/test/stock_reserve.yml 1970-01-01 00:00:00 +0000
+++ stock_reserve/test/stock_reserve.yml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,63 @@
1-
2 I create a product to test the stock reservation
3-
4 !record {model: product.product, id: product_sorbet}:
5 default_code: 001SORBET
6 name: Sorbet
7 type: product
8 categ_id: product.product_category_1
9 list_price: 100.0
10 standard_price: 70.0
11 uom_id: product.product_uom_kgm
12 uom_po_id: product.product_uom_kgm
13 procure_method: make_to_stock
14 valuation: real_time
15 cost_method: average
16 property_stock_account_input: account.o_expense
17 property_stock_account_output: account.o_income
18-
19 I update the current stock of the Sorbet with 10 kgm
20-
21 !record {model: stock.change.product.qty, id: change_qty}:
22 new_quantity: 10
23 product_id: product_sorbet
24-
25 !python {model: stock.change.product.qty}: |
26 context['active_id'] = ref('stock_reserve.product_sorbet')
27 self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
28-
29 I check Virtual stock of Sorbet after update stock.
30-
31 !python {model: product.product}: |
32 product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
33 assert product.virtual_available == 10, "Stock is not updated."
34-
35 I create a stock reservation for 5 kgm
36-
37 !record {model: stock.reservation, id: reserv_sorbet1}:
38 product_id: product_sorbet
39 product_qty: 5.0
40 product_uom: product.product_uom_kgm
41 name: reserve 5 kgm of sorbet for test
42-
43 I confirm the reservation
44-
45 !python {model: stock.reservation}: |
46 self.reserve(cr, uid, [ref('reserv_sorbet1')], context=context)
47-
48 I check Virtual stock of Sorbet after update reservation
49-
50 !python {model: product.product}: |
51 product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
52 assert product.virtual_available == 5, "Stock is not updated."
53-
54 I release the reservation
55-
56 !python {model: stock.reservation}: |
57 self.release(cr, uid, [ref('reserv_sorbet1')], context=context)
58-
59 I check Virtual stock of Sorbet after update reservation
60-
61 !python {model: product.product}: |
62 product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
63 assert product.virtual_available == 10, "Stock is not updated."
064
=== added directory 'stock_reserve/view'
=== added file 'stock_reserve/view/product.xml'
--- stock_reserve/view/product.xml 1970-01-01 00:00:00 +0000
+++ stock_reserve/view/product.xml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4
5 <record model="ir.ui.view" id="product_form_view">
6 <field name="name">product.product.form.reserve</field>
7 <field name="model">product.product</field>
8 <field name="inherit_id" ref="procurement.product_form_view_procurement_button"/>
9 <field name="arch" type="xml">
10 <xpath expr="//div[@name='buttons']" position="inside">
11 <button string="Stock Reservations"
12 name="open_stock_reservation"
13 type="object"/>
14 </xpath>
15 </field>
16 </record>
17
18 </data>
19</openerp>
020
=== added file 'stock_reserve/view/stock_reserve.xml'
--- stock_reserve/view/stock_reserve.xml 1970-01-01 00:00:00 +0000
+++ stock_reserve/view/stock_reserve.xml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,140 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4 <record id="view_stock_reservation_form" model="ir.ui.view">
5 <field name="name">stock.reservation.form</field>
6 <field name="model">stock.reservation</field>
7 <field name="arch" type="xml">
8 <form string="Stock Reservations" version="7.0">
9 <header>
10 <button name="reserve" type="object"
11 string="Reserve"
12 class="oe_highlight"
13 states="draft"/>
14 <button name="release" type="object"
15 string="Release"
16 class="oe_highlight"
17 states="assigned,confirmed,done"/>
18 <button name="open_move" type="object"
19 string="View Reservation Move"/>
20 <field name="state" widget="statusbar"
21 statusbar_visible="draft,assigned"/>
22 </header>
23 <sheet>
24 <group>
25 <group name="main_grp" string="Details">
26 <field name="product_id"
27 on_change="onchange_product_id(product_id)"
28 />
29 <label for="product_qty" />
30 <div>
31 <field name="product_qty"
32 on_change="onchange_quantity(product_id, product_qty)"
33 class="oe_inline"/>
34 <field name="product_uom"
35 groups="product.group_uom" class="oe_inline"/>
36 </div>
37 <field name="name"/>
38 <field name="date_validity" />
39 <field name="create_date" groups="base.group_no_one"/>
40 <field name="company_id"
41 groups="base.group_multi_company"
42 widget="selection"/>
43 </group>
44 <group name="location" string="Locations"
45 groups="stock.group_locations">
46 <field name="location_id"/>
47 <field name="location_dest_id"/>
48 </group>
49 <group name="note" string="Notes">
50 <field name="note" nolabel="1"/>
51 </group>
52 </group>
53 </sheet>
54 </form>
55 </field>
56 </record>
57
58 <record id="view_stock_reservation_tree" model="ir.ui.view">
59 <field name="name">stock.reservation.tree</field>
60 <field name="model">stock.reservation</field>
61 <field name="arch" type="xml">
62 <tree string="Stock Reservations" version="7.0"
63 colors="blue:state == 'draft';grey:state == 'cancel'" >
64 <field name="name" />
65 <field name="product_id" />
66 <field name="move_id" />
67 <field name="product_qty" sum="Total" />
68 <field name="product_uom" />
69 <field name="date_validity" />
70 <field name="state"/>
71 <button name="reserve" type="object"
72 string="Reserve"
73 icon="terp-locked"
74 states="draft"/>
75 <button name="release" type="object"
76 string="Release"
77 icon="gtk-undo"
78 states="assigned,confirmed,done"/>
79 </tree>
80 </field>
81 </record>
82
83 <record id="view_stock_reservation_search" model="ir.ui.view">
84 <field name="name">stock.reservation.search</field>
85 <field name="model">stock.reservation</field>
86 <field name="arch" type="xml">
87 <search string="Stock Reservations" version="7.0">
88 <filter name="draft" string="Draft"
89 domain="[('state', '=', 'draft')]"
90 help="Not already reserved"/>
91 <filter name="reserved" string="Reserved"
92 domain="[('state', '=', 'assigned')]"
93 help="Moves are reserved."/>
94 <filter name="cancel" string="Released"
95 domain="[('state', '=', 'cancel')]"
96 help="Reservations have been released."/>
97 <field name="name" />
98 <field name="product_id" />
99 <field name="move_id" />
100 <group expand="0" string="Group By...">
101 <filter string="Status"
102 name="groupby_state"
103 domain="[]" context="{'group_by': 'state'}"/>
104 <filter string="Product" domain="[]"
105 name="groupby_product"
106 context="{'group_by': 'product_id'}"/>
107 <filter string="Product UoM" domain="[]"
108 name="groupby_product_uom"
109 context="{'group_by': 'product_uom'}"/>
110 </group>
111 </search>
112 </field>
113 </record>
114
115 <record id="action_stock_reservation" model="ir.actions.act_window">
116 <field name="name">Stock Reservations</field>
117 <field name="res_model">stock.reservation</field>
118 <field name="type">ir.actions.act_window</field>
119 <field name="view_type">form</field>
120 <field name="view_id" ref="view_stock_reservation_tree"/>
121 <field name="search_view_id" ref="view_stock_reservation_search"/>
122 <field name="context">{'search_default_draft': 1,
123 'search_default_reserved': 1,
124 'search_default_groupby_product': 1}</field>
125 <field name="help" type="html">
126 <p class="oe_view_nocontent_create">
127 Click to create a stock reservation.
128 </p><p>
129 This menu allow you to prepare and reserve some quantities
130 of products.
131 </p>
132 </field>
133 </record>
134
135 <menuitem action="action_stock_reservation"
136 id="menu_action_stock_reservation"
137 parent="stock.menu_stock_inventory_control"
138 sequence="30"/>
139 </data>
140</openerp>
0141
=== added directory 'stock_reserve_sale'
=== added file 'stock_reserve_sale/__init__.py'
--- stock_reserve_sale/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/__init__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import model
23from . import wizard
024
=== added file 'stock_reserve_sale/__openerp__.py'
--- stock_reserve_sale/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/__openerp__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,65 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22{'name': 'Stock Reserve Sales',
23 'version': '0.1',
24 'author': 'Camptocamp',
25 'category': 'Warehouse',
26 'license': 'AGPL-3',
27 'complexity': 'normal',
28 'images': [],
29 'website': "http://www.camptocamp.com",
30 'description': """
31Stock Reserve Sales
32===================
33
34Allows to create stock reservations for quotation lines before the
35confirmation of the quotation. The reservations might have a validity
36date and in any case they are lifted when the quotation is canceled or
37confirmed.
38
39Reservations can be done only on "make to stock" and stockable products.
40
41The reserved products are substracted from the virtual stock. It means
42that if you reserved a quantity of products which bring the virtual
43stock below the minimum, the orderpoint will be triggered and new
44purchase orders will be generated. It also implies that the max may be
45exceeded if the reservations are canceled.
46
47If you want to prevent sales orders to be confirmed when the stock is
48insufficient at the order date, you may want to install the
49`sale_exception_nostock` module.
50
51""",
52 'depends': ['sale_stock',
53 'stock_reserve',
54 ],
55 'demo': [],
56 'data': ['wizard/sale_stock_reserve_view.xml',
57 'view/sale.xml',
58 'view/stock_reserve.xml',
59 ],
60 'auto_install': False,
61 'test': ['test/sale_reserve.yml',
62 'test/sale_line_reserve.yml',
63 ],
64 'installable': True,
65 }
066
=== added directory 'stock_reserve_sale/i18n'
=== added directory 'stock_reserve_sale/model'
=== added file 'stock_reserve_sale/model/__init__.py'
--- stock_reserve_sale/model/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/model/__init__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import sale
23from . import stock_reserve
024
=== added file 'stock_reserve_sale/model/sale.py'
--- stock_reserve_sale/model/sale.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/model/sale.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,184 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.osv import orm, fields
23from openerp.tools.translate import _
24
25
26class sale_order(orm.Model):
27 _inherit = 'sale.order'
28
29 def _stock_reservation(self, cr, uid, ids, fields, args, context=None):
30 result = {}
31 for order_id in ids:
32 result[order_id] = {'has_stock_reservation': False,
33 'is_stock_reservable': False}
34 for sale in self.browse(cr, uid, ids, context=context):
35 for line in sale.order_line:
36 if line.reservation_ids:
37 result[sale.id]['has_stock_reservation'] = True
38 if line.is_stock_reservable:
39 result[sale.id]['is_stock_reservable'] = True
40 if sale.state not in ('draft', 'sent'):
41 result[sale.id]['is_stock_reservable'] = False
42 return result
43
44 _columns = {
45 'has_stock_reservation': fields.function(
46 _stock_reservation,
47 type='boolean',
48 readonly=True,
49 multi='stock_reservation',
50 string='Has Stock Reservations'),
51 'is_stock_reservable': fields.function(
52 _stock_reservation,
53 type='boolean',
54 readonly=True,
55 multi='stock_reservation',
56 string='Can Have Stock Reservations'),
57 }
58
59 def release_all_stock_reservation(self, cr, uid, ids, context=None):
60 sales = self.browse(cr, uid, ids, context=context)
61 line_ids = [line.id for sale in sales for line in sale.order_line]
62 line_obj = self.pool.get('sale.order.line')
63 line_obj.release_stock_reservation(cr, uid, line_ids, context=context)
64 return True
65
66 def action_button_confirm(self, cr, uid, ids, context=None):
67 self.release_all_stock_reservation(cr, uid, ids, context=context)
68 return super(sale_order, self).action_button_confirm(
69 cr, uid, ids, context=context)
70
71 def action_cancel(self, cr, uid, ids, context=None):
72 self.release_all_stock_reservation(cr, uid, ids, context=context)
73 return super(sale_order, self).action_cancel(
74 cr, uid, ids, context=context)
75
76
77class sale_order_line(orm.Model):
78 _inherit = 'sale.order.line'
79
80 def _is_stock_reservable(self, cr, uid, ids, fields, args, context=None):
81 result = {}.fromkeys(ids, False)
82 for line in self.browse(cr, uid, ids, context=context):
83 if line.state != 'draft':
84 continue
85 if line.type == 'make_to_order':
86 continue
87 if (not line.product_id or line.product_id.type == 'service'):
88 continue
89 if not line.reservation_ids:
90 result[line.id] = True
91 return result
92
93 _columns = {
94 'reservation_ids': fields.one2many(
95 'stock.reservation',
96 'sale_line_id',
97 string='Stock Reservation'),
98 'is_stock_reservable': fields.function(
99 _is_stock_reservable,
100 type='boolean',
101 readonly=True,
102 string='Can be reserved'),
103 }
104
105 def copy_data(self, cr, uid, id, default=None, context=None):
106 if default is None:
107 default = {}
108 default['reservation_ids'] = False
109 return super(sale_order_line, self).copy_data(
110 cr, uid, id, default=default, context=context)
111
112 def release_stock_reservation(self, cr, uid, ids, context=None):
113 lines = self.browse(cr, uid, ids, context=context)
114 reserv_ids = [reserv.id for line in lines
115 for reserv in line.reservation_ids]
116 reserv_obj = self.pool.get('stock.reservation')
117 reserv_obj.release(cr, uid, reserv_ids, context=context)
118 return True
119
120 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
121 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
122 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
123 result = super(sale_order_line, self).product_id_change(
124 cr, uid, ids, pricelist, product, qty=qty, uom=uom,
125 qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
126 lang=lang, update_tax=update_tax, date_order=date_order,
127 packaging=packaging, fiscal_position=fiscal_position,
128 flag=flag, context=context)
129 if not ids: # warn only if we change an existing line
130 return result
131 assert len(ids) == 1, "Expected 1 ID, got %r" % ids
132 line = self.browse(cr, uid, ids[0], context=context)
133 if qty != line.product_uom_qty and line.reservation_ids:
134 msg = _("As you changed the quantity of the line, "
135 "the quantity of the stock reservation will "
136 "be automatically adjusted to %.2f.") % qty
137 msg += "\n\n"
138 result.setdefault('warning', {})
139 if result['warning'].get('message'):
140 result['warning']['message'] += msg
141 else:
142 result['warning'] = {
143 'title': _('Configuration Error!'),
144 'message': msg,
145 }
146 return result
147
148 def write(self, cr, uid, ids, vals, context=None):
149 block_on_reserve = ('product_id', 'product_uom', 'product_uos',
150 'type')
151 update_on_reserve = ('price_unit', 'product_uom_qty', 'product_uos_qty')
152 keys = set(vals.keys())
153 test_block = keys.intersection(block_on_reserve)
154 test_update = keys.intersection(update_on_reserve)
155 if test_block:
156 for line in self.browse(cr, uid, ids, context=context):
157 if not line.reservation_ids:
158 continue
159 raise orm.except_orm(
160 _('Error'),
161 _('You cannot change the product or unit of measure '
162 'of lines with a stock reservation. '
163 'Release the reservation '
164 'before changing the product.'))
165 res = super(sale_order_line, self).write(cr, uid, ids, vals, context=context)
166 if test_update:
167 for line in self.browse(cr, uid, ids, context=context):
168 if not line.reservation_ids:
169 continue
170 if len(line.reservation_ids) > 1:
171 raise orm.except_orm(
172 _('Error'),
173 _('Several stock reservations are linked with the '
174 'line. Impossible to adjust their quantity. '
175 'Please release the reservation '
176 'before changing the quantity.'))
177
178 line.reservation_ids[0].write(
179 {'price_unit': line.price_unit,
180 'product_qty': line.product_uom_qty,
181 'product_uos_qty': line.product_uos_qty,
182 }
183 )
184 return res
0185
=== added file 'stock_reserve_sale/model/stock_reserve.py'
--- stock_reserve_sale/model/stock_reserve.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/model/stock_reserve.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,50 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.osv import orm, fields
23
24
25class stock_reservation(orm.Model):
26 _inherit = 'stock.reservation'
27
28 _columns = {
29 'sale_line_id': fields.many2one(
30 'sale.order.line',
31 string='Sale Order Line',
32 ondelete='cascade'),
33 'sale_id': fields.related(
34 'sale_line_id', 'order_id',
35 type='many2one',
36 relation='sale.order',
37 string='Sale Order')
38 }
39
40 def release(self, cr, uid, ids, context=None):
41 self.write(cr, uid, ids, {'sale_line_id': False}, context=context)
42 return super(stock_reservation, self).release(
43 cr, uid, ids, context=context)
44
45 def copy_data(self, cr, uid, id, default=None, context=None):
46 if default is None:
47 default = {}
48 default['sale_line_id'] = False
49 return super(stock_reservation, self).copy_data(
50 cr, uid, id, default=default, context=context)
051
=== added directory 'stock_reserve_sale/test'
=== added file 'stock_reserve_sale/test/sale_line_reserve.yml'
--- stock_reserve_sale/test/sale_line_reserve.yml 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/test/sale_line_reserve.yml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,118 @@
1-
2 I create a product to test the stock reservation
3-
4 !record {model: product.product, id: product_yogurt}:
5 default_code: 001yogurt
6 name: yogurt
7 type: product
8 categ_id: product.product_category_1
9 list_price: 100.0
10 standard_price: 70.0
11 uom_id: product.product_uom_kgm
12 uom_po_id: product.product_uom_kgm
13 procure_method: make_to_stock
14 valuation: real_time
15 cost_method: average
16 property_stock_account_input: account.o_expense
17 property_stock_account_output: account.o_income
18-
19 I update the current stock of the yogurt with 10 kgm
20-
21 !record {model: stock.change.product.qty, id: change_qty}:
22 new_quantity: 10
23 product_id: product_yogurt
24-
25 !python {model: stock.change.product.qty}: |
26 context['active_id'] = ref('product_yogurt')
27 self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
28-
29 In order to test reservation of the sales order, I create a sales order
30-
31 !record {model: sale.order, id: sale_reserve_02}:
32 partner_id: base.res_partner_2
33 payment_term: account.account_payment_term
34-
35 And I create a sales order line
36-
37 !record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}:
38 name: Yogurt
39 product_id: product_yogurt
40 product_uom_qty: 4
41 product_uom: product.product_uom_kgm
42 order_id: sale_reserve_02
43-
44 And I create a stock reserve for this line
45-
46 !record {model: sale.stock.reserve, id: wizard_reserve_02_01}:
47 note: Reservation for the sales order line
48-
49 I call the wizard to reserve the products of the sales order
50-
51 !python {model: sale.stock.reserve}: |
52 active_id = ref('sale_line_reserve_02_01')
53 context['active_id'] = active_id
54 context['active_ids'] = [active_id]
55 context['active_model'] = 'sale.order.line'
56 self.button_reserve(cr, uid, [ref('wizard_reserve_02_01')], context=context)
57-
58 I check Virtual stock of yogurt after update reservation
59-
60 !python {model: product.product}: |
61 product = self.browse(cr, uid, ref('product_yogurt'), context=context)
62 assert product.virtual_available == 6, "Stock is not updated."
63-
64 And I create a MTO sales order line
65-
66 !record {model: sale.order.line, id: sale_line_reserve_02_02, view: sale.view_order_line_tree}:
67 order_id: sale_reserve_02
68 name: Mouse, Wireless
69 product_id: product.product_product_12
70 type: make_to_order
71 product_uom_qty: 4
72 product_uom: product.product_uom_kgm
73-
74 And I try to create a stock reserve for this MTO line
75-
76 !record {model: sale.stock.reserve, id: wizard_reserve_02_02}:
77 note: Reservation for the sales order line
78-
79 I call the wizard to reserve the products of the sales order
80-
81 !python {model: sale.stock.reserve}: |
82 active_id = ref('sale_line_reserve_02_02')
83 context['active_id'] = active_id
84 context['active_ids'] = [active_id]
85 context['active_model'] = 'sale.order.line'
86 self.button_reserve(cr, uid, [ref('wizard_reserve_02_02')], context=context)
87-
88 I should not have a stock reservation for a MTO line
89-
90 !python {model: stock.reservation}: |
91 reserv_ids = self.search(
92 cr, uid,
93 [('sale_line_id', '=', ref('sale_line_reserve_02_02'))],
94 context=context)
95 assert not reserv_ids, "No stock reservation should be created for MTO lines"
96-
97 And I change the quantity in the first line
98-
99 !record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}:
100 product_uom_qty: 5
101-
102
103 I check Virtual stock of yogurt after change of reservations
104-
105 !python {model: product.product}: |
106 product = self.browse(cr, uid, ref('product_yogurt'), context=context)
107 assert product.virtual_available == 5, "Stock is not updated."
108-
109 I release the sales order's reservations for the first line
110-
111 !python {model: sale.order.line}: |
112 self.release_stock_reservation(cr, uid, [ref('sale_line_reserve_02_01')], context=context)
113-
114 I check Virtual stock of yogurt after release of reservations
115-
116 !python {model: product.product}: |
117 product = self.browse(cr, uid, ref('product_yogurt'), context=context)
118 assert product.virtual_available == 10, "Stock is not updated."
0119
=== added file 'stock_reserve_sale/test/sale_reserve.yml'
--- stock_reserve_sale/test/sale_reserve.yml 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/test/sale_reserve.yml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,65 @@
1-
2 I create a product to test the stock reservation
3-
4 !record {model: product.product, id: product_gelato}:
5 default_code: 001GELATO
6 name: Gelato
7 type: product
8 categ_id: product.product_category_1
9 list_price: 100.0
10 standard_price: 70.0
11 uom_id: product.product_uom_kgm
12 uom_po_id: product.product_uom_kgm
13 procure_method: make_to_stock
14 valuation: real_time
15 cost_method: average
16 property_stock_account_input: account.o_expense
17 property_stock_account_output: account.o_income
18-
19 I update the current stock of the Gelato with 10 kgm
20-
21 !record {model: stock.change.product.qty, id: change_qty}:
22 new_quantity: 10
23 product_id: product_gelato
24-
25 !python {model: stock.change.product.qty}: |
26 context['active_id'] = ref('product_gelato')
27 self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
28-
29 In order to test reservation of the sales order, I create a sales order
30-
31 !record {model: sale.order, id: sale_reserve_01}:
32 partner_id: base.res_partner_2
33 payment_term: account.account_payment_term
34 order_line:
35 - product_id: product_gelato
36 product_uom_qty: 4
37-
38 I call the wizard to reserve the products of the sales order
39-
40 !record {model: sale.stock.reserve, id: wizard_reserve_01}:
41 note: Reservation for the sales order
42-
43 !python {model: sale.stock.reserve}: |
44 active_id = ref('sale_reserve_01')
45 context['active_id'] = active_id
46 context['active_ids'] = [active_id]
47 context['active_model'] = 'sale.order'
48 self.button_reserve(cr, uid, [ref('wizard_reserve_01')], context=context)
49-
50 I check Virtual stock of Gelato after update reservation
51-
52 !python {model: product.product}: |
53 product = self.browse(cr, uid, ref('product_gelato'), context=context)
54 assert product.virtual_available == 6, "Stock is not updated."
55-
56 I release the sales order's reservations
57-
58 !python {model: sale.order}: |
59 self.release_all_stock_reservation(cr, uid, [ref('sale_reserve_01')], context=context)
60-
61 I check Virtual stock of Gelato after release of reservations
62-
63 !python {model: product.product}: |
64 product = self.browse(cr, uid, ref('product_gelato'), context=context)
65 assert product.virtual_available == 10, "Stock is not updated."
066
=== added directory 'stock_reserve_sale/view'
=== added file 'stock_reserve_sale/view/sale.xml'
--- stock_reserve_sale/view/sale.xml 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/view/sale.xml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,67 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4
5 <record id="view_order_form_reserve" model="ir.ui.view">
6 <field name="name">sale.order.form.reserve</field>
7 <field name="model">sale.order</field>
8 <field name="inherit_id" ref="sale_stock.view_order_form_inherit"/>
9 <field name="arch" type="xml">
10 <button name="action_quotation_send" position="before">
11 <field name="is_stock_reservable" invisible="1"/>
12 <button name="%(action_sale_stock_reserve)d"
13 type="action"
14 string="Reserve Stock"
15 help="Pre-book products from stock"
16 attrs="{'invisible': [('is_stock_reservable', '=', False)]}"
17 />
18 </button>
19
20 <field name="order_line" position="attributes">
21 <attribute name="options">{"reload_on_button": 1}</attribute>
22 </field>
23
24 <xpath expr="//field[@name='order_line']/form//field[@name='state']" position="before">
25 <field name="reservation_ids" invisible="1"/>
26 <button name="%(action_sale_stock_reserve)d"
27 type="action"
28 string="Reserve Stock"
29 attrs="{'invisible': ['|', ('reservation_ids', '!=', []),
30 ('state', '!=', 'draft')]}" />
31 <button name="release_stock_reservation"
32 type="object"
33 string="Release Reservation"
34 attrs="{'invisible': ['|', ('reservation_ids', '=', []),
35 ('state', '!=', 'draft')]}" />
36 </xpath>
37
38 <xpath expr="//field[@name='order_line']/tree/field[@name='price_subtotal']" position="after">
39 <field name="reservation_ids" invisible="1"/>
40 <field name="is_stock_reservable" invisible="1"/>
41 <button name="%(action_sale_stock_reserve)d"
42 type="action"
43 string="Reserve Stock"
44 icon="terp-locked"
45 attrs="{'invisible': [('is_stock_reservable', '=', False)]}" />
46 <button name="release_stock_reservation"
47 type="object"
48 string="Release Reservation"
49 icon="gtk-undo"
50 attrs="{'invisible': [('reservation_ids', '=', [])]}" />
51 </xpath>
52
53 <field name="invoiced" position="before">
54 <label for="has_stock_reservation"/>
55 <div>
56 <field name="has_stock_reservation"/>
57 <button name="release_all_stock_reservation"
58 string="cancel all"
59 type="object" class="oe_link"
60 attrs="{'invisible': [('has_stock_reservation', '=', False)]}"/>
61 </div>
62 </field>
63 </field>
64 </record>
65
66 </data>
67</openerp>
068
=== added file 'stock_reserve_sale/view/stock_reserve.xml'
--- stock_reserve_sale/view/stock_reserve.xml 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/view/stock_reserve.xml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4 <record id="view_stock_reservation_form" model="ir.ui.view">
5 <field name="name">stock.reservation.form</field>
6 <field name="model">stock.reservation</field>
7 <field name="inherit_id" ref="stock_reserve.view_stock_reservation_form"/>
8 <field name="arch" type="xml">
9 <group name="location" position="after">
10 <group name="sale" string="Sales">
11 <field name="sale_id"/>
12 <field name="sale_line_id"/>
13 </group>
14 </group>
15 </field>
16 </record>
17
18 <record id="view_stock_reservation_tree" model="ir.ui.view">
19 <field name="name">stock.reservation.tree</field>
20 <field name="model">stock.reservation</field>
21 <field name="inherit_id" ref="stock_reserve.view_stock_reservation_tree"/>
22 <field name="arch" type="xml">
23 <field name="move_id" position="before">
24 <field name="sale_id"/>
25 </field>
26 </field>
27 </record>
28
29 </data>
30</openerp>
031
=== added directory 'stock_reserve_sale/wizard'
=== added file 'stock_reserve_sale/wizard/__init__.py'
--- stock_reserve_sale/wizard/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/wizard/__init__.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from . import sale_stock_reserve
023
=== added file 'stock_reserve_sale/wizard/sale_stock_reserve.py'
--- stock_reserve_sale/wizard/sale_stock_reserve.py 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/wizard/sale_stock_reserve.py 2013-09-10 07:53:22 +0000
@@ -0,0 +1,111 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Guewen Baconnier
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21
22from openerp.osv import orm, fields
23
24
25class sale_stock_reserve(orm.TransientModel):
26 _name = 'sale.stock.reserve'
27
28 _columns = {
29 'location_id': fields.many2one(
30 'stock.location',
31 'Source Location',
32 required=True),
33 'location_dest_id': fields.many2one(
34 'stock.location',
35 'Reservation Location',
36 required=True,
37 help="Location where the system will reserve the "
38 "products."),
39 'date_validity': fields.date(
40 "Validity Date",
41 help="If a date is given, the reservations will be released "
42 "at the end of the validity."),
43 'note': fields.text('Notes'),
44 }
45
46 def _default_location_id(self, cr, uid, context=None):
47 reserv_obj = self.pool.get('stock.reservation')
48 return reserv_obj._default_location_id(cr, uid, context=context)
49
50 def _default_location_dest_id(self, cr, uid, context=None):
51 reserv_obj = self.pool.get('stock.reservation')
52 return reserv_obj._default_location_dest_id(cr, uid, context=context)
53
54 _defaults = {
55 'location_id': _default_location_id,
56 'location_dest_id': _default_location_dest_id,
57 }
58
59 def _prepare_stock_reservation(self, cr, uid, form, line, context=None):
60 product_uos = line.product_uos.id if line.product_uos else False
61 return {'product_id': line.product_id.id,
62 'product_uom': line.product_uom.id,
63 'product_qty': line.product_uom_qty,
64 'date_validity': form.date_validity,
65 'name': "{} ({})".format(line.order_id.name, line.name),
66 'location_id': form.location_id.id,
67 'location_dest_id': form.location_dest_id.id,
68 'note': form.note,
69 'product_uos_qty': line.product_uos_qty,
70 'product_uos': product_uos,
71 'price_unit': line.price_unit,
72 'sale_line_id': line.id,
73 }
74
75 def stock_reserve(self, cr, uid, ids, line_ids, context=None):
76 assert len(ids) == 1, "Expected 1 ID, got %r" % ids
77 reserv_obj = self.pool.get('stock.reservation')
78 line_obj = self.pool.get('sale.order.line')
79
80 form = self.browse(cr, uid, ids[0], context=context)
81 lines = line_obj.browse(cr, uid, line_ids, context=context)
82 for line in lines:
83 if not line.is_stock_reservable:
84 continue
85 vals = self._prepare_stock_reservation(cr, uid, form, line,
86 context=context)
87 reserv_id = reserv_obj.create(cr, uid, vals, context=context)
88 reserv_obj.reserve(cr, uid, [reserv_id], context=context)
89 return True
90
91 def button_reserve(self, cr, uid, ids, context=None):
92 assert len(ids) == 1, "Expected 1 ID, got %r" % ids
93 if context is None:
94 context = {}
95 close = {'type': 'ir.actions.act_window_close'}
96 active_model = context.get('active_model')
97 active_ids = context.get('active_ids')
98 if not (active_model and active_ids):
99 return close
100
101 line_obj = self.pool.get('sale.order.line')
102 if active_model == 'sale.order':
103 sale_obj = self.pool.get('sale.order')
104 sales = sale_obj.browse(cr, uid, active_ids, context=context)
105 line_ids = [line.id for sale in sales for line in sale.order_line]
106
107 if active_model == 'sale.order.line':
108 line_ids = active_ids
109
110 self.stock_reserve(cr, uid, ids, line_ids, context=context)
111 return close
0112
=== added file 'stock_reserve_sale/wizard/sale_stock_reserve_view.xml'
--- stock_reserve_sale/wizard/sale_stock_reserve_view.xml 1970-01-01 00:00:00 +0000
+++ stock_reserve_sale/wizard/sale_stock_reserve_view.xml 2013-09-10 07:53:22 +0000
@@ -0,0 +1,44 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="0">
4
5 <record id="view_sale_stock_reserve_form" model="ir.ui.view">
6 <field name="name">sale.stock.reserve.form</field>
7 <field name="model">sale.stock.reserve</field>
8 <field name="arch" type="xml">
9 <form string="Reserve Stock" version="7.0">
10 <p class="oe_grey">
11 A stock reservation will be created for the products
12 of the selected quotation lines. If a validity date is specified,
13 the reservation will be released once the date has passed.
14 </p>
15 <group>
16 <field name="location_id"/>
17 <field name="location_dest_id"/>
18 <field name="date_validity"/>
19 </group>
20 <group name="note" string="Notes">
21 <field name="note" nolabel="1"/>
22 </group>
23 <footer>
24 <button string="Reserve"
25 name="button_reserve"
26 type="object"
27 class="oe_highlight" />
28 or
29 <button special="cancel" class="oe_link" string="Cancel"/>
30 </footer>
31 </form>
32 </field>
33 </record>
34
35 <record id="action_sale_stock_reserve" model="ir.actions.act_window">
36 <field name="name">Reserve Stock for Quotation Lines</field>
37 <field name="type">ir.actions.act_window</field>
38 <field name="res_model">sale.stock.reserve</field>
39 <field name="view_type">form</field>
40 <field name="view_mode">form</field>
41 <field name="target">new</field>
42 </record>
43 </data>
44</openerp>

Subscribers

People subscribed via source and target branches