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