[MERGE] Sync with trunk
[odoo/odoo.git] / addons / sale / sale.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
24 import time
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 netsvc
30
31 class sale_order(osv.osv):
32     _name = "sale.order"
33     _inherit = ['mail.thread', 'ir.needaction_mixin']
34     _description = "Sales Order"
35     _track = {
36         'state': {
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']
39         },
40     }
41
42     def copy(self, cr, uid, id, default=None, context=None):
43         if not default:
44             default = {}
45         default.update({
46             'state': 'draft',
47             'invoice_ids': [],
48             'date_confirm': False,
49             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
50         })
51         return super(sale_order, self).copy(cr, uid, id, default, context=context)
52
53     def _amount_line_tax(self, cr, uid, line, context=None):
54         val = 0.0
55         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']:
56             val += c.get('amount', 0.0)
57         return val
58
59     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
60         cur_obj = self.pool.get('res.currency')
61         res = {}
62         for order in self.browse(cr, uid, ids, context=context):
63             res[order.id] = {
64                 'amount_untaxed': 0.0,
65                 'amount_tax': 0.0,
66                 'amount_total': 0.0,
67             }
68             val = val1 = 0.0
69             cur = order.pricelist_id.currency_id
70             for line in order.order_line:
71                 val1 += line.price_subtotal
72                 val += self._amount_line_tax(cr, uid, line, context=context)
73             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
74             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
75             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
76         return res
77
78
79     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
80         res = {}
81         for sale in self.browse(cursor, user, ids, context=context):
82             if sale.invoiced:
83                 res[sale.id] = 100.0
84                 continue
85             tot = 0.0
86             for invoice in sale.invoice_ids:
87                 if invoice.state not in ('draft', 'cancel'):
88                     tot += invoice.amount_untaxed
89             if tot:
90                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
91             else:
92                 res[sale.id] = 0.0
93         return res
94
95     def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
96         res = {}
97         for sale in self.browse(cursor, user, ids, context=context):
98             res[sale.id] = False
99             if sale.invoice_ids:
100                 res[sale.id] = True
101         return res
102
103     def _invoiced(self, cursor, user, ids, name, arg, context=None):
104         res = {}
105         for sale in self.browse(cursor, user, ids, context=context):
106             res[sale.id] = True
107             invoice_existence = False
108             for invoice in sale.invoice_ids:
109                 if invoice.state!='cancel':
110                     invoice_existence = True
111                     if invoice.state != 'paid':
112                         res[sale.id] = False
113                         break
114             if not invoice_existence or sale.state == 'manual':
115                 res[sale.id] = False
116         return res
117
118     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
119         if not len(args):
120             return []
121         clause = ''
122         sale_clause = ''
123         no_invoiced = False
124         for arg in args:
125             if arg[1] == '=':
126                 if arg[2]:
127                     clause += 'AND inv.state = \'paid\''
128                 else:
129                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
130                     sale_clause = ',  sale_order AS sale '
131                     no_invoiced = True
132
133         cursor.execute('SELECT rel.order_id ' \
134                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
135                 'WHERE rel.invoice_id = inv.id ' + clause)
136         res = cursor.fetchall()
137         if no_invoiced:
138             cursor.execute('SELECT sale.id ' \
139                     'FROM sale_order AS sale ' \
140                     'WHERE sale.id NOT IN ' \
141                         '(SELECT rel.order_id ' \
142                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
143             res.extend(cursor.fetchall())
144         if not res:
145             return [('id', '=', 0)]
146         return [('id', 'in', [x[0] for x in res])]
147
148     def _get_order(self, cr, uid, ids, context=None):
149         result = {}
150         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
151             result[line.order_id.id] = True
152         return result.keys()
153
154     def _get_default_company(self, cr, uid, context=None):
155         company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
156         if not company_id:
157             raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
158         return company_id
159
160     _columns = {
161         'name': fields.char('Order Reference', size=64, required=True,
162             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
163         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
164         'client_order_ref': fields.char('Customer Reference', size=64),
165         'state': fields.selection([
166             ('draft', 'Draft Quotation'),
167             ('sent', 'Quotation Sent'),
168             ('cancel', 'Cancelled'),
169             ('waiting_date', 'Waiting Schedule'),
170             ('progress', 'Sales Order'),
171             ('manual', 'Sale to Invoice'),
172             ('invoice_except', 'Invoice Exception'),
173             ('done', 'Done'),
174             ], 'Status', readonly=True, track_visibility='onchange',
175             help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sales order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
176         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
177         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
178         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
179         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
180         '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'),
181         '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."),
182         '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."),
183         'order_policy': fields.selection([
184                 ('manual', 'On Demand'),
185             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
186             help="""This field controls how invoice and delivery operations are synchronized."""),
187         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
188         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
189         '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."),
190
191         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192         '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)."),
193         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
194         'invoiced': fields.function(_invoiced, string='Paid',
195             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
196         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
197             fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
198         'note': fields.text('Terms and conditions'),
199
200         'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
201             store={
202                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
203                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
204             },
205             multi='sums', help="The amount without tax.", track_visibility='always'),
206         'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
207             store={
208                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
209                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
210             },
211             multi='sums', help="The tax amount."),
212         'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
213             store={
214                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
215                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
216             },
217             multi='sums', help="The total amount."),
218
219         'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sales order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
220         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
221         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
222         'company_id': fields.many2one('res.company', 'Company'),
223     }
224     _defaults = {
225         'date_order': fields.date.context_today,
226         'order_policy': 'manual',
227         'company_id': _get_default_company,
228         'state': 'draft',
229         'user_id': lambda obj, cr, uid, context: uid,
230         'name': lambda obj, cr, uid, context: '/',
231         'invoice_quantity': 'order',
232         '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'],
233         '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'],
234         'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
235     }
236     _sql_constraints = [
237         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
238     ]
239     _order = 'name desc'
240
241     # Form filling
242     def unlink(self, cr, uid, ids, context=None):
243         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
244         unlink_ids = []
245         for s in sale_orders:
246             if s['state'] in ['draft', 'cancel']:
247                 unlink_ids.append(s['id'])
248             else:
249                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
250
251         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
252
253     def copy_quotation(self, cr, uid, ids, context=None):
254         id = self.copy(cr, uid, ids[0], context=None)
255         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
256         view_id = view_ref and view_ref[1] or False,
257         return {
258             'type': 'ir.actions.act_window',
259             'name': _('Sales Order'),
260             'res_model': 'sale.order',
261             'res_id': id,
262             'view_type': 'form',
263             'view_mode': 'form',
264             'view_id': view_id,
265             'target': 'current',
266             'nodestroy': True,
267         }
268
269     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
270         context = context or {}
271         if not pricelist_id:
272             return {}
273         value = {
274             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
275         }
276         if not order_lines:
277             return {'value': value}
278         warning = {
279             'title': _('Pricelist Warning!'),
280             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
281         }
282         return {'warning': warning, 'value': value}
283
284     def get_salenote(self, cr, uid, ids, partner_id, context=None):
285         context_lang = context.copy() 
286         if partner_id:
287             partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
288             context_lang.update({'lang': partner_lang})
289         return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
290             
291     def onchange_partner_id(self, cr, uid, ids, part, context=None):
292         if not part:
293             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
294
295         part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
296         addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
297         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
298         payment_term = part.property_payment_term and part.property_payment_term.id or False
299         fiscal_position = part.property_account_position and part.property_account_position.id or False
300         dedicated_salesman = part.user_id and part.user_id.id or uid
301         val = {
302             'partner_invoice_id': addr['invoice'],
303             'partner_shipping_id': addr['delivery'],
304             'payment_term': payment_term,
305             'fiscal_position': fiscal_position,
306             'user_id': dedicated_salesman,
307         }
308         if pricelist:
309             val['pricelist_id'] = pricelist
310         sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
311         if sale_note: val.update({'note': sale_note})  
312         return {'value': val}
313
314     def create(self, cr, uid, vals, context=None):
315         if context is None:
316             context = {}
317         if vals.get('name', '/') == '/':
318             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
319         context.update({'mail_create_nolog': True})
320         new_id = super(sale_order, self).create(cr, uid, vals, context=context)
321         self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
322         return new_id
323
324     def button_dummy(self, cr, uid, ids, context=None):
325         return True
326
327     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
328     #        can be removed after 6.1.
329     def _inv_get(self, cr, uid, order, context=None):
330         return {}
331
332     def _prepare_invoice(self, cr, uid, order, lines, context=None):
333         """Prepare the dict of values to create the new invoice for a
334            sales order. This method may be overridden to implement custom
335            invoice generation (making sure to call super() to establish
336            a clean extension chain).
337
338            :param browse_record order: sale.order record to invoice
339            :param list(int) line: list of invoice line IDs that must be
340                                   attached to the invoice
341            :return: dict of value to create() the invoice
342         """
343         if context is None:
344             context = {}
345         journal_ids = self.pool.get('account.journal').search(cr, uid,
346             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
347             limit=1)
348         if not journal_ids:
349             raise osv.except_osv(_('Error!'),
350                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
351         invoice_vals = {
352             'name': order.client_order_ref or '',
353             'origin': order.name,
354             'type': 'out_invoice',
355             'reference': order.client_order_ref or order.name,
356             'account_id': order.partner_id.property_account_receivable.id,
357             'partner_id': order.partner_invoice_id.id,
358             'journal_id': journal_ids[0],
359             'invoice_line': [(6, 0, lines)],
360             'currency_id': order.pricelist_id.currency_id.id,
361             'comment': order.note,
362             'payment_term': order.payment_term and order.payment_term.id or False,
363             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
364             'date_invoice': context.get('date_invoice', False),
365             'company_id': order.company_id.id,
366             'user_id': order.user_id and order.user_id.id or False
367         }
368
369         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
370         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
371         return invoice_vals
372
373     def _make_invoice(self, cr, uid, order, lines, context=None):
374         inv_obj = self.pool.get('account.invoice')
375         obj_invoice_line = self.pool.get('account.invoice.line')
376         if context is None:
377             context = {}
378         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
379         from_line_invoice_ids = []
380         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
381             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
382                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
383                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
384         for preinv in order.invoice_ids:
385             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
386                 for preline in preinv.invoice_line:
387                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
388                     lines.append(inv_line_id)
389         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
390         inv_id = inv_obj.create(cr, uid, inv, context=context)
391         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
392         if data.get('value', False):
393             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
394         inv_obj.button_compute(cr, uid, [inv_id])
395         return inv_id
396
397     def print_quotation(self, cr, uid, ids, context=None):
398         '''
399         This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
400         '''
401         assert len(ids) == 1, 'This option should only be used for a single id at a time'
402         self.signal_quotation_sent(cr, uid, ids)
403         datas = {
404                  'model': 'sale.order',
405                  'ids': ids,
406                  'form': self.read(cr, uid, ids[0], context=context),
407         }
408         return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
409
410     def manual_invoice(self, cr, uid, ids, context=None):
411         """ create invoices for the given sales orders (ids), and open the form
412             view of one of the newly created invoices
413         """
414         mod_obj = self.pool.get('ir.model.data')
415         
416         # create invoices through the sales orders' workflow
417         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
418         self.signal_manual_invoice(cr, uid, ids)
419         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
420         # determine newly created invoices
421         new_inv_ids = list(inv_ids1 - inv_ids0)
422
423         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
424         res_id = res and res[1] or False,
425
426         return {
427             'name': _('Customer Invoices'),
428             'view_type': 'form',
429             'view_mode': 'form',
430             'view_id': [res_id],
431             'res_model': 'account.invoice',
432             'context': "{'type':'out_invoice'}",
433             'type': 'ir.actions.act_window',
434             'nodestroy': True,
435             'target': 'current',
436             'res_id': new_inv_ids and new_inv_ids[0] or False,
437         }
438
439     def action_view_invoice(self, cr, uid, ids, context=None):
440         '''
441         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.
442         '''
443         mod_obj = self.pool.get('ir.model.data')
444         act_obj = self.pool.get('ir.actions.act_window')
445
446         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
447         id = result and result[1] or False
448         result = act_obj.read(cr, uid, [id], context=context)[0]
449         #compute the number of invoices to display
450         inv_ids = []
451         for so in self.browse(cr, uid, ids, context=context):
452             inv_ids += [invoice.id for invoice in so.invoice_ids]
453         #choose the view_mode accordingly
454         if len(inv_ids)>1:
455             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
456         else:
457             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
458             result['views'] = [(res and res[1] or False, 'form')]
459             result['res_id'] = inv_ids and inv_ids[0] or False
460         return result
461
462     def test_no_product(self, cr, uid, order, context):
463         for line in order.order_line:
464             if line.product_id and (line.product_id.type<>'service'):
465                 return False
466         return True
467
468     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
469         if states is None:
470             states = ['confirmed', 'done', 'exception']
471         res = False
472         invoices = {}
473         invoice_ids = []
474         invoice = self.pool.get('account.invoice')
475         obj_sale_order_line = self.pool.get('sale.order.line')
476         partner_currency = {}
477         if context is None:
478             context = {}
479         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
480         # last day of the last month as invoice date
481         if date_invoice:
482             context['date_invoice'] = date_invoice
483         for o in self.browse(cr, uid, ids, context=context):
484             currency_id = o.pricelist_id.currency_id.id
485             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
486                 raise osv.except_osv(
487                     _('Error!'),
488                     _('You cannot group sales having different currencies for the same partner.'))
489
490             partner_currency[o.partner_id.id] = currency_id
491             lines = []
492             for line in o.order_line:
493                 if line.invoiced:
494                     continue
495                 elif (line.state in states):
496                     lines.append(line.id)
497             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
498             if created_lines:
499                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
500         if not invoices:
501             for o in self.browse(cr, uid, ids, context=context):
502                 for i in o.invoice_ids:
503                     if i.state == 'draft':
504                         return i.id
505         for val in invoices.values():
506             if grouped:
507                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
508                 invoice_ref = ''
509                 for o, l in val:
510                     invoice_ref += o.name + '|'
511                     self.write(cr, uid, [o.id], {'state': 'progress'})
512                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
513                 #remove last '|' in invoice_ref
514                 if len(invoice_ref) >= 1: 
515                     invoice_ref = invoice_ref[:-1]
516                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
517             else:
518                 for order, il in val:
519                     res = self._make_invoice(cr, uid, order, il, context=context)
520                     invoice_ids.append(res)
521                     self.write(cr, uid, [order.id], {'state': 'progress'})
522                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
523         return res
524
525     def action_invoice_cancel(self, cr, uid, ids, context=None):
526         self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
527         return True
528
529     def action_invoice_end(self, cr, uid, ids, context=None):
530         for this in self.browse(cr, uid, ids, context=context):
531             for line in this.order_line:
532                 if line.state == 'exception':
533                     line.write({'state': 'confirmed'})
534             if this.state == 'invoice_except':
535                 this.write({'state': 'progress'})
536         return True
537
538     def action_cancel(self, cr, uid, ids, context=None):
539         if context is None:
540             context = {}
541         sale_order_line_obj = self.pool.get('sale.order.line')
542         account_invoice_obj = self.pool.get('account.invoice')
543         for sale in self.browse(cr, uid, ids, context=context):
544             for inv in sale.invoice_ids:
545                 if inv.state not in ('draft', 'cancel'):
546                     raise osv.except_osv(
547                         _('Cannot cancel this sales order!'),
548                         _('First cancel all invoices attached to this sales order.'))
549             for r in self.read(cr, uid, ids, ['invoice_ids']):
550                 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
551             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
552                     {'state': 'cancel'})
553         self.write(cr, uid, ids, {'state': 'cancel'})
554         return True
555
556     def action_button_confirm(self, cr, uid, ids, context=None):
557         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
558         self.signal_order_confirm(cr, uid, ids)
559
560         # redisplay the record as a sales order
561         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
562         view_id = view_ref and view_ref[1] or False,
563         return {
564             'type': 'ir.actions.act_window',
565             'name': _('Sales Order'),
566             'res_model': 'sale.order',
567             'res_id': ids[0],
568             'view_type': 'form',
569             'view_mode': 'form',
570             'view_id': view_id,
571             'target': 'current',
572             'nodestroy': True,
573         }
574
575     def action_wait(self, cr, uid, ids, context=None):
576         context = context or {}
577         for o in self.browse(cr, uid, ids):
578             if not o.order_line:
579                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
580             noprod = self.test_no_product(cr, uid, o, context)
581             if (o.order_policy == 'manual') or noprod:
582                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
583             else:
584                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
585             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
586         return True
587
588     def action_quotation_send(self, cr, uid, ids, context=None):
589         '''
590         This function opens a window to compose an email, with the edi sale template message loaded by default
591         '''
592         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
593         ir_model_data = self.pool.get('ir.model.data')
594         try:
595             template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
596         except ValueError:
597             template_id = False
598         try:
599             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
600         except ValueError:
601             compose_form_id = False 
602         ctx = dict(context)
603         ctx.update({
604             'default_model': 'sale.order',
605             'default_res_id': ids[0],
606             'default_use_template': bool(template_id),
607             'default_template_id': template_id,
608             'default_composition_mode': 'comment',
609             'mark_so_as_sent': True
610         })
611         return {
612             'type': 'ir.actions.act_window',
613             'view_type': 'form',
614             'view_mode': 'form',
615             'res_model': 'mail.compose.message',
616             'views': [(compose_form_id, 'form')],
617             'view_id': compose_form_id,
618             'target': 'new',
619             'context': ctx,
620         }
621
622     def action_done(self, cr, uid, ids, context=None):
623         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
624
625
626
627 # TODO add a field price_unit_uos
628 # - update it on change product and unit price
629 # - use it in report if there is a uos
630 class sale_order_line(osv.osv):
631
632     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
633         tax_obj = self.pool.get('account.tax')
634         cur_obj = self.pool.get('res.currency')
635         res = {}
636         if context is None:
637             context = {}
638         for line in self.browse(cr, uid, ids, context=context):
639             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
640             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
641             cur = line.order_id.pricelist_id.currency_id
642             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
643         return res
644
645     def _get_uom_id(self, cr, uid, *args):
646         try:
647             proxy = self.pool.get('ir.model.data')
648             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
649             return result[1]
650         except Exception, ex:
651             return False
652
653     def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
654         res = dict.fromkeys(ids, False)
655         for this in self.browse(cr, uid, ids, context=context):
656             res[this.id] = this.invoice_lines and \
657                 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) 
658         return res
659
660     def _order_lines_from_invoice(self, cr, uid, ids, context=None):
661         # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
662         cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
663                                                   sale_order_line sol ON (sol.order_id = rel.order_id)
664                                     WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
665         return [i[0] for i in cr.fetchall()]
666
667     _name = 'sale.order.line'
668     _description = 'Sales Order Line'
669     _columns = {
670         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
671         'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
672         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
673         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}),
674         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
675         'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
676             store={
677                 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
678                 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)}),
679         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
680         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
681          help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."),
682         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
683         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
684         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
685         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
686         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
687         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
688         'product_uos': fields.many2one('product.uom', 'Product UoS'),
689         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
690         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
691         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
692                 help='* The \'Draft\' status is set when the related sales order in draft status. \
693                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
694                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
695                     \n* The \'Done\' status is set when the sales order line has been picked. \
696                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
697         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
698         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
699         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
700     }
701     _order = 'order_id desc, sequence, id'
702     _defaults = {
703         'product_uom' : _get_uom_id,
704         'discount': 0.0,
705         'product_uom_qty': 1,
706         'product_uos_qty': 1,
707         'sequence': 10,
708         'state': 'draft',
709         'type': 'make_to_stock',
710         'price_unit': 0.0,
711     }
712
713     def _get_line_qty(self, cr, uid, line, context=None):
714         if (line.order_id.invoice_quantity=='order'):
715             if line.product_uos:
716                 return line.product_uos_qty or 0.0
717         return line.product_uom_qty
718
719     def _get_line_uom(self, cr, uid, line, context=None):
720         if (line.order_id.invoice_quantity=='order'):
721             if line.product_uos:
722                 return line.product_uos.id
723         return line.product_uom.id
724
725     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
726         """Prepare the dict of values to create the new invoice line for a
727            sales order line. This method may be overridden to implement custom
728            invoice generation (making sure to call super() to establish
729            a clean extension chain).
730
731            :param browse_record line: sale.order.line record to invoice
732            :param int account_id: optional ID of a G/L account to force
733                (this is used for returning products including service)
734            :return: dict of values to create() the invoice line
735         """
736         res = {}
737         if not line.invoiced:
738             if not account_id:
739                 if line.product_id:
740                     account_id = line.product_id.property_account_income.id
741                     if not account_id:
742                         account_id = line.product_id.categ_id.property_account_income_categ.id
743                     if not account_id:
744                         raise osv.except_osv(_('Error!'),
745                                 _('Please define income account for this product: "%s" (id:%d).') % \
746                                     (line.product_id.name, line.product_id.id,))
747                 else:
748                     prop = self.pool.get('ir.property').get(cr, uid,
749                             'property_account_income_categ', 'product.category',
750                             context=context)
751                     account_id = prop and prop.id or False
752             uosqty = self._get_line_qty(cr, uid, line, context=context)
753             uos_id = self._get_line_uom(cr, uid, line, context=context)
754             pu = 0.0
755             if uosqty:
756                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
757                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
758             fpos = line.order_id.fiscal_position or False
759             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
760             if not account_id:
761                 raise osv.except_osv(_('Error!'),
762                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
763             res = {
764                 'name': line.name,
765                 'sequence': line.sequence,
766                 'origin': line.order_id.name,
767                 'account_id': account_id,
768                 'price_unit': pu,
769                 'quantity': uosqty,
770                 'discount': line.discount,
771                 'uos_id': uos_id,
772                 'product_id': line.product_id.id or False,
773                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
774                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
775             }
776
777         return res
778
779     def invoice_line_create(self, cr, uid, ids, context=None):
780         if context is None:
781             context = {}
782
783         create_ids = []
784         sales = set()
785         for line in self.browse(cr, uid, ids, context=context):
786             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
787             if vals:
788                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
789                 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
790                 sales.add(line.order_id.id)
791                 create_ids.append(inv_id)
792         # Trigger workflow events
793         wf_service = netsvc.LocalService("workflow")
794         for sale_id in sales:
795             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
796         return create_ids
797
798     def button_cancel(self, cr, uid, ids, context=None):
799         for line in self.browse(cr, uid, ids, context=context):
800             if line.invoiced:
801                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
802         return self.write(cr, uid, ids, {'state': 'cancel'})
803
804     def button_confirm(self, cr, uid, ids, context=None):
805         return self.write(cr, uid, ids, {'state': 'confirmed'})
806
807     def button_done(self, cr, uid, ids, context=None):
808         wf_service = netsvc.LocalService("workflow")
809         res = self.write(cr, uid, ids, {'state': 'done'})
810         for line in self.browse(cr, uid, ids, context=context):
811             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
812         return res
813
814     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
815         product_obj = self.pool.get('product.product')
816         if not product_id:
817             return {'value': {'product_uom': product_uos,
818                 'product_uom_qty': product_uos_qty}, 'domain': {}}
819
820         product = product_obj.browse(cr, uid, product_id)
821         value = {
822             'product_uom': product.uom_id.id,
823         }
824         # FIXME must depend on uos/uom of the product and not only of the coeff.
825         try:
826             value.update({
827                 'product_uom_qty': product_uos_qty / product.uos_coeff,
828                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
829             })
830         except ZeroDivisionError:
831             pass
832         return {'value': value}
833
834     def copy_data(self, cr, uid, id, default=None, context=None):
835         if not default:
836             default = {}
837         default.update({'state': 'draft',  'invoice_lines': []})
838         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
839
840     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
841             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
842             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
843         context = context or {}
844         lang = lang or context.get('lang',False)
845         if not  partner_id:
846             raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
847         warning = {}
848         product_uom_obj = self.pool.get('product.uom')
849         partner_obj = self.pool.get('res.partner')
850         product_obj = self.pool.get('product.product')
851         context = {'lang': lang, 'partner_id': partner_id}
852         if partner_id:
853             lang = partner_obj.browse(cr, uid, partner_id).lang
854         context_partner = {'lang': lang, 'partner_id': partner_id}
855
856         if not product:
857             return {'value': {'th_weight': 0,
858                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
859                    'product_uos': []}}
860         if not date_order:
861             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
862
863         result = {}
864         warning_msgs = ''
865         product_obj = product_obj.browse(cr, uid, product, context=context_partner)
866
867         uom2 = False
868         if uom:
869             uom2 = product_uom_obj.browse(cr, uid, uom)
870             if product_obj.uom_id.category_id.id != uom2.category_id.id:
871                 uom = False
872         if uos:
873             if product_obj.uos_id:
874                 uos2 = product_uom_obj.browse(cr, uid, uos)
875                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
876                     uos = False
877             else:
878                 uos = False
879         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
880         if update_tax: #The quantity only have changed
881             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
882
883         if not flag:
884             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
885             if product_obj.description_sale:
886                 result['name'] += '\n'+product_obj.description_sale
887         domain = {}
888         if (not uom) and (not uos):
889             result['product_uom'] = product_obj.uom_id.id
890             if product_obj.uos_id:
891                 result['product_uos'] = product_obj.uos_id.id
892                 result['product_uos_qty'] = qty * product_obj.uos_coeff
893                 uos_category_id = product_obj.uos_id.category_id.id
894             else:
895                 result['product_uos'] = False
896                 result['product_uos_qty'] = qty
897                 uos_category_id = False
898             result['th_weight'] = qty * product_obj.weight
899             domain = {'product_uom':
900                         [('category_id', '=', product_obj.uom_id.category_id.id)],
901                         'product_uos':
902                         [('category_id', '=', uos_category_id)]}
903         elif uos and not uom: # only happens if uom is False
904             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
905             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
906             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
907         elif uom: # whether uos is set or not
908             default_uom = product_obj.uom_id and product_obj.uom_id.id
909             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
910             if product_obj.uos_id:
911                 result['product_uos'] = product_obj.uos_id.id
912                 result['product_uos_qty'] = qty * product_obj.uos_coeff
913             else:
914                 result['product_uos'] = False
915                 result['product_uos_qty'] = qty
916             result['th_weight'] = q * product_obj.weight        # Round the quantity up
917
918         if not uom2:
919             uom2 = product_obj.uom_id
920         # get unit price
921
922         if not pricelist:
923             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
924                     'Please set one before choosing a product.')
925             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
926         else:
927             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
928                     product, qty or 1.0, partner_id, {
929                         'uom': uom or result.get('product_uom'),
930                         'date': date_order,
931                         })[pricelist]
932             if price is False:
933                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
934                         "You have to change either the product, the quantity or the pricelist.")
935
936                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
937             else:
938                 result.update({'price_unit': price})
939         if warning_msgs:
940             warning = {
941                        'title': _('Configuration Error!'),
942                        'message' : warning_msgs
943                     }
944         return {'value': result, 'domain': domain, 'warning': warning}
945
946     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
947             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
948             lang=False, update_tax=True, date_order=False, context=None):
949         context = context or {}
950         lang = lang or ('lang' in context and context['lang'])
951         if not uom:
952             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
953         return self.product_id_change(cursor, user, ids, pricelist, product,
954                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
955                 partner_id=partner_id, lang=lang, update_tax=update_tax,
956                 date_order=date_order, context=context)
957
958     def unlink(self, cr, uid, ids, context=None):
959         if context is None:
960             context = {}
961         """Allows to delete sales order lines in draft,cancel states"""
962         for rec in self.browse(cr, uid, ids, context=context):
963             if rec.state not in ['draft', 'cancel']:
964                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
965         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
966
967 class res_company(osv.Model):
968     _inherit = "res.company"
969     _columns = {
970         'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
971     }
972
973
974 class mail_compose_message(osv.Model):
975     _inherit = 'mail.compose.message'
976
977     def send_mail(self, cr, uid, ids, context=None):
978         context = context or {}
979         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
980             context = dict(context, mail_post_autofollow=True)
981             self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
982         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
983
984
985 class account_invoice(osv.Model):
986     _inherit = 'account.invoice'
987
988     def confirm_paid(self, cr, uid, ids, context=None):
989         sale_order_obj = self.pool.get('sale.order')
990         res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
991         so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
992         if so_ids:
993             sale_order_obj.message_post(cr, uid, so_ids, body=_("Invoice paid"), context=context)
994         return res
995
996     def unlink(self, cr, uid, ids, context=None):
997         """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
998         invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
999         #if we can't cancel all invoices, do nothing
1000         if len(invoice_ids) == len(ids):
1001             #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1002             #it will trigger the workflow to put the sale order in an 'invoice exception' state
1003             wf_service = netsvc.LocalService("workflow")
1004             for id in ids:
1005                 wf_service.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1006         return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1007
1008 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: