1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime, timedelta
23 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from tools.translate import _
28 from tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
29 import decimal_precision as dp
32 class sale_shop(osv.osv):
34 _description = "Sales Shop"
36 'name': fields.char('Shop Name', size=64, required=True),
37 'payment_default_id': fields.many2one('account.payment.term', 'Default Payment Term', required=True),
38 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist'),
39 'project_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('parent_id', '!=', False)]),
40 'company_id': fields.many2one('res.company', 'Company', required=False),
43 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'sale.shop', context=c),
48 class sale_order(osv.osv):
50 _inherit = ['mail.thread', 'ir.needaction_mixin']
51 _description = "Sales Order"
53 def onchange_shop_id(self, cr, uid, ids, shop_id, context=None):
56 shop = self.pool.get('sale.shop').browse(cr, uid, shop_id, context=context)
57 if shop.project_id.id:
58 v['project_id'] = shop.project_id.id
59 if shop.pricelist_id.id:
60 v['pricelist_id'] = shop.pricelist_id.id
63 def copy(self, cr, uid, id, default=None, context=None):
69 'date_confirm': False,
70 'name': self.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
72 return super(sale_order, self).copy(cr, uid, id, default, context=context)
74 def _amount_line_tax(self, cr, uid, line, context=None):
76 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']:
77 val += c.get('amount', 0.0)
80 def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
81 cur_obj = self.pool.get('res.currency')
83 for order in self.browse(cr, uid, ids, context=context):
85 'amount_untaxed': 0.0,
90 cur = order.pricelist_id.currency_id
91 for line in order.order_line:
92 val1 += line.price_subtotal
93 val += self._amount_line_tax(cr, uid, line, context=context)
94 res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
95 res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
96 res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
100 def _invoiced_rate(self, cursor, user, ids, name, arg, context=None):
102 for sale in self.browse(cursor, user, ids, context=context):
107 for invoice in sale.invoice_ids:
108 if invoice.state not in ('draft', 'cancel'):
109 tot += invoice.amount_untaxed
111 res[sale.id] = min(100.0, tot * 100.0 / (sale.amount_untaxed or 1.00))
116 def _invoice_exists(self, cursor, user, ids, name, arg, context=None):
118 for sale in self.browse(cursor, user, ids, context=context):
124 def _invoiced(self, cursor, user, ids, name, arg, context=None):
126 for sale in self.browse(cursor, user, ids, context=context):
128 invoice_existence = False
129 for invoice in sale.invoice_ids:
130 if invoice.state!='cancel':
131 invoice_existence = True
132 if invoice.state != 'paid':
135 if not invoice_existence or sale.state == 'manual':
139 def _invoiced_search(self, cursor, user, obj, name, args, context=None):
148 clause += 'AND inv.state = \'paid\''
150 clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
151 sale_clause = ', sale_order AS sale '
154 cursor.execute('SELECT rel.order_id ' \
155 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
156 'WHERE rel.invoice_id = inv.id ' + clause)
157 res = cursor.fetchall()
159 cursor.execute('SELECT sale.id ' \
160 'FROM sale_order AS sale ' \
161 'WHERE sale.id NOT IN ' \
162 '(SELECT rel.order_id ' \
163 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
164 res.extend(cursor.fetchall())
166 return [('id', '=', 0)]
167 return [('id', 'in', [x[0] for x in res])]
169 def _get_order(self, cr, uid, ids, context=None):
171 for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
172 result[line.order_id.id] = True
176 'name': fields.char('Order Reference', size=64, required=True,
177 readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
178 'shop_id': fields.many2one('sale.shop', 'Shop', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
179 'origin': fields.char('Source Document', size=64, help="Reference of the document that generated this sales order request."),
180 'client_order_ref': fields.char('Customer Reference', size=64),
181 'state': fields.selection([
182 ('draft', 'Draft Quotation'),
183 ('sent', 'Quotation Sent'),
184 ('cancel', 'Cancelled'),
185 ('waiting_date', 'Waiting Schedule'),
186 ('progress', 'Sale Order'),
187 ('manual', 'Sale to Invoice'),
188 ('invoice_except', 'Invoice Exception'),
190 ], 'Status', readonly=True, help="Gives the status of the quotation or sales order. \nThe exception status is automatically set when a cancel operation occurs in the processing of a document linked to the sale order. \nThe 'Waiting Schedule' status is set when the invoice is confirmed but waiting for the scheduler to run on the order date.", select=True),
191 'date_order': fields.date('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
192 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
193 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed."),
194 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
195 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True),
196 '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."),
197 '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."),
198 'order_policy': fields.selection([
199 ('manual', 'On Demand'),
200 ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
201 help="""This field controls how invoice and delivery operations are synchronized.
202 - With 'Before Delivery', a draft invoice is created, and it must be paid before delivery."""),
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", 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)]}),
208 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
209 'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', 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 sale order has at least one invoice."),
214 'note': fields.text('Terms and conditions'),
216 'amount_untaxed': fields.function(_amount_all, 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."),
222 'amount_tax': fields.function(_amount_all, 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, 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 'invoice_quantity': fields.selection([('order', 'Ordered Quantities')], 'Invoice on', help="The sale order will automatically create the invoice proposition (draft invoice).", required=True, readonly=True, states={'draft': [('readonly', False)]}),
236 'payment_term': fields.many2one('account.payment.term', 'Payment Term'),
237 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position'),
238 'company_id': fields.related('shop_id','company_id',type='many2one',relation='res.company',string='Company',store=True,readonly=True)
241 'date_order': fields.date.context_today,
242 'order_policy': 'manual',
244 'user_id': lambda obj, cr, uid, context: uid,
245 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'sale.order'),
246 'invoice_quantity': 'order',
247 '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'],
248 '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 ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
256 def unlink(self, cr, uid, ids, context=None):
257 sale_orders = self.read(cr, uid, ids, ['state'], context=context)
259 for s in sale_orders:
260 if s['state'] in ['draft', 'cancel']:
261 unlink_ids.append(s['id'])
263 raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sale order, you must cancel it before !'))
265 return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
267 def copy_quotation(self, cr, uid, ids, context=None):
268 id = self.copy(cr, uid, ids[0], context=None)
269 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
270 view_id = view_ref and view_ref[1] or False,
272 'type': 'ir.actions.act_window',
273 'name': _('Sales Order'),
274 'res_model': 'sale.order',
283 def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
287 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
290 return {'value': value}
292 'title': _('Pricelist Warning!'),
293 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
295 return {'warning': warning, 'value': value}
297 def onchange_partner_id(self, cr, uid, ids, part):
299 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
301 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
302 part = self.pool.get('res.partner').browse(cr, uid, part)
303 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
304 payment_term = part.property_payment_term and part.property_payment_term.id or False
305 fiscal_position = part.property_account_position and part.property_account_position.id or False
306 dedicated_salesman = part.user_id and part.user_id.id or uid
308 'partner_invoice_id': addr['invoice'],
309 'partner_shipping_id': addr['delivery'],
310 'payment_term': payment_term,
311 'fiscal_position': fiscal_position,
312 'user_id': dedicated_salesman,
315 val['pricelist_id'] = pricelist
316 return {'value': val}
318 def create(self, cr, uid, vals, context=None):
319 order = super(sale_order, self).create(cr, uid, vals, context=context)
321 self.create_send_note(cr, uid, [order], context=context)
324 def button_dummy(self, cr, uid, ids, context=None):
327 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
328 # can be removed after 6.1.
329 def _inv_get(self, cr, uid, order, context=None):
332 def _prepare_invoice(self, cr, uid, order, lines, context=None):
333 """Prepare the dict of values to create the new invoice for a
334 sale order. This method may be overridden to implement custom
335 invoice generation (making sure to call super() to establish
336 a clean extension chain).
338 :param browse_record order: sale.order record to invoice
339 :param list(int) line: list of invoice line IDs that must be
340 attached to the invoice
341 :return: dict of value to create() the invoice
345 journal_ids = self.pool.get('account.journal').search(cr, uid,
346 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
349 raise osv.except_osv(_('Error!'),
350 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
352 'name': order.client_order_ref or '',
353 'origin': order.name,
354 'type': 'out_invoice',
355 'reference': order.client_order_ref or order.name,
356 'account_id': order.partner_id.property_account_receivable.id,
357 'partner_id': order.partner_id.id,
358 'journal_id': journal_ids[0],
359 'invoice_line': [(6, 0, lines)],
360 'currency_id': order.pricelist_id.currency_id.id,
361 'comment': order.note,
362 'payment_term': order.payment_term and order.payment_term.id or False,
363 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
364 'date_invoice': context.get('date_invoice', False),
365 'company_id': order.company_id.id,
366 'user_id': order.user_id and order.user_id.id or False
369 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
370 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
373 def _make_invoice(self, cr, uid, order, lines, context=None):
374 inv_obj = self.pool.get('account.invoice')
375 obj_invoice_line = self.pool.get('account.invoice.line')
378 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
379 from_line_invoice_ids = []
380 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
381 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
382 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
383 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
384 for preinv in order.invoice_ids:
385 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
386 for preline in preinv.invoice_line:
387 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
388 lines.append(inv_line_id)
389 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
390 inv_id = inv_obj.create(cr, uid, inv, context=context)
391 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
392 if data.get('value', False):
393 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
394 inv_obj.button_compute(cr, uid, [inv_id])
397 def print_quotation(self, cr, uid, ids, context=None):
399 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
401 assert len(ids) == 1, 'This option should only be used for a single id at a time'
402 wf_service = netsvc.LocalService("workflow")
403 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
405 'model': 'sale.order',
407 'form': self.read(cr, uid, ids[0], context=context),
409 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
411 def manual_invoice(self, cr, uid, ids, context=None):
412 """ create invoices for the given sale orders (ids), and open the form
413 view of one of the newly created invoices
415 mod_obj = self.pool.get('ir.model.data')
416 wf_service = netsvc.LocalService("workflow")
418 # create invoices through the sale orders' workflow
419 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
421 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
422 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
423 # determine newly created invoices
424 new_inv_ids = list(inv_ids1 - inv_ids0)
426 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
427 res_id = res and res[1] or False,
430 'name': _('Customer Invoices'),
434 'res_model': 'account.invoice',
435 'context': "{'type':'out_invoice'}",
436 'type': 'ir.actions.act_window',
439 'res_id': new_inv_ids and new_inv_ids[0] or False,
442 def action_view_invoice(self, cr, uid, ids, context=None):
444 This function returns an action that display existing invoices of given sale order ids. It can either be a in a list or in a form view, if there is only one invoice to show.
446 mod_obj = self.pool.get('ir.model.data')
447 act_obj = self.pool.get('ir.actions.act_window')
449 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
450 id = result and result[1] or False
451 result = act_obj.read(cr, uid, [id], context=context)[0]
452 #compute the number of invoices to display
454 for so in self.browse(cr, uid, ids, context=context):
455 inv_ids += [invoice.id for invoice in so.invoice_ids]
456 #choose the view_mode accordingly
458 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
460 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
461 result['views'] = [(res and res[1] or False, 'form')]
462 result['res_id'] = inv_ids and inv_ids[0] or False
465 def test_no_product(self, cr, uid, order, context):
466 for line in order.order_line:
467 if line.product_id and (line.product_id.type<>'service'):
471 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
473 states = ['confirmed', 'done', 'exception']
477 invoice = self.pool.get('account.invoice')
478 obj_sale_order_line = self.pool.get('sale.order.line')
479 partner_currency = {}
482 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
483 # last day of the last month as invoice date
485 context['date_inv'] = date_inv
486 for o in self.browse(cr, uid, ids, context=context):
487 currency_id = o.pricelist_id.currency_id.id
488 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
489 raise osv.except_osv(
491 _('You cannot group sales having different currencies for the same partner.'))
493 partner_currency[o.partner_id.id] = currency_id
495 for line in o.order_line:
498 elif (line.state in states):
499 lines.append(line.id)
500 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
502 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
504 for o in self.browse(cr, uid, ids, context=context):
505 for i in o.invoice_ids:
506 if i.state == 'draft':
508 for val in invoices.values():
510 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
513 invoice_ref += o.name + '|'
514 self.write(cr, uid, [o.id], {'state': 'progress'})
515 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
516 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
518 for order, il in val:
519 res = self._make_invoice(cr, uid, order, il, context=context)
520 invoice_ids.append(res)
521 self.write(cr, uid, [order.id], {'state': 'progress'})
522 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
524 self.invoice_send_note(cr, uid, ids, res, context)
527 def action_invoice_cancel(self, cr, uid, ids, context=None):
530 for sale in self.browse(cr, uid, ids, context=context):
531 for line in sale.order_line:
533 # Check if the line is invoiced (has asociated invoice
534 # lines from non-cancelled invoices).
537 for iline in line.invoice_lines:
538 if iline.invoice_id and iline.invoice_id.state != 'cancel':
541 # Update the line (only when needed)
542 if line.invoiced != invoiced:
543 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
544 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
547 def action_invoice_end(self, cr, uid, ids, context=None):
548 for order in self.browse(cr, uid, ids, context=context):
550 # Update the sale order lines state (and invoiced flag).
552 for line in order.order_line:
555 # Check if the line is invoiced (has asociated invoice
556 # lines from non-cancelled invoices).
559 for iline in line.invoice_lines:
560 if iline.invoice_id and iline.invoice_id.state != 'cancel':
563 if line.invoiced != invoiced:
564 vals['invoiced'] = invoiced
565 # If the line was in exception state, now it gets confirmed.
566 if line.state == 'exception':
567 vals['state'] = 'confirmed'
568 # Update the line (only when needed).
570 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
572 # Update the sales order state.
574 if order.state == 'invoice_except':
575 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
576 self.invoice_paid_send_note(cr, uid, [order.id], context=context)
579 def action_cancel(self, cr, uid, ids, context=None):
580 wf_service = netsvc.LocalService("workflow")
583 sale_order_line_obj = self.pool.get('sale.order.line')
584 for sale in self.browse(cr, uid, ids, context=context):
585 for inv in sale.invoice_ids:
586 if inv.state not in ('draft', 'cancel'):
587 raise osv.except_osv(
588 _('Cannot cancel this sales order!'),
589 _('First cancel all invoices attached to this sales order.'))
590 for r in self.read(cr, uid, ids, ['invoice_ids']):
591 for inv in r['invoice_ids']:
592 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
593 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
595 self.cancel_send_note(cr, uid, [sale.id], context=None)
596 self.write(cr, uid, ids, {'state': 'cancel'})
599 def action_button_confirm(self, cr, uid, ids, context=None):
600 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
601 wf_service = netsvc.LocalService('workflow')
602 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
604 # redisplay the record as a sale order
605 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
606 view_id = view_ref and view_ref[1] or False,
608 'type': 'ir.actions.act_window',
609 'name': _('Sales Order'),
610 'res_model': 'sale.order',
619 def action_wait(self, cr, uid, ids, context=None):
620 for o in self.browse(cr, uid, ids):
622 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
623 noprod = self.test_no_product(cr, uid, o, context)
624 if (o.order_policy == 'manual') or noprod:
625 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
627 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
628 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
629 self.confirm_send_note(cr, uid, ids, context)
632 def action_quotation_send(self, cr, uid, ids, context=None):
634 This function opens a window to compose an email, with the edi sale template message loaded by default
636 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
637 mod_obj = self.pool.get('ir.model.data')
638 template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
639 template_id = template and template[1] or False
640 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
641 res_id = res and res[1] or False
644 'default_model': 'sale.order',
645 'default_res_id': ids[0],
646 'default_use_template': True,
647 'default_template_id': template_id,
648 'default_composition_mode': 'comment',
649 'mark_so_as_sent': True
654 'res_model': 'mail.compose.message',
655 'views': [(res_id, 'form')],
657 'type': 'ir.actions.act_window',
663 def action_done(self, cr, uid, ids, context=None):
664 self.done_send_note(cr, uid, ids, context=context)
665 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
667 # ------------------------------------------------
668 # OpenChatter methods and notifications
669 # ------------------------------------------------
671 def needaction_domain_get(self, cr, uid, ids, context=None):
672 return [('state', '=', 'draft'), ('user_id','=',uid)]
674 def create_send_note(self, cr, uid, ids, context=None):
675 for obj in self.browse(cr, uid, ids, context=context):
676 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
678 def confirm_send_note(self, cr, uid, ids, context=None):
679 for obj in self.browse(cr, uid, ids, context=context):
680 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>converted</b> to Sale Order of %s %s.") % (obj.partner_id.name, obj.amount_total, obj.pricelist_id.currency_id.symbol), context=context)
682 def cancel_send_note(self, cr, uid, ids, context=None):
683 for obj in self.browse(cr, uid, ids, context=context):
684 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
686 def done_send_note(self, cr, uid, ids, context=None):
687 for obj in self.browse(cr, uid, ids, context=context):
688 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> set to <b>Done</b>") % (obj.partner_id.name), context=context)
690 def invoice_paid_send_note(self, cr, uid, ids, context=None):
691 self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
693 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
694 for order in self.browse(cr, uid, ids, context=context):
695 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
696 self.message_post(cr, uid, [order.id], body=_("Draft Invoice of %s %s <b>waiting for validation</b>.") % (invoice.amount_total, invoice.currency_id.symbol), context=context)
700 # TODO add a field price_unit_uos
701 # - update it on change product and unit price
702 # - use it in report if there is a uos
703 class sale_order_line(osv.osv):
705 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
706 tax_obj = self.pool.get('account.tax')
707 cur_obj = self.pool.get('res.currency')
711 for line in self.browse(cr, uid, ids, context=context):
712 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
713 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
714 cur = line.order_id.pricelist_id.currency_id
715 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
718 def _get_uom_id(self, cr, uid, *args):
720 proxy = self.pool.get('ir.model.data')
721 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
723 except Exception, ex:
726 _name = 'sale.order.line'
727 _description = 'Sales Order Line'
729 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
730 'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
731 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
732 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
733 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
734 'invoiced': fields.boolean('Invoiced', readonly=True),
735 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
736 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
737 help="From stock: When needed, the product is taken from the stock or we wait for replenishment.\nOn order: When needed, the product is purchased or produced."),
738 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
739 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
740 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
741 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
742 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
743 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
744 'product_uos': fields.many2one('product.uom', 'Product UoS'),
745 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
746 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
747 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
748 help='* The \'Draft\' status is set when the related sales order in draft status. \
749 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
750 \n* The \'Exception\' status is set when the related sales order is set as exception. \
751 \n* The \'Done\' status is set when the sales order line has been picked. \
752 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
753 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
754 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
755 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
757 _order = 'sequence, id'
759 'product_uom' : _get_uom_id,
761 'product_uom_qty': 1,
762 'product_uos_qty': 1,
766 'type': 'make_to_stock',
770 def _get_line_qty(self, cr, uid, line, context=None):
771 if (line.order_id.invoice_quantity=='order'):
773 return line.product_uos_qty or 0.0
774 return line.product_uom_qty
776 def _get_line_uom(self, cr, uid, line, context=None):
777 if (line.order_id.invoice_quantity=='order'):
779 return line.product_uos.id
780 return line.product_uom.id
782 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
783 """Prepare the dict of values to create the new invoice line for a
784 sale order line. This method may be overridden to implement custom
785 invoice generation (making sure to call super() to establish
786 a clean extension chain).
788 :param browse_record line: sale.order.line record to invoice
789 :param int account_id: optional ID of a G/L account to force
790 (this is used for returning products including service)
791 :return: dict of values to create() the invoice line
794 if not line.invoiced:
797 account_id = line.product_id.product_tmpl_id.property_account_income.id
799 account_id = line.product_id.categ_id.property_account_income_categ.id
801 raise osv.except_osv(_('Error!'),
802 _('Please define income account for this product: "%s" (id:%d).') % \
803 (line.product_id.name, line.product_id.id,))
805 prop = self.pool.get('ir.property').get(cr, uid,
806 'property_account_income_categ', 'product.category',
808 account_id = prop and prop.id or False
809 uosqty = self._get_line_qty(cr, uid, line, context=context)
810 uos_id = self._get_line_uom(cr, uid, line, context=context)
813 pu = round(line.price_unit * line.product_uom_qty / uosqty,
814 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
815 fpos = line.order_id.fiscal_position or False
816 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
818 raise osv.except_osv(_('Error!'),
819 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
822 'sequence': line.sequence,
823 'origin': line.order_id.name,
824 'account_id': account_id,
827 'discount': line.discount,
829 'product_id': line.product_id.id or False,
830 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
831 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
836 def invoice_line_create(self, cr, uid, ids, context=None):
842 for line in self.browse(cr, uid, ids, context=context):
843 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
845 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
846 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
847 self.write(cr, uid, [line.id], {'invoiced': True})
848 sales.add(line.order_id.id)
849 create_ids.append(inv_id)
850 # Trigger workflow events
851 wf_service = netsvc.LocalService("workflow")
852 for sale_id in sales:
853 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
856 def button_cancel(self, cr, uid, ids, context=None):
857 for line in self.browse(cr, uid, ids, context=context):
859 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
860 return self.write(cr, uid, ids, {'state': 'cancel'})
862 def button_confirm(self, cr, uid, ids, context=None):
863 return self.write(cr, uid, ids, {'state': 'confirmed'})
865 def button_done(self, cr, uid, ids, context=None):
866 wf_service = netsvc.LocalService("workflow")
867 res = self.write(cr, uid, ids, {'state': 'done'})
868 for line in self.browse(cr, uid, ids, context=context):
869 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
872 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
873 product_obj = self.pool.get('product.product')
875 return {'value': {'product_uom': product_uos,
876 'product_uom_qty': product_uos_qty}, 'domain': {}}
878 product = product_obj.browse(cr, uid, product_id)
880 'product_uom': product.uom_id.id,
882 # FIXME must depend on uos/uom of the product and not only of the coeff.
885 'product_uom_qty': product_uos_qty / product.uos_coeff,
886 'th_weight': product_uos_qty / product.uos_coeff * product.weight
888 except ZeroDivisionError:
890 return {'value': value}
892 def copy_data(self, cr, uid, id, default=None, context=None):
895 default.update({'state': 'draft', 'invoiced': False, 'invoice_lines': []})
896 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
898 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
899 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
900 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
901 context = context or {}
902 lang = lang or context.get('lang',False)
904 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
906 product_uom_obj = self.pool.get('product.uom')
907 partner_obj = self.pool.get('res.partner')
908 product_obj = self.pool.get('product.product')
909 context = {'lang': lang, 'partner_id': partner_id}
911 lang = partner_obj.browse(cr, uid, partner_id).lang
912 context_partner = {'lang': lang, 'partner_id': partner_id}
915 return {'value': {'th_weight': 0,
916 'product_uos_qty': qty}, 'domain': {'product_uom': [],
919 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
923 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
927 uom2 = product_uom_obj.browse(cr, uid, uom)
928 if product_obj.uom_id.category_id.id != uom2.category_id.id:
931 if product_obj.uos_id:
932 uos2 = product_uom_obj.browse(cr, uid, uos)
933 if product_obj.uos_id.category_id.id != uos2.category_id.id:
937 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
938 if update_tax: #The quantity only have changed
939 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
942 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
943 if product_obj.description_sale:
944 result['name'] += '\n'+product_obj.description_sale
946 if (not uom) and (not uos):
947 result['product_uom'] = product_obj.uom_id.id
948 if product_obj.uos_id:
949 result['product_uos'] = product_obj.uos_id.id
950 result['product_uos_qty'] = qty * product_obj.uos_coeff
951 uos_category_id = product_obj.uos_id.category_id.id
953 result['product_uos'] = False
954 result['product_uos_qty'] = qty
955 uos_category_id = False
956 result['th_weight'] = qty * product_obj.weight
957 domain = {'product_uom':
958 [('category_id', '=', product_obj.uom_id.category_id.id)],
960 [('category_id', '=', uos_category_id)]}
961 elif uos and not uom: # only happens if uom is False
962 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
963 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
964 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
965 elif uom: # whether uos is set or not
966 default_uom = product_obj.uom_id and product_obj.uom_id.id
967 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
968 result['product_uom'] = default_uom
969 if product_obj.uos_id:
970 result['product_uos'] = product_obj.uos_id.id
971 result['product_uos_qty'] = qty * product_obj.uos_coeff
973 result['product_uos'] = False
974 result['product_uos_qty'] = qty
975 result['th_weight'] = q * product_obj.weight # Round the quantity up
978 uom2 = product_obj.uom_id
982 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
983 'Please set one before choosing a product.')
984 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
986 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
987 product, qty or 1.0, partner_id, {
988 'uom': uom or result.get('product_uom'),
992 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
993 "You have to change either the product, the quantity or the pricelist.")
995 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
997 result.update({'price_unit': price})
1000 'title': _('Configuration Error!'),
1001 'message' : warning_msgs
1003 return {'value': result, 'domain': domain, 'warning': warning}
1005 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1006 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1007 lang=False, update_tax=True, date_order=False, context=None):
1008 context = context or {}
1009 lang = lang or ('lang' in context and context['lang'])
1011 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1012 return self.product_id_change(cursor, user, ids, pricelist, product,
1013 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1014 partner_id=partner_id, lang=lang, update_tax=update_tax,
1015 date_order=date_order, context=context)
1017 def unlink(self, cr, uid, ids, context=None):
1020 """Allows to delete sales order lines in draft,cancel states"""
1021 for rec in self.browse(cr, uid, ids, context=context):
1022 if rec.state not in ['draft', 'cancel']:
1023 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1024 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1028 class mail_compose_message(osv.osv):
1029 _inherit = 'mail.compose.message'
1030 def send_mail(self, cr, uid, ids, context=None):
1031 context = context or {}
1032 if context.get('mark_so_as_sent', False) and context.get('default_res_id', False):
1033 wf_service = netsvc.LocalService("workflow")
1034 wf_service.trg_validate(uid, 'sale.order', context.get('default_res_id', False), 'quotation_sent', cr)
1035 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1037 mail_compose_message()
1039 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: