From 69539e567b9dce558580efc56584a1c5f561efaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=BE=20=D0=9A=D0=B0=D1=82?=
=?UTF-8?q?=D1=8E=D1=85=D0=B0?=
Date: Tue, 13 Sep 2016 16:31:46 +0300
Subject: [PATCH] stock_reserve_sale: port 9.0
---
stock_reserve_sale/__openerp__.py | 7 +-
stock_reserve_sale/model/sale.py | 193 ++++++++++--------
stock_reserve_sale/model/stock_reserve.py | 3 +-
.../security/ir.model.access.csv | 3 +
stock_reserve_sale/test/sale_line_reserve.yml | 16 +-
stock_reserve_sale/test/sale_reserve.yml | 82 +++++++-
stock_reserve_sale/view/sale.xml | 12 +-
.../wizard/sale_stock_reserve.py | 110 ++--------
.../wizard/sale_stock_reserve_view.xml | 3 -
9 files changed, 226 insertions(+), 203 deletions(-)
create mode 100644 stock_reserve_sale/security/ir.model.access.csv
diff --git a/stock_reserve_sale/__openerp__.py b/stock_reserve_sale/__openerp__.py
index c919e6ac9..3d60d2b8a 100644
--- a/stock_reserve_sale/__openerp__.py
+++ b/stock_reserve_sale/__openerp__.py
@@ -20,7 +20,7 @@
##############################################################################
{'name': 'Stock Reserve Sales',
- 'version': '8.0.1.0.0',
+ 'version': '9.0.1.0.0',
'author': "Camptocamp,Odoo Community Association (OCA)",
'category': 'Warehouse',
'license': 'AGPL-3',
@@ -31,13 +31,14 @@
'stock_reserve',
],
'demo': [],
- 'data': ['wizard/sale_stock_reserve_view.xml',
+ 'data': ['security/ir.model.access.csv',
+ 'wizard/sale_stock_reserve_view.xml',
'view/sale.xml',
'view/stock_reserve.xml',
],
'test': ['test/sale_reserve.yml',
'test/sale_line_reserve.yml',
],
- 'installable': False,
+ 'installable': True,
'auto_install': False,
}
diff --git a/stock_reserve_sale/model/sale.py b/stock_reserve_sale/model/sale.py
index 5723f473f..ddfa0dbdd 100644
--- a/stock_reserve_sale/model/sale.py
+++ b/stock_reserve_sale/model/sale.py
@@ -20,8 +20,10 @@
##############################################################################
from openerp import models, fields, api
-from openerp.exceptions import except_orm
+from openerp.exceptions import UserError
from openerp.tools.translate import _
+from openerp.tools.float_utils import float_compare
+import openerp.addons.decimal_precision as dp
class SaleOrder(models.Model):
@@ -31,7 +33,7 @@ class SaleOrder(models.Model):
@api.depends('state',
'order_line.reservation_ids',
'order_line.is_stock_reservable')
- def _stock_reservation(self):
+ def _compute_stock_reservation(self):
for sale in self:
has_stock_reservation = False
is_stock_reservable = False
@@ -46,29 +48,26 @@ class SaleOrder(models.Model):
sale.has_stock_reservation = has_stock_reservation
has_stock_reservation = fields.Boolean(
- compute='_stock_reservation',
+ compute='_compute_stock_reservation',
readonly=True,
- multi='stock_reservation',
store=True,
string='Has Stock Reservations')
is_stock_reservable = fields.Boolean(
- compute='_stock_reservation',
+ compute='_compute_stock_reservation',
readonly=True,
- multi='stock_reservation',
store=True,
string='Can Have Stock Reservations')
@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 = self.mapped('order_line')
lines.release_stock_reservation()
return True
@api.multi
- def action_button_confirm(self):
+ def action_confirm(self):
self.release_all_stock_reservation()
- return super(SaleOrder, self).action_button_confirm()
+ return super(SaleOrder, self).action_confirm()
@api.multi
def action_cancel(self):
@@ -87,22 +86,22 @@ class SaleOrderLine(models.Model):
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]
+ product_route_ids = (product.route_ids.mapped('id') +
+ product.categ_id.total_route_ids.mapped('id'))
rules = ProcurementRule.search([('route_id', 'in', product_route_ids)],
order='route_sequence, sequence',
limit=1)
if not rules:
warehouse = self.order_id.warehouse_id
- wh_routes = warehouse.route_ids
- wh_route_ids = [route.id for route in wh_routes]
+ wh_route_ids = warehouse.route_ids.mapped('id')
domain = ['|', ('warehouse_id', '=', warehouse.id),
('warehouse_id', '=', False),
('route_id', 'in', wh_route_ids)]
rules = ProcurementRule.search(domain,
- order='route_sequence, sequence')
+ order='route_sequence, sequence',
+ limit=1)
if rules:
return rules[0]
@@ -120,7 +119,7 @@ class SaleOrderLine(models.Model):
@api.depends('state',
'product_id.route_ids',
'product_id.type')
- def _is_stock_reservable(self):
+ def _compute_is_stock_reservation(self):
for line in self:
reservable = False
if (not (line.state != 'draft' or
@@ -137,98 +136,116 @@ class SaleOrderLine(models.Model):
string='Stock Reservation',
copy=False)
is_stock_reservable = fields.Boolean(
- compute='_is_stock_reservable',
+ compute='_compute_is_stock_reservation',
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]
- reservations = self.env['stock.reservation'].browse(reserv_ids)
- reservations.release()
+ def _prepare_stock_reservation(self, date_validity=False, note=False):
+ self.ensure_one()
+
+ try:
+ owner_id = self.stock_owner_id and self.stock_owner_id.id or False
+ except AttributeError:
+ owner_id = False
+ # module sale_owner_stock_sourcing not installed, fine
+
+ return {'product_id': self.product_id.id,
+ 'product_uom': self.product_uom.id,
+ 'product_uom_qty': self.product_uom_qty,
+ 'date_validity': date_validity,
+ 'name': "%s (%s)" % (self.order_id.name, self.name),
+ 'note': note,
+ 'price_unit': self.price_unit,
+ 'sale_line_id': self.id,
+ 'restrict_partner_id': owner_id,
+ }
+
+ @api.multi
+ def acquire_stock_reservation(self, date_validity=False, note=False):
+ reserv_obj = self.env['stock.reservation'].sudo()
+
+ reservations = reserv_obj.browse()
+ for line in self:
+ if not line.is_stock_reservable:
+ continue
+
+ vals = line._prepare_stock_reservation(
+ date_validity=date_validity, note=note)
+
+ # Place picking_type_id in context. This is required
+ # to make reserve automaticaly find location_id and
+ # location_dest_id
+ pick_type_id = line.order_id.warehouse_id.int_type_id.id
+ reserv_obj_ctx = reserv_obj.with_context(
+ default_picking_type_id=pick_type_id)
+
+ reservations |= reserv_obj_ctx.create(vals)
+ reservations.reserve()
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(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,
- 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:
+ @api.multi
+ def release_stock_reservation(self):
+ self.mapped('reservation_ids').sudo().release()
+ return True
+
+ @api.onchange('product_id', 'product_uom_qty')
+ def onchange_product_id_qty(self):
+ reserved_qty = sum(self.reservation_ids.mapped('product_uom_qty'))
+
+ precision_digits = dp.get_precision(
+ 'Product Unit of Measure')(self.env.cr)[1]
+ qty_equal = float_compare(
+ self.product_uom_qty, reserved_qty,
+ precision_digits=precision_digits) == 0.0
+ if not qty_equal and self.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'] = {
+ "be automatically adjusted to %.2f."
+ "") % self.product_uom_qty
+ return {
+ 'warning': {
'title': _('Configuration Error!'),
'message': msg,
- }
- return result
+ },
+ }
+ return {}
+
+ @api.multi
+ def _update_reservation_price_qty(self):
+ for line in self:
+ if not line.reservation_ids:
+ continue
+ if len(line.reservation_ids) > 1:
+ raise UserError(
+ _('Several stock reservations are linked with the '
+ 'line. Impossible to adjust their quantity. '
+ 'Please release the reservation '
+ 'before changing the quantity.'))
+
+ line.reservation_ids.sudo().write({
+ 'price_unit': line.price_unit,
+ 'product_uom_qty': line.product_uom_qty,
+ })
@api.multi
def write(self, vals):
block_on_reserve = ('product_id',
- 'product_uom',
- 'product_uos',
+ 'product_uom_id',
'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:
- if not line.reservation_ids:
- continue
- 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.'))
+ if test_block and len(self.mapped('reservation_ids')) > 0:
+ raise UserError(
+ _('You cannot change the product or unit of measure '
+ 'of lines with a stock reservation. '
+ 'Release the reservation '
+ 'before changing the product.'))
res = super(SaleOrderLine, self).write(vals)
if test_update:
- for line in self:
- if not line.reservation_ids:
- continue
- if len(line.reservation_ids) > 1:
- 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.write(
- {'price_unit': line.price_unit,
- 'product_uom_qty': line.product_uom_qty,
- 'product_uos_qty': line.product_uos_qty,
- }
- )
+ self._update_reservation_price_qty()
return res
diff --git a/stock_reserve_sale/model/stock_reserve.py b/stock_reserve_sale/model/stock_reserve.py
index db68449bb..af8620908 100644
--- a/stock_reserve_sale/model/stock_reserve.py
+++ b/stock_reserve_sale/model/stock_reserve.py
@@ -37,6 +37,5 @@ class StockReservation(models.Model):
@api.multi
def release(self):
- for rec in self:
- rec.sale_line_id = False
+ self.write({'sale_line_id': False})
return super(StockReservation, self).release()
diff --git a/stock_reserve_sale/security/ir.model.access.csv b/stock_reserve_sale/security/ir.model.access.csv
new file mode 100644
index 000000000..84479265d
--- /dev/null
+++ b/stock_reserve_sale/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_stock_reservation_sale_user,stock.reservation sale_user,stock_reserve.model_stock_reservation,base.group_sale_salesman,1,0,0,0
+
diff --git a/stock_reserve_sale/test/sale_line_reserve.yml b/stock_reserve_sale/test/sale_line_reserve.yml
index 0a798c3c2..4c89d22be 100644
--- a/stock_reserve_sale/test/sale_line_reserve.yml
+++ b/stock_reserve_sale/test/sale_line_reserve.yml
@@ -12,8 +12,6 @@
uom_po_id: product.product_uom_kgm
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
-
@@ -27,18 +25,18 @@
-
In order to test reservation of the sales order, I create a sales order
-
- !record {model: sale.order, id: sale_reserve_02}:
+ !record {model: sale.order, id: sale_reserve_04}:
partner_id: base.res_partner_2
- payment_term: account.account_payment_term
+ payment_term_id: 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
+ name: "Yogurt"
product_id: product_yogurt
product_uom_qty: 4
product_uom: product.product_uom_kgm
- order_id: sale_reserve_02
+ order_id: sale_reserve_04
-
And I create a stock reserve for this line
-
@@ -54,7 +52,7 @@
-
!python {model: product.product}: |
product = self.browse(cr, uid, ref('product_yogurt'), context=context)
- assert product.virtual_available == 6, "Stock is not updated."
+ assert product.virtual_available == 6, "Stock is not updated. (%s)" % product.virtual_available
-
I set product_12 to MTO (doesn't work)
-
@@ -72,8 +70,8 @@
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
+ order_id: sale_reserve_04
+ name: "Mouse, Wireless"
product_id: product.product_product_12
product_uom_qty: 4
product_uom: product.product_uom_unit
diff --git a/stock_reserve_sale/test/sale_reserve.yml b/stock_reserve_sale/test/sale_reserve.yml
index 02acab415..d2e2e0ab6 100644
--- a/stock_reserve_sale/test/sale_reserve.yml
+++ b/stock_reserve_sale/test/sale_reserve.yml
@@ -21,8 +21,6 @@
uom_po_id: product.product_uom_kgm
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
-
@@ -38,11 +36,13 @@
-
!record {model: sale.order, id: sale_reserve_01}:
partner_id: base.res_partner_2
- payment_term: account.account_payment_term
+ payment_term_id: account.account_payment_term
order_line:
- product_id: product_gelato
+ name: Gelato
product_uom_qty: 4
- product_id: product_gelato
+ name: Gelato
product_uom_qty: 1
-
I call the wizard to reserve the products of the sales order
@@ -71,3 +71,79 @@
!python {model: product.product, id: product_gelato}: |
from nose.tools import *
assert_almost_equal(self.virtual_available, 10.0)
+
+
+-
+ In order to test reservation of the sales order, I create a sales order
+-
+ !record {model: sale.order, id: sale_reserve_02_test_confirm}:
+ partner_id: base.res_partner_2
+ payment_term_id: account.account_payment_term
+ order_line:
+ - product_id: product_gelato
+ name: Gelato
+ product_uom_qty: 4
+-
+ I call the wizard to reserve the products of the sales order
+-
+ !python {model: sale.stock.reserve}: |
+ active_id = ref('sale_reserve_02_test_confirm')
+ context['active_id'] = active_id
+ context['active_ids'] = [active_id]
+ context['active_model'] = 'sale.order'
+ wizard_id = self.create(cr, uid, {}, context=context)
+ self.button_reserve(cr, uid, [wizard_id], context=context)
+
+-
+ I confirm sale order and test that reservations are released
+-
+ !python {model: sale.order}: |
+ active_id = ref('sale_reserve_02_test_confirm')
+ order = self.browse(cr, uid, active_id, context=context)
+ reservations = order.mapped('order_line.reservation_ids')
+ assert reservations.mapped('state') == ['assigned'], "wrong reservations state"
+ order.action_confirm()
+ assert reservations.mapped('state') == ['cancel'], "wrong reservations state"
+ assert len(reservations.mapped('sale_line_id')) == 0, "bindings to sale lines are cleaned"
+
+-
+ I cancel sale order (2)
+-
+ !python {model: sale.order}: |
+ active_id = ref('sale_reserve_02_test_confirm')
+ self.action_cancel(cr, uid, active_id, context=context)
+
+
+-
+ In order to test reservation of the sales order, I create a sales order
+-
+ !record {model: sale.order, id: sale_reserve_03_cancel}:
+ partner_id: base.res_partner_2
+ payment_term_id: account.account_payment_term
+ order_line:
+ - product_id: product_gelato
+ name: Gelato
+ product_uom_qty: 4
+-
+ I call the wizard to reserve the products of the sales order
+-
+ !python {model: sale.stock.reserve}: |
+ active_id = ref('sale_reserve_03_cancel')
+ context['active_id'] = active_id
+ context['active_ids'] = [active_id]
+ context['active_model'] = 'sale.order'
+ wizard_id = self.create(cr, uid, {}, context=context)
+ self.button_reserve(cr, uid, [wizard_id], context=context)
+
+-
+ I cancel sale order and tet that reservations are released
+-
+ !python {model: sale.order}: |
+ active_id = ref('sale_reserve_03_cancel')
+ order = self.browse(cr, uid, active_id, context=context)
+ reservations = order.mapped('order_line.reservation_ids')
+ assert reservations.mapped('state') == ['assigned'], "wrong reservations state"
+ order.action_cancel()
+ assert reservations.mapped('state') == ['cancel'], "wrong reservations state"
+ assert len(reservations.mapped('sale_line_id')) == 0, "bindings to sale lines are cleaned"
+
diff --git a/stock_reserve_sale/view/sale.xml b/stock_reserve_sale/view/sale.xml
index e5849b04c..a615b003c 100644
--- a/stock_reserve_sale/view/sale.xml
+++ b/stock_reserve_sale/view/sale.xml
@@ -5,16 +5,21 @@
sale.order.form.reserve
sale.order
-
+
@@ -23,6 +28,7 @@
+
-
+
@@ -59,7 +65,7 @@
type="object" class="oe_link"
attrs="{'invisible': [('has_stock_reservation', '=', False)]}"/>
-
+
diff --git a/stock_reserve_sale/wizard/sale_stock_reserve.py b/stock_reserve_sale/wizard/sale_stock_reserve.py
index 67d9fb313..083866a17 100644
--- a/stock_reserve_sale/wizard/sale_stock_reserve.py
+++ b/stock_reserve_sale/wizard/sale_stock_reserve.py
@@ -19,114 +19,40 @@
#
##############################################################################
-from openerp import models, fields, api, exceptions
+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()
-
- def _default_owner(self):
- """If sale_owner_stock_sourcing is installed, it adds an owner field
- on sale order lines. Use it.
-
- """
- model = self.env[self.env.context['active_model']]
- if model._name == 'sale.order':
- lines = model.browse(self.env.context['active_id']).order_line
- else:
- lines = model.browse(self.env.context['active_ids'])
-
- try:
- owners = set([l.stock_owner_id for l in lines])
- except AttributeError:
- return self.env['res.partner']
- # module sale_owner_stock_sourcing not installed, fine
-
- if len(owners) == 1:
- return owners.pop()
- elif len(owners) > 1:
- raise exceptions.Warning(
- 'The lines have different owners. Please reserve them '
- 'individually with the reserve button on each one.')
-
- return self.env['res.partner']
-
- 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')
- owner_id = fields.Many2one('res.partner', 'Stock Owner',
- default=_default_owner)
- @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,
- 'restrict_partner_id': self.owner_id.id,
- }
+ @api.model
+ def _get_so_lines(self):
+ active_model = self.env.context.get('active_model')
+ active_ids = self.env.context.get('active_ids')
+ active_id = self.env.context.get('active_id')
- @api.multi
- def stock_reserve(self, line_ids):
- self.ensure_one()
+ if active_model == 'sale.order' and active_id:
+ lines = self.env[active_model].browse(active_id).order_line
+ elif active_model == 'sale.order.line' and active_ids:
+ lines = self.env[active_model].browse(active_ids)
+ else:
+ lines = self.env['sale.order.line'].browse()
- 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
+ return lines
@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]
+ lines = self._get_so_lines()
+ if lines:
+ lines.acquire_stock_reservation(date_validity=self.date_validity,
+ note=self.note)
- if active_model == 'sale.order.line':
- line_ids = active_ids
-
- self.stock_reserve(line_ids)
- return close
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/stock_reserve_sale/wizard/sale_stock_reserve_view.xml b/stock_reserve_sale/wizard/sale_stock_reserve_view.xml
index 69b3fb4ff..656132257 100644
--- a/stock_reserve_sale/wizard/sale_stock_reserve_view.xml
+++ b/stock_reserve_sale/wizard/sale_stock_reserve_view.xml
@@ -13,10 +13,7 @@
the reservation will be released once the date has passed.
-
-
-