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, DATETIME_FORMATS_MAP, 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 _inherit = ['ir.needaction_mixin', 'mail.thread']
52 _description = "Sales Order"
55 def copy(self, cr, uid, id, default=None, context=None):
63 'date_confirm': False,
64 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
66 return super(sale_order, self).copy(cr, uid, id, default, context=context)
68 def _amount_line_tax(self, cr, uid, line, context=None):
70 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.product_id, line.order_id.partner_id)['taxes']:
71 val += c.get('amount', 0.0)
74 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
75 cur_obj = self.pool.get('res.currency')
77 for order in self.browse(cr, uid, ids, context=context):
79 'amount_untaxed': 0.0,
84 cur = order.pricelist_id.currency_id
85 for line in order.order_line:
86 val1 += line.price_subtotal
87 val += self._amount_line_tax(cr, uid, line, context=context)
88 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
89 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
90 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
94 def _picked_rate(self, cr, uid, ids, name, arg, context=None):
100 tmp[id] = {'picked': 0.0, 'total': 0.0}
102 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
106 stock_picking p on (p.id=m.picking_id)
108 procurement_order mp on (mp.move_id=m.id)
110 p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
112 for item in cr.dictfetchall():
113 if item['move_state'] == 'cancel':
116 if item['picking_type'] == 'in':#this is a returned picking
117 tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
118 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
119 tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
121 tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
122 if item['procurement_state'] == 'done' or item['move_state'] == 'done':
123 tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
125 for order in self.browse(cr, uid, ids, context=context):
127 res[order.id] = 100.0
129 res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
132 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
134 for sale in self.browse(cursor, user, ids, context=context):
139 for invoice in sale.invoice_ids:
140 if invoice.state not in ('draft', 'cancel'):
141 tot += invoice.amount_untaxed
143 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
148 def _invoiced(self, cursor, user, ids, name, arg, context=None):
150 for sale in self.browse(cursor, user, ids, context=context):
152 invoice_existence = False
153 for invoice in sale.invoice_ids:
154 if invoice.state!='cancel':
155 invoice_existence = True
156 if invoice.state != 'paid':
159 if not invoice_existence:
163 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
172 clause += 'AND inv.state = \'paid\''
174 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
175 sale_clause = ', sale_order AS sale '
178 cursor.execute('SELECT rel.order_id ' \
179 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
180 'WHERE rel.invoice_id = inv.id ' + clause)
181 res = cursor.fetchall()
183 cursor.execute('SELECT sale.id ' \
184 'FROM sale_order AS sale ' \
185 'WHERE sale.id NOT IN ' \
186 '(SELECT rel.order_id ' \
187 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
188 res.extend(cursor.fetchall())
190 return [('id', '=', 0)]
191 return [('id', 'in', [x[0] for x in res])]
193 def _get_order(self, cr, uid, ids, context=None):
195 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
196 result[line.order_id.id] = True
200 'name': fields.char('Order Reference', size=64, required=True,
201 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
202 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
203 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
204 'client_order_ref': fields.char('Customer Reference', size=64),
205 'state': fields.selection([
206 ('draft', 'Draft Quotation'),
207 ('sent', 'Quotation Sent'),
208 ('cancel', 'Cancelled'),
209 ('waiting_date', 'Waiting Schedule'),
210 ('progress', 'Sale Order'),
211 ('manual', 'Sale to Invoice'),
212 ('shipping_except', 'Shipping Exception'),
213 ('invoice_except', 'Invoice Exception'),
215 ], 'Status', 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),
216 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
217 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
218 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
219 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
220 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
221 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
222 'partner_shipping_id': fields.many2one('res.partner', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Shipping address for current sales order."),
224 '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."),
225 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
226 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
227 help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
228 'order_policy': fields.selection([
229 ('manual', 'On Demand'),
230 ('picking', 'On Delivery Order'),
231 ('prepaid', 'Before Delivery'),
232 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
233 help="""This field controls how invoice and delivery operations are synchronized.
234 - With 'On Demand', the invoice is created manually when needed.
235 - With 'On Delivery Order', a draft invoice is generated after all pickings have been processed.
236 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
237 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
238 'project_id': fields.many2one('account.analytic.account', 'Contract/Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
240 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
241 '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)."),
242 'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
243 '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."),
244 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
245 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
246 'invoiced': fields.function(_invoiced, string='Paid',
247 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
248 'note': fields.text('Terms and conditions'),
250 'amount_untaxed': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Untaxed Amount',
252 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
253 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
255 multi='sums', help="The amount without tax."),
256 'amount_tax': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Taxes',
258 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
259 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
261 multi='sums', help="The tax amount."),
262 'amount_total': fields.function(_amount_all, digits_compute= dp.get_precision('Sale Price'), string='Total',
264 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
265 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
267 multi='sums', help="The total amount."),
269 '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)]}),
270 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
271 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
272 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
275 'picking_policy': 'direct',
276 'date_order': fields.date.context_today,
277 'order_policy': 'manual',
279 'user_id': lambda obj, cr, uid, context: uid,
280 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
281 'invoice_quantity': 'order',
282 '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'],
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 sales order, you must cancel it before ! To cancel a sale order, you must first cancel related picking for 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, context=None):
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 self.action_cancel_draft_send_note(cr, uid, ids, context=context)
327 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context={}):
328 if (not pricelist_id) or (not order_lines):
331 'title': _('Pricelist Warning!'),
332 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
334 return {'warning': warning}
336 def onchange_partner_order_id(self, cr, uid, ids, order_id, invoice_id=False, shipping_id=False, context={}):
341 val['partner_invoice_id'] = order_id
343 val['partner_shipping_id'] = order_id
344 return {'value': val}
346 def onchange_partner_id(self, cr, uid, ids, part):
348 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
350 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
351 part = self.pool.get('res.partner').browse(cr, uid, part)
352 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
353 payment_term = part.property_payment_term and part.property_payment_term.id or False
354 fiscal_position = part.property_account_position and part.property_account_position.id or False
355 dedicated_salesman = part.user_id and part.user_id.id or uid
357 'partner_invoice_id': addr['invoice'],
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 order = super(sale_order, self).create(cr, uid, vals, context=context)
393 self.create_send_note(cr, uid, [order], 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 'invoice_line': [(6, 0, lines)],
433 'currency_id': order.pricelist_id.currency_id.id,
434 'comment': order.note,
435 'payment_term': order.payment_term and order.payment_term.id or False,
436 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
437 'date_invoice': context.get('date_invoice', False),
438 'company_id': order.company_id.id,
439 'user_id': order.user_id and order.user_id.id or False
442 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
443 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
447 def _make_invoice(self, cr, uid, order, lines, context=None):
448 inv_obj = self.pool.get('account.invoice')
449 obj_invoice_line = self.pool.get('account.invoice.line')
452 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
453 from_line_invoice_ids = []
454 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
455 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
456 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
457 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
458 for preinv in order.invoice_ids:
459 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
460 for preline in preinv.invoice_line:
461 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
462 lines.append(inv_line_id)
463 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
464 inv_id = inv_obj.create(cr, uid, inv, context=context)
465 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
466 if data.get('value', False):
467 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
468 inv_obj.button_compute(cr, uid, [inv_id])
471 def print_quotation(self, cr, uid, ids, context=None):
473 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
475 assert len(ids) == 1, 'This option should only be used for a single id at a time'
476 wf_service = netsvc.LocalService("workflow")
477 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
479 'model': 'sale.order',
481 'form': self.read(cr, uid, ids[0], context=context),
483 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
485 def manual_invoice(self, cr, uid, ids, context=None):
486 mod_obj = self.pool.get('ir.model.data')
487 wf_service = netsvc.LocalService("workflow")
491 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
492 inv_ids.add(record.id)
493 # inv_ids would have old invoices if any
495 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
496 for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
497 inv_ids1.add(record.id)
498 inv_ids = list(inv_ids1.difference(inv_ids))
500 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
501 res_id = res and res[1] or False,
504 'name': _('Customer Invoices'),
508 'res_model': 'account.invoice',
509 'context': "{'type':'out_invoice'}",
510 'type': 'ir.actions.act_window',
513 'res_id': inv_ids and inv_ids[0] or False,
516 def action_view_invoice(self, cr, uid, ids, context=None):
518 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
520 mod_obj = self.pool.get('ir.model.data')
522 'name': _('Cutomer Invoice'),
524 'res_model': 'account.invoice',
525 'context': "{'type':'out_invoice', 'journal_type': 'sale'}",
526 'type': 'ir.actions.act_window',
530 #compute the number of invoices to display
532 for so in self.browse(cr, uid, ids, context=context):
533 inv_ids += [invoice.id for invoice in so.invoice_ids]
534 #choose the view_mode accordingly
536 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree')
538 'view_mode': 'tree,form',
539 'res_id': inv_ids or False
542 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
545 'res_id': inv_ids and inv_ids[0] or False,
547 result.update(view_id = res and res[1] or False)
551 def action_view_delivery(self, cr, uid, ids, context=None):
553 This function returns an action that display existing delivery orders of given sale order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
555 mod_obj = self.pool.get('ir.model.data')
557 'name': _('Delivery Order'),
559 'res_model': 'stock.picking',
560 'context': "{'type':'out'}",
561 'type': 'ir.actions.act_window',
565 #compute the number of delivery orders to display
567 for so in self.browse(cr, uid, ids, context=context):
568 pick_ids += [picking.id for picking in so.picking_ids]
569 #choose the view_mode accordingly
570 if len(pick_ids) > 1:
571 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_tree')
573 'view_mode': 'tree,form',
574 'res_id': pick_ids or False
577 res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
580 'res_id': pick_ids and pick_ids[0] or False,
582 result.update(view_id = res and res[1] or False)
585 def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_inv = False, context=None):
589 picking_obj = self.pool.get('stock.picking')
590 invoice = self.pool.get('account.invoice')
591 obj_sale_order_line = self.pool.get('sale.order.line')
592 partner_currency = {}
595 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
596 # last day of the last month as invoice date
598 context['date_inv'] = date_inv
599 for o in self.browse(cr, uid, ids, context=context):
600 currency_id = o.pricelist_id.currency_id.id
601 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
602 raise osv.except_osv(
604 _('You cannot group sales having different currencies for the same partner.'))
606 partner_currency[o.partner_id.id] = currency_id
608 for line in o.order_line:
611 elif (line.state in states):
612 lines.append(line.id)
613 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
615 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
617 for o in self.browse(cr, uid, ids, context=context):
618 for i in o.invoice_ids:
619 if i.state == 'draft':
621 for val in invoices.values():
623 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
626 invoice_ref += o.name + '|'
627 self.write(cr, uid, [o.id], {'state': 'progress'})
628 if o.order_policy == 'picking':
629 picking_obj.write(cr, uid, map(lambda x: x.id, o.picking_ids), {'invoice_state': 'invoiced'})
630 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
631 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
633 for order, il in val:
634 res = self._make_invoice(cr, uid, order, il, context=context)
635 invoice_ids.append(res)
636 self.write(cr, uid, [order.id], {'state': 'progress'})
637 if order.order_policy == 'picking':
638 picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
639 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
641 self.invoice_send_note(cr, uid, ids, res, context)
644 def action_invoice_cancel(self, cr, uid, ids, context=None):
647 for sale in self.browse(cr, uid, ids, context=context):
648 for line in sale.order_line:
650 # Check if the line is invoiced (has asociated invoice
651 # lines from non-cancelled invoices).
654 for iline in line.invoice_lines:
655 if iline.invoice_id and iline.invoice_id.state != 'cancel':
658 # Update the line (only when needed)
659 if line.invoiced != invoiced:
660 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
661 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
664 def action_invoice_end(self, cr, uid, ids, context=None):
665 for order in self.browse(cr, uid, ids, context=context):
667 # Update the sale order lines state (and invoiced flag).
669 for line in order.order_line:
672 # Check if the line is invoiced (has asociated invoice
673 # lines from non-cancelled invoices).
676 for iline in line.invoice_lines:
677 if iline.invoice_id and iline.invoice_id.state != 'cancel':
680 if line.invoiced != invoiced:
681 vals['invoiced'] = invoiced
682 # If the line was in exception state, now it gets confirmed.
683 if line.state == 'exception':
684 vals['state'] = 'confirmed'
685 # Update the line (only when needed).
687 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
689 # Update the sales order state.
691 if order.state == 'invoice_except':
692 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
695 def action_cancel(self, cr, uid, ids, context=None):
696 wf_service = netsvc.LocalService("workflow")
699 sale_order_line_obj = self.pool.get('sale.order.line')
700 proc_obj = self.pool.get('procurement.order')
701 for sale in self.browse(cr, uid, ids, context=context):
702 for pick in sale.picking_ids:
703 if pick.state not in ('draft', 'cancel'):
704 raise osv.except_osv(
705 _('Could not cancel sales order!'),
706 _('First cancelled all picking attached to this sale order.'))
707 if pick.state == 'cancel':
708 for mov in pick.move_lines:
709 proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
711 for proc in proc_ids:
712 wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
713 for r in self.read(cr, uid, ids, ['picking_ids']):
714 for pick in r['picking_ids']:
715 wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
716 for inv in sale.invoice_ids:
717 if inv.state not in ('draft', 'cancel'):
718 raise osv.except_osv(
719 _('Could not cancel this sales order!'),
720 _('First cancelled all invoices attached to this sale order.'))
721 for r in self.read(cr, uid, ids, ['invoice_ids']):
722 for inv in r['invoice_ids']:
723 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
724 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
726 self.cancel_send_note(cr, uid, [sale.id], context=None)
727 self.write(cr, uid, ids, {'state': 'cancel'})
730 def action_button_confirm(self, cr, uid, ids, context=None):
731 assert len(ids) == 1, 'This option should only be used for a single id at a time'
732 wf_service = netsvc.LocalService('workflow')
733 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
735 # redisplay the record as a sale order
736 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
737 view_id = view_ref and view_ref[1] or False,
739 'type': 'ir.actions.act_window',
740 'name': _('Sales Order'),
741 'res_model': 'sale.order',
750 def action_wait(self, cr, uid, ids, context=None):
751 for o in self.browse(cr, uid, ids):
753 raise osv.except_osv(_('Error !'),_('You cannot confirm a sale order which has no line.'))
754 if (o.order_policy == 'manual'):
755 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
757 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
758 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
759 self.confirm_send_note(cr, uid, ids, context)
762 def action_quotation_send(self, cr, uid, ids, context=None):
764 This function opens a window to compose an email, with the edi sale template message loaded by default
766 assert len(ids) == 1, 'This option should only be used for a single id at a time'
767 mod_obj = self.pool.get('ir.model.data')
768 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
769 template_id = template and template[1] or False
770 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
771 res_id = res and res[1] or False
772 ctx = dict(context, active_model='sale.order', active_id=ids[0])
773 ctx.update({'mail.compose.template_id': template_id})
777 'res_model': 'mail.compose.message',
778 'views': [(res_id,'form')],
780 'type': 'ir.actions.act_window',
786 def procurement_lines_get(self, cr, uid, ids, *args):
788 for order in self.browse(cr, uid, ids, context={}):
789 for line in order.order_line:
790 if line.procurement_id:
791 res.append(line.procurement_id.id)
794 # if mode == 'finished':
795 # returns True if all lines are done, False otherwise
796 # if mode == 'canceled':
797 # returns True if there is at least one canceled line, False otherwise
798 def test_state(self, cr, uid, ids, mode, *args):
799 assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
804 write_cancel_ids = []
805 for order in self.browse(cr, uid, ids, context={}):
806 for line in order.order_line:
807 if (not line.procurement_id) or (line.procurement_id.state=='done'):
808 if line.state != 'done':
809 write_done_ids.append(line.id)
812 if line.procurement_id:
813 if (line.procurement_id.state == 'cancel'):
815 if line.state != 'exception':
816 write_cancel_ids.append(line.id)
820 self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
822 self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
824 if mode == 'finished':
826 elif mode == 'canceled':
832 def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
835 'origin': order.name,
836 'date_planned': date_planned,
837 'product_id': line.product_id.id,
838 'product_qty': line.product_uom_qty,
839 'product_uom': line.product_uom.id,
840 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
841 or line.product_uom_qty,
842 'product_uos': (line.product_uos and line.product_uos.id)\
843 or line.product_uom.id,
844 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
845 'procure_method': line.type,
847 'company_id': order.company_id.id,
851 def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
852 location_id = order.shop_id.warehouse_id.lot_stock_id.id
853 output_id = order.shop_id.warehouse_id.lot_output_id.id
855 'name': line.name[:250],
856 'picking_id': picking_id,
857 'product_id': line.product_id.id,
858 'date': date_planned,
859 'date_expected': date_planned,
860 'product_qty': line.product_uom_qty,
861 'product_uom': line.product_uom.id,
862 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
863 'product_uos': (line.product_uos and line.product_uos.id)\
864 or line.product_uom.id,
865 'product_packaging': line.product_packaging.id,
866 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
867 'location_id': location_id,
868 'location_dest_id': output_id,
869 'sale_line_id': line.id,
870 'tracking_id': False,
874 'company_id': order.company_id.id,
875 'price_unit': line.product_id.standard_price or 0.0
878 def _prepare_order_picking(self, cr, uid, order, context=None):
879 pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
882 'origin': order.name,
883 'date': order.date_order,
886 'move_type': order.picking_policy,
888 'partner_id': order.partner_shipping_id.id,
890 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
891 'company_id': order.company_id.id,
894 def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
895 # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
897 Define ship_recreate for process after shipping exception
898 param order: sale order to which the order lines belong
899 param line: sale order line records to procure
900 param move_id: the ID of stock move
901 param proc_id: the ID of procurement
903 move_obj = self.pool.get('stock.move')
904 if order.state == 'shipping_except':
905 for pick in order.picking_ids:
906 for move in pick.move_lines:
907 if move.state == 'cancel':
908 mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
910 for mov in move_obj.browse(cr, uid, mov_ids):
911 # 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?
912 move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
913 self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
916 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
917 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
918 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
921 def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
922 """Create the required procurements to supply sale order lines, also connecting
923 the procurements to appropriate stock moves in order to bring the goods to the
924 sale order's requested location.
926 If ``picking_id`` is provided, the stock moves will be added to it, otherwise
927 a standard outgoing picking will be created to wrap the stock moves, as returned
928 by :meth:`~._prepare_order_picking`.
930 Modules that wish to customize the procurements or partition the stock moves over
931 multiple stock pickings may override this method and call ``super()`` with
932 different subsets of ``order_lines`` and/or preset ``picking_id`` values.
934 :param browse_record order: sale order to which the order lines belong
935 :param list(browse_record) order_lines: sale order line records to procure
936 :param int picking_id: optional ID of a stock picking to which the created stock moves
937 will be added. A new picking will be created if ommitted.
940 move_obj = self.pool.get('stock.move')
941 picking_obj = self.pool.get('stock.picking')
942 procurement_obj = self.pool.get('procurement.order')
945 for line in order_lines:
946 if line.state == 'done':
949 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
952 if line.product_id.product_tmpl_id.type in ('product', 'consu'):
954 picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
955 move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
957 # a service has no stock move
960 proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
961 proc_ids.append(proc_id)
962 line.write({'procurement_id': proc_id})
963 self.ship_recreate(cr, uid, order, line, move_id, proc_id)
965 wf_service = netsvc.LocalService("workflow")
967 wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
968 self.delivery_send_note(cr, uid, [order.id], picking_id, context)
971 for proc_id in proc_ids:
972 wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
975 if order.state == 'shipping_except':
976 val['state'] = 'progress'
977 val['shipped'] = False
979 if (order.order_policy == 'manual'):
980 for line in order.order_line:
981 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
982 val['state'] = 'manual'
987 def action_ship_create(self, cr, uid, ids, context=None):
988 for order in self.browse(cr, uid, ids, context=context):
989 self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
992 def action_ship_end(self, cr, uid, ids, context=None):
993 for order in self.browse(cr, uid, ids, context=context):
994 val = {'shipped': True}
995 if order.state == 'shipping_except':
996 val['state'] = 'progress'
997 if (order.order_policy == 'manual'):
998 for line in order.order_line:
999 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
1000 val['state'] = 'manual'
1002 for line in order.order_line:
1004 if line.state == 'exception':
1005 towrite.append(line.id)
1007 self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
1008 res = self.write(cr, uid, [order.id], val)
1010 self.delivery_end_send_note(cr, uid, [order.id], context=context)
1013 def has_stockable_products(self, cr, uid, ids, *args):
1014 for order in self.browse(cr, uid, ids):
1015 for order_line in order.order_line:
1016 if order_line.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
1020 # ------------------------------------------------
1021 # OpenChatter methods and notifications
1022 # ------------------------------------------------
1024 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1025 result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
1026 for obj in self.browse(cr, uid, ids, context=context):
1027 if (obj.state == 'manual' or obj.state == 'progress'):
1028 result[obj.id].append(obj.user_id.id)
1031 def create_send_note(self, cr, uid, ids, context=None):
1032 for obj in self.browse(cr, uid, ids, context=context):
1033 self.message_subscribe(cr, uid, [obj.id], [obj.user_id.id], context=context)
1034 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
1036 def confirm_send_note(self, cr, uid, ids, context=None):
1037 for obj in self.browse(cr, uid, ids, context=context):
1038 self.message_append_note(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
1040 def cancel_send_note(self, cr, uid, ids, context=None):
1041 for obj in self.browse(cr, uid, ids, context=context):
1042 self.message_append_note(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
1044 def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
1045 for order in self.browse(cr, uid, ids, context=context):
1046 for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
1047 # convert datetime field to a datetime, using server format, then
1048 # convert it to the user TZ and re-render it with %Z to add the timezone
1049 picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
1050 picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
1051 self.message_append_note(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
1053 def delivery_end_send_note(self, cr, uid, ids, context=None):
1054 self.message_append_note(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
1056 def invoice_paid_send_note(self, cr, uid, ids, context=None):
1057 self.message_append_note(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
1059 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
1060 for order in self.browse(cr, uid, ids, context=context):
1061 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
1062 self.message_append_note(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
1064 def action_cancel_draft_send_note(self, cr, uid, ids, context=None):
1065 return self.message_append_note(cr, uid, ids, body='Sale order has been set in draft.', context=context)
1070 # TODO add a field price_unit_uos
1071 # - update it on change product and unit price
1072 # - use it in report if there is a uos
1073 class sale_order_line(osv.osv):
1075 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
1076 tax_obj = self.pool.get('account.tax')
1077 cur_obj = self.pool.get('res.currency')
1081 for line in self.browse(cr, uid, ids, context=context):
1082 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
1083 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
1084 cur = line.order_id.pricelist_id.currency_id
1085 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
1088 def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
1090 for line in self.browse(cr, uid, ids, context=context):
1092 res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
1097 def _get_uom_id(self, cr, uid, *args):
1099 proxy = self.pool.get('ir.model.data')
1100 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
1102 except Exception, ex:
1105 _name = 'sale.order.line'
1106 _description = 'Sales Order Line'
1108 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
1109 'name': fields.char('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1110 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
1111 '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)]}),
1112 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
1113 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
1114 'invoiced': fields.boolean('Invoiced', readonly=True),
1115 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1116 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Sale Price'), readonly=True, states={'draft': [('readonly', False)]}),
1117 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Sale Price')),
1118 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
1119 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
1120 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."),
1121 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
1122 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner'),
1123 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
1124 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1125 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
1126 'product_uos': fields.many2one('product.uom', 'Product UoS'),
1127 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1128 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
1129 'discount': fields.float('Discount (%)', digits=(16, 2), readonly=True, states={'draft': [('readonly', False)]}),
1130 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
1131 'notes': fields.text('Notes'),
1132 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
1133 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
1134 help='* The \'Draft\' state is set when the related sales order in draft state. \
1135 \n* The \'Confirmed\' state is set when the related sales order is confirmed. \
1136 \n* The \'Exception\' state is set when the related sales order is set as exception. \
1137 \n* The \'Done\' state is set when the sales order line has been picked. \
1138 \n* The \'Cancelled\' state is set when a user cancel the sales order related.'),
1139 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
1140 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
1141 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1143 _order = 'sequence, id'
1145 'product_uom' : _get_uom_id,
1148 'product_uom_qty': 1,
1149 'product_uos_qty': 1,
1153 'type': 'make_to_stock',
1154 'product_packaging': False,
1158 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
1159 """Prepare the dict of values to create the new invoice line for a
1160 sale order line. This method may be overridden to implement custom
1161 invoice generation (making sure to call super() to establish
1162 a clean extension chain).
1164 :param browse_record line: sale.order.line record to invoice
1165 :param int account_id: optional ID of a G/L account to force
1166 (this is used for returning products including service)
1167 :return: dict of values to create() the invoice line
1170 def _get_line_qty(line):
1171 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1172 if line.product_uos:
1173 return line.product_uos_qty or 0.0
1174 return line.product_uom_qty
1176 return self.pool.get('procurement.order').quantity_get(cr, uid,
1177 line.procurement_id.id, context=context)
1179 def _get_line_uom(line):
1180 if (line.order_id.invoice_quantity=='order') or not line.procurement_id:
1181 if line.product_uos:
1182 return line.product_uos.id
1183 return line.product_uom.id
1185 return self.pool.get('procurement.order').uom_get(cr, uid,
1186 line.procurement_id.id, context=context)
1188 if not line.invoiced:
1191 account_id = line.product_id.product_tmpl_id.property_account_income.id
1193 account_id = line.product_id.categ_id.property_account_income_categ.id
1195 raise osv.except_osv(_('Error !'),
1196 _('There is no income account defined for this product: "%s" (id:%d)') % \
1197 (line.product_id.name, line.product_id.id,))
1199 prop = self.pool.get('ir.property').get(cr, uid,
1200 'property_account_income_categ', 'product.category',
1202 account_id = prop and prop.id or False
1203 uosqty = _get_line_qty(line)
1204 uos_id = _get_line_uom(line)
1207 pu = round(line.price_unit * line.product_uom_qty / uosqty,
1208 self.pool.get('decimal.precision').precision_get(cr, uid, 'Sale Price'))
1209 fpos = line.order_id.fiscal_position or False
1210 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
1212 raise osv.except_osv(_('Error !'),
1213 _('There is no Fiscal Position defined or income category account defined for Product Categories default Properties.'))
1216 'origin': line.order_id.name,
1217 'account_id': account_id,
1220 'discount': line.discount,
1222 'product_id': line.product_id.id or False,
1223 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
1225 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
1230 def invoice_line_create(self, cr, uid, ids, context=None):
1236 for line in self.browse(cr, uid, ids, context=context):
1237 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
1239 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
1240 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
1241 self.write(cr, uid, [line.id], {'invoiced': True})
1242 sales.add(line.order_id.id)
1243 create_ids.append(inv_id)
1244 # Trigger workflow events
1245 wf_service = netsvc.LocalService("workflow")
1246 for sale_id in sales:
1247 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
1250 def button_cancel(self, cr, uid, ids, context=None):
1251 for line in self.browse(cr, uid, ids, context=context):
1253 raise osv.except_osv(_('Invalid action !'), _('You cannot cancel a sale order line that has already been invoiced!'))
1254 for move_line in line.move_ids:
1255 if move_line.state != 'cancel':
1256 raise osv.except_osv(
1257 _('Cannot cancel sale order line!'),
1258 _('You must first cancel stock moves attached to this sales order line.'))
1259 return self.write(cr, uid, ids, {'state': 'cancel'})
1261 def button_confirm(self, cr, uid, ids, context=None):
1262 return self.write(cr, uid, ids, {'state': 'confirmed'})
1264 def button_done(self, cr, uid, ids, context=None):
1265 wf_service = netsvc.LocalService("workflow")
1266 res = self.write(cr, uid, ids, {'state': 'done'})
1267 for line in self.browse(cr, uid, ids, context=context):
1268 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
1271 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1272 product_obj = self.pool.get('product.product')
1274 return {'value': {'product_uom': product_uos,
1275 'product_uom_qty': product_uos_qty}, 'domain': {}}
1277 product = product_obj.browse(cr, uid, product_id)
1279 'product_uom': product.uom_id.id,
1281 # FIXME must depend on uos/uom of the product and not only of the coeff.
1284 'product_uom_qty': product_uos_qty / product.uos_coeff,
1285 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1287 except ZeroDivisionError:
1289 return {'value': value}
1291 def copy_data(self, cr, uid, id, default=None, context=None):
1294 default.update({'state': 'draft', 'move_ids': [], 'invoiced': False, 'invoice_lines': []})
1295 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
1297 def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
1298 partner_id=False, packaging=False, flag=False, context=None):
1300 return {'value': {'product_packaging': False}}
1301 product_obj = self.pool.get('product.product')
1302 product_uom_obj = self.pool.get('product.uom')
1303 pack_obj = self.pool.get('product.packaging')
1308 res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
1309 product=product, qty=qty, uom=uom, partner_id=partner_id,
1310 packaging=packaging, flag=False, context=context)
1311 warning_msgs = res.get('warning') and res['warning']['message']
1313 products = product_obj.browse(cr, uid, product, context=context)
1314 if not products.packaging:
1315 packaging = result['product_packaging'] = False
1316 elif not packaging and products.packaging and not flag:
1317 packaging = products.packaging[0].id
1318 result['product_packaging'] = packaging
1321 default_uom = products.uom_id and products.uom_id.id
1322 pack = pack_obj.browse(cr, uid, packaging, context=context)
1323 q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
1324 # qty = qty - qty % q + q
1325 if qty and (q and not (qty % q) == 0):
1326 ean = pack.ean or _('(n/a)')
1329 if not warning_msgs:
1330 warn_msg = _("You selected a quantity of %d Units.\n"
1331 "But it's not compatible with the selected packaging.\n"
1332 "Here is a proposition of quantities according to the packaging:\n"
1333 "EAN: %s Quantity: %s Type of ul: %s") % \
1334 (qty, ean, qty_pack, type_ul.name)
1335 warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
1337 'title': _('Configuration Error !'),
1338 'message': warning_msgs
1340 result['product_uom_qty'] = qty
1342 return {'value': result, 'warning': warning}
1344 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1345 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1346 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1347 context = context or {}
1348 lang = lang or context.get('lang',False)
1350 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
1352 product_uom_obj = self.pool.get('product.uom')
1353 partner_obj = self.pool.get('res.partner')
1354 product_obj = self.pool.get('product.product')
1355 context = {'lang': lang, 'partner_id': partner_id}
1357 lang = partner_obj.browse(cr, uid, partner_id).lang
1358 context_partner = {'lang': lang, 'partner_id': partner_id}
1361 return {'value': {'th_weight': 0, 'product_packaging': False,
1362 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1365 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1367 res = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context)
1368 result = res.get('value', {})
1369 warning_msgs = res.get('warning') and res['warning']['message'] or ''
1370 product_obj = product_obj.browse(cr, uid, product, context=context)
1374 uom2 = product_uom_obj.browse(cr, uid, uom)
1375 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1378 if product_obj.uos_id:
1379 uos2 = product_uom_obj.browse(cr, uid, uos)
1380 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1384 if product_obj.description_sale:
1385 result['notes'] = product_obj.description_sale
1386 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
1387 if update_tax: #The quantity only have changed
1388 result['delay'] = (product_obj.sale_delay or 0.0)
1389 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1390 result.update({'type': product_obj.procure_method})
1393 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1395 if (not uom) and (not uos):
1396 result['product_uom'] = product_obj.uom_id.id
1397 if product_obj.uos_id:
1398 result['product_uos'] = product_obj.uos_id.id
1399 result['product_uos_qty'] = qty * product_obj.uos_coeff
1400 uos_category_id = product_obj.uos_id.category_id.id
1402 result['product_uos'] = False
1403 result['product_uos_qty'] = qty
1404 uos_category_id = False
1405 result['th_weight'] = qty * product_obj.weight
1406 domain = {'product_uom':
1407 [('category_id', '=', product_obj.uom_id.category_id.id)],
1409 [('category_id', '=', uos_category_id)]}
1411 elif uos and not uom: # only happens if uom is False
1412 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1413 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1414 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1415 elif uom: # whether uos is set or not
1416 default_uom = product_obj.uom_id and product_obj.uom_id.id
1417 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1418 if product_obj.uos_id:
1419 result['product_uos'] = product_obj.uos_id.id
1420 result['product_uos_qty'] = qty * product_obj.uos_coeff
1422 result['product_uos'] = False
1423 result['product_uos_qty'] = qty
1424 result['th_weight'] = q * product_obj.weight # Round the quantity up
1427 uom2 = product_obj.uom_id
1428 compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
1429 if (product_obj.type=='product') and int(compare_qty) == -1 \
1430 and (product_obj.procure_method=='make_to_stock'):
1431 warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
1432 (qty, uom2 and uom2.name or product_obj.uom_id.name,
1433 max(0,product_obj.virtual_available), product_obj.uom_id.name,
1434 max(0,product_obj.qty_available), product_obj.uom_id.name)
1435 warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
1439 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1440 'Please set one before choosing a product.')
1441 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1443 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1444 product, qty or 1.0, partner_id, {
1445 'uom': uom or result.get('product_uom'),
1449 warn_msg = _("Couldn't find a pricelist line matching this product and quantity.\n"
1450 "You have to change either the product, the quantity or the pricelist.")
1452 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1454 result.update({'price_unit': price})
1457 'title': _('Configuration Error !'),
1458 'message' : warning_msgs
1460 return {'value': result, 'domain': domain, 'warning': warning}
1462 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1463 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1464 lang=False, update_tax=True, date_order=False, context=None):
1465 context = context or {}
1466 lang = lang or ('lang' in context and context['lang'])
1467 res = self.product_id_change(cursor, user, ids, pricelist, product,
1468 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1469 partner_id=partner_id, lang=lang, update_tax=update_tax,
1470 date_order=date_order, context=context)
1471 if 'product_uom' in res['value']:
1472 del res['value']['product_uom']
1474 res['value']['price_unit'] = 0.0
1477 def unlink(self, cr, uid, ids, context=None):
1480 """Allows to delete sales order lines in draft,cancel states"""
1481 for rec in self.browse(cr, uid, ids, context=context):
1482 if rec.state not in ['draft', 'cancel']:
1483 raise osv.except_osv(_('Invalid action !'), _('Cannot delete a sales order line which is in state \'%s\'!') %(rec.state,))
1484 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1488 class mail_message(osv.osv):
1489 _inherit = 'mail.message'
1491 def _postprocess_sent_message(self, cr, uid, message, context=None):
1492 if message.model == 'sale.order':
1493 wf_service = netsvc.LocalService("workflow")
1494 wf_service.trg_validate(uid, 'sale.order', message.res_id, 'quotation_sent', cr)
1495 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1499 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: