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
24 from openerp.osv import fields, osv
25 from openerp.tools.translate import _
26 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
27 import openerp.addons.decimal_precision as dp
28 from openerp import workflow
30 class sale_order(osv.osv):
32 _inherit = ['mail.thread', 'ir.needaction_mixin']
33 _description = "Sales Order"
36 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
37 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
41 def _amount_line_tax(self, cr, uid, line, context=None):
43 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']:
44 val += c.get('amount', 0.0)
47 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
48 """ Wrapper because of direct method passing as parameter for function fields """
49 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
51 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
52 cur_obj = self.pool.get('res.currency')
54 for order in self.browse(cr, uid, ids, context=context):
56 'amount_untaxed': 0.0,
61 cur = order.pricelist_id.currency_id
62 for line in order.order_line:
63 val1 += line.price_subtotal
64 val += self._amount_line_tax(cr, uid, line, context=context)
65 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
66 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
67 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
71 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
73 for sale in self.browse(cursor, user, ids, context=context):
78 for invoice in sale.invoice_ids:
79 if invoice.state not in ('draft', 'cancel'):
80 tot += invoice.amount_untaxed
82 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
87 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
89 for sale in self.browse(cursor, user, ids, context=context):
95 def _invoiced(self, cursor, user, ids, name, arg, context=None):
97 for sale in self.browse(cursor, user, ids, context=context):
99 invoice_existence = False
100 for invoice in sale.invoice_ids:
101 if invoice.state!='cancel':
102 invoice_existence = True
103 if invoice.state != 'paid':
106 if not invoice_existence or sale.state == 'manual':
110 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
117 if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
118 clause += 'AND inv.state = \'paid\''
120 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
121 sale_clause = ', sale_order AS sale '
124 cursor.execute('SELECT rel.order_id ' \
125 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
126 'WHERE rel.invoice_id = inv.id ' + clause)
127 res = cursor.fetchall()
129 cursor.execute('SELECT sale.id ' \
130 'FROM sale_order AS sale ' \
131 'WHERE sale.id NOT IN ' \
132 '(SELECT rel.order_id ' \
133 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
134 res.extend(cursor.fetchall())
136 return [('id', '=', 0)]
137 return [('id', 'in', [x[0] for x in res])]
139 def _get_order(self, cr, uid, ids, context=None):
141 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
142 result[line.order_id.id] = True
145 def _get_default_company(self, cr, uid, context=None):
146 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
148 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
151 def _get_default_section_id(self, cr, uid, context=None):
152 """ Gives default section by checking if present in the context """
153 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
155 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
158 def _resolve_section_id_from_context(self, cr, uid, context=None):
159 """ Returns ID of section based on the value of 'section_id'
160 context key, or None if it cannot be resolved to a single
165 if type(context.get('default_section_id')) in (int, long):
166 return context.get('default_section_id')
167 if isinstance(context.get('default_section_id'), basestring):
168 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
169 if len(section_ids) == 1:
170 return int(section_ids[0][0])
174 'name': fields.char('Order Reference', required=True, copy=False,
175 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
176 'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
177 'client_order_ref': fields.char('Reference/Description', copy=False),
178 'state': fields.selection([
179 ('draft', 'Draft Quotation'),
180 ('sent', 'Quotation Sent'),
181 ('cancel', 'Cancelled'),
182 ('waiting_date', 'Waiting Schedule'),
183 ('progress', 'Sales Order'),
184 ('manual', 'Sale to Invoice'),
185 ('shipping_except', 'Shipping Exception'),
186 ('invoice_except', 'Invoice Exception'),
188 ], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\
189 \nThe exception status is automatically set when a cancel operation occurs \
190 in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
191 but waiting for the scheduler to run on the order date.", select=True),
192 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False),
193 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
194 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False),
195 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
196 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
197 '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."),
198 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
199 'order_policy': fields.selection([
200 ('manual', 'On Demand'),
201 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
202 help="""This field controls how invoice and delivery operations are synchronized."""),
203 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
204 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
205 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
207 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
208 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, copy=False, 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)."),
209 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
210 'invoiced': fields.function(_invoiced, string='Paid',
211 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
212 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
213 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
214 'note': fields.text('Terms and conditions'),
216 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
218 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
219 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
221 multi='sums', help="The amount without tax.", track_visibility='always'),
222 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
224 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
225 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
227 multi='sums', help="The tax amount."),
228 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
230 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
231 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
233 multi='sums', help="The total amount."),
235 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
236 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
237 'company_id': fields.many2one('res.company', 'Company'),
238 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
239 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
243 'date_order': fields.datetime.now,
244 'order_policy': 'manual',
245 'company_id': _get_default_company,
247 'user_id': lambda obj, cr, uid, context: uid,
248 'name': lambda obj, cr, uid, context: '/',
249 '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'],
250 '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'],
251 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
252 'section_id': lambda s, cr, uid, c: s._get_default_section_id(cr, uid, c),
255 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
257 _order = 'date_order desc, id desc'
260 def unlink(self, cr, uid, ids, context=None):
261 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
263 for s in sale_orders:
264 if s['state'] in ['draft', 'cancel']:
265 unlink_ids.append(s['id'])
267 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
269 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
271 def copy_quotation(self, cr, uid, ids, context=None):
272 id = self.copy(cr, uid, ids[0], context=context)
273 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
274 view_id = view_ref and view_ref[1] or False,
276 'type': 'ir.actions.act_window',
277 'name': _('Sales Order'),
278 'res_model': 'sale.order',
287 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
288 context = context or {}
292 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
294 if not order_lines or order_lines == [(6, 0, [])]:
295 return {'value': value}
297 'title': _('Pricelist Warning!'),
298 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
300 return {'warning': warning, 'value': value}
302 def get_salenote(self, cr, uid, ids, partner_id, context=None):
303 context_lang = context.copy()
305 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
306 context_lang.update({'lang': partner_lang})
307 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
309 def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None):
311 if not fiscal_position:
313 company_id = self._get_default_company(cr, uid, context=context)
314 fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context)
316 r['value']['fiscal_position'] = fiscal_position
319 def onchange_partner_id(self, cr, uid, ids, part, context=None):
321 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
323 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
324 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
325 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
326 payment_term = part.property_payment_term and part.property_payment_term.id or False
327 dedicated_salesman = part.user_id and part.user_id.id or uid
329 'partner_invoice_id': addr['invoice'],
330 'partner_shipping_id': addr['delivery'],
331 'payment_term': payment_term,
332 'user_id': dedicated_salesman,
334 delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context)
335 val.update(delivery_onchange['value'])
337 val['pricelist_id'] = pricelist
338 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
339 if sale_note: val.update({'note': sale_note})
340 return {'value': val}
342 def create(self, cr, uid, vals, context=None):
345 if vals.get('name', '/') == '/':
346 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
347 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position']):
348 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
349 if not vals.get('fiscal_position') and vals.get('partner_shipping_id'):
350 delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
351 defaults.update(delivery_onchange['value'])
352 vals = dict(defaults, **vals)
353 ctx = dict(context or {}, mail_create_nolog=True)
354 new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
355 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
358 def button_dummy(self, cr, uid, ids, context=None):
361 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
362 # can be removed after 6.1.
363 def _inv_get(self, cr, uid, order, context=None):
366 def _prepare_invoice(self, cr, uid, order, lines, context=None):
367 """Prepare the dict of values to create the new invoice for a
368 sales order. This method may be overridden to implement custom
369 invoice generation (making sure to call super() to establish
370 a clean extension chain).
372 :param browse_record order: sale.order record to invoice
373 :param list(int) line: list of invoice line IDs that must be
374 attached to the invoice
375 :return: dict of value to create() the invoice
379 journal_ids = self.pool.get('account.journal').search(cr, uid,
380 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
383 raise osv.except_osv(_('Error!'),
384 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
386 'name': order.client_order_ref or '',
387 'origin': order.name,
388 'type': 'out_invoice',
389 'reference': order.client_order_ref or order.name,
390 'account_id': order.partner_id.property_account_receivable.id,
391 'partner_id': order.partner_invoice_id.id,
392 'journal_id': journal_ids[0],
393 'invoice_line': [(6, 0, lines)],
394 'currency_id': order.pricelist_id.currency_id.id,
395 'comment': order.note,
396 'payment_term': order.payment_term and order.payment_term.id or False,
397 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
398 'date_invoice': context.get('date_invoice', False),
399 'company_id': order.company_id.id,
400 'user_id': order.user_id and order.user_id.id or False,
401 'section_id' : order.section_id.id
404 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
405 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
408 def _make_invoice(self, cr, uid, order, lines, context=None):
409 inv_obj = self.pool.get('account.invoice')
410 obj_invoice_line = self.pool.get('account.invoice.line')
413 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
414 from_line_invoice_ids = []
415 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
416 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
417 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
418 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
419 for preinv in order.invoice_ids:
420 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
421 for preline in preinv.invoice_line:
422 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
423 lines.append(inv_line_id)
424 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
425 inv_id = inv_obj.create(cr, uid, inv, context=context)
426 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
427 if data.get('value', False):
428 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
429 inv_obj.button_compute(cr, uid, [inv_id])
432 def print_quotation(self, cr, uid, ids, context=None):
434 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
436 assert len(ids) == 1, 'This option should only be used for a single id at a time'
437 self.signal_workflow(cr, uid, ids, 'quotation_sent')
438 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
440 def manual_invoice(self, cr, uid, ids, context=None):
441 """ create invoices for the given sales orders (ids), and open the form
442 view of one of the newly created invoices
444 mod_obj = self.pool.get('ir.model.data')
446 # create invoices through the sales orders' workflow
447 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
448 self.signal_workflow(cr, uid, ids, 'manual_invoice')
449 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
450 # determine newly created invoices
451 new_inv_ids = list(inv_ids1 - inv_ids0)
453 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
454 res_id = res and res[1] or False,
457 'name': _('Customer Invoices'),
461 'res_model': 'account.invoice',
462 'context': "{'type':'out_invoice'}",
463 'type': 'ir.actions.act_window',
466 'res_id': new_inv_ids and new_inv_ids[0] or False,
469 def action_view_invoice(self, cr, uid, ids, context=None):
471 This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
473 mod_obj = self.pool.get('ir.model.data')
474 act_obj = self.pool.get('ir.actions.act_window')
476 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
477 id = result and result[1] or False
478 result = act_obj.read(cr, uid, [id], context=context)[0]
479 #compute the number of invoices to display
481 for so in self.browse(cr, uid, ids, context=context):
482 inv_ids += [invoice.id for invoice in so.invoice_ids]
483 #choose the view_mode accordingly
485 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
487 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
488 result['views'] = [(res and res[1] or False, 'form')]
489 result['res_id'] = inv_ids and inv_ids[0] or False
492 def test_no_product(self, cr, uid, order, context):
493 for line in order.order_line:
494 if line.product_id and (line.product_id.type<>'service'):
498 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
500 states = ['confirmed', 'done', 'exception']
504 invoice = self.pool.get('account.invoice')
505 obj_sale_order_line = self.pool.get('sale.order.line')
506 partner_currency = {}
507 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
508 # last day of the last month as invoice date
510 context = dict(context or {}, date_invoice=date_invoice)
511 for o in self.browse(cr, uid, ids, context=context):
512 currency_id = o.pricelist_id.currency_id.id
513 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
514 raise osv.except_osv(
516 _('You cannot group sales having different currencies for the same partner.'))
518 partner_currency[o.partner_id.id] = currency_id
520 for line in o.order_line:
523 elif (line.state in states):
524 lines.append(line.id)
525 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
527 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
529 for o in self.browse(cr, uid, ids, context=context):
530 for i in o.invoice_ids:
531 if i.state == 'draft':
533 for val in invoices.values():
535 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
539 invoice_ref += (o.client_order_ref or o.name) + '|'
540 origin_ref += (o.origin or o.name) + '|'
541 self.write(cr, uid, [o.id], {'state': 'progress'})
542 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
543 self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
544 #remove last '|' in invoice_ref
545 if len(invoice_ref) >= 1:
546 invoice_ref = invoice_ref[:-1]
547 if len(origin_ref) >= 1:
548 origin_ref = origin_ref[:-1]
549 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
551 for order, il in val:
552 res = self._make_invoice(cr, uid, order, il, context=context)
553 invoice_ids.append(res)
554 self.write(cr, uid, [order.id], {'state': 'progress'})
555 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
556 self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
559 def action_invoice_cancel(self, cr, uid, ids, context=None):
560 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
563 def action_invoice_end(self, cr, uid, ids, context=None):
564 for this in self.browse(cr, uid, ids, context=context):
565 for line in this.order_line:
566 if line.state == 'exception':
567 line.write({'state': 'confirmed'})
568 if this.state == 'invoice_except':
569 this.write({'state': 'progress'})
572 def action_cancel(self, cr, uid, ids, context=None):
575 sale_order_line_obj = self.pool.get('sale.order.line')
576 account_invoice_obj = self.pool.get('account.invoice')
577 for sale in self.browse(cr, uid, ids, context=context):
578 for inv in sale.invoice_ids:
579 if inv.state not in ('draft', 'cancel'):
580 raise osv.except_osv(
581 _('Cannot cancel this sales order!'),
582 _('First cancel all invoices attached to this sales order.'))
583 inv.signal_workflow('invoice_cancel')
584 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
586 self.write(cr, uid, ids, {'state': 'cancel'})
589 def action_button_confirm(self, cr, uid, ids, context=None):
590 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
591 self.signal_workflow(cr, uid, ids, 'order_confirm')
593 # redisplay the record as a sales order
594 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
595 view_id = view_ref and view_ref[1] or False,
597 'type': 'ir.actions.act_window',
598 'name': _('Sales Order'),
599 'res_model': 'sale.order',
608 def action_wait(self, cr, uid, ids, context=None):
609 context = context or {}
610 for o in self.browse(cr, uid, ids):
612 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
613 noprod = self.test_no_product(cr, uid, o, context)
614 if (o.order_policy == 'manual') or noprod:
615 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
617 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
618 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
621 def action_quotation_send(self, cr, uid, ids, context=None):
623 This function opens a window to compose an email, with the edi sale template message loaded by default
625 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
626 ir_model_data = self.pool.get('ir.model.data')
628 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
632 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
634 compose_form_id = False
637 'default_model': 'sale.order',
638 'default_res_id': ids[0],
639 'default_use_template': bool(template_id),
640 'default_template_id': template_id,
641 'default_composition_mode': 'comment',
642 'mark_so_as_sent': True
645 'type': 'ir.actions.act_window',
648 'res_model': 'mail.compose.message',
649 'views': [(compose_form_id, 'form')],
650 'view_id': compose_form_id,
655 def action_done(self, cr, uid, ids, context=None):
656 for order in self.browse(cr, uid, ids, context=context):
657 self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
658 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
660 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
661 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
664 'origin': order.name,
665 'date_planned': date_planned,
666 'product_id': line.product_id.id,
667 'product_qty': line.product_uom_qty,
668 'product_uom': line.product_uom.id,
669 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
670 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
671 'company_id': order.company_id.id,
672 'group_id': group_id,
673 'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
674 'sale_line_id': line.id
677 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
678 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
681 def _prepare_procurement_group(self, cr, uid, order, context=None):
682 return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
684 def procurement_needed(self, cr, uid, ids, context=None):
685 #when sale is installed only, there is no need to create procurements, that's only
686 #further installed modules (sale_service, sale_stock) that will change this.
687 sale_line_obj = self.pool.get('sale.order.line')
689 for order in self.browse(cr, uid, ids, context=context):
690 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
693 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
694 for sale_order in self.browse(cr, uid, ids, context=context):
695 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
698 def action_ship_create(self, cr, uid, ids, context=None):
699 """Create the required procurements to supply sales order lines, also connecting
700 the procurements to appropriate stock moves in order to bring the goods to the
701 sales order's requested location.
705 procurement_obj = self.pool.get('procurement.order')
706 sale_line_obj = self.pool.get('sale.order.line')
707 for order in self.browse(cr, uid, ids, context=context):
709 vals = self._prepare_procurement_group(cr, uid, order, context=context)
710 if not order.procurement_group_id:
711 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
712 order.write({'procurement_group_id': group_id}, context=context)
714 for line in order.order_line:
715 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
716 if line.procurement_ids:
717 #first check them to see if they are in exception or not (one of the related moves is cancelled)
718 procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
720 #run again procurement that are in exception in order to trigger another move
721 proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
722 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
723 if (line.state == 'done') or not line.product_id:
725 vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
726 proc_id = procurement_obj.create(cr, uid, vals, context=context)
727 proc_ids.append(proc_id)
728 #Confirm procurement order such that rules will be applied on it
729 #note that the workflow normally ensure proc_ids isn't an empty list
730 procurement_obj.run(cr, uid, proc_ids, context=context)
732 #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
733 if order.state == 'shipping_except':
734 val = {'state': 'progress', 'shipped': False}
736 if (order.order_policy == 'manual'):
737 for line in order.order_line:
738 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
739 val['state'] = 'manual'
746 def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
747 '''Update taxes of order lines for each line where a product is defined
749 :param list ids: not used
750 :param int fiscal_position: sale order fiscal position
751 :param list order_lines: command list for one2many write method
754 fiscal_obj = self.pool.get('account.fiscal.position')
755 product_obj = self.pool.get('product.product')
756 line_obj = self.pool.get('sale.order.line')
760 fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
762 for line in order_lines:
763 # create (0, 0, { fields })
764 # update (1, ID, { fields })
765 if line[0] in [0, 1]:
767 if line[2].get('product_id'):
768 prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
770 prod = line_obj.browse(cr, uid, line[1], context=context).product_id
771 if prod and prod.taxes_id:
772 line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
773 order_line.append(line)
776 # link all (6, 0, IDS)
777 elif line[0] in [4, 6]:
778 line_ids = line[0] == 4 and [line[1]] or line[2]
779 for line_id in line_ids:
780 prod = line_obj.browse(cr, uid, line_id, context=context).product_id
781 if prod and prod.taxes_id:
782 order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
784 order_line.append([4, line_id])
786 order_line.append(line)
787 return {'value': {'order_line': order_line}}
789 def test_procurements_done(self, cr, uid, ids, context=None):
790 for sale in self.browse(cr, uid, ids, context=context):
791 for line in sale.order_line:
792 if not all([x.state == 'done' for x in line.procurement_ids]):
796 def test_procurements_except(self, cr, uid, ids, context=None):
797 for sale in self.browse(cr, uid, ids, context=context):
798 for line in sale.order_line:
799 if any([x.state == 'cancel' for x in line.procurement_ids]):
804 # TODO add a field price_unit_uos
805 # - update it on change product and unit price
806 # - use it in report if there is a uos
807 class sale_order_line(osv.osv):
809 def need_procurement(self, cr, uid, ids, context=None):
810 #when sale is installed only, there is no need to create procurements, that's only
811 #further installed modules (sale_service, sale_stock) that will change this.
814 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
815 tax_obj = self.pool.get('account.tax')
816 cur_obj = self.pool.get('res.currency')
820 for line in self.browse(cr, uid, ids, context=context):
821 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
822 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
823 cur = line.order_id.pricelist_id.currency_id
824 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
827 def _get_uom_id(self, cr, uid, *args):
829 proxy = self.pool.get('ir.model.data')
830 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
832 except Exception, ex:
835 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
836 res = dict.fromkeys(ids, False)
837 for this in self.browse(cr, uid, ids, context=context):
838 res[this.id] = this.invoice_lines and \
839 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
842 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
843 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
844 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
845 sale_order_line sol ON (sol.order_id = rel.order_id)
846 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
847 return [i[0] for i in cr.fetchall()]
850 _name = 'sale.order.line'
851 _description = 'Sales Order Line'
853 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
854 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
855 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
856 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
857 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
858 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
860 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
861 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
863 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
864 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
865 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
866 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
867 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
868 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
869 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
870 'product_uos': fields.many2one('product.uom', 'Product UoS'),
871 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
872 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
873 'state': fields.selection(
874 [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')],
875 'Status', required=True, readonly=True, copy=False,
876 help='* The \'Draft\' status is set when the related sales order in draft status. \
877 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
878 \n* The \'Exception\' status is set when the related sales order is set as exception. \
879 \n* The \'Done\' status is set when the sales order line has been picked. \
880 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
881 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
882 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
883 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
884 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
885 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
887 _order = 'order_id desc, sequence, id'
889 'product_uom' : _get_uom_id,
891 'product_uom_qty': 1,
892 'product_uos_qty': 1,
901 def _get_line_qty(self, cr, uid, line, context=None):
903 return line.product_uos_qty or 0.0
904 return line.product_uom_qty
906 def _get_line_uom(self, cr, uid, line, context=None):
908 return line.product_uos.id
909 return line.product_uom.id
911 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
912 """Prepare the dict of values to create the new invoice line for a
913 sales order line. This method may be overridden to implement custom
914 invoice generation (making sure to call super() to establish
915 a clean extension chain).
917 :param browse_record line: sale.order.line record to invoice
918 :param int account_id: optional ID of a G/L account to force
919 (this is used for returning products including service)
920 :return: dict of values to create() the invoice line
923 if not line.invoiced:
926 account_id = line.product_id.property_account_income.id
928 account_id = line.product_id.categ_id.property_account_income_categ.id
930 raise osv.except_osv(_('Error!'),
931 _('Please define income account for this product: "%s" (id:%d).') % \
932 (line.product_id.name, line.product_id.id,))
934 prop = self.pool.get('ir.property').get(cr, uid,
935 'property_account_income_categ', 'product.category',
937 account_id = prop and prop.id or False
938 uosqty = self._get_line_qty(cr, uid, line, context=context)
939 uos_id = self._get_line_uom(cr, uid, line, context=context)
942 pu = round(line.price_unit * line.product_uom_qty / uosqty,
943 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
944 fpos = line.order_id.fiscal_position or False
945 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
947 raise osv.except_osv(_('Error!'),
948 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
951 'sequence': line.sequence,
952 'origin': line.order_id.name,
953 'account_id': account_id,
956 'discount': line.discount,
958 'product_id': line.product_id.id or False,
959 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
960 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
965 def invoice_line_create(self, cr, uid, ids, context=None):
971 for line in self.browse(cr, uid, ids, context=context):
972 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
974 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
975 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
976 sales.add(line.order_id.id)
977 create_ids.append(inv_id)
978 # Trigger workflow events
979 for sale_id in sales:
980 workflow.trg_write(uid, 'sale.order', sale_id, cr)
983 def button_cancel(self, cr, uid, ids, context=None):
984 for line in self.browse(cr, uid, ids, context=context):
986 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
987 return self.write(cr, uid, ids, {'state': 'cancel'})
989 def button_confirm(self, cr, uid, ids, context=None):
990 return self.write(cr, uid, ids, {'state': 'confirmed'})
992 def button_done(self, cr, uid, ids, context=None):
993 res = self.write(cr, uid, ids, {'state': 'done'})
994 for line in self.browse(cr, uid, ids, context=context):
995 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
998 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
999 product_obj = self.pool.get('product.product')
1001 return {'value': {'product_uom': product_uos,
1002 'product_uom_qty': product_uos_qty}, 'domain': {}}
1004 product = product_obj.browse(cr, uid, product_id)
1006 'product_uom': product.uom_id.id,
1008 # FIXME must depend on uos/uom of the product and not only of the coeff.
1011 'product_uom_qty': product_uos_qty / product.uos_coeff,
1012 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1014 except ZeroDivisionError:
1016 return {'value': value}
1018 def create(self, cr, uid, values, context=None):
1019 if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
1020 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1021 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1022 qty=float(values.get('product_uom_qty', False)),
1023 uom=values.get('product_uom', False),
1024 qty_uos=float(values.get('product_uos_qty', False)),
1025 uos=values.get('product_uos', False),
1026 name=values.get('name', False),
1027 partner_id=order['partner_id'][0],
1028 date_order=order['date_order'],
1029 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1030 flag=False, # Force name update
1033 values = dict(defaults, **values)
1034 return super(sale_order_line, self).create(cr, uid, values, context=context)
1036 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1037 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1038 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1039 context = context or {}
1040 lang = lang or context.get('lang', False)
1042 raise osv.except_osv(_('No Customer Defined!'), _('Before choosing a product,\n select a customer in the sales form.'))
1044 product_uom_obj = self.pool.get('product.uom')
1045 partner_obj = self.pool.get('res.partner')
1046 product_obj = self.pool.get('product.product')
1047 context = {'lang': lang, 'partner_id': partner_id}
1048 partner = partner_obj.browse(cr, uid, partner_id)
1050 context_partner = {'lang': lang, 'partner_id': partner_id}
1053 return {'value': {'th_weight': 0,
1054 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1057 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1061 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
1065 uom2 = product_uom_obj.browse(cr, uid, uom)
1066 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1069 if product_obj.uos_id:
1070 uos2 = product_uom_obj.browse(cr, uid, uos)
1071 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1077 if not fiscal_position:
1078 fpos = partner.property_account_position or False
1080 fpos = self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position)
1081 if update_tax: #The quantity only have changed
1082 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
1085 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
1086 if product_obj.description_sale:
1087 result['name'] += '\n'+product_obj.description_sale
1089 if (not uom) and (not uos):
1090 result['product_uom'] = product_obj.uom_id.id
1091 if product_obj.uos_id:
1092 result['product_uos'] = product_obj.uos_id.id
1093 result['product_uos_qty'] = qty * product_obj.uos_coeff
1094 uos_category_id = product_obj.uos_id.category_id.id
1096 result['product_uos'] = False
1097 result['product_uos_qty'] = qty
1098 uos_category_id = False
1099 result['th_weight'] = qty * product_obj.weight
1100 domain = {'product_uom':
1101 [('category_id', '=', product_obj.uom_id.category_id.id)],
1103 [('category_id', '=', uos_category_id)]}
1104 elif uos and not uom: # only happens if uom is False
1105 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1106 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1107 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1108 elif uom: # whether uos is set or not
1109 default_uom = product_obj.uom_id and product_obj.uom_id.id
1110 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
1111 if product_obj.uos_id:
1112 result['product_uos'] = product_obj.uos_id.id
1113 result['product_uos_qty'] = qty * product_obj.uos_coeff
1115 result['product_uos'] = False
1116 result['product_uos_qty'] = qty
1117 result['th_weight'] = q * product_obj.weight # Round the quantity up
1120 uom2 = product_obj.uom_id
1124 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
1125 'Please set one before choosing a product.')
1126 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
1128 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
1129 product, qty or 1.0, partner_id, {
1130 'uom': uom or result.get('product_uom'),
1134 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
1135 "You have to change either the product, the quantity or the pricelist.")
1137 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1139 result.update({'price_unit': price})
1142 'title': _('Configuration Error!'),
1143 'message' : warning_msgs
1145 return {'value': result, 'domain': domain, 'warning': warning}
1147 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1148 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1149 lang=False, update_tax=True, date_order=False, context=None):
1150 context = context or {}
1151 lang = lang or ('lang' in context and context['lang'])
1153 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1154 return self.product_id_change(cursor, user, ids, pricelist, product,
1155 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1156 partner_id=partner_id, lang=lang, update_tax=update_tax,
1157 date_order=date_order, context=context)
1159 def unlink(self, cr, uid, ids, context=None):
1162 """Allows to delete sales order lines in draft,cancel states"""
1163 for rec in self.browse(cr, uid, ids, context=context):
1164 if rec.state not in ['draft', 'cancel']:
1165 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1166 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1168 class res_company(osv.Model):
1169 _inherit = "res.company"
1171 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
1175 class mail_compose_message(osv.Model):
1176 _inherit = 'mail.compose.message'
1178 def send_mail(self, cr, uid, ids, context=None):
1179 context = context or {}
1180 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1181 context = dict(context, mail_post_autofollow=True)
1182 self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
1183 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1186 class account_invoice(osv.Model):
1187 _inherit = 'account.invoice'
1189 def _get_default_section_id(self, cr, uid, context=None):
1190 """ Gives default section by checking if present in the context """
1191 section_id = self._resolve_section_id_from_context(cr, uid, context=context) or False
1193 section_id = self.pool.get('res.users').browse(cr, uid, uid, context).default_section_id.id or False
1196 def _resolve_section_id_from_context(self, cr, uid, context=None):
1197 """ Returns ID of section based on the value of 'section_id'
1198 context key, or None if it cannot be resolved to a single
1203 if type(context.get('default_section_id')) in (int, long):
1204 return context.get('default_section_id')
1205 if isinstance(context.get('default_section_id'), basestring):
1206 section_ids = self.pool.get('crm.case.section').name_search(cr, uid, name=context['default_section_id'], context=context)
1207 if len(section_ids) == 1:
1208 return int(section_ids[0][0])
1212 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
1216 'section_id': lambda self, cr, uid, c=None: self._get_default_section_id(cr, uid, context=c)
1219 def confirm_paid(self, cr, uid, ids, context=None):
1220 sale_order_obj = self.pool.get('sale.order')
1221 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1222 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1223 for so_id in so_ids:
1224 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1227 def unlink(self, cr, uid, ids, context=None):
1228 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1229 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1230 #if we can't cancel all invoices, do nothing
1231 if len(invoice_ids) == len(ids):
1232 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1233 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1235 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1236 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1239 class procurement_order(osv.osv):
1240 _inherit = 'procurement.order'
1242 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1245 def write(self, cr, uid, ids, vals, context=None):
1246 if isinstance(ids, (int, long)):
1248 res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
1249 from openerp import workflow
1250 if vals.get('state') in ['done', 'cancel', 'exception']:
1251 for proc in self.browse(cr, uid, ids, context=context):
1252 if proc.sale_line_id and proc.sale_line_id.order_id and proc.move_ids:
1253 order_id = proc.sale_line_id.order_id.id
1254 if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
1255 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
1256 if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
1257 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
1260 class product_product(osv.Model):
1261 _inherit = 'product.product'
1263 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1264 SaleOrderLine = self.pool['sale.order.line']
1266 product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1267 for product_id in ids
1271 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1275 class product_template(osv.Model):
1276 _inherit = 'product.template'
1278 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1279 res = dict.fromkeys(ids, 0)
1280 for template in self.browse(cr, uid, ids, context=context):
1281 res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
1284 def action_view_sales(self, cr, uid, ids, context=None):
1285 act_obj = self.pool.get('ir.actions.act_window')
1286 mod_obj = self.pool.get('ir.model.data')
1288 for template in self.browse(cr, uid, ids, context=context):
1289 product_ids += [x.id for x in template.product_variant_ids]
1290 result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
1291 result = act_obj.read(cr, uid, [result], context=context)[0]
1292 result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
1297 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1301 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: