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