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 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="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),
210 'date_order': fields.date('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 product 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', 'Contract/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': fields.date.context_today,
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, company_id)', 'Order Reference must be unique per Company!'),
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={}):
327 if (not pricelist_id) or (not order_lines):
330 'title': _('Pricelist Warning!'),
331 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
333 return {'warning': warning}
335 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
340 val['partner_invoice_id'] = order_id
342 val['partner_shipping_id'] = order_id
343 return {'value': val}
345 def onchange_partner_id(self, cr, uid, ids, part):
347 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'partner_order_id': False, 'payment_term': False, 'fiscal_position': False}}
349 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
350 part = self.pool.get('res.partner').browse(cr, uid, part)
351 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
352 payment_term = part.property_payment_term and part.property_payment_term.id or False
353 fiscal_position = part.property_account_position and part.property_account_position.id or False
354 dedicated_salesman = part.user_id and part.user_id.id or uid
356 'partner_invoice_id': addr['invoice'],
357 'partner_order_id': addr['contact'],
358 'partner_shipping_id': addr['delivery'],
359 'payment_term': payment_term,
360 'fiscal_position': fiscal_position,
361 'user_id': dedicated_salesman,
364 val['pricelist_id'] = pricelist
365 return {'value': val}
367 def shipping_policy_change(self, cr, uid, ids, policy, context=None):
371 if policy == 'prepaid':
373 elif policy == 'picking':
374 inv_qty = 'procurement'
375 return {'value': {'invoice_quantity': inv_qty}}
377 def write(self, cr, uid, ids, vals, context=None):
378 if vals.get('order_policy', False):
379 if vals['order_policy'] == 'prepaid':
380 vals.update({'invoice_quantity': 'order'})
381 elif vals['order_policy'] == 'picking':
382 vals.update({'invoice_quantity': 'procurement'})
383 return super(sale_order, self).write(cr, uid, ids, vals, context=context)
385 def create(self, cr, uid, vals, context=None):
386 if vals.get('order_policy', False):
387 if vals['order_policy'] == 'prepaid':
388 vals.update({'invoice_quantity': 'order'})
389 if vals['order_policy'] == 'picking':
390 vals.update({'invoice_quantity': 'procurement'})
391 return super(sale_order, self).create(cr, uid, vals, context=context)
393 def button_dummy(self, cr, uid, ids, context=None):
396 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
397 # can be removed after 6.1.
398 def _inv_get(self, cr, uid, order, context=None):
401 def _prepare_invoice(self, cr, uid, order, lines, context=None):
402 """Prepare the dict of values to create the new invoice for a
403 sale order. This method may be overridden to implement custom
404 invoice generation (making sure to call super() to establish
405 a clean extension chain).
407 :param browse_record order: sale.order record to invoice
408 :param list(int) line: list of invoice line IDs that must be
409 attached to the invoice
410 :return: dict of value to create() the invoice
414 journal_ids = self.pool.get('account.journal').search(cr, uid,
415 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
418 raise osv.except_osv(_('Error !'),
419 _('There is no sales journal defined for this company: "%s" (id:%d)') % (order.company_id.name, order.company_id.id))
422 'name': order.client_order_ref or '',
423 'origin': order.name,
424 'type': 'out_invoice',
425 'reference': order.client_order_ref or order.name,
426 'account_id': order.partner_id.property_account_receivable.id,
427 'partner_id': order.partner_id.id,
428 'journal_id': journal_ids[0],
429 'address_invoice_id': order.partner_invoice_id.id,
430 'address_contact_id': order.partner_order_id.id,
431 'invoice_line': [(6, 0, lines)],
432 'currency_id': order.pricelist_id.currency_id.id,
433 'comment': order.note,
434 'payment_term': order.payment_term and order.payment_term.id or False,
435 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
436 'date_invoice': context.get('date_invoice', False),
437 'company_id': order.company_id.id,
438 'user_id': order.user_id and order.user_id.id or False
441 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
442 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
446 def _make_invoice(self, cr, uid, order, lines, context=None):
447 inv_obj = self.pool.get('account.invoice')
448 obj_invoice_line = self.pool.get('account.invoice.line')
451 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
452 from_line_invoice_ids = []
453 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
454 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
455 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
456 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
457 for preinv in order.invoice_ids:
458 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
459 for preline in preinv.invoice_line:
460 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
461 lines.append(inv_line_id)
462 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
463 inv_id = inv_obj.create(cr, uid, inv, context=context)
464 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
465 if data.get('value', False):
466 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
467 inv_obj.button_compute(cr, uid, [inv_id])
470 def manual_invoice(self, cr, uid, ids, context=None):
471 mod_obj = self.pool.get('ir.model.data')
472 wf_service = netsvc.LocalService("workflow")
476 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
477 inv_ids.add(record.id)
478 # inv_ids would have old invoices if any
480 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
481 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
482 inv_ids1.add(record.id)
483 inv_ids = list(inv_ids1.difference(inv_ids))
485 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
486 res_id = res and res[1] or False,
489 'name': _('Customer Invoices'),
493 'res_model': 'account.invoice',
494 'context': "{'type':'out_invoice'}",
495 'type': 'ir.actions.act_window',
498 'res_id': inv_ids and inv_ids[0] or False,
501 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
505 picking_obj = self.pool.get('stock.picking')
506 invoice = self.pool.get('account.invoice')
507 obj_sale_order_line = self.pool.get('sale.order.line')
508 partner_currency = {}
511 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
512 # last day of the last month as invoice date
514 context['date_inv'] = date_inv
515 for o in self.browse(cr, uid, ids, context=context):
516 currency_id = o.pricelist_id.currency_id.id
517 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
518 raise osv.except_osv(
520 _('You cannot group sales having different currencies for the same partner.'))
522 partner_currency[o.partner_id.id] = currency_id
524 for line in o.order_line:
527 elif (line.state in states):
528 lines.append(line.id)
529 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
531 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
533 for o in self.browse(cr, uid, ids, context=context):
534 for i in o.invoice_ids:
535 if i.state == 'draft':
537 for val in invoices.values():
539 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
542 invoice_ref += o.name + '|'
543 self.write(cr, uid, [o.id], {'state': 'progress'})
544 if o.order_policy == 'picking':
545 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
546 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
547 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
549 for order, il in val:
550 res = self._make_invoice(cr, uid, order, il, context=context)
551 invoice_ids.append(res)
552 self.write(cr, uid, [order.id], {'state': 'progress'})
553 if order.order_policy == 'picking':
554 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
555 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
558 def action_invoice_cancel(self, cr, uid, ids, context=None):
561 for sale in self.browse(cr, uid, ids, context=context):
562 for line in sale.order_line:
564 # Check if the line is invoiced (has asociated invoice
565 # lines from non-cancelled invoices).
568 for iline in line.invoice_lines:
569 if iline.invoice_id and iline.invoice_id.state != 'cancel':
572 # Update the line (only when needed)
573 if line.invoiced != invoiced:
574 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
575 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
578 def action_invoice_end(self, cr, uid, ids, context=None):
579 for order in self.browse(cr, uid, ids, context=context):
581 # Update the sale order lines state (and invoiced flag).
583 for line in order.order_line:
586 # Check if the line is invoiced (has asociated invoice
587 # lines from non-cancelled invoices).
590 for iline in line.invoice_lines:
591 if iline.invoice_id and iline.invoice_id.state != 'cancel':
594 if line.invoiced != invoiced:
595 vals['invoiced'] = invoiced
596 # If the line was in exception state, now it gets confirmed.
597 if line.state == 'exception':
598 vals['state'] = 'confirmed'
599 # Update the line (only when needed).
601 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
603 # Update the sales order state.
605 if order.state == 'invoice_except':
606 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
609 def action_cancel(self, cr, uid, ids, context=None):
610 wf_service = netsvc.LocalService("workflow")
613 sale_order_line_obj = self.pool.get('sale.order.line')
614 proc_obj = self.pool.get('procurement.order')
615 for sale in self.browse(cr, uid, ids, context=context):
616 for pick in sale.picking_ids:
617 if pick.state not in ('draft', 'cancel'):
618 raise osv.except_osv(
619 _('Could not cancel sales order !'),
620 _('You must first cancel all picking attached to this sales order.'))
621 if pick.state == 'cancel':
622 for mov in pick.move_lines:
623 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
625 for proc in proc_ids:
626 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
627 for r in self.read(cr, uid, ids, ['picking_ids']):
628 for pick in r['picking_ids']:
629 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
630 for inv in sale.invoice_ids:
631 if inv.state not in ('draft', 'cancel'):
632 raise osv.except_osv(
633 _('Could not cancel this sales order !'),
634 _('You must first cancel all invoices attached to this sales order.'))
635 for r in self.read(cr, uid, ids, ['invoice_ids']):
636 for inv in r['invoice_ids']:
637 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
638 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
640 message = _("The sales order '%s' has been cancelled.") % (sale.name,)
641 self.log(cr, uid, sale.id, message)
642 self.write(cr, uid, ids, {'state': 'cancel'})
645 def action_wait(self, cr, uid, ids, context=None):
646 for o in self.browse(cr, uid, ids):
648 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
649 if (o.order_policy == 'manual'):
650 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
652 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
653 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
654 message = _("The quotation '%s' has been converted to a sales order.") % (o.name,)
655 self.log(cr, uid, o.id, message)
658 def procurement_lines_get(self, cr, uid, ids, *args):
660 for order in self.browse(cr, uid, ids, context={}):
661 for line in order.order_line:
662 if line.procurement_id:
663 res.append(line.procurement_id.id)
666 # if mode == 'finished':
667 # returns True if all lines are done, False otherwise
668 # if mode == 'canceled':
669 # returns True if there is at least one canceled line, False otherwise
670 def test_state(self, cr, uid, ids, mode, *args):
671 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
676 write_cancel_ids = []
677 for order in self.browse(cr, uid, ids, context={}):
678 for line in order.order_line:
679 if (not line.procurement_id) or (line.procurement_id.state=='done'):
680 if line.state != 'done':
681 write_done_ids.append(line.id)
684 if line.procurement_id:
685 if (line.procurement_id.state == 'cancel'):
687 if line.state != 'exception':
688 write_cancel_ids.append(line.id)
692 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
694 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
696 if mode == 'finished':
698 elif mode == 'canceled':
704 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
707 'origin': order.name,
708 'date_planned': date_planned,
709 'product_id': line.product_id.id,
710 'product_qty': line.product_uom_qty,
711 'product_uom': line.product_uom.id,
712 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
713 or line.product_uom_qty,
714 'product_uos': (line.product_uos and line.product_uos.id)\
715 or line.product_uom.id,
716 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
717 'procure_method': line.type,
719 'company_id': order.company_id.id,
723 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
724 location_id = order.shop_id.warehouse_id.lot_stock_id.id
725 output_id = order.shop_id.warehouse_id.lot_output_id.id
727 'name': line.name[:250],
728 'picking_id': picking_id,
729 'product_id': line.product_id.id,
730 'date': date_planned,
731 'date_expected': date_planned,
732 'product_qty': line.product_uom_qty,
733 'product_uom': line.product_uom.id,
734 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
735 'product_uos': (line.product_uos and line.product_uos.id)\
736 or line.product_uom.id,
737 'product_packaging': line.product_packaging.id,
738 'address_id': line.address_allotment_id.id or order.partner_shipping_id.id,
739 'location_id': location_id,
740 'location_dest_id': output_id,
741 'sale_line_id': line.id,
742 'tracking_id': False,
746 'company_id': order.company_id.id,
747 'price_unit': line.product_id.standard_price or 0.0
750 def _prepare_order_picking(self, cr, uid, order, context=None):
751 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
754 'origin': order.name,
755 'date': order.date_order,
758 'move_type': order.picking_policy,
760 'address_id': order.partner_shipping_id.id,
762 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
763 'company_id': order.company_id.id,
766 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
767 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
769 Define ship_recreate for process after shipping exception
770 param order: sale order to which the order lines belong
771 param line: sale order line records to procure
772 param move_id: the ID of stock move
773 param proc_id: the ID of procurement
775 move_obj = self.pool.get('stock.move')
776 if order.state == 'shipping_except':
777 for pick in order.picking_ids:
778 for move in pick.move_lines:
779 if move.state == 'cancel':
780 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
782 for mov in move_obj.browse(cr, uid, mov_ids):
783 # 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?
784 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
785 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
788 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
789 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
790 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
793 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
794 """Create the required procurements to supply sale order lines, also connecting
795 the procurements to appropriate stock moves in order to bring the goods to the
796 sale order's requested location.
798 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
799 a standard outgoing picking will be created to wrap the stock moves, as returned
800 by :meth:`~._prepare_order_picking`.
802 Modules that wish to customize the procurements or partition the stock moves over
803 multiple stock pickings may override this method and call ``super()`` with
804 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
806 :param browse_record order: sale order to which the order lines belong
807 :param list(browse_record) order_lines: sale order line records to procure
808 :param int picking_id: optional ID of a stock picking to which the created stock moves
809 will be added. A new picking will be created if ommitted.
812 move_obj = self.pool.get('stock.move')
813 picking_obj = self.pool.get('stock.picking')
814 procurement_obj = self.pool.get('procurement.order')
817 for line in order_lines:
818 if line.state == 'done':
821 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
824 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
826 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
827 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
829 # a service has no stock move
832 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
833 proc_ids.append(proc_id)
834 line.write({'procurement_id': proc_id})
835 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
837 wf_service = netsvc.LocalService("workflow")
839 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
841 for proc_id in proc_ids:
842 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
845 if order.state == 'shipping_except':
846 val['state'] = 'progress'
847 val['shipped'] = False
849 if (order.order_policy == 'manual'):
850 for line in order.order_line:
851 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
852 val['state'] = 'manual'
857 def action_ship_create(self, cr, uid, ids, context=None):
858 for order in self.browse(cr, uid, ids, context=context):
859 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
862 def action_ship_end(self, cr, uid, ids, context=None):
863 for order in self.browse(cr, uid, ids, context=context):
864 val = {'shipped': True}
865 if order.state == 'shipping_except':
866 val['state'] = 'progress'
867 if (order.order_policy == 'manual'):
868 for line in order.order_line:
869 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
870 val['state'] = 'manual'
872 for line in order.order_line:
874 if line.state == 'exception':
875 towrite.append(line.id)
877 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
878 self.write(cr, uid, [order.id], val)
881 def _log_event(self, cr, uid, ids, factor=0.7, name='Open Order'):
882 invs = self.read(cr, uid, ids, ['date_order', 'partner_id', 'amount_untaxed'])
884 part = inv['partner_id'] and inv['partner_id'][0]
885 pr = inv['amount_untaxed'] or 0.0
886 partnertype = 'customer'
889 'name': 'Order: '+name,
891 'description': 'Order '+str(inv['id']),
894 'date': time.strftime(DEFAULT_SERVER_DATE_FORMAT),
896 'partner_type': partnertype,
898 'planned_revenue': pr,
902 self.pool.get('res.partner.event').create(cr, uid, event)
904 def has_stockable_products(self, cr, uid, ids, *args):
905 for order in self.browse(cr, uid, ids):
906 for order_line in order.order_line:
907 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
912 # TODO add a field price_unit_uos
913 # - update it on change product and unit price
914 # - use it in report if there is a uos
915 class sale_order_line(osv.osv):
917 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
918 tax_obj = self.pool.get('account.tax')
919 cur_obj = self.pool.get('res.currency')
923 for line in self.browse(cr, uid, ids, context=context):
924 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
925 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)
926 cur = line.order_id.pricelist_id.currency_id
927 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
930 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
932 for line in self.browse(cr, uid, ids, context=context):
934 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
939 def _get_uom_id(self, cr, uid, *args):
941 proxy = self.pool.get('ir.model.data')
942 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
944 except Exception, ex:
947 _name = 'sale.order.line'
948 _description = 'Sales Order Line'
950 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
951 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
952 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
953 '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)]}),
954 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
955 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
956 'invoiced': fields.boolean('Invoiced', readonly=True),
957 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
958 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
959 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
960 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
961 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
962 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."),
963 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
964 'address_allotment_id': fields.many2one('res.partner.address', 'Allotment Partner'),
965 'product_uom_qty': fields.float('Quantity (UoM)', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
966 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
967 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
968 'product_uos': fields.many2one('product.uom', 'Product UoS'),
969 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
970 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
971 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
972 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
973 'notes': fields.text('Notes'),
974 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
975 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'State', required=True, readonly=True,
976 help='* The \'Draft\' state is set when the related sales order in draft state. \
977 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
978 \n* The \'Exception\' state is set when the related sales order is set as exception. \
979 \n* The \'Done\' state is set when the sales order line has been picked. \
980 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
981 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
982 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesman'),
983 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
985 _order = 'sequence, id'
987 'product_uom' : _get_uom_id,
990 'product_uom_qty': 1,
991 'product_uos_qty': 1,
995 'type': 'make_to_stock',
996 'product_packaging': False,
1000 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1001 """Prepare the dict of values to create the new invoice line for a
1002 sale order line. This method may be overridden to implement custom
1003 invoice generation (making sure to call super() to establish
1004 a clean extension chain).
1006 :param browse_record line: sale.order.line record to invoice
1007 :param int account_id: optional ID of a G/L account to force
1008 (this is used for returning products including service)
1009 :return: dict of values to create() the invoice line
1012 def _get_line_qty(line):
1013 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1014 if line.product_uos:
1015 return line.product_uos_qty or 0.0
1016 return line.product_uom_qty
1018 return self.pool.get('procurement.order').quantity_get(cr, uid,
1019 line.procurement_id.id, context=context)
1021 def _get_line_uom(line):
1022 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1023 if line.product_uos:
1024 return line.product_uos.id
1025 return line.product_uom.id
1027 return self.pool.get('procurement.order').uom_get(cr, uid,
1028 line.procurement_id.id, context=context)
1030 if not line.invoiced:
1033 account_id = line.product_id.product_tmpl_id.property_account_income.id
1035 account_id = line.product_id.categ_id.property_account_income_categ.id
1037 raise osv.except_osv(_('Error !'),
1038 _('There is no income account defined for this product: "%s" (id:%d)') % \
1039 (line.product_id.name, line.product_id.id,))
1041 prop = self.pool.get('ir.property').get(cr, uid,
1042 'property_account_income_categ', 'product.category',
1044 account_id = prop and prop.id or False
1045 uosqty = _get_line_qty(line)
1046 uos_id = _get_line_uom(line)
1049 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1050 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1051 fpos = line.order_id.fiscal_position or False
1052 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1054 raise osv.except_osv(_('Error !'),
1055 _('There is no income category account defined in default Properties for Product Category or Fiscal Position is not defined !'))
1058 'origin': line.order_id.name,
1059 'account_id': account_id,
1062 'discount': line.discount,
1064 'product_id': line.product_id.id or False,
1065 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1067 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1072 def invoice_line_create(self, cr, uid, ids, context=None):
1078 for line in self.browse(cr, uid, ids, context=context):
1079 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1081 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1082 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1083 self.write(cr, uid, [line.id], {'invoiced': True})
1084 sales.add(line.order_id.id)
1085 create_ids.append(inv_id)
1086 # Trigger workflow events
1087 wf_service = netsvc.LocalService("workflow")
1088 for sale_id in sales:
1089 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1092 def button_cancel(self, cr, uid, ids, context=None):
1093 for line in self.browse(cr, uid, ids, context=context):
1095 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1096 for move_line in line.move_ids:
1097 if move_line.state != 'cancel':
1098 raise osv.except_osv(
1099 _('Could not cancel sales order line!'),
1100 _('You must first cancel stock moves attached to this sales order line.'))
1101 return self.write(cr, uid, ids, {'state': 'cancel'})
1103 def button_confirm(self, cr, uid, ids, context=None):
1104 return self.write(cr, uid, ids, {'state': 'confirmed'})
1106 def button_done(self, cr, uid, ids, context=None):
1107 wf_service = netsvc.LocalService("workflow")
1108 res = self.write(cr, uid, ids, {'state': 'done'})
1109 for line in self.browse(cr, uid, ids, context=context):
1110 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1113 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1114 product_obj = self.pool.get('product.product')
1116 return {'value': {'product_uom': product_uos,
1117 'product_uom_qty': product_uos_qty}, 'domain': {}}
1119 product = product_obj.browse(cr, uid, product_id)
1121 'product_uom': product.uom_id.id,
1123 # FIXME must depend on uos/uom of the product and not only of the coeff.
1126 'product_uom_qty': product_uos_qty / product.uos_coeff,
1127 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1129 except ZeroDivisionError:
1131 return {'value': value}
1133 def copy_data(self, cr, uid, id, default=None, context=None):
1136 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1137 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1139 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1140 partner_id=False, packaging=False, flag=False, context=None):
1142 return {'value': {'product_packaging': False}}
1143 product_obj = self.pool.get('product.product')
1144 product_uom_obj = self.pool.get('product.uom')
1145 pack_obj = self.pool.get('product.packaging')
1150 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1151 product=product, qty=qty, uom=uom, partner_id=partner_id,
1152 packaging=packaging, flag=False, context=context)
1153 warning_msgs = res.get('warning') and res['warning']['message']
1155 products = product_obj.browse(cr, uid, product, context=context)
1156 if not products.packaging:
1157 packaging = result['product_packaging'] = False
1158 elif not packaging and products.packaging and not flag:
1159 packaging = products.packaging[0].id
1160 result['product_packaging'] = packaging
1163 default_uom = products.uom_id and products.uom_id.id
1164 pack = pack_obj.browse(cr, uid, packaging, context=context)
1165 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1166 # qty = qty - qty % q + q
1167 if qty and (q and not (qty % q) == 0):
1168 ean = pack.ean or _('(n/a)')
1171 if not warning_msgs:
1172 warn_msg = _("You selected a quantity of %d Units.\n"
1173 "But it's not compatible with the selected packaging.\n"
1174 "Here is a proposition of quantities according to the packaging:\n"
1175 "EAN: %s Quantity: %s Type of ul: %s") % \
1176 (qty, ean, qty_pack, type_ul.name)
1177 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1179 'title': _('Configuration Error !'),
1180 'message': warning_msgs
1182 result['product_uom_qty'] = qty
1184 return {'value': result, 'warning': warning}
1186 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1187 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1188 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1189 context = context or {}
1190 lang = lang or context.get('lang',False)
1192 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.'))
1194 product_uom_obj = self.pool.get('product.uom')
1195 partner_obj = self.pool.get('res.partner')
1196 product_obj = self.pool.get('product.product')
1197 context = {'lang': lang, 'partner_id': partner_id}
1199 lang = partner_obj.browse(cr, uid, partner_id).lang
1200 context_partner = {'lang': lang, 'partner_id': partner_id}
1203 return {'value': {'th_weight': 0, 'product_packaging': False,
1204 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1207 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1209 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1210 result = res.get('value', {})
1211 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1212 product_obj = product_obj.browse(cr, uid, product, context=context)
1216 uom2 = product_uom_obj.browse(cr, uid, uom)
1217 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1220 if product_obj.uos_id:
1221 uos2 = product_uom_obj.browse(cr, uid, uos)
1222 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1226 if product_obj.description_sale:
1227 result['notes'] = product_obj.description_sale
1228 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1229 if update_tax: #The quantity only have changed
1230 result['delay'] = (product_obj.sale_delay or 0.0)
1231 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1232 result.update({'type': product_obj.procure_method})
1235 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1237 if (not uom) and (not uos):
1238 result['product_uom'] = product_obj.uom_id.id
1239 if product_obj.uos_id:
1240 result['product_uos'] = product_obj.uos_id.id
1241 result['product_uos_qty'] = qty * product_obj.uos_coeff
1242 uos_category_id = product_obj.uos_id.category_id.id
1244 result['product_uos'] = False
1245 result['product_uos_qty'] = qty
1246 uos_category_id = False
1247 result['th_weight'] = qty * product_obj.weight
1248 domain = {'product_uom':
1249 [('category_id', '=', product_obj.uom_id.category_id.id)],
1251 [('category_id', '=', uos_category_id)]}
1253 elif uos and not uom: # only happens if uom is False
1254 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1255 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1256 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1257 elif uom: # whether uos is set or not
1258 default_uom = product_obj.uom_id and product_obj.uom_id.id
1259 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1260 if product_obj.uos_id:
1261 result['product_uos'] = product_obj.uos_id.id
1262 result['product_uos_qty'] = qty * product_obj.uos_coeff
1264 result['product_uos'] = False
1265 result['product_uos_qty'] = qty
1266 result['th_weight'] = q * product_obj.weight # Round the quantity up
1269 uom2 = product_obj.uom_id
1270 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1271 if (product_obj.type=='product') and int(compare_qty) == -1 \
1272 and (product_obj.procure_method=='make_to_stock'):
1273 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1274 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1275 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1276 max(0,product_obj.qty_available), product_obj.uom_id.name)
1277 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1281 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1282 'Please set one before choosing a product.')
1283 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1285 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1286 product, qty or 1.0, partner_id, {
1287 'uom': uom or result.get('product_uom'),
1291 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1292 "You have to change either the product, the quantity or the pricelist.")
1294 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1296 result.update({'price_unit': price})
1299 'title': _('Configuration Error !'),
1300 'message' : warning_msgs
1302 return {'value': result, 'domain': domain, 'warning': warning}
1304 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1305 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1306 lang=False, update_tax=True, date_order=False, context=None):
1307 context = context or {}
1308 lang = lang or ('lang' in context and context['lang'])
1309 res = self.product_id_change(cursor, user, ids, pricelist, product,
1310 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1311 partner_id=partner_id, lang=lang, update_tax=update_tax,
1312 date_order=date_order, context=context)
1313 if 'product_uom' in res['value']:
1314 del res['value']['product_uom']
1316 res['value']['price_unit'] = 0.0
1319 def unlink(self, cr, uid, ids, context=None):
1322 """Allows to delete sales order lines in draft,cancel states"""
1323 for rec in self.browse(cr, uid, ids, context=context):
1324 if rec.state not in ['draft', 'cancel']:
1325 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1326 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1330 class sale_config_picking_policy(osv.osv_memory):
1331 _name = 'sale.config.picking_policy'
1332 _inherit = 'res.config'
1335 'name': fields.char('Name', size=64),
1336 'sale_orders': fields.boolean('Based on Sales Orders',),
1337 'deli_orders': fields.boolean('Based on Delivery Orders'),
1338 'task_work': fields.boolean('Based on Tasks\' Work'),
1339 'timesheet': fields.boolean('Based on Timesheet'),
1340 'order_policy': fields.selection([
1341 ('manual', 'Invoice Based on Sales Orders'),
1342 ('picking', 'Invoice Based on Deliveries'),
1343 ], 'Main Method Based On', required=True, help="You can generate invoices based on sales orders or based on shippings."),
1344 'charge_delivery': fields.boolean('Do you charge the delivery?'),
1345 'time_unit': fields.many2one('product.uom','Main Working Time Unit')
1348 'order_policy': 'manual',
1349 '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,
1352 def onchange_order(self, cr, uid, ids, sale, deli, context=None):
1355 res.update({'order_policy': 'manual'})
1357 res.update({'order_policy': 'picking'})
1358 return {'value':res}
1360 def execute(self, cr, uid, ids, context=None):
1361 ir_values_obj = self.pool.get('ir.values')
1362 data_obj = self.pool.get('ir.model.data')
1363 menu_obj = self.pool.get('ir.ui.menu')
1364 module_obj = self.pool.get('ir.module.module')
1365 module_upgrade_obj = self.pool.get('base.module.upgrade')
1368 group_id = data_obj.get_object(cr, uid, 'base', 'group_sale_salesman').id
1370 wizard = self.browse(cr, uid, ids)[0]
1372 if wizard.sale_orders:
1373 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_invoicing_sales_order_lines').id
1374 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1376 if wizard.deli_orders:
1377 menu_id = data_obj.get_object(cr, uid, 'sale', 'menu_action_picking_list_to_invoice').id
1378 menu_obj.write(cr, uid, menu_id, {'groups_id':[(4,group_id)]})
1380 if wizard.task_work:
1381 module_name.append('project_timesheet')
1382 module_name.append('project_mrp')
1383 module_name.append('account_analytic_analysis')
1385 if wizard.timesheet:
1386 module_name.append('account_analytic_analysis')
1388 if wizard.charge_delivery:
1389 module_name.append('delivery')
1391 if len(module_name):
1393 need_install = False
1395 for module in module_name:
1396 data_id = module_obj.name_search(cr, uid , module, [], '=')
1397 module_ids.append(data_id[0][0])
1399 for module in module_obj.browse(cr, uid, module_ids):
1400 if module.state == 'uninstalled':
1401 module_obj.state_update(cr, uid, [module.id], 'to install', ['uninstalled'], context)
1405 pooler.restart_pool(cr.dbname, update_module=True)[1]
1407 if wizard.time_unit:
1408 prod_id = data_obj.get_object(cr, uid, 'product', 'product_consultant').id
1409 product_obj = self.pool.get('product.product')
1410 product_obj.write(cr, uid, prod_id, {'uom_id':wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
1412 ir_values_obj.set(cr, uid, 'default', False, 'order_policy', ['sale.order'], wizard.order_policy)
1413 if wizard.task_work and wizard.time_unit:
1414 company_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
1415 self.pool.get('res.company').write(cr, uid, [company_id], {
1416 'project_time_mode_id': wizard.time_unit.id
1419 sale_config_picking_policy()
1421 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: