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