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