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 @@ +