[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
This commit is contained in:
Guewen Baconnier 2015-03-27 16:04:09 +01:00 committed by Olivier Dony
parent a7cb326e5a
commit 3a314780c0
12 changed files with 138 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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