[MERGE] Forward-port of latest 7.0 bugfixes, up to rev. 9929 revid:dle@openerp.com...
[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 workflow
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             'date_order': fields.date.context_today(self, cr, uid, context=context),
47             'state': 'draft',
48             'invoice_ids': [],
49             'date_confirm': False,
50             'client_order_ref': '',
51             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
52         })
53         return super(sale_order, self).copy(cr, uid, id, default, context=context)
54
55     def _amount_line_tax(self, cr, uid, line, context=None):
56         val = 0.0
57         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']:
58             val += c.get('amount', 0.0)
59         return val
60
61     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
62         cur_obj = self.pool.get('res.currency')
63         res = {}
64         for order in self.browse(cr, uid, ids, context=context):
65             res[order.id] = {
66                 'amount_untaxed': 0.0,
67                 'amount_tax': 0.0,
68                 'amount_total': 0.0,
69             }
70             val = val1 = 0.0
71             cur = order.pricelist_id.currency_id
72             for line in order.order_line:
73                 val1 += line.price_subtotal
74                 val += self._amount_line_tax(cr, uid, line, context=context)
75             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
76             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
77             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
78         return res
79
80
81     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
82         res = {}
83         for sale in self.browse(cursor, user, ids, context=context):
84             if sale.invoiced:
85                 res[sale.id] = 100.0
86                 continue
87             tot = 0.0
88             for invoice in sale.invoice_ids:
89                 if invoice.state not in ('draft', 'cancel'):
90                     tot += invoice.amount_untaxed
91             if tot:
92                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
93             else:
94                 res[sale.id] = 0.0
95         return res
96
97     def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
98         res = {}
99         for sale in self.browse(cursor, user, ids, context=context):
100             res[sale.id] = False
101             if sale.invoice_ids:
102                 res[sale.id] = True
103         return res
104
105     def _invoiced(self, cursor, user, ids, name, arg, context=None):
106         res = {}
107         for sale in self.browse(cursor, user, ids, context=context):
108             res[sale.id] = True
109             invoice_existence = False
110             for invoice in sale.invoice_ids:
111                 if invoice.state!='cancel':
112                     invoice_existence = True
113                     if invoice.state != 'paid':
114                         res[sale.id] = False
115                         break
116             if not invoice_existence or sale.state == 'manual':
117                 res[sale.id] = False
118         return res
119
120     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
121         if not len(args):
122             return []
123         clause = ''
124         sale_clause = ''
125         no_invoiced = False
126         for arg in args:
127             if arg[1] == '=':
128                 if arg[2]:
129                     clause += 'AND inv.state = \'paid\''
130                 else:
131                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
132                     sale_clause = ',  sale_order AS sale '
133                     no_invoiced = True
134
135         cursor.execute('SELECT rel.order_id ' \
136                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
137                 'WHERE rel.invoice_id = inv.id ' + clause)
138         res = cursor.fetchall()
139         if no_invoiced:
140             cursor.execute('SELECT sale.id ' \
141                     'FROM sale_order AS sale ' \
142                     'WHERE sale.id NOT IN ' \
143                         '(SELECT rel.order_id ' \
144                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
145             res.extend(cursor.fetchall())
146         if not res:
147             return [('id', '=', 0)]
148         return [('id', 'in', [x[0] for x in res])]
149
150     def _get_order(self, cr, uid, ids, context=None):
151         result = {}
152         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
153             result[line.order_id.id] = True
154         return result.keys()
155
156     def _get_default_company(self, cr, uid, context=None):
157         company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
158         if not company_id:
159             raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
160         return company_id
161
162     _columns = {
163         'name': fields.char('Order Reference', size=64, required=True,
164             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
165         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
166         'client_order_ref': fields.char('Customer Reference', size=64),
167         'state': fields.selection([
168             ('draft', 'Draft Quotation'),
169             ('sent', 'Quotation Sent'),
170             ('cancel', 'Cancelled'),
171             ('waiting_date', 'Waiting Schedule'),
172             ('progress', 'Sales Order'),
173             ('manual', 'Sale to Invoice'),
174             ('invoice_except', 'Invoice Exception'),
175             ('done', 'Done'),
176             ], 'Status', readonly=True, track_visibility='onchange',
177             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),
178         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
179         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
180         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
181         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
182         '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'),
183         '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."),
184         '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."),
185         'order_policy': fields.selection([
186                 ('manual', 'On Demand'),
187             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
188             help="""This field controls how invoice and delivery operations are synchronized."""),
189         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
190         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
191         '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."),
192
193         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
194         '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)."),
195         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
196         'invoiced': fields.function(_invoiced, string='Paid',
197             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
198         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
199             fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
200         'note': fields.text('Terms and conditions'),
201
202         'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
203             store={
204                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
205                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
206             },
207             multi='sums', help="The amount without tax.", track_visibility='always'),
208         'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Taxes',
209             store={
210                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
211                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
212             },
213             multi='sums', help="The tax amount."),
214         'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
215             store={
216                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
217                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
218             },
219             multi='sums', help="The total amount."),
220
221         '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)]}),
222         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
223         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
224         'company_id': fields.many2one('res.company', 'Company'),
225     }
226     _defaults = {
227         'date_order': fields.date.context_today,
228         'order_policy': 'manual',
229         'company_id': _get_default_company,
230         'state': 'draft',
231         'user_id': lambda obj, cr, uid, context: uid,
232         'name': lambda obj, cr, uid, context: '/',
233         'invoice_quantity': 'order',
234         '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'],
235         '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'],
236         'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note
237     }
238     _sql_constraints = [
239         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
240     ]
241     _order = 'date_order desc, id desc'
242
243     # Form filling
244     def unlink(self, cr, uid, ids, context=None):
245         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
246         unlink_ids = []
247         for s in sale_orders:
248             if s['state'] in ['draft', 'cancel']:
249                 unlink_ids.append(s['id'])
250             else:
251                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
252
253         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
254
255     def copy_quotation(self, cr, uid, ids, context=None):
256         id = self.copy(cr, uid, ids[0], context=None)
257         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
258         view_id = view_ref and view_ref[1] or False,
259         return {
260             'type': 'ir.actions.act_window',
261             'name': _('Sales Order'),
262             'res_model': 'sale.order',
263             'res_id': id,
264             'view_type': 'form',
265             'view_mode': 'form',
266             'view_id': view_id,
267             'target': 'current',
268             'nodestroy': True,
269         }
270
271     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
272         context = context or {}
273         if not pricelist_id:
274             return {}
275         value = {
276             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
277         }
278         if not order_lines:
279             return {'value': value}
280         warning = {
281             'title': _('Pricelist Warning!'),
282             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
283         }
284         return {'warning': warning, 'value': value}
285
286     def get_salenote(self, cr, uid, ids, partner_id, context=None):
287         context_lang = context.copy() 
288         if partner_id:
289             partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
290             context_lang.update({'lang': partner_lang})
291         return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
292             
293     def onchange_partner_id(self, cr, uid, ids, part, context=None):
294         if not part:
295             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
296
297         part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
298         addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
299         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
300         payment_term = part.property_payment_term and part.property_payment_term.id or False
301         fiscal_position = part.property_account_position and part.property_account_position.id or False
302         dedicated_salesman = part.user_id and part.user_id.id or uid
303         val = {
304             'partner_invoice_id': addr['invoice'],
305             'partner_shipping_id': addr['delivery'],
306             'payment_term': payment_term,
307             'fiscal_position': fiscal_position,
308             'user_id': dedicated_salesman,
309         }
310         if pricelist:
311             val['pricelist_id'] = pricelist
312         sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
313         if sale_note: val.update({'note': sale_note})  
314         return {'value': val}
315
316     def create(self, cr, uid, vals, context=None):
317         if context is None:
318             context = {}
319         if vals.get('name', '/') == '/':
320             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
321         if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
322             defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
323             vals = dict(defaults, **vals)
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_invoice_id.id or 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         for sale_id in sales:
799             workflow.trg_write(uid, 'sale.order', sale_id, cr)
800         return create_ids
801
802     def button_cancel(self, cr, uid, ids, context=None):
803         for line in self.browse(cr, uid, ids, context=context):
804             if line.invoiced:
805                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
806         return self.write(cr, uid, ids, {'state': 'cancel'})
807
808     def button_confirm(self, cr, uid, ids, context=None):
809         return self.write(cr, uid, ids, {'state': 'confirmed'})
810
811     def button_done(self, cr, uid, ids, context=None):
812         res = self.write(cr, uid, ids, {'state': 'done'})
813         for line in self.browse(cr, uid, ids, context=context):
814             workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
815         return res
816
817     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
818         product_obj = self.pool.get('product.product')
819         if not product_id:
820             return {'value': {'product_uom': product_uos,
821                 'product_uom_qty': product_uos_qty}, 'domain': {}}
822
823         product = product_obj.browse(cr, uid, product_id)
824         value = {
825             'product_uom': product.uom_id.id,
826         }
827         # FIXME must depend on uos/uom of the product and not only of the coeff.
828         try:
829             value.update({
830                 'product_uom_qty': product_uos_qty / product.uos_coeff,
831                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
832             })
833         except ZeroDivisionError:
834             pass
835         return {'value': value}
836
837     def create(self, cr, uid, values, context=None):
838         if values.get('order_id') and values.get('product_id') and  any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
839             order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
840             defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
841                 qty=float(values.get('product_uom_qty', False)),
842                 uom=values.get('product_uom', False),
843                 qty_uos=float(values.get('product_uos_qty', False)),
844                 uos=values.get('product_uos', False),
845                 name=values.get('name', False),
846                 partner_id=order['partner_id'][0],
847                 date_order=order['date_order'],
848                 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
849                 flag=False,  # Force name update
850                 context=context
851             )['value']
852             values = dict(defaults, **values)
853         return super(sale_order_line, self).create(cr, uid, values, context=context)
854
855     def copy_data(self, cr, uid, id, default=None, context=None):
856         if not default:
857             default = {}
858         default.update({'state': 'draft',  'invoice_lines': []})
859         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
860
861     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
862             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
863             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
864         context = context or {}
865         lang = lang or context.get('lang',False)
866         if not  partner_id:
867             raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
868         warning = {}
869         product_uom_obj = self.pool.get('product.uom')
870         partner_obj = self.pool.get('res.partner')
871         product_obj = self.pool.get('product.product')
872         context = {'lang': lang, 'partner_id': partner_id}
873         if partner_id:
874             lang = partner_obj.browse(cr, uid, partner_id).lang
875         context_partner = {'lang': lang, 'partner_id': partner_id}
876
877         if not product:
878             return {'value': {'th_weight': 0,
879                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
880                    'product_uos': []}}
881         if not date_order:
882             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
883
884         result = {}
885         warning_msgs = ''
886         product_obj = product_obj.browse(cr, uid, product, context=context_partner)
887
888         uom2 = False
889         if uom:
890             uom2 = product_uom_obj.browse(cr, uid, uom)
891             if product_obj.uom_id.category_id.id != uom2.category_id.id:
892                 uom = False
893         if uos:
894             if product_obj.uos_id:
895                 uos2 = product_uom_obj.browse(cr, uid, uos)
896                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
897                     uos = False
898             else:
899                 uos = False
900         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
901         if update_tax: #The quantity only have changed
902             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
903
904         if not flag:
905             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
906             if product_obj.description_sale:
907                 result['name'] += '\n'+product_obj.description_sale
908         domain = {}
909         if (not uom) and (not uos):
910             result['product_uom'] = product_obj.uom_id.id
911             if product_obj.uos_id:
912                 result['product_uos'] = product_obj.uos_id.id
913                 result['product_uos_qty'] = qty * product_obj.uos_coeff
914                 uos_category_id = product_obj.uos_id.category_id.id
915             else:
916                 result['product_uos'] = False
917                 result['product_uos_qty'] = qty
918                 uos_category_id = False
919             result['th_weight'] = qty * product_obj.weight
920             domain = {'product_uom':
921                         [('category_id', '=', product_obj.uom_id.category_id.id)],
922                         'product_uos':
923                         [('category_id', '=', uos_category_id)]}
924         elif uos and not uom: # only happens if uom is False
925             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
926             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
927             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
928         elif uom: # whether uos is set or not
929             default_uom = product_obj.uom_id and product_obj.uom_id.id
930             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
931             if product_obj.uos_id:
932                 result['product_uos'] = product_obj.uos_id.id
933                 result['product_uos_qty'] = qty * product_obj.uos_coeff
934             else:
935                 result['product_uos'] = False
936                 result['product_uos_qty'] = qty
937             result['th_weight'] = q * product_obj.weight        # Round the quantity up
938
939         if not uom2:
940             uom2 = product_obj.uom_id
941         # get unit price
942
943         if not pricelist:
944             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
945                     'Please set one before choosing a product.')
946             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
947         else:
948             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
949                     product, qty or 1.0, partner_id, {
950                         'uom': uom or result.get('product_uom'),
951                         'date': date_order,
952                         })[pricelist]
953             if price is False:
954                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
955                         "You have to change either the product, the quantity or the pricelist.")
956
957                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
958             else:
959                 result.update({'price_unit': price})
960         if warning_msgs:
961             warning = {
962                        'title': _('Configuration Error!'),
963                        'message' : warning_msgs
964                     }
965         return {'value': result, 'domain': domain, 'warning': warning}
966
967     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
968             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
969             lang=False, update_tax=True, date_order=False, context=None):
970         context = context or {}
971         lang = lang or ('lang' in context and context['lang'])
972         if not uom:
973             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
974         return self.product_id_change(cursor, user, ids, pricelist, product,
975                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
976                 partner_id=partner_id, lang=lang, update_tax=update_tax,
977                 date_order=date_order, context=context)
978
979     def unlink(self, cr, uid, ids, context=None):
980         if context is None:
981             context = {}
982         """Allows to delete sales order lines in draft,cancel states"""
983         for rec in self.browse(cr, uid, ids, context=context):
984             if rec.state not in ['draft', 'cancel']:
985                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
986         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
987
988 class res_company(osv.Model):
989     _inherit = "res.company"
990     _columns = {
991         'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
992     }
993
994
995 class mail_compose_message(osv.Model):
996     _inherit = 'mail.compose.message'
997
998     def send_mail(self, cr, uid, ids, context=None):
999         context = context or {}
1000         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1001             context = dict(context, mail_post_autofollow=True)
1002             self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1003         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1004
1005
1006 class account_invoice(osv.Model):
1007     _inherit = 'account.invoice'
1008
1009     def confirm_paid(self, cr, uid, ids, context=None):
1010         sale_order_obj = self.pool.get('sale.order')
1011         res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1012         so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1013         for so_id in so_ids:
1014             sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1015         return res
1016
1017     def unlink(self, cr, uid, ids, context=None):
1018         """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1019         invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1020         #if we can't cancel all invoices, do nothing
1021         if len(invoice_ids) == len(ids):
1022             #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1023             #it will trigger the workflow to put the sale order in an 'invoice exception' state
1024             for id in ids:
1025                 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1026         return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1027
1028 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: