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