Merge pull request #14 from yvaucher/8.0-stock_reserve_sale

8.0 port module stock_reserve_sale
This commit is contained in:
Yannick Vaucher
2014-10-03 11:31:45 +02:00
13 changed files with 251 additions and 225 deletions

View File

@@ -1,110 +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
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
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

View File

@@ -57,9 +57,9 @@ insufficient at the order date, you may want to install the
'view/sale.xml',
'view/stock_reserve.xml',
],
'auto_install': False,
'test': ['test/sale_reserve.yml',
'test/sale_line_reserve.yml',
],
'installable': False,
'installable': True,
'auto_install': False,
}

View File

@@ -19,102 +19,134 @@
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields, api
from openerp.exceptions import except_orm
from openerp.tools.translate import _
class sale_order(orm.Model):
class SaleOrder(models.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):
@api.multi
@api.depends('state',
'order_line.reservation_ids',
'order_line.is_stock_reservable')
def _stock_reservation(self):
for sale in self:
has_stock_reservation = False
is_stock_reservable = False
for line in sale.order_line:
if line.reservation_ids:
result[sale.id]['has_stock_reservation'] = True
has_stock_reservation = True
if line.is_stock_reservable:
result[sale.id]['is_stock_reservable'] = True
is_stock_reservable = True
if sale.state not in ('draft', 'sent'):
result[sale.id]['is_stock_reservable'] = False
return result
is_stock_reservable = False
sale.is_stock_reservable = is_stock_reservable
sale.has_stock_reservation = has_stock_reservation
_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'),
}
has_stock_reservation = fields.Boolean(
compute='_stock_reservation',
readonly=True,
multi='stock_reservation',
store=True,
string='Has Stock Reservations')
is_stock_reservable = fields.Boolean(
compute='_stock_reservation',
readonly=True,
multi='stock_reservation',
store=True,
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)
@api.multi
def release_all_stock_reservation(self):
line_ids = [line.id for order in self for line in order.order_line]
lines = self.env['sale.order.line'].browse(line_ids)
lines.release_stock_reservation()
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)
@api.multi
def action_button_confirm(self):
self.release_all_stock_reservation()
return super(SaleOrder, self).action_button_confirm()
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)
@api.multi
def action_cancel(self):
self.release_all_stock_reservation()
return super(SaleOrder, self).action_cancel()
class sale_order_line(orm.Model):
class SaleOrderLine(models.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
@api.multi
def _get_line_rule(self):
""" Get applicable rule for this product
_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'),
}
Reproduce get suitable rule from procurement
to predict source location """
ProcurementRule = self.env['procurement.rule']
product = self.product_id
product_route_ids = [x.id for x in product.route_ids +
product.categ_id.total_route_ids]
rules = ProcurementRule.search([('route_id', 'in', product_route_ids)],
order='route_sequence, sequence',
limit=1)
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)
if not rules:
warehouse = self.order_id.warehouse_id
wh_routes = warehouse.route_ids
wh_route_ids = [route.id for route in wh_routes]
domain = ['|', ('warehouse_id', '=', warehouse.id),
('warehouse_id', '=', False),
('route_id', 'in', wh_route_ids)]
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
rules = ProcurementRule.search(domain,
order='route_sequence, sequence')
if rules:
return rules[0]
return False
@api.multi
def _get_procure_method(self):
""" Get procure_method depending on product routes """
rule = self._get_line_rule()
if rule:
return rule.procure_method
return False
@api.multi
@api.depends('state',
'product_id.route_ids',
'product_id.type')
def _is_stock_reservable(self):
for line in self:
reservable = False
if (not (line.state != 'draft'
or line._get_procure_method() == 'make_to_order'
or not line.product_id
or line.product_id.type == 'service')
and not line.reservation_ids):
reservable = True
line.is_stock_reservable = reservable
reservation_ids = fields.One2many(
'stock.reservation',
'sale_line_id',
string='Stock Reservation',
copy=False)
is_stock_reservable = fields.Boolean(
compute='_is_stock_reservable',
readonly=True,
string='Can be reserved')
@api.multi
def release_stock_reservation(self):
reserv_ids = [reserv.id for line in self
for reserv in line.reservation_ids]
reserv_obj = self.pool.get('stock.reservation')
reserv_obj.release(cr, uid, reserv_ids, context=context)
reservations = self.env['stock.reservation'].browse(reserv_ids)
reservations.release()
return True
def product_id_change(self, cr, uid, ids,
@@ -133,7 +165,7 @@ class sale_order_line(orm.Model):
fiscal_position=False,
flag=False,
context=None):
result = super(sale_order_line, self).product_id_change(
result = super(SaleOrderLine, 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,
@@ -158,7 +190,8 @@ class sale_order_line(orm.Model):
}
return result
def write(self, cr, uid, ids, vals, context=None):
@api.multi
def write(self, vals):
block_on_reserve = ('product_id',
'product_uom',
'product_uos',
@@ -170,33 +203,31 @@ class sale_order_line(orm.Model):
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):
for line in self:
if not line.reservation_ids:
continue
raise orm.except_orm(
raise 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)
res = super(SaleOrderLine, self).write(vals)
if test_update:
for line in self.browse(cr, uid, ids, context=context):
for line in self:
if not line.reservation_ids:
continue
if len(line.reservation_ids) > 1:
raise orm.except_orm(
raise 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(
line.reservation_ids.write(
{'price_unit': line.price_unit,
'product_qty': line.product_uom_qty,
'product_uom_qty': line.product_uom_qty,
'product_uos_qty': line.product_uos_qty,
}
)

View File

@@ -19,32 +19,24 @@
#
##############################################################################
from openerp.osv import orm, fields
from openerp import models, fields, api
class stock_reservation(orm.Model):
class StockReservation(models.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')
}
sale_line_id = fields.Many2one(
'sale.order.line',
string='Sale Order Line',
ondelete='cascade',
copy=False)
sale_id = fields.Many2one(
'sale.order',
string='Sale Order',
related='sale_line_id.order_id')
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)
@api.multi
def release(self):
for rec in self:
rec.sale_line_id = False
return super(StockReservation, self).release()

View File

@@ -10,7 +10,6 @@
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
@@ -60,6 +59,19 @@
!python {model: product.product}: |
product = self.browse(cr, uid, ref('product_yogurt'), context=context)
assert product.virtual_available == 6, "Stock is not updated."
-
I set product_12 to MTO (doesn't work)
-
!record {model: product.product, id: product.product_product_12}:
route_ids:
- stock.route_warehouse0_mto
-
I set MTO for real
-
!python {model: product.product}: |
product = self.browse(cr, uid, ref('product.product_product_12'), context=context)
self.write(cr, uid, ref('product.product_product_12'),
{'route_ids': [(6, False, [ref('stock.route_warehouse0_mto')])]}, context=context)
-
And I create a MTO sales order line
-
@@ -67,9 +79,8 @@
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
product_uom: product.product_uom_unit
-
And I try to create a stock reserve for this MTO line
-

View File

@@ -10,7 +10,6 @@
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

View File

@@ -0,0 +1,103 @@
# -*- 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 SaleStockReserve(models.TransientModel):
_name = 'sale.stock.reserve'
@api.model
def _default_location_id(self):
return self.env['stock.reservation']._default_location_id()
@api.model
def _default_location_dest_id(self):
return self.env['stock.reservation']._default_location_dest_id()
location_id = fields.Many2one(
'stock.location',
'Source Location',
required=True,
default=_default_location_id)
location_dest_id = fields.Many2one(
'stock.location',
'Reservation Location',
required=True,
help="Location where the system will reserve the "
"products.",
default=_default_location_dest_id)
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')
@api.multi
def _prepare_stock_reservation(self, line):
self.ensure_one()
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_uom_qty': line.product_uom_qty,
'date_validity': self.date_validity,
'name': "%s (%s)" % (line.order_id.name, line.name),
'location_id': self.location_id.id,
'location_dest_id': self.location_dest_id.id,
'note': self.note,
'product_uos_qty': line.product_uos_qty,
'product_uos': product_uos,
'price_unit': line.price_unit,
'sale_line_id': line.id,
}
@api.multi
def stock_reserve(self, line_ids):
self.ensure_one()
lines = self.env['sale.order.line'].browse(line_ids)
for line in lines:
if not line.is_stock_reservable:
continue
vals = self._prepare_stock_reservation(line)
reserv = self.env['stock.reservation'].create(vals)
reserv.reserve()
return True
@api.multi
def button_reserve(self):
env = self.env
self.ensure_one()
close = {'type': 'ir.actions.act_window_close'}
active_model = env.context.get('active_model')
active_ids = env.context.get('active_ids')
if not (active_model and active_ids):
return close
if active_model == 'sale.order':
sales = env['sale.order'].browse(active_ids)
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(line_ids)
return close