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