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, float_compare
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 invoice_existence = False
151 for invoice in sale.invoice_ids:
152 if invoice.state!='cancel':
153 invoice_existence = True
154 if invoice.state != 'paid':
157 if not invoice_existence:
161 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
170 clause += 'AND inv.state = \'paid\''
172 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
173 sale_clause = ', sale_order AS sale '
176 cursor.execute('SELECT rel.order_id ' \
177 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
178 'WHERE rel.invoice_id = inv.id ' + clause)
179 res = cursor.fetchall()
181 cursor.execute('SELECT sale.id ' \
182 'FROM sale_order AS sale ' \
183 'WHERE sale.id NOT IN ' \
184 '(SELECT rel.order_id ' \
185 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
186 res.extend(cursor.fetchall())
188 return [('id', '=', 0)]
189 return [('id', 'in', [x[0] for x in res])]
191 def _get_order(self, cr, uid, ids, context=None):
193 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
194 result[line.order_id.id] = True
198 'name': fields.char('Order Reference', size=64, required=True,
199 readonly=True, states={'draft': [('readonly', False)]}, select=True),
200 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)]}),
201 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
202 'client_order_ref': fields.char('Customer Reference', size=64),
203 'state': fields.selection([
204 ('draft', 'Quotation'),
205 ('waiting_date', 'Waiting Schedule'),
206 ('manual', 'To Invoice'),
207 ('progress', 'In Progress'),
208 ('shipping_except', 'Shipping Exception'),
209 ('invoice_except', 'Invoice Exception'),
211 ('cancel', 'Cancelled')
212 ], 'Order State', readonly=True, help="Gives the state of the quotation or sales order. \nThe exception state is automatically set when a cancel operation occurs in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception). \nThe 'Waiting Schedule' state is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
213 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)]}),
214 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
215 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
216 'user_id': fields.many2one('res.users', 'Salesman', states={'draft': [('readonly', False)]}, select=True),
217 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)]}, required=True, change_default=True, select=True),
218 '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."),
219 '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."),
220 '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."),
222 '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."),
223 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
224 '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?"""),
225 'order_policy': fields.selection([
226 ('prepaid', 'Pay before delivery'),
227 ('manual', 'Deliver & invoice on demand'),
228 ('picking', 'Invoice based on deliveries'),
229 ('postpaid', 'Invoice on order after delivery'),
230 ], 'Invoice Policy', required=True, readonly=True, states={'draft': [('readonly', False)]},
231 help="""The Invoice Policy is used to synchronise invoice and delivery operations.
232 - The 'Pay before delivery' choice will first generate the invoice and then generate the picking order after the payment of this invoice.
233 - 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.
234 - The 'Invoice on order after delivery' choice will generate the draft invoice based on sales order after all picking lists have been finished.
235 - The 'Invoice based on deliveries' choice is used to create an invoice during the picking process."""),
236 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist for current sales order."),
237 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)]}, help="The analytic account related to a sales order."),
239 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)]}),
240 '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)."),
241 '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."),
242 '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."),
243 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
244 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
245 'invoiced': fields.function(_invoiced, string='Paid',
246 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
247 'note': fields.text('Notes'),
249 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
251 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
252 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
254 multi='sums', help="The amount without tax."),
255 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
257 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
258 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
260 multi='sums', help="The tax amount."),
261 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
263 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
264 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
266 multi='sums', help="The total amount."),
268 '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)]}),
269 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
270 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
271 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
274 'picking_policy': 'direct',
275 'date_order': fields.date.context_today,
276 'order_policy': 'manual',
278 'user_id': lambda obj, cr, uid, context: uid,
279 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
280 'invoice_quantity': 'order',
281 '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'],
282 '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'],
283 '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'],
286 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
291 def unlink(self, cr, uid, ids, context=None):
292 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
294 for s in sale_orders:
295 if s['state'] in ['draft', 'cancel']:
296 unlink_ids.append(s['id'])
298 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.'))
300 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
302 def onchange_shop_id(self, cr, uid, ids, shop_id):
305 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id)
306 v['project_id'] = shop.project_id.id
307 # Que faire si le client a une pricelist a lui ?
308 if shop.pricelist_id.id:
309 v['pricelist_id'] = shop.pricelist_id.id
312 def action_cancel_draft(self, cr, uid, ids, *args):
315 cr.execute('select id from sale_order_line where order_id IN %s and state=%s', (tuple(ids), 'cancel'))
316 line_ids = map(lambda x: x[0], cr.fetchall())
317 self.write(cr, uid, ids, {'state': 'draft', 'invoice_ids': [], 'shipped': 0})
318 self.pool.get('sale.order.line').write(cr, uid, line_ids, {'invoiced': False, 'state': 'draft', 'invoice_lines': [(6, 0, [])]})
319 wf_service = netsvc.LocalService("workflow")
321 # Deleting the existing instance of workflow for SO
322 wf_service.trg_delete(uid, 'sale.order', inv_id, cr)
323 wf_service.trg_create(uid, 'sale.order', inv_id, cr)
324 for (id,name) in self.name_get(cr, uid, ids):
325 message = _("The sales order '%s' has been set in draft state.") %(name,)
326 self.log(cr, uid, id, message)
329 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
330 if (not pricelist_id) or (not order_lines):
333 'title': _('Pricelist Warning!'),
334 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
336 return {'warning': warning}
338 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
343 val['partner_invoice_id'] = order_id
345 val['partner_shipping_id'] = order_id
346 return {'value': val}
348 def onchange_partner_id(self, cr, uid, ids, part):
350 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
352 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
353 part = self.pool.get('res.partner').browse(cr, uid, part)
354 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
355 payment_term = part.property_payment_term and part.property_payment_term.id or False
356 fiscal_position = part.property_account_position and part.property_account_position.id or False
357 dedicated_salesman = part.user_id and part.user_id.id or uid
359 'partner_invoice_id': addr['invoice'],
360 'partner_order_id': addr['contact'],
361 'partner_shipping_id': addr['delivery'],
362 'payment_term': payment_term,
363 'fiscal_position': fiscal_position,
364 'user_id': dedicated_salesman,
367 val['pricelist_id'] = pricelist
368 return {'value': val}
370 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
374 if policy == 'prepaid':
376 elif policy == 'picking':
377 inv_qty = 'procurement'
378 return {'value': {'invoice_quantity': inv_qty}}
380 def write(self, cr, uid, ids, vals, context=None):
381 if vals.get('order_policy', False):
382 if vals['order_policy'] == 'prepaid':
383 vals.update({'invoice_quantity': 'order'})
384 elif vals['order_policy'] == 'picking':
385 vals.update({'invoice_quantity': 'procurement'})
386 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
388 def create(self, cr, uid, vals, context=None):
389 if vals.get('order_policy', False):
390 if vals['order_policy'] == 'prepaid':
391 vals.update({'invoice_quantity': 'order'})
392 if vals['order_policy'] == 'picking':
393 vals.update({'invoice_quantity': 'procurement'})
394 return super(sale_order, self).create(cr, uid, vals, context=context)
396 def button_dummy(self, cr, uid, ids, context=None):
399 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
400 # can be removed after 6.1.
401 def _inv_get(self, cr, uid, order, context=None):
404 def _prepare_invoice(self, cr, uid, order, lines, context=None):
405 """Prepare the dict of values to create the new invoice for a
406 sale order. This method may be overridden to implement custom
407 invoice generation (making sure to call super() to establish
408 a clean extension chain).
410 :param browse_record order: sale.order record to invoice
411 :param list(int) line: list of invoice line IDs that must be
412 attached to the invoice
413 :return: dict of value to create() the invoice
417 journal_ids = self.pool.get('account.journal').search(cr, uid,
418 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
421 raise osv.except_osv(_('Error !'),
422 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
425 'name': order.client_order_ref or '',
426 'origin': order.name,
427 'type': 'out_invoice',
428 'reference': order.client_order_ref or order.name,
429 'account_id': order.partner_id.property_account_receivable.id,
430 'partner_id': order.partner_id.id,
431 'journal_id': journal_ids[0],
432 'address_invoice_id': order.partner_invoice_id.id,
433 'address_contact_id': order.partner_order_id.id,
434 'invoice_line': [(6, 0, lines)],
435 'currency_id': order.pricelist_id.currency_id.id,
436 'comment': order.note,
437 'payment_term': order.payment_term and order.payment_term.id or False,
438 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
439 'date_invoice': context.get('date_invoice', False),
440 'company_id': order.company_id.id,
441 'user_id': order.user_id and order.user_id.id or False
444 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
445 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
449 def _make_invoice(self, cr, uid, order, lines, context=None):
450 inv_obj = self.pool.get('account.invoice')
451 obj_invoice_line = self.pool.get('account.invoice.line')
454 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
455 from_line_invoice_ids = []
456 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
457 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
458 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
459 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
460 for preinv in order.invoice_ids:
461 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
462 for preline in preinv.invoice_line:
463 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
464 lines.append(inv_line_id)
465 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
466 inv_id = inv_obj.create(cr, uid, inv, context=context)
467 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
468 if data.get('value', False):
469 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
470 inv_obj.button_compute(cr, uid, [inv_id])
473 def manual_invoice(self, cr, uid, ids, context=None):
474 mod_obj = self.pool.get('ir.model.data')
475 wf_service = netsvc.LocalService("workflow")
479 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
480 inv_ids.add(record.id)
481 # inv_ids would have old invoices if any
483 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
484 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
485 inv_ids1.add(record.id)
486 inv_ids = list(inv_ids1.difference(inv_ids))
488 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
489 res_id = res and res[1] or False,
492 'name': _('Customer Invoices'),
496 'res_model': 'account.invoice',
497 'context': "{'type':'out_invoice'}",
498 'type': 'ir.actions.act_window',
501 'res_id': inv_ids and inv_ids[0] or False,
504 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
508 picking_obj = self.pool.get('stock.picking')
509 invoice = self.pool.get('account.invoice')
510 obj_sale_order_line = self.pool.get('sale.order.line')
511 partner_currency = {}
514 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
515 # last day of the last month as invoice date
517 context['date_inv'] = date_inv
518 for o in self.browse(cr, uid, ids, context=context):
519 currency_id = o.pricelist_id.currency_id.id
520 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
521 raise osv.except_osv(
523 _('You cannot group sales having different currencies for the same partner.'))
525 partner_currency[o.partner_id.id] = currency_id
527 for line in o.order_line:
530 elif (line.state in states):
531 lines.append(line.id)
532 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
534 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
536 for o in self.browse(cr, uid, ids, context=context):
537 for i in o.invoice_ids:
538 if i.state == 'draft':
540 for val in invoices.values():
542 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
545 invoice_ref += o.name + '|'
546 self.write(cr, uid, [o.id], {'state': 'progress'})
547 if o.order_policy == 'picking':
548 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
549 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
550 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
552 for order, il in val:
553 res = self._make_invoice(cr, uid, order, il, context=context)
554 invoice_ids.append(res)
555 self.write(cr, uid, [order.id], {'state': 'progress'})
556 if order.order_policy == 'picking':
557 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
558 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
561 def action_invoice_cancel(self, cr, uid, ids, context=None):
564 for sale in self.browse(cr, uid, ids, context=context):
565 for line in sale.order_line:
567 # Check if the line is invoiced (has asociated invoice
568 # lines from non-cancelled invoices).
571 for iline in line.invoice_lines:
572 if iline.invoice_id and iline.invoice_id.state != 'cancel':
575 # Update the line (only when needed)
576 if line.invoiced != invoiced:
577 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
578 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
581 def action_invoice_end(self, cr, uid, ids, context=None):
582 for order in self.browse(cr, uid, ids, context=context):
584 # Update the sale order lines state (and invoiced flag).
586 for line in order.order_line:
589 # Check if the line is invoiced (has asociated invoice
590 # lines from non-cancelled invoices).
593 for iline in line.invoice_lines:
594 if iline.invoice_id and iline.invoice_id.state != 'cancel':
597 if line.invoiced != invoiced:
598 vals['invoiced'] = invoiced
599 # If the line was in exception state, now it gets confirmed.
600 if line.state == 'exception':
601 vals['state'] = 'confirmed'
602 # Update the line (only when needed).
604 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
606 # Update the sales order state.
608 if order.state == 'invoice_except':
609 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
612 def action_cancel(self, cr, uid, ids, context=None):
613 wf_service = netsvc.LocalService("workflow")
616 sale_order_line_obj = self.pool.get('sale.order.line')
617 proc_obj = self.pool.get('procurement.order')
618 for sale in self.browse(cr, uid, ids, context=context):
619 for pick in sale.picking_ids:
620 if pick.state not in ('draft', 'cancel'):
621 raise osv.except_osv(
622 _('Could not cancel sales order !'),
623 _('You must first cancel all picking attached to this sales order.'))
624 if pick.state == 'cancel':
625 for mov in pick.move_lines:
626 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
628 for proc in proc_ids:
629 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
630 for r in self.read(cr, uid, ids, ['picking_ids']):
631 for pick in r['picking_ids']:
632 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
633 for inv in sale.invoice_ids:
634 if inv.state not in ('draft', 'cancel'):
635 raise osv.except_osv(
636 _('Could not cancel this sales order !'),
637 _('You must first cancel all invoices attached to this sales order.'))
638 for r in self.read(cr, uid, ids, ['invoice_ids']):
639 for inv in r['invoice_ids']:
640 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
641 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
643 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
644 self.log(cr, uid, sale.id, message)
645 self.write(cr, uid, ids, {'state': 'cancel'})
648 def action_wait(self, cr, uid, ids, context=None):
649 for o in self.browse(cr, uid, ids):
651 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
652 if (o.order_policy == 'manual'):
653 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
655 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
656 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
657 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
658 self.log(cr, uid, o.id, message)
661 def procurement_lines_get(self, cr, uid, ids, *args):
663 for order in self.browse(cr, uid, ids, context={}):
664 for line in order.order_line:
665 if line.procurement_id:
666 res.append(line.procurement_id.id)
669 # if mode == 'finished':
670 # returns True if all lines are done, False otherwise
671 # if mode == 'canceled':
672 # returns True if there is at least one canceled line, False otherwise
673 def test_state(self, cr, uid, ids, mode, *args):
674 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
679 write_cancel_ids = []
680 for order in self.browse(cr, uid, ids, context={}):
681 for line in order.order_line:
682 if (not line.procurement_id) or (line.procurement_id.state=='done'):
683 if line.state != 'done':
684 write_done_ids.append(line.id)
687 if line.procurement_id:
688 if (line.procurement_id.state == 'cancel'):
690 if line.state != 'exception':
691 write_cancel_ids.append(line.id)
695 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
697 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
699 if mode == 'finished':
701 elif mode == 'canceled':
707 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
710 'origin': order.name,
711 'date_planned': date_planned,
712 'product_id': line.product_id.id,
713 'product_qty': line.product_uom_qty,
714 'product_uom': line.product_uom.id,
715 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
716 or line.product_uom_qty,
717 'product_uos': (line.product_uos and line.product_uos.id)\
718 or line.product_uom.id,
719 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
720 'procure_method': line.type,
722 'company_id': order.company_id.id,
726 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
727 location_id = order.shop_id.warehouse_id.lot_stock_id.id
728 output_id = order.shop_id.warehouse_id.lot_output_id.id
730 'name': line.name[:250],
731 'picking_id': picking_id,
732 'product_id': line.product_id.id,
733 'date': date_planned,
734 'date_expected': date_planned,
735 'product_qty': line.product_uom_qty,
736 'product_uom': line.product_uom.id,
737 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
738 'product_uos': (line.product_uos and line.product_uos.id)\
739 or line.product_uom.id,
740 'product_packaging': line.product_packaging.id,
741 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
742 'location_id': location_id,
743 'location_dest_id': output_id,
744 'sale_line_id': line.id,
745 'tracking_id': False,
749 'company_id': order.company_id.id,
750 'price_unit': line.product_id.standard_price or 0.0
753 def _prepare_order_picking(self, cr, uid, order, context=None):
754 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
757 'origin': order.name,
758 'date': order.date_order,
761 'move_type': order.picking_policy,
763 'address_id': order.partner_shipping_id.id,
765 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
766 'company_id': order.company_id.id,
769 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
770 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
772 Define ship_recreate for process after shipping exception
773 param order: sale order to which the order lines belong
774 param line: sale order line records to procure
775 param move_id: the ID of stock move
776 param proc_id: the ID of procurement
778 move_obj = self.pool.get('stock.move')
779 if order.state == 'shipping_except':
780 for pick in order.picking_ids:
781 for move in pick.move_lines:
782 if move.state == 'cancel':
783 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
785 for mov in move_obj.browse(cr, uid, mov_ids):
786 # 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?
787 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
788 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
791 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
792 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
793 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
796 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
797 """Create the required procurements to supply sale order lines, also connecting
798 the procurements to appropriate stock moves in order to bring the goods to the
799 sale order's requested location.
801 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
802 a standard outgoing picking will be created to wrap the stock moves, as returned
803 by :meth:`~._prepare_order_picking`.
805 Modules that wish to customize the procurements or partition the stock moves over
806 multiple stock pickings may override this method and call ``super()`` with
807 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
809 :param browse_record order: sale order to which the order lines belong
810 :param list(browse_record) order_lines: sale order line records to procure
811 :param int picking_id: optional ID of a stock picking to which the created stock moves
812 will be added. A new picking will be created if ommitted.
815 move_obj = self.pool.get('stock.move')
816 picking_obj = self.pool.get('stock.picking')
817 procurement_obj = self.pool.get('procurement.order')
820 for line in order_lines:
821 if line.state == 'done':
824 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
827 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
829 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
830 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
832 # a service has no stock move
835 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
836 proc_ids.append(proc_id)
837 line.write({'procurement_id': proc_id})
838 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
840 wf_service = netsvc.LocalService("workflow")
842 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
844 for proc_id in proc_ids:
845 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
848 if order.state == 'shipping_except':
849 val['state'] = 'progress'
850 val['shipped'] = False
852 if (order.order_policy == 'manual'):
853 for line in order.order_line:
854 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
855 val['state'] = 'manual'
860 def action_ship_create(self, cr, uid, ids, context=None):
861 for order in self.browse(cr, uid, ids, context=context):
862 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
865 def action_ship_end(self, cr, uid, ids, context=None):
866 for order in self.browse(cr, uid, ids, context=context):
867 val = {'shipped': True}
868 if order.state == 'shipping_except':
869 val['state'] = 'progress'
870 if (order.order_policy == 'manual'):
871 for line in order.order_line:
872 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
873 val['state'] = 'manual'
875 for line in order.order_line:
877 if line.state == 'exception':
878 towrite.append(line.id)
880 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
881 self.write(cr, uid, [order.id], val)
884 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
885 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
887 part = inv['partner_id'] and inv['partner_id'][0]
888 pr = inv['amount_untaxed'] or 0.0
889 partnertype = 'customer'
892 'name': 'Order: '+name,
894 'description': 'Order '+str(inv['id']),
897 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
899 'partner_type': partnertype,
901 'planned_revenue': pr,
905 self.pool.get('res.partner.event').create(cr, uid, event)
907 def has_stockable_products(self, cr, uid, ids, *args):
908 for order in self.browse(cr, uid, ids):
909 for order_line in order.order_line:
910 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
915 # TODO add a field price_unit_uos
916 # - update it on change product and unit price
917 # - use it in report if there is a uos
918 class sale_order_line(osv.osv):
920 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
921 tax_obj = self.pool.get('account.tax')
922 cur_obj = self.pool.get('res.currency')
926 for line in self.browse(cr, uid, ids, context=context):
927 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
928 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)
929 cur = line.order_id.pricelist_id.currency_id
930 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
933 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
935 for line in self.browse(cr, uid, ids, context=context):
937 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
942 def _get_uom_id(self, cr, uid, *args):
944 proxy = self.pool.get('ir.model.data')
945 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
947 except Exception, ex:
950 _name = 'sale.order.line'
951 _description = 'Sales Order Line'
953 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
954 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
955 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
956 '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)]}),
957 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
958 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
959 'invoiced': fields.boolean('Invoiced', readonly=True),
960 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
961 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
962 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
963 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
964 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
965 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."),
966 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
967 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
968 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
969 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
970 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
971 'product_uos': fields.many2one('product.uom', 'Product UoS'),
972 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
973 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
974 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
975 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
976 'notes': fields.text('Notes'),
977 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
978 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
979 help='* The \'Draft\' state is set when the related sales order in draft state. \
980 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
981 \n* The \'Exception\' state is set when the related sales order is set as exception. \
982 \n* The \'Done\' state is set when the sales order line has been picked. \
983 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
984 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
985 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
986 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
988 _order = 'sequence, id'
990 'product_uom' : _get_uom_id,
993 'product_uom_qty': 1,
994 'product_uos_qty': 1,
998 'type': 'make_to_stock',
999 'product_packaging': False,
1003 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1004 """Prepare the dict of values to create the new invoice line for a
1005 sale order line. This method may be overridden to implement custom
1006 invoice generation (making sure to call super() to establish
1007 a clean extension chain).
1009 :param browse_record line: sale.order.line record to invoice
1010 :param int account_id: optional ID of a G/L account to force
1011 (this is used for returning products including service)
1012 :return: dict of values to create() the invoice line
1015 def _get_line_qty(line):
1016 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1017 if line.product_uos:
1018 return line.product_uos_qty or 0.0
1019 return line.product_uom_qty
1021 return self.pool.get('procurement.order').quantity_get(cr, uid,
1022 line.procurement_id.id, context=context)
1024 def _get_line_uom(line):
1025 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1026 if line.product_uos:
1027 return line.product_uos.id
1028 return line.product_uom.id
1030 return self.pool.get('procurement.order').uom_get(cr, uid,
1031 line.procurement_id.id, context=context)
1033 if not line.invoiced:
1036 account_id = line.product_id.product_tmpl_id.property_account_income.id
1038 account_id = line.product_id.categ_id.property_account_income_categ.id
1040 raise osv.except_osv(_('Error !'),
1041 _('There is no income account defined for this product: "%s" (id:%d)') % \
1042 (line.product_id.name, line.product_id.id,))
1044 prop = self.pool.get('ir.property').get(cr, uid,
1045 'property_account_income_categ', 'product.category',
1047 account_id = prop and prop.id or False
1048 uosqty = _get_line_qty(line)
1049 uos_id = _get_line_uom(line)
1052 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1053 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1054 fpos = line.order_id.fiscal_position or False
1055 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1057 raise osv.except_osv(_('Error !'),
1058 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1061 'origin': line.order_id.name,
1062 'account_id': account_id,
1065 'discount': line.discount,
1067 'product_id': line.product_id.id or False,
1068 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1070 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1075 def invoice_line_create(self, cr, uid, ids, context=None):
1081 for line in self.browse(cr, uid, ids, context=context):
1082 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1084 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1085 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1086 self.write(cr, uid, [line.id], {'invoiced': True})
1087 sales.add(line.order_id.id)
1088 create_ids.append(inv_id)
1089 # Trigger workflow events
1090 wf_service = netsvc.LocalService("workflow")
1091 for sale_id in sales:
1092 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1095 def button_cancel(self, cr, uid, ids, context=None):
1096 for line in self.browse(cr, uid, ids, context=context):
1098 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1099 for move_line in line.move_ids:
1100 if move_line.state != 'cancel':
1101 raise osv.except_osv(
1102 _('Could not cancel sales order line!'),
1103 _('You must first cancel stock moves attached to this sales order line.'))
1104 return self.write(cr, uid, ids, {'state': 'cancel'})
1106 def button_confirm(self, cr, uid, ids, context=None):
1107 return self.write(cr, uid, ids, {'state': 'confirmed'})
1109 def button_done(self, cr, uid, ids, context=None):
1110 wf_service = netsvc.LocalService("workflow")
1111 res = self.write(cr, uid, ids, {'state': 'done'})
1112 for line in self.browse(cr, uid, ids, context=context):
1113 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1116 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1117 product_obj = self.pool.get('product.product')
1119 return {'value': {'product_uom': product_uos,
1120 'product_uom_qty': product_uos_qty}, 'domain': {}}
1122 product = product_obj.browse(cr, uid, product_id)
1124 'product_uom': product.uom_id.id,
1126 # FIXME must depend on uos/uom of the product and not only of the coeff.
1129 'product_uom_qty': product_uos_qty / product.uos_coeff,
1130 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1132 except ZeroDivisionError:
1134 return {'value': value}
1136 def copy_data(self, cr, uid, id, default=None, context=None):
1139 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1140 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1142 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1143 partner_id=False, packaging=False, flag=False, context=None):
1145 return {'value': {'product_packaging': False}}
1146 product_obj = self.pool.get('product.product')
1147 product_uom_obj = self.pool.get('product.uom')
1148 pack_obj = self.pool.get('product.packaging')
1153 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1154 product=product, qty=qty, uom=uom, partner_id=partner_id,
1155 packaging=packaging, flag=False, context=context)
1156 warning_msgs = res.get('warning') and res['warning']['message']
1158 products = product_obj.browse(cr, uid, product, context=context)
1159 if not products.packaging:
1160 packaging = result['product_packaging'] = False
1161 elif not packaging and products.packaging and not flag:
1162 packaging = products.packaging[0].id
1163 result['product_packaging'] = packaging
1166 default_uom = products.uom_id and products.uom_id.id
1167 pack = pack_obj.browse(cr, uid, packaging, context=context)
1168 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1169 # qty = qty - qty % q + q
1170 if qty and (q and not (qty % q) == 0):
1171 ean = pack.ean or _('(n/a)')
1174 if not warning_msgs:
1175 warn_msg = _("You selected a quantity of %d Units.\n"
1176 "But it's not compatible with the selected packaging.\n"
1177 "Here is a proposition of quantities according to the packaging:\n"
1178 "EAN: %s Quantity: %s Type of ul: %s") % \
1179 (qty, ean, qty_pack, type_ul.name)
1180 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1182 'title': _('Configuration Error !'),
1183 'message': warning_msgs
1185 result['product_uom_qty'] = qty
1187 return {'value': result, 'warning': warning}
1189 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1190 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1191 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1193 onchange handler for product_id.
1195 :param dict context: 'force_product_uom' key in context override
1196 default onchange behaviour to force using the UoM
1197 defined on the provided product
1201 lang = lang or context.get('lang',False)
1203 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.'))
1205 product_uom_obj = self.pool.get('product.uom')
1206 partner_obj = self.pool.get('res.partner')
1207 product_obj = self.pool.get('product.product')
1208 context = dict(context, lang=lang, partner_id=partner_id)
1210 lang = partner_obj.browse(cr, uid, partner_id, context=context).lang
1211 context_partner = dict(context, lang=lang)
1214 return {'value': {'th_weight': 0, 'product_packaging': False,
1215 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1218 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1220 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1221 result = res.get('value', {})
1222 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1223 product_obj = product_obj.browse(cr, uid, product, context=context)
1227 uom2 = product_uom_obj.browse(cr, uid, uom, context=context)
1228 if product_obj.uom_id.category_id.id != uom2.category_id.id or context.get('force_product_uom'):
1232 if product_obj.uos_id:
1233 uos2 = product_uom_obj.browse(cr, uid, uos, context=context)
1234 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1238 if product_obj.description_sale:
1239 result['notes'] = product_obj.description_sale
1240 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position, context=context) or False
1241 if update_tax: #The quantity only have changed
1242 result['delay'] = (product_obj.sale_delay or 0.0)
1243 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1244 result.update({'type': product_obj.procure_method})
1247 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1249 if (not uom) and (not uos):
1250 result['product_uom'] = product_obj.uom_id.id
1251 if product_obj.uos_id:
1252 result['product_uos'] = product_obj.uos_id.id
1253 result['product_uos_qty'] = qty * product_obj.uos_coeff
1254 uos_category_id = product_obj.uos_id.category_id.id
1256 result['product_uos'] = False
1257 result['product_uos_qty'] = qty
1258 uos_category_id = False
1259 result['th_weight'] = qty * product_obj.weight
1260 domain = {'product_uom':
1261 [('category_id', '=', product_obj.uom_id.category_id.id)],
1263 [('category_id', '=', uos_category_id)]}
1265 elif uos and not uom: # only happens if uom is False
1266 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1267 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1268 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1269 elif uom: # whether uos is set or not
1270 default_uom = product_obj.uom_id and product_obj.uom_id.id
1271 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1272 if product_obj.uos_id:
1273 result['product_uos'] = product_obj.uos_id.id
1274 result['product_uos_qty'] = qty * product_obj.uos_coeff
1276 result['product_uos'] = False
1277 result['product_uos_qty'] = qty
1278 result['th_weight'] = q * product_obj.weight # Round the quantity up
1281 uom2 = product_obj.uom_id
1282 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1283 if (product_obj.type=='product') and int(compare_qty) == -1 \
1284 and (product_obj.procure_method=='make_to_stock'):
1285 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1286 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1287 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1288 max(0,product_obj.qty_available), product_obj.uom_id.name)
1289 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1293 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1294 'Please set one before choosing a product.')
1295 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1297 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1298 product, qty or 1.0, partner_id, dict(context,
1299 uom=uom or result.get('product_uom'),
1303 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1304 "You have to change either the product, the quantity or the pricelist.")
1306 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1308 result.update({'price_unit': price})
1311 'title': _('Configuration Error !'),
1312 'message' : warning_msgs
1314 return {'value': result, 'domain': domain, 'warning': warning}
1316 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1317 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1318 lang=False, update_tax=True, date_order=False, context=None):
1321 lang = lang or context.get('lang',False)
1322 res = self.product_id_change(cursor, user, ids, pricelist, product,
1323 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1324 partner_id=partner_id, lang=lang, update_tax=update_tax,
1325 date_order=date_order, context=context)
1326 if 'product_uom' in res['value']:
1327 del res['value']['product_uom']
1329 res['value']['price_unit'] = 0.0
1332 def unlink(self, cr, uid, ids, context=None):
1335 """Allows to delete sales order lines in draft,cancel states"""
1336 for rec in self.browse(cr, uid, ids, context=context):
1337 if rec.state not in ['draft', 'cancel']:
1338 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1339 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1343 class sale_config_picking_policy(osv.osv_memory):
1344 _name = 'sale.config.picking_policy'
1345 _inherit = 'res.config'
1348 'name': fields.char('Name', size=64),
1349 'sale_orders': fields.boolean('Based on Sales Orders',),
1350 'deli_orders': fields.boolean('Based on Delivery Orders'),
1351 'task_work': fields.boolean('Based on Tasks\' Work'),
1352 'timesheet': fields.boolean('Based on Timesheet'),
1353 'order_policy': fields.selection([
1354 ('manual', 'Invoice Based on Sales Orders'),
1355 ('picking', 'Invoice Based on Deliveries'),
1356 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1357 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1358 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1361 'order_policy': 'manual',
1362 '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,
1365 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1368 res.update({'order_policy': 'manual'})
1370 res.update({'order_policy': 'picking'})
1371 return {'value':res}
1373 def execute(self, cr, uid, ids, context=None):
1374 ir_values_obj = self.pool.get('ir.values')
1375 data_obj = self.pool.get('ir.model.data')
1376 menu_obj = self.pool.get('ir.ui.menu')
1377 module_obj = self.pool.get('ir.module.module')
1378 module_upgrade_obj = self.pool.get('base.module.upgrade')
1381 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1383 wizard = self.browse(cr, uid, ids)[0]
1385 if wizard.sale_orders:
1386 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1387 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1389 if wizard.deli_orders:
1390 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1391 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1393 if wizard.task_work:
1394 module_name.append('project_timesheet')
1395 module_name.append('project_mrp')
1396 module_name.append('account_analytic_analysis')
1398 if wizard.timesheet:
1399 module_name.append('account_analytic_analysis')
1401 if wizard.charge_delivery:
1402 module_name.append('delivery')
1404 if len(module_name):
1406 need_install = False
1408 for module in module_name:
1409 data_id = module_obj.name_search(cr, uid , module, [], '=')
1410 module_ids.append(data_id[0][0])
1412 for module in module_obj.browse(cr, uid, module_ids):
1413 if module.state == 'uninstalled':
1414 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1418 pooler.restart_pool(cr.dbname, update_module=True)[1]
1420 if wizard.time_unit:
1421 prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1422 product_obj = self.pool.get('product.product')
1423 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1425 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1426 if wizard.task_work and wizard.time_unit:
1427 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1428 self.pool.get('res.company').write(cr, uid, [company_id], {
1429 'project_time_mode_id': wizard.time_unit.id
1432 sale_config_picking_policy()
1434 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: