mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
Merge pull request #8 from yvaucher/8.0-stock_reserve
8.0 port module stock_reserve
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
# -*- 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
|
||||
|
||||
|
||||
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
|
||||
@@ -1,181 +0,0 @@
|
||||
# -*- 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'):
|
||||
# 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
|
||||
@@ -1,63 +0,0 @@
|
||||
-
|
||||
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."
|
||||
@@ -1,19 +0,0 @@
|
||||
<?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>
|
||||
@@ -19,7 +19,8 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{'name': 'Stock Reserve',
|
||||
{'name': 'Stock Reservation',
|
||||
'summary': 'Stock reservations on products',
|
||||
'version': '0.1',
|
||||
'author': 'Camptocamp',
|
||||
'category': 'Warehouse',
|
||||
@@ -28,8 +29,8 @@
|
||||
'images': [],
|
||||
'website': "http://www.camptocamp.com",
|
||||
'description': """
|
||||
Stock Reserve
|
||||
=============
|
||||
Stock Reservation
|
||||
=================
|
||||
|
||||
Allows to create stock reservations on products.
|
||||
|
||||
@@ -42,6 +43,12 @@ 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.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
|
||||
* Yannick Vaucher <yannick.vaucher@camptocamp.com>
|
||||
|
||||
""",
|
||||
'depends': ['stock',
|
||||
],
|
||||
@@ -54,5 +61,5 @@ exceeded if the reservations are canceled.
|
||||
'auto_install': False,
|
||||
'test': ['test/stock_reserve.yml',
|
||||
],
|
||||
'installable': False,
|
||||
'installable': True,
|
||||
}
|
||||
84
stock_reserve/model/product.py
Normal file
84
stock_reserve/model/product.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- 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 import models, fields, api
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
reservation_count = fields.Integer(
|
||||
compute='_reservation_count',
|
||||
string='# Sales')
|
||||
|
||||
@api.multi
|
||||
def _reservation_count(self):
|
||||
StockReservation = self.env['stock.reservation']
|
||||
product_ids = self._get_products()
|
||||
domain = [('product_id', 'in', product_ids),
|
||||
('state', 'in', ['draft', 'assigned'])]
|
||||
reservations = StockReservation.search(domain)
|
||||
self.reservation_count = sum(reserv.product_uom_qty
|
||||
for reserv in reservations)
|
||||
|
||||
@api.multi
|
||||
def action_view_reservations(self):
|
||||
assert len(self._ids) == 1, "Expected 1 ID, got %r" % self._ids
|
||||
ref = 'stock_reserve.action_stock_reservation_tree'
|
||||
product_ids = self._get_products()
|
||||
action_dict = self._get_act_window_dict(ref)
|
||||
action_dict['domain'] = [('product_id', 'in', product_ids)]
|
||||
action_dict['context'] = {
|
||||
'search_default_draft': 1,
|
||||
'search_default_reserved': 1
|
||||
}
|
||||
return action_dict
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
reservation_count = fields.Integer(
|
||||
compute='_reservation_count',
|
||||
string='# Sales')
|
||||
|
||||
@api.multi
|
||||
def _reservation_count(self):
|
||||
StockReservation = self.env['stock.reservation']
|
||||
product_id = self._ids[0]
|
||||
domain = [('product_id', '=', product_id),
|
||||
('state', 'in', ['draft', 'assigned'])]
|
||||
reservations = StockReservation.search(domain)
|
||||
self.reservation_count = sum(reserv.product_uom_qty
|
||||
for reserv in reservations)
|
||||
|
||||
@api.multi
|
||||
def action_view_reservations(self):
|
||||
assert len(self._ids) == 1, "Expected 1 ID, got %r" % self._ids
|
||||
ref = 'stock_reserve.action_stock_reservation_tree'
|
||||
product_id = self._ids[0]
|
||||
action_dict = self.product_tmpl_id._get_act_window_dict(ref)
|
||||
action_dict['domain'] = [('product_id', '=', product_id)]
|
||||
action_dict['context'] = {
|
||||
'search_default_draft': 1,
|
||||
'search_default_reserved': 1
|
||||
}
|
||||
return action_dict
|
||||
198
stock_reserve/model/stock_reserve.py
Normal file
198
stock_reserve/model/stock_reserve.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# -*- 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 import models, fields, api
|
||||
from openerp.exceptions import except_orm
|
||||
from openerp.tools.translate import _
|
||||
|
||||
|
||||
class StockReservation(models.Model):
|
||||
""" Allow to reserve products.
|
||||
|
||||
The fields mandatory for the creation of a reservation are:
|
||||
|
||||
* product_id
|
||||
* product_uom_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'}
|
||||
|
||||
move_id = fields.Many2one(
|
||||
'stock.move',
|
||||
'Reservation Move',
|
||||
required=True,
|
||||
readonly=True,
|
||||
ondelete='cascade',
|
||||
select=1)
|
||||
date_validity = fields.Date('Validity Date')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""
|
||||
Ensure default value of computed field `product_qty` is not set
|
||||
as it would raise an error
|
||||
"""
|
||||
res = super(StockReservation, self).default_get(fields_list)
|
||||
if 'product_qty' in res:
|
||||
del res['product_qty']
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_location_from_ref(self, ref):
|
||||
""" Get a location from a xmlid if allowed
|
||||
:param ref: tuple (module, xmlid)
|
||||
"""
|
||||
data_obj = self.env['ir.model.data']
|
||||
try:
|
||||
location = data_obj.xmlid_to_object(ref, raise_if_not_found=True)
|
||||
location.check_access_rule('read')
|
||||
location_id = location.id
|
||||
except (except_orm, ValueError):
|
||||
location_id = False
|
||||
return location_id
|
||||
|
||||
@api.model
|
||||
def _default_picking_type_id(self):
|
||||
""" Search for an internal picking type
|
||||
"""
|
||||
type_obj = self.env['stock.picking.type']
|
||||
|
||||
types = type_obj.search([('code', '=', 'internal')], limit=1)
|
||||
if types:
|
||||
return types[0].id
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _default_location_id(self):
|
||||
move_obj = self.env['stock.move']
|
||||
picking_type_id = self._default_picking_type_id()
|
||||
return (move_obj
|
||||
.with_context(default_picking_type_id=picking_type_id)
|
||||
._default_location_source())
|
||||
|
||||
@api.model
|
||||
def _default_location_dest_id(self):
|
||||
ref = 'stock_reserve.stock_location_reservation'
|
||||
return self.get_location_from_ref(ref)
|
||||
|
||||
_defaults = {
|
||||
'picking_type_id': _default_picking_type_id,
|
||||
'location_id': _default_location_id,
|
||||
'location_dest_id': _default_location_dest_id,
|
||||
'product_uom_qty': 1.0,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def reserve(self):
|
||||
""" 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_recs = self.move_id
|
||||
move_recs.date_expected = fields.Datetime.now()
|
||||
move_recs.action_confirm()
|
||||
move_recs.force_assign()
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def release(self):
|
||||
"""
|
||||
Release moves from reservation
|
||||
"""
|
||||
move_recs = self.move_id
|
||||
move_recs.action_cancel()
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def release_validity_exceeded(self, ids=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(domain)
|
||||
reserv_ids.release()
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
""" Release the reservation before the unlink """
|
||||
self.release()
|
||||
return super(StockReservation, self).unlink()
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
""" set product_uom and name from product onchange """
|
||||
# save value before reading of self.move_id as this last one erase
|
||||
# product_id value
|
||||
product = self.product_id
|
||||
# WARNING this gettattr erase self.product_id
|
||||
move = self.move_id
|
||||
result = move.onchange_product_id(
|
||||
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
|
||||
self.name = vals.get('name')
|
||||
self.product_uom = vals.get('product_uom')
|
||||
# repeat assignation of product_id so we don't loose it
|
||||
self.product_id = product.id
|
||||
|
||||
@api.onchange('product_uom_qty')
|
||||
def _onchange_quantity(self):
|
||||
""" On change of product quantity avoid negative quantities """
|
||||
if not self.product_id or self.product_uom_qty <= 0.0:
|
||||
self.product_uom_qty = 0.0
|
||||
|
||||
@api.multi
|
||||
def open_move(self):
|
||||
assert len(self.ids) == 1, "1 ID expected, got %r" % self.ids
|
||||
reserv = self.move_id
|
||||
IrModelData = self.env['ir.model.data']
|
||||
ref_form2 = 'stock.action_move_form2'
|
||||
action = IrModelData.xmlid_to_object(ref_form2)
|
||||
action_dict = action.read()[0]
|
||||
action_dict['name'] = _('Reservation Move')
|
||||
# open directly in the form view
|
||||
ref_form = 'stock.view_move_form'
|
||||
view_id = IrModelData.xmlid_to_res_id(ref_form)
|
||||
action_dict.update(
|
||||
views=[(view_id, 'form')],
|
||||
res_id=reserv.id,
|
||||
)
|
||||
return action_dict
|
||||
114
stock_reserve/test/stock_reserve.yml
Normal file
114
stock_reserve/test/stock_reserve.yml
Normal file
@@ -0,0 +1,114 @@
|
||||
-
|
||||
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
|
||||
-
|
||||
I create a stock orderpoint for the product
|
||||
-
|
||||
!record {model: stock.warehouse.orderpoint, id: sorbet_orderpoint}:
|
||||
warehouse_id: stock.warehouse0
|
||||
location_id: stock.stock_location_stock
|
||||
product_id: product_sorbet
|
||||
product_uom: product.product_uom_kgm
|
||||
product_min_qty: 4.0
|
||||
product_max_qty: 15.0
|
||||
-
|
||||
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 6 kgm
|
||||
-
|
||||
!record {model: stock.reservation, id: reserv_sorbet1}:
|
||||
product_id: product_sorbet
|
||||
product_uom_qty: 6.0
|
||||
product_uom: product.product_uom_kgm
|
||||
name: reserve 6 kg of sorbet for test
|
||||
-
|
||||
I confirm the reservation
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
self.reserve(cr, uid, [ref('reserv_sorbet1')], context=context)
|
||||
-
|
||||
I create a stock reservation for 500g
|
||||
-
|
||||
!record {model: stock.reservation, id: reserv_sorbet2}:
|
||||
product_id: product_sorbet
|
||||
product_uom_qty: 500
|
||||
product_uom: product.product_uom_gram
|
||||
name: reserve 500g of sorbet for test
|
||||
-
|
||||
I confirm the reservation
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
self.reserve(cr, uid, [ref('reserv_sorbet2')], 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 == 3.5, "Stock is not updated."
|
||||
-
|
||||
I run the scheduler
|
||||
-
|
||||
!python {model: procurement.order}: |
|
||||
self.run_scheduler(cr, uid)
|
||||
-
|
||||
The procurement linked to the orderpoint must be in exception (no routes configured)
|
||||
-
|
||||
!python {model: stock.warehouse.orderpoint}: |
|
||||
orderpoint = self.browse(cr, uid, ref('sorbet_orderpoint'))
|
||||
assert orderpoint.procurement_ids[0].state == 'exception', 'procurement must be in exception as there is no rule for procurement'
|
||||
import math
|
||||
assert orderpoint.procurement_ids[0].product_qty == math.ceil(15 - 3.5), 'wrong product qty ordered'
|
||||
-
|
||||
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 == 9.5, "Stock is not updated."
|
||||
-
|
||||
I set the validity of the second reservation to yesterday
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
import datetime
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
yesterday = datetime.date.today() - datetime.timedelta(days=1)
|
||||
yesterday = yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
self.write(cr, uid, [ref('reserv_sorbet2')], {'date_validity': yesterday})
|
||||
-
|
||||
I call the function releasing expired reservations
|
||||
-
|
||||
!python {model: stock.reservation}: |
|
||||
self.release_validity_exceeded(cr, uid, [])
|
||||
-
|
||||
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.0, "Stock is not updated."
|
||||
33
stock_reserve/view/product.xml
Normal file
33
stock_reserve/view/product.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<record model="ir.ui.view" id="product_template_form_view_reservation_button">
|
||||
<field name="name">product.template.reservation.button</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_only_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='buttons']" position="inside">
|
||||
<button class="oe_inline oe_stat_button" name="action_view_reservations"
|
||||
type="object" groups="base.group_sale_salesman" icon="fa-lock">
|
||||
<field string="Stock Reservations" name="reservation_count" widget="statinfo" />
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_product_form_view_reservation_button">
|
||||
<field name="name">product.template.reservation.button</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_normal_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='buttons']" position="inside">
|
||||
<button class="oe_inline oe_stat_button" name="action_view_reservations"
|
||||
type="object" groups="base.group_sale_salesman" icon="fa-lock">
|
||||
<field string="Stock Reservations" name="reservation_count" widget="statinfo" />
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -23,14 +23,10 @@
|
||||
<sheet>
|
||||
<group>
|
||||
<group name="main_grp" string="Details">
|
||||
<field name="product_id"
|
||||
on_change="onchange_product_id(product_id)"
|
||||
/>
|
||||
<label for="product_qty" />
|
||||
<field name="product_id" />
|
||||
<label for="product_uom_qty" />
|
||||
<div>
|
||||
<field name="product_qty"
|
||||
on_change="onchange_quantity(product_id, product_qty)"
|
||||
class="oe_inline"/>
|
||||
<field name="product_uom_qty" class="oe_inline"/>
|
||||
<field name="product_uom"
|
||||
groups="product.group_uom" class="oe_inline"/>
|
||||
</div>
|
||||
@@ -64,7 +60,7 @@
|
||||
<field name="name" />
|
||||
<field name="product_id" />
|
||||
<field name="move_id" />
|
||||
<field name="product_qty" sum="Total" />
|
||||
<field name="product_uom_qty" sum="Total" />
|
||||
<field name="product_uom" />
|
||||
<field name="date_validity" />
|
||||
<field name="state"/>
|
||||
@@ -112,11 +108,10 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_stock_reservation" model="ir.actions.act_window">
|
||||
<record id="action_stock_reservation_tree" 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,
|
||||
@@ -132,7 +127,7 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_stock_reservation"
|
||||
<menuitem action="action_stock_reservation_tree"
|
||||
id="menu_action_stock_reservation"
|
||||
parent="stock.menu_stock_inventory_control"
|
||||
sequence="30"/>
|
||||
Reference in New Issue
Block a user