mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[MRG][ADD] stock_reserve, stock_reserve_sale: create stock reservation manually or from quotations
This commit is contained in:
22
stock_reserve/__init__.py
Normal file
22
stock_reserve/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import model
|
||||
58
stock_reserve/__openerp__.py
Normal file
58
stock_reserve/__openerp__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{'name': 'Stock Reserve',
|
||||
'version': '0.1',
|
||||
'author': 'Camptocamp',
|
||||
'category': 'Warehouse',
|
||||
'license': 'AGPL-3',
|
||||
'complexity': 'normal',
|
||||
'images': [],
|
||||
'website': "http://www.camptocamp.com",
|
||||
'description': """
|
||||
Stock Reserve
|
||||
=============
|
||||
|
||||
Allows to create stock reservations on products.
|
||||
|
||||
Each reservation can have a validity date, once passed, the reservation
|
||||
is automatically lifted.
|
||||
|
||||
The reserved products are substracted from the virtual stock. It means
|
||||
that if you reserved a quantity of products which bring the virtual
|
||||
stock below the minimum, the orderpoint will be triggered and new
|
||||
purchase orders will be generated. It also implies that the max may be
|
||||
exceeded if the reservations are canceled.
|
||||
|
||||
""",
|
||||
'depends': ['stock',
|
||||
],
|
||||
'demo': [],
|
||||
'data': ['view/stock_reserve.xml',
|
||||
'view/product.xml',
|
||||
'data/stock_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'auto_install': False,
|
||||
'test': ['test/stock_reserve.yml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
26
stock_reserve/data/stock_data.xml
Normal file
26
stock_reserve/data/stock_data.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<record id="stock_location_reservation" model="stock.location">
|
||||
<field name="name">Reservation Stock</field>
|
||||
<field name="location_id" ref="stock.stock_location_company"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Release the stock.reservation when the validity date has
|
||||
passed -->
|
||||
<record forcecreate="True" id="ir_cron_release_stock_reservation" model="ir.cron">
|
||||
<field name="name">Release the stock reservation having a passed validity date</field>
|
||||
<field eval="True" name="active" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall" />
|
||||
<field name="model">stock.reservation</field>
|
||||
<field name="function">release_validity_exceeded</field>
|
||||
<field name="args">()</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
23
stock_reserve/model/__init__.py
Normal file
23
stock_reserve/model/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import stock_reserve
|
||||
from . import product
|
||||
40
stock_reserve/model/product.py
Normal file
40
stock_reserve/model/product.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
|
||||
|
||||
class product_product(orm.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
def open_stock_reservation(self, cr, uid, ids, context=None):
|
||||
assert len(ids) == 1, "Expected 1 ID, got %r" % ids
|
||||
mod_obj = self.pool.get('ir.model.data')
|
||||
act_obj = self.pool.get('ir.actions.act_window')
|
||||
get_ref = mod_obj.get_object_reference
|
||||
__, action_id = get_ref(cr, uid, 'stock_reserve',
|
||||
'action_stock_reservation')
|
||||
action = act_obj.read(cr, uid, action_id, context=context)
|
||||
action['context'] = {'search_default_draft': 1,
|
||||
'search_default_reserved': 1,
|
||||
'default_product_id': ids[0],
|
||||
'search_default_product_id': ids[0]}
|
||||
return action
|
||||
177
stock_reserve/model/stock_reserve.py
Normal file
177
stock_reserve/model/stock_reserve.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
from openerp.tools.translate import _
|
||||
|
||||
|
||||
class stock_reservation(orm.Model):
|
||||
""" Allow to reserve products.
|
||||
|
||||
The fields mandatory for the creation of a reservation are:
|
||||
|
||||
* product_id
|
||||
* product_qty
|
||||
* product_uom
|
||||
* name
|
||||
|
||||
The following fields are required but have default values that you may
|
||||
want to override:
|
||||
|
||||
* company_id
|
||||
* location_id
|
||||
* dest_location_id
|
||||
|
||||
Optionally, you may be interested to define:
|
||||
|
||||
* date_validity (once passed, the reservation will be released)
|
||||
* note
|
||||
"""
|
||||
_name = 'stock.reservation'
|
||||
_description = 'Stock Reservation'
|
||||
_inherits = {'stock.move': 'move_id'}
|
||||
|
||||
_columns = {
|
||||
'move_id': fields.many2one('stock.move',
|
||||
'Reservation Move',
|
||||
required=True,
|
||||
readonly=True,
|
||||
ondelete='cascade',
|
||||
select=1),
|
||||
'date_validity': fields.date('Validity Date'),
|
||||
}
|
||||
|
||||
def get_location_from_ref(self, cr, uid, ref, context=None):
|
||||
""" Get a location from a xmlid if allowed
|
||||
:param ref: tuple (module, xmlid)
|
||||
"""
|
||||
location_obj = self.pool.get('stock.location')
|
||||
data_obj = self.pool.get('ir.model.data')
|
||||
get_ref = data_obj.get_object_reference
|
||||
try:
|
||||
__, location_id = get_ref(cr, uid, *ref)
|
||||
location_obj.check_access_rule(cr, uid, [location_id],
|
||||
'read', context=context)
|
||||
except (orm.except_orm, ValueError):
|
||||
location_id = False
|
||||
return location_id
|
||||
|
||||
def _default_location_id(self, cr, uid, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
move_obj = self.pool.get('stock.move')
|
||||
context['picking_type'] = 'internal'
|
||||
return move_obj._default_location_source(cr, uid, context=context)
|
||||
|
||||
def _default_location_dest_id(self, cr, uid, context=None):
|
||||
ref = ('stock_reserve', 'stock_location_reservation')
|
||||
return self.get_location_from_ref(cr, uid, ref, context=context)
|
||||
|
||||
_defaults = {
|
||||
'type': 'internal',
|
||||
'location_id': _default_location_id,
|
||||
'location_dest_id': _default_location_dest_id,
|
||||
'product_qty': 1.0,
|
||||
}
|
||||
|
||||
def reserve(self, cr, uid, ids, context=None):
|
||||
""" Confirm a reservation
|
||||
|
||||
The reservation is done using the default UOM of the product.
|
||||
A date until which the product is reserved can be specified.
|
||||
"""
|
||||
move_obj = self.pool.get('stock.move')
|
||||
reservations = self.browse(cr, uid, ids, context=context)
|
||||
move_ids = [reserv.move_id.id for reserv in reservations]
|
||||
move_obj.write(cr, uid, move_ids,
|
||||
{'date_expected': fields.datetime.now()},
|
||||
context=context)
|
||||
move_obj.action_confirm(cr, uid, move_ids, context=context)
|
||||
move_obj.force_assign(cr, uid, move_ids, context=context)
|
||||
return True
|
||||
|
||||
def release(self, cr, uid, ids, context=None):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
reservations = self.read(cr, uid, ids, ['move_id'],
|
||||
context=context, load='_classic_write')
|
||||
move_obj = self.pool.get('stock.move')
|
||||
move_ids = [reserv['move_id'] for reserv in reservations]
|
||||
move_obj.action_cancel(cr, uid, move_ids, context=context)
|
||||
return True
|
||||
|
||||
def release_validity_exceeded(self, cr, uid, ids=None, context=None):
|
||||
""" Release all the reservation having an exceeded validity date """
|
||||
domain = [('date_validity', '<', fields.date.today()),
|
||||
('state', '=', 'assigned')]
|
||||
if ids:
|
||||
domain.append(('id', 'in', ids))
|
||||
reserv_ids = self.search(cr, uid, domain, context=context)
|
||||
self.release(cr, uid, reserv_ids, context=context)
|
||||
return True
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
""" Release the reservation before the unlink """
|
||||
self.release(cr, uid, ids, context=context)
|
||||
return super(stock_reservation, self).unlink(cr, uid, ids,
|
||||
context=context)
|
||||
|
||||
def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
|
||||
move_obj = self.pool.get('stock.move')
|
||||
if ids:
|
||||
reserv = self.read(cr, uid, ids, ['move_id'], context=context,
|
||||
load='_classic_write')
|
||||
move_ids = [rv['move_id'] for rv in reserv]
|
||||
else:
|
||||
move_ids = []
|
||||
result = move_obj.onchange_product_id(
|
||||
cr, uid, move_ids, prod_id=product_id, loc_id=False,
|
||||
loc_dest_id=False, partner_id=False)
|
||||
if result.get('value'):
|
||||
vals = result['value']
|
||||
# only keep the existing fields on the view
|
||||
keep = ('product_uom', 'name')
|
||||
result['value'] = dict((key, value) for key, value in
|
||||
result['value'].iteritems() if
|
||||
key in keep)
|
||||
return result
|
||||
|
||||
def onchange_quantity(self, cr, uid, ids, product_id, product_qty, context=None):
|
||||
""" On change of product quantity avoid negative quantities """
|
||||
if not product_id or product_qty <= 0.0:
|
||||
return {'value': {'product_qty': 0.0}}
|
||||
return {}
|
||||
|
||||
def open_move(self, cr, uid, ids, context=None):
|
||||
assert len(ids) == 1, "1 ID expected, got %r" % ids
|
||||
reserv = self.read(cr, uid, ids[0], ['move_id'], context=context,
|
||||
load='_classic_write')
|
||||
mod_obj = self.pool.get('ir.model.data')
|
||||
act_obj = self.pool.get('ir.actions.act_window')
|
||||
get_ref = mod_obj.get_object_reference
|
||||
__, action_id = get_ref(cr, uid, 'stock', 'action_move_form2')
|
||||
action = act_obj.read(cr, uid, action_id, context=context)
|
||||
action['name'] = _('Reservation Move')
|
||||
# open directly in the form view
|
||||
__, view_id = get_ref(cr, uid, 'stock', 'view_move_form')
|
||||
action['views'] = [(view_id, 'form')]
|
||||
action['res_id'] = reserv['move_id']
|
||||
return action
|
||||
3
stock_reserve/security/ir.model.access.csv
Normal file
3
stock_reserve/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_stock_reservation_manager,stock.reservation manager,model_stock_reservation,stock.group_stock_manager,1,1,1,1
|
||||
access_stock_reservation_user,stock.reservation user,model_stock_reservation,stock.group_stock_user,1,1,1,0
|
||||
|
63
stock_reserve/test/stock_reserve.yml
Normal file
63
stock_reserve/test/stock_reserve.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
-
|
||||
I create a product to test the stock reservation
|
||||
-
|
||||
!record {model: product.product, id: product_sorbet}:
|
||||
default_code: 001SORBET
|
||||
name: Sorbet
|
||||
type: product
|
||||
categ_id: product.product_category_1
|
||||
list_price: 100.0
|
||||
standard_price: 70.0
|
||||
uom_id: product.product_uom_kgm
|
||||
uom_po_id: product.product_uom_kgm
|
||||
procure_method: make_to_stock
|
||||
valuation: real_time
|
||||
cost_method: average
|
||||
property_stock_account_input: account.o_expense
|
||||
property_stock_account_output: account.o_income
|
||||
-
|
||||
I update the current stock of the Sorbet with 10 kgm
|
||||
-
|
||||
!record {model: stock.change.product.qty, id: change_qty}:
|
||||
new_quantity: 10
|
||||
product_id: product_sorbet
|
||||
-
|
||||
!python {model: stock.change.product.qty}: |
|
||||
context['active_id'] = ref('stock_reserve.product_sorbet')
|
||||
self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
|
||||
-
|
||||
I check Virtual stock of Sorbet after update stock.
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
|
||||
assert product.virtual_available == 10, "Stock is not updated."
|
||||
-
|
||||
I create a stock reservation for 5 kgm
|
||||
-
|
||||
!record {model: stock.reservation, id: reserv_sorbet1}:
|
||||
product_id: product_sorbet
|
||||
product_qty: 5.0
|
||||
product_uom: product.product_uom_kgm
|
||||
name: reserve 5 kgm of sorbet for test
|
||||
-
|
||||
I confirm the reservation
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
self.reserve(cr, uid, [ref('reserv_sorbet1')], context=context)
|
||||
-
|
||||
I check Virtual stock of Sorbet after update reservation
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
|
||||
assert product.virtual_available == 5, "Stock is not updated."
|
||||
-
|
||||
I release the reservation
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
self.release(cr, uid, [ref('reserv_sorbet1')], context=context)
|
||||
-
|
||||
I check Virtual stock of Sorbet after update reservation
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('stock_reserve.product_sorbet'), context=context)
|
||||
assert product.virtual_available == 10, "Stock is not updated."
|
||||
19
stock_reserve/view/product.xml
Normal file
19
stock_reserve/view/product.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<record model="ir.ui.view" id="product_form_view">
|
||||
<field name="name">product.product.form.reserve</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="procurement.product_form_view_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='buttons']" position="inside">
|
||||
<button string="Stock Reservations"
|
||||
name="open_stock_reservation"
|
||||
type="object"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
140
stock_reserve/view/stock_reserve.xml
Normal file
140
stock_reserve/view/stock_reserve.xml
Normal file
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
<record id="view_stock_reservation_form" model="ir.ui.view">
|
||||
<field name="name">stock.reservation.form</field>
|
||||
<field name="model">stock.reservation</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Stock Reservations" version="7.0">
|
||||
<header>
|
||||
<button name="reserve" type="object"
|
||||
string="Reserve"
|
||||
class="oe_highlight"
|
||||
states="draft"/>
|
||||
<button name="release" type="object"
|
||||
string="Release"
|
||||
class="oe_highlight"
|
||||
states="assigned,confirmed,done"/>
|
||||
<button name="open_move" type="object"
|
||||
string="View Reservation Move"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,assigned"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group name="main_grp" string="Details">
|
||||
<field name="product_id"
|
||||
on_change="onchange_product_id(product_id)"
|
||||
/>
|
||||
<label for="product_qty" />
|
||||
<div>
|
||||
<field name="product_qty"
|
||||
on_change="onchange_quantity(product_id, product_qty)"
|
||||
class="oe_inline"/>
|
||||
<field name="product_uom"
|
||||
groups="product.group_uom" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="name"/>
|
||||
<field name="date_validity" />
|
||||
<field name="create_date" groups="base.group_no_one"/>
|
||||
<field name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
widget="selection"/>
|
||||
</group>
|
||||
<group name="location" string="Locations"
|
||||
groups="stock.group_locations">
|
||||
<field name="location_id"/>
|
||||
<field name="location_dest_id"/>
|
||||
</group>
|
||||
<group name="note" string="Notes">
|
||||
<field name="note" nolabel="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_stock_reservation_tree" model="ir.ui.view">
|
||||
<field name="name">stock.reservation.tree</field>
|
||||
<field name="model">stock.reservation</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Stock Reservations" version="7.0"
|
||||
colors="blue:state == 'draft';grey:state == 'cancel'" >
|
||||
<field name="name" />
|
||||
<field name="product_id" />
|
||||
<field name="move_id" />
|
||||
<field name="product_qty" sum="Total" />
|
||||
<field name="product_uom" />
|
||||
<field name="date_validity" />
|
||||
<field name="state"/>
|
||||
<button name="reserve" type="object"
|
||||
string="Reserve"
|
||||
icon="terp-locked"
|
||||
states="draft"/>
|
||||
<button name="release" type="object"
|
||||
string="Release"
|
||||
icon="gtk-undo"
|
||||
states="assigned,confirmed,done"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_stock_reservation_search" model="ir.ui.view">
|
||||
<field name="name">stock.reservation.search</field>
|
||||
<field name="model">stock.reservation</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Stock Reservations" version="7.0">
|
||||
<filter name="draft" string="Draft"
|
||||
domain="[('state', '=', 'draft')]"
|
||||
help="Not already reserved"/>
|
||||
<filter name="reserved" string="Reserved"
|
||||
domain="[('state', '=', 'assigned')]"
|
||||
help="Moves are reserved."/>
|
||||
<filter name="cancel" string="Released"
|
||||
domain="[('state', '=', 'cancel')]"
|
||||
help="Reservations have been released."/>
|
||||
<field name="name" />
|
||||
<field name="product_id" />
|
||||
<field name="move_id" />
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Status"
|
||||
name="groupby_state"
|
||||
domain="[]" context="{'group_by': 'state'}"/>
|
||||
<filter string="Product" domain="[]"
|
||||
name="groupby_product"
|
||||
context="{'group_by': 'product_id'}"/>
|
||||
<filter string="Product UoM" domain="[]"
|
||||
name="groupby_product_uom"
|
||||
context="{'group_by': 'product_uom'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_stock_reservation" model="ir.actions.act_window">
|
||||
<field name="name">Stock Reservations</field>
|
||||
<field name="res_model">stock.reservation</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="view_stock_reservation_tree"/>
|
||||
<field name="search_view_id" ref="view_stock_reservation_search"/>
|
||||
<field name="context">{'search_default_draft': 1,
|
||||
'search_default_reserved': 1,
|
||||
'search_default_groupby_product': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create a stock reservation.
|
||||
</p><p>
|
||||
This menu allow you to prepare and reserve some quantities
|
||||
of products.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_stock_reservation"
|
||||
id="menu_action_stock_reservation"
|
||||
parent="stock.menu_stock_inventory_control"
|
||||
sequence="30"/>
|
||||
</data>
|
||||
</openerp>
|
||||
23
stock_reserve_sale/__init__.py
Normal file
23
stock_reserve_sale/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import model
|
||||
from . import wizard
|
||||
65
stock_reserve_sale/__openerp__.py
Normal file
65
stock_reserve_sale/__openerp__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{'name': 'Stock Reserve Sales',
|
||||
'version': '0.1',
|
||||
'author': 'Camptocamp',
|
||||
'category': 'Warehouse',
|
||||
'license': 'AGPL-3',
|
||||
'complexity': 'normal',
|
||||
'images': [],
|
||||
'website': "http://www.camptocamp.com",
|
||||
'description': """
|
||||
Stock Reserve Sales
|
||||
===================
|
||||
|
||||
Allows to create stock reservations for quotation lines before the
|
||||
confirmation of the quotation. The reservations might have a validity
|
||||
date and in any case they are lifted when the quotation is canceled or
|
||||
confirmed.
|
||||
|
||||
Reservations can be done only on "make to stock" and stockable products.
|
||||
|
||||
The reserved products are substracted from the virtual stock. It means
|
||||
that if you reserved a quantity of products which bring the virtual
|
||||
stock below the minimum, the orderpoint will be triggered and new
|
||||
purchase orders will be generated. It also implies that the max may be
|
||||
exceeded if the reservations are canceled.
|
||||
|
||||
If you want to prevent sales orders to be confirmed when the stock is
|
||||
insufficient at the order date, you may want to install the
|
||||
`sale_exception_nostock` module.
|
||||
|
||||
""",
|
||||
'depends': ['sale_stock',
|
||||
'stock_reserve',
|
||||
],
|
||||
'demo': [],
|
||||
'data': ['wizard/sale_stock_reserve_view.xml',
|
||||
'view/sale.xml',
|
||||
'view/stock_reserve.xml',
|
||||
],
|
||||
'auto_install': False,
|
||||
'test': ['test/sale_reserve.yml',
|
||||
'test/sale_line_reserve.yml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
23
stock_reserve_sale/model/__init__.py
Normal file
23
stock_reserve_sale/model/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import sale
|
||||
from . import stock_reserve
|
||||
184
stock_reserve_sale/model/sale.py
Normal file
184
stock_reserve_sale/model/sale.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
from openerp.tools.translate import _
|
||||
|
||||
|
||||
class sale_order(orm.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
def _stock_reservation(self, cr, uid, ids, fields, args, context=None):
|
||||
result = {}
|
||||
for order_id in ids:
|
||||
result[order_id] = {'has_stock_reservation': False,
|
||||
'is_stock_reservable': False}
|
||||
for sale in self.browse(cr, uid, ids, context=context):
|
||||
for line in sale.order_line:
|
||||
if line.reservation_ids:
|
||||
result[sale.id]['has_stock_reservation'] = True
|
||||
if line.is_stock_reservable:
|
||||
result[sale.id]['is_stock_reservable'] = True
|
||||
if sale.state not in ('draft', 'sent'):
|
||||
result[sale.id]['is_stock_reservable'] = False
|
||||
return result
|
||||
|
||||
_columns = {
|
||||
'has_stock_reservation': fields.function(
|
||||
_stock_reservation,
|
||||
type='boolean',
|
||||
readonly=True,
|
||||
multi='stock_reservation',
|
||||
string='Has Stock Reservations'),
|
||||
'is_stock_reservable': fields.function(
|
||||
_stock_reservation,
|
||||
type='boolean',
|
||||
readonly=True,
|
||||
multi='stock_reservation',
|
||||
string='Can Have Stock Reservations'),
|
||||
}
|
||||
|
||||
def release_all_stock_reservation(self, cr, uid, ids, context=None):
|
||||
sales = self.browse(cr, uid, ids, context=context)
|
||||
line_ids = [line.id for sale in sales for line in sale.order_line]
|
||||
line_obj = self.pool.get('sale.order.line')
|
||||
line_obj.release_stock_reservation(cr, uid, line_ids, context=context)
|
||||
return True
|
||||
|
||||
def action_button_confirm(self, cr, uid, ids, context=None):
|
||||
self.release_all_stock_reservation(cr, uid, ids, context=context)
|
||||
return super(sale_order, self).action_button_confirm(
|
||||
cr, uid, ids, context=context)
|
||||
|
||||
def action_cancel(self, cr, uid, ids, context=None):
|
||||
self.release_all_stock_reservation(cr, uid, ids, context=context)
|
||||
return super(sale_order, self).action_cancel(
|
||||
cr, uid, ids, context=context)
|
||||
|
||||
|
||||
class sale_order_line(orm.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
def _is_stock_reservable(self, cr, uid, ids, fields, args, context=None):
|
||||
result = {}.fromkeys(ids, False)
|
||||
for line in self.browse(cr, uid, ids, context=context):
|
||||
if line.state != 'draft':
|
||||
continue
|
||||
if line.type == 'make_to_order':
|
||||
continue
|
||||
if (not line.product_id or line.product_id.type == 'service'):
|
||||
continue
|
||||
if not line.reservation_ids:
|
||||
result[line.id] = True
|
||||
return result
|
||||
|
||||
_columns = {
|
||||
'reservation_ids': fields.one2many(
|
||||
'stock.reservation',
|
||||
'sale_line_id',
|
||||
string='Stock Reservation'),
|
||||
'is_stock_reservable': fields.function(
|
||||
_is_stock_reservable,
|
||||
type='boolean',
|
||||
readonly=True,
|
||||
string='Can be reserved'),
|
||||
}
|
||||
|
||||
def copy_data(self, cr, uid, id, default=None, context=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
default['reservation_ids'] = False
|
||||
return super(sale_order_line, self).copy_data(
|
||||
cr, uid, id, default=default, context=context)
|
||||
|
||||
def release_stock_reservation(self, cr, uid, ids, context=None):
|
||||
lines = self.browse(cr, uid, ids, context=context)
|
||||
reserv_ids = [reserv.id for line in lines
|
||||
for reserv in line.reservation_ids]
|
||||
reserv_obj = self.pool.get('stock.reservation')
|
||||
reserv_obj.release(cr, uid, reserv_ids, context=context)
|
||||
return True
|
||||
|
||||
def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
|
||||
uom=False, qty_uos=0, uos=False, name='', partner_id=False,
|
||||
lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
|
||||
result = super(sale_order_line, self).product_id_change(
|
||||
cr, uid, ids, pricelist, product, qty=qty, uom=uom,
|
||||
qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
|
||||
lang=lang, update_tax=update_tax, date_order=date_order,
|
||||
packaging=packaging, fiscal_position=fiscal_position,
|
||||
flag=flag, context=context)
|
||||
if not ids: # warn only if we change an existing line
|
||||
return result
|
||||
assert len(ids) == 1, "Expected 1 ID, got %r" % ids
|
||||
line = self.browse(cr, uid, ids[0], context=context)
|
||||
if qty != line.product_uom_qty and line.reservation_ids:
|
||||
msg = _("As you changed the quantity of the line, "
|
||||
"the quantity of the stock reservation will "
|
||||
"be automatically adjusted to %.2f.") % qty
|
||||
msg += "\n\n"
|
||||
result.setdefault('warning', {})
|
||||
if result['warning'].get('message'):
|
||||
result['warning']['message'] += msg
|
||||
else:
|
||||
result['warning'] = {
|
||||
'title': _('Configuration Error!'),
|
||||
'message': msg,
|
||||
}
|
||||
return result
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
block_on_reserve = ('product_id', 'product_uom', 'product_uos',
|
||||
'type')
|
||||
update_on_reserve = ('price_unit', 'product_uom_qty', 'product_uos_qty')
|
||||
keys = set(vals.keys())
|
||||
test_block = keys.intersection(block_on_reserve)
|
||||
test_update = keys.intersection(update_on_reserve)
|
||||
if test_block:
|
||||
for line in self.browse(cr, uid, ids, context=context):
|
||||
if not line.reservation_ids:
|
||||
continue
|
||||
raise orm.except_orm(
|
||||
_('Error'),
|
||||
_('You cannot change the product or unit of measure '
|
||||
'of lines with a stock reservation. '
|
||||
'Release the reservation '
|
||||
'before changing the product.'))
|
||||
res = super(sale_order_line, self).write(cr, uid, ids, vals, context=context)
|
||||
if test_update:
|
||||
for line in self.browse(cr, uid, ids, context=context):
|
||||
if not line.reservation_ids:
|
||||
continue
|
||||
if len(line.reservation_ids) > 1:
|
||||
raise orm.except_orm(
|
||||
_('Error'),
|
||||
_('Several stock reservations are linked with the '
|
||||
'line. Impossible to adjust their quantity. '
|
||||
'Please release the reservation '
|
||||
'before changing the quantity.'))
|
||||
|
||||
line.reservation_ids[0].write(
|
||||
{'price_unit': line.price_unit,
|
||||
'product_qty': line.product_uom_qty,
|
||||
'product_uos_qty': line.product_uos_qty,
|
||||
}
|
||||
)
|
||||
return res
|
||||
50
stock_reserve_sale/model/stock_reserve.py
Normal file
50
stock_reserve_sale/model/stock_reserve.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
|
||||
|
||||
class stock_reservation(orm.Model):
|
||||
_inherit = 'stock.reservation'
|
||||
|
||||
_columns = {
|
||||
'sale_line_id': fields.many2one(
|
||||
'sale.order.line',
|
||||
string='Sale Order Line',
|
||||
ondelete='cascade'),
|
||||
'sale_id': fields.related(
|
||||
'sale_line_id', 'order_id',
|
||||
type='many2one',
|
||||
relation='sale.order',
|
||||
string='Sale Order')
|
||||
}
|
||||
|
||||
def release(self, cr, uid, ids, context=None):
|
||||
self.write(cr, uid, ids, {'sale_line_id': False}, context=context)
|
||||
return super(stock_reservation, self).release(
|
||||
cr, uid, ids, context=context)
|
||||
|
||||
def copy_data(self, cr, uid, id, default=None, context=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
default['sale_line_id'] = False
|
||||
return super(stock_reservation, self).copy_data(
|
||||
cr, uid, id, default=default, context=context)
|
||||
118
stock_reserve_sale/test/sale_line_reserve.yml
Normal file
118
stock_reserve_sale/test/sale_line_reserve.yml
Normal file
@@ -0,0 +1,118 @@
|
||||
-
|
||||
I create a product to test the stock reservation
|
||||
-
|
||||
!record {model: product.product, id: product_yogurt}:
|
||||
default_code: 001yogurt
|
||||
name: yogurt
|
||||
type: product
|
||||
categ_id: product.product_category_1
|
||||
list_price: 100.0
|
||||
standard_price: 70.0
|
||||
uom_id: product.product_uom_kgm
|
||||
uom_po_id: product.product_uom_kgm
|
||||
procure_method: make_to_stock
|
||||
valuation: real_time
|
||||
cost_method: average
|
||||
property_stock_account_input: account.o_expense
|
||||
property_stock_account_output: account.o_income
|
||||
-
|
||||
I update the current stock of the yogurt with 10 kgm
|
||||
-
|
||||
!record {model: stock.change.product.qty, id: change_qty}:
|
||||
new_quantity: 10
|
||||
product_id: product_yogurt
|
||||
-
|
||||
!python {model: stock.change.product.qty}: |
|
||||
context['active_id'] = ref('product_yogurt')
|
||||
self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
|
||||
-
|
||||
In order to test reservation of the sales order, I create a sales order
|
||||
-
|
||||
!record {model: sale.order, id: sale_reserve_02}:
|
||||
partner_id: base.res_partner_2
|
||||
payment_term: account.account_payment_term
|
||||
-
|
||||
And I create a sales order line
|
||||
-
|
||||
!record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}:
|
||||
name: Yogurt
|
||||
product_id: product_yogurt
|
||||
product_uom_qty: 4
|
||||
product_uom: product.product_uom_kgm
|
||||
order_id: sale_reserve_02
|
||||
-
|
||||
And I create a stock reserve for this line
|
||||
-
|
||||
!record {model: sale.stock.reserve, id: wizard_reserve_02_01}:
|
||||
note: Reservation for the sales order line
|
||||
-
|
||||
I call the wizard to reserve the products of the sales order
|
||||
-
|
||||
!python {model: sale.stock.reserve}: |
|
||||
active_id = ref('sale_line_reserve_02_01')
|
||||
context['active_id'] = active_id
|
||||
context['active_ids'] = [active_id]
|
||||
context['active_model'] = 'sale.order.line'
|
||||
self.button_reserve(cr, uid, [ref('wizard_reserve_02_01')], context=context)
|
||||
-
|
||||
I check Virtual stock of yogurt after update reservation
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('product_yogurt'), context=context)
|
||||
assert product.virtual_available == 6, "Stock is not updated."
|
||||
-
|
||||
And I create a MTO sales order line
|
||||
-
|
||||
!record {model: sale.order.line, id: sale_line_reserve_02_02, view: sale.view_order_line_tree}:
|
||||
order_id: sale_reserve_02
|
||||
name: Mouse, Wireless
|
||||
product_id: product.product_product_12
|
||||
type: make_to_order
|
||||
product_uom_qty: 4
|
||||
product_uom: product.product_uom_kgm
|
||||
-
|
||||
And I try to create a stock reserve for this MTO line
|
||||
-
|
||||
!record {model: sale.stock.reserve, id: wizard_reserve_02_02}:
|
||||
note: Reservation for the sales order line
|
||||
-
|
||||
I call the wizard to reserve the products of the sales order
|
||||
-
|
||||
!python {model: sale.stock.reserve}: |
|
||||
active_id = ref('sale_line_reserve_02_02')
|
||||
context['active_id'] = active_id
|
||||
context['active_ids'] = [active_id]
|
||||
context['active_model'] = 'sale.order.line'
|
||||
self.button_reserve(cr, uid, [ref('wizard_reserve_02_02')], context=context)
|
||||
-
|
||||
I should not have a stock reservation for a MTO line
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
reserv_ids = self.search(
|
||||
cr, uid,
|
||||
[('sale_line_id', '=', ref('sale_line_reserve_02_02'))],
|
||||
context=context)
|
||||
assert not reserv_ids, "No stock reservation should be created for MTO lines"
|
||||
-
|
||||
And I change the quantity in the first line
|
||||
-
|
||||
!record {model: sale.order.line, id: sale_line_reserve_02_01, view: sale.view_order_line_tree}:
|
||||
product_uom_qty: 5
|
||||
-
|
||||
|
||||
I check Virtual stock of yogurt after change of reservations
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('product_yogurt'), context=context)
|
||||
assert product.virtual_available == 5, "Stock is not updated."
|
||||
-
|
||||
I release the sales order's reservations for the first line
|
||||
-
|
||||
!python {model: sale.order.line}: |
|
||||
self.release_stock_reservation(cr, uid, [ref('sale_line_reserve_02_01')], context=context)
|
||||
-
|
||||
I check Virtual stock of yogurt after release of reservations
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('product_yogurt'), context=context)
|
||||
assert product.virtual_available == 10, "Stock is not updated."
|
||||
65
stock_reserve_sale/test/sale_reserve.yml
Normal file
65
stock_reserve_sale/test/sale_reserve.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
-
|
||||
I create a product to test the stock reservation
|
||||
-
|
||||
!record {model: product.product, id: product_gelato}:
|
||||
default_code: 001GELATO
|
||||
name: Gelato
|
||||
type: product
|
||||
categ_id: product.product_category_1
|
||||
list_price: 100.0
|
||||
standard_price: 70.0
|
||||
uom_id: product.product_uom_kgm
|
||||
uom_po_id: product.product_uom_kgm
|
||||
procure_method: make_to_stock
|
||||
valuation: real_time
|
||||
cost_method: average
|
||||
property_stock_account_input: account.o_expense
|
||||
property_stock_account_output: account.o_income
|
||||
-
|
||||
I update the current stock of the Gelato with 10 kgm
|
||||
-
|
||||
!record {model: stock.change.product.qty, id: change_qty}:
|
||||
new_quantity: 10
|
||||
product_id: product_gelato
|
||||
-
|
||||
!python {model: stock.change.product.qty}: |
|
||||
context['active_id'] = ref('product_gelato')
|
||||
self.change_product_qty(cr, uid, [ref('change_qty')], context=context)
|
||||
-
|
||||
In order to test reservation of the sales order, I create a sales order
|
||||
-
|
||||
!record {model: sale.order, id: sale_reserve_01}:
|
||||
partner_id: base.res_partner_2
|
||||
payment_term: account.account_payment_term
|
||||
order_line:
|
||||
- product_id: product_gelato
|
||||
product_uom_qty: 4
|
||||
-
|
||||
I call the wizard to reserve the products of the sales order
|
||||
-
|
||||
!record {model: sale.stock.reserve, id: wizard_reserve_01}:
|
||||
note: Reservation for the sales order
|
||||
-
|
||||
!python {model: sale.stock.reserve}: |
|
||||
active_id = ref('sale_reserve_01')
|
||||
context['active_id'] = active_id
|
||||
context['active_ids'] = [active_id]
|
||||
context['active_model'] = 'sale.order'
|
||||
self.button_reserve(cr, uid, [ref('wizard_reserve_01')], context=context)
|
||||
-
|
||||
I check Virtual stock of Gelato after update reservation
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('product_gelato'), context=context)
|
||||
assert product.virtual_available == 6, "Stock is not updated."
|
||||
-
|
||||
I release the sales order's reservations
|
||||
-
|
||||
!python {model: sale.order}: |
|
||||
self.release_all_stock_reservation(cr, uid, [ref('sale_reserve_01')], context=context)
|
||||
-
|
||||
I check Virtual stock of Gelato after release of reservations
|
||||
-
|
||||
!python {model: product.product}: |
|
||||
product = self.browse(cr, uid, ref('product_gelato'), context=context)
|
||||
assert product.virtual_available == 10, "Stock is not updated."
|
||||
67
stock_reserve_sale/view/sale.xml
Normal file
67
stock_reserve_sale/view/sale.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="view_order_form_reserve" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.reserve</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale_stock.view_order_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_quotation_send" position="before">
|
||||
<field name="is_stock_reservable" invisible="1"/>
|
||||
<button name="%(action_sale_stock_reserve)d"
|
||||
type="action"
|
||||
string="Reserve Stock"
|
||||
help="Pre-book products from stock"
|
||||
attrs="{'invisible': [('is_stock_reservable', '=', False)]}"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<field name="order_line" position="attributes">
|
||||
<attribute name="options">{"reload_on_button": 1}</attribute>
|
||||
</field>
|
||||
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='state']" position="before">
|
||||
<field name="reservation_ids" invisible="1"/>
|
||||
<button name="%(action_sale_stock_reserve)d"
|
||||
type="action"
|
||||
string="Reserve Stock"
|
||||
attrs="{'invisible': ['|', ('reservation_ids', '!=', []),
|
||||
('state', '!=', 'draft')]}" />
|
||||
<button name="release_stock_reservation"
|
||||
type="object"
|
||||
string="Release Reservation"
|
||||
attrs="{'invisible': ['|', ('reservation_ids', '=', []),
|
||||
('state', '!=', 'draft')]}" />
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='order_line']/tree/field[@name='price_subtotal']" position="after">
|
||||
<field name="reservation_ids" invisible="1"/>
|
||||
<field name="is_stock_reservable" invisible="1"/>
|
||||
<button name="%(action_sale_stock_reserve)d"
|
||||
type="action"
|
||||
string="Reserve Stock"
|
||||
icon="terp-locked"
|
||||
attrs="{'invisible': [('is_stock_reservable', '=', False)]}" />
|
||||
<button name="release_stock_reservation"
|
||||
type="object"
|
||||
string="Release Reservation"
|
||||
icon="gtk-undo"
|
||||
attrs="{'invisible': [('reservation_ids', '=', [])]}" />
|
||||
</xpath>
|
||||
|
||||
<field name="invoiced" position="before">
|
||||
<label for="has_stock_reservation"/>
|
||||
<div>
|
||||
<field name="has_stock_reservation"/>
|
||||
<button name="release_all_stock_reservation"
|
||||
string="cancel all"
|
||||
type="object" class="oe_link"
|
||||
attrs="{'invisible': [('has_stock_reservation', '=', False)]}"/>
|
||||
</div>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
30
stock_reserve_sale/view/stock_reserve.xml
Normal file
30
stock_reserve_sale/view/stock_reserve.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
<record id="view_stock_reservation_form" model="ir.ui.view">
|
||||
<field name="name">stock.reservation.form</field>
|
||||
<field name="model">stock.reservation</field>
|
||||
<field name="inherit_id" ref="stock_reserve.view_stock_reservation_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="location" position="after">
|
||||
<group name="sale" string="Sales">
|
||||
<field name="sale_id"/>
|
||||
<field name="sale_line_id"/>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_stock_reservation_tree" model="ir.ui.view">
|
||||
<field name="name">stock.reservation.tree</field>
|
||||
<field name="model">stock.reservation</field>
|
||||
<field name="inherit_id" ref="stock_reserve.view_stock_reservation_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="move_id" position="before">
|
||||
<field name="sale_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
22
stock_reserve_sale/wizard/__init__.py
Normal file
22
stock_reserve_sale/wizard/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import sale_stock_reserve
|
||||
111
stock_reserve_sale/wizard/sale_stock_reserve.py
Normal file
111
stock_reserve_sale/wizard/sale_stock_reserve.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Guewen Baconnier
|
||||
# Copyright 2013 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
|
||||
|
||||
class sale_stock_reserve(orm.TransientModel):
|
||||
_name = 'sale.stock.reserve'
|
||||
|
||||
_columns = {
|
||||
'location_id': fields.many2one(
|
||||
'stock.location',
|
||||
'Source Location',
|
||||
required=True),
|
||||
'location_dest_id': fields.many2one(
|
||||
'stock.location',
|
||||
'Reservation Location',
|
||||
required=True,
|
||||
help="Location where the system will reserve the "
|
||||
"products."),
|
||||
'date_validity': fields.date(
|
||||
"Validity Date",
|
||||
help="If a date is given, the reservations will be released "
|
||||
"at the end of the validity."),
|
||||
'note': fields.text('Notes'),
|
||||
}
|
||||
|
||||
def _default_location_id(self, cr, uid, context=None):
|
||||
reserv_obj = self.pool.get('stock.reservation')
|
||||
return reserv_obj._default_location_id(cr, uid, context=context)
|
||||
|
||||
def _default_location_dest_id(self, cr, uid, context=None):
|
||||
reserv_obj = self.pool.get('stock.reservation')
|
||||
return reserv_obj._default_location_dest_id(cr, uid, context=context)
|
||||
|
||||
_defaults = {
|
||||
'location_id': _default_location_id,
|
||||
'location_dest_id': _default_location_dest_id,
|
||||
}
|
||||
|
||||
def _prepare_stock_reservation(self, cr, uid, form, line, context=None):
|
||||
product_uos = line.product_uos.id if line.product_uos else False
|
||||
return {'product_id': line.product_id.id,
|
||||
'product_uom': line.product_uom.id,
|
||||
'product_qty': line.product_uom_qty,
|
||||
'date_validity': form.date_validity,
|
||||
'name': "{} ({})".format(line.order_id.name, line.name),
|
||||
'location_id': form.location_id.id,
|
||||
'location_dest_id': form.location_dest_id.id,
|
||||
'note': form.note,
|
||||
'product_uos_qty': line.product_uos_qty,
|
||||
'product_uos': product_uos,
|
||||
'price_unit': line.price_unit,
|
||||
'sale_line_id': line.id,
|
||||
}
|
||||
|
||||
def stock_reserve(self, cr, uid, ids, line_ids, context=None):
|
||||
assert len(ids) == 1, "Expected 1 ID, got %r" % ids
|
||||
reserv_obj = self.pool.get('stock.reservation')
|
||||
line_obj = self.pool.get('sale.order.line')
|
||||
|
||||
form = self.browse(cr, uid, ids[0], context=context)
|
||||
lines = line_obj.browse(cr, uid, line_ids, context=context)
|
||||
for line in lines:
|
||||
if not line.is_stock_reservable:
|
||||
continue
|
||||
vals = self._prepare_stock_reservation(cr, uid, form, line,
|
||||
context=context)
|
||||
reserv_id = reserv_obj.create(cr, uid, vals, context=context)
|
||||
reserv_obj.reserve(cr, uid, [reserv_id], context=context)
|
||||
return True
|
||||
|
||||
def button_reserve(self, cr, uid, ids, context=None):
|
||||
assert len(ids) == 1, "Expected 1 ID, got %r" % ids
|
||||
if context is None:
|
||||
context = {}
|
||||
close = {'type': 'ir.actions.act_window_close'}
|
||||
active_model = context.get('active_model')
|
||||
active_ids = context.get('active_ids')
|
||||
if not (active_model and active_ids):
|
||||
return close
|
||||
|
||||
line_obj = self.pool.get('sale.order.line')
|
||||
if active_model == 'sale.order':
|
||||
sale_obj = self.pool.get('sale.order')
|
||||
sales = sale_obj.browse(cr, uid, active_ids, context=context)
|
||||
line_ids = [line.id for sale in sales for line in sale.order_line]
|
||||
|
||||
if active_model == 'sale.order.line':
|
||||
line_ids = active_ids
|
||||
|
||||
self.stock_reserve(cr, uid, ids, line_ids, context=context)
|
||||
return close
|
||||
44
stock_reserve_sale/wizard/sale_stock_reserve_view.xml
Normal file
44
stock_reserve_sale/wizard/sale_stock_reserve_view.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="view_sale_stock_reserve_form" model="ir.ui.view">
|
||||
<field name="name">sale.stock.reserve.form</field>
|
||||
<field name="model">sale.stock.reserve</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reserve Stock" version="7.0">
|
||||
<p class="oe_grey">
|
||||
A stock reservation will be created for the products
|
||||
of the selected quotation lines. If a validity date is specified,
|
||||
the reservation will be released once the date has passed.
|
||||
</p>
|
||||
<group>
|
||||
<field name="location_id"/>
|
||||
<field name="location_dest_id"/>
|
||||
<field name="date_validity"/>
|
||||
</group>
|
||||
<group name="note" string="Notes">
|
||||
<field name="note" nolabel="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Reserve"
|
||||
name="button_reserve"
|
||||
type="object"
|
||||
class="oe_highlight" />
|
||||
or
|
||||
<button special="cancel" class="oe_link" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_stock_reserve" model="ir.actions.act_window">
|
||||
<field name="name">Reserve Stock for Quotation Lines</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">sale.stock.reserve</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
Reference in New Issue
Block a user