[MERGE] forward port of branch saas-4 up to fa07bc8
[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 import time
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
27 import openerp.addons.decimal_precision as dp
28 from openerp import workflow
29
30 class sale_order(osv.osv):
31     _name = "sale.order"
32     _inherit = ['mail.thread', 'ir.needaction_mixin']
33     _description = "Sales Order"
34     _track = {
35         'state': {
36             'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
37             'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
38         },
39     }
40
41     def copy(self, cr, uid, id, default=None, context=None):
42         if not default:
43             default = {}
44         default.update({
45             'date_order': fields.datetime.now(),
46             'state': 'draft',
47             'invoice_ids': [],
48             'date_confirm': False,
49             'client_order_ref': '',
50             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
51             'procurement_group_id': False,
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] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
132                 clause += 'AND inv.state = \'paid\''
133             else:
134                 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
135                 sale_clause = ',  sale_order AS sale '
136                 no_invoiced = True
137
138         cursor.execute('SELECT rel.order_id ' \
139                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
140                 'WHERE rel.invoice_id = inv.id ' + clause)
141         res = cursor.fetchall()
142         if no_invoiced:
143             cursor.execute('SELECT sale.id ' \
144                     'FROM sale_order AS sale ' \
145                     'WHERE sale.id NOT IN ' \
146                         '(SELECT rel.order_id ' \
147                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
148             res.extend(cursor.fetchall())
149         if not res:
150             return [('id', '=', 0)]
151         return [('id', 'in', [x[0] for x in res])]
152
153     def _get_order(self, cr, uid, ids, context=None):
154         result = {}
155         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
156             result[line.order_id.id] = True
157         return result.keys()
158
159     def _get_default_company(self, cr, uid, context=None):
160         company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
161         if not company_id:
162             raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
163         return company_id
164
165     def _get_default_section_id(self, cr, uid, context=None):
166         """ Gives default section by checking if present in the context """
167         section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
168         if not section_id:
169             section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
170         return section_id
171
172     def _resolve_section_id_from_context(self, cr, uid, context=None):
173         """ Returns ID of section based on the value of 'section_id'
174             context key, or None if it cannot be resolved to a single
175             Sales Team.
176         """
177         if context is None:
178             context = {}
179         if type(context.get('default_section_id')) in (int, long):
180             return context.get('default_section_id')
181         if isinstance(context.get('default_section_id'), basestring):
182             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
183             if len(section_ids) == 1:
184                 return int(section_ids[0][0])
185         return None
186
187     _columns = {
188         'name': fields.char('Order Reference', size=64, required=True,
189             readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
190         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
191         'client_order_ref': fields.char('Reference/Description', size=64),
192         'state': fields.selection([
193             ('draft', 'Draft Quotation'),
194             ('sent', 'Quotation Sent'),
195             ('cancel', 'Cancelled'),
196             ('waiting_date', 'Waiting Schedule'),
197             ('progress', 'Sales Order'),
198             ('manual', 'Sale to Invoice'),
199             ('shipping_except', 'Shipping Exception'),
200             ('invoice_except', 'Invoice Exception'),
201             ('done', 'Done'),
202             ], 'Status', readonly=True, help="Gives the status of the quotation or sales order.\
203               \nThe exception status is automatically set when a cancel operation occurs \
204               in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
205                but waiting for the scheduler to run on the order date.", select=True),
206         'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
207         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
208         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
209         'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
210         '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'),
211         '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."),
212         '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."),
213         'order_policy': fields.selection([
214                 ('manual', 'On Demand'),
215             ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
216             help="""This field controls how invoice and delivery operations are synchronized."""),
217         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
218         'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
219         '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."),
220
221         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
222         '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)."),
223         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
224         'invoiced': fields.function(_invoiced, string='Paid',
225             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
226         'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
227             fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
228         'note': fields.text('Terms and conditions'),
229
230         'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
231             store={
232                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
233                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
234             },
235             multi='sums', help="The amount without tax.", track_visibility='always'),
236         'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
237             store={
238                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
239                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
240             },
241             multi='sums', help="The tax amount."),
242         'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
243             store={
244                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
245                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
246             },
247             multi='sums', help="The total amount."),
248
249         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
250         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
251         'company_id': fields.many2one('res.company', 'Company'),
252         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
253         'procurement_group_id': fields.many2one('procurement.group', 'Procurement group'),
254
255     }
256     _defaults = {
257         'date_order': fields.datetime.now,
258         'order_policy': 'manual',
259         'company_id': _get_default_company,
260         'state': 'draft',
261         'user_id': lambda obj, cr, uid, context: uid,
262         'name': lambda obj, cr, uid, context: '/',
263         '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'],
264         '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'],
265         'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
266         'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
267     }
268     _sql_constraints = [
269         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
270     ]
271     _order = 'date_order desc, id desc'
272
273     # Form filling
274     def unlink(self, cr, uid, ids, context=None):
275         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
276         unlink_ids = []
277         for s in sale_orders:
278             if s['state'] in ['draft', 'cancel']:
279                 unlink_ids.append(s['id'])
280             else:
281                 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
282
283         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
284
285     def copy_quotation(self, cr, uid, ids, context=None):
286         id = self.copy(cr, uid, ids[0], context=context)
287         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
288         view_id = view_ref and view_ref[1] or False,
289         return {
290             'type': 'ir.actions.act_window',
291             'name': _('Sales Order'),
292             'res_model': 'sale.order',
293             'res_id': id,
294             'view_type': 'form',
295             'view_mode': 'form',
296             'view_id': view_id,
297             'target': 'current',
298             'nodestroy': True,
299         }
300
301     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
302         context = context or {}
303         if not pricelist_id:
304             return {}
305         value = {
306             'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
307         }
308         if not order_lines:
309             return {'value': value}
310         warning = {
311             'title': _('Pricelist Warning!'),
312             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
313         }
314         return {'warning': warning, 'value': value}
315
316     def get_salenote(self, cr, uid, ids, partner_id, context=None):
317         context_lang = context.copy() 
318         if partner_id:
319             partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
320             context_lang.update({'lang': partner_lang})
321         return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
322
323     def onchange_partner_id(self, cr, uid, ids, part, context=None):
324         if not part:
325             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False,  'payment_term': False, 'fiscal_position': False}}
326
327         part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
328         addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
329         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
330         payment_term = part.property_payment_term and part.property_payment_term.id or False
331         fiscal_position = part.property_account_position and part.property_account_position.id or False
332         dedicated_salesman = part.user_id and part.user_id.id or uid
333         val = {
334             'partner_invoice_id': addr['invoice'],
335             'partner_shipping_id': addr['delivery'],
336             'payment_term': payment_term,
337             'fiscal_position': fiscal_position,
338             'user_id': dedicated_salesman,
339         }
340         if pricelist:
341             val['pricelist_id'] = pricelist
342         sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
343         if sale_note: val.update({'note': sale_note})  
344         return {'value': val}
345
346     def create(self, cr, uid, vals, context=None):
347         if context is None:
348             context = {}        
349         if vals.get('name', '/') == '/':
350             vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
351         if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
352             defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value']
353             vals = dict(defaults, **vals)
354         context.update({'mail_create_nolog': True})
355         new_id = super(sale_order, self).create(cr, uid, vals, context=context)
356         self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=context)
357         return new_id
358
359     def button_dummy(self, cr, uid, ids, context=None):
360         return True
361
362     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
363     #        can be removed after 6.1.
364     def _inv_get(self, cr, uid, order, context=None):
365         return {}
366
367     def _prepare_invoice(self, cr, uid, order, lines, context=None):
368         """Prepare the dict of values to create the new invoice for a
369            sales order. This method may be overridden to implement custom
370            invoice generation (making sure to call super() to establish
371            a clean extension chain).
372
373            :param browse_record order: sale.order record to invoice
374            :param list(int) line: list of invoice line IDs that must be
375                                   attached to the invoice
376            :return: dict of value to create() the invoice
377         """
378         if context is None:
379             context = {}
380         journal_ids = self.pool.get('account.journal').search(cr, uid,
381             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
382             limit=1)
383         if not journal_ids:
384             raise osv.except_osv(_('Error!'),
385                 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
386         invoice_vals = {
387             'name': order.client_order_ref or '',
388             'origin': order.name,
389             'type': 'out_invoice',
390             'reference': order.client_order_ref or order.name,
391             'account_id': order.partner_id.property_account_receivable.id,
392             'partner_id': order.partner_invoice_id.id,
393             'journal_id': journal_ids[0],
394             'invoice_line': [(6, 0, lines)],
395             'currency_id': order.pricelist_id.currency_id.id,
396             'comment': order.note,
397             'payment_term': order.payment_term and order.payment_term.id or False,
398             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
399             'date_invoice': context.get('date_invoice', False),
400             'company_id': order.company_id.id,
401             'user_id': order.user_id and order.user_id.id or False,
402             'section_id' : order.section_id.id
403         }
404
405         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
406         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
407         return invoice_vals
408
409     def _make_invoice(self, cr, uid, order, lines, context=None):
410         inv_obj = self.pool.get('account.invoice')
411         obj_invoice_line = self.pool.get('account.invoice.line')
412         if context is None:
413             context = {}
414         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
415         from_line_invoice_ids = []
416         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
417             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
418                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
419                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
420         for preinv in order.invoice_ids:
421             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
422                 for preline in preinv.invoice_line:
423                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
424                     lines.append(inv_line_id)
425         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
426         inv_id = inv_obj.create(cr, uid, inv, context=context)
427         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
428         if data.get('value', False):
429             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
430         inv_obj.button_compute(cr, uid, [inv_id])
431         return inv_id
432
433     def print_quotation(self, cr, uid, ids, context=None):
434         '''
435         This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
436         '''
437         assert len(ids) == 1, 'This option should only be used for a single id at a time'
438         self.signal_quotation_sent(cr, uid, ids)
439         return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
440
441     def manual_invoice(self, cr, uid, ids, context=None):
442         """ create invoices for the given sales orders (ids), and open the form
443             view of one of the newly created invoices
444         """
445         mod_obj = self.pool.get('ir.model.data')
446         
447         # create invoices through the sales orders' workflow
448         inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
449         self.signal_manual_invoice(cr, uid, ids)
450         inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
451         # determine newly created invoices
452         new_inv_ids = list(inv_ids1 - inv_ids0)
453
454         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
455         res_id = res and res[1] or False,
456
457         return {
458             'name': _('Customer Invoices'),
459             'view_type': 'form',
460             'view_mode': 'form',
461             'view_id': [res_id],
462             'res_model': 'account.invoice',
463             'context': "{'type':'out_invoice'}",
464             'type': 'ir.actions.act_window',
465             'nodestroy': True,
466             'target': 'current',
467             'res_id': new_inv_ids and new_inv_ids[0] or False,
468         }
469
470     def action_view_invoice(self, cr, uid, ids, context=None):
471         '''
472         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.
473         '''
474         mod_obj = self.pool.get('ir.model.data')
475         act_obj = self.pool.get('ir.actions.act_window')
476
477         result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
478         id = result and result[1] or False
479         result = act_obj.read(cr, uid, [id], context=context)[0]
480         #compute the number of invoices to display
481         inv_ids = []
482         for so in self.browse(cr, uid, ids, context=context):
483             inv_ids += [invoice.id for invoice in so.invoice_ids]
484         #choose the view_mode accordingly
485         if len(inv_ids)>1:
486             result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
487         else:
488             res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
489             result['views'] = [(res and res[1] or False, 'form')]
490             result['res_id'] = inv_ids and inv_ids[0] or False
491         return result
492
493     def test_no_product(self, cr, uid, order, context):
494         for line in order.order_line:
495             if line.product_id and (line.product_id.type<>'service'):
496                 return False
497         return True
498
499     def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
500         if states is None:
501             states = ['confirmed', 'done', 'exception']
502         res = False
503         invoices = {}
504         invoice_ids = []
505         invoice = self.pool.get('account.invoice')
506         obj_sale_order_line = self.pool.get('sale.order.line')
507         partner_currency = {}
508         if context is None:
509             context = {}
510         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
511         # last day of the last month as invoice date
512         if date_invoice:
513             context['date_invoice'] = date_invoice
514         for o in self.browse(cr, uid, ids, context=context):
515             currency_id = o.pricelist_id.currency_id.id
516             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
517                 raise osv.except_osv(
518                     _('Error!'),
519                     _('You cannot group sales having different currencies for the same partner.'))
520
521             partner_currency[o.partner_id.id] = currency_id
522             lines = []
523             for line in o.order_line:
524                 if line.invoiced:
525                     continue
526                 elif (line.state in states):
527                     lines.append(line.id)
528             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
529             if created_lines:
530                 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
531         if not invoices:
532             for o in self.browse(cr, uid, ids, context=context):
533                 for i in o.invoice_ids:
534                     if i.state == 'draft':
535                         return i.id
536         for val in invoices.values():
537             if grouped:
538                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
539                 invoice_ref = ''
540                 origin_ref = ''
541                 for o, l in val:
542                     invoice_ref += (o.client_order_ref or o.name) + '|'
543                     origin_ref += (o.origin or o.name) + '|'
544                     self.write(cr, uid, [o.id], {'state': 'progress'})
545                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
546                 #remove last '|' in invoice_ref
547                 if len(invoice_ref) >= 1:
548                     invoice_ref = invoice_ref[:-1]
549                 if len(origin_ref) >= 1:
550                     origin_ref = origin_ref[:-1]
551                 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
552             else:
553                 for order, il in val:
554                     res = self._make_invoice(cr, uid, order, il, context=context)
555                     invoice_ids.append(res)
556                     self.write(cr, uid, [order.id], {'state': 'progress'})
557                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
558         return res
559
560     def action_invoice_cancel(self, cr, uid, ids, context=None):
561         self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
562         return True
563
564     def action_invoice_end(self, cr, uid, ids, context=None):
565         for this in self.browse(cr, uid, ids, context=context):
566             for line in this.order_line:
567                 if line.state == 'exception':
568                     line.write({'state': 'confirmed'})
569             if this.state == 'invoice_except':
570                 this.write({'state': 'progress'})
571         return True
572
573     def action_cancel(self, cr, uid, ids, context=None):
574         if context is None:
575             context = {}
576         sale_order_line_obj = self.pool.get('sale.order.line')
577         account_invoice_obj = self.pool.get('account.invoice')
578         for sale in self.browse(cr, uid, ids, context=context):
579             for inv in sale.invoice_ids:
580                 if inv.state not in ('draft', 'cancel'):
581                     raise osv.except_osv(
582                         _('Cannot cancel this sales order!'),
583                         _('First cancel all invoices attached to this sales order.'))
584             for r in self.read(cr, uid, ids, ['invoice_ids']):
585                 account_invoice_obj.signal_invoice_cancel(cr, uid, r['invoice_ids'])
586             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
587                     {'state': 'cancel'})
588         self.write(cr, uid, ids, {'state': 'cancel'})
589         return True
590
591     def action_button_confirm(self, cr, uid, ids, context=None):
592         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
593         self.signal_order_confirm(cr, uid, ids)
594
595         # redisplay the record as a sales order
596         view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
597         view_id = view_ref and view_ref[1] or False,
598         return {
599             'type': 'ir.actions.act_window',
600             'name': _('Sales Order'),
601             'res_model': 'sale.order',
602             'res_id': ids[0],
603             'view_type': 'form',
604             'view_mode': 'form',
605             'view_id': view_id,
606             'target': 'current',
607             'nodestroy': True,
608         }
609
610     def action_wait(self, cr, uid, ids, context=None):
611         context = context or {}
612         for o in self.browse(cr, uid, ids):
613             if not o.order_line:
614                 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
615             noprod = self.test_no_product(cr, uid, o, context)
616             if (o.order_policy == 'manual') or noprod:
617                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
618             else:
619                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
620             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
621         return True
622
623     def action_quotation_send(self, cr, uid, ids, context=None):
624         '''
625         This function opens a window to compose an email, with the edi sale template message loaded by default
626         '''
627         assert len(ids) == 1, 'This option should only be used for a single id at a time.'
628         ir_model_data = self.pool.get('ir.model.data')
629         try:
630             template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
631         except ValueError:
632             template_id = False
633         try:
634             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
635         except ValueError:
636             compose_form_id = False 
637         ctx = dict()
638         ctx.update({
639             'default_model': 'sale.order',
640             'default_res_id': ids[0],
641             'default_use_template': bool(template_id),
642             'default_template_id': template_id,
643             'default_composition_mode': 'comment',
644             'mark_so_as_sent': True
645         })
646         return {
647             'type': 'ir.actions.act_window',
648             'view_type': 'form',
649             'view_mode': 'form',
650             'res_model': 'mail.compose.message',
651             'views': [(compose_form_id, 'form')],
652             'view_id': compose_form_id,
653             'target': 'new',
654             'context': ctx,
655         }
656
657     def action_done(self, cr, uid, ids, context=None):
658         for order in self.browse(cr, uid, ids, context=context):
659             self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
660         return self.write(cr, uid, ids, {'state': 'done'}, context=context)
661
662     def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
663         date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
664         return {
665             'name': line.name,
666             'origin': order.name,
667             'date_planned': date_planned,
668             'product_id': line.product_id.id,
669             'product_qty': line.product_uom_qty,
670             'product_uom': line.product_uom.id,
671             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
672             'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
673             'company_id': order.company_id.id,
674             'group_id': group_id,
675             'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
676             'sale_line_id': line.id
677         }
678
679     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
680         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
681         return date_planned
682
683     def _prepare_procurement_group(self, cr, uid, order, context=None):
684         return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
685
686     def procurement_needed(self, cr, uid, ids, context=None):
687         #when sale is installed only, there is no need to create procurements, that's only
688         #further installed modules (project_mrp, sale_stock) that will change this.
689         sale_line_obj = self.pool.get('sale.order.line')
690         res = []
691         for order in self.browse(cr, uid, ids, context=context):
692             res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
693         return any(res)
694
695     def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
696         for sale_order in self.browse(cr, uid, ids, context=context):
697             self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
698         return True
699
700     def action_ship_create(self, cr, uid, ids, context=None):
701         """Create the required procurements to supply sales order lines, also connecting
702         the procurements to appropriate stock moves in order to bring the goods to the
703         sales order's requested location.
704
705         :return: True
706         """
707         procurement_obj = self.pool.get('procurement.order')
708         sale_line_obj = self.pool.get('sale.order.line')
709         for order in self.browse(cr, uid, ids, context=context):
710             proc_ids = []
711             vals = self._prepare_procurement_group(cr, uid, order, context=context)
712             if not order.procurement_group_id:
713                 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
714                 order.write({'procurement_group_id': group_id}, context=context)
715
716             for line in order.order_line:
717                 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
718                 if line.procurement_ids:
719                     #first check them to see if they are in exception or not (one of the related moves is cancelled)
720                     procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
721                     line.refresh()
722                     #run again procurement that are in exception in order to trigger another move
723                     proc_ids += [x.id for x in line.procurement_ids if x.state == 'exception']
724                 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
725                     if (line.state == 'done') or not line.product_id:
726                         continue
727                     vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
728                     proc_id = procurement_obj.create(cr, uid, vals, context=context)
729                     proc_ids.append(proc_id)
730             #Confirm procurement order such that rules will be applied on it
731             #note that the workflow normally ensure proc_ids isn't an empty list
732             procurement_obj.run(cr, uid, proc_ids, context=context)
733
734             #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
735             if order.state == 'shipping_except':
736                 val = {'state': 'progress', 'shipped': False}
737
738                 if (order.order_policy == 'manual'):
739                     for line in order.order_line:
740                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
741                             val['state'] = 'manual'
742                             break
743                 order.write(val)
744         return True
745
746     # if mode == 'finished':
747     #   returns True if all lines are done, False otherwise
748     # if mode == 'canceled':
749     #   returns True if there is at least one canceled line, False otherwise
750     def test_state(self, cr, uid, ids, mode, *args):
751         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
752         finished = True
753         canceled = False
754         write_done_ids = []
755         write_cancel_ids = []
756         for order in self.browse(cr, uid, ids, context={}):
757
758             #TODO: Need to rethink what happens when cancelling
759             for line in order.order_line:
760                 states =  [x.state for x in line.procurement_ids]
761                 cancel = states and all([x == 'cancel' for x in states])
762                 doneorcancel = all([x in ('done', 'cancel') for x in states])
763                 if cancel:
764                     canceled = True
765                     if line.state != 'exception':
766                             write_cancel_ids.append(line.id)
767                 if not doneorcancel:
768                     finished = False 
769                 if doneorcancel and not cancel:
770                     write_done_ids.append(line.id)
771
772         if write_done_ids:
773             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
774         if write_cancel_ids:
775             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
776             
777         if mode == 'finished':
778             return finished
779         elif mode == 'canceled':
780             return canceled
781
782
783     def procurement_lines_get(self, cr, uid, ids, *args):
784         res = []
785         for order in self.browse(cr, uid, ids, context={}):
786             for line in order.order_line:
787                 res += [x.id for x in line.procurement_ids]
788         return res
789
790     def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
791         '''Update taxes of order lines for each line where a product is defined
792
793         :param list ids: not used
794         :param int fiscal_position: sale order fiscal position
795         :param list order_lines: command list for one2many write method
796         '''
797         order_line = []
798         fiscal_obj = self.pool.get('account.fiscal.position')
799         product_obj = self.pool.get('product.product')
800         line_obj = self.pool.get('sale.order.line')
801
802         fpos = False
803         if fiscal_position:
804             fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
805         
806         for line in order_lines:
807             # create    (0, 0,  { fields })
808             # update    (1, ID, { fields })
809             if line[0] in [0, 1]:
810                 prod = None
811                 if line[2].get('product_id'):
812                     prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
813                 elif line[1]:
814                     prod =  line_obj.browse(cr, uid, line[1], context=context).product_id
815                 if prod and prod.taxes_id:
816                     line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
817                 order_line.append(line)
818
819             # link      (4, ID)
820             # link all  (6, 0, IDS)
821             elif line[0] in [4, 6]:
822                 line_ids = line[0] == 4 and [line[1]] or line[2]
823                 for line_id in line_ids:
824                     prod = line_obj.browse(cr, uid, line_id, context=context).product_id
825                     if prod and prod.taxes_id:
826                         order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
827                     else:
828                         order_line.append([4, line_id])
829             else:
830                 order_line.append(line)
831         return {'value': {'order_line': order_line}}
832
833
834 # TODO add a field price_unit_uos
835 # - update it on change product and unit price
836 # - use it in report if there is a uos
837 class sale_order_line(osv.osv):
838
839     def need_procurement(self, cr, uid, ids, context=None):
840         #when sale is installed only, there is no need to create procurements, that's only
841         #further installed modules (project_mrp, sale_stock) that will change this.
842         return False
843
844     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
845         tax_obj = self.pool.get('account.tax')
846         cur_obj = self.pool.get('res.currency')
847         res = {}
848         if context is None:
849             context = {}
850         for line in self.browse(cr, uid, ids, context=context):
851             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
852             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
853             cur = line.order_id.pricelist_id.currency_id
854             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
855         return res
856
857     def _get_uom_id(self, cr, uid, *args):
858         try:
859             proxy = self.pool.get('ir.model.data')
860             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
861             return result[1]
862         except Exception, ex:
863             return False
864
865     def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
866         res = dict.fromkeys(ids, False)
867         for this in self.browse(cr, uid, ids, context=context):
868             res[this.id] = this.invoice_lines and \
869                 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines) 
870         return res
871
872     def _order_lines_from_invoice(self, cr, uid, ids, context=None):
873         # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
874         cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
875                                                   sale_order_line sol ON (sol.order_id = rel.order_id)
876                                     WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
877         return [i[0] for i in cr.fetchall()]
878
879
880     _name = 'sale.order.line'
881     _description = 'Sales Order Line'
882     _columns = {
883         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
884         'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
885         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
886         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
887         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
888         'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
889             store={
890                 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
891                 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
892             }),
893         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
894         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
895         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
896         'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
897         'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
898         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
899         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
900         'product_uos': fields.many2one('product.uom', 'Product UoS'),
901         'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
902         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
903         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
904                 help='* The \'Draft\' status is set when the related sales order in draft status. \
905                     \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
906                     \n* The \'Exception\' status is set when the related sales order is set as exception. \
907                     \n* The \'Done\' status is set when the sales order line has been picked. \
908                     \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
909         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
910         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
911         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
912         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
913         'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
914     }
915     _order = 'order_id desc, sequence, id'
916     _defaults = {
917         'product_uom' : _get_uom_id,
918         'discount': 0.0,
919         'product_uom_qty': 1,
920         'product_uos_qty': 1,
921         'sequence': 10,
922         'state': 'draft',
923         'price_unit': 0.0,
924         'delay': 0.0,
925     }
926
927     def _get_line_qty(self, cr, uid, line, context=None):
928         if line.product_uos:
929             return line.product_uos_qty or 0.0
930         return line.product_uom_qty
931
932     def _get_line_uom(self, cr, uid, line, context=None):
933         if line.product_uos:
934             return line.product_uos.id
935         return line.product_uom.id
936
937     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
938         """Prepare the dict of values to create the new invoice line for a
939            sales order line. This method may be overridden to implement custom
940            invoice generation (making sure to call super() to establish
941            a clean extension chain).
942
943            :param browse_record line: sale.order.line record to invoice
944            :param int account_id: optional ID of a G/L account to force
945                (this is used for returning products including service)
946            :return: dict of values to create() the invoice line
947         """
948         res = {}
949         if not line.invoiced:
950             if not account_id:
951                 if line.product_id:
952                     account_id = line.product_id.property_account_income.id
953                     if not account_id:
954                         account_id = line.product_id.categ_id.property_account_income_categ.id
955                     if not account_id:
956                         raise osv.except_osv(_('Error!'),
957                                 _('Please define income account for this product: "%s" (id:%d).') % \
958                                     (line.product_id.name, line.product_id.id,))
959                 else:
960                     prop = self.pool.get('ir.property').get(cr, uid,
961                             'property_account_income_categ', 'product.category',
962                             context=context)
963                     account_id = prop and prop.id or False
964             uosqty = self._get_line_qty(cr, uid, line, context=context)
965             uos_id = self._get_line_uom(cr, uid, line, context=context)
966             pu = 0.0
967             if uosqty:
968                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
969                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
970             fpos = line.order_id.fiscal_position or False
971             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
972             if not account_id:
973                 raise osv.except_osv(_('Error!'),
974                             _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
975             res = {
976                 'name': line.name,
977                 'sequence': line.sequence,
978                 'origin': line.order_id.name,
979                 'account_id': account_id,
980                 'price_unit': pu,
981                 'quantity': uosqty,
982                 'discount': line.discount,
983                 'uos_id': uos_id,
984                 'product_id': line.product_id.id or False,
985                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
986                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
987             }
988
989         return res
990
991     def invoice_line_create(self, cr, uid, ids, context=None):
992         if context is None:
993             context = {}
994
995         create_ids = []
996         sales = set()
997         for line in self.browse(cr, uid, ids, context=context):
998             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
999             if vals:
1000                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1001                 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
1002                 sales.add(line.order_id.id)
1003                 create_ids.append(inv_id)
1004         # Trigger workflow events
1005         for sale_id in sales:
1006             workflow.trg_write(uid, 'sale.order', sale_id, cr)
1007         return create_ids
1008
1009     def button_cancel(self, cr, uid, ids, context=None):
1010         for line in self.browse(cr, uid, ids, context=context):
1011             if line.invoiced:
1012                 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
1013         return self.write(cr, uid, ids, {'state': 'cancel'})
1014
1015     def button_confirm(self, cr, uid, ids, context=None):
1016         return self.write(cr, uid, ids, {'state': 'confirmed'})
1017
1018     def button_done(self, cr, uid, ids, context=None):
1019         res = self.write(cr, uid, ids, {'state': 'done'})
1020         for line in self.browse(cr, uid, ids, context=context):
1021             workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
1022         return res
1023
1024     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1025         product_obj = self.pool.get('product.product')
1026         if not product_id:
1027             return {'value': {'product_uom': product_uos,
1028                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1029
1030         product = product_obj.browse(cr, uid, product_id)
1031         value = {
1032             'product_uom': product.uom_id.id,
1033         }
1034         # FIXME must depend on uos/uom of the product and not only of the coeff.
1035         try:
1036             value.update({
1037                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1038                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1039             })
1040         except ZeroDivisionError:
1041             pass
1042         return {'value': value}
1043
1044     def create(self, cr, uid, values, context=None):
1045         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']):
1046             order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1047             defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1048                 qty=float(values.get('product_uom_qty', False)),
1049                 uom=values.get('product_uom', False),
1050                 qty_uos=float(values.get('product_uos_qty', False)),
1051                 uos=values.get('product_uos', False),
1052                 name=values.get('name', False),
1053                 partner_id=order['partner_id'][0],
1054                 date_order=order['date_order'],
1055                 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1056                 flag=False,  # Force name update
1057                 context=context
1058             )['value']
1059             values = dict(defaults, **values)
1060         return super(sale_order_line, self).create(cr, uid, values, context=context)
1061
1062     def copy_data(self, cr, uid, id, default=None, context=None):
1063         if not default:
1064             default = {}
1065         default.update({'state': 'draft', 'invoice_lines': [], 'procurement_ids': []})
1066         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1067
1068     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1069             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1070             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1071         context = context or {}
1072         lang = lang or context.get('lang', False)
1073         if not partner_id:
1074             raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
1075         warning = False
1076         product_uom_obj = self.pool.get('product.uom')
1077         partner_obj = self.pool.get('res.partner')
1078         product_obj = self.pool.get('product.product')
1079         context = {'lang': lang, 'partner_id': partner_id}
1080         partner = partner_obj.browse(cr, uid, partner_id)
1081         lang = partner.lang
1082         context_partner = {'lang': lang, 'partner_id': partner_id}
1083
1084         if not product:
1085             return {'value': {'th_weight': 0,
1086                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1087                    'product_uos': []}}
1088         if not date_order:
1089             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1090
1091         result = {}
1092         warning_msgs = ''
1093         product_obj = product_obj.browse(cr, uid, product, context=context_partner)
1094
1095         uom2 = False
1096         if uom:
1097             uom2 = product_uom_obj.browse(cr, uid, uom)
1098             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1099                 uom = False
1100         if uos:
1101             if product_obj.uos_id:
1102                 uos2 = product_uom_obj.browse(cr, uid, uos)
1103                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1104                     uos = False
1105             else:
1106                 uos = False
1107
1108         fpos = False
1109         if not fiscal_position:
1110             fpos = partner.property_account_position or False
1111         else:
1112             fpos = self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position)
1113         if update_tax: #The quantity only have changed
1114             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1115
1116         if not flag:
1117             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1118             if product_obj.description_sale:
1119                 result['name'] += '\n'+product_obj.description_sale
1120         domain = {}
1121         if (not uom) and (not uos):
1122             result['product_uom'] = product_obj.uom_id.id
1123             if product_obj.uos_id:
1124                 result['product_uos'] = product_obj.uos_id.id
1125                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1126                 uos_category_id = product_obj.uos_id.category_id.id
1127             else:
1128                 result['product_uos'] = False
1129                 result['product_uos_qty'] = qty
1130                 uos_category_id = False
1131             result['th_weight'] = qty * product_obj.weight
1132             domain = {'product_uom':
1133                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1134                         'product_uos':
1135                         [('category_id', '=', uos_category_id)]}
1136         elif uos and not uom: # only happens if uom is False
1137             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1138             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1139             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1140         elif uom: # whether uos is set or not
1141             default_uom = product_obj.uom_id and product_obj.uom_id.id
1142             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1143             if product_obj.uos_id:
1144                 result['product_uos'] = product_obj.uos_id.id
1145                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1146             else:
1147                 result['product_uos'] = False
1148                 result['product_uos_qty'] = qty
1149             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1150
1151         if not uom2:
1152             uom2 = product_obj.uom_id
1153         # get unit price
1154
1155         if not pricelist:
1156             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1157                     'Please set one before choosing a product.')
1158             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1159         else:
1160             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1161                     product, qty or 1.0, partner_id, {
1162                         'uom': uom or result.get('product_uom'),
1163                         'date': date_order,
1164                         })[pricelist]
1165             if price is False:
1166                 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1167                         "You have to change either the product, the quantity or the pricelist.")
1168
1169                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1170             else:
1171                 result.update({'price_unit': price})
1172         if warning_msgs:
1173             warning = {
1174                        'title': _('Configuration Error!'),
1175                        'message' : warning_msgs
1176                     }
1177         return {'value': result, 'domain': domain, 'warning': warning}
1178
1179     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1180             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1181             lang=False, update_tax=True, date_order=False, context=None):
1182         context = context or {}
1183         lang = lang or ('lang' in context and context['lang'])
1184         if not uom:
1185             return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1186         return self.product_id_change(cursor, user, ids, pricelist, product,
1187                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1188                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1189                 date_order=date_order, context=context)
1190
1191     def unlink(self, cr, uid, ids, context=None):
1192         if context is None:
1193             context = {}
1194         """Allows to delete sales order lines in draft,cancel states"""
1195         for rec in self.browse(cr, uid, ids, context=context):
1196             if rec.state not in ['draft', 'cancel']:
1197                 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1198         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1199
1200 class res_company(osv.Model):
1201     _inherit = "res.company"
1202     _columns = {
1203         'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
1204     }
1205
1206
1207 class mail_compose_message(osv.Model):
1208     _inherit = 'mail.compose.message'
1209
1210     def send_mail(self, cr, uid, ids, context=None):
1211         context = context or {}
1212         if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1213             context = dict(context, mail_post_autofollow=True)
1214             self.pool.get('sale.order').signal_quotation_sent(cr, uid, [context['default_res_id']])
1215         return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1216
1217
1218 class account_invoice(osv.Model):
1219     _inherit = 'account.invoice'
1220
1221     def _get_default_section_id(self, cr, uid, context=None):
1222         """ Gives default section by checking if present in the context """
1223         section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
1224         if not section_id:
1225             section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
1226         return section_id
1227
1228     def _resolve_section_id_from_context(self, cr, uid, context=None):
1229         """ Returns ID of section based on the value of 'section_id'
1230             context key, or None if it cannot be resolved to a single
1231             Sales Team.
1232         """
1233         if context is None:
1234             context = {}
1235         if type(context.get('default_section_id')) in (int, long):
1236             return context.get('default_section_id')
1237         if isinstance(context.get('default_section_id'), basestring):
1238             section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
1239             if len(section_ids) == 1:
1240                 return int(section_ids[0][0])
1241         return None
1242
1243     _columns = {
1244         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
1245     }
1246
1247     _defaults = {
1248         'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
1249     }
1250
1251     def confirm_paid(self, cr, uid, ids, context=None):
1252         sale_order_obj = self.pool.get('sale.order')
1253         res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1254         so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1255         for so_id in so_ids:
1256             sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1257         return res
1258
1259     def unlink(self, cr, uid, ids, context=None):
1260         """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1261         invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1262         #if we can't cancel all invoices, do nothing
1263         if len(invoice_ids) == len(ids):
1264             #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1265             #it will trigger the workflow to put the sale order in an 'invoice exception' state
1266             for id in ids:
1267                 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1268         return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1269
1270
1271 class procurement_order(osv.osv):
1272     _inherit = 'procurement.order'
1273     _columns = {
1274         'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1275     }
1276
1277
1278 class product_product(osv.Model):
1279     _inherit = 'product.product'
1280
1281     def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1282         SaleOrderLine = self.pool['sale.order.line']
1283         return {
1284             product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1285             for product_id in ids
1286         }
1287     _columns = {
1288         'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1289
1290     }
1291
1292
1293 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: