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