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