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)