a99377c62898b4d11faf8f094d79f8ebbe17ad34
[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 import pooler
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, float_compare
29 import decimal_precision as dp
30 import netsvc
31 #from openerp.addons.base.res import res_config_sale as sale_config
32
33 class sale_shop(osv.osv):
34     _name = "sale.shop"
35     _description = "Sales Shop"
36     _columns = {
37         'name': fields.char('Shop Name', size=64, required=True),
38         'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
39         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
40         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
41         'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
42         'company_id': fields.many2one('res.company', 'Company', required=False),
43     }
44     _defaults = {
45         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
46     }
47
48 sale_shop()
49
50 class sale_order(osv.osv):
51     _name = "sale.order"
52     _description = "Sales Order"
53
54     def copy(self, cr, uid, id, default=None, context=None):
55         if not default:
56             default = {}
57         default.update({
58             'state': 'draft',
59             'shipped': False,
60             'invoice_ids': [],
61             'picking_ids': [],
62             'date_confirm': False,
63             'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64         })
65         return super(sale_order, self).copy(cr, uid, id, default, context=context)
66
67     def _amount_line_tax(self, cr, uid, line, context=None):
68         val = 0.0
69         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.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)['taxes']:
70             val += c.get('amount', 0.0)
71         return val
72
73     def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
74         cur_obj = self.pool.get('res.currency')
75         res = {}
76         for order in self.browse(cr, uid, ids, context=context):
77             res[order.id] = {
78                 'amount_untaxed': 0.0,
79                 'amount_tax': 0.0,
80                 'amount_total': 0.0,
81             }
82             val = val1 = 0.0
83             cur = order.pricelist_id.currency_id
84             for line in order.order_line:
85                 val1 += line.price_subtotal
86                 val += self._amount_line_tax(cr, uid, line, context=context)
87             res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
88             res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
89             res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
90         return res
91
92     # This is False
93     def _picked_rate(self, cr, uid, ids, name, arg, context=None):
94         if not ids:
95             return {}
96         res = {}
97         tmp = {}
98         for id in ids:
99             tmp[id] = {'picked': 0.0, 'total': 0.0}
100         cr.execute('''SELECT
101                 p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
102             FROM
103                 stock_move m
104             LEFT JOIN
105                 stock_picking p on (p.id=m.picking_id)
106             LEFT JOIN
107                 procurement_order mp on (mp.move_id=m.id)
108             WHERE
109                 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
110
111         for item in cr.dictfetchall():
112             if item['move_state'] == 'cancel':
113                 continue
114
115             if item['picking_type'] == 'in':#this is a returned picking
116                 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
117                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
118                     tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
119             else:
120                 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
121                 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
122                     tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
123
124         for order in self.browse(cr, uid, ids, context=context):
125             if order.shipped:
126                 res[order.id] = 100.0
127             else:
128                 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
129         return res
130
131     def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
132         res = {}
133         for sale in self.browse(cursor, user, ids, context=context):
134             if sale.invoiced:
135                 res[sale.id] = 100.0
136                 continue
137             tot = 0.0
138             for invoice in sale.invoice_ids:
139                 if invoice.state not in ('draft', 'cancel'):
140                     tot += invoice.amount_untaxed
141             if tot:
142                 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
143             else:
144                 res[sale.id] = 0.0
145         return res
146
147     def _invoiced(self, cursor, user, ids, name, arg, context=None):
148         res = {}
149         for sale in self.browse(cursor, user, ids, context=context):
150             res[sale.id] = True
151             invoice_existence = False
152             for invoice in sale.invoice_ids:
153                 if invoice.state!='cancel':
154                     invoice_existence = True
155                     if invoice.state != 'paid':
156                         res[sale.id] = False
157                         break
158             if not invoice_existence:
159                 res[sale.id] = False
160         return res
161
162     def _invoiced_search(self, cursor, user, obj, name, args, context=None):
163         if not len(args):
164             return []
165         clause = ''
166         sale_clause = ''
167         no_invoiced = False
168         for arg in args:
169             if arg[1] == '=':
170                 if arg[2]:
171                     clause += 'AND inv.state = \'paid\''
172                 else:
173                     clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\'  AND inv.state <> \'paid\'  AND rel.order_id = sale.id '
174                     sale_clause = ',  sale_order AS sale '
175                     no_invoiced = True
176
177         cursor.execute('SELECT rel.order_id ' \
178                 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
179                 'WHERE rel.invoice_id = inv.id ' + clause)
180         res = cursor.fetchall()
181         if no_invoiced:
182             cursor.execute('SELECT sale.id ' \
183                     'FROM sale_order AS sale ' \
184                     'WHERE sale.id NOT IN ' \
185                         '(SELECT rel.order_id ' \
186                         'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
187             res.extend(cursor.fetchall())
188         if not res:
189             return [('id', '=', 0)]
190         return [('id', 'in', [x[0] for x in res])]
191
192     def _get_order(self, cr, uid, ids, context=None):
193         result = {}
194         for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
195             result[line.order_id.id] = True
196         return result.keys()
197
198     _columns = {
199         'name': fields.char('Order Reference', size=64, required=True,
200             readonly=True, states={'draft': [('readonly', False)]}, select=True),
201         'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
202         'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
203         'client_order_ref': fields.char('Customer Reference', size=64),
204         'state': fields.selection([
205             ('draft', 'Quotation'),
206             ('waiting_date', 'Waiting Schedule'),
207             ('manual', 'To Invoice'),
208             ('progress', 'In Progress'),
209             ('shipping_except', 'Shipping Exception'),
210             ('invoice_except', 'Invoice Exception'),
211             ('done', 'Done'),
212             ('cancel', 'Cancelled')
213             ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
214         'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
215         'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
216         'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
217         'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
218         'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
219         'partner_invoice_id': fields.many2one('res.partner.address', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Invoice address for current sales order."),
220         'partner_order_id': fields.many2one('res.partner.address', 'Ordering Contact', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="The name and address of the contact who requested the order or quotation."),
221         'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
222
223         'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
224         'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
225             'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
226         'order_policy': fields.selection([
227             ('prepaid', 'Pay before delivery'),
228             ('manual', 'Deliver & invoice on demand'),
229             ('picking', 'Invoice based on deliveries'),
230             ('postpaid', 'Invoice on order after delivery'),
231         ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
232                     help="""The Invoice Policy is used to synchronise invoice and delivery operations.
233   - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
234   - The 'Deliver & Invoice on demand' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice based on the sale order or the sale order lines.
235   - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
236   - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
237         'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
238         'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
239
240         'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
241         '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)."),
242         'picking_ids': fields.one2many('stock.picking', 'sale_id', 'Related Picking', readonly=True, help="This is a list of picking that has been generated for this sales order."),
243         'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
244         'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246         'invoiced': fields.function(_invoiced, string='Paid',
247             fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248         'note': fields.text('Notes'),
249
250         'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
251             store = {
252                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
254             },
255             multi='sums', help="The amount without tax."),
256         'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
257             store = {
258                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
260             },
261             multi='sums', help="The tax amount."),
262         'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
263             store = {
264                 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265                 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
266             },
267             multi='sums', help="The total amount."),
268
269         'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice). Ordered and delivered quantities may not be the same. You have to choose if you want your invoice based on ordered or shipped quantities. If the product is a service, shipped quantities means hours spent on the associated tasks.", required=True, readonly=True, states={'draft': [('readonly', False)]}),
270         'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271         'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272         'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True),
273         'tax_id': fields.many2one('account.tax', 'Tax'),
274     }
275     _defaults = {
276         'picking_policy': 'direct',
277         'date_order': fields.date.context_today,
278         'order_policy': 'manual',
279         'state': 'draft',
280         'user_id': lambda obj, cr, uid, context: uid,
281         'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
282         'invoice_quantity': 'order',
283         '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'],
284         'partner_order_id': lambda self, cr, uid, context: context.get('partner_id', False) and  self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['contact'])['contact'],
285         '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'],
286     }
287     _sql_constraints = [
288         ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
289     ]
290     _order = 'name desc'
291
292     # Form filling
293     def unlink(self, cr, uid, ids, context=None):
294         sale_orders = self.read(cr, uid, ids, ['state'], context=context)
295         unlink_ids = []
296         for s in sale_orders:
297             if s['state'] in ['draft', 'cancel']:
298                 unlink_ids.append(s['id'])
299             else:
300                 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
301
302         return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
303
304     def onchange_shop_id(self, cr, uid, ids, shop_id):
305         v = {}
306         if shop_id:
307             shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
308             v['project_id'] = shop.project_id.id
309             # Que faire si le client a une pricelist a lui ?
310             if shop.pricelist_id.id:
311                 v['pricelist_id'] = shop.pricelist_id.id
312         return {'value': v}
313     
314     def onchange_taxes(self, cr, uid, ids, tax_id, context=None):
315         sol_obj = self.pool.get('sale.order.line')
316         res = {'value': {}}
317         for order in self.browse(cr, uid, ids, context=context):
318             for line in order.order_line:
319                 sol_obj.write(cr, uid, line.id, {'tax_id': [(4,tax_id)]})
320         return res
321
322     def action_cancel_draft(self, cr, uid, ids, *args):
323         if not len(ids):
324             return False
325         cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
326         line_ids = map(lambda x: x[0], cr.fetchall())
327         self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
328         self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
329         wf_service = netsvc.LocalService("workflow")
330         for inv_id in ids:
331             # Deleting the existing instance of workflow for SO
332             wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
333             wf_service.trg_create(uid, 'sale.order', inv_id, cr)
334         for (id,name) in self.name_get(cr, uid, ids):
335             message = _("The sales order '%s' has been set in draft state.") %(name,)
336             self.log(cr, uid, id, message)
337         return True
338
339     def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
340         if (not pricelist_id) or (not order_lines):
341             return {}
342         warning = {
343             'title': _('Pricelist Warning!'),
344             'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
345         }
346         return {'warning': warning}
347
348     def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
349         if not order_id:
350             return {}
351         val = {}
352         if not invoice_id:
353             val['partner_invoice_id'] = order_id
354         if not shipping_id:
355             val['partner_shipping_id'] = order_id
356         return {'value': val}
357
358     def onchange_partner_id(self, cr, uid, ids, part):
359         if not part:
360             return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
361
362         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
363         part = self.pool.get('res.partner').browse(cr, uid, part)
364         pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
365         payment_term = part.property_payment_term and part.property_payment_term.id or False
366         fiscal_position = part.property_account_position and part.property_account_position.id or False
367         dedicated_salesman = part.user_id and part.user_id.id or uid
368         val = {
369             'partner_invoice_id': addr['invoice'],
370             'partner_order_id': addr['contact'],
371             'partner_shipping_id': addr['delivery'],
372             'payment_term': payment_term,
373             'fiscal_position': fiscal_position,
374             'user_id': dedicated_salesman,
375         }
376         if pricelist:
377             val['pricelist_id'] = pricelist
378         return {'value': val}
379
380     def shipping_policy_change(self, cr, uid, ids, policy, context=None):
381         if not policy:
382             return {}
383         inv_qty = 'order'
384         if policy == 'prepaid':
385             inv_qty = 'order'
386         elif policy == 'picking':
387             inv_qty = 'procurement'
388         return {'value': {'invoice_quantity': inv_qty}}
389
390     def write(self, cr, uid, ids, vals, context=None):
391         if vals.get('order_policy', False):
392             if vals['order_policy'] == 'prepaid':
393                 vals.update({'invoice_quantity': 'order'})
394             elif vals['order_policy'] == 'picking':
395                 vals.update({'invoice_quantity': 'procurement'})
396         return super(sale_order, self).write(cr, uid, ids, vals, context=context)
397
398     def create(self, cr, uid, vals, context=None):
399         if vals.get('order_policy', False):
400             if vals['order_policy'] == 'prepaid':
401                 vals.update({'invoice_quantity': 'order'})
402             if vals['order_policy'] == 'picking':
403                 vals.update({'invoice_quantity': 'procurement'})
404         return super(sale_order, self).create(cr, uid, vals, context=context)
405
406     def button_dummy(self, cr, uid, ids, context=None):
407         return True
408
409     # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
410     #        can be removed after 6.1.
411     def _inv_get(self, cr, uid, order, context=None):
412         return {}
413
414     def _prepare_invoice(self, cr, uid, order, lines, context=None):
415         """Prepare the dict of values to create the new invoice for a
416            sale order. This method may be overridden to implement custom
417            invoice generation (making sure to call super() to establish
418            a clean extension chain).
419
420            :param browse_record order: sale.order record to invoice
421            :param list(int) line: list of invoice line IDs that must be
422                                   attached to the invoice
423            :return: dict of value to create() the invoice
424         """
425         if context is None:
426             context = {}
427         journal_ids = self.pool.get('account.journal').search(cr, uid,
428             [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
429             limit=1)
430         if not journal_ids:
431             raise osv.except_osv(_('Error !'),
432                 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
433
434         invoice_vals = {
435             'name': order.client_order_ref or '',
436             'origin': order.name,
437             'type': 'out_invoice',
438             'reference': order.client_order_ref or order.name,
439             'account_id': order.partner_id.property_account_receivable.id,
440             'partner_id': order.partner_id.id,
441             'journal_id': journal_ids[0],
442             'address_invoice_id': order.partner_invoice_id.id,
443             'address_contact_id': order.partner_order_id.id,
444             'invoice_line': [(6, 0, lines)],
445             'currency_id': order.pricelist_id.currency_id.id,
446             'comment': order.note,
447             'payment_term': order.payment_term and order.payment_term.id or False,
448             'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
449             'date_invoice': context.get('date_invoice', False),
450             'company_id': order.company_id.id,
451             'user_id': order.user_id and order.user_id.id or False
452         }
453
454         # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
455         invoice_vals.update(self._inv_get(cr, uid, order, context=context))
456
457         return invoice_vals
458
459     def _make_invoice(self, cr, uid, order, lines, context=None):
460         inv_obj = self.pool.get('account.invoice')
461         obj_invoice_line = self.pool.get('account.invoice.line')
462         if context is None:
463             context = {}
464         invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
465         from_line_invoice_ids = []
466         for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
467             for invoice_line_id in invoiced_sale_line_id.invoice_lines:
468                 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
469                     from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
470         for preinv in order.invoice_ids:
471             if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
472                 for preline in preinv.invoice_line:
473                     inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
474                     lines.append(inv_line_id)
475         inv = self._prepare_invoice(cr, uid, order, lines, context=context)
476         inv_id = inv_obj.create(cr, uid, inv, context=context)
477         data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
478         if data.get('value', False):
479             inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
480         inv_obj.button_compute(cr, uid, [inv_id])
481         return inv_id
482
483     def manual_invoice(self, cr, uid, ids, context=None):
484         mod_obj = self.pool.get('ir.model.data')
485         wf_service = netsvc.LocalService("workflow")
486         inv_ids = set()
487         inv_ids1 = set()
488         for id in ids:
489             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
490                 inv_ids.add(record.id)
491         # inv_ids would have old invoices if any
492         for id in ids:
493             wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
494             for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
495                 inv_ids1.add(record.id)
496         inv_ids = list(inv_ids1.difference(inv_ids))
497
498         res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
499         res_id = res and res[1] or False,
500
501         return {
502             'name': _('Customer Invoices'),
503             'view_type': 'form',
504             'view_mode': 'form',
505             'view_id': [res_id],
506             'res_model': 'account.invoice',
507             'context': "{'type':'out_invoice'}",
508             'type': 'ir.actions.act_window',
509             'nodestroy': True,
510             'target': 'current',
511             'res_id': inv_ids and inv_ids[0] or False,
512         }
513
514     def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
515         res = False
516         invoices = {}
517         invoice_ids = []
518         picking_obj = self.pool.get('stock.picking')
519         invoice = self.pool.get('account.invoice')
520         obj_sale_order_line = self.pool.get('sale.order.line')
521         partner_currency = {}
522         if context is None:
523             context = {}
524         # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
525         # last day of the last month as invoice date
526         if date_inv:
527             context['date_inv'] = date_inv
528         for o in self.browse(cr, uid, ids, context=context):
529             currency_id = o.pricelist_id.currency_id.id
530             if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
531                 raise osv.except_osv(
532                     _('Error !'),
533                     _('You cannot group sales having different currencies for the same partner.'))
534
535             partner_currency[o.partner_id.id] = currency_id
536             lines = []
537             for line in o.order_line:
538                 if line.invoiced:
539                     continue
540                 elif (line.state in states):
541                     lines.append(line.id)
542             created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
543             if created_lines:
544                 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
545         if not invoices:
546             for o in self.browse(cr, uid, ids, context=context):
547                 for i in o.invoice_ids:
548                     if i.state == 'draft':
549                         return i.id
550         for val in invoices.values():
551             if grouped:
552                 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
553                 invoice_ref = ''
554                 for o, l in val:
555                     invoice_ref += o.name + '|'
556                     self.write(cr, uid, [o.id], {'state': 'progress'})
557                     if o.order_policy == 'picking':
558                         picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
559                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
560                 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
561             else:
562                 for order, il in val:
563                     res = self._make_invoice(cr, uid, order, il, context=context)
564                     invoice_ids.append(res)
565                     self.write(cr, uid, [order.id], {'state': 'progress'})
566                     if order.order_policy == 'picking':
567                         picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
568                     cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
569         return res
570
571     def action_invoice_cancel(self, cr, uid, ids, context=None):
572         if context is None:
573             context = {}
574         for sale in self.browse(cr, uid, ids, context=context):
575             for line in sale.order_line:
576                 #
577                 # Check if the line is invoiced (has asociated invoice
578                 # lines from non-cancelled invoices).
579                 #
580                 invoiced = False
581                 for iline in line.invoice_lines:
582                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
583                         invoiced = True
584                         break
585                 # Update the line (only when needed)
586                 if line.invoiced != invoiced:
587                     self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
588         self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
589         return True
590
591     def action_invoice_end(self, cr, uid, ids, context=None):
592         for order in self.browse(cr, uid, ids, context=context):
593             #
594             # Update the sale order lines state (and invoiced flag).
595             #
596             for line in order.order_line:
597                 vals = {}
598                 #
599                 # Check if the line is invoiced (has asociated invoice
600                 # lines from non-cancelled invoices).
601                 #
602                 invoiced = False
603                 for iline in line.invoice_lines:
604                     if iline.invoice_id and iline.invoice_id.state != 'cancel':
605                         invoiced = True
606                         break
607                 if line.invoiced != invoiced:
608                     vals['invoiced'] = invoiced
609                 # If the line was in exception state, now it gets confirmed.
610                 if line.state == 'exception':
611                     vals['state'] = 'confirmed'
612                 # Update the line (only when needed).
613                 if vals:
614                     self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
615             #
616             # Update the sales order state.
617             #
618             if order.state == 'invoice_except':
619                 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
620         return True
621
622     def action_cancel(self, cr, uid, ids, context=None):
623         wf_service = netsvc.LocalService("workflow")
624         if context is None:
625             context = {}
626         sale_order_line_obj = self.pool.get('sale.order.line')
627         proc_obj = self.pool.get('procurement.order')
628         for sale in self.browse(cr, uid, ids, context=context):
629             for pick in sale.picking_ids:
630                 if pick.state not in ('draft', 'cancel'):
631                     raise osv.except_osv(
632                         _('Could not cancel sales order !'),
633                         _('You must first cancel all picking attached to this sales order.'))
634                 if pick.state == 'cancel':
635                     for mov in pick.move_lines:
636                         proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
637                         if proc_ids:
638                             for proc in proc_ids:
639                                 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
640             for r in self.read(cr, uid, ids, ['picking_ids']):
641                 for pick in r['picking_ids']:
642                     wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
643             for inv in sale.invoice_ids:
644                 if inv.state not in ('draft', 'cancel'):
645                     raise osv.except_osv(
646                         _('Could not cancel this sales order !'),
647                         _('You must first cancel all invoices attached to this sales order.'))
648             for r in self.read(cr, uid, ids, ['invoice_ids']):
649                 for inv in r['invoice_ids']:
650                     wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
651             sale_order_line_obj.write(cr, uid, [l.id for l in  sale.order_line],
652                     {'state': 'cancel'})
653             message = _("The sales order '%s' has been cancelled.") % (sale.name,)
654             self.log(cr, uid, sale.id, message)
655         self.write(cr, uid, ids, {'state': 'cancel'})
656         return True
657
658     def action_wait(self, cr, uid, ids, context=None):
659         for o in self.browse(cr, uid, ids):
660             if not o.order_line:
661                 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
662             if (o.order_policy == 'manual'):
663                 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
664             else:
665                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
666             self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
667             message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
668             self.log(cr, uid, o.id, message)
669         return True
670
671     def procurement_lines_get(self, cr, uid, ids, *args):
672         res = []
673         for order in self.browse(cr, uid, ids, context={}):
674             for line in order.order_line:
675                 if line.procurement_id:
676                     res.append(line.procurement_id.id)
677         return res
678
679     # if mode == 'finished':
680     #   returns True if all lines are done, False otherwise
681     # if mode == 'canceled':
682     #   returns True if there is at least one canceled line, False otherwise
683     def test_state(self, cr, uid, ids, mode, *args):
684         assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
685         finished = True
686         canceled = False
687         notcanceled = False
688         write_done_ids = []
689         write_cancel_ids = []
690         for order in self.browse(cr, uid, ids, context={}):
691             for line in order.order_line:
692                 if (not line.procurement_id) or (line.procurement_id.state=='done'):
693                     if line.state != 'done':
694                         write_done_ids.append(line.id)
695                 else:
696                     finished = False
697                 if line.procurement_id:
698                     if (line.procurement_id.state == 'cancel'):
699                         canceled = True
700                         if line.state != 'exception':
701                             write_cancel_ids.append(line.id)
702                     else:
703                         notcanceled = True
704         if write_done_ids:
705             self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
706         if write_cancel_ids:
707             self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
708
709         if mode == 'finished':
710             return finished
711         elif mode == 'canceled':
712             return canceled
713             if notcanceled:
714                 return False
715             return canceled
716
717     def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
718         return {
719             'name': line.name,
720             'origin': order.name,
721             'date_planned': date_planned,
722             'product_id': line.product_id.id,
723             'product_qty': line.product_uom_qty,
724             'product_uom': line.product_uom.id,
725             'product_uos_qty': (line.product_uos and line.product_uos_qty)\
726                     or line.product_uom_qty,
727             'product_uos': (line.product_uos and line.product_uos.id)\
728                     or line.product_uom.id,
729             'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
730             'procure_method': line.type,
731             'move_id': move_id,
732             'company_id': order.company_id.id,
733             'note': line.notes
734         }
735
736     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
737         location_id = order.shop_id.warehouse_id.lot_stock_id.id
738         output_id = order.shop_id.warehouse_id.lot_output_id.id
739         return {
740             'name': line.name[:250],
741             'picking_id': picking_id,
742             'product_id': line.product_id.id,
743             'date': date_planned,
744             'date_expected': date_planned,
745             'product_qty': line.product_uom_qty,
746             'product_uom': line.product_uom.id,
747             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
748             'product_uos': (line.product_uos and line.product_uos.id)\
749                     or line.product_uom.id,
750             'product_packaging': line.product_packaging.id,
751             'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
752             'location_id': location_id,
753             'location_dest_id': output_id,
754             'sale_line_id': line.id,
755             'tracking_id': False,
756             'state': 'draft',
757             #'state': 'waiting',
758             'note': line.notes,
759             'company_id': order.company_id.id,
760             'price_unit': line.product_id.standard_price or 0.0
761         }
762
763     def _prepare_order_picking(self, cr, uid, order, context=None):
764         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
765         return {
766             'name': pick_name,
767             'origin': order.name,
768             'date': order.date_order,
769             'type': 'out',
770             'state': 'auto',
771             'move_type': order.picking_policy,
772             'sale_id': order.id,
773             'address_id': order.partner_shipping_id.id,
774             'note': order.note,
775             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
776             'company_id': order.company_id.id,
777         }
778
779     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
780         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
781         """
782         Define ship_recreate for process after shipping exception
783         param order: sale order to which the order lines belong
784         param line: sale order line records to procure
785         param move_id: the ID of stock move
786         param proc_id: the ID of procurement
787         """
788         move_obj = self.pool.get('stock.move')
789         if order.state == 'shipping_except':
790             for pick in order.picking_ids:
791                 for move in pick.move_lines:
792                     if move.state == 'cancel':
793                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
794                         if mov_ids:
795                             for mov in move_obj.browse(cr, uid, mov_ids):
796                                 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
797                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
798                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
799         return True
800
801     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
802         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
803         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
804         return date_planned
805
806     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
807         """Create the required procurements to supply sale order lines, also connecting
808         the procurements to appropriate stock moves in order to bring the goods to the
809         sale order's requested location.
810
811         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
812         a standard outgoing picking will be created to wrap the stock moves, as returned
813         by :meth:`~._prepare_order_picking`.
814
815         Modules that wish to customize the procurements or partition the stock moves over
816         multiple stock pickings may override this method and call ``super()`` with
817         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
818
819         :param browse_record order: sale order to which the order lines belong
820         :param list(browse_record) order_lines: sale order line records to procure
821         :param int picking_id: optional ID of a stock picking to which the created stock moves
822                                will be added. A new picking will be created if ommitted.
823         :return: True
824         """
825         move_obj = self.pool.get('stock.move')
826         picking_obj = self.pool.get('stock.picking')
827         procurement_obj = self.pool.get('procurement.order')
828         proc_ids = []
829
830         for line in order_lines:
831             if line.state == 'done':
832                 continue
833
834             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
835
836             if line.product_id:
837                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
838                     if not picking_id:
839                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
840                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
841                 else:
842                     # a service has no stock move
843                     move_id = False
844
845                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
846                 proc_ids.append(proc_id)
847                 line.write({'procurement_id': proc_id})
848                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
849
850         wf_service = netsvc.LocalService("workflow")
851         if picking_id:
852             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
853
854         for proc_id in proc_ids:
855             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
856
857         val = {}
858         if order.state == 'shipping_except':
859             val['state'] = 'progress'
860             val['shipped'] = False
861
862             if (order.order_policy == 'manual'):
863                 for line in order.order_line:
864                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
865                         val['state'] = 'manual'
866                         break
867         order.write(val)
868         return True
869
870     def action_ship_create(self, cr, uid, ids, context=None):
871         for order in self.browse(cr, uid, ids, context=context):
872             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
873         return True
874
875     def action_ship_end(self, cr, uid, ids, context=None):
876         for order in self.browse(cr, uid, ids, context=context):
877             val = {'shipped': True}
878             if order.state == 'shipping_except':
879                 val['state'] = 'progress'
880                 if (order.order_policy == 'manual'):
881                     for line in order.order_line:
882                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
883                             val['state'] = 'manual'
884                             break
885             for line in order.order_line:
886                 towrite = []
887                 if line.state == 'exception':
888                     towrite.append(line.id)
889                 if towrite:
890                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
891             self.write(cr, uid, [order.id], val)
892         return True
893
894     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
895         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
896         for inv in invs:
897             part = inv['partner_id'] and inv['partner_id'][0]
898             pr = inv['amount_untaxed'] or 0.0
899             partnertype = 'customer'
900             eventtype = 'sale'
901             event = {
902                 'name': 'Order: '+name,
903                 'som': False,
904                 'description': 'Order '+str(inv['id']),
905                 'document': '',
906                 'partner_id': part,
907                 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
908                 'user_id': uid,
909                 'partner_type': partnertype,
910                 'probability': 1.0,
911                 'planned_revenue': pr,
912                 'planned_cost': 0.0,
913                 'type': eventtype
914             }
915             self.pool.get('res.partner.event').create(cr, uid, event)
916
917     def has_stockable_products(self, cr, uid, ids, *args):
918         for order in self.browse(cr, uid, ids):
919             for order_line in order.order_line:
920                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
921                     return True
922         return False
923 sale_order()
924
925 # TODO add a field price_unit_uos
926 # - update it on change product and unit price
927 # - use it in report if there is a uos
928 class sale_order_line(osv.osv):
929
930     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
931         tax_obj = self.pool.get('account.tax')
932         cur_obj = self.pool.get('res.currency')
933         res = {}
934         if context is None:
935             context = {}
936         for line in self.browse(cr, uid, ids, context=context):
937             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
938             taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.order_id.partner_invoice_id.id, line.product_id, line.order_id.partner_id)
939             cur = line.order_id.pricelist_id.currency_id
940             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
941         return res
942
943     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
944         res = {}
945         for line in self.browse(cr, uid, ids, context=context):
946             try:
947                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
948             except:
949                 res[line.id] = 1
950         return res
951
952     def _get_uom_id(self, cr, uid, *args):
953         try:
954             proxy = self.pool.get('ir.model.data')
955             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
956             return result[1]
957         except Exception, ex:
958             return False
959
960     _name = 'sale.order.line'
961     _description = 'Sales Order Line'
962     _columns = {
963         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
964         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
965         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
966         'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
967         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
968         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
969         'invoiced': fields.boolean('Invoiced', readonly=True),
970         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
971         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
972         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
973         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
974         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
975             help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
976         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
977         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
978         'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
979         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
980         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
981         'product_uos': fields.many2one('product.uom', 'Product UoS'),
982         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
983         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
984         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
985         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
986         'notes': fields.text('Notes'),
987         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
988         'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
989                 help='* The \'Draft\' state is set when the related sales order in draft state. \
990                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
991                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
992                     \n* The \'Done\' state is set when the sales order line has been picked. \
993                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
994         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
995         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
996         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
997     }
998     _order = 'sequence, id'
999     _defaults = {
1000         'product_uom' : _get_uom_id,
1001         'discount': 0.0,
1002         'delay': 0.0,
1003         'product_uom_qty': 1,
1004         'product_uos_qty': 1,
1005         'sequence': 10,
1006         'invoiced': 0,
1007         'state': 'draft',
1008         'type': 'make_to_stock',
1009         'product_packaging': False,
1010         'price_unit': 0.0,
1011     }
1012
1013     def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1014         """Prepare the dict of values to create the new invoice line for a
1015            sale order line. This method may be overridden to implement custom
1016            invoice generation (making sure to call super() to establish
1017            a clean extension chain).
1018
1019            :param browse_record line: sale.order.line record to invoice
1020            :param int account_id: optional ID of a G/L account to force
1021                (this is used for returning products including service)
1022            :return: dict of values to create() the invoice line
1023         """
1024
1025         def _get_line_qty(line):
1026             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1027                 if line.product_uos:
1028                     return line.product_uos_qty or 0.0
1029                 return line.product_uom_qty
1030             else:
1031                 return self.pool.get('procurement.order').quantity_get(cr, uid,
1032                         line.procurement_id.id, context=context)
1033
1034         def _get_line_uom(line):
1035             if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1036                 if line.product_uos:
1037                     return line.product_uos.id
1038                 return line.product_uom.id
1039             else:
1040                 return self.pool.get('procurement.order').uom_get(cr, uid,
1041                         line.procurement_id.id, context=context)
1042
1043         if not line.invoiced:
1044             if not account_id:
1045                 if line.product_id:
1046                     account_id = line.product_id.product_tmpl_id.property_account_income.id
1047                     if not account_id:
1048                         account_id = line.product_id.categ_id.property_account_income_categ.id
1049                     if not account_id:
1050                         raise osv.except_osv(_('Error !'),
1051                                 _('There is no income account defined for this product: "%s" (id:%d)') % \
1052                                     (line.product_id.name, line.product_id.id,))
1053                 else:
1054                     prop = self.pool.get('ir.property').get(cr, uid,
1055                             'property_account_income_categ', 'product.category',
1056                             context=context)
1057                     account_id = prop and prop.id or False
1058             uosqty = _get_line_qty(line)
1059             uos_id = _get_line_uom(line)
1060             pu = 0.0
1061             if uosqty:
1062                 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1063                         self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1064             fpos = line.order_id.fiscal_position or False
1065             account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1066             if not account_id:
1067                 raise osv.except_osv(_('Error !'),
1068                             _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1069             return {
1070                 'name': line.name,
1071                 'origin': line.order_id.name,
1072                 'account_id': account_id,
1073                 'price_unit': pu,
1074                 'quantity': uosqty,
1075                 'discount': line.discount,
1076                 'uos_id': uos_id,
1077                 'product_id': line.product_id.id or False,
1078                 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1079                 'note': line.notes,
1080                 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1081             }
1082
1083         return False
1084
1085     def invoice_line_create(self, cr, uid, ids, context=None):
1086         if context is None:
1087             context = {}
1088
1089         create_ids = []
1090         sales = set()
1091         for line in self.browse(cr, uid, ids, context=context):
1092             vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1093             if vals:
1094                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1095                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1096                 self.write(cr, uid, [line.id], {'invoiced': True})
1097                 sales.add(line.order_id.id)
1098                 create_ids.append(inv_id)
1099         # Trigger workflow events
1100         wf_service = netsvc.LocalService("workflow")
1101         for sale_id in sales:
1102             wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1103         return create_ids
1104
1105     def button_cancel(self, cr, uid, ids, context=None):
1106         for line in self.browse(cr, uid, ids, context=context):
1107             if line.invoiced:
1108                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1109             for move_line in line.move_ids:
1110                 if move_line.state != 'cancel':
1111                     raise osv.except_osv(
1112                             _('Could not cancel sales order line!'),
1113                             _('You must first cancel stock moves attached to this sales order line.'))
1114         return self.write(cr, uid, ids, {'state': 'cancel'})
1115
1116     def button_confirm(self, cr, uid, ids, context=None):
1117         return self.write(cr, uid, ids, {'state': 'confirmed'})
1118
1119     def button_done(self, cr, uid, ids, context=None):
1120         wf_service = netsvc.LocalService("workflow")
1121         res = self.write(cr, uid, ids, {'state': 'done'})
1122         for line in self.browse(cr, uid, ids, context=context):
1123             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1124         return res
1125
1126     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1127         product_obj = self.pool.get('product.product')
1128         if not product_id:
1129             return {'value': {'product_uom': product_uos,
1130                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1131
1132         product = product_obj.browse(cr, uid, product_id)
1133         value = {
1134             'product_uom': product.uom_id.id,
1135         }
1136         # FIXME must depend on uos/uom of the product and not only of the coeff.
1137         try:
1138             value.update({
1139                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1140                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1141             })
1142         except ZeroDivisionError:
1143             pass
1144         return {'value': value}
1145
1146     def copy_data(self, cr, uid, id, default=None, context=None):
1147         if not default:
1148             default = {}
1149         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1150         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1151
1152     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1153                                    partner_id=False, packaging=False, flag=False, context=None):
1154         if not product:
1155             return {'value': {'product_packaging': False}}
1156         product_obj = self.pool.get('product.product')
1157         product_uom_obj = self.pool.get('product.uom')
1158         pack_obj = self.pool.get('product.packaging')
1159         warning = {}
1160         result = {}
1161         warning_msgs = ''
1162         if flag:
1163             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1164                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1165                     packaging=packaging, flag=False, context=context)
1166             warning_msgs = res.get('warning') and res['warning']['message']
1167
1168         products = product_obj.browse(cr, uid, product, context=context)
1169         if not products.packaging:
1170             packaging = result['product_packaging'] = False
1171         elif not packaging and products.packaging and not flag:
1172             packaging = products.packaging[0].id
1173             result['product_packaging'] = packaging
1174
1175         if packaging:
1176             default_uom = products.uom_id and products.uom_id.id
1177             pack = pack_obj.browse(cr, uid, packaging, context=context)
1178             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1179 #            qty = qty - qty % q + q
1180             if qty and (q and not (qty % q) == 0):
1181                 ean = pack.ean or _('(n/a)')
1182                 qty_pack = pack.qty
1183                 type_ul = pack.ul
1184                 if not warning_msgs:
1185                     warn_msg = _("You selected a quantity of %d Units.\n"
1186                                 "But it's not compatible with the selected packaging.\n"
1187                                 "Here is a proposition of quantities according to the packaging:\n"
1188                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1189                                     (qty, ean, qty_pack, type_ul.name)
1190                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1191                 warning = {
1192                        'title': _('Configuration Error !'),
1193                        'message': warning_msgs
1194                 }
1195             result['product_uom_qty'] = qty
1196
1197         return {'value': result, 'warning': warning}
1198
1199     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1200             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1201             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1202         context = context or {}
1203         lang = lang or context.get('lang',False)
1204         if not  partner_id:
1205             raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1206         warning = {}
1207         product_uom_obj = self.pool.get('product.uom')
1208         partner_obj = self.pool.get('res.partner')
1209         product_obj = self.pool.get('product.product')
1210         context = {'lang': lang, 'partner_id': partner_id}
1211         if partner_id:
1212             lang = partner_obj.browse(cr, uid, partner_id).lang
1213         context_partner = {'lang': lang, 'partner_id': partner_id}
1214
1215         if not product:
1216             return {'value': {'th_weight': 0, 'product_packaging': False,
1217                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1218                    'product_uos': []}}
1219         if not date_order:
1220             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1221
1222         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1223         result = res.get('value', {})
1224         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1225         product_obj = product_obj.browse(cr, uid, product, context=context)
1226
1227         uom2 = False
1228         if uom:
1229             uom2 = product_uom_obj.browse(cr, uid, uom)
1230             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1231                 uom = False
1232         if uos:
1233             if product_obj.uos_id:
1234                 uos2 = product_uom_obj.browse(cr, uid, uos)
1235                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1236                     uos = False
1237             else:
1238                 uos = False
1239         if product_obj.description_sale:
1240             result['notes'] = product_obj.description_sale
1241         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1242         if update_tax: #The quantity only have changed
1243             result['delay'] = (product_obj.sale_delay or 0.0)
1244             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1245             result.update({'type': product_obj.procure_method})
1246
1247         if not flag:
1248             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1249         domain = {}
1250         if (not uom) and (not uos):
1251             result['product_uom'] = product_obj.uom_id.id
1252             if product_obj.uos_id:
1253                 result['product_uos'] = product_obj.uos_id.id
1254                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1255                 uos_category_id = product_obj.uos_id.category_id.id
1256             else:
1257                 result['product_uos'] = False
1258                 result['product_uos_qty'] = qty
1259                 uos_category_id = False
1260             result['th_weight'] = qty * product_obj.weight
1261             domain = {'product_uom':
1262                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1263                         'product_uos':
1264                         [('category_id', '=', uos_category_id)]}
1265
1266         elif uos and not uom: # only happens if uom is False
1267             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1268             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1269             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1270         elif uom: # whether uos is set or not
1271             default_uom = product_obj.uom_id and product_obj.uom_id.id
1272             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1273             if product_obj.uos_id:
1274                 result['product_uos'] = product_obj.uos_id.id
1275                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1276             else:
1277                 result['product_uos'] = False
1278                 result['product_uos_qty'] = qty
1279             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1280
1281         if not uom2:
1282             uom2 = product_obj.uom_id
1283         compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1284         if (product_obj.type=='product') and int(compare_qty) == -1 \
1285           and (product_obj.procure_method=='make_to_stock'):
1286             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1287                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1288                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1289                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1290             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1291         # get unit price
1292
1293         if not pricelist:
1294             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1295                     'Please set one before choosing a product.')
1296             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1297         else:
1298             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1299                     product, qty or 1.0, partner_id, {
1300                         'uom': uom or result.get('product_uom'),
1301                         'date': date_order,
1302                         })[pricelist]
1303             if price is False:
1304                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1305                         "You have to change either the product, the quantity or the pricelist.")
1306
1307                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1308             else:
1309                 result.update({'price_unit': price})
1310         if warning_msgs:
1311             warning = {
1312                        'title': _('Configuration Error !'),
1313                        'message' : warning_msgs
1314                     }
1315         return {'value': result, 'domain': domain, 'warning': warning}
1316
1317     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1318             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1319             lang=False, update_tax=True, date_order=False, context=None):
1320         context = context or {}
1321         lang = lang or ('lang' in context and context['lang'])
1322         res = self.product_id_change(cursor, user, ids, pricelist, product,
1323                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1324                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1325                 date_order=date_order, context=context)
1326         if 'product_uom' in res['value']:
1327             del res['value']['product_uom']
1328         if not uom:
1329             res['value']['price_unit'] = 0.0
1330         return res
1331
1332     def unlink(self, cr, uid, ids, context=None):
1333         if context is None:
1334             context = {}
1335         """Allows to delete sales order lines in draft,cancel states"""
1336         for rec in self.browse(cr, uid, ids, context=context):
1337             if rec.state not in ['draft', 'cancel']:
1338                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1339         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1340
1341 sale_order_line()
1342
1343 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: