[IMP] replace sale configuration wizard
[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 import pooler
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="Givwizard = self.browse(cr, uid, ids)[0]es 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, string='Picked', type='float'),
240         'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
241         'invoiced': fields.function(_invoiced, 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, 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, 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, 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, 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, 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'
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, context=None):
1017         context = context or {}
1018         lang = lang or ('lang' in context and context['lang'])
1019         if not  partner_id:
1020             raise osv.except_osv(_('No Customer Defined !'), _('You have to select a customer in the sales form !\nPlease set one customer before choosing a product.'))
1021         warning = {}
1022         warning_msgs = ''
1023         product_uom_obj = self.pool.get('product.uom')
1024         partner_obj = self.pool.get('res.partner')
1025         product_obj = self.pool.get('product.product')
1026         if partner_id:
1027             lang = partner_obj.browse(cr, uid, partner_id).lang
1028         context = {'lang': lang, 'partner_id': partner_id}
1029
1030         if not product:
1031             return {'value': {'th_weight': 0, 'product_packaging': False,
1032                 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1033                    'product_uos': []}}
1034         if not date_order:
1035             date_order = time.strftime('%Y-%m-%d')
1036
1037         result = {}
1038         product_obj = product_obj.browse(cr, uid, product, context=context)
1039         if not packaging and product_obj.packaging:
1040             packaging = product_obj.packaging[0].id
1041             result['product_packaging'] = packaging
1042         
1043         if packaging:
1044             default_uom = product_obj.uom_id and product_obj.uom_id.id
1045             pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1046             q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1047 #            qty = qty - qty % q + q
1048             if qty and (q and not (qty % q) == 0):
1049                 ean = pack.ean or _('(n/a)')
1050                 qty_pack = pack.qty
1051                 type_ul = pack.ul
1052                 warn_msg = _("You selected a quantity of %d Units.\n"
1053                             "But it's not compatible with the selected packaging.\n"
1054                             "Here is a proposition of quantities according to the packaging:\n"
1055                             "EAN: %s Quantity: %s Type of ul: %s") % \
1056                                 (qty, ean, qty_pack, type_ul.name)
1057                 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
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             warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1119                     (qty, uom2 and uom2.name or product_obj.uom_id.name,
1120                      max(0,product_obj.virtual_available), product_obj.uom_id.name,
1121                      max(0,product_obj.qty_available), product_obj.uom_id.name)
1122             warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1123         # get unit price
1124         
1125         if not pricelist:
1126             warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1127                     'Please set one before choosing a product.')
1128             warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
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 or result.get('product_uom'),
1133                         'date': date_order,
1134                         })[pricelist]
1135             if price is False:
1136                 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1137                         "You have to change either the product, the quantity or the pricelist.")
1138
1139                 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1140             else:
1141                 result.update({'price_unit': price})
1142         if warning_msgs:
1143             warning = {
1144                        'title': _('Configuration Error !'),
1145                        'message' : warning_msgs  
1146                     }
1147         return {'value': result, 'domain': domain, 'warning': warning}
1148
1149     def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1150             uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1151             lang=False, update_tax=True, date_order=False, context=None):
1152         context = context or {}
1153         lang = lang or ('lang' in context and context['lang'])
1154         res = self.product_id_change(cursor, user, ids, pricelist, product,
1155                 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1156                 partner_id=partner_id, lang=lang, update_tax=update_tax,
1157                 date_order=date_order)
1158         if 'product_uom' in res['value']:
1159             del res['value']['product_uom']
1160         if not uom:
1161             res['value']['price_unit'] = 0.0
1162         return res
1163
1164     def unlink(self, cr, uid, ids, context=None):
1165         if context is None:
1166             context = {}
1167         """Allows to delete sales order lines in draft,cancel states"""
1168         for rec in self.browse(cr, uid, ids, context=context):
1169             if rec.state not in ['draft', 'cancel']:
1170                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is %s !') %(rec.state,))
1171         return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1172
1173 sale_order_line()
1174
1175 class sale_config_picking_policy(osv.osv_memory):
1176     _name = 'sale.config.picking_policy'
1177     _inherit = 'res.config'
1178     '''def _get_options(cr, uid, ids, context=None):
1179         product_obj = self.pool.get('product.uom')
1180         uom_ids = product_obj.search(cr, uid, [])
1181         res = []
1182         for i in product_obj.browse(cr, uid, uom_ids):
1183             if i.category_id.name == 'Working Time':
1184                 res.append(i.id)
1185         
1186         return res'''
1187     _columns = {
1188         'name': fields.char('Name', size=64),
1189         'order_policy': fields.selection([
1190             ('manual', 'Invoice Based on Sales Orders'),
1191             ('picking', 'Invoice Based on Deliveries'),
1192         ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings.", readonly=True),
1193         'sale_orders': fields.boolean('Based on Sales Orders',),
1194         'deli_orders': fields.boolean('Based on Delivery Orders'),
1195         'task_work': fields.boolean('Based on Tasks\' Work'),
1196         'timesheet': fields.boolean('Based on Timesheet'),
1197         'charge_delivery': fields.boolean('Do you charge the delivery', readonly=True),
1198         
1199         'time_unit': fields.many2one('product.uom','Main Working Time Unit', readonly=True)
1200     }
1201     _defaults = {
1202         'order_policy': 'manual',
1203         
1204     }
1205     
1206     def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1207         res = {}
1208         
1209         if sale or deli:
1210             res.update({'order_policy': 'manual'})
1211         elif not sale and not deli:
1212             res.update({'order_policy': 'manual'})
1213         else:
1214             return {}
1215             
1216         return {'value':res}
1217     
1218         
1219     def execute(self, cr, uid, ids, context=None):
1220         ir_values_obj = self.pool.get('ir.values')
1221         data_obj = self.pool.get('ir.model.data')
1222         menu_obj = self.pool.get('ir.ui.menu')
1223         module_obj = self.pool.get('ir.module.module')
1224         module_upgrade_obj = self.pool.get('base.module.upgrade')
1225         module_name = []
1226         group_ids=[]
1227         group_name = ['group_sale_salesman','group_sale_manager']
1228         print cr, uid, ids
1229         print self.read(cr,uid,ids)
1230         for name in group_name:
1231             data_id = data_obj.name_search(cr, uid, name)
1232             group_ids.append(data_obj.browse(cr,uid,data_id[0][0]).res_id)
1233         
1234         wizard = self.browse(cr, uid, ids)[0]
1235         
1236         if wizard.sale_orders:
1237             menu_name = 'menu_invoicing_sales_order_lines'
1238             data_id = data_obj.name_search(cr, uid, menu_name)
1239             print data_id
1240             menu_id = data_obj.browse(cr,uid,data_id[0][0]).res_id
1241             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_ids[0]),(4,group_ids[1])]}) 
1242         
1243         if wizard.deli_orders:
1244             menu_name = 'menu_action_picking_list_to_invoice'
1245             data_id = data_obj.name_search(cr, uid, menu_name)
1246             print data_id
1247             menu_id = data_obj.browse(cr,uid,data_id[0][0]).res_id
1248             menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_ids[0]),(4,group_ids[1])]})
1249         
1250         if wizard.task_work:
1251             module_name.append('project_timesheet')
1252             module_name.append('account_analytic_analysis')
1253             
1254         if wizard.timesheet:
1255             module_name.append('account_analytic_analysis')
1256             
1257         if wizard.charge_delivery:
1258             module_name.append('delivery')    
1259         
1260         if wizard.time_unit:
1261             print "xyz"
1262           
1263         if len(module_name):
1264             module_ids = []
1265             need_install = False
1266             module_ids = []
1267             for module in module_name:
1268                 data_id = module_obj.name_search(cr,uid,module)
1269                 module_ids.append(data_id[0][0])
1270                
1271             for module in module_obj.browse(cr, uid, module_ids):
1272                 if module.state == 'uninstalled':
1273                     module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1274                     need_install = True
1275                     cr.commit()
1276             if need_install:
1277                 pooler.restart_pool(cr.dbname, update_module=True)[1]
1278         
1279         ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)  
1280         
1281    
1282 sale_config_picking_policy()
1283
1284 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: