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