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