From 3a314780c020439851e516c9221febc22fbf69c6 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 27 Mar 2015 16:04:09 +0100 Subject: [PATCH] [FIX] sale_*, delivery: more consistent ignore of cancelled SO lines Sales Order lines have a cancelled state, but this state is not always considered when looping over lines. This check is done in some places already and this patch's aim is to do it in the remaining places. - Cancel the procurement of a sale line in sale.order.line instead of sale.order, so a line canceled individually with sale_order_line.button_cancel will properly cancel it procurement. - Sale report: uses the state of lines instead of Sales order, so canceled lines of not-canceled orders are correctly represented in the analysis. - test: do not create invoices lines for canceled sale lines - test: creation of moves with canceled lines - test: check if lines are still canceled when sale order is done Closes #6036 --- addons/delivery/delivery.py | 2 + addons/event_sale/event_sale.py | 2 + addons/sale/__openerp__.py | 1 + addons/sale/report/sale_report.py | 4 +- addons/sale/sale.py | 27 ++++++--- addons/sale/test/canceled_lines_order.yml | 60 +++++++++++++++++++ addons/sale/wizard/sale_line_invoice.py | 2 +- addons/sale_margin/sale_margin.py | 2 + addons/sale_order_dates/sale_order_dates.py | 2 + addons/sale_stock/__openerp__.py | 1 + addons/sale_stock/sale_stock.py | 2 + .../test/sale_order_canceled_line.yml | 45 ++++++++++++++ 12 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 addons/sale/test/canceled_lines_order.yml create mode 100644 addons/sale_stock/test/sale_order_canceled_line.yml diff --git a/addons/delivery/delivery.py b/addons/delivery/delivery.py index 4cc7e7d3d53..d1a08b9ae45 100644 --- a/addons/delivery/delivery.py +++ b/addons/delivery/delivery.py @@ -211,6 +211,8 @@ class delivery_grid(osv.osv): total_delivery = 0.0 product_uom_obj = self.pool.get('product.uom') for line in order.order_line: + if line.state == 'cancel': + continue if line.is_delivery: total_delivery += line.price_subtotal + self.pool['sale.order']._amount_line_tax(cr, uid, line, context=context) if not line.product_id or line.is_delivery: diff --git a/addons/event_sale/event_sale.py b/addons/event_sale/event_sale.py index 270563cb385..8a0d2ba1e44 100644 --- a/addons/event_sale/event_sale.py +++ b/addons/event_sale/event_sale.py @@ -105,6 +105,8 @@ class sale_order_line(osv.osv): context = dict(context or {}) registration_obj = self.pool.get('event.registration') for order_line in self.browse(cr, uid, ids, context=context): + if order_line.state == 'cancel': + continue if order_line.event_id: dic = { 'name': order_line.order_id.partner_invoice_id.name, diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index ebff536fd18..2871e5fa546 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -86,6 +86,7 @@ The Dashboard for the Sales Manager will include 'test/cancel_order.yml', 'test/delete_order.yml', 'test/edi_sale_order.yml', + 'test/canceled_lines_order.yml', ], 'installable': True, 'auto_install': False, diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index e04833ca6f6..9d503ff7b71 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -72,7 +72,7 @@ class sale_report(osv.osv): s.user_id as user_id, s.company_id as company_id, extract(epoch from avg(date_trunc('day',s.date_confirm)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay, - s.state, + l.state, t.categ_id as categ_id, s.pricelist_id as pricelist_id, s.project_id as analytic_account_id, @@ -102,7 +102,7 @@ class sale_report(osv.osv): s.partner_id, s.user_id, s.company_id, - s.state, + l.state, s.pricelist_id, s.project_id, s.section_id diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 5af2cd770ea..4e7598fc187 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -501,6 +501,8 @@ class sale_order(osv.osv): def test_no_product(self, cr, uid, order, context): for line in order.order_line: + if line.state == 'cancel': + continue if line.product_id and (line.product_id.type<>'service'): return False return True @@ -584,7 +586,6 @@ class sale_order(osv.osv): context = {} sale_order_line_obj = self.pool.get('sale.order.line') account_invoice_obj = self.pool.get('account.invoice') - procurement_obj = self.pool.get('procurement.order') for sale in self.browse(cr, uid, ids, context=context): for inv in sale.invoice_ids: if inv.state not in ('draft', 'cancel'): @@ -592,9 +593,8 @@ class sale_order(osv.osv): _('Cannot cancel this sales order!'), _('First cancel all invoices attached to this sales order.')) inv.signal_workflow('invoice_cancel') - procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in sale.order_line],[])) - sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], - {'state': 'cancel'}) + line_ids = [l.id for l in sale.order_line if l.state != 'cancel'] + sale_order_line_obj.button_cancel(cr, uid, line_ids, context=context) self.write(cr, uid, ids, {'state': 'cancel'}) return True @@ -606,14 +606,14 @@ class sale_order(osv.osv): def action_wait(self, cr, uid, ids, context=None): context = context or {} for o in self.browse(cr, uid, ids): - if not o.order_line: + if not any(line.state != 'cancel' for line in o.order_line): raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.')) noprod = self.test_no_product(cr, uid, o, context) if (o.order_policy == 'manual') or noprod: self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) else: self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) - self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line]) + self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line if x.state != 'cancel']) return True def action_quotation_send(self, cr, uid, ids, context=None): @@ -652,7 +652,7 @@ class sale_order(osv.osv): def action_done(self, cr, uid, ids, context=None): for order in self.browse(cr, uid, ids, context=context): - self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context) + self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], {'state': 'done'}, context=context) return self.write(cr, uid, ids, {'state': 'done'}, context=context) def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): @@ -685,7 +685,7 @@ class sale_order(osv.osv): sale_line_obj = self.pool.get('sale.order.line') res = [] for order in self.browse(cr, uid, ids, context=context): - res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context)) + res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], context=context)) return any(res) def action_ignore_delivery_exception(self, cr, uid, ids, context=None): @@ -712,6 +712,8 @@ class sale_order(osv.osv): order.write({'procurement_group_id': group_id}) for line in order.order_line: + if line.state == 'cancel': + continue #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate) if line.procurement_ids: #first check them to see if they are in exception or not (one of the related moves is cancelled) @@ -793,6 +795,8 @@ class sale_order(osv.osv): def test_procurements_done(self, cr, uid, ids, context=None): for sale in self.browse(cr, uid, ids, context=context): for line in sale.order_line: + if line.state == 'cancel': + continue if not all([x.state == 'done' for x in line.procurement_ids]): return False return True @@ -800,6 +804,8 @@ class sale_order(osv.osv): def test_procurements_except(self, cr, uid, ids, context=None): for sale in self.browse(cr, uid, ids, context=context): for line in sale.order_line: + if line.state == 'cancel': + continue if any([x.state == 'cancel' for x in line.procurement_ids]): return True return False @@ -995,9 +1001,12 @@ class sale_order_line(osv.osv): return create_ids def button_cancel(self, cr, uid, ids, context=None): - for line in self.browse(cr, uid, ids, context=context): + lines = self.browse(cr, uid, ids, context=context) + for line in lines: if line.invoiced: raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.')) + procurement_obj = self.pool['procurement.order'] + procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in lines], []), context=context) return self.write(cr, uid, ids, {'state': 'cancel'}) def button_confirm(self, cr, uid, ids, context=None): diff --git a/addons/sale/test/canceled_lines_order.yml b/addons/sale/test/canceled_lines_order.yml new file mode 100644 index 00000000000..912f878e53e --- /dev/null +++ b/addons/sale/test/canceled_lines_order.yml @@ -0,0 +1,60 @@ +- + I create a draft Sale Order with 2 lines but 1 canceled in order to check if the canceled lines are not considered in the logic +- + !record {model: sale.order, id: sale_order_cl_2}: + partner_id: base.res_partner_15 + partner_invoice_id: base.res_partner_address_25 + partner_shipping_id: base.res_partner_address_25 + pricelist_id: product.list0 + order_policy: manual +- + !record {model: sale.order.line, id: sale_order_cl_2_line_1}: + order_id: sale_order_cl_2 + product_id: product.product_product_27 + product_uom_qty: 1 + product_uom: 1 + price_unit: 3645 + name: 'Laptop Customized' +- + !record {model: sale.order.line, id: sale_order_cl_2_line_2}: + order_id: sale_order_cl_2 + product_id: product.product_product_12 + product_uom_qty: 1 + product_uom: 1 + price_unit: 12.50 + name: 'Mouse, Wireless' +- + I cancel the first line +- + !python {model: sale.order.line, id: sale_order_cl_2_line_1}: | + self.button_cancel() +- + I confirm the sale order +- + !workflow {model: sale.order, action: order_confirm, ref: sale_order_cl_2} +- + Invoice the whole sale order +- + !python {model: sale.advance.payment.inv}: | + ctx = context.copy() + ctx.update({"active_model": 'sale.order', + "active_ids": [ref("sale_order_cl_2")], + "active_id":ref("sale_order_cl_2")}) + pay_id = self.create(cr, uid, {'advance_payment_method': 'all'}) + self.create_invoices(cr, uid, [pay_id], context=ctx) +- + I check the invoice +- + !python {model: sale.order, id: sale_order_cl_2}: | + invoice = self.invoice_ids + assert len(invoice.invoice_line) == 1, "Only 1 line should be invoiced because the other one is canceled, got %d" % len(invoice.invoice_line) +- + I set the sale to done +- + !python {model: sale.order, id: sale_order_cl_2}: | + self.action_done() +- + And check if the canceled line is still canceled +- + !assert {model: sale.order.line, id: sale_order_cl_2_line_1, string: The canceled line should still be canceled}: + - state == 'cancel' diff --git a/addons/sale/wizard/sale_line_invoice.py b/addons/sale/wizard/sale_line_invoice.py index ba25b4fc85f..7bbf266193a 100644 --- a/addons/sale/wizard/sale_line_invoice.py +++ b/addons/sale/wizard/sale_line_invoice.py @@ -102,7 +102,7 @@ class sale_order_line_make_invoice(osv.osv_memory): sales_order_obj.message_post(cr, uid, [order.id], body=_("Invoice created"), context=context) data_sale = sales_order_obj.browse(cr, uid, order.id, context=context) for line in data_sale.order_line: - if not line.invoiced: + if not line.invoiced and line.state != 'cancel': flag = False break if flag: diff --git a/addons/sale_margin/sale_margin.py b/addons/sale_margin/sale_margin.py index cdc3d99043d..9744d84934a 100644 --- a/addons/sale_margin/sale_margin.py +++ b/addons/sale_margin/sale_margin.py @@ -71,6 +71,8 @@ class sale_order(osv.osv): for sale in self.browse(cr, uid, ids, context=context): result[sale.id] = 0.0 for line in sale.order_line: + if line.state == 'cancel': + continue result[sale.id] += line.margin or 0.0 return result diff --git a/addons/sale_order_dates/sale_order_dates.py b/addons/sale_order_dates/sale_order_dates.py index cfefa372f8c..c392e1d0ea9 100644 --- a/addons/sale_order_dates/sale_order_dates.py +++ b/addons/sale_order_dates/sale_order_dates.py @@ -61,6 +61,8 @@ class sale_order_dates(osv.osv): dates_list = [] order_datetime = datetime.strptime(order.date_order, DEFAULT_SERVER_DATETIME_FORMAT) for line in order.order_line: + if line.state == 'cancel': + continue dt = order_datetime + timedelta(days=line.delay or 0.0) dt_s = dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) dates_list.append(dt_s) diff --git a/addons/sale_stock/__openerp__.py b/addons/sale_stock/__openerp__.py index 3f6b718ba32..a72a10a9e99 100644 --- a/addons/sale_stock/__openerp__.py +++ b/addons/sale_stock/__openerp__.py @@ -62,6 +62,7 @@ You can choose flexible invoicing methods: 'test/picking_order_policy.yml', 'test/prepaid_order_policy.yml', 'test/sale_order_onchange.yml', + 'test/sale_order_canceled_line.yml', ], 'installable': True, 'auto_install': True, diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 312ebb674bc..fed88af30d8 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -182,6 +182,8 @@ class sale_order(osv.osv): def has_stockable_products(self, cr, uid, ids, *args): for order in self.browse(cr, uid, ids): for order_line in order.order_line: + if order_line.state == 'cancel': + continue if order_line.product_id and order_line.product_id.type in ('product', 'consu'): return True return False diff --git a/addons/sale_stock/test/sale_order_canceled_line.yml b/addons/sale_stock/test/sale_order_canceled_line.yml new file mode 100644 index 00000000000..4e229fc4d66 --- /dev/null +++ b/addons/sale_stock/test/sale_order_canceled_line.yml @@ -0,0 +1,45 @@ +- + I create a draft Sale Order with 2 lines but 1 canceled in order to check if the canceled lines are not considered in the logic +- + !record {model: sale.order, id: sale_order_cl_3}: + partner_id: base.res_partner_15 + partner_invoice_id: base.res_partner_address_25 + partner_shipping_id: base.res_partner_address_25 + pricelist_id: product.list0 + order_policy: manual +- + !record {model: sale.order.line, id: sale_order_cl_3_line_1}: + order_id: sale_order_cl_3 + product_id: product.product_product_27 + product_uom_qty: 1 + product_uom: 1 + price_unit: 3645 + name: 'Laptop Customized' +- + !record {model: sale.order.line, id: sale_order_cl_3_line_2}: + order_id: sale_order_cl_3 + product_id: product.product_product_12 + product_uom_qty: 1 + product_uom: 1 + price_unit: 12.50 + name: 'Mouse, Wireless' +- + I cancel the first line +- + !python {model: sale.order.line, id: sale_order_cl_3_line_1}: | + self.button_cancel() +- + I confirm the sale order +- + !workflow {model: sale.order, action: order_confirm, ref: sale_order_cl_3} +- + I check that no procurement has been generated for the canceled line +- + !assert {model: sale.order.line, id: sale_order_cl_3_line_1, string: The canceled line should not have a procurement}: + - not procurement_ids +- + I check that we have only 1 stock move, for the not canceled line +- + !python {model: sale.order, id: sale_order_cl_3}: | + moves = self.picking_ids.mapped('move_lines') + assert len(moves) == 1, "We should have 1 move, got %s" % len(moves)