[FIX] notes on tasks from SO
[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="Givwizard = self.browse(cr, uid, ids)[0]es 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': lambda *a: time.strftime(DEFAULT_SERVER_DATE_FORMAT),
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, *args):
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': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
635             else:
636                 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
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             # Added by mrp, but mrp is not a dependency, this will trigger a warning
704             # if mrp is not installed.
705             'property_ids': [(6, 0, [x.id for x in line.property_ids])],
706             'company_id': order.company_id.id,
707             'note': line.notes
708         }
709
710     def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
711         location_id = order.shop_id.warehouse_id.lot_stock_id.id
712         output_id = order.shop_id.warehouse_id.lot_output_id.id
713         return {
714             'name': line.name[:250],
715             'picking_id': picking_id,
716             'product_id': line.product_id.id,
717             'date': date_planned,
718             'date_expected': date_planned,
719             'product_qty': line.product_uom_qty,
720             'product_uom': line.product_uom.id,
721             'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
722             'product_uos': (line.product_uos and line.product_uos.id)\
723                     or line.product_uom.id,
724             'product_packaging': line.product_packaging.id,
725             'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
726             'location_id': location_id,
727             'location_dest_id': output_id,
728             'sale_line_id': line.id,
729             'tracking_id': False,
730             'state': 'draft',
731             #'state': 'waiting',
732             'note': line.notes,
733             'company_id': order.company_id.id,
734             'price_unit': line.product_id.standard_price or 0.0
735         }
736
737     def _prepare_order_picking(self, cr, uid, order, context=None):
738         pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
739         return {
740             'name': pick_name,
741             'origin': order.name,
742             'date': order.date_order,
743             'type': 'out',
744             'state': 'auto',
745             'move_type': order.picking_policy,
746             'sale_id': order.id,
747             'address_id': order.partner_shipping_id.id,
748             'note': order.note,
749             'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
750             'company_id': order.company_id.id,
751         }
752
753     def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
754         # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
755         """
756         Define ship_recreate for process after shipping exception
757         param order: sale order to which the order lines belong
758         param line: sale order line records to procure
759         param move_id: the ID of stock move
760         param proc_id: the ID of procurement
761         """
762         move_obj = self.pool.get('stock.move')
763         if order.state == 'shipping_except':
764             for pick in order.picking_ids:
765                 for move in pick.move_lines:
766                     if move.state == 'cancel':
767                         mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
768                         if mov_ids:
769                             for mov in move_obj.browse(cr, uid, mov_ids):
770                                 # 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?
771                                 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
772                                 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
773         return True
774
775     def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
776         date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)    
777         date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
778         return date_planned
779
780     def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
781         """Create the required procurements to supply sale order lines, also connecting
782         the procurements to appropriate stock moves in order to bring the goods to the
783         sale order's requested location.
784
785         If ``picking_id`` is provided, the stock moves will be added to it, otherwise
786         a standard outgoing picking will be created to wrap the stock moves, as returned
787         by :meth:`~._prepare_order_picking`.
788
789         Modules that wish to customize the procurements or partition the stock moves over
790         multiple stock pickings may override this method and call ``super()`` with
791         different subsets of ``order_lines`` and/or preset ``picking_id`` values.
792
793         :param browse_record order: sale order to which the order lines belong
794         :param list(browse_record) order_lines: sale order line records to procure
795         :param int picking_id: optional ID of a stock picking to which the created stock moves
796                                will be added. A new picking will be created if ommitted.
797         :return: True
798         """
799         move_obj = self.pool.get('stock.move')
800         picking_obj = self.pool.get('stock.picking')
801         procurement_obj = self.pool.get('procurement.order')
802         proc_ids = []
803
804         for line in order_lines:
805             if line.state == 'done':
806                 continue
807
808             date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
809
810             if line.product_id:
811                 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
812                     if not picking_id:
813                         picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
814                     move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
815                 else:
816                     # a service has no stock move
817                     move_id = False
818
819                 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
820                 proc_ids.append(proc_id)
821                 line.write({'procurement_id': proc_id})
822                 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
823
824         wf_service = netsvc.LocalService("workflow")
825         if picking_id:
826             wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
827
828         for proc_id in proc_ids:
829             wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
830
831         val = {}
832         if order.state == 'shipping_except':
833             val['state'] = 'progress'
834             val['shipped'] = False
835
836             if (order.order_policy == 'manual'):
837                 for line in order.order_line:
838                     if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
839                         val['state'] = 'manual'
840                         break
841         order.write(val)
842         return True
843
844     def action_ship_create(self, cr, uid, ids, context=None):
845         for order in self.browse(cr, uid, ids, context=context):
846             self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
847         return True
848
849     def action_ship_end(self, cr, uid, ids, context=None):
850         for order in self.browse(cr, uid, ids, context=context):
851             val = {'shipped': True}
852             if order.state == 'shipping_except':
853                 val['state'] = 'progress'
854                 if (order.order_policy == 'manual'):
855                     for line in order.order_line:
856                         if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
857                             val['state'] = 'manual'
858                             break
859             for line in order.order_line:
860                 towrite = []
861                 if line.state == 'exception':
862                     towrite.append(line.id)
863                 if towrite:
864                     self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
865             self.write(cr, uid, [order.id], val)
866         return True
867
868     def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
869         invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
870         for inv in invs:
871             part = inv['partner_id'] and inv['partner_id'][0]
872             pr = inv['amount_untaxed'] or 0.0
873             partnertype = 'customer'
874             eventtype = 'sale'
875             event = {
876                 'name': 'Order: '+name,
877                 'som': False,
878                 'description': 'Order '+str(inv['id']),
879                 'document': '',
880                 'partner_id': part,
881                 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
882                 'user_id': uid,
883                 'partner_type': partnertype,
884                 'probability': 1.0,
885                 'planned_revenue': pr,
886                 'planned_cost': 0.0,
887                 'type': eventtype
888             }
889             self.pool.get('res.partner.event').create(cr, uid, event)
890
891     def has_stockable_products(self, cr, uid, ids, *args):
892         for order in self.browse(cr, uid, ids):
893             for order_line in order.order_line:
894                 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
895                     return True
896         return False
897 sale_order()
898
899 # TODO add a field price_unit_uos
900 # - update it on change product and unit price
901 # - use it in report if there is a uos
902 class sale_order_line(osv.osv):
903
904     def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
905         tax_obj = self.pool.get('account.tax')
906         cur_obj = self.pool.get('res.currency')
907         res = {}
908         if context is None:
909             context = {}
910         for line in self.browse(cr, uid, ids, context=context):
911             price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
912             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)
913             cur = line.order_id.pricelist_id.currency_id
914             res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
915         return res
916
917     def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
918         res = {}
919         for line in self.browse(cr, uid, ids, context=context):
920             try:
921                 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
922             except:
923                 res[line.id] = 1
924         return res
925
926     def _get_uom_id(self, cr, uid, *args):
927         try:
928             proxy = self.pool.get('ir.model.data')
929             result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
930             return result[1]
931         except Exception, ex:
932             return False
933
934     _name = 'sale.order.line'
935     _description = 'Sales Order Line'
936     _columns = {
937         'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
938         'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
939         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
940         '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)]}),
941         'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
942         'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
943         'invoiced': fields.boolean('Invoiced', readonly=True),
944         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
945         'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
946         'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
947         'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
948         'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
949             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."),
950         'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
951         'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
952         'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
953         'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
954         'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
955         'product_uos': fields.many2one('product.uom', 'Product UoS'),
956         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
957         'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
958         'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
959         'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
960         'notes': fields.text('Notes'),
961         'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
962         'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
963                 help='* The \'Draft\' state is set when the related sales order in draft state. \
964                     \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
965                     \n* The \'Exception\' state is set when the related sales order is set as exception. \
966                     \n* The \'Done\' state is set when the sales order line has been picked. \
967                     \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
968         'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
969         'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
970         'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
971     }
972     _order = 'sequence, id'
973     _defaults = {
974         'product_uom' : _get_uom_id,
975         'discount': 0.0,
976         'delay': 0.0,
977         'product_uom_qty': 1,
978         'product_uos_qty': 1,
979         'sequence': 10,
980         'invoiced': 0,
981         'state': 'draft',
982         'type': 'make_to_stock',
983         'product_packaging': False,
984         'price_unit': 0.0,
985     }
986
987     def invoice_line_create(self, cr, uid, ids, context=None):
988         if context is None:
989             context = {}
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         create_ids = []
1010         sales = {}
1011         for line in self.browse(cr, uid, ids, context=context):
1012             if not line.invoiced:
1013                 if line.product_id:
1014                     a = line.product_id.product_tmpl_id.property_account_income.id
1015                     if not a:
1016                         a = line.product_id.categ_id.property_account_income_categ.id
1017                     if not a:
1018                         raise osv.except_osv(_('Error !'),
1019                                 _('There is no income account defined ' \
1020                                         'for this product: "%s" (id:%d)') % \
1021                                         (line.product_id.name, line.product_id.id,))
1022                 else:
1023                     prop = self.pool.get('ir.property').get(cr, uid,
1024                             'property_account_income_categ', 'product.category',
1025                             context=context)
1026                     a = prop and prop.id or False
1027                 uosqty = _get_line_qty(line)
1028                 uos_id = _get_line_uom(line)
1029                 pu = 0.0
1030                 if uosqty:
1031                     pu = round(line.price_unit * line.product_uom_qty / uosqty,
1032                             self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1033                 fpos = line.order_id.fiscal_position or False
1034                 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
1035                 if not a:
1036                     raise osv.except_osv(_('Error !'),
1037                                 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1038                 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
1039                     'name': line.name,
1040                     'origin': line.order_id.name,
1041                     'account_id': a,
1042                     'price_unit': pu,
1043                     'quantity': uosqty,
1044                     'discount': line.discount,
1045                     'uos_id': uos_id,
1046                     'product_id': line.product_id.id or False,
1047                     'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1048                     'note': line.notes,
1049                     'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1050                 })
1051                 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1052                 self.write(cr, uid, [line.id], {'invoiced': True})
1053                 sales[line.order_id.id] = True
1054                 create_ids.append(inv_id)
1055         # Trigger workflow events
1056         wf_service = netsvc.LocalService("workflow")
1057         for sid in sales.keys():
1058             wf_service.trg_write(uid, 'sale.order', sid, cr)
1059         return create_ids
1060
1061     def button_cancel(self, cr, uid, ids, context=None):
1062         for line in self.browse(cr, uid, ids, context=context):
1063             if line.invoiced:
1064                 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1065             for move_line in line.move_ids:
1066                 if move_line.state != 'cancel':
1067                     raise osv.except_osv(
1068                             _('Could not cancel sales order line!'),
1069                             _('You must first cancel stock moves attached to this sales order line.'))
1070         return self.write(cr, uid, ids, {'state': 'cancel'})
1071
1072     def button_confirm(self, cr, uid, ids, context=None):
1073         return self.write(cr, uid, ids, {'state': 'confirmed'})
1074
1075     def button_done(self, cr, uid, ids, context=None):
1076         wf_service = netsvc.LocalService("workflow")
1077         res = self.write(cr, uid, ids, {'state': 'done'})
1078         for line in self.browse(cr, uid, ids, context=context):
1079             wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1080         return res
1081
1082     def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1083         product_obj = self.pool.get('product.product')
1084         if not product_id:
1085             return {'value': {'product_uom': product_uos,
1086                 'product_uom_qty': product_uos_qty}, 'domain': {}}
1087
1088         product = product_obj.browse(cr, uid, product_id)
1089         value = {
1090             'product_uom': product.uom_id.id,
1091         }
1092         # FIXME must depend on uos/uom of the product and not only of the coeff.
1093         try:
1094             value.update({
1095                 'product_uom_qty': product_uos_qty / product.uos_coeff,
1096                 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1097             })
1098         except ZeroDivisionError:
1099             pass
1100         return {'value': value}
1101
1102     def copy_data(self, cr, uid, id, default=None, context=None):
1103         if not default:
1104             default = {}
1105         default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1106         return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1107
1108     def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1109                                    partner_id=False, packaging=False, flag=False, context=None):
1110         if not product:
1111             return {'value': {'product_packaging': False}}
1112         product_obj = self.pool.get('product.product')
1113         product_uom_obj = self.pool.get('product.uom')
1114         pack_obj = self.pool.get('product.packaging')
1115         warning = {}
1116         result = {}
1117         warning_msgs = ''
1118         if flag:
1119             res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1120                     product=product, qty=qty, uom=uom, partner_id=partner_id,
1121                     packaging=packaging, flag=False, context=context)
1122             warning_msgs = res.get('warning') and res['warning']['message']
1123
1124         products = product_obj.browse(cr, uid, product, context=context)
1125         if not products.packaging:
1126             packaging = result['product_packaging'] = False
1127         elif not packaging and products.packaging and not flag:
1128             packaging = products.packaging[0].id
1129             result['product_packaging'] = packaging
1130
1131         if packaging:
1132             default_uom = products.uom_id and products.uom_id.id
1133             pack = pack_obj.browse(cr, uid, packaging, context=context)
1134             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1135 #            qty = qty - qty % q + q
1136             if qty and (q and not (qty % q) == 0):
1137                 ean = pack.ean or _('(n/a)')
1138                 qty_pack = pack.qty
1139                 type_ul = pack.ul
1140                 if not warning_msgs:
1141                     warn_msg = _("You selected a quantity of %d Units.\n"
1142                                 "But it's not compatible with the selected packaging.\n"
1143                                 "Here is a proposition of quantities according to the packaging:\n"
1144                                 "EAN: %s Quantity: %s Type of ul: %s") % \
1145                                     (qty, ean, qty_pack, type_ul.name)
1146                     warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1147                 warning = {
1148                        'title': _('Configuration Error !'),
1149                        'message': warning_msgs
1150                 }
1151             result['product_uom_qty'] = qty
1152
1153         return {'value': result, 'warning': warning}
1154
1155     def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1156             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1157             lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1158         context = context or {}
1159         lang = lang or context.get('lang',False)
1160         if not  partner_id:
1161             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.'))
1162         warning = {}
1163         product_uom_obj = self.pool.get('product.uom')
1164         partner_obj = self.pool.get('res.partner')
1165         product_obj = self.pool.get('product.product')
1166         context = {'lang': lang, 'partner_id': partner_id}
1167         if partner_id:
1168             lang = partner_obj.browse(cr, uid, partner_id).lang
1169         context_partner = {'lang': lang, 'partner_id': partner_id}
1170
1171         if not product:
1172             return {'value': {'th_weight': 0, 'product_packaging': False,
1173                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1174                    'product_uos': []}}
1175         if not date_order:
1176             date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1177
1178         res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1179         result = res.get('value', {})
1180         warning_msgs = res.get('warning') and res['warning']['message'] or ''
1181         product_obj = product_obj.browse(cr, uid, product, context=context)
1182
1183         uom2 = False
1184         if uom:
1185             uom2 = product_uom_obj.browse(cr, uid, uom)
1186             if product_obj.uom_id.category_id.id != uom2.category_id.id:
1187                 uom = False
1188         if uos:
1189             if product_obj.uos_id:
1190                 uos2 = product_uom_obj.browse(cr, uid, uos)
1191                 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1192                     uos = False
1193             else:
1194                 uos = False
1195         if product_obj.description_sale:
1196             result['notes'] = product_obj.description_sale
1197         fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1198         if update_tax: #The quantity only have changed
1199             result['delay'] = (product_obj.sale_delay or 0.0)
1200             result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1201             result.update({'type': product_obj.procure_method})
1202
1203         if not flag:
1204             result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1205         domain = {}
1206         if (not uom) and (not uos):
1207             result['product_uom'] = product_obj.uom_id.id
1208             if product_obj.uos_id:
1209                 result['product_uos'] = product_obj.uos_id.id
1210                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1211                 uos_category_id = product_obj.uos_id.category_id.id
1212             else:
1213                 result['product_uos'] = False
1214                 result['product_uos_qty'] = qty
1215                 uos_category_id = False
1216             result['th_weight'] = qty * product_obj.weight
1217             domain = {'product_uom':
1218                         [('category_id', '=', product_obj.uom_id.category_id.id)],
1219                         'product_uos':
1220                         [('category_id', '=', uos_category_id)]}
1221
1222         elif uos and not uom: # only happens if uom is False
1223             result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1224             result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1225             result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1226         elif uom: # whether uos is set or not
1227             default_uom = product_obj.uom_id and product_obj.uom_id.id
1228             q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1229             if product_obj.uos_id:
1230                 result['product_uos'] = product_obj.uos_id.id
1231                 result['product_uos_qty'] = qty * product_obj.uos_coeff
1232             else:
1233                 result['product_uos'] = False
1234                 result['product_uos_qty'] = qty
1235             result['th_weight'] = q * product_obj.weight        # Round the quantity up
1236
1237         if not uom2:
1238             uom2 = product_obj.uom_id
1239         if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1240           and (product_obj.procure_method=='make_to_stock'):
1241             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1242                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1243                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1244                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1245             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1246         # get unit price
1247
1248         if not pricelist:
1249             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1250                     'Please set one before choosing a product.')
1251             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1252         else:
1253             price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1254                     product, qty or 1.0, partner_id, {
1255                         'uom': uom or result.get('product_uom'),
1256                         'date': date_order,
1257                         })[pricelist]
1258             if price is False:
1259                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1260                         "You have to change either the product, the quantity or the pricelist.")
1261
1262                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1263             else:
1264                 result.update({'price_unit': price})
1265         if warning_msgs:
1266             warning = {
1267                        'title': _('Configuration Error !'),
1268                        'message' : warning_msgs
1269                     }
1270         return {'value': result, 'domain': domain, 'warning': warning}
1271
1272     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1273             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1274             lang=False, update_tax=True, date_order=False, context=None):
1275         context = context or {}
1276         lang = lang or ('lang' in context and context['lang'])
1277         res = self.product_id_change(cursor, user, ids, pricelist, product,
1278                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1279                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1280                 date_order=date_order)
1281         if 'product_uom' in res['value']:
1282             del res['value']['product_uom']
1283         if not uom:
1284             res['value']['price_unit'] = 0.0
1285         return res
1286
1287     def unlink(self, cr, uid, ids, context=None):
1288         if context is None:
1289             context = {}
1290         """Allows to delete sales order lines in draft,cancel states"""
1291         for rec in self.browse(cr, uid, ids, context=context):
1292             if rec.state not in ['draft', 'cancel']:
1293                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1294         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1295
1296 sale_order_line()
1297
1298 class sale_config_picking_policy(osv.osv_memory):
1299     _name = 'sale.config.picking_policy'
1300     _inherit = 'res.config'
1301
1302     _columns = {
1303         'name': fields.char('Name', size=64),
1304         'sale_orders': fields.boolean('Based on Sales Orders',),
1305         'deli_orders': fields.boolean('Based on Delivery Orders'),
1306         'task_work': fields.boolean('Based on Tasks\' Work'),
1307         'timesheet': fields.boolean('Based on Timesheet'),
1308         'order_policy': fields.selection([
1309             ('manual', 'Invoice Based on Sales Orders'),
1310             ('picking', 'Invoice Based on Deliveries'),
1311         ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1312         'charge_delivery': fields.boolean('Do you charge the delivery?'),
1313         'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1314     }
1315     _defaults = {
1316         'order_policy': 'manual',
1317         '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,
1318     }
1319
1320     def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1321         res = {}
1322         if sale:
1323             res.update({'order_policy': 'manual'})
1324         elif deli:
1325             res.update({'order_policy': 'picking'})
1326         return {'value':res}
1327
1328     def execute(self, cr, uid, ids, context=None):
1329         ir_values_obj = self.pool.get('ir.values')
1330         data_obj = self.pool.get('ir.model.data')
1331         menu_obj = self.pool.get('ir.ui.menu')
1332         module_obj = self.pool.get('ir.module.module')
1333         module_upgrade_obj = self.pool.get('base.module.upgrade')
1334         module_name = []
1335
1336         group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1337
1338         wizard = self.browse(cr, uid, ids)[0]
1339
1340         if wizard.sale_orders:
1341             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1342             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1343
1344         if wizard.deli_orders:
1345             menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1346             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1347
1348         if wizard.task_work:
1349             module_name.append('project_timesheet')
1350             module_name.append('project_mrp')
1351             module_name.append('account_analytic_analysis')
1352
1353         if wizard.timesheet:
1354             module_name.append('account_analytic_analysis')
1355
1356         if wizard.charge_delivery:
1357             module_name.append('delivery')
1358
1359         if len(module_name):
1360             module_ids = []
1361             need_install = False
1362             module_ids = []
1363             for module in module_name:
1364                 data_id = module_obj.name_search(cr, uid , module, [], '=')
1365                 module_ids.append(data_id[0][0])
1366
1367             for module in module_obj.browse(cr, uid, module_ids):
1368                 if module.state == 'uninstalled':
1369                     module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1370                     need_install = True
1371                     cr.commit()
1372             if need_install:
1373                 pooler.restart_pool(cr.dbname, update_module=True)[1]
1374
1375         if wizard.time_unit:
1376             prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1377             product_obj = self.pool.get('product.product')
1378             product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1379
1380         ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1381         if wizard.task_work and wizard.time_unit:
1382             company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1383             self.pool.get('res.company').write(cr, uid, [company_id], {
1384                 'project_time_mode_id': wizard.time_unit.id
1385             }, context=context)
1386
1387 sale_config_picking_policy()
1388
1389 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: