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