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):
284 context = context or {}
288 'currency_id': self.pool.get('product.pricelist').browse(cr, uid, pricelist_id, context=context).currency_id.id
291 return {'value': value}
293 'title': _('Pricelist Warning!'),
294 'message' : _('If you change the pricelist of this order (and eventually the currency), prices of existing order lines will not be updated.')
296 return {'warning': warning, 'value': value}
298 def onchange_partner_id(self, cr, uid, ids, part):
300 return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}}
302 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['delivery', 'invoice', 'contact'])
303 part = self.pool.get('res.partner').browse(cr, uid, part)
304 pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
305 payment_term = part.property_payment_term and part.property_payment_term.id or False
306 fiscal_position = part.property_account_position and part.property_account_position.id or False
307 dedicated_salesman = part.user_id and part.user_id.id or uid
309 'partner_invoice_id': addr['invoice'],
310 'partner_shipping_id': addr['delivery'],
311 'payment_term': payment_term,
312 'fiscal_position': fiscal_position,
313 'user_id': dedicated_salesman,
316 val['pricelist_id'] = pricelist
317 return {'value': val}
319 def create(self, cr, uid, vals, context=None):
320 order = super(sale_order, self).create(cr, uid, vals, context=context)
322 self.create_send_note(cr, uid, [order], context=context)
325 def button_dummy(self, cr, uid, ids, context=None):
328 # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
329 # can be removed after 6.1.
330 def _inv_get(self, cr, uid, order, context=None):
333 def _prepare_invoice(self, cr, uid, order, lines, context=None):
334 """Prepare the dict of values to create the new invoice for a
335 sale order. This method may be overridden to implement custom
336 invoice generation (making sure to call super() to establish
337 a clean extension chain).
339 :param browse_record order: sale.order record to invoice
340 :param list(int) line: list of invoice line IDs that must be
341 attached to the invoice
342 :return: dict of value to create() the invoice
346 journal_ids = self.pool.get('account.journal').search(cr, uid,
347 [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
350 raise osv.except_osv(_('Error!'),
351 _('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
353 'name': order.client_order_ref or '',
354 'origin': order.name,
355 'type': 'out_invoice',
356 'reference': order.client_order_ref or order.name,
357 'account_id': order.partner_id.property_account_receivable.id,
358 'partner_id': order.partner_id.id,
359 'journal_id': journal_ids[0],
360 'invoice_line': [(6, 0, lines)],
361 'currency_id': order.pricelist_id.currency_id.id,
362 'comment': order.note,
363 'payment_term': order.payment_term and order.payment_term.id or False,
364 'fiscal_position': order.fiscal_position.id or order.partner_id.property_account_position.id,
365 'date_invoice': context.get('date_invoice', False),
366 'company_id': order.company_id.id,
367 'user_id': order.user_id and order.user_id.id or False
370 # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
371 invoice_vals.update(self._inv_get(cr, uid, order, context=context))
374 def _make_invoice(self, cr, uid, order, lines, context=None):
375 inv_obj = self.pool.get('account.invoice')
376 obj_invoice_line = self.pool.get('account.invoice.line')
379 invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
380 from_line_invoice_ids = []
381 for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
382 for invoice_line_id in invoiced_sale_line_id.invoice_lines:
383 if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
384 from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
385 for preinv in order.invoice_ids:
386 if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
387 for preline in preinv.invoice_line:
388 inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
389 lines.append(inv_line_id)
390 inv = self._prepare_invoice(cr, uid, order, lines, context=context)
391 inv_id = inv_obj.create(cr, uid, inv, context=context)
392 data = inv_obj.onchange_payment_term_date_invoice(cr, uid, [inv_id], inv['payment_term'], time.strftime(DEFAULT_SERVER_DATE_FORMAT))
393 if data.get('value', False):
394 inv_obj.write(cr, uid, [inv_id], data['value'], context=context)
395 inv_obj.button_compute(cr, uid, [inv_id])
398 def print_quotation(self, cr, uid, ids, context=None):
400 This function prints the sale order and mark it as sent, so that we can see more easily the next step of the workflow
402 assert len(ids) == 1, 'This option should only be used for a single id at a time'
403 wf_service = netsvc.LocalService("workflow")
404 wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cr)
406 'model': 'sale.order',
408 'form': self.read(cr, uid, ids[0], context=context),
410 return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
412 def manual_invoice(self, cr, uid, ids, context=None):
413 """ create invoices for the given sale orders (ids), and open the form
414 view of one of the newly created invoices
416 mod_obj = self.pool.get('ir.model.data')
417 wf_service = netsvc.LocalService("workflow")
419 # create invoices through the sale orders' workflow
420 inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
422 wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
423 inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
424 # determine newly created invoices
425 new_inv_ids = list(inv_ids1 - inv_ids0)
427 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
428 res_id = res and res[1] or False,
431 'name': _('Customer Invoices'),
435 'res_model': 'account.invoice',
436 'context': "{'type':'out_invoice'}",
437 'type': 'ir.actions.act_window',
440 'res_id': new_inv_ids and new_inv_ids[0] or False,
443 def action_view_invoice(self, cr, uid, ids, context=None):
445 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.
447 mod_obj = self.pool.get('ir.model.data')
448 act_obj = self.pool.get('ir.actions.act_window')
450 result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
451 id = result and result[1] or False
452 result = act_obj.read(cr, uid, [id], context=context)[0]
453 #compute the number of invoices to display
455 for so in self.browse(cr, uid, ids, context=context):
456 inv_ids += [invoice.id for invoice in so.invoice_ids]
457 #choose the view_mode accordingly
459 result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
461 res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
462 result['views'] = [(res and res[1] or False, 'form')]
463 result['res_id'] = inv_ids and inv_ids[0] or False
466 def test_no_product(self, cr, uid, order, context):
467 for line in order.order_line:
468 if line.product_id and (line.product_id.type<>'service'):
472 def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_inv = False, context=None):
474 states = ['confirmed', 'done', 'exception']
478 invoice = self.pool.get('account.invoice')
479 obj_sale_order_line = self.pool.get('sale.order.line')
480 partner_currency = {}
483 # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
484 # last day of the last month as invoice date
486 context['date_inv'] = date_inv
487 for o in self.browse(cr, uid, ids, context=context):
488 currency_id = o.pricelist_id.currency_id.id
489 if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
490 raise osv.except_osv(
492 _('You cannot group sales having different currencies for the same partner.'))
494 partner_currency[o.partner_id.id] = currency_id
496 for line in o.order_line:
499 elif (line.state in states):
500 lines.append(line.id)
501 created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
503 invoices.setdefault(o.partner_id.id, []).append((o, created_lines))
505 for o in self.browse(cr, uid, ids, context=context):
506 for i in o.invoice_ids:
507 if i.state == 'draft':
509 for val in invoices.values():
511 res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
514 invoice_ref += o.name + '|'
515 self.write(cr, uid, [o.id], {'state': 'progress'})
516 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
517 invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
519 for order, il in val:
520 res = self._make_invoice(cr, uid, order, il, context=context)
521 invoice_ids.append(res)
522 self.write(cr, uid, [order.id], {'state': 'progress'})
523 cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
525 self.invoice_send_note(cr, uid, ids, res, context)
528 def action_invoice_cancel(self, cr, uid, ids, context=None):
531 for sale in self.browse(cr, uid, ids, context=context):
532 for line in sale.order_line:
534 # Check if the line is invoiced (has asociated invoice
535 # lines from non-cancelled invoices).
538 for iline in line.invoice_lines:
539 if iline.invoice_id and iline.invoice_id.state != 'cancel':
542 # Update the line (only when needed)
543 if line.invoiced != invoiced:
544 self.pool.get('sale.order.line').write(cr, uid, [line.id], {'invoiced': invoiced}, context=context)
545 self.write(cr, uid, ids, {'state': 'invoice_except', 'invoice_ids': False}, context=context)
548 def action_invoice_end(self, cr, uid, ids, context=None):
549 for order in self.browse(cr, uid, ids, context=context):
551 # Update the sale order lines state (and invoiced flag).
553 for line in order.order_line:
556 # Check if the line is invoiced (has asociated invoice
557 # lines from non-cancelled invoices).
560 for iline in line.invoice_lines:
561 if iline.invoice_id and iline.invoice_id.state != 'cancel':
564 if line.invoiced != invoiced:
565 vals['invoiced'] = invoiced
566 # If the line was in exception state, now it gets confirmed.
567 if line.state == 'exception':
568 vals['state'] = 'confirmed'
569 # Update the line (only when needed).
571 self.pool.get('sale.order.line').write(cr, uid, [line.id], vals, context=context)
573 # Update the sales order state.
575 if order.state == 'invoice_except':
576 self.write(cr, uid, [order.id], {'state': 'progress'}, context=context)
577 self.invoice_paid_send_note(cr, uid, [order.id], context=context)
580 def action_cancel(self, cr, uid, ids, context=None):
581 wf_service = netsvc.LocalService("workflow")
584 sale_order_line_obj = self.pool.get('sale.order.line')
585 for sale in self.browse(cr, uid, ids, context=context):
586 for inv in sale.invoice_ids:
587 if inv.state not in ('draft', 'cancel'):
588 raise osv.except_osv(
589 _('Cannot cancel this sales order!'),
590 _('First cancel all invoices attached to this sales order.'))
591 for r in self.read(cr, uid, ids, ['invoice_ids']):
592 for inv in r['invoice_ids']:
593 wf_service.trg_validate(uid, 'account.invoice', inv, 'invoice_cancel', cr)
594 sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line],
596 self.cancel_send_note(cr, uid, [sale.id], context=None)
597 self.write(cr, uid, ids, {'state': 'cancel'})
600 def action_button_confirm(self, cr, uid, ids, context=None):
601 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
602 wf_service = netsvc.LocalService('workflow')
603 wf_service.trg_validate(uid, 'sale.order', ids[0], 'order_confirm', cr)
605 # redisplay the record as a sale order
606 view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
607 view_id = view_ref and view_ref[1] or False,
609 'type': 'ir.actions.act_window',
610 'name': _('Sales Order'),
611 'res_model': 'sale.order',
620 def action_wait(self, cr, uid, ids, context=None):
621 context = context or {}
622 for o in self.browse(cr, uid, ids):
624 raise osv.except_osv(_('Error!'),_('You cannot confirm a sale order which has no line.'))
625 noprod = self.test_no_product(cr, uid, o, context)
626 if (o.order_policy == 'manual') or noprod:
627 self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
629 self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
630 self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line])
631 self.confirm_send_note(cr, uid, ids, context)
634 def action_quotation_send(self, cr, uid, ids, context=None):
636 This function opens a window to compose an email, with the edi sale template message loaded by default
638 assert len(ids) == 1, 'This option should only be used for a single id at a time.'
639 ir_model_data = self.pool.get('ir.model.data')
641 template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
645 compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
647 compose_form_id = False
650 'default_model': 'sale.order',
651 'default_res_id': ids[0],
652 'default_use_template': bool(template_id),
653 'default_template_id': template_id,
654 'default_composition_mode': 'comment',
655 'mark_so_as_sent': True
658 'type': 'ir.actions.act_window',
661 'res_model': 'mail.compose.message',
662 'views': [(compose_form_id, 'form')],
663 'view_id': compose_form_id,
668 def action_done(self, cr, uid, ids, context=None):
669 self.done_send_note(cr, uid, ids, context=context)
670 return self.write(cr, uid, ids, {'state': 'done'}, context=context)
672 # ------------------------------------------------
673 # OpenChatter methods and notifications
674 # ------------------------------------------------
676 def needaction_domain_get(self, cr, uid, ids, context=None):
677 return [('state', '=', 'draft'), ('user_id','=',uid)]
679 def create_send_note(self, cr, uid, ids, context=None):
680 for obj in self.browse(cr, uid, ids, context=context):
681 self.message_post(cr, uid, [obj.id], body=_("Quotation for <em>%s</em> <b>created</b>.") % (obj.partner_id.name), context=context)
683 def confirm_send_note(self, cr, uid, ids, context=None):
684 for obj in self.browse(cr, uid, ids, context=context):
685 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), subtype="sale.mt_order_confirmed", context=context)
687 def cancel_send_note(self, cr, uid, ids, context=None):
688 for obj in self.browse(cr, uid, ids, context=context):
689 self.message_post(cr, uid, [obj.id], body=_("Sale Order for <em>%s</em> <b>cancelled</b>.") % (obj.partner_id.name), context=context)
691 def done_send_note(self, cr, uid, ids, context=None):
692 for obj in self.browse(cr, uid, ids, context=context):
693 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)
695 def invoice_paid_send_note(self, cr, uid, ids, context=None):
696 self.message_post(cr, uid, ids, body=_("Invoice <b>paid</b>."), context=context)
698 def invoice_send_note(self, cr, uid, ids, invoice_id, context=None):
699 for order in self.browse(cr, uid, ids, context=context):
700 for invoice in (inv for inv in order.invoice_ids if inv.id == invoice_id):
701 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)
705 # TODO add a field price_unit_uos
706 # - update it on change product and unit price
707 # - use it in report if there is a uos
708 class sale_order_line(osv.osv):
710 def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
711 tax_obj = self.pool.get('account.tax')
712 cur_obj = self.pool.get('res.currency')
716 for line in self.browse(cr, uid, ids, context=context):
717 price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
718 taxes = tax_obj.compute_all(cr, uid, line.tax_id, price, line.product_uom_qty, line.product_id, line.order_id.partner_id)
719 cur = line.order_id.pricelist_id.currency_id
720 res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
723 def _get_uom_id(self, cr, uid, *args):
725 proxy = self.pool.get('ir.model.data')
726 result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
728 except Exception, ex:
731 _name = 'sale.order.line'
732 _description = 'Sales Order Line'
734 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
735 'name': fields.text('Description', size=256, required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
736 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
737 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True),
738 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
739 'invoiced': fields.boolean('Invoiced', readonly=True),
740 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
741 'type': fields.selection([('make_to_stock', 'from stock'), ('make_to_order', 'on order')], 'Procurement Method', required=True, readonly=True, states={'draft': [('readonly', False)]},
742 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."),
743 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits_compute= dp.get_precision('Account')),
744 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
745 'address_allotment_id': fields.many2one('res.partner', 'Allotment Partner',help="A partner to whom the particular product needs to be allotted."),
746 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
747 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
748 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
749 'product_uos': fields.many2one('product.uom', 'Product UoS'),
750 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
751 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
752 'state': fields.selection([('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')], 'Status', required=True, readonly=True,
753 help='* The \'Draft\' status is set when the related sales order in draft status. \
754 \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
755 \n* The \'Exception\' status is set when the related sales order is set as exception. \
756 \n* The \'Done\' status is set when the sales order line has been picked. \
757 \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
758 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
759 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
760 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
762 _order = 'sequence, id'
764 'product_uom' : _get_uom_id,
766 'product_uom_qty': 1,
767 'product_uos_qty': 1,
771 'type': 'make_to_stock',
775 def _get_line_qty(self, cr, uid, line, context=None):
776 if (line.order_id.invoice_quantity=='order'):
778 return line.product_uos_qty or 0.0
779 return line.product_uom_qty
781 def _get_line_uom(self, cr, uid, line, context=None):
782 if (line.order_id.invoice_quantity=='order'):
784 return line.product_uos.id
785 return line.product_uom.id
787 def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
788 """Prepare the dict of values to create the new invoice line for a
789 sale order line. This method may be overridden to implement custom
790 invoice generation (making sure to call super() to establish
791 a clean extension chain).
793 :param browse_record line: sale.order.line record to invoice
794 :param int account_id: optional ID of a G/L account to force
795 (this is used for returning products including service)
796 :return: dict of values to create() the invoice line
799 if not line.invoiced:
802 account_id = line.product_id.product_tmpl_id.property_account_income.id
804 account_id = line.product_id.categ_id.property_account_income_categ.id
806 raise osv.except_osv(_('Error!'),
807 _('Please define income account for this product: "%s" (id:%d).') % \
808 (line.product_id.name, line.product_id.id,))
810 prop = self.pool.get('ir.property').get(cr, uid,
811 'property_account_income_categ', 'product.category',
813 account_id = prop and prop.id or False
814 uosqty = self._get_line_qty(cr, uid, line, context=context)
815 uos_id = self._get_line_uom(cr, uid, line, context=context)
818 pu = round(line.price_unit * line.product_uom_qty / uosqty,
819 self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
820 fpos = line.order_id.fiscal_position or False
821 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
823 raise osv.except_osv(_('Error!'),
824 _('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
827 'sequence': line.sequence,
828 'origin': line.order_id.name,
829 'account_id': account_id,
832 'discount': line.discount,
834 'product_id': line.product_id.id or False,
835 'invoice_line_tax_id': [(6, 0, [x.id for x in line.tax_id])],
836 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
841 def invoice_line_create(self, cr, uid, ids, context=None):
847 for line in self.browse(cr, uid, ids, context=context):
848 vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
850 inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
851 cr.execute('insert into sale_order_line_invoice_rel (order_line_id,invoice_id) values (%s,%s)', (line.id, inv_id))
852 self.write(cr, uid, [line.id], {'invoiced': True})
853 sales.add(line.order_id.id)
854 create_ids.append(inv_id)
855 # Trigger workflow events
856 wf_service = netsvc.LocalService("workflow")
857 for sale_id in sales:
858 wf_service.trg_write(uid, 'sale.order', sale_id, cr)
861 def button_cancel(self, cr, uid, ids, context=None):
862 for line in self.browse(cr, uid, ids, context=context):
864 raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line that has already been invoiced.'))
865 return self.write(cr, uid, ids, {'state': 'cancel'})
867 def button_confirm(self, cr, uid, ids, context=None):
868 return self.write(cr, uid, ids, {'state': 'confirmed'})
870 def button_done(self, cr, uid, ids, context=None):
871 wf_service = netsvc.LocalService("workflow")
872 res = self.write(cr, uid, ids, {'state': 'done'})
873 for line in self.browse(cr, uid, ids, context=context):
874 wf_service.trg_write(uid, 'sale.order', line.order_id.id, cr)
877 def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
878 product_obj = self.pool.get('product.product')
880 return {'value': {'product_uom': product_uos,
881 'product_uom_qty': product_uos_qty}, 'domain': {}}
883 product = product_obj.browse(cr, uid, product_id)
885 'product_uom': product.uom_id.id,
887 # FIXME must depend on uos/uom of the product and not only of the coeff.
890 'product_uom_qty': product_uos_qty / product.uos_coeff,
891 'th_weight': product_uos_qty / product.uos_coeff * product.weight
893 except ZeroDivisionError:
895 return {'value': value}
897 def copy_data(self, cr, uid, id, default=None, context=None):
900 default.update({'state': 'draft', 'invoiced': False, 'invoice_lines': []})
901 return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
903 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
904 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
905 lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
906 context = context or {}
907 lang = lang or context.get('lang',False)
909 raise osv.except_osv(_('No Customer Defined !'), _('Before choosing a product,\n select a customer in the sales form.'))
911 product_uom_obj = self.pool.get('product.uom')
912 partner_obj = self.pool.get('res.partner')
913 product_obj = self.pool.get('product.product')
914 context = {'lang': lang, 'partner_id': partner_id}
916 lang = partner_obj.browse(cr, uid, partner_id).lang
917 context_partner = {'lang': lang, 'partner_id': partner_id}
920 return {'value': {'th_weight': 0,
921 'product_uos_qty': qty}, 'domain': {'product_uom': [],
924 date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
928 product_obj = product_obj.browse(cr, uid, product, context=context_partner)
932 uom2 = product_uom_obj.browse(cr, uid, uom)
933 if product_obj.uom_id.category_id.id != uom2.category_id.id:
936 if product_obj.uos_id:
937 uos2 = product_uom_obj.browse(cr, uid, uos)
938 if product_obj.uos_id.category_id.id != uos2.category_id.id:
942 fpos = fiscal_position and self.pool.get('account.fiscal.position').browse(cr, uid, fiscal_position) or False
943 if update_tax: #The quantity only have changed
944 result['tax_id'] = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, product_obj.taxes_id)
947 result['name'] = self.pool.get('product.product').name_get(cr, uid, [product_obj.id], context=context_partner)[0][1]
948 if product_obj.description_sale:
949 result['name'] += '\n'+product_obj.description_sale
951 if (not uom) and (not uos):
952 result['product_uom'] = product_obj.uom_id.id
953 if product_obj.uos_id:
954 result['product_uos'] = product_obj.uos_id.id
955 result['product_uos_qty'] = qty * product_obj.uos_coeff
956 uos_category_id = product_obj.uos_id.category_id.id
958 result['product_uos'] = False
959 result['product_uos_qty'] = qty
960 uos_category_id = False
961 result['th_weight'] = qty * product_obj.weight
962 domain = {'product_uom':
963 [('category_id', '=', product_obj.uom_id.category_id.id)],
965 [('category_id', '=', uos_category_id)]}
966 elif uos and not uom: # only happens if uom is False
967 result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
968 result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
969 result['th_weight'] = result['product_uom_qty'] * product_obj.weight
970 elif uom: # whether uos is set or not
971 default_uom = product_obj.uom_id and product_obj.uom_id.id
972 q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
973 result['product_uom'] = default_uom
974 if product_obj.uos_id:
975 result['product_uos'] = product_obj.uos_id.id
976 result['product_uos_qty'] = qty * product_obj.uos_coeff
978 result['product_uos'] = False
979 result['product_uos_qty'] = qty
980 result['th_weight'] = q * product_obj.weight # Round the quantity up
983 uom2 = product_obj.uom_id
987 warn_msg = _('You have to select a pricelist or a customer in the sales form !\n'
988 'Please set one before choosing a product.')
989 warning_msgs += _("No Pricelist ! : ") + warn_msg +"\n\n"
991 price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist],
992 product, qty or 1.0, partner_id, {
993 'uom': uom or result.get('product_uom'),
997 warn_msg = _("Cannot find a pricelist line matching this product and quantity.\n"
998 "You have to change either the product, the quantity or the pricelist.")
1000 warning_msgs += _("No valid pricelist line found ! :") + warn_msg +"\n\n"
1002 result.update({'price_unit': price})
1005 'title': _('Configuration Error!'),
1006 'message' : warning_msgs
1008 return {'value': result, 'domain': domain, 'warning': warning}
1010 def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
1011 uom=False, qty_uos=0, uos=False, name='', partner_id=False,
1012 lang=False, update_tax=True, date_order=False, context=None):
1013 context = context or {}
1014 lang = lang or ('lang' in context and context['lang'])
1016 return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
1017 return self.product_id_change(cursor, user, ids, pricelist, product,
1018 qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
1019 partner_id=partner_id, lang=lang, update_tax=update_tax,
1020 date_order=date_order, context=context)
1022 def unlink(self, cr, uid, ids, context=None):
1025 """Allows to delete sales order lines in draft,cancel states"""
1026 for rec in self.browse(cr, uid, ids, context=context):
1027 if rec.state not in ['draft', 'cancel']:
1028 raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a sales order line which is in state \'%s\'.') %(rec.state,))
1029 return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
1032 class mail_compose_message(osv.osv):
1033 _inherit = 'mail.compose.message'
1034 def send_mail(self, cr, uid, ids, context=None):
1035 context = context or {}
1036 if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
1037 wf_service = netsvc.LocalService("workflow")
1038 wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr)
1039 return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
1041 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: