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 res_company(osv.Model):
31 _inherit = "res.company"
33 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
36 class sale_order(osv.osv):
38 _inherit = ['mail.thread', 'ir.needaction_mixin']
39 _description = "Sales Order"
42 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'],
43 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent']
47 def _amount_line_tax(self, cr, uid, line, context=None):
49 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']:
50 val += c.get('amount', 0.0)
53 def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
54 """ Wrapper because of direct method passing as parameter for function fields """
55 return self._amount_all(cr, uid, ids, field_name, arg, context=context)
57 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
58 cur_obj = self.pool.get('res.currency')
60 for order in self.browse(cr, uid, ids, context=context):
62 'amount_untaxed': 0.0,
67 cur = order.pricelist_id.currency_id
68 for line in order.order_line:
69 val1 += line.price_subtotal
70 val += self._amount_line_tax(cr, uid, line, context=context)
71 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
72 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
73 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
77 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
79 for sale in self.browse(cursor, user, ids, context=context):
84 for invoice in sale.invoice_ids:
85 if invoice.state not in ('draft', 'cancel'):
86 tot += invoice.amount_untaxed
88 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
93 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
95 for sale in self.browse(cursor, user, ids, context=context):
101 def _invoiced(self, cursor, user, ids, name, arg, context=None):
103 for sale in self.browse(cursor, user, ids, context=context):
105 invoice_existence = False
106 for invoice in sale.invoice_ids:
107 if invoice.state!='cancel':
108 invoice_existence = True
109 if invoice.state != 'paid':
112 if not invoice_existence or sale.state == 'manual':
116 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
123 if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
124 clause += 'AND inv.state = \'paid\''
126 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
127 sale_clause = ', sale_order AS sale '
130 cursor.execute('SELECT rel.order_id ' \
131 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
132 'WHERE rel.invoice_id = inv.id ' + clause)
133 res = cursor.fetchall()
135 cursor.execute('SELECT sale.id ' \
136 'FROM sale_order AS sale ' \
137 'WHERE sale.id NOT IN ' \
138 '(SELECT rel.order_id ' \
139 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
140 res.extend(cursor.fetchall())
142 return [('id', '=', 0)]
143 return [('id', 'in', [x[0] for x in res])]
145 def _get_order(self, cr, uid, ids, context=None):
147 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
148 result[line.order_id.id] = True
151 def _get_default_company(self, cr, uid, context=None):
152 company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
154 raise osv.except_osv(_('Error!'), _('There is no default company for the current user!'))
157 def _get_default_team_id(self, cr, uid, context=None):
158 """ Gives default team by checking if present in the context """
159 team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False
162 def _resolve_team_id_from_context(self, cr, uid, context=None):
163 """ Returns ID of team based on the value of 'team_id'
164 context key, or None if it cannot be resolved to a single
169 if type(context.get('default_team_id')) in (int, long):
170 return context.get('default_team_id')
171 if isinstance(context.get('default_team_id'), basestring):
172 team_ids = self.pool.get('crm.team').name_search(cr, uid, name=context['default_team_id'], context=context)
173 if len(team_ids) == 1:
174 return int(team_ids[0][0])
178 'name': fields.char('Order Reference', required=True, copy=False,
179 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
180 'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
181 'client_order_ref': fields.char('Customer Reference', copy=False),
182 'state': fields.selection([
183 ('draft', 'Draft Quotation'),
184 ('sent', 'Quotation Sent'),
185 ('cancel', 'Cancelled'),
186 ('waiting_date', 'Waiting Schedule'),
187 ('progress', 'Sales Order'),
188 ('manual', 'Sale to Invoice'),
189 ('shipping_except', 'Shipping Exception'),
190 ('invoice_except', 'Invoice Exception'),
192 ], 'Status', readonly=True, copy=False, help="Gives the status of the quotation or sales order.\
193 \nThe exception status is automatically set when a cancel operation occurs \
194 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\
195 but waiting for the scheduler to run on the order date.", select=True),
196 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False),
197 'validity_date': fields.date('Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
198 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
199 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False),
200 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
201 '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'),
202 '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."),
203 '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."),
204 'order_policy': fields.selection([
205 ('manual', 'On Demand'),
206 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
207 help="""This field controls how invoice and delivery operations are synchronized."""),
208 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
209 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
210 '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."),
212 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
213 '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)."),
214 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced Ratio', type='float'),
215 'invoiced': fields.function(_invoiced, string='Paid',
216 fnct_search=_invoiced_search, type='boolean', help="It indicates that an invoice has been paid."),
217 'invoice_exists': fields.function(_invoice_exists, string='Invoiced',
218 fnct_search=_invoiced_search, type='boolean', help="It indicates that sales order has at least one invoice."),
219 'note': fields.text('Terms and conditions'),
221 'amount_untaxed': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Untaxed Amount',
223 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
224 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
226 multi='sums', help="The amount without tax.", track_visibility='always'),
227 'amount_tax': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Taxes',
229 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
230 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
232 multi='sums', help="The tax amount."),
233 'amount_total': fields.function(_amount_all_wrapper, digits_compute=dp.get_precision('Account'), string='Total',
235 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
236 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
238 multi='sums', help="The total amount."),
240 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
241 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
242 'company_id': fields.many2one('res.company', 'Company'),
243 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id', change_default=True),
244 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
245 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
249 'date_order': fields.datetime.now,
250 'order_policy': 'manual',
251 'company_id': _get_default_company,
253 'user_id': lambda obj, cr, uid, context: uid,
254 'name': lambda obj, cr, uid, context: '/',
255 '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'],
256 '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'],
257 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
258 'team_id': lambda s, cr, uid, c: s._get_default_team_id(cr, uid, c),
261 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
263 _order = 'date_order desc, id desc'
266 def unlink(self, cr, uid, ids, context=None):
267 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
269 for s in sale_orders:
270 if s['state'] in ['draft', 'cancel']:
271 unlink_ids.append(s['id'])
273 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it before!'))
275 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
277 def copy_quotation(self, cr, uid, ids, context=None):
278 id = self.copy(cr, uid, ids[0], context=context)
279 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
280 view_id = view_ref and view_ref[1] or False,
282 'type': 'ir.actions.act_window',
283 'name': _('Sales Order'),
284 'res_model': 'sale.order',
293 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
294 context = context or {}
298 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
300 if not order_lines or order_lines == [(6, 0, [])]:
301 return {'value': value}
303 'title': _('Pricelist Warning!'),
304 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
306 return {'warning': warning, 'value': value}
308 def get_salenote(self, cr, uid, ids, partner_id, context=None):
309 context_lang = context.copy()
311 partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
312 context_lang.update({'lang': partner_lang})
313 return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
315 def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None):
317 if not fiscal_position:
319 company_id = self._get_default_company(cr, uid, context=context)
320 fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context)
322 r['value']['fiscal_position'] = fiscal_position
325 def onchange_partner_id(self, cr, uid, ids, part, context=None):
327 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
329 part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
330 addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'])
331 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
332 payment_term = part.property_payment_term and part.property_payment_term.id or False
333 dedicated_salesman = part.user_id and part.user_id.id or uid
335 'partner_invoice_id': addr['invoice'],
336 'partner_shipping_id': addr['delivery'],
337 'payment_term': payment_term,
338 'user_id': dedicated_salesman,
340 delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context)
341 val.update(delivery_onchange['value'])
343 val['pricelist_id'] = pricelist
344 sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
345 if sale_note: val.update({'note': sale_note})
346 return {'value': val}
348 def create(self, cr, uid, vals, context=None):
351 if vals.get('name', '/') == '/':
352 vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/'
353 if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position']):
354 defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
355 if not vals.get('fiscal_position') and vals.get('partner_shipping_id'):
356 delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
357 defaults.update(delivery_onchange['value'])
358 vals = dict(defaults, **vals)
359 ctx = dict(context or {}, mail_create_nolog=True)
360 new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
361 self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
364 def button_dummy(self, cr, uid, ids, context=None):
367 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
368 # can be removed after 6.1.
369 def _inv_get(self, cr, uid, order, context=None):
372 def _prepare_invoice(self, cr, uid, order, lines, context=None):
373 """Prepare the dict of values to create the new invoice for a
374 sales order. This method may be overridden to implement custom
375 invoice generation (making sure to call super() to establish
376 a clean extension chain).
378 :param browse_record order: sale.order record to invoice
379 :param list(int) line: list of invoice line IDs that must be
380 attached to the invoice
381 :return: dict of value to create() the invoice
385 journal_ids = self.pool.get('account.journal').search(cr, uid,
386 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
389 raise osv.except_osv(_('Error!'),
390 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
392 'name': order.client_order_ref or '',
393 'origin': order.name,
394 'type': 'out_invoice',
395 'reference': order.client_order_ref or order.name,
396 'account_id': order.partner_id.property_account_receivable.id,
397 'partner_id': order.partner_invoice_id.id,
398 'journal_id': journal_ids[0],
399 'invoice_line': [(6, 0, lines)],
400 'currency_id': order.pricelist_id.currency_id.id,
401 'comment': order.note,
402 'payment_term': order.payment_term and order.payment_term.id or False,
403 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
404 'date_invoice': context.get('date_invoice', False),
405 'company_id': order.company_id.id,
406 'user_id': order.user_id and order.user_id.id or False,
407 'team_id' : order.team_id.id
410 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
411 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
414 def _make_invoice(self, cr, uid, order, lines, context=None):
415 inv_obj = self.pool.get('account.invoice')
416 obj_invoice_line = self.pool.get('account.invoice.line')
419 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
420 from_line_invoice_ids = []
421 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
422 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
423 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
424 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
425 for preinv in order.invoice_ids:
426 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
427 for preline in preinv.invoice_line:
428 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
429 lines.append(inv_line_id)
430 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
431 inv_id = inv_obj.create(cr, uid, inv, context=context)
432 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
433 if data.get('value', False):
434 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
435 inv_obj.button_compute(cr, uid, [inv_id])
438 def print_quotation(self, cr, uid, ids, context=None):
440 This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
442 assert len(ids) == 1, 'This option should only be used for a single id at a time'
443 self.signal_workflow(cr, uid, ids, 'quotation_sent')
444 return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
446 def manual_invoice(self, cr, uid, ids, context=None):
447 """ create invoices for the given sales orders (ids), and open the form
448 view of one of the newly created invoices
450 mod_obj = self.pool.get('ir.model.data')
452 # create invoices through the sales orders' workflow
453 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
454 self.signal_workflow(cr, uid, ids, 'manual_invoice')
455 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
456 # determine newly created invoices
457 new_inv_ids = list(inv_ids1 - inv_ids0)
459 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
460 res_id = res and res[1] or False,
463 'name': _('Customer Invoices'),
467 'res_model': 'account.invoice',
468 'context': "{'type':'out_invoice'}",
469 'type': 'ir.actions.act_window',
472 'res_id': new_inv_ids and new_inv_ids[0] or False,
475 def action_view_invoice(self, cr, uid, ids, context=None):
477 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.
479 mod_obj = self.pool.get('ir.model.data')
480 act_obj = self.pool.get('ir.actions.act_window')
482 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
483 id = result and result[1] or False
484 result = act_obj.read(cr, uid, [id], context=context)[0]
485 #compute the number of invoices to display
487 for so in self.browse(cr, uid, ids, context=context):
488 inv_ids += [invoice.id for invoice in so.invoice_ids]
489 #choose the view_mode accordingly
491 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
493 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
494 result['views'] = [(res and res[1] or False, 'form')]
495 result['res_id'] = inv_ids and inv_ids[0] or False
498 def test_no_product(self, cr, uid, order, context):
499 for line in order.order_line:
500 if line.product_id and (line.product_id.type<>'service'):
504 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
506 states = ['confirmed', 'done', 'exception']
510 invoice = self.pool.get('account.invoice')
511 obj_sale_order_line = self.pool.get('sale.order.line')
512 partner_currency = {}
513 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
514 # last day of the last month as invoice date
516 context = dict(context or {}, date_invoice=date_invoice)
517 for o in self.browse(cr, uid, ids, context=context):
518 currency_id = o.pricelist_id.currency_id.id
519 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
520 raise osv.except_osv(
522 _('You cannot group sales having different currencies for the same partner.'))
524 partner_currency[o.partner_id.id] = currency_id
526 for line in o.order_line:
529 elif (line.state in states):
530 lines.append(line.id)
531 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
533 invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
535 for o in self.browse(cr, uid, ids, context=context):
536 for i in o.invoice_ids:
537 if i.state == 'draft':
539 for val in invoices.values():
541 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
545 invoice_ref += (o.client_order_ref or o.name) + '|'
546 origin_ref += (o.origin or o.name) + '|'
547 self.write(cr, uid, [o.id], {'state': 'progress'})
548 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
549 self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
550 #remove last '|' in invoice_ref
551 if len(invoice_ref) >= 1:
552 invoice_ref = invoice_ref[:-1]
553 if len(origin_ref) >= 1:
554 origin_ref = origin_ref[:-1]
555 invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
557 for order, il in val:
558 res = self._make_invoice(cr, uid, order, il, context=context)
559 invoice_ids.append(res)
560 self.write(cr, uid, [order.id], {'state': 'progress'})
561 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
562 self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
565 def action_invoice_cancel(self, cr, uid, ids, context=None):
566 self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
569 def action_invoice_end(self, cr, uid, ids, context=None):
570 for this in self.browse(cr, uid, ids, context=context):
571 for line in this.order_line:
572 if line.state == 'exception':
573 line.write({'state': 'confirmed'})
574 if this.state == 'invoice_except':
575 this.write({'state': 'progress'})
578 def action_cancel(self, cr, uid, ids, context=None):
581 sale_order_line_obj = self.pool.get('sale.order.line')
582 account_invoice_obj = self.pool.get('account.invoice')
583 for sale in self.browse(cr, uid, ids, context=context):
584 for inv in sale.invoice_ids:
585 if inv.state not in ('draft', 'cancel'):
586 raise osv.except_osv(
587 _('Cannot cancel this sales order!'),
588 _('First cancel all invoices attached to this sales order.'))
589 inv.signal_workflow('invoice_cancel')
590 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
592 self.write(cr, uid, ids, {'state': 'cancel'})
595 def action_button_confirm(self, cr, uid, ids, context=None):
596 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
597 self.signal_workflow(cr, uid, ids, 'order_confirm')
600 def action_wait(self, cr, uid, ids, context=None):
601 context = context or {}
602 for o in self.browse(cr, uid, ids):
604 raise osv.except_osv(_('Error!'),_('You cannot confirm a sales order which has no line.'))
605 noprod = self.test_no_product(cr, uid, o, context)
606 if (o.order_policy == 'manual') or noprod:
607 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
609 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
610 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
613 def action_quotation_send(self, cr, uid, ids, context=None):
615 This function opens a window to compose an email, with the edi sale template message loaded by default
617 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
618 ir_model_data = self.pool.get('ir.model.data')
620 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
624 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
626 compose_form_id = False
629 'default_model': 'sale.order',
630 'default_res_id': ids[0],
631 'default_use_template': bool(template_id),
632 'default_template_id': template_id,
633 'default_composition_mode': 'comment',
634 'mark_so_as_sent': True
637 'type': 'ir.actions.act_window',
640 'res_model': 'mail.compose.message',
641 'views': [(compose_form_id, 'form')],
642 'view_id': compose_form_id,
647 def action_done(self, cr, uid, ids, context=None):
648 for order in self.browse(cr, uid, ids, context=context):
649 self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context)
650 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
652 def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
653 date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
656 'origin': order.name,
657 'date_planned': date_planned,
658 'product_id': line.product_id.id,
659 'product_qty': line.product_uom_qty,
660 'product_uom': line.product_uom.id,
661 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
662 'product_uos': (line.product_uos and line.product_uos.id) or line.product_uom.id,
663 'company_id': order.company_id.id,
664 'group_id': group_id,
665 'invoice_state': (order.order_policy == 'picking') and '2binvoiced' or 'none',
666 'sale_line_id': line.id
669 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
670 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
673 def _prepare_procurement_group(self, cr, uid, order, context=None):
674 return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
676 def procurement_needed(self, cr, uid, ids, context=None):
677 #when sale is installed only, there is no need to create procurements, that's only
678 #further installed modules (sale_service, sale_stock) that will change this.
679 sale_line_obj = self.pool.get('sale.order.line')
681 for order in self.browse(cr, uid, ids, context=context):
682 res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context))
685 def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
686 for sale_order in self.browse(cr, uid, ids, context=context):
687 self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_exists else 'manual'}, context=context)
690 def action_ship_create(self, cr, uid, ids, context=None):
691 """Create the required procurements to supply sales order lines, also connecting
692 the procurements to appropriate stock moves in order to bring the goods to the
693 sales order's requested location.
697 procurement_obj = self.pool.get('procurement.order')
698 sale_line_obj = self.pool.get('sale.order.line')
699 for order in self.browse(cr, uid, ids, context=context):
701 vals = self._prepare_procurement_group(cr, uid, order, context=context)
702 if not order.procurement_group_id:
703 group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
704 order.write({'procurement_group_id': group_id})
706 for line in order.order_line:
707 #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
708 if line.procurement_ids:
709 #first check them to see if they are in exception or not (one of the related moves is cancelled)
710 procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
712 #run again procurement that are in exception in order to trigger another move
713 proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
714 procurement_obj.reset_to_confirmed(cr, uid, proc_ids, context=context)
715 elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
716 if (line.state == 'done') or not line.product_id:
718 vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context)
719 proc_id = procurement_obj.create(cr, uid, vals, context=context)
720 proc_ids.append(proc_id)
721 #Confirm procurement order such that rules will be applied on it
722 #note that the workflow normally ensure proc_ids isn't an empty list
723 procurement_obj.run(cr, uid, proc_ids, context=context)
725 #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
726 if order.state == 'shipping_except':
727 val = {'state': 'progress', 'shipped': False}
729 if (order.order_policy == 'manual'):
730 for line in order.order_line:
731 if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
732 val['state'] = 'manual'
739 def onchange_fiscal_position(self, cr, uid, ids, fiscal_position, order_lines, context=None):
740 '''Update taxes of order lines for each line where a product is defined
742 :param list ids: not used
743 :param int fiscal_position: sale order fiscal position
744 :param list order_lines: command list for one2many write method
747 fiscal_obj = self.pool.get('account.fiscal.position')
748 product_obj = self.pool.get('product.product')
749 line_obj = self.pool.get('sale.order.line')
753 fpos = fiscal_obj.browse(cr, uid, fiscal_position, context=context)
755 for line in order_lines:
756 # create (0, 0, { fields })
757 # update (1, ID, { fields })
758 if line[0] in [0, 1]:
760 if line[2].get('product_id'):
761 prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
763 prod = line_obj.browse(cr, uid, line[1], context=context).product_id
764 if prod and prod.taxes_id:
765 line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
766 order_line.append(line)
769 # link all (6, 0, IDS)
770 elif line[0] in [4, 6]:
771 line_ids = line[0] == 4 and [line[1]] or line[2]
772 for line_id in line_ids:
773 prod = line_obj.browse(cr, uid, line_id, context=context).product_id
774 if prod and prod.taxes_id:
775 order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
777 order_line.append([4, line_id])
779 order_line.append(line)
780 return {'value': {'order_line': order_line}}
782 def test_procurements_done(self, cr, uid, ids, context=None):
783 for sale in self.browse(cr, uid, ids, context=context):
784 for line in sale.order_line:
785 if not all([x.state == 'done' for x in line.procurement_ids]):
789 def test_procurements_except(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 any([x.state == 'cancel' for x in line.procurement_ids]):
797 # TODO add a field price_unit_uos
798 # - update it on change product and unit price
799 # - use it in report if there is a uos
800 class sale_order_line(osv.osv):
802 def need_procurement(self, cr, uid, ids, context=None):
803 #when sale is installed only, there is no need to create procurements, that's only
804 #further installed modules (sale_service, sale_stock) that will change this.
805 prod_obj = self.pool.get('product.product')
806 for line in self.browse(cr, uid, ids, context=context):
807 if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
811 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
812 tax_obj = self.pool.get('account.tax')
813 cur_obj = self.pool.get('res.currency')
817 for line in self.browse(cr, uid, ids, context=context):
818 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
819 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
820 cur = line.order_id.pricelist_id.currency_id
821 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
824 def _get_uom_id(self, cr, uid, *args):
826 proxy = self.pool.get('ir.model.data')
827 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
829 except Exception, ex:
832 def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
833 res = dict.fromkeys(ids, False)
834 for this in self.browse(cr, uid, ids, context=context):
835 res[this.id] = this.invoice_lines and \
836 all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
839 def _order_lines_from_invoice(self, cr, uid, ids, context=None):
840 # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
841 cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
842 sale_order_line sol ON (sol.order_id = rel.order_id)
843 WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
844 return [i[0] for i in cr.fetchall()]
846 def _get_price_reduce(self, cr, uid, ids, field_name, arg, context=None):
847 res = dict.fromkeys(ids, 0.0)
848 for line in self.browse(cr, uid, ids, context=context):
849 res[line.id] = line.price_subtotal / line.product_uom_qty
852 _name = 'sale.order.line'
853 _description = 'Sales Order Line'
855 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
856 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
857 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
858 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
859 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
860 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
862 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
863 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
865 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
866 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
867 'price_reduce': fields.function(_get_price_reduce, type='float', string='Price Reduce', digits_compute=dp.get_precision('Product Price')),
868 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
869 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
870 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
871 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
872 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
873 'product_uos': fields.many2one('product.uom', 'Product UoS'),
874 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
875 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
876 'state': fields.selection(
877 [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')],
878 'Status', required=True, readonly=True, copy=False,
879 help='* The \'Draft\' status is set when the related sales order in draft status. \
880 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
881 \n* The \'Exception\' status is set when the related sales order is set as exception. \
882 \n* The \'Done\' status is set when the sales order line has been picked. \
883 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
884 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
885 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
886 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
887 '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)]}),
888 'procurement_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
890 _order = 'order_id desc, sequence, id'
892 'product_uom' : _get_uom_id,
894 'product_uom_qty': 1,
895 'product_uos_qty': 1,
904 def _get_line_qty(self, cr, uid, line, context=None):
906 return line.product_uos_qty or 0.0
907 return line.product_uom_qty
909 def _get_line_uom(self, cr, uid, line, context=None):
911 return line.product_uos.id
912 return line.product_uom.id
914 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
915 """Prepare the dict of values to create the new invoice line for a
916 sales order line. This method may be overridden to implement custom
917 invoice generation (making sure to call super() to establish
918 a clean extension chain).
920 :param browse_record line: sale.order.line record to invoice
921 :param int account_id: optional ID of a G/L account to force
922 (this is used for returning products including service)
923 :return: dict of values to create() the invoice line
926 if not line.invoiced:
929 account_id = line.product_id.property_account_income.id
931 account_id = line.product_id.categ_id.property_account_income_categ.id
933 raise osv.except_osv(_('Error!'),
934 _('Please define income account for this product: "%s" (id:%d).') % \
935 (line.product_id.name, line.product_id.id,))
937 prop = self.pool.get('ir.property').get(cr, uid,
938 'property_account_income_categ', 'product.category',
940 account_id = prop and prop.id or False
941 uosqty = self._get_line_qty(cr, uid, line, context=context)
942 uos_id = self._get_line_uom(cr, uid, line, context=context)
945 pu = round(line.price_unit * line.product_uom_qty / uosqty,
946 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
947 fpos = line.order_id.fiscal_position or False
948 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
950 raise osv.except_osv(_('Error!'),
951 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
954 'sequence': line.sequence,
955 'origin': line.order_id.name,
956 'account_id': account_id,
959 'discount': line.discount,
961 'product_id': line.product_id.id or False,
962 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
963 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
968 def invoice_line_create(self, cr, uid, ids, context=None):
974 for line in self.browse(cr, uid, ids, context=context):
975 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
977 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
978 self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
979 sales.add(line.order_id.id)
980 create_ids.append(inv_id)
981 # Trigger workflow events
982 for sale_id in sales:
983 workflow.trg_write(uid, 'sale.order', sale_id, cr)
986 def button_cancel(self, cr, uid, ids, context=None):
987 for line in self.browse(cr, uid, ids, context=context):
989 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sales order line that has already been invoiced.'))
990 return self.write(cr, uid, ids, {'state': 'cancel'})
992 def button_confirm(self, cr, uid, ids, context=None):
993 return self.write(cr, uid, ids, {'state': 'confirmed'})
995 def button_done(self, cr, uid, ids, context=None):
996 res = self.write(cr, uid, ids, {'state': 'done'})
997 for line in self.browse(cr, uid, ids, context=context):
998 workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
1001 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
1002 product_obj = self.pool.get('product.product')
1004 return {'value': {'product_uom': product_uos,
1005 'product_uom_qty': product_uos_qty}, 'domain': {}}
1007 product = product_obj.browse(cr, uid, product_id)
1009 'product_uom': product.uom_id.id,
1011 # FIXME must depend on uos/uom of the product and not only of the coeff.
1014 'product_uom_qty': product_uos_qty / product.uos_coeff,
1015 'th_weight': product_uos_qty / product.uos_coeff * product.weight
1017 except ZeroDivisionError:
1019 return {'value': value}
1021 def create(self, cr, uid, values, context=None):
1022 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']):
1023 order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position'], context=context)
1024 defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
1025 qty=float(values.get('product_uom_qty', False)),
1026 uom=values.get('product_uom', False),
1027 qty_uos=float(values.get('product_uos_qty', False)),
1028 uos=values.get('product_uos', False),
1029 name=values.get('name', False),
1030 partner_id=order['partner_id'][0],
1031 date_order=order['date_order'],
1032 fiscal_position=order['fiscal_position'][0] if order['fiscal_position'] else False,
1033 flag=False, # Force name update
1036 if defaults.get('tax_id'):
1037 defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
1038 values = dict(defaults, **values)
1039 return super(sale_order_line, self).create(cr, uid, values, context=context)
1041 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
1042 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1043 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
1046 Partner = self.pool['res.partner']
1047 ProductUom = self.pool['product.uom']
1048 Product = self.pool['product.product']
1049 ctx_product = dict(context)
1052 partner = Partner.browse(cr, uid, partner_id, context=context)
1053 ctx_product['lang'] = partner.lang
1054 ctx_product['partner_id'] = partner_id
1056 ctx_product['lang'] = lang
1059 return {'value': {'th_weight': 0,
1060 'product_uos_qty': qty}, 'domain': {'product_uom': [],
1063 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
1066 product_obj = Product.browse(cr, uid, product, context=ctx_product)
1070 uom2 = ProductUom.browse(cr, uid, uom, context=context)
1071 if product_obj.uom_id.category_id.id != uom2.category_id.id:
1074 if product_obj.uos_id:
1075 uos2 = ProductUom.browse(cr, uid, uos, context=context)
1076 if product_obj.uos_id.category_id.id != uos2.category_id.id:
1082 if not fiscal_position:
1083 fpos = partner and partner.property_account_position or False
1085 fpos = self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position)
1086 if update_tax: # The quantity only have changed
1087 result['tax_id'] = self.pool['account.fiscal.position'].map_tax(cr, uid, fpos, product_obj.taxes_id)
1090 result['name'] = Product.name_get(cr, uid, [product_obj.id], context=ctx_product)[0][1]
1091 if product_obj.description_sale:
1092 result['name'] += '\n'+product_obj.description_sale
1094 if (not uom) and (not uos):
1095 result['product_uom'] = product_obj.uom_id.id
1096 if product_obj.uos_id:
1097 result['product_uos'] = product_obj.uos_id.id
1098 result['product_uos_qty'] = qty * product_obj.uos_coeff
1099 uos_category_id = product_obj.uos_id.category_id.id
1101 result['product_uos'] = False
1102 result['product_uos_qty'] = qty
1103 uos_category_id = False
1104 result['th_weight'] = qty * product_obj.weight
1105 domain = {'product_uom':
1106 [('category_id', '=', product_obj.uom_id.category_id.id)],
1108 [('category_id', '=', uos_category_id)]}
1109 elif uos and not uom: # only happens if uom is False
1110 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
1111 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
1112 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
1113 elif uom: # whether uos is set or not
1114 default_uom = product_obj.uom_id and product_obj.uom_id.id
1115 q = ProductUom._compute_qty(cr, uid, uom, qty, default_uom)
1116 if product_obj.uos_id:
1117 result['product_uos'] = product_obj.uos_id.id
1118 result['product_uos_qty'] = qty * product_obj.uos_coeff
1120 result['product_uos'] = False
1121 result['product_uos_qty'] = qty
1122 result['th_weight'] = q * product_obj.weight # Round the quantity up
1125 uom2 = product_obj.uom_id
1127 if pricelist and partner_id:
1128 price = self.pool['product.pricelist'].price_get(cr, uid, [pricelist],
1129 product, qty or 1.0, partner_id, {
1130 'uom': uom or result.get('product_uom'),
1134 price = Product.price_get(cr, uid, [product], ptype='list_price', context=ctx_product)[product] or False
1135 result.update({'price_unit': price})
1136 return {'value': result, 'domain': domain}
1138 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1139 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1140 lang=False, update_tax=True, date_order=False, context=None):
1141 context = context or {}
1142 lang = lang or ('lang' in context and context['lang'])
1144 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1145 return self.product_id_change(cursor, user, ids, pricelist, product,
1146 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1147 partner_id=partner_id, lang=lang, update_tax=update_tax,
1148 date_order=date_order, context=context)
1150 def unlink(self, cr, uid, ids, context=None):
1153 """Allows to delete sales order lines in draft,cancel states"""
1154 for rec in self.browse(cr, uid, ids, context=context):
1155 if rec.state not in ['draft', 'cancel']:
1156 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1157 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1160 class mail_compose_message(osv.Model):
1161 _inherit = 'mail.compose.message'
1163 def send_mail(self, cr, uid, ids, context=None):
1164 context = context or {}
1165 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1166 context = dict(context, mail_post_autofollow=True)
1167 self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
1168 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1171 class account_invoice(osv.Model):
1172 _inherit = 'account.invoice'
1174 def _get_default_team_id(self, cr, uid, context=None):
1175 """ Gives default team by checking if present in the context """
1176 team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False
1179 def _resolve_team_id_from_context(self, cr, uid, context=None):
1180 """ Returns ID of team based on the value of 'team_id'
1181 context key, or None if it cannot be resolved to a single
1186 if type(context.get('default_team_id')) in (int, long):
1187 return context.get('default_team_id')
1188 if isinstance(context.get('default_team_id'), basestring):
1189 team_ids = self.pool.get('crm.team').name_search(cr, uid, name=context['default_team_id'], context=context)
1190 if len(team_ids) == 1:
1191 return int(team_ids[0][0])
1195 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'),
1199 'team_id': lambda self, cr, uid, c=None: self._get_default_team_id(cr, uid, context=c)
1202 def confirm_paid(self, cr, uid, ids, context=None):
1203 sale_order_obj = self.pool.get('sale.order')
1204 res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
1205 so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
1206 for so_id in so_ids:
1207 sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
1210 def unlink(self, cr, uid, ids, context=None):
1211 """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
1212 invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
1213 #if we can't cancel all invoices, do nothing
1214 if len(invoice_ids) == len(ids):
1215 #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
1216 #it will trigger the workflow to put the sale order in an 'invoice exception' state
1218 workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
1219 return super(account_invoice, self).unlink(cr, uid, ids, context=context)
1222 class procurement_order(osv.osv):
1223 _inherit = 'procurement.order'
1225 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
1228 def write(self, cr, uid, ids, vals, context=None):
1229 if isinstance(ids, (int, long)):
1231 res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
1232 from openerp import workflow
1233 if vals.get('state') in ['done', 'cancel', 'exception']:
1234 for proc in self.browse(cr, uid, ids, context=context):
1235 if proc.sale_line_id and proc.sale_line_id.order_id:
1236 order_id = proc.sale_line_id.order_id.id
1237 if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
1238 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
1239 if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
1240 workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
1243 class product_product(osv.Model):
1244 _inherit = 'product.product'
1246 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1247 SaleOrderLine = self.pool['sale.order.line']
1249 product_id: SaleOrderLine.search_count(cr,uid, [('product_id', '=', product_id)], context=context)
1250 for product_id in ids
1254 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1258 class product_template(osv.Model):
1259 _inherit = 'product.template'
1261 def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
1262 res = dict.fromkeys(ids, 0)
1263 for template in self.browse(cr, uid, ids, context=context):
1264 res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
1267 def action_view_sales(self, cr, uid, ids, context=None):
1268 act_obj = self.pool.get('ir.actions.act_window')
1269 mod_obj = self.pool.get('ir.model.data')
1271 for template in self.browse(cr, uid, ids, context=context):
1272 product_ids += [x.id for x in template.product_variant_ids]
1273 result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
1274 result = act_obj.read(cr, uid, [result], context=context)[0]
1275 result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
1280 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
1284 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: