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