1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
25 from openerp.osv import fields, osv
26 from openerp.tools.translate import _
27 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
28 import openerp.addons.decimal_precision as dp
29 from openerp import workflow
31 class sale_order(osv.osv):
33 _inherit = ['mail.thread', 'ir.needaction_mixin']
34 _description = "Sales Order"
37 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
38 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
42 def copy(self, cr, uid, id, default=None, context=None):
46 'date_order': fields.date.context_today(self, cr, uid, context=context),
49 'date_confirm': False,
50 'client_order_ref': '',
51 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
52 'procurement_group_id': False,
54 return super(sale_order, self).copy(cr, uid, id, default, context=context)
56 def _amount_line_tax(self, cr, uid, line, context=None):
58 for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id, line.price_unit * (1-(line.discount or 0.0)/100.0), line.product_uom_qty, line.product_id, line.order_id.partner_id)['taxes']:
59 val += c.get('amount', 0.0)
62 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
63 """ Wrapper because of direct method passing as parameter for function fields """
64 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
66 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
67 cur_obj = self.pool.get('res.currency')
69 for order in self.browse(cr, uid, ids, context=context):
71 'amount_untaxed': 0.0,
76 cur = order.pricelist_id.currency_id
77 for line in order.order_line:
78 val1 += line.price_subtotal
79 val += self._amount_line_tax(cr, uid, line, context=context)
80 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
81 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
82 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
86 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
88 for sale in self.browse(cursor, user, ids, context=context):
93 for invoice in sale.invoice_ids:
94 if invoice.state not in ('draft', 'cancel'):
95 tot += invoice.amount_untaxed
97 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
102 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
104 for sale in self.browse(cursor, user, ids, context=context):
110 def _invoiced(self, cursor, user, ids, name, arg, context=None):
112 for sale in self.browse(cursor, user, ids, context=context):
114 invoice_existence = False
115 for invoice in sale.invoice_ids:
116 if invoice.state!='cancel':
117 invoice_existence = True
118 if invoice.state != 'paid':
121 if not invoice_existence or sale.state == 'manual':
125 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
134 clause += 'AND inv.state = \'paid\''
136 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
137 sale_clause = ', sale_order AS sale '
140 cursor.execute('SELECT rel.order_id ' \
141 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
142 'WHERE rel.invoice_id = inv.id ' + clause)
143 res = cursor.fetchall()
145 cursor.execute('SELECT sale.id ' \
146 'FROM sale_order AS sale ' \
147 'WHERE sale.id NOT IN ' \
148 '(SELECT rel.order_id ' \
149 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
150 res.extend(cursor.fetchall())
152 return [('id', '=', 0)]
153 return [('id', 'in', [x[0] for x in res])]
155 def _get_order(self, cr, uid, ids, context=None):
157 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
158 result[line.order_id.id] = True
161 def _get_default_company(self, cr, uid, context=None):
162 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
164 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
168 'name': fields.char('Order Reference', size=64, required=True,
169 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
170 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
171 'client_order_ref': fields.char('Customer Reference', size=64),
172 'state': fields.selection([
173 ('draft', 'Draft Quotation'),
174 ('sent', 'Quotation Sent'),
175 ('cancel', 'Cancelled'),
176 ('waiting_date', 'Waiting Schedule'),
177 ('progress', 'Sales Order'),
178 ('manual', 'Sale to Invoice'),
179 ('shipping_except', 'Shipping Exception'),
180 ('invoice_except', 'Invoice Exception'),
182 ], 'Status', readonly=True, help="Gives the status of the quotation or sales order.\
183 \nThe exception status is automatically set when a cancel operation occurs \
184 in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
185 but waiting for the scheduler to run on the order date.", select=True),
186 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
187 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
188 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
189 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
190 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
191 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
192 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
193 'order_policy': fields.selection([
194 ('manual', 'On Demand'),
195 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
196 help="""This field controls how invoice and delivery operations are synchronized."""),
197 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
198 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
199 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
201 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
202 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
203 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
204 'invoiced': fields.function(_invoiced, string='Paid',
205 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
206 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
207 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
208 'note': fields.text('Terms and conditions'),
210 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
212 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
213 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
215 multi='sums', help="The amount without tax.", track_visibility='always'),
216 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
218 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
219 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
221 multi='sums', help="The tax amount."),
222 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
224 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
225 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
227 multi='sums', help="The total amount."),
229 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
230 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
231 'company_id': fields.many2one('res.company', 'Company'),
232 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group'),
235 'date_order': fields.datetime.now,
236 'order_policy': 'manual',
237 'company_id': _get_default_company,
239 'user_id': lambda obj, cr, uid, context: uid,
240 'name': lambda obj, cr, uid, context: '/',
241 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
242 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
243 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
246 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
248 _order = 'date_order desc, id desc'
251 def unlink(self, cr, uid, ids, context=None):
252 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
254 for s in sale_orders:
255 if s['state'] in ['draft', 'cancel']:
256 unlink_ids.append(s['id'])
258 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
260 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
262 def copy_quotation(self, cr, uid, ids, context=None):
263 id = self.copy(cr, uid, ids[0], context=None)
264 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
265 view_id = view_ref and view_ref[1] or False,
267 'type': 'ir.actions.act_window',
268 'name': _('Sales Order'),
269 'res_model': 'sale.order',
278 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
279 context = context or {}
283 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
286 return {'value': value}
288 'title': _('Pricelist Warning!'),
289 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
291 return {'warning': warning, 'value': value}
293 def get_salenote(self, cr, uid, ids, partner_id, context=None):
294 context_lang = context.copy()
296 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
297 context_lang.update({'lang': partner_lang})
298 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
300 def onchange_partner_id(self, cr, uid, ids, part, context=None):
302 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
304 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
305 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
306 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
307 payment_term = part.property_payment_term and part.property_payment_term.id or False
308 fiscal_position = part.property_account_position and part.property_account_position.id or False
309 dedicated_salesman = part.user_id and part.user_id.id or uid
311 'partner_invoice_id': addr['invoice'],
312 'partner_shipping_id': addr['delivery'],
313 'payment_term': payment_term,
314 'fiscal_position': fiscal_position,
315 'user_id': dedicated_salesman,
318 val['pricelist_id'] = pricelist
319 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
320 if sale_note: val.update({'note': sale_note})
321 return {'value': val}
323 def create(self, cr, uid, vals, context=None):
326 if vals.get('name', '/') == '/':
327 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
328 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
329 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
330 vals = dict(defaults, **vals)
331 context.update({'mail_create_nolog': True})
332 new_id = super(sale_order, self).create(cr, uid, vals, context=context)
333 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
336 def button_dummy(self, cr, uid, ids, context=None):
339 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
340 # can be removed after 6.1.
341 def _inv_get(self, cr, uid, order, context=None):
344 def _prepare_invoice(self, cr, uid, order, lines, context=None):
345 """Prepare the dict of values to create the new invoice for a
346 sales order. This method may be overridden to implement custom
347 invoice generation (making sure to call super() to establish
348 a clean extension chain).
350 :param browse_record order: sale.order record to invoice
351 :param list(int) line: list of invoice line IDs that must be
352 attached to the invoice
353 :return: dict of value to create() the invoice
357 journal_ids = self.pool.get('account.journal').search(cr, uid,
358 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
361 raise osv.except_osv(_('Error!'),
362 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
364 'name': order.client_order_ref or '',
365 'origin': order.name,
366 'type': 'out_invoice',
367 'reference': order.client_order_ref or order.name,
368 'account_id': order.partner_id.property_account_receivable.id,
369 'partner_id': order.partner_invoice_id.id,
370 'journal_id': journal_ids[0],
371 'invoice_line': [(6, 0, lines)],
372 'currency_id': order.pricelist_id.currency_id.id,
373 'comment': order.note,
374 'payment_term': order.payment_term and order.payment_term.id or False,
375 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
376 'date_invoice': context.get('date_invoice', False),
377 'company_id': order.company_id.id,
378 'user_id': order.user_id and order.user_id.id or False
381 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
382 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
385 def _make_invoice(self, cr, uid, order, lines, context=None):
386 inv_obj = self.pool.get('account.invoice')
387 obj_invoice_line = self.pool.get('account.invoice.line')
390 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
391 from_line_invoice_ids = []
392 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
393 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
394 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
395 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
396 for preinv in order.invoice_ids:
397 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
398 for preline in preinv.invoice_line:
399 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
400 lines.append(inv_line_id)
401 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
402 inv_id = inv_obj.create(cr, uid, inv, context=context)
403 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
404 if data.get('value', False):
405 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
406 inv_obj.button_compute(cr, uid, [inv_id])
409 def print_quotation(self, cr, uid, ids, context=None):
411 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
413 assert len(ids) == 1, 'This option should only be used for a single id at a time'
414 self.signal_quotation_sent(cr, uid, ids)
415 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
417 def manual_invoice(self, cr, uid, ids, context=None):
418 """ create invoices for the given sales orders (ids), and open the form
419 view of one of the newly created invoices
421 mod_obj = self.pool.get('ir.model.data')
423 # create invoices through the sales orders' workflow
424 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
425 self.signal_manual_invoice(cr, uid, ids)
426 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
427 # determine newly created invoices
428 new_inv_ids = list(inv_ids1 - inv_ids0)
430 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
431 res_id = res and res[1] or False,
434 'name': _('Customer Invoices'),
438 'res_model': 'account.invoice',
439 'context': "{'type':'out_invoice'}",
440 'type': 'ir.actions.act_window',
443 'res_id': new_inv_ids and new_inv_ids[0] or False,
446 def action_view_invoice(self, cr, uid, ids, context=None):
448 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
450 mod_obj = self.pool.get('ir.model.data')
451 act_obj = self.pool.get('ir.actions.act_window')
453 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
454 id = result and result[1] or False
455 result = act_obj.read(cr, uid, [id], context=context)[0]
456 #compute the number of invoices to display
458 for so in self.browse(cr, uid, ids, context=context):
459 inv_ids += [invoice.id for invoice in so.invoice_ids]
460 #choose the view_mode accordingly
462 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
464 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
465 result['views'] = [(res and res[1] or False, 'form')]
466 result['res_id'] = inv_ids and inv_ids[0] or False
469 def test_no_product(self, cr, uid, order, context):
470 for line in order.order_line:
471 if line.product_id and (line.product_id.type<>'service'):
475 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
477 states = ['confirmed', 'done', 'exception']
481 invoice = self.pool.get('account.invoice')
482 obj_sale_order_line = self.pool.get('sale.order.line')
483 partner_currency = {}
486 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
487 # last day of the last month as invoice date
489 context['date_invoice'] = date_invoice
490 for o in self.browse(cr, uid, ids, context=context):
491 currency_id = o.pricelist_id.currency_id.id
492 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
493 raise osv.except_osv(
495 _('You cannot group sales having different currencies for the same partner.'))
497 partner_currency[o.partner_id.id] = currency_id
499 for line in o.order_line:
502 elif (line.state in states):
503 lines.append(line.id)
504 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
506 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
508 for o in self.browse(cr, uid, ids, context=context):
509 for i in o.invoice_ids:
510 if i.state == 'draft':
512 for val in invoices.values():
514 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
517 invoice_ref += o.name + '|'
518 self.write(cr, uid, [o.id], {'state': 'progress'})
519 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
520 #remove last '|' in invoice_ref
521 if len(invoice_ref) >= 1:
522 invoice_ref = invoice_ref[:-1]
523 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
525 for order, il in val:
526 res = self._make_invoice(cr, uid, order, il, context=context)
527 invoice_ids.append(res)
528 self.write(cr, uid, [order.id], {'state': 'progress'})
529 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
532 def action_invoice_cancel(self, cr, uid, ids, context=None):
533 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
536 def action_invoice_end(self, cr, uid, ids, context=None):
537 for this in self.browse(cr, uid, ids, context=context):
538 for line in this.order_line:
539 if line.state == 'exception':
540 line.write({'state': 'confirmed'})
541 if this.state == 'invoice_except':
542 this.write({'state': 'progress'})
545 def action_cancel(self, cr, uid, ids, context=None):
548 sale_order_line_obj = self.pool.get('sale.order.line')
549 account_invoice_obj = self.pool.get('account.invoice')
550 for sale in self.browse(cr, uid, ids, context=context):
551 for inv in sale.invoice_ids:
552 if inv.state not in ('draft', 'cancel'):
553 raise osv.except_osv(
554 _('Cannot cancel this sales order!'),
555 _('First cancel all invoices attached to this sales order.'))
556 for r in self.read(cr, uid, ids, ['invoice_ids']):
557 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
558 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
560 self.write(cr, uid, ids, {'state': 'cancel'})
563 def action_button_confirm(self, cr, uid, ids, context=None):
564 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
565 self.signal_order_confirm(cr, uid, ids)
567 # redisplay the record as a sales order
568 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
569 view_id = view_ref and view_ref[1] or False,
571 'type': 'ir.actions.act_window',
572 'name': _('Sales Order'),
573 'res_model': 'sale.order',
582 def action_wait(self, cr, uid, ids, context=None):
583 context = context or {}
584 for o in self.browse(cr, uid, ids):
586 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
587 noprod = self.test_no_product(cr, uid, o, context)
588 if (o.order_policy == 'manual') or noprod:
589 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
591 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
592 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
595 def action_quotation_send(self, cr, uid, ids, context=None):
597 This function opens a window to compose an email, with the edi sale template message loaded by default
599 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
600 ir_model_data = self.pool.get('ir.model.data')
602 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
606 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
608 compose_form_id = False
611 'default_model': 'sale.order',
612 'default_res_id': ids[0],
613 'default_use_template': bool(template_id),
614 'default_template_id': template_id,
615 'default_composition_mode': 'comment',
616 'mark_so_as_sent': True
619 'type': 'ir.actions.act_window',
622 'res_model': 'mail.compose.message',
623 'views': [(compose_form_id, 'form')],
624 'view_id': compose_form_id,
629 def action_done(self, cr, uid, ids, context=None):
630 for order in self.browse(cr, uid, ids, context=context):
631 self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
632 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
634 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
635 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
638 'origin': order.name,
639 'date_planned': date_planned,
640 'product_id': line.product_id.id,
641 'product_qty': line.product_uom_qty,
642 'product_uom': line.product_uom.id,
643 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
644 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
645 'company_id': order.company_id.id,
646 'group_id': group_id,
647 'invoice_state': (order.order_policy=='picking') and '2binvoiced' or 'none',
648 'sale_line_id': line.id
651 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
652 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=line.delay or 0.0)
655 def _prepare_procurement_group(self, cr, uid, order, context=None):
656 return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
658 def procurement_needed(self, cr, uid, ids, context=None):
659 #when sale is installed only, there is no need to create procurements, that's only
660 #further installed modules (project_mrp, sale_stock) that will change this.
661 sale_line_obj = self.pool.get('sale.order.line')
663 for order in self.browse(cr, uid, ids, context=context):
664 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
667 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
668 for sale_order in self.browse(cr, uid, ids, context=context):
669 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
672 def action_ship_create(self, cr, uid, ids, context=None):
673 """Create the required procurements to supply sales order lines, also connecting
674 the procurements to appropriate stock moves in order to bring the goods to the
675 sales order's requested location.
679 procurement_obj = self.pool.get('procurement.order')
680 sale_line_obj = self.pool.get('sale.order.line')
681 for order in self.browse(cr, uid, ids, context=context):
683 vals = self._prepare_procurement_group(cr, uid, order, context=context)
684 if not order.procurement_group_id:
685 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
686 order.write({'procurement_group_id': group_id}, context=context)
688 for line in order.order_line:
689 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
690 if line.procurement_ids:
691 #first check them to see if they are in exception or not (one of the related moves is cancelled)
692 procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
694 #run again procurement that are in exception in order to trigger another move
695 proc_ids += [x.id for x in line.procurement_ids if x.state == 'exception']
696 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
697 if (line.state == 'done') or not line.product_id:
699 vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
700 proc_id = procurement_obj.create(cr, uid, vals, context=context)
701 proc_ids.append(proc_id)
702 #Confirm procurement order such that rules will be applied on it
703 #note that the workflow normally ensure proc_ids isn't an empty list
704 procurement_obj.run(cr, uid, proc_ids, context=context)
706 #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
707 if order.state == 'shipping_except':
708 val = {'state': 'progress', 'shipped': False}
710 if (order.order_policy == 'manual'):
711 for line in order.order_line:
712 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
713 val['state'] = 'manual'
718 # if mode == 'finished':
719 # returns True if all lines are done, False otherwise
720 # if mode == 'canceled':
721 # returns True if there is at least one canceled line, False otherwise
722 def test_state(self, cr, uid, ids, mode, *args):
723 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
727 write_cancel_ids = []
728 for order in self.browse(cr, uid, ids, context={}):
730 #TODO: Need to rethink what happens when cancelling
731 for line in order.order_line:
732 states = [x.state for x in line.procurement_ids]
733 cancel = states and all([x == 'cancel' for x in states])
734 doneorcancel = all([x in ('done', 'cancel') for x in states])
737 if line.state != 'exception':
738 write_cancel_ids.append(line.id)
741 if doneorcancel and not cancel:
742 write_done_ids.append(line.id)
745 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
747 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
749 if mode == 'finished':
751 elif mode == 'canceled':
755 def procurement_lines_get(self, cr, uid, ids, *args):
757 for order in self.browse(cr, uid, ids, context={}):
758 for line in order.order_line:
759 res += [x.id for x in line.procurement_ids]
766 # TODO add a field price_unit_uos
767 # - update it on change product and unit price
768 # - use it in report if there is a uos
769 class sale_order_line(osv.osv):
771 def need_procurement(self, cr, uid, ids, context=None):
772 #when sale is installed only, there is no need to create procurements, that's only
773 #further installed modules (project_mrp, sale_stock) that will change this.
776 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
777 tax_obj = self.pool.get('account.tax')
778 cur_obj = self.pool.get('res.currency')
782 for line in self.browse(cr, uid, ids, context=context):
783 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
784 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
785 cur = line.order_id.pricelist_id.currency_id
786 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
789 def _get_uom_id(self, cr, uid, *args):
791 proxy = self.pool.get('ir.model.data')
792 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
794 except Exception, ex:
797 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
798 res = dict.fromkeys(ids, False)
799 for this in self.browse(cr, uid, ids, context=context):
800 res[this.id] = this.invoice_lines and \
801 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
804 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
805 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
806 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
807 sale_order_line sol ON (sol.order_id = rel.order_id)
808 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
809 return [i[0] for i in cr.fetchall()]
812 _name = 'sale.order.line'
813 _description = 'Sales Order Line'
815 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
816 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
817 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
818 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}),
819 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
820 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
822 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
823 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
825 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
826 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
827 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
828 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
829 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
830 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
831 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
832 'product_uos': fields.many2one('product.uom', 'Product UoS'),
833 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
834 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
835 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
836 help='* The \'Draft\' status is set when the related sales order in draft status. \
837 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
838 \n* The \'Exception\' status is set when the related sales order is set as exception. \
839 \n* The \'Done\' status is set when the sales order line has been picked. \
840 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
841 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
842 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
843 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
844 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
845 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
847 _order = 'order_id desc, sequence, id'
849 'product_uom' : _get_uom_id,
851 'product_uom_qty': 1,
852 'product_uos_qty': 1,
859 def _get_line_qty(self, cr, uid, line, context=None):
861 return line.product_uos_qty or 0.0
862 return line.product_uom_qty
864 def _get_line_uom(self, cr, uid, line, context=None):
866 return line.product_uos.id
867 return line.product_uom.id
869 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
870 """Prepare the dict of values to create the new invoice line for a
871 sales order line. This method may be overridden to implement custom
872 invoice generation (making sure to call super() to establish
873 a clean extension chain).
875 :param browse_record line: sale.order.line record to invoice
876 :param int account_id: optional ID of a G/L account to force
877 (this is used for returning products including service)
878 :return: dict of values to create() the invoice line
881 if not line.invoiced:
884 account_id = line.product_id.property_account_income.id
886 account_id = line.product_id.categ_id.property_account_income_categ.id
888 raise osv.except_osv(_('Error!'),
889 _('Please define income account for this product: "%s" (id:%d).') % \
890 (line.product_id.name, line.product_id.id,))
892 prop = self.pool.get('ir.property').get(cr, uid,
893 'property_account_income_categ', 'product.category',
895 account_id = prop and prop.id or False
896 uosqty = self._get_line_qty(cr, uid, line, context=context)
897 uos_id = self._get_line_uom(cr, uid, line, context=context)
900 pu = round(line.price_unit * line.product_uom_qty / uosqty,
901 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
902 fpos = line.order_id.fiscal_position or False
903 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
905 raise osv.except_osv(_('Error!'),
906 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
909 'sequence': line.sequence,
910 'origin': line.order_id.name,
911 'account_id': account_id,
914 'discount': line.discount,
916 'product_id': line.product_id.id or False,
917 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
918 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
923 def invoice_line_create(self, cr, uid, ids, context=None):
929 for line in self.browse(cr, uid, ids, context=context):
930 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
932 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
933 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
934 sales.add(line.order_id.id)
935 create_ids.append(inv_id)
936 # Trigger workflow events
937 for sale_id in sales:
938 workflow.trg_write(uid, 'sale.order', sale_id, cr)
941 def button_cancel(self, cr, uid, ids, context=None):
942 for line in self.browse(cr, uid, ids, context=context):
944 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
945 return self.write(cr, uid, ids, {'state': 'cancel'})
947 def button_confirm(self, cr, uid, ids, context=None):
948 return self.write(cr, uid, ids, {'state': 'confirmed'})
950 def button_done(self, cr, uid, ids, context=None):
951 res = self.write(cr, uid, ids, {'state': 'done'})
952 for line in self.browse(cr, uid, ids, context=context):
953 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
956 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
957 product_obj = self.pool.get('product.product')
959 return {'value': {'product_uom': product_uos,
960 'product_uom_qty': product_uos_qty}, 'domain': {}}
962 product = product_obj.browse(cr, uid, product_id)
964 'product_uom': product.uom_id.id,
966 # FIXME must depend on uos/uom of the product and not only of the coeff.
969 'product_uom_qty': product_uos_qty / product.uos_coeff,
970 'th_weight': product_uos_qty / product.uos_coeff * product.weight
972 except ZeroDivisionError:
974 return {'value': value}
976 def create(self, cr, uid, values, context=None):
977 if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
978 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
979 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
980 qty=float(values.get('product_uom_qty', False)),
981 uom=values.get('product_uom', False),
982 qty_uos=float(values.get('product_uos_qty', False)),
983 uos=values.get('product_uos', False),
984 name=values.get('name', False),
985 partner_id=order['partner_id'][0],
986 date_order=order['date_order'],
987 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
988 flag=False, # Force name update
991 values = dict(defaults, **values)
992 return super(sale_order_line, self).create(cr, uid, values, context=context)
994 def copy_data(self, cr, uid, id, default=None, context=None):
997 default.update({'state': 'draft', 'invoice_lines': [], 'procurement_ids': []})
998 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1000 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1001 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1002 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1003 context = context or {}
1004 lang = lang or context.get('lang', False)
1006 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
1008 product_uom_obj = self.pool.get('product.uom')
1009 partner_obj = self.pool.get('res.partner')
1010 product_obj = self.pool.get('product.product')
1011 context = {'lang': lang, 'partner_id': partner_id}
1013 lang = partner_obj.browse(cr, uid, partner_id).lang
1014 context_partner = {'lang': lang, 'partner_id': partner_id}
1017 return {'value': {'th_weight': 0,
1018 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1021 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1025 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
1029 uom2 = product_uom_obj.browse(cr, uid, uom)
1030 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1033 if product_obj.uos_id:
1034 uos2 = product_uom_obj.browse(cr, uid, uos)
1035 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1039 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1040 if update_tax: #The quantity only have changed
1041 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1044 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1045 if product_obj.description_sale:
1046 result['name'] += '\n'+product_obj.description_sale
1048 if (not uom) and (not uos):
1049 result['product_uom'] = product_obj.uom_id.id
1050 if product_obj.uos_id:
1051 result['product_uos'] = product_obj.uos_id.id
1052 result['product_uos_qty'] = qty * product_obj.uos_coeff
1053 uos_category_id = product_obj.uos_id.category_id.id
1055 result['product_uos'] = False
1056 result['product_uos_qty'] = qty
1057 uos_category_id = False
1058 result['th_weight'] = qty * product_obj.weight
1059 domain = {'product_uom':
1060 [('category_id', '=', product_obj.uom_id.category_id.id)],
1062 [('category_id', '=', uos_category_id)]}
1063 elif uos and not uom: # only happens if uom is False
1064 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1065 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1066 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1067 elif uom: # whether uos is set or not
1068 default_uom = product_obj.uom_id and product_obj.uom_id.id
1069 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1070 if product_obj.uos_id:
1071 result['product_uos'] = product_obj.uos_id.id
1072 result['product_uos_qty'] = qty * product_obj.uos_coeff
1074 result['product_uos'] = False
1075 result['product_uos_qty'] = qty
1076 result['th_weight'] = q * product_obj.weight # Round the quantity up
1079 uom2 = product_obj.uom_id
1083 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1084 'Please set one before choosing a product.')
1085 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1087 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1088 product, qty or 1.0, partner_id, {
1089 'uom': uom or result.get('product_uom'),
1093 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1094 "You have to change either the product, the quantity or the pricelist.")
1096 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1098 result.update({'price_unit': price})
1101 'title': _('Configuration Error!'),
1102 'message' : warning_msgs
1104 return {'value': result, 'domain': domain, 'warning': warning}
1106 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1107 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1108 lang=False, update_tax=True, date_order=False, context=None):
1109 context = context or {}
1110 lang = lang or ('lang' in context and context['lang'])
1112 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1113 return self.product_id_change(cursor, user, ids, pricelist, product,
1114 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1115 partner_id=partner_id, lang=lang, update_tax=update_tax,
1116 date_order=date_order, context=context)
1118 def unlink(self, cr, uid, ids, context=None):
1121 """Allows to delete sales order lines in draft,cancel states"""
1122 for rec in self.browse(cr, uid, ids, context=context):
1123 if rec.state not in ['draft', 'cancel']:
1124 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1125 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1127 class res_company(osv.Model):
1128 _inherit = "res.company"
1130 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
1134 class mail_compose_message(osv.Model):
1135 _inherit = 'mail.compose.message'
1137 def send_mail(self, cr, uid, ids, context=None):
1138 context = context or {}
1139 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1140 context = dict(context, mail_post_autofollow=True)
1141 self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1142 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1145 class account_invoice(osv.Model):
1146 _inherit = 'account.invoice'
1148 def confirm_paid(self, cr, uid, ids, context=None):
1149 sale_order_obj = self.pool.get('sale.order')
1150 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1151 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1152 for so_id in so_ids:
1153 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1156 def unlink(self, cr, uid, ids, context=None):
1157 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1158 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1159 #if we can't cancel all invoices, do nothing
1160 if len(invoice_ids) == len(ids):
1161 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1162 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1164 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1165 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1167 class procurement_order(osv.osv):
1168 _inherit = 'procurement.order'
1170 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),