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 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
29 import decimal_precision as dp
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
39 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
40 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
41 'company_id': fields.many2one('res.company', 'Company', required=False),
44 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
49 class sale_order(osv.osv):
51 _description = "Sales Order"
53 def copy(self, cr, uid, id, default=None, context=None):
61 'date_confirm': False,
62 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
64 return super(sale_order, self).copy(cr, uid, id, default, context=context)
66 def _amount_line_tax(self, cr, uid, line, context=None):
68 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']:
69 val += c.get('amount', 0.0)
72 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
73 cur_obj = self.pool.get('res.currency')
75 for order in self.browse(cr, uid, ids, context=context):
77 'amount_untaxed': 0.0,
82 cur = order.pricelist_id.currency_id
83 for line in order.order_line:
84 val1 += line.price_subtotal
85 val += self._amount_line_tax(cr, uid, line, context=context)
86 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
87 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
88 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
92 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
98 tmp[id] = {'picked': 0.0, 'total': 0.0}
100 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
104 stock_picking p on (p.id=m.picking_id)
106 procurement_order mp on (mp.move_id=m.id)
108 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
110 for item in cr.dictfetchall():
111 if item['move_state'] == 'cancel':
114 if item['picking_type'] == 'in':#this is a returned picking
115 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
116 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
117 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
119 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
120 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
121 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
123 for order in self.browse(cr, uid, ids, context=context):
125 res[order.id] = 100.0
127 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
130 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
132 for sale in self.browse(cursor, user, ids, context=context):
137 for invoice in sale.invoice_ids:
138 if invoice.state not in ('draft', 'cancel'):
139 tot += invoice.amount_untaxed
141 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
146 def _invoiced(self, cursor, user, ids, name, arg, context=None):
148 for sale in self.browse(cursor, user, ids, context=context):
150 for invoice in sale.invoice_ids:
151 if invoice.state != 'paid':
154 if not sale.invoice_ids:
158 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
167 clause += 'AND inv.state = \'paid\''
169 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
170 sale_clause = ', sale_order AS sale '
173 cursor.execute('SELECT rel.order_id ' \
174 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
175 'WHERE rel.invoice_id = inv.id ' + clause)
176 res = cursor.fetchall()
178 cursor.execute('SELECT sale.id ' \
179 'FROM sale_order AS sale ' \
180 'WHERE sale.id NOT IN ' \
181 '(SELECT rel.order_id ' \
182 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
183 res.extend(cursor.fetchall())
185 return [('id', '=', 0)]
186 return [('id', 'in', [x[0] for x in res])]
188 def _get_order(self, cr, uid, ids, context=None):
190 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
191 result[line.order_id.id] = True
195 'name': fields.char('Order Reference', size=64, required=True,
196 readonly=True, states={'draft': [('readonly', False)]}, select=True),
197 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
198 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
199 'client_order_ref': fields.char('Customer Reference', size=64),
200 'state': fields.selection([
201 ('draft', 'Quotation'),
202 ('waiting_date', 'Waiting Schedule'),
203 ('manual', 'To Invoice'),
204 ('progress', 'In Progress'),
205 ('shipping_except', 'Shipping Exception'),
206 ('invoice_except', 'Invoice Exception'),
208 ('cancel', 'Cancelled')
209 ], '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),
210 'date_order': fields.date('Ordered Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
211 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
212 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
213 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
214 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
215 '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."),
216 '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."),
217 'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
219 '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."),
220 'picking_policy': fields.selection([('direct', 'Deliver each products when available'), ('one', 'Deliver all products at once')],
221 '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?"""),
222 'order_policy': fields.selection([
223 ('prepaid', 'Pay before delivery'),
224 ('manual', 'Deliver & invoice on demand'),
225 ('picking', 'Invoice based on deliveries'),
226 ('postpaid', 'Invoice on order after delivery'),
227 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
228 help="""The Invoice Policy is used to synchronise invoice and delivery operations.
229 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
230 - 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.
231 - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
232 - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
233 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
234 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
236 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
237 '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)."),
238 '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."),
239 '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."),
240 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
241 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
242 'invoiced': fields.function(_invoiced, string='Paid',
243 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
244 'note': fields.text('Notes'),
246 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
248 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
249 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
251 multi='sums', help="The amount without tax."),
252 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
254 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
255 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
257 multi='sums', help="The tax amount."),
258 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
260 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
261 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
263 multi='sums', help="The total amount."),
265 '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)]}),
266 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
267 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
268 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
271 'picking_policy': 'direct',
272 'date_order': lambda *a: time.strftime(DEFAULT_SERVER_DATE_FORMAT),
273 'order_policy': 'manual',
275 'user_id': lambda obj, cr, uid, context: uid,
276 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
277 'invoice_quantity': 'order',
278 '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'],
279 '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'],
280 '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'],
283 ('name_uniq', 'unique(name)', 'Order Reference must be unique !'),
288 def unlink(self, cr, uid, ids, context=None):
289 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
291 for s in sale_orders:
292 if s['state'] in ['draft', 'cancel']:
293 unlink_ids.append(s['id'])
295 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.'))
297 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
299 def onchange_shop_id(self, cr, uid, ids, shop_id):
302 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
303 v['project_id'] = shop.project_id.id
304 # Que faire si le client a une pricelist a lui ?
305 if shop.pricelist_id.id:
306 v['pricelist_id'] = shop.pricelist_id.id
309 def action_cancel_draft(self, cr, uid, ids, *args):
312 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
313 line_ids = map(lambda x: x[0], cr.fetchall())
314 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
315 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
316 wf_service = netsvc.LocalService("workflow")
318 # Deleting the existing instance of workflow for SO
319 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
320 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
321 for (id,name) in self.name_get(cr, uid, ids):
322 message = _("The sales order '%s' has been set in draft state.") %(name,)
323 self.log(cr, uid, id, message)
326 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
328 if (not pricelist_id) or (not order_lines):
331 'title': _('Pricelist Warning!'),
332 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
334 return {'warning': warning}
336 def onchange_partner_id(self, cr, uid, ids, part):
338 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
340 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
341 part = self.pool.get('res.partner').browse(cr, uid, part)
342 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
343 payment_term = part.property_payment_term and part.property_payment_term.id or False
344 fiscal_position = part.property_account_position and part.property_account_position.id or False
345 dedicated_salesman = part.user_id and part.user_id.id or uid
347 'partner_invoice_id': addr['invoice'],
348 'partner_order_id': addr['contact'],
349 'partner_shipping_id': addr['delivery'],
350 'payment_term': payment_term,
351 'fiscal_position': fiscal_position,
352 'user_id': dedicated_salesman,
355 val['pricelist_id'] = pricelist
356 return {'value': val}
358 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
362 if policy == 'prepaid':
364 elif policy == 'picking':
365 inv_qty = 'procurement'
366 return {'value': {'invoice_quantity': inv_qty}}
368 def write(self, cr, uid, ids, vals, context=None):
369 if vals.get('order_policy', False):
370 if vals['order_policy'] == 'prepaid':
371 vals.update({'invoice_quantity': 'order'})
372 elif vals['order_policy'] == 'picking':
373 vals.update({'invoice_quantity': 'procurement'})
374 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
376 def create(self, cr, uid, vals, context=None):
377 if vals.get('order_policy', False):
378 if vals['order_policy'] == 'prepaid':
379 vals.update({'invoice_quantity': 'order'})
380 if vals['order_policy'] == 'picking':
381 vals.update({'invoice_quantity': 'procurement'})
382 return super(sale_order, self).create(cr, uid, vals, context=context)
384 def button_dummy(self, cr, uid, ids, context=None):
387 #FIXME: the method should return the list of invoices created (invoice_ids)
388 # and not the id of the last invoice created (res). The problem is that we
389 # cannot change it directly since the method is called by the sales order
390 # workflow and I suppose it expects a single id...
391 def _inv_get(self, cr, uid, order, context=None):
394 def _make_invoice(self, cr, uid, order, lines, context=None):
395 journal_obj = self.pool.get('account.journal')
396 inv_obj = self.pool.get('account.invoice')
397 obj_invoice_line = self.pool.get('account.invoice.line')
401 journal_ids = journal_obj.search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)], limit=1)
403 raise osv.except_osv(_('Error !'),
404 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
405 a = order.partner_id.property_account_receivable.id
406 pay_term = order.payment_term and order.payment_term.id or False
407 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
408 from_line_invoice_ids = []
409 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
410 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
411 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
412 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
413 for preinv in order.invoice_ids:
414 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
415 for preline in preinv.invoice_line:
416 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
417 lines.append(inv_line_id)
419 'name': order.client_order_ref or '',
420 'origin': order.name,
421 'type': 'out_invoice',
422 'reference': order.client_order_ref or order.name,
424 'partner_id': order.partner_id.id,
425 'journal_id': journal_ids[0],
426 'address_invoice_id': order.partner_invoice_id.id,
427 'address_contact_id': order.partner_order_id.id,
428 'invoice_line': [(6, 0, lines)],
429 'currency_id': order.pricelist_id.currency_id.id,
430 'comment': order.note,
431 'payment_term': pay_term,
432 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
433 'date_invoice': context.get('date_invoice',False),
434 'company_id': order.company_id.id,
435 'user_id': order.user_id and order.user_id.id or False
437 inv.update(self._inv_get(cr, uid, order))
438 inv_id = inv_obj.create(cr, uid, inv, context=context)
439 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], pay_term, time.strftime(DEFAULT_SERVER_DATE_FORMAT))
440 if data.get('value', False):
441 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
442 inv_obj.button_compute(cr, uid, [inv_id])
445 def manual_invoice(self, cr, uid, ids, context=None):
446 mod_obj = self.pool.get('ir.model.data')
447 wf_service = netsvc.LocalService("workflow")
451 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
452 inv_ids.add(record.id)
453 # inv_ids would have old invoices if any
455 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
456 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
457 inv_ids1.add(record.id)
458 inv_ids = list(inv_ids1.difference(inv_ids))
460 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
461 res_id = res and res[1] or False,
464 'name': _('Customer Invoices'),
468 'res_model': 'account.invoice',
469 'context': "{'type':'out_invoice'}",
470 'type': 'ir.actions.act_window',
473 'res_id': inv_ids and inv_ids[0] or False,
476 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
480 picking_obj = self.pool.get('stock.picking')
481 invoice = self.pool.get('account.invoice')
482 obj_sale_order_line = self.pool.get('sale.order.line')
483 partner_currency = {}
486 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
487 # last day of the last month as invoice date
489 context['date_inv'] = date_inv
490 for o in self.browse(cr, uid, ids, context=context):
491 currency_id = o.pricelist_id.currency_id.id
492 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
493 raise osv.except_osv(
495 _('You cannot group sales having different currencies for the same partner.'))
497 partner_currency[o.partner_id.id] = currency_id
499 for line in o.order_line:
502 elif (line.state in states):
503 lines.append(line.id)
504 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
506 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
508 for o in self.browse(cr, uid, ids, context=context):
509 for i in o.invoice_ids:
510 if i.state == 'draft':
512 for val in invoices.values():
514 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
517 invoice_ref += o.name + '|'
518 self.write(cr, uid, [o.id], {'state': 'progress'})
519 if o.order_policy == 'picking':
520 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
521 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
522 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
524 for order, il in val:
525 res = self._make_invoice(cr, uid, order, il, context=context)
526 invoice_ids.append(res)
527 self.write(cr, uid, [order.id], {'state': 'progress'})
528 if order.order_policy == 'picking':
529 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
530 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
533 def action_invoice_cancel(self, cr, uid, ids, context=None):
536 for sale in self.browse(cr, uid, ids, context=context):
537 for line in sale.order_line:
539 # Check if the line is invoiced (has asociated invoice
540 # lines from non-cancelled invoices).
543 for iline in line.invoice_lines:
544 if iline.invoice_id and iline.invoice_id.state != 'cancel':
547 # Update the line (only when needed)
548 if line.invoiced != invoiced:
549 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
550 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
553 def action_invoice_end(self, cr, uid, ids, context=None):
554 for order in self.browse(cr, uid, ids, context=context):
556 # Update the sale order lines state (and invoiced flag).
558 for line in order.order_line:
561 # Check if the line is invoiced (has asociated invoice
562 # lines from non-cancelled invoices).
565 for iline in line.invoice_lines:
566 if iline.invoice_id and iline.invoice_id.state != 'cancel':
569 if line.invoiced != invoiced:
570 vals['invoiced'] = invoiced
571 # If the line was in exception state, now it gets confirmed.
572 if line.state == 'exception':
573 vals['state'] = 'confirmed'
574 # Update the line (only when needed).
576 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
578 # Update the sales order state.
580 if order.state == 'invoice_except':
581 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
584 def action_cancel(self, cr, uid, ids, context=None):
585 wf_service = netsvc.LocalService("workflow")
588 sale_order_line_obj = self.pool.get('sale.order.line')
589 proc_obj = self.pool.get('procurement.order')
590 for sale in self.browse(cr, uid, ids, context=context):
591 for pick in sale.picking_ids:
592 if pick.state not in ('draft', 'cancel'):
593 raise osv.except_osv(
594 _('Could not cancel sales order !'),
595 _('You must first cancel all picking attached to this sales order.'))
596 if pick.state == 'cancel':
597 for mov in pick.move_lines:
598 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
600 for proc in proc_ids:
601 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
602 for r in self.read(cr, uid, ids, ['picking_ids']):
603 for pick in r['picking_ids']:
604 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
605 for inv in sale.invoice_ids:
606 if inv.state not in ('draft', 'cancel'):
607 raise osv.except_osv(
608 _('Could not cancel this sales order !'),
609 _('You must first cancel all invoices attached to this sales order.'))
610 for r in self.read(cr, uid, ids, ['invoice_ids']):
611 for inv in r['invoice_ids']:
612 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
613 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
615 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
616 self.log(cr, uid, sale.id, message)
617 self.write(cr, uid, ids, {'state': 'cancel'})
620 def action_wait(self, cr, uid, ids, *args):
621 for o in self.browse(cr, uid, ids):
623 raise osv.except_osv(_('No Sale Order Lines !'), _('Please create sale order lines.'))
624 if (o.order_policy == 'manual'):
625 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
627 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': time.strftime(DEFAULT_SERVER_DATE_FORMAT)})
628 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
629 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
630 self.log(cr, uid, o.id, message)
633 def procurement_lines_get(self, cr, uid, ids, *args):
635 for order in self.browse(cr, uid, ids, context={}):
636 for line in order.order_line:
637 if line.procurement_id:
638 res.append(line.procurement_id.id)
641 # if mode == 'finished':
642 # returns True if all lines are done, False otherwise
643 # if mode == 'canceled':
644 # returns True if there is at least one canceled line, False otherwise
645 def test_state(self, cr, uid, ids, mode, *args):
646 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
651 write_cancel_ids = []
652 for order in self.browse(cr, uid, ids, context={}):
653 for line in order.order_line:
654 if (not line.procurement_id) or (line.procurement_id.state=='done'):
655 if line.state != 'done':
656 write_done_ids.append(line.id)
659 if line.procurement_id:
660 if (line.procurement_id.state == 'cancel'):
662 if line.state != 'exception':
663 write_cancel_ids.append(line.id)
667 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
669 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
671 if mode == 'finished':
673 elif mode == 'canceled':
679 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, *args):
682 'origin': order.name,
683 'date_planned': date_planned,
684 'product_id': line.product_id.id,
685 'product_qty': line.product_uom_qty,
686 'product_uom': line.product_uom.id,
687 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
688 or line.product_uom_qty,
689 'product_uos': (line.product_uos and line.product_uos.id)\
690 or line.product_uom.id,
691 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
692 'procure_method': line.type,
694 'property_ids': [(6, 0, [x.id for x in line.property_ids])],
695 'company_id': order.company_id.id,
696 'sale_line_id': line.id,
699 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, *args):
700 location_id = order.shop_id.warehouse_id.lot_stock_id.id
701 output_id = order.shop_id.warehouse_id.lot_output_id.id
703 'name': line.name[:64],
704 'picking_id': picking_id,
705 'product_id': line.product_id.id,
706 'date': date_planned,
707 'date_expected': date_planned,
708 'product_qty': line.product_uom_qty,
709 'product_uom': line.product_uom.id,
710 'product_uos_qty': line.product_uos_qty,
711 'product_uos': (line.product_uos and line.product_uos.id)\
712 or line.product_uom.id,
713 'product_packaging': line.product_packaging.id,
714 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
715 'location_id': location_id,
716 'location_dest_id': output_id,
717 'sale_line_id': line.id,
718 'tracking_id': False,
722 'company_id': order.company_id.id,
723 'price_unit': line.product_id.standard_price or 0.0
726 def _prepare_order_picking(self, cr, uid, order, *args):
727 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
730 'origin': order.name,
731 'date': order.date_order,
734 'move_type': order.picking_policy,
736 'address_id': order.partner_shipping_id.id,
738 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
739 'company_id': order.company_id.id,
742 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, *args):
743 """Create the required procurements to supply sale order lines, also connecting
744 the procurements to appropriate stock moves in order to bring the goods to the
745 sale order's requested location.
747 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
748 a standard outgoing picking will be created to wrap the stock moves, as returned
749 by :meth:`~._prepare_order_picking`.
751 Modules that wish to customize the procurements or partition the stock moves over
752 multiple stock pickings may override this method and call ``super()`` with
753 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
755 :param browse_record order: sale order to which the order lines belong
756 :param list(browse_record) order_lines: sale order line records to procure
757 :param int picking_id: optional ID of a stock picking to which the created stock moves
758 will be added. A new picking will be created if ommitted.
762 for line in order_lines:
763 if line.state == 'done':
766 date_planned = datetime.strptime(order.date_order, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
767 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
770 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
772 picking_id = self.pool.get('stock.picking').create(cr, uid, self._prepare_order_picking(cr, uid, order, *args))
773 move_id = self.pool.get('stock.move').create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, *args))
775 # a service has no stock move
778 proc_id = self.pool.get('procurement.order').create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, *args))
779 proc_ids.append(proc_id)
780 line.write({'procurement_id': proc_id})
782 # FIXME: deals with potentially cancelled shipments, seems broken, see below
783 # FIXME: was introduced by revid: mtr@mtr-20101125100355-0a1b7m792t63mssv
784 if order.state == 'shipping_except':
785 for pick in order.picking_ids:
786 for move in pick.move_lines:
787 if move.state == 'cancel':
788 mov_ids = self.pool.get('stock.move').search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
790 for mov in move_obj.browse(cr, uid, mov_ids):
791 # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
792 self.pool.get('stock.move').write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
793 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
795 wf_service = netsvc.LocalService("workflow")
797 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
799 for proc_id in proc_ids:
800 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
803 if order.state == 'shipping_except':
804 val['state'] = 'progress'
805 val['shipped'] = False
807 if (order.order_policy == 'manual'):
808 for line in order.order_line:
809 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
810 val['state'] = 'manual'
815 def action_ship_create(self, cr, uid, ids, *args):
816 for order in self.browse(cr, uid, ids, context={}):
817 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, *args)
820 def action_ship_end(self, cr, uid, ids, context=None):
821 for order in self.browse(cr, uid, ids, context=context):
822 val = {'shipped': True}
823 if order.state == 'shipping_except':
824 val['state'] = 'progress'
825 if (order.order_policy == 'manual'):
826 for line in order.order_line:
827 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
828 val['state'] = 'manual'
830 for line in order.order_line:
832 if line.state == 'exception':
833 towrite.append(line.id)
835 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
836 self.write(cr, uid, [order.id], val)
839 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
840 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
842 part = inv['partner_id'] and inv['partner_id'][0]
843 pr = inv['amount_untaxed'] or 0.0
844 partnertype = 'customer'
847 'name': 'Order: '+name,
849 'description': 'Order '+str(inv['id']),
852 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
854 'partner_type': partnertype,
856 'planned_revenue': pr,
860 self.pool.get('res.partner.event').create(cr, uid, event)
862 def has_stockable_products(self, cr, uid, ids, *args):
863 for order in self.browse(cr, uid, ids):
864 for order_line in order.order_line:
865 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
870 # TODO add a field price_unit_uos
871 # - update it on change product and unit price
872 # - use it in report if there is a uos
873 class sale_order_line(osv.osv):
875 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
876 tax_obj = self.pool.get('account.tax')
877 cur_obj = self.pool.get('res.currency')
881 for line in self.browse(cr, uid, ids, context=context):
882 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
883 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)
884 cur = line.order_id.pricelist_id.currency_id
885 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
888 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
890 for line in self.browse(cr, uid, ids, context=context):
892 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
897 def _get_uom_id(self, cr, uid, *args):
899 proxy = self.pool.get('ir.model.data')
900 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
902 except Exception, ex:
905 _name = 'sale.order.line'
906 _description = 'Sales Order Line'
908 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
909 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
910 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
911 '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)]}),
912 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
913 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
914 'invoiced': fields.boolean('Invoiced', readonly=True),
915 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
916 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
917 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
918 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
919 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
920 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."),
921 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
922 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
923 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
924 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
925 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
926 'product_uos': fields.many2one('product.uom', 'Product UoS'),
927 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
928 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
929 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
930 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
931 'notes': fields.text('Notes'),
932 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
933 'state': fields.selection([('draft', 'Draft'),('confirmed', 'Confirmed'),('done', 'Done'),('cancel', 'Cancelled'),('exception', 'Exception')], 'State', required=True, readonly=True,
934 help='* The \'Draft\' state is set when the related sales order in draft state. \
935 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
936 \n* The \'Exception\' state is set when the related sales order is set as exception. \
937 \n* The \'Done\' state is set when the sales order line has been picked. \
938 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
939 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
940 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
941 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
943 _order = 'sequence, id'
945 'product_uom' : _get_uom_id,
948 'product_uom_qty': 1,
949 'product_uos_qty': 1,
953 'type': 'make_to_stock',
954 'product_packaging': False,
958 def invoice_line_create(self, cr, uid, ids, context=None):
962 def _get_line_qty(line):
963 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
965 return line.product_uos_qty or 0.0
966 return line.product_uom_qty
968 return self.pool.get('procurement.order').quantity_get(cr, uid,
969 line.procurement_id.id, context=context)
971 def _get_line_uom(line):
972 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
974 return line.product_uos.id
975 return line.product_uom.id
977 return self.pool.get('procurement.order').uom_get(cr, uid,
978 line.procurement_id.id, context=context)
982 for line in self.browse(cr, uid, ids, context=context):
983 if not line.invoiced:
985 a = line.product_id.product_tmpl_id.property_account_income.id
987 a = line.product_id.categ_id.property_account_income_categ.id
989 raise osv.except_osv(_('Error !'),
990 _('There is no income account defined ' \
991 'for this product: "%s" (id:%d)') % \
992 (line.product_id.name, line.product_id.id,))
994 prop = self.pool.get('ir.property').get(cr, uid,
995 'property_account_income_categ', 'product.category',
997 a = prop and prop.id or False
998 uosqty = _get_line_qty(line)
999 uos_id = _get_line_uom(line)
1002 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1003 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1004 fpos = line.order_id.fiscal_position or False
1005 a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a)
1007 raise osv.except_osv(_('Error !'),
1008 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1009 inv_id = self.pool.get('account.invoice.line').create(cr, uid, {
1011 'origin': line.order_id.name,
1015 'discount': line.discount,
1017 'product_id': line.product_id.id or False,
1018 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1020 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1022 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1023 self.write(cr, uid, [line.id], {'invoiced': True})
1024 sales[line.order_id.id] = True
1025 create_ids.append(inv_id)
1026 # Trigger workflow events
1027 wf_service = netsvc.LocalService("workflow")
1028 for sid in sales.keys():
1029 wf_service.trg_write(uid, 'sale.order', sid, cr)
1032 def button_cancel(self, cr, uid, ids, context=None):
1033 for line in self.browse(cr, uid, ids, context=context):
1035 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1036 for move_line in line.move_ids:
1037 if move_line.state != 'cancel':
1038 raise osv.except_osv(
1039 _('Could not cancel sales order line!'),
1040 _('You must first cancel stock moves attached to this sales order line.'))
1041 return self.write(cr, uid, ids, {'state': 'cancel'})
1043 def button_confirm(self, cr, uid, ids, context=None):
1044 return self.write(cr, uid, ids, {'state': 'confirmed'})
1046 def button_done(self, cr, uid, ids, context=None):
1047 wf_service = netsvc.LocalService("workflow")
1048 res = self.write(cr, uid, ids, {'state': 'done'})
1049 for line in self.browse(cr, uid, ids, context=context):
1050 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1053 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1054 product_obj = self.pool.get('product.product')
1056 return {'value': {'product_uom': product_uos,
1057 'product_uom_qty': product_uos_qty}, 'domain': {}}
1059 product = product_obj.browse(cr, uid, product_id)
1061 'product_uom': product.uom_id.id,
1063 # FIXME must depend on uos/uom of the product and not only of the coeff.
1066 'product_uom_qty': product_uos_qty / product.uos_coeff,
1067 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1069 except ZeroDivisionError:
1071 return {'value': value}
1073 def copy_data(self, cr, uid, id, default=None, context=None):
1076 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1077 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1079 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1080 partner_id=False, packaging=False, flag=False, context=None):
1082 return {'value': {'product_packaging': False}}
1083 product_obj = self.pool.get('product.product')
1084 product_uom_obj = self.pool.get('product.uom')
1085 pack_obj = self.pool.get('product.packaging')
1090 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1091 product=product, qty=qty, uom=uom, partner_id=partner_id,
1092 packaging=packaging, flag=False, context=context)
1093 warning_msgs = res.get('warning') and res['warning']['message']
1095 products = product_obj.browse(cr, uid, product, context=context)
1096 if not products.packaging:
1097 packaging = result['product_packaging'] = False
1098 elif not packaging and products.packaging and not flag:
1099 packaging = products.packaging[0].id
1100 result['product_packaging'] = packaging
1103 default_uom = products.uom_id and products.uom_id.id
1104 pack = pack_obj.browse(cr, uid, packaging, context=context)
1105 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1106 # qty = qty - qty % q + q
1107 if qty and (q and not (qty % q) == 0):
1108 ean = pack.ean or _('(n/a)')
1111 if not warning_msgs:
1112 warn_msg = _("You selected a quantity of %d Units.\n"
1113 "But it's not compatible with the selected packaging.\n"
1114 "Here is a proposition of quantities according to the packaging:\n"
1115 "EAN: %s Quantity: %s Type of ul: %s") % \
1116 (qty, ean, qty_pack, type_ul.name)
1117 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1119 'title': _('Configuration Error !'),
1120 'message': warning_msgs
1122 result['product_uom_qty'] = qty
1124 return {'value': result, 'warning': warning}
1126 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1127 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1128 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1129 context = context or {}
1130 lang = lang or context.get('lang',False)
1132 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.'))
1134 product_uom_obj = self.pool.get('product.uom')
1135 partner_obj = self.pool.get('res.partner')
1136 product_obj = self.pool.get('product.product')
1137 context = {'lang': lang, 'partner_id': partner_id}
1139 lang = partner_obj.browse(cr, uid, partner_id).lang
1140 context_partner = {'lang': lang, 'partner_id': partner_id}
1143 return {'value': {'th_weight': 0, 'product_packaging': False,
1144 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1147 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1149 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1150 result = res.get('value', {})
1151 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1152 product_obj = product_obj.browse(cr, uid, product, context=context)
1156 uom2 = product_uom_obj.browse(cr, uid, uom)
1157 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1160 if product_obj.uos_id:
1161 uos2 = product_uom_obj.browse(cr, uid, uos)
1162 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1166 if product_obj.description_sale:
1167 result['notes'] = product_obj.description_sale
1168 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1169 if update_tax: #The quantity only have changed
1170 result['delay'] = (product_obj.sale_delay or 0.0)
1171 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1172 result.update({'type': product_obj.procure_method})
1175 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1177 if (not uom) and (not uos):
1178 result['product_uom'] = product_obj.uom_id.id
1179 if product_obj.uos_id:
1180 result['product_uos'] = product_obj.uos_id.id
1181 result['product_uos_qty'] = qty * product_obj.uos_coeff
1182 uos_category_id = product_obj.uos_id.category_id.id
1184 result['product_uos'] = False
1185 result['product_uos_qty'] = qty
1186 uos_category_id = False
1187 result['th_weight'] = qty * product_obj.weight
1188 domain = {'product_uom':
1189 [('category_id', '=', product_obj.uom_id.category_id.id)],
1191 [('category_id', '=', uos_category_id)]}
1193 elif uos and not uom: # only happens if uom is False
1194 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1195 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1196 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1197 elif uom: # whether uos is set or not
1198 default_uom = product_obj.uom_id and product_obj.uom_id.id
1199 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1200 if product_obj.uos_id:
1201 result['product_uos'] = product_obj.uos_id.id
1202 result['product_uos_qty'] = qty * product_obj.uos_coeff
1204 result['product_uos'] = False
1205 result['product_uos_qty'] = qty
1206 result['th_weight'] = q * product_obj.weight # Round the quantity up
1209 uom2 = product_obj.uom_id
1210 if (product_obj.type=='product') and (product_obj.virtual_available * uom2.factor < qty * product_obj.uom_id.factor) \
1211 and (product_obj.procure_method=='make_to_stock'):
1212 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1213 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1214 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1215 max(0,product_obj.qty_available), product_obj.uom_id.name)
1216 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1220 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1221 'Please set one before choosing a product.')
1222 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1224 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1225 product, qty or 1.0, partner_id, {
1226 'uom': uom or result.get('product_uom'),
1230 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1231 "You have to change either the product, the quantity or the pricelist.")
1233 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1235 result.update({'price_unit': price})
1238 'title': _('Configuration Error !'),
1239 'message' : warning_msgs
1241 return {'value': result, 'domain': domain, 'warning': warning}
1243 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1244 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1245 lang=False, update_tax=True, date_order=False, context=None):
1246 context = context or {}
1247 lang = lang or ('lang' in context and context['lang'])
1248 res = self.product_id_change(cursor, user, ids, pricelist, product,
1249 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1250 partner_id=partner_id, lang=lang, update_tax=update_tax,
1251 date_order=date_order)
1252 if 'product_uom' in res['value']:
1253 del res['value']['product_uom']
1255 res['value']['price_unit'] = 0.0
1258 def unlink(self, cr, uid, ids, context=None):
1261 """Allows to delete sales order lines in draft,cancel states"""
1262 for rec in self.browse(cr, uid, ids, context=context):
1263 if rec.state not in ['draft', 'cancel']:
1264 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1265 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1269 class sale_config_picking_policy(osv.osv_memory):
1270 _name = 'sale.config.picking_policy'
1271 _inherit = 'res.config'
1274 'name': fields.char('Name', size=64),
1275 'sale_orders': fields.boolean('Based on Sales Orders',),
1276 'deli_orders': fields.boolean('Based on Delivery Orders'),
1277 'task_work': fields.boolean('Based on Tasks\' Work'),
1278 'timesheet': fields.boolean('Based on Timesheet'),
1279 'order_policy': fields.selection([
1280 ('manual', 'Invoice Based on Sales Orders'),
1281 ('picking', 'Invoice Based on Deliveries'),
1282 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1283 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1284 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1287 'order_policy': 'manual',
1288 'time_unit': lambda self, cr, uid, c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c) and self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Hour'))], context=c)[0] or False,
1291 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1294 res.update({'order_policy': 'manual'})
1296 res.update({'order_policy': 'picking'})
1297 return {'value':res}
1299 def execute(self, cr, uid, ids, context=None):
1300 ir_values_obj = self.pool.get('ir.values')
1301 data_obj = self.pool.get('ir.model.data')
1302 menu_obj = self.pool.get('ir.ui.menu')
1303 module_obj = self.pool.get('ir.module.module')
1304 module_upgrade_obj = self.pool.get('base.module.upgrade')
1307 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1309 wizard = self.browse(cr, uid, ids)[0]
1311 if wizard.sale_orders:
1312 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1313 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1315 if wizard.deli_orders:
1316 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1317 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1319 if wizard.task_work:
1320 module_name.append('project_timesheet')
1321 module_name.append('project_mrp')
1322 module_name.append('account_analytic_analysis')
1324 if wizard.timesheet:
1325 module_name.append('account_analytic_analysis')
1327 if wizard.charge_delivery:
1328 module_name.append('delivery')
1330 if len(module_name):
1332 need_install = False
1334 for module in module_name:
1335 data_id = module_obj.name_search(cr, uid , module, [], '=')
1336 module_ids.append(data_id[0][0])
1338 for module in module_obj.browse(cr, uid, module_ids):
1339 if module.state == 'uninstalled':
1340 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1344 pooler.restart_pool(cr.dbname, update_module=True)[1]
1346 if wizard.time_unit:
1347 prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1348 product_obj = self.pool.get('product.product')
1349 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1351 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1352 if wizard.task_work and wizard.time_unit:
1353 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1354 self.pool.get('res.company').write(cr, uid, [company_id], {
1355 'project_time_mode_id': wizard.time_unit.id
1358 sale_config_picking_policy()
1360 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: