1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from tools.translate import _
28 import decimal_precision as dp
31 class sale_shop(osv.osv):
33 _description = "Sales Shop"
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),
43 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
48 class sale_order(osv.osv):
50 _description = "Sales Order"
52 def copy(self, cr, uid, id, default=None, context=None):
60 'date_confirm': False,
61 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
63 return super(sale_order, self).copy(cr, uid, id, default, context=context)
65 def _amount_line_tax(self, cr, uid, line, context=None):
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)
71 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
72 cur_obj = self.pool.get('res.currency')
74 for order in self.browse(cr, uid, ids, context=context):
76 'amount_untaxed': 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']
91 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
97 tmp[id] = {'picked': 0.0, 'total': 0.0}
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
103 stock_picking p on (p.id=m.picking_id)
105 procurement_order mp on (mp.move_id=m.id)
107 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
109 for item in cr.dictfetchall():
110 if item['move_state'] == 'cancel':
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
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
122 for order in self.browse(cr, uid, ids, context=context):
124 res[order.id] = 100.0
126 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
129 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
131 for sale in self.browse(cursor, user, ids, context=context):
136 for invoice in sale.invoice_ids:
137 if invoice.state not in ('draft', 'cancel'):
138 tot += invoice.amount_untaxed
140 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
145 def _invoiced(self, cursor, user, ids, name, arg, context=None):
147 for sale in self.browse(cursor, user, ids, context=context):
149 for invoice in sale.invoice_ids:
150 if invoice.state != 'paid':
153 if not sale.invoice_ids:
157 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
166 clause += 'AND inv.state = \'paid\''
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 '
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()
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())
184 return [('id', '=', 0)]
185 return [('id', 'in', [x[0] for x in res])]
187 def _get_order(self, cr, uid, ids, context=None):
189 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
190 result[line.order_id.id] = True
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', 'To Invoice'),
203 ('progress', 'In Progress'),
204 ('shipping_except', 'Shipping Exception'),
205 ('invoice_except', 'Invoice Exception'),
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.datetime('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."),
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', 'Pay before delivery'),
223 ('manual', 'Deliver & invoice on demand'),
224 ('picking', 'Invoice based on deliveries'),
225 ('postpaid', 'Invoice on order after delivery'),
226 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
227 help="""The Invoice 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 'Deliver & Invoice on demand' will create the picking order directly and wait for the user to manually click on the 'Invoice' button to generate the draft invoice based on the sale order or the sale order lines.
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 based on deliveries' 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."),
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'),
245 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
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),
250 multi='sums', help="The amount without tax."),
251 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
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),
256 multi='sums', help="The tax amount."),
257 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
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),
262 multi='sums', help="The total amount."),
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)
270 'picking_policy': 'direct',
271 'date_order': lambda *a: time.strftime('%Y-%m-%d'),
272 'order_policy': 'manual',
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'],
282 ('name_uniq', 'unique(name)', 'Order Reference must be unique !'),
287 def unlink(self, cr, uid, ids, context=None):
288 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
290 for s in sale_orders:
291 if s['state'] in ['draft', 'cancel']:
292 unlink_ids.append(s['id'])
294 raise osv.except_osv(_('Invalid action !'), _('In order to delete a confirmed sale order, you must cancel it before ! To cancel a sale order, you must first cancel related picking or delivery orders.'))
296 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
298 def onchange_shop_id(self, cr, uid, ids, shop_id):
301 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
302 v['project_id'] = shop.project_id.id
303 # Que faire si le client a une pricelist a lui ?
304 if shop.pricelist_id.id:
305 v['pricelist_id'] = shop.pricelist_id.id
308 def action_cancel_draft(self, cr, uid, ids, *args):
311 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
312 line_ids = map(lambda x: x[0], cr.fetchall())
313 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
314 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
315 wf_service = netsvc.LocalService("workflow")
317 # Deleting the existing instance of workflow for SO
318 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
319 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
320 for (id,name) in self.name_get(cr, uid, ids):
321 message = _("The sales order '%s' has been set in draft state.") %(name,)
322 self.log(cr, uid, id, message)
325 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
327 if (not pricelist_id) or (not order_lines):
330 'title': _('Pricelist Warning!'),
331 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
333 return {'warning': warning}
335 def onchange_partner_id(self, cr, uid, ids, part):
337 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
339 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
340 part = self.pool.get('res.partner').browse(cr, uid, part)
341 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
342 payment_term = part.property_payment_term and part.property_payment_term.id or False
343 fiscal_position = part.property_account_position and part.property_account_position.id or False
344 dedicated_salesman = part.user_id and part.user_id.id or uid
346 'partner_invoice_id': addr['invoice'],
347 'partner_order_id': addr['contact'],
348 'partner_shipping_id': addr['delivery'],
349 'payment_term': payment_term,
350 'fiscal_position': fiscal_position,
351 'user_id': dedicated_salesman,
354 val['pricelist_id'] = pricelist
355 return {'value': val}
357 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
361 if policy == 'prepaid':
363 elif policy == 'picking':
364 inv_qty = 'procurement'
365 return {'value': {'invoice_quantity': inv_qty}}
367 def write(self, cr, uid, ids, vals, context=None):
368 if vals.get('order_policy', False):
369 if vals['order_policy'] == 'prepaid':
370 vals.update({'invoice_quantity': 'order'})
371 elif vals['order_policy'] == 'picking':
372 vals.update({'invoice_quantity': 'procurement'})
373 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
375 def create(self, cr, uid, vals, context=None):
376 if vals.get('order_policy', False):
377 if vals['order_policy'] == 'prepaid':
378 vals.update({'invoice_quantity': 'order'})
379 if vals['order_policy'] == 'picking':
380 vals.update({'invoice_quantity': 'procurement'})
381 return super(sale_order, self).create(cr, uid, vals, context=context)
383 def button_dummy(self, cr, uid, ids, context=None):
386 #FIXME: the method should return the list of invoices created (invoice_ids)
387 # and not the id of the last invoice created (res). The problem is that we
388 # cannot change it directly since the method is called by the sales order
389 # workflow and I suppose it expects a single id...
390 def _inv_get(self, cr, uid, order, context=None):
393 def _make_invoice(self, cr, uid, order, lines, context=None):
394 journal_obj = self.pool.get('account.journal')
395 inv_obj = self.pool.get('account.invoice')
396 obj_invoice_line = self.pool.get('account.invoice.line')
400 journal_ids = journal_obj.search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1)
402 raise osv.except_osv(_('Error !'),
403 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
404 a = order.partner_id.property_account_receivable.id
405 pay_term = order.payment_term and order.payment_term.id or False
406 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
407 from_line_invoice_ids = []
408 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
409 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
410 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
411 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
412 for preinv in order.invoice_ids:
413 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
414 for preline in preinv.invoice_line:
415 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
416 lines.append(inv_line_id)
418 'name': order.client_order_ref or '',
419 'origin': order.name,
420 'type': 'out_invoice',
421 'reference': order.client_order_ref or order.name,
423 'partner_id': order.partner_id.id,
424 'journal_id': journal_ids[0],
425 'address_invoice_id': order.partner_invoice_id.id,
426 'address_contact_id': order.partner_order_id.id,
427 'invoice_line': [(6, 0, lines)],
428 'currency_id': order.pricelist_id.currency_id.id,
429 'comment': order.note,
430 'payment_term': pay_term,
431 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
432 'date_invoice': context.get('date_invoice',False),
433 'company_id': order.company_id.id,
434 'user_id': order.user_id and order.user_id.id or False
436 inv.update(self._inv_get(cr, uid, order))
437 inv_id = inv_obj.create(cr, uid, inv, context=context)
438 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime('%Y-%m-%d'))
439 if data.get('value', False):
440 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
441 inv_obj.button_compute(cr, uid, [inv_id])
444 def manual_invoice(self, cr, uid, ids, context=None):
445 mod_obj = self.pool.get('ir.model.data')
446 wf_service = netsvc.LocalService("workflow")
450 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
451 inv_ids.add(record.id)
452 # inv_ids would have old invoices if any
454 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
455 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
456 inv_ids1.add(record.id)
457 inv_ids = list(inv_ids1.difference(inv_ids))
459 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
460 res_id = res and res[1] or False,
463 'name': _('Customer Invoices'),
467 'res_model': 'account.invoice',
468 'context': "{'type':'out_invoice'}",
469 'type': 'ir.actions.act_window',
472 'res_id': inv_ids and inv_ids[0] or False,
475 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
479 picking_obj = self.pool.get('stock.picking')
480 invoice = self.pool.get('account.invoice')
481 obj_sale_order_line = self.pool.get('sale.order.line')
482 partner_currency = {}
485 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
486 # last day of the last month as invoice date
488 context['date_inv'] = date_inv
489 for o in self.browse(cr, uid, ids, context=context):
490 currency_id = o.pricelist_id.currency_id.id
491 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
492 raise osv.except_osv(
494 _('You cannot group sales having different currencies for the same partner.'))
496 partner_currency[o.partner_id.id] = currency_id
498 for line in o.order_line:
501 elif (line.state in states):
502 lines.append(line.id)
503 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
505 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
507 for o in self.browse(cr, uid, ids, context=context):
508 for i in o.invoice_ids:
509 if i.state == 'draft':
511 for val in invoices.values():
513 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
516 invoice_ref += o.name + '|'
517 self.write(cr, uid, [o.id], {'state': 'progress'})
518 if o.order_policy == 'picking':
519 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
520 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
521 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
523 for order, il in val:
524 res = self._make_invoice(cr, uid, order, il, context=context)
525 invoice_ids.append(res)
526 self.write(cr, uid, [order.id], {'state': 'progress'})
527 if order.order_policy == 'picking':
528 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
529 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
532 def action_invoice_cancel(self, cr, uid, ids, context=None):
535 for sale in self.browse(cr, uid, ids, context=context):
536 for line in sale.order_line:
538 # Check if the line is invoiced (has asociated invoice
539 # lines from non-cancelled invoices).
542 for iline in line.invoice_lines:
543 if iline.invoice_id and iline.invoice_id.state != 'cancel':
546 # Update the line (only when needed)
547 if line.invoiced != invoiced:
548 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
549 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
552 def action_invoice_end(self, cr, uid, ids, context=None):
553 for order in self.browse(cr, uid, ids, context=context):
555 # Update the sale order lines state (and invoiced flag).
557 for line in order.order_line:
560 # Check if the line is invoiced (has asociated invoice
561 # lines from non-cancelled invoices).
564 for iline in line.invoice_lines:
565 if iline.invoice_id and iline.invoice_id.state != 'cancel':
568 if line.invoiced != invoiced:
569 vals['invoiced'] = invoiced
570 # If the line was in exception state, now it gets confirmed.
571 if line.state == 'exception':
572 vals['state'] = 'confirmed'
573 # Update the line (only when needed).
575 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
577 # Update the sales order state.
579 if order.state == 'invoice_except':
580 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
583 def action_cancel(self, cr, uid, ids, context=None):
584 wf_service = netsvc.LocalService("workflow")
587 sale_order_line_obj = self.pool.get('sale.order.line')
588 proc_obj = self.pool.get('procurement.order')
589 for sale in self.browse(cr, uid, ids, context=context):
590 for pick in sale.picking_ids:
591 if pick.state not in ('draft', 'cancel'):
592 raise osv.except_osv(
593 _('Could not cancel sales order !'),
594 _('You must first cancel all picking attached to this sales order.'))
595 if pick.state == 'cancel':
596 for mov in pick.move_lines:
597 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
599 for proc in proc_ids:
600 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
601 for r in self.read(cr, uid, ids, ['picking_ids']):
602 for pick in r['picking_ids']:
603 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
604 for inv in sale.invoice_ids:
605 if inv.state not in ('draft', 'cancel'):
606 raise osv.except_osv(
607 _('Could not cancel this sales order !'),
608 _('You must first cancel all invoices attached to this sales order.'))
609 for r in self.read(cr, uid, ids, ['invoice_ids']):
610 for inv in r['invoice_ids']:
611 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
612 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
614 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
615 self.log(cr, uid, sale.id, message)
616 self.write(cr, uid, ids, {'state': 'cancel'})
619 def action_wait(self, cr, uid, ids, *args):
620 for o in self.browse(cr, uid, ids):
621 if (o.order_policy == 'manual'):
622 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime('%Y-%m-%d')})
624 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime('%Y-%m-%d')})
625 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
626 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
627 self.log(cr, uid, o.id, message)
630 def procurement_lines_get(self, cr, uid, ids, *args):
632 for order in self.browse(cr, uid, ids, context={}):
633 for line in order.order_line:
634 if line.procurement_id:
635 res.append(line.procurement_id.id)
638 # if mode == 'finished':
639 # returns True if all lines are done, False otherwise
640 # if mode == 'canceled':
641 # returns True if there is at least one canceled line, False otherwise
642 def test_state(self, cr, uid, ids, mode, *args):
643 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
648 write_cancel_ids = []
649 for order in self.browse(cr, uid, ids, context={}):
650 for line in order.order_line:
651 if (not line.procurement_id) or (line.procurement_id.state=='done'):
652 if line.state != 'done':
653 write_done_ids.append(line.id)
656 if line.procurement_id:
657 if (line.procurement_id.state == 'cancel'):
659 if line.state != 'exception':
660 write_cancel_ids.append(line.id)
664 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
666 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
668 if mode == 'finished':
670 elif mode == 'canceled':
676 def action_ship_create(self, cr, uid, ids, *args):
677 wf_service = netsvc.LocalService("workflow")
679 move_obj = self.pool.get('stock.move')
680 proc_obj = self.pool.get('procurement.order')
681 company = self.pool.get('res.users').browse(cr, uid, uid).company_id
682 for order in self.browse(cr, uid, ids, context={}):
684 output_id = order.shop_id.warehouse_id.lot_output_id.id
686 for line in order.order_line:
688 date_planned = datetime.strptime(order.date_order, '%Y-%m-%d') + relativedelta(days=line.delay or 0.0)
689 date_planned = (date_planned - timedelta(days=company.security_lead)).strftime('%Y-%m-%d %H:%M:%S')
691 if line.state == 'done':
694 if line.product_id and line.product_id.product_tmpl_id.type in ('product', 'consu'):
695 location_id = order.shop_id.warehouse_id.lot_stock_id.id
697 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
698 picking_id = self.pool.get('stock.picking').create(cr, uid, {
700 'origin': order.name,
703 'move_type': order.picking_policy,
705 'address_id': order.partner_shipping_id.id,
707 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
708 'company_id': order.company_id.id,
710 move_id = self.pool.get('stock.move').create(cr, uid, {
711 'name': line.name[:64],
712 'picking_id': picking_id,
713 'product_id': line.product_id.id,
714 'date': date_planned,
715 'date_expected': date_planned,
716 'product_qty': line.product_uom_qty,
717 'product_uom': line.product_uom.id,
718 'product_uos_qty': line.product_uos_qty,
719 'product_uos': (line.product_uos and line.product_uos.id)\
720 or line.product_uom.id,
721 'product_packaging': line.product_packaging.id,
722 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
723 'location_id': location_id,
724 'location_dest_id': output_id,
725 'sale_line_id': line.id,
726 'tracking_id': False,
730 'company_id': order.company_id.id,
731 'price_unit': line.product_id.standard_price or 0.0
735 proc_id = self.pool.get('procurement.order').create(cr, uid, {
737 'origin': order.name,
738 'date_planned': date_planned,
739 'product_id': line.product_id.id,
740 'product_qty': line.product_uom_qty,
741 'product_uom': line.product_uom.id,
742 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
743 or line.product_uom_qty,
744 'product_uos': (line.product_uos and line.product_uos.id)\
745 or line.product_uom.id,
746 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
747 'procure_method': line.type,
749 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
750 'company_id': order.company_id.id,
751 'sale_line_id': line.id,
753 proc_ids.append(proc_id)
754 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'procurement_id': proc_id})
755 if order.state == 'shipping_except':
756 for pick in order.picking_ids:
757 for move in pick.move_lines:
758 if move.state == 'cancel':
759 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
761 for mov in move_obj.browse(cr, uid, mov_ids):
762 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
763 proc_obj.write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
768 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
770 for proc_id in proc_ids:
771 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
773 if order.state == 'shipping_except':
774 val['state'] = 'progress'
775 val['shipped'] = False
777 if (order.order_policy == 'manual'):
778 for line in order.order_line:
779 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
780 val['state'] = 'manual'
782 self.write(cr, uid, [order.id], val)
785 def action_ship_end(self, cr, uid, ids, context=None):
786 for order in self.browse(cr, uid, ids, context=context):
787 val = {'shipped': True}
788 if order.state == 'shipping_except':
789 val['state'] = 'progress'
790 if (order.order_policy == 'manual'):
791 for line in order.order_line:
792 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
793 val['state'] = 'manual'
795 for line in order.order_line:
797 if line.state == 'exception':
798 towrite.append(line.id)
800 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
801 self.write(cr, uid, [order.id], val)
804 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
805 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
807 part = inv['partner_id'] and inv['partner_id'][0]
808 pr = inv['amount_untaxed'] or 0.0
809 partnertype = 'customer'
812 'name': 'Order: '+name,
814 'description': 'Order '+str(inv['id']),
817 'date': time.strftime('%Y-%m-%d'),
819 'partner_type': partnertype,
821 'planned_revenue': pr,
825 self.pool.get('res.partner.event').create(cr, uid, event)
827 def has_stockable_products(self, cr, uid, ids, *args):
828 for order in self.browse(cr, uid, ids):
829 for order_line in order.order_line:
830 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
835 # TODO add a field price_unit_uos
836 # - update it on change product and unit price
837 # - use it in report if there is a uos
838 class sale_order_line(osv.osv):
840 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
841 tax_obj = self.pool.get('account.tax')
842 cur_obj = self.pool.get('res.currency')
846 for line in self.browse(cr, uid, ids, context=context):
847 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
848 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)
849 cur = line.order_id.pricelist_id.currency_id
850 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
853 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
855 for line in self.browse(cr, uid, ids, context=context):
857 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
862 def _get_uom_id(self, cr, uid, *args):
864 proxy = self.pool.get('ir.model.data')
865 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
867 except Exception, ex:
870 _name = 'sale.order.line'
871 _description = 'Sales Order Line'
873 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
874 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
875 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
876 '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)]}),
877 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
878 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
879 'invoiced': fields.boolean('Invoiced', readonly=True),
880 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
881 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
882 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
883 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
884 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
885 help="If 'on order', it triggers a procurement when the sale order is confirmed to create a task, purchase order or manufacturing order linked to this sale order line."),
886 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
887 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
888 'product_uom_qty': fields.float('Quantity (UoM)', digits=(16, 2), required=True, readonly=True, states={'draft': [('readonly', False)]}),
889 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
890 'product_uos_qty': fields.float('Quantity (UoS)', readonly=True, states={'draft': [('readonly', False)]}),
891 'product_uos': fields.many2one('product.uom', 'Product UoS'),
892 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
893 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
894 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
895 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
896 'notes': fields.text('Notes'),
897 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
898 'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
899 help='* The \'Draft\' state is set when the related sales order in draft state. \
900 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
901 \n* The \'Exception\' state is set when the related sales order is set as exception. \
902 \n* The \'Done\' state is set when the sales order line has been picked. \
903 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
904 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
905 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
906 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
908 _order = 'sequence, id'
910 'product_uom' : _get_uom_id,
913 'product_uom_qty': 1,
914 'product_uos_qty': 1,
918 'type': 'make_to_stock',
919 'product_packaging': False,
923 def invoice_line_create(self, cr, uid, ids, context=None):
927 def _get_line_qty(line):
928 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
930 return line.product_uos_qty or 0.0
931 return line.product_uom_qty
933 return self.pool.get('procurement.order').quantity_get(cr, uid,
934 line.procurement_id.id, context=context)
936 def _get_line_uom(line):
937 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
939 return line.product_uos.id
940 return line.product_uom.id
942 return self.pool.get('procurement.order').uom_get(cr, uid,
943 line.procurement_id.id, context=context)
947 for line in self.browse(cr, uid, ids, context=context):
948 if not line.invoiced:
950 a = line.product_id.product_tmpl_id.property_account_income.id
952 a = line.product_id.categ_id.property_account_income_categ.id
954 raise osv.except_osv(_('Error !'),
955 _('There is no income account defined ' \
956 'for this product: "%s" (id:%d)') % \
957 (line.product_id.name, line.product_id.id,))
959 prop = self.pool.get('ir.property').get(cr, uid,
960 'property_account_income_categ', 'product.category',
962 a = prop and prop.id or False
963 uosqty = _get_line_qty(line)
964 uos_id = _get_line_uom(line)
967 pu = round(line.price_unit * line.product_uom_qty / uosqty,
968 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
969 fpos = line.order_id.fiscal_position or False
970 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
972 raise osv.except_osv(_('Error !'),
973 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
974 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
976 'origin': line.order_id.name,
980 'discount': line.discount,
982 'product_id': line.product_id.id or False,
983 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
985 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
987 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
988 self.write(cr, uid, [line.id], {'invoiced': True})
989 sales[line.order_id.id] = True
990 create_ids.append(inv_id)
991 # Trigger workflow events
992 wf_service = netsvc.LocalService("workflow")
993 for sid in sales.keys():
994 wf_service.trg_write(uid, 'sale.order', sid, cr)
997 def button_cancel(self, cr, uid, ids, context=None):
998 for line in self.browse(cr, uid, ids, context=context):
1000 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1001 for move_line in line.move_ids:
1002 if move_line.state != 'cancel':
1003 raise osv.except_osv(
1004 _('Could not cancel sales order line!'),
1005 _('You must first cancel stock moves attached to this sales order line.'))
1006 return self.write(cr, uid, ids, {'state': 'cancel'})
1008 def button_confirm(self, cr, uid, ids, context=None):
1009 return self.write(cr, uid, ids, {'state': 'confirmed'})
1011 def button_done(self, cr, uid, ids, context=None):
1012 wf_service = netsvc.LocalService("workflow")
1013 res = self.write(cr, uid, ids, {'state': 'done'})
1014 for line in self.browse(cr, uid, ids, context=context):
1015 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1018 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1019 product_obj = self.pool.get('product.product')
1021 return {'value': {'product_uom': product_uos,
1022 'product_uom_qty': product_uos_qty}, 'domain': {}}
1024 product = product_obj.browse(cr, uid, product_id)
1026 'product_uom': product.uom_id.id,
1028 # FIXME must depend on uos/uom of the product and not only of the coeff.
1031 'product_uom_qty': product_uos_qty / product.uos_coeff,
1032 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1034 except ZeroDivisionError:
1036 return {'value': value}
1038 def copy_data(self, cr, uid, id, default=None, context=None):
1041 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1042 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1044 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1045 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1046 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1047 context = context or {}
1048 lang = lang or context.get('lang',False)
1050 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.'))
1053 product_uom_obj = self.pool.get('product.uom')
1054 partner_obj = self.pool.get('res.partner')
1055 product_obj = self.pool.get('product.product')
1056 context = {'lang': lang, 'partner_id': partner_id}
1058 lang = partner_obj.browse(cr, uid, partner_id).lang
1059 context_partner = {'lang': lang, 'partner_id': partner_id}
1062 return {'value': {'th_weight': 0, 'product_packaging': False,
1063 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1066 date_order = time.strftime('%Y-%m-%d')
1069 product_obj = product_obj.browse(cr, uid, product, context=context)
1070 if not packaging and product_obj.packaging:
1071 packaging = product_obj.packaging[0].id
1072 result['product_packaging'] = packaging
1075 default_uom = product_obj.uom_id and product_obj.uom_id.id
1076 pack = self.pool.get('product.packaging').browse(cr, uid, packaging, context=context)
1077 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1078 # qty = qty - qty % q + q
1079 if qty and (q and not (qty % q) == 0):
1080 ean = pack.ean or _('(n/a)')
1083 warn_msg = _("You selected a quantity of %d Units.\n"
1084 "But it's not compatible with the selected packaging.\n"
1085 "Here is a proposition of quantities according to the packaging:\n"
1086 "EAN: %s Quantity: %s Type of ul: %s") % \
1087 (qty, ean, qty_pack, type_ul.name)
1088 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1089 result['product_uom_qty'] = qty
1093 uom2 = product_uom_obj.browse(cr, uid, uom)
1094 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1097 if product_obj.uos_id:
1098 uos2 = product_uom_obj.browse(cr, uid, uos)
1099 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1103 if product_obj.description_sale:
1104 result['notes'] = product_obj.description_sale
1105 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1106 if update_tax: #The quantity only have changed
1107 result['delay'] = (product_obj.sale_delay or 0.0)
1108 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1109 result.update({'type': product_obj.procure_method})
1112 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1114 if (not uom) and (not uos):
1115 result['product_uom'] = product_obj.uom_id.id
1116 if product_obj.uos_id:
1117 result['product_uos'] = product_obj.uos_id.id
1118 result['product_uos_qty'] = qty * product_obj.uos_coeff
1119 uos_category_id = product_obj.uos_id.category_id.id
1121 result['product_uos'] = False
1122 result['product_uos_qty'] = qty
1123 uos_category_id = False
1124 result['th_weight'] = qty * product_obj.weight
1125 domain = {'product_uom':
1126 [('category_id', '=', product_obj.uom_id.category_id.id)],
1128 [('category_id', '=', uos_category_id)]}
1130 elif uos and not uom: # only happens if uom is False
1131 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1132 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1133 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1134 elif uom: # whether uos is set or not
1135 default_uom = product_obj.uom_id and product_obj.uom_id.id
1136 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1137 if product_obj.uos_id:
1138 result['product_uos'] = product_obj.uos_id.id
1139 result['product_uos_qty'] = qty * product_obj.uos_coeff
1141 result['product_uos'] = False
1142 result['product_uos_qty'] = qty
1143 result['th_weight'] = q * product_obj.weight # Round the quantity up
1146 uom2 = product_obj.uom_id
1147 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1148 and (product_obj.procure_method=='make_to_stock'):
1149 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1150 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1151 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1152 max(0,product_obj.qty_available), product_obj.uom_id.name)
1153 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1157 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1158 'Please set one before choosing a product.')
1159 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1161 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1162 product, qty or 1.0, partner_id, {
1163 'uom': uom or result.get('product_uom'),
1167 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1168 "You have to change either the product, the quantity or the pricelist.")
1170 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1172 result.update({'price_unit': price})
1175 'title': _('Configuration Error !'),
1176 'message' : warning_msgs
1178 return {'value': result, 'domain': domain, 'warning': warning}
1180 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1181 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1182 lang=False, update_tax=True, date_order=False, context=None):
1183 context = context or {}
1184 lang = lang or ('lang' in context and context['lang'])
1185 res = self.product_id_change(cursor, user, ids, pricelist, product,
1186 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1187 partner_id=partner_id, lang=lang, update_tax=update_tax,
1188 date_order=date_order)
1189 if 'product_uom' in res['value']:
1190 del res['value']['product_uom']
1192 res['value']['price_unit'] = 0.0
1195 def unlink(self, cr, uid, ids, context=None):
1198 """Allows to delete sales order lines in draft,cancel states"""
1199 for rec in self.browse(cr, uid, ids, context=context):
1200 if rec.state not in ['draft', 'cancel']:
1201 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1202 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1206 class sale_config_picking_policy(osv.osv_memory):
1207 _name = 'sale.config.picking_policy'
1208 _inherit = 'res.config'
1210 'name': fields.char('Name', size=64),
1211 'sale_orders': fields.boolean('Based on Sales Orders',),
1212 'deli_orders': fields.boolean('Based on Delivery Orders'),
1213 'task_work': fields.boolean('Based on Tasks\' Work'),
1214 'timesheet': fields.boolean('Based on Timesheet'),
1215 'order_policy': fields.selection([
1216 ('manual', 'Invoice Based on Sales Orders'),
1217 ('picking', 'Invoice Based on Deliveries'),
1218 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1219 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1220 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1223 'order_policy': 'manual',
1226 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1229 res.update({'order_policy': 'manual'})
1231 res.update({'order_policy': 'picking'})
1232 return {'value':res}
1234 def execute(self, cr, uid, ids, context=None):
1235 ir_values_obj = self.pool.get('ir.values')
1236 data_obj = self.pool.get('ir.model.data')
1237 menu_obj = self.pool.get('ir.ui.menu')
1238 module_obj = self.pool.get('ir.module.module')
1239 module_upgrade_obj = self.pool.get('base.module.upgrade')
1242 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1244 wizard = self.browse(cr, uid, ids)[0]
1246 if wizard.sale_orders:
1247 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1248 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1250 if wizard.deli_orders:
1251 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1252 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1254 if wizard.task_work:
1255 module_name.append('project_timesheet')
1256 module_name.append('account_analytic_analysis')
1258 if wizard.timesheet:
1259 module_name.append('account_analytic_analysis')
1261 if wizard.charge_delivery:
1262 module_name.append('delivery')
1264 if len(module_name):
1266 need_install = False
1268 for module in module_name:
1269 data_id = module_obj.name_search(cr,uid,module)
1270 module_ids.append(data_id[0][0])
1272 for module in module_obj.browse(cr, uid, module_ids):
1273 if module.state == 'uninstalled':
1274 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1278 pooler.restart_pool(cr.dbname, update_module=True)[1]
1280 if wizard.time_unit:
1281 prod_id = data_obj.get_object(cr, uid, 'hr_timesheet', 'product_consultant').id
1282 product_obj = self.pool.get('product.product')
1283 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1285 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1286 if wizard.task_work and wizard.time_unit:
1287 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1288 self.pool.get('res.company').write(cr, uid, [company_id], {
1289 'project_time_mode_id': wizard.time_unit.id
1292 sale_config_picking_policy()
1294 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: