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