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 ##############################################################################
23 from lxml import etree
24 import decimal_precision as dp
28 from osv import fields, osv, orm
29 from tools.translate import _
31 class account_invoice(osv.osv):
32 def _amount_all(self, cr, uid, ids, name, args, context=None):
34 for invoice in self.browse(cr, uid, ids, context=context):
36 'amount_untaxed': 0.0,
40 for line in invoice.invoice_line:
41 res[invoice.id]['amount_untaxed'] += line.price_subtotal
42 for line in invoice.tax_line:
43 res[invoice.id]['amount_tax'] += line.amount
44 res[invoice.id]['amount_total'] = res[invoice.id]['amount_tax'] + res[invoice.id]['amount_untaxed']
47 def _get_journal(self, cr, uid, context=None):
50 type_inv = context.get('type', 'out_invoice')
51 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
52 company_id = context.get('company_id', user.company_id.id)
53 type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale_refund', 'in_refund': 'purchase_refund'}
54 journal_obj = self.pool.get('account.journal')
55 res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale')),
56 ('company_id', '=', company_id)],
58 return res and res[0] or False
60 def _get_currency(self, cr, uid, context=None):
62 journal_id = self._get_journal(cr, uid, context=context)
64 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
65 res = journal.currency and journal.currency.id or journal.company_id.currency_id.id
68 def _get_journal_analytic(self, cr, uid, type_inv, context=None):
69 type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale', 'in_refund': 'purchase'}
70 tt = type2journal.get(type_inv, 'sale')
71 result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context)
73 raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s'!") % (tt,))
76 def _get_type(self, cr, uid, context=None):
79 return context.get('type', 'out_invoice')
81 def _reconciled(self, cr, uid, ids, name, args, context=None):
84 res[id] = self.test_paid(cr, uid, [id])
87 def _get_reference_type(self, cr, uid, context=None):
88 return [('none', _('Free Reference'))]
90 def _amount_residual(self, cr, uid, ids, name, args, context=None):
92 for invoice in self.browse(cr, uid, ids, context=context):
93 result[invoice.id] = 0.0
95 for m in invoice.move_id.line_id:
96 if m.account_id.type in ('receivable','payable'):
97 result[invoice.id] += m.amount_residual_currency
100 # Give Journal Items related to the payment reconciled to this invoice
101 # Return ids of partial and total payments related to the selected invoices
102 def _get_lines(self, cr, uid, ids, name, arg, context=None):
104 for invoice in self.browse(cr, uid, ids, context=context):
107 if not invoice.move_id:
109 data_lines = [x for x in invoice.move_id.line_id if x.account_id.id == invoice.account_id.id]
111 for line in data_lines:
113 if line.reconcile_id:
114 ids_line = line.reconcile_id.line_id
115 elif line.reconcile_partial_id:
116 ids_line = line.reconcile_partial_id.line_partial_ids
117 l = map(lambda x: x.id, ids_line)
118 partial_ids.append(line.id)
119 res[id] =[x for x in l if x <> line.id and x not in partial_ids]
122 def _get_invoice_line(self, cr, uid, ids, context=None):
124 for line in self.pool.get('account.invoice.line').browse(cr, uid, ids, context=context):
125 result[line.invoice_id.id] = True
128 def _get_invoice_tax(self, cr, uid, ids, context=None):
130 for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
131 result[tax.invoice_id.id] = True
134 def _compute_lines(self, cr, uid, ids, name, args, context=None):
136 for invoice in self.browse(cr, uid, ids, context=context):
140 for m in invoice.move_id.line_id:
143 temp_lines = map(lambda x: x.id, m.reconcile_id.line_id)
144 elif m.reconcile_partial_id:
145 temp_lines = map(lambda x: x.id, m.reconcile_partial_id.line_partial_ids)
146 lines += [x for x in temp_lines if x not in lines]
149 lines = filter(lambda x: x not in src, lines)
150 result[invoice.id] = lines
153 def _get_invoice_from_line(self, cr, uid, ids, context=None):
155 for line in self.pool.get('account.move.line').browse(cr, uid, ids, context=context):
156 if line.reconcile_partial_id:
157 for line2 in line.reconcile_partial_id.line_partial_ids:
158 move[line2.move_id.id] = True
159 if line.reconcile_id:
160 for line2 in line.reconcile_id.line_id:
161 move[line2.move_id.id] = True
164 invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
167 def _get_invoice_from_reconcile(self, cr, uid, ids, context=None):
169 for r in self.pool.get('account.move.reconcile').browse(cr, uid, ids, context=context):
170 for line in r.line_partial_ids:
171 move[line.move_id.id] = True
172 for line in r.line_id:
173 move[line.move_id.id] = True
177 invoice_ids = self.pool.get('account.invoice').search(cr, uid, [('move_id','in',move.keys())], context=context)
180 _name = "account.invoice"
181 _inherit = ['mail.thread']
182 _description = 'Invoice'
186 'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
187 'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
188 'type': fields.selection([
189 ('out_invoice','Customer Invoice'),
190 ('in_invoice','Supplier Invoice'),
191 ('out_refund','Customer Refund'),
192 ('in_refund','Supplier Refund'),
193 ],'Type', readonly=True, select=True, change_default=True),
195 'number': fields.related('move_id','name', type='char', readonly=True, size=64, relation='account.move', store=True, string='Number'),
196 'internal_number': fields.char('Invoice Number', size=32, readonly=True, help="Unique number of the invoice, computed automatically when the invoice is created."),
197 'reference': fields.char('Invoice Reference', size=64, help="The partner reference of this invoice."),
198 'reference_type': fields.selection(_get_reference_type, 'Payment Reference',
199 required=True, readonly=True, states={'draft':[('readonly',False)]}),
200 'comment': fields.text('Additional Information'),
202 'state': fields.selection([
204 ('proforma','Pro-forma'),
205 ('proforma2','Pro-forma'),
208 ('cancel','Cancelled'),
209 ],'State', select=True, readonly=True,
210 help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
211 \n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
212 \n* The \'Open\' state is used when user create invoice,a invoice number is generated.Its in open state till user does not pay invoice. \
213 \n* The \'Paid\' state is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled. \
214 \n* The \'Cancelled\' state is used when user cancel invoice.'),
215 'sent': fields.boolean('Sent', readonly=True, help="It indicates that the invoice has been sent."),
216 'date_invoice': fields.date('Invoice Date', readonly=True, states={'draft':[('readonly',False)]}, select=True, help="Keep empty to use the current date"),
217 'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, select=True,
218 help="If you use payment terms, the due date will be computed automatically at the generation "\
219 "of accounting entries. If you want to force a due date, make sure that the payment term is not set on the invoice. If you keep the payment term and the due date empty, it means direct payment."),
220 'partner_id': fields.many2one('res.partner', 'Partner', change_default=True, readonly=True, required=True, states={'draft':[('readonly',False)]}),
221 'payment_term': fields.many2one('account.payment.term', 'Payment Term',readonly=True, states={'draft':[('readonly',False)]},
222 help="If you use payment terms, the due date will be computed automatically at the generation "\
223 "of accounting entries. If you keep the payment term and the due date empty, it means direct payment. "\
224 "The payment term may compute several due dates, for example 50% now, 50% in one month."),
225 'period_id': fields.many2one('account.period', 'Force Period', domain=[('state','<>','done')], help="Keep empty to use the period of the validation(invoice) date.", readonly=True, states={'draft':[('readonly',False)]}),
227 'account_id': fields.many2one('account.account', 'Account', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The partner account used for this invoice."),
228 'invoice_line': fields.one2many('account.invoice.line', 'invoice_id', 'Invoice Lines', readonly=True, states={'draft':[('readonly',False)]}),
229 'tax_line': fields.one2many('account.invoice.tax', 'invoice_id', 'Tax Lines', readonly=True, states={'draft':[('readonly',False)]}),
231 'move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, select=1, ondelete='restrict', help="Link to the automatically generated Journal Items."),
232 'amount_untaxed': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Untaxed',
234 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
235 'account.invoice.tax': (_get_invoice_tax, None, 20),
236 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
239 'amount_tax': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Tax',
241 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
242 'account.invoice.tax': (_get_invoice_tax, None, 20),
243 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
246 'amount_total': fields.function(_amount_all, digits_compute=dp.get_precision('Account'), string='Total',
248 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line'], 20),
249 'account.invoice.tax': (_get_invoice_tax, None, 20),
250 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 20),
253 'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}),
254 'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
255 'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
256 'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), states={'open':[('readonly',True)],'close':[('readonly',True)]}),
257 'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',
259 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, None, 50), # Check if we can remove ?
260 'account.move.line': (_get_invoice_from_line, None, 50),
261 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
262 }, help="It indicates that the invoice has been paid and the journal entry of the invoice has been reconciled with one or several journal entries of payment."),
263 'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
264 help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Supplier Refund, otherwise a Partner bank account number.', readonly=True, states={'draft':[('readonly',False)]}),
265 'move_lines':fields.function(_get_lines, type='many2many', relation='account.move.line', string='Entry Lines'),
266 'residual': fields.function(_amount_residual, digits_compute=dp.get_precision('Account'), string='Balance',
268 'account.invoice': (lambda self, cr, uid, ids, c={}: ids, ['invoice_line','move_id'], 50),
269 'account.invoice.tax': (_get_invoice_tax, None, 50),
270 'account.invoice.line': (_get_invoice_line, ['price_unit','invoice_line_tax_id','quantity','discount','invoice_id'], 50),
271 'account.move.line': (_get_invoice_from_line, None, 50),
272 'account.move.reconcile': (_get_invoice_from_reconcile, None, 50),
274 help="Remaining amount due."),
275 'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
276 'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
277 'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, states={'draft':[('readonly',False)]}),
278 'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
283 'journal_id': _get_journal,
284 'currency_id': _get_currency,
285 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.invoice', context=c),
286 'reference_type': 'none',
288 'internal_number': False,
289 'user_id': lambda s, cr, u, c: u,
293 ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
296 def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
297 journal_obj = self.pool.get('account.journal')
301 if context.get('active_model', '') in ['res.partner'] and context.get('active_ids', False) and context['active_ids']:
302 partner = self.pool.get(context['active_model']).read(cr, uid, context['active_ids'], ['supplier','customer'])[0]
304 view_id = self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'account.invoice.tree')])
306 if view_type == 'form':
307 if partner['supplier'] and not partner['customer']:
308 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.supplier.form')])
310 view_id = self.pool.get('ir.ui.view').search(cr,uid,[('name', '=', 'account.invoice.form')])
311 if view_id and isinstance(view_id, (list, tuple)):
313 res = super(account_invoice,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
315 type = context.get('journal_type', False)
316 for field in res['fields']:
317 if field == 'journal_id' and type:
318 journal_select = journal_obj._name_search(cr, uid, '', [('type', '=', type)], context=context, limit=None, name_get_uid=1)
319 res['fields'][field]['selection'] = journal_select
321 doc = etree.XML(res['arch'])
323 if context.get('type', False):
324 for node in doc.xpath("//field[@name='partner_bank_id']"):
325 if context['type'] == 'in_refund':
326 node.set('domain', "[('partner_id.ref_companies', 'in', [company_id])]")
327 elif context['type'] == 'out_refund':
328 node.set('domain', "[('partner_id', '=', partner_id)]")
329 res['arch'] = etree.tostring(doc)
331 if view_type == 'search':
332 if context.get('type', 'in_invoice') in ('out_invoice', 'out_refund'):
333 for node in doc.xpath("//group[@name='extended filter']"):
335 res['arch'] = etree.tostring(doc)
337 if view_type == 'tree':
338 partner_string = _('Customer')
339 if context.get('type', 'out_invoice') in ('in_invoice', 'in_refund'):
340 partner_string = _('Supplier')
341 for node in doc.xpath("//field[@name='reference']"):
342 node.set('invisible', '0')
343 for node in doc.xpath("//field[@name='partner_id']"):
344 node.set('string', partner_string)
345 res['arch'] = etree.tostring(doc)
348 def get_log_context(self, cr, uid, context=None):
351 res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form')
352 view_id = res and res[1] or False
353 context['view_id'] = view_id
356 def create(self, cr, uid, vals, context=None):
360 res = super(account_invoice, self).create(cr, uid, vals, context)
362 self.create_send_note(cr, uid, [res], context=context)
365 if '"journal_id" viol' in e.args[0]:
366 raise orm.except_orm(_('Configuration Error!'),
367 _('There is no Accounting Journal of type Sale/Purchase defined!'))
369 raise orm.except_orm(_('Unknown Error'), str(e))
371 def invoice_print(self, cr, uid, ids, context=None):
373 This function prints the invoice and mark it as sent, so that we can see more easily the next step of the workflow
375 assert len(ids) == 1, 'This option should only be used for a single id at a time'
376 self.write(cr, uid, ids, {'sent': True}, context=context)
379 'model': 'account.invoice',
380 'form': self.read(cr, uid, ids[0], context=context)
383 'type': 'ir.actions.report.xml',
384 'report_name': 'account.invoice',
389 def action_invoice_sent(self, cr, uid, ids, context=None):
391 This function opens a window to compose an email, with the edi invoice template message loaded by default
393 mod_obj = self.pool.get('ir.model.data')
394 template = mod_obj.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')
395 template_id = template and template[1] or False
396 res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
397 res_id = res and res[1] or False
398 ctx = dict(context, active_model='account.invoice', active_id=ids[0])
399 ctx.update({'mail.compose.template_id': template_id})
403 'res_model': 'mail.compose.message',
404 'views': [(res_id, 'form')],
406 'type': 'ir.actions.act_window',
412 def confirm_paid(self, cr, uid, ids, context=None):
415 self.write(cr, uid, ids, {'state':'paid'}, context=context)
416 self.confirm_paid_send_note(cr, uid, ids, context=context)
419 def unlink(self, cr, uid, ids, context=None):
422 invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context)
425 if t['state'] in ('draft', 'cancel') and t['internal_number']== False:
426 unlink_ids.append(t['id'])
428 raise osv.except_osv(_('Invalid action !'), _('You can not delete an invoice which is open or paid. We suggest you to refund it instead.'))
429 osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
432 def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
433 date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
434 invoice_addr_id = False
435 partner_payment_term = False
438 fiscal_position = False
440 opt = [('uid', str(uid))]
443 opt.insert(0, ('id', partner_id))
444 res = self.pool.get('res.partner').address_get(cr, uid, [partner_id], ['invoice'])
445 invoice_addr_id = res['invoice']
446 p = self.pool.get('res.partner').browse(cr, uid, partner_id)
448 if p.property_account_receivable.company_id.id != company_id and p.property_account_payable.company_id.id != company_id:
449 property_obj = self.pool.get('ir.property')
450 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
451 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('res_id','=','res.partner,'+str(partner_id)+''),('company_id','=',company_id)])
453 rec_pro_id = property_obj.search(cr,uid,[('name','=','property_account_receivable'),('company_id','=',company_id)])
455 pay_pro_id = property_obj.search(cr,uid,[('name','=','property_account_payable'),('company_id','=',company_id)])
456 rec_line_data = property_obj.read(cr,uid,rec_pro_id,['name','value_reference','res_id'])
457 pay_line_data = property_obj.read(cr,uid,pay_pro_id,['name','value_reference','res_id'])
458 rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
459 pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
460 if not rec_res_id and not pay_res_id:
461 raise osv.except_osv(_('Configuration Error !'),
462 _('Can not find a chart of accounts for this company, you should create one.'))
463 account_obj = self.pool.get('account.account')
464 rec_obj_acc = account_obj.browse(cr, uid, [rec_res_id])
465 pay_obj_acc = account_obj.browse(cr, uid, [pay_res_id])
466 p.property_account_receivable = rec_obj_acc[0]
467 p.property_account_payable = pay_obj_acc[0]
469 if type in ('out_invoice', 'out_refund'):
470 acc_id = p.property_account_receivable.id
471 partner_payment_term = p.property_payment_term and p.property_payment_term.id or False
473 acc_id = p.property_account_payable.id
474 partner_payment_term = p.property_supplier_payment_term and p.property_supplier_payment_term.id or False
475 fiscal_position = p.property_account_position and p.property_account_position.id or False
477 bank_id = p.bank_ids[0].id
480 'account_id': acc_id,
481 'payment_term': partner_payment_term,
482 'fiscal_position': fiscal_position
486 if type in ('in_invoice', 'in_refund'):
487 result['value']['partner_bank_id'] = bank_id
489 if payment_term != partner_payment_term:
490 if partner_payment_term:
491 to_update = self.onchange_payment_term_date_invoice(
492 cr, uid, ids, partner_payment_term, date_invoice)
493 result['value'].update(to_update['value'])
495 result['value']['date_due'] = False
497 if partner_bank_id != bank_id:
498 to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
499 result['value'].update(to_update['value'])
502 def onchange_journal_id(self, cr, uid, ids, journal_id=False, context=None):
505 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
506 currency_id = journal.currency and journal.currency.id or journal.company_id.currency_id.id
508 'currency_id': currency_id,
513 def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice):
515 if not payment_term_id:
518 date_invoice = time.strftime('%Y-%m-%d')
519 pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice)
521 pterm_list = [line[0] for line in pterm_list]
523 res = {'value':{'date_due': pterm_list[-1]}}
525 raise osv.except_osv(_('Data Insufficient !'), _('The payment term of supplier does not have a payment term line!'))
528 def onchange_invoice_line(self, cr, uid, ids, lines):
531 def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
534 def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
537 obj_journal = self.pool.get('account.journal')
538 account_obj = self.pool.get('account.account')
539 inv_line_obj = self.pool.get('account.invoice.line')
540 if company_id and part_id and type:
542 partner_obj = self.pool.get('res.partner').browse(cr,uid,part_id)
543 if partner_obj.property_account_payable and partner_obj.property_account_receivable:
544 if partner_obj.property_account_payable.company_id.id != company_id and partner_obj.property_account_receivable.company_id.id != company_id:
545 property_obj = self.pool.get('ir.property')
546 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
547 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('res_id','=','res.partner,'+str(part_id)+''),('company_id','=',company_id)])
549 rec_pro_id = property_obj.search(cr, uid, [('name','=','property_account_receivable'),('company_id','=',company_id)])
551 pay_pro_id = property_obj.search(cr, uid, [('name','=','property_account_payable'),('company_id','=',company_id)])
552 rec_line_data = property_obj.read(cr, uid, rec_pro_id, ['name','value_reference','res_id'])
553 pay_line_data = property_obj.read(cr, uid, pay_pro_id, ['name','value_reference','res_id'])
554 rec_res_id = rec_line_data and rec_line_data[0].get('value_reference',False) and int(rec_line_data[0]['value_reference'].split(',')[1]) or False
555 pay_res_id = pay_line_data and pay_line_data[0].get('value_reference',False) and int(pay_line_data[0]['value_reference'].split(',')[1]) or False
556 if not rec_res_id and not pay_res_id:
557 raise osv.except_osv(_('Configuration Error !'),
558 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
559 if type in ('out_invoice', 'out_refund'):
563 val= {'account_id': acc_id}
566 inv_obj = self.browse(cr,uid,ids)
567 for line in inv_obj[0].invoice_line:
569 if line.account_id.company_id.id != company_id:
570 result_id = account_obj.search(cr, uid, [('name','=',line.account_id.name),('company_id','=',company_id)])
572 raise osv.except_osv(_('Configuration Error !'),
573 _('Can not find a chart of account, you should create one from the configuration of the accounting menu.'))
574 inv_line_obj.write(cr, uid, [line.id], {'account_id': result_id[-1]})
577 for inv_line in invoice_line:
578 obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id'])
579 if obj_l.company_id.id != company_id:
580 raise osv.except_osv(_('Configuration Error !'),
581 _('Invoice line account company does not match with invoice company.'))
584 if company_id and type:
585 if type in ('out_invoice'):
586 journal_type = 'sale'
587 elif type in ('out_refund'):
588 journal_type = 'sale_refund'
589 elif type in ('in_refund'):
590 journal_type = 'purchase_refund'
592 journal_type = 'purchase'
593 journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)])
595 val['journal_id'] = journal_ids[0]
596 ir_values_obj = self.pool.get('ir.values')
597 res_journal_default = ir_values_obj.get(cr, uid, 'default', 'type=%s' % (type), ['account.invoice'])
598 for r in res_journal_default:
599 if r[1] == 'journal_id' and r[2] in journal_ids:
600 val['journal_id'] = r[2]
601 if not val.get('journal_id', False):
602 raise osv.except_osv(_('Configuration Error !'), (_('Can\'t find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Financial Accounting\Accounts\Journals.') % (journal_type)))
603 dom = {'journal_id': [('id', 'in', journal_ids)]}
605 journal_ids = obj_journal.search(cr, uid, [])
607 return {'value': val, 'domain': dom}
609 # go from canceled state to draft state
610 def action_cancel_draft(self, cr, uid, ids, *args):
611 self.write(cr, uid, ids, {'state':'draft'})
612 wf_service = netsvc.LocalService("workflow")
614 wf_service.trg_delete(uid, 'account.invoice', inv_id, cr)
615 wf_service.trg_create(uid, 'account.invoice', inv_id, cr)
621 # return the ids of the move lines which has the same account than the invoice
623 def move_line_id_payment_get(self, cr, uid, ids, *args):
624 if not ids: return []
625 result = self.move_line_id_payment_gets(cr, uid, ids, *args)
626 return result.get(ids[0], [])
628 def move_line_id_payment_gets(self, cr, uid, ids, *args):
630 if not ids: return res
631 cr.execute('SELECT i.id, l.id '\
632 'FROM account_move_line l '\
633 'LEFT JOIN account_invoice i ON (i.move_id=l.move_id) '\
635 'AND l.account_id=i.account_id',
637 for r in cr.fetchall():
638 res.setdefault(r[0], [])
639 res[r[0]].append( r[1] )
642 def copy(self, cr, uid, id, default=None, context=None):
643 default = default or {}
649 'internal_number': False,
653 if 'date_invoice' not in default:
657 if 'date_due' not in default:
661 return super(account_invoice, self).copy(cr, uid, id, default, context)
663 def test_paid(self, cr, uid, ids, *args):
664 res = self.move_line_id_payment_get(cr, uid, ids)
669 cr.execute('select reconcile_id from account_move_line where id=%s', (id,))
670 ok = ok and bool(cr.fetchone()[0])
673 def button_reset_taxes(self, cr, uid, ids, context=None):
677 ait_obj = self.pool.get('account.invoice.tax')
679 cr.execute("DELETE FROM account_invoice_tax WHERE invoice_id=%s AND manual is False", (id,))
680 partner = self.browse(cr, uid, id, context=ctx).partner_id
682 ctx.update({'lang': partner.lang})
683 for taxe in ait_obj.compute(cr, uid, id, context=ctx).values():
684 ait_obj.create(cr, uid, taxe)
685 # Update the stored value (fields.function), so we write to trigger recompute
686 self.pool.get('account.invoice').write(cr, uid, ids, {'invoice_line':[]}, context=ctx)
689 def button_compute(self, cr, uid, ids, context=None, set_total=False):
690 self.button_reset_taxes(cr, uid, ids, context)
691 for inv in self.browse(cr, uid, ids, context=context):
693 self.pool.get('account.invoice').write(cr, uid, [inv.id], {'check_total': inv.amount_total})
696 def _convert_ref(self, cr, uid, ref):
697 return (ref or '').replace('/','')
699 def _get_analytic_lines(self, cr, uid, id, context=None):
702 inv = self.browse(cr, uid, id)
703 cur_obj = self.pool.get('res.currency')
705 company_currency = inv.company_id.currency_id.id
706 if inv.type in ('out_invoice', 'in_refund'):
711 iml = self.pool.get('account.invoice.line').move_line_get(cr, uid, inv.id, context=context)
713 if il['account_analytic_id']:
714 if inv.type in ('in_invoice', 'in_refund'):
717 ref = self._convert_ref(cr, uid, inv.number)
718 if not inv.journal_id.analytic_journal_id:
719 raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,))
720 il['analytic_lines'] = [(0,0, {
722 'date': inv['date_invoice'],
723 'account_id': il['account_analytic_id'],
724 'unit_amount': il['quantity'],
725 'amount': cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, il['price'], context={'date': inv.date_invoice}) * sign,
726 'product_id': il['product_id'],
727 'product_uom_id': il['uos_id'],
728 'general_account_id': il['account_id'],
729 'journal_id': inv.journal_id.analytic_journal_id.id,
734 def action_date_assign(self, cr, uid, ids, *args):
735 for inv in self.browse(cr, uid, ids):
736 res = self.onchange_payment_term_date_invoice(cr, uid, inv.id, inv.payment_term.id, inv.date_invoice)
737 if res and res['value']:
738 self.write(cr, uid, [inv.id], res['value'])
741 def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
742 """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
743 Hook method to be overridden in additional modules to verify and possibly alter the
744 move lines to be created by an invoice, for special cases.
745 :param invoice_browse: browsable record of the invoice that is generating the move lines
746 :param move_lines: list of dictionaries with the account.move.lines (as for create())
747 :return: the (possibly updated) final move_lines to create for this invoice
751 def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj):
753 for tax in compute_taxes.values():
754 ait_obj.create(cr, uid, tax)
757 for tax in inv.tax_line:
760 key = (tax.tax_code_id.id, tax.base_code_id.id, tax.account_id.id, tax.account_analytic_id.id)
762 if not key in compute_taxes:
763 raise osv.except_osv(_('Warning !'), _('Global taxes defined, but they are not in invoice lines !'))
764 base = compute_taxes[key]['base']
765 if abs(base - tax.base) > inv.company_id.currency_id.rounding:
766 raise osv.except_osv(_('Warning !'), _('Tax base different!\nClick on compute to update the tax base.'))
767 for key in compute_taxes:
768 if not key in tax_key:
769 raise osv.except_osv(_('Warning !'), _('Taxes are missing!\nClick on compute button.'))
771 def compute_invoice_totals(self, cr, uid, inv, company_currency, ref, invoice_move_lines):
774 cur_obj = self.pool.get('res.currency')
775 for i in invoice_move_lines:
776 if inv.currency_id.id != company_currency:
777 i['currency_id'] = inv.currency_id.id
778 i['amount_currency'] = i['price']
779 i['price'] = cur_obj.compute(cr, uid, inv.currency_id.id,
780 company_currency, i['price'],
781 context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')})
783 i['amount_currency'] = False
784 i['currency_id'] = False
786 if inv.type in ('out_invoice','in_refund'):
788 total_currency += i['amount_currency'] or i['price']
789 i['price'] = - i['price']
792 total_currency -= i['amount_currency'] or i['price']
793 return total, total_currency, invoice_move_lines
795 def inv_line_characteristic_hashcode(self, invoice, invoice_line):
796 """Overridable hashcode generation for invoice lines. Lines having the same hashcode
797 will be grouped together if the journal has the 'group line' option. Of course a module
798 can add fields to invoice lines that would need to be tested too before merging lines
800 return "%s-%s-%s-%s-%s"%(
801 invoice_line['account_id'],
802 invoice_line.get('tax_code_id',"False"),
803 invoice_line.get('product_id',"False"),
804 invoice_line.get('analytic_account_id',"False"),
805 invoice_line.get('date_maturity',"False"))
807 def group_lines(self, cr, uid, iml, line, inv):
808 """Merge account move lines (and hence analytic lines) if invoice line hashcodes are equals"""
809 if inv.journal_id.group_invoice_lines:
812 tmp = self.inv_line_characteristic_hashcode(inv, l)
815 am = line2[tmp]['debit'] - line2[tmp]['credit'] + (l['debit'] - l['credit'])
816 line2[tmp]['debit'] = (am > 0) and am or 0.0
817 line2[tmp]['credit'] = (am < 0) and -am or 0.0
818 line2[tmp]['tax_amount'] += l['tax_amount']
819 line2[tmp]['analytic_lines'] += l['analytic_lines']
823 for key, val in line2.items():
824 line.append((0,0,val))
827 def action_move_create(self, cr, uid, ids, context=None):
828 """Creates invoice related analytics and financial move lines"""
829 ait_obj = self.pool.get('account.invoice.tax')
830 cur_obj = self.pool.get('res.currency')
831 period_obj = self.pool.get('account.period')
832 payment_term_obj = self.pool.get('account.payment.term')
833 journal_obj = self.pool.get('account.journal')
834 move_obj = self.pool.get('account.move')
837 for inv in self.browse(cr, uid, ids, context=context):
838 if not inv.journal_id.sequence_id:
839 raise osv.except_osv(_('Error !'), _('Please define sequence on the journal related to this invoice.'))
840 if not inv.invoice_line:
841 raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.'))
846 ctx.update({'lang': inv.partner_id.lang})
847 if not inv.date_invoice:
848 self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx)
849 company_currency = inv.company_id.currency_id.id
850 # create the analytical lines
851 # one move line per invoice line
852 iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx)
853 # check if taxes are all computed
854 compute_taxes = ait_obj.compute(cr, uid, inv.id, context=ctx)
855 self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
857 # I disabled the check_total feature
858 #if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0):
859 # raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.'))
862 total_fixed = total_percent = 0
863 for line in inv.payment_term.line_ids:
864 if line.value == 'fixed':
865 total_fixed += line.value_amount
866 if line.value == 'procent':
867 total_percent += line.value_amount
868 total_fixed = (total_fixed * 100) / (inv.amount_total or 1.0)
869 if (total_fixed + total_percent) > 100:
870 raise osv.except_osv(_('Error !'), _("Can not create the invoice !\nThe related payment term is probably misconfigured as it gives a computed amount greater than the total invoiced amount. The latest line of your payment term must be of type 'balance' to avoid rounding issues."))
872 # one move line per tax line
873 iml += ait_obj.move_line_get(cr, uid, inv.id)
876 if inv.type in ('in_invoice', 'in_refund'):
878 entry_type = 'journal_pur_voucher'
879 if inv.type == 'in_refund':
880 entry_type = 'cont_voucher'
882 ref = self._convert_ref(cr, uid, inv.number)
883 entry_type = 'journal_sale_vou'
884 if inv.type == 'out_refund':
885 entry_type = 'cont_voucher'
887 diff_currency_p = inv.currency_id.id <> company_currency
888 # create one move line for the total and possibly adjust the other lines amount
891 total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml)
892 acc_id = inv.account_id.id
894 name = inv['name'] or '/'
897 totlines = payment_term_obj.compute(cr,
898 uid, inv.payment_term.id, total, inv.date_invoice or False, context=ctx)
900 res_amount_currency = total_currency
902 ctx.update({'date': inv.date_invoice})
904 if inv.currency_id.id != company_currency:
905 amount_currency = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, t[1], context=ctx)
907 amount_currency = False
909 # last line add the diff
910 res_amount_currency -= amount_currency or 0
912 if i == len(totlines):
913 amount_currency += res_amount_currency
919 'account_id': acc_id,
920 'date_maturity': t[0],
921 'amount_currency': diff_currency_p \
922 and amount_currency or False,
923 'currency_id': diff_currency_p \
924 and inv.currency_id.id or False,
932 'account_id': acc_id,
933 'date_maturity': inv.date_due or False,
934 'amount_currency': diff_currency_p \
935 and total_currency or False,
936 'currency_id': diff_currency_p \
937 and inv.currency_id.id or False,
941 date = inv.date_invoice or time.strftime('%Y-%m-%d')
942 part = inv.partner_id.id
944 line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part, date, context=ctx)),iml)
946 line = self.group_lines(cr, uid, iml, line, inv)
948 journal_id = inv.journal_id.id
949 journal = journal_obj.browse(cr, uid, journal_id, context=ctx)
950 if journal.centralisation:
951 raise osv.except_osv(_('UserError'),
952 _('You cannot create an invoice on a centralised journal. Uncheck the centralised counterpart box in the related journal from the configuration menu.'))
954 line = self.finalize_invoice_move_lines(cr, uid, inv, line)
957 'ref': inv.reference and inv.reference or inv.name,
959 'journal_id': journal_id,
961 'narration':inv.comment
963 period_id = inv.period_id and inv.period_id.id or False
964 ctx.update({'company_id': inv.company_id.id})
966 period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
967 period_id = period_ids and period_ids[0] or False
969 move['period_id'] = period_id
971 i[2]['period_id'] = period_id
973 move_id = move_obj.create(cr, uid, move, context=ctx)
974 new_move_name = move_obj.browse(cr, uid, move_id, context=ctx).name
975 # make the invoice point to that move
976 self.write(cr, uid, [inv.id], {'move_id': move_id,'period_id':period_id, 'move_name':new_move_name}, context=ctx)
977 # Pass invoice in context in method post: used if you want to get the same
978 # account move reference when creating the same invoice after a cancelled one:
979 ctx.update({'invoice':inv})
980 move_obj.post(cr, uid, [move_id], context=ctx)
981 self._log_event(cr, uid, ids)
984 def invoice_validate(self, cr, uid, ids, context=None):
985 self.write(cr, uid, ids, {'state':'open'}, context=context)
988 def line_get_convert(self, cr, uid, x, part, date, context=None):
990 'date_maturity': x.get('date_maturity', False),
992 'name': x['name'][:64],
994 'debit': x['price']>0 and x['price'],
995 'credit': x['price']<0 and -x['price'],
996 'account_id': x['account_id'],
997 'analytic_lines': x.get('analytic_lines', []),
998 'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
999 'currency_id': x.get('currency_id', False),
1000 'tax_code_id': x.get('tax_code_id', False),
1001 'tax_amount': x.get('tax_amount', False),
1002 'ref': x.get('ref', False),
1003 'quantity': x.get('quantity',1.00),
1004 'product_id': x.get('product_id', False),
1005 'product_uom_id': x.get('uos_id', False),
1006 'analytic_account_id': x.get('account_analytic_id', False),
1009 def action_number(self, cr, uid, ids, context=None):
1012 #TODO: not correct fix but required a frech values before reading it.
1013 self.write(cr, uid, ids, {})
1015 for obj_inv in self.browse(cr, uid, ids, context=context):
1017 invtype = obj_inv.type
1018 number = obj_inv.number
1019 move_id = obj_inv.move_id and obj_inv.move_id.id or False
1020 reference = obj_inv.reference or ''
1022 self.write(cr, uid, ids, {'internal_number':number})
1024 if invtype in ('in_invoice', 'in_refund'):
1026 ref = self._convert_ref(cr, uid, number)
1030 ref = self._convert_ref(cr, uid, number)
1032 cr.execute('UPDATE account_move SET ref=%s ' \
1033 'WHERE id=%s AND (ref is null OR ref = \'\')',
1035 cr.execute('UPDATE account_move_line SET ref=%s ' \
1036 'WHERE move_id=%s AND (ref is null OR ref = \'\')',
1038 cr.execute('UPDATE account_analytic_line SET ref=%s ' \
1039 'FROM account_move_line ' \
1040 'WHERE account_move_line.move_id = %s ' \
1041 'AND account_analytic_line.move_id = account_move_line.id',
1044 for inv_id, name in self.name_get(cr, uid, [id]):
1045 ctx = context.copy()
1046 if obj_inv.type in ('out_invoice', 'out_refund'):
1047 ctx = self.get_log_context(cr, uid, context=ctx)
1048 message = _("Invoice '%s' is validated.") % name
1049 self.message_append_note(cr, uid, [inv_id], body=message, context=context)
1052 def action_cancel(self, cr, uid, ids, *args):
1053 context = {} # TODO: Use context from arguments
1054 account_move_obj = self.pool.get('account.move')
1055 invoices = self.read(cr, uid, ids, ['move_id', 'payment_ids'])
1056 move_ids = [] # ones that we will need to remove
1059 move_ids.append(i['move_id'][0])
1060 if i['payment_ids']:
1061 account_move_line_obj = self.pool.get('account.move.line')
1062 pay_ids = account_move_line_obj.browse(cr, uid, i['payment_ids'])
1063 for move_line in pay_ids:
1064 if move_line.reconcile_partial_id and move_line.reconcile_partial_id.line_partial_ids:
1065 raise osv.except_osv(_('Error !'), _('You can not cancel an invoice which is partially paid! You need to unreconcile related payment entries first!'))
1067 # First, set the invoices as cancelled and detach the move ids
1068 self.write(cr, uid, ids, {'state':'cancel', 'move_id':False})
1070 # second, invalidate the move(s)
1071 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
1072 # delete the move this invoice was pointing to
1073 # Note that the corresponding move_lines and move_reconciles
1074 # will be automatically deleted too
1075 account_move_obj.unlink(cr, uid, move_ids, context=context)
1076 self._log_event(cr, uid, ids, -1.0, 'Cancel Invoice')
1077 self.invoice_cancel_send_note(cr, uid, ids, context=context)
1082 def list_distinct_taxes(self, cr, uid, ids):
1083 invoices = self.browse(cr, uid, ids)
1085 for inv in invoices:
1086 for tax in inv.tax_line:
1087 if not tax['name'] in taxes:
1088 taxes[tax['name']] = {'name': tax['name']}
1089 return taxes.values()
1091 def _log_event(self, cr, uid, ids, factor=1.0, name='Open Invoice'):
1092 #TODO: implement messages system
1095 def name_get(self, cr, uid, ids, context=None):
1099 'out_invoice': 'CI: ',
1100 'in_invoice': 'SI: ',
1101 'out_refund': 'OR: ',
1102 'in_refund': 'SR: ',
1104 return [(r['id'], (r['number']) or types[r['type']] + (r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')]
1106 def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
1113 ids = self.search(cr, user, [('number','=',name)] + args, limit=limit, context=context)
1115 ids = self.search(cr, user, [('name',operator,name)] + args, limit=limit, context=context)
1116 return self.name_get(cr, user, ids, context)
1118 def _refund_cleanup_lines(self, cr, uid, lines):
1121 del line['invoice_id']
1122 for field in ('company_id', 'partner_id', 'account_id', 'product_id',
1123 'uos_id', 'account_analytic_id', 'tax_code_id', 'base_code_id'):
1125 line[field] = line[field][0]
1126 if 'invoice_line_tax_id' in line:
1127 line['invoice_line_tax_id'] = [(6,0, line.get('invoice_line_tax_id', [])) ]
1128 return map(lambda x: (0,0,x), lines)
1130 def refund(self, cr, uid, ids, date=None, period_id=None, description=None, journal_id=None):
1131 invoices = self.read(cr, uid, ids, ['name', 'type', 'number', 'reference', 'comment', 'date_due', 'partner_id', 'partner_contact', 'partner_insite', 'partner_ref', 'payment_term', 'account_id', 'currency_id', 'invoice_line', 'tax_line', 'journal_id'])
1132 obj_invoice_line = self.pool.get('account.invoice.line')
1133 obj_invoice_tax = self.pool.get('account.invoice.tax')
1134 obj_journal = self.pool.get('account.journal')
1136 for invoice in invoices:
1140 'out_invoice': 'out_refund', # Customer Invoice
1141 'in_invoice': 'in_refund', # Supplier Invoice
1142 'out_refund': 'out_invoice', # Customer Refund
1143 'in_refund': 'in_invoice', # Supplier Refund
1146 invoice_lines = obj_invoice_line.read(cr, uid, invoice['invoice_line'])
1147 invoice_lines = self._refund_cleanup_lines(cr, uid, invoice_lines)
1149 tax_lines = obj_invoice_tax.read(cr, uid, invoice['tax_line'])
1150 tax_lines = filter(lambda l: l['manual'], tax_lines)
1151 tax_lines = self._refund_cleanup_lines(cr, uid, tax_lines)
1153 refund_journal_ids = [journal_id]
1154 elif invoice['type'] == 'in_invoice':
1155 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','purchase_refund')])
1157 refund_journal_ids = obj_journal.search(cr, uid, [('type','=','sale_refund')])
1160 date = time.strftime('%Y-%m-%d')
1162 'type': type_dict[invoice['type']],
1163 'date_invoice': date,
1166 'invoice_line': invoice_lines,
1167 'tax_line': tax_lines,
1168 'journal_id': refund_journal_ids
1172 'period_id': period_id,
1176 'name': description,
1178 # take the id part of the tuple returned for many2one fields
1179 for field in ('partner_id',
1180 'account_id', 'currency_id', 'payment_term', 'journal_id'):
1181 invoice[field] = invoice[field] and invoice[field][0]
1182 # create the new invoice
1183 new_ids.append(self.create(cr, uid, invoice))
1187 def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id, writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''):
1190 #TODO check if we can use different period for payment and the writeoff line
1191 assert len(ids)==1, "Can only pay one invoice at a time"
1192 invoice = self.browse(cr, uid, ids[0], context=context)
1193 src_account_id = invoice.account_id.id
1194 # Take the seq as name for move
1195 types = {'out_invoice': -1, 'in_invoice': 1, 'out_refund': 1, 'in_refund': -1}
1196 direction = types[invoice.type]
1197 #take the choosen date
1198 if 'date_p' in context and context['date_p']:
1199 date=context['date_p']
1201 date=time.strftime('%Y-%m-%d')
1203 # Take the amount in currency and the currency of the payment
1204 if 'amount_currency' in context and context['amount_currency'] and 'currency_id' in context and context['currency_id']:
1205 amount_currency = context['amount_currency']
1206 currency_id = context['currency_id']
1208 amount_currency = False
1211 pay_journal = self.pool.get('account.journal').read(cr, uid, pay_journal_id, ['type'], context=context)
1212 if invoice.type in ('in_invoice', 'out_invoice'):
1213 if pay_journal['type'] == 'bank':
1214 entry_type = 'bank_pay_voucher' # Bank payment
1216 entry_type = 'pay_voucher' # Cash payment
1218 entry_type = 'cont_voucher'
1219 if invoice.type in ('in_invoice', 'in_refund'):
1220 ref = invoice.reference
1222 ref = self._convert_ref(cr, uid, invoice.number)
1223 # Pay attention to the sign for both debit/credit AND amount_currency
1225 'debit': direction * pay_amount>0 and direction * pay_amount,
1226 'credit': direction * pay_amount<0 and - direction * pay_amount,
1227 'account_id': src_account_id,
1228 'partner_id': invoice.partner_id.id,
1231 'currency_id':currency_id,
1232 'amount_currency':amount_currency and direction * amount_currency or 0.0,
1233 'company_id': invoice.company_id.id,
1236 'debit': direction * pay_amount<0 and - direction * pay_amount,
1237 'credit': direction * pay_amount>0 and direction * pay_amount,
1238 'account_id': pay_account_id,
1239 'partner_id': invoice.partner_id.id,
1242 'currency_id':currency_id,
1243 'amount_currency':amount_currency and - direction * amount_currency or 0.0,
1244 'company_id': invoice.company_id.id,
1248 name = invoice.invoice_line and invoice.invoice_line[0].name or invoice.number
1252 lines = [(0, 0, l1), (0, 0, l2)]
1253 move = {'ref': ref, 'line_id': lines, 'journal_id': pay_journal_id, 'period_id': period_id, 'date': date}
1254 move_id = self.pool.get('account.move').create(cr, uid, move, context=context)
1258 line = self.pool.get('account.move.line')
1259 move_ids = [move_id,]
1261 move_ids.append(invoice.move_id.id)
1262 cr.execute('SELECT id FROM account_move_line '\
1263 'WHERE move_id IN %s',
1264 ((move_id, invoice.move_id.id),))
1265 lines = line.browse(cr, uid, map(lambda x: x[0], cr.fetchall()) )
1266 for l in lines+invoice.payment_ids:
1267 if l.account_id.id == src_account_id:
1268 line_ids.append(l.id)
1269 total += (l.debit or 0.0) - (l.credit or 0.0)
1271 inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
1272 if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
1273 self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
1275 code = invoice.currency_id.symbol
1276 # TODO: use currency's formatting function
1277 msg = _("Invoice '%s' is paid partially: %s%s of %s%s (%s%s remaining)") % \
1278 (name, pay_amount, code, invoice.amount_total, code, total, code)
1279 self.message_append_note(cr, uid, [inv_id], body=msg, context=context)
1280 self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
1282 # Update the stored value (fields.function), so we write to trigger recompute
1283 self.pool.get('account.invoice').write(cr, uid, ids, {}, context=context)
1286 # -----------------------------------------
1287 # OpenChatter notifications and need_action
1288 # -----------------------------------------
1290 def _get_document_type(self, type):
1292 'out_invoice': 'Customer invoice',
1293 'in_invoice': 'Supplier invoice',
1294 'out_refund': 'Customer Refund',
1295 'in_refund': 'Supplier Refund',
1297 return type_dict.get(type, 'Invoice')
1299 def create_send_note(self, cr, uid, ids, context=None):
1300 for obj in self.browse(cr, uid, ids, context=context):
1301 self.message_append_note(cr, uid, [obj.id],body=_("%s <b>created</b>.") % (self._get_document_type(obj.type)), context=context)
1303 def confirm_paid_send_note(self, cr, uid, ids, context=None):
1304 for obj in self.browse(cr, uid, ids, context=context):
1305 self.message_append_note(cr, uid, [obj.id], body=_("%s <b>paid</b>.") % (self._get_document_type(obj.type)), context=context)
1307 def invoice_cancel_send_note(self, cr, uid, ids, context=None):
1308 for obj in self.browse(cr, uid, ids, context=context):
1309 self.message_append_note(cr, uid, [obj.id], body=_("%s <b>cancelled</b>.") % (self._get_document_type(obj.type)), context=context)
1313 class account_invoice_line(osv.osv):
1315 def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict):
1317 tax_obj = self.pool.get('account.tax')
1318 cur_obj = self.pool.get('res.currency')
1319 for line in self.browse(cr, uid, ids):
1320 price = line.price_unit * (1-(line.discount or 0.0)/100.0)
1321 taxes = tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, price, line.quantity, product=line.product_id, partner=line.invoice_id.partner_id)
1322 res[line.id] = taxes['total']
1324 cur = line.invoice_id.currency_id
1325 res[line.id] = cur_obj.round(cr, uid, cur, res[line.id])
1328 def _price_unit_default(self, cr, uid, context=None):
1331 if context.get('check_total', False):
1332 t = context['check_total']
1333 for l in context.get('invoice_line', {}):
1334 if isinstance(l, (list, tuple)) and len(l) >= 3 and l[2]:
1335 tax_obj = self.pool.get('account.tax')
1336 p = l[2].get('price_unit', 0) * (1-l[2].get('discount', 0)/100.0)
1337 t = t - (p * l[2].get('quantity'))
1338 taxes = l[2].get('invoice_line_tax_id')
1339 if len(taxes[0]) >= 3 and taxes[0][2]:
1340 taxes = tax_obj.browse(cr, uid, list(taxes[0][2]))
1341 for tax in tax_obj.compute_all(cr, uid, taxes, p,l[2].get('quantity'), l[2].get('product_id', False), context.get('partner_id', False))['taxes']:
1342 t = t - tax['amount']
1346 _name = "account.invoice.line"
1347 _description = "Invoice Line"
1349 'name': fields.text('Description', required=True),
1350 'origin': fields.char('Source', size=256, help="Reference of the document that produced this invoice."),
1351 'invoice_id': fields.many2one('account.invoice', 'Invoice Reference', ondelete='cascade', select=True),
1352 'uos_id': fields.many2one('product.uom', 'Unit of Measure', ondelete='set null'),
1353 'product_id': fields.many2one('product.product', 'Product', ondelete='set null'),
1354 'account_id': fields.many2one('account.account', 'Account', required=True, domain=[('type','<>','view'), ('type', '<>', 'closed')], help="The income or expense account related to the selected product."),
1355 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Account')),
1356 'price_subtotal': fields.function(_amount_line, string='Subtotal', type="float",
1357 digits_compute= dp.get_precision('Account'), store=True),
1358 'quantity': fields.float('Quantity', required=True),
1359 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Account')),
1360 'invoice_line_tax_id': fields.many2many('account.tax', 'account_invoice_line_tax', 'invoice_line_id', 'tax_id', 'Taxes', domain=[('parent_id','=',False)]),
1361 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic Account'),
1362 'company_id': fields.related('invoice_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1363 'partner_id': fields.related('invoice_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True)
1368 'price_unit': _price_unit_default,
1371 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1374 res = super(account_invoice_line,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
1375 if context.get('type', False):
1376 doc = etree.XML(res['arch'])
1377 for node in doc.xpath("//field[@name='product_id']"):
1378 if context['type'] in ('in_invoice', 'in_refund'):
1379 node.set('domain', "[('purchase_ok', '=', True)]")
1381 node.set('domain', "[('sale_ok', '=', True)]")
1382 res['arch'] = etree.tostring(doc)
1385 def product_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1388 company_id = company_id if company_id != None else context.get('company_id',False)
1389 context = dict(context)
1390 context.update({'company_id': company_id})
1392 raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") )
1394 if type in ('in_invoice', 'in_refund'):
1395 return {'value': {}, 'domain':{'product_uom':[]}}
1397 return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
1398 part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
1399 fpos_obj = self.pool.get('account.fiscal.position')
1400 fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
1403 context.update({'lang': part.lang})
1405 res = self.pool.get('product.product').browse(cr, uid, product, context=context)
1407 if type in ('out_invoice','out_refund'):
1408 a = res.product_tmpl_id.property_account_income.id
1410 a = res.categ_id.property_account_income_categ.id
1412 a = res.product_tmpl_id.property_account_expense.id
1414 a = res.categ_id.property_account_expense_categ.id
1415 a = fpos_obj.map_account(cr, uid, fpos, a)
1417 result['account_id'] = a
1419 if type in ('out_invoice', 'out_refund'):
1420 taxes = res.taxes_id and res.taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1422 taxes = res.supplier_taxes_id and res.supplier_taxes_id or (a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False)
1423 tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
1425 if type in ('in_invoice', 'in_refund'):
1426 result.update( {'price_unit': price_unit or res.standard_price,'invoice_line_tax_id': tax_id} )
1428 result.update({'price_unit': res.list_price, 'invoice_line_tax_id': tax_id})
1429 result['name'] = res.partner_ref
1432 result['uos_id'] = res.uom_id.id or uom or False
1434 result['name'] += '\n'+res.description
1435 if result['uos_id']:
1436 res2 = res.uom_id.category_id.id
1438 domain = {'uos_id':[('category_id','=',res2 )]}
1440 res_final = {'value':result, 'domain':domain}
1442 if not company_id or not currency_id:
1445 company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
1446 currency = self.pool.get('res.currency').browse(cr, uid, currency_id, context=context)
1448 if company.currency_id.id != currency.id:
1449 if type in ('in_invoice', 'in_refund'):
1450 res_final['value']['price_unit'] = res.standard_price
1451 new_price = res_final['value']['price_unit'] * currency.rate
1452 res_final['value']['price_unit'] = new_price
1455 uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1456 if res.uom_id.category_id.id == uom.category_id.id:
1457 new_price = res_final['value']['price_unit'] * uom.factor_inv
1458 res_final['value']['price_unit'] = new_price
1461 def uos_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
1464 company_id = company_id if company_id != None else context.get('company_id',False)
1465 context = dict(context)
1466 context.update({'company_id': company_id})
1468 res = self.product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context)
1469 if 'uos_id' in res['value']:
1470 del res['value']['uos_id']
1472 res['value']['price_unit'] = 0.0
1474 prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
1475 prod_uom = self.pool.get('product.uom').browse(cr, uid, uom, context=context)
1476 if prod.uom_id.category_id.id != prod_uom.category_id.id:
1478 'title': _('Warning!'),
1479 'message': _('You selected an Unit of Measure which is not compatible with the product.')
1481 return {'value': res['value'], 'warning': warning}
1484 def move_line_get(self, cr, uid, invoice_id, context=None):
1486 tax_obj = self.pool.get('account.tax')
1487 cur_obj = self.pool.get('res.currency')
1490 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1491 company_currency = inv.company_id.currency_id.id
1493 for line in inv.invoice_line:
1494 mres = self.move_line_get_item(cr, uid, line, context)
1498 tax_code_found= False
1499 for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id,
1500 (line.price_unit * (1.0 - (line['discount'] or 0.0) / 100.0)),
1501 line.quantity, line.product_id,
1502 inv.partner_id)['taxes']:
1504 if inv.type in ('out_invoice', 'in_invoice'):
1505 tax_code_id = tax['base_code_id']
1506 tax_amount = line.price_subtotal * tax['base_sign']
1508 tax_code_id = tax['ref_base_code_id']
1509 tax_amount = line.price_subtotal * tax['ref_base_sign']
1514 res.append(self.move_line_get_item(cr, uid, line, context))
1515 res[-1]['price'] = 0.0
1516 res[-1]['account_analytic_id'] = False
1517 elif not tax_code_id:
1519 tax_code_found = True
1521 res[-1]['tax_code_id'] = tax_code_id
1522 res[-1]['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, tax_amount, context={'date': inv.date_invoice})
1525 def move_line_get_item(self, cr, uid, line, context=None):
1528 'name': line.name.split('\n')[0][:64],
1529 'price_unit':line.price_unit,
1530 'quantity':line.quantity,
1531 'price':line.price_subtotal,
1532 'account_id':line.account_id.id,
1533 'product_id':line.product_id.id,
1534 'uos_id':line.uos_id.id,
1535 'account_analytic_id':line.account_analytic_id.id,
1536 'taxes':line.invoice_line_tax_id,
1539 # Set the tax field according to the account and the fiscal position
1541 def onchange_account_id(self, cr, uid, ids, product_id, partner_id, inv_type, fposition_id, account_id):
1544 taxes = self.pool.get('account.account').browse(cr, uid, account_id).tax_ids
1545 fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False
1546 tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, fpos, taxes)
1548 product_change_result = self.product_id_change(cr, uid, ids, product_id, False, type=inv_type,
1549 partner_id=partner_id, fposition_id=fposition_id)
1550 unique_tax_ids = set(tax_ids)
1551 if product_change_result and 'value' in product_change_result and 'invoice_line_tax_id' in product_change_result['value']:
1552 unique_tax_ids |= set(product_change_result['value']['invoice_line_tax_id'])
1553 return {'value':{'invoice_line_tax_id': list(unique_tax_ids)}}
1555 account_invoice_line()
1557 class account_invoice_tax(osv.osv):
1558 _name = "account.invoice.tax"
1559 _description = "Invoice Tax"
1561 def _count_factor(self, cr, uid, ids, name, args, context=None):
1563 for invoice_tax in self.browse(cr, uid, ids, context=context):
1564 res[invoice_tax.id] = {
1568 if invoice_tax.amount <> 0.0:
1569 factor_tax = invoice_tax.tax_amount / invoice_tax.amount
1570 res[invoice_tax.id]['factor_tax'] = factor_tax
1572 if invoice_tax.base <> 0.0:
1573 factor_base = invoice_tax.base_amount / invoice_tax.base
1574 res[invoice_tax.id]['factor_base'] = factor_base
1579 'invoice_id': fields.many2one('account.invoice', 'Invoice Line', ondelete='cascade', select=True),
1580 'name': fields.char('Tax Description', size=64, required=True),
1581 'account_id': fields.many2one('account.account', 'Tax Account', required=True, domain=[('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')]),
1582 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'),
1583 'base': fields.float('Base', digits_compute=dp.get_precision('Account')),
1584 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
1585 'manual': fields.boolean('Manual'),
1586 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of invoice tax."),
1587 'base_code_id': fields.many2one('account.tax.code', 'Base Code', help="The account basis of the tax declaration."),
1588 'base_amount': fields.float('Base Code Amount', digits_compute=dp.get_precision('Account')),
1589 'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', help="The tax basis of the tax declaration."),
1590 'tax_amount': fields.float('Tax Code Amount', digits_compute=dp.get_precision('Account')),
1591 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
1592 'factor_base': fields.function(_count_factor, string='Multipication factor for Base code', type='float', multi="all"),
1593 'factor_tax': fields.function(_count_factor, string='Multipication factor Tax code', type='float', multi="all")
1596 def base_change(self, cr, uid, ids, base, currency_id=False, company_id=False, date_invoice=False):
1597 cur_obj = self.pool.get('res.currency')
1598 company_obj = self.pool.get('res.company')
1599 company_currency = False
1602 factor = self.read(cr, uid, ids[0], ['factor_base'])['factor_base']
1604 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1605 if currency_id and company_currency:
1606 base = cur_obj.compute(cr, uid, currency_id, company_currency, base*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1607 return {'value': {'base_amount':base}}
1609 def amount_change(self, cr, uid, ids, amount, currency_id=False, company_id=False, date_invoice=False):
1610 cur_obj = self.pool.get('res.currency')
1611 company_obj = self.pool.get('res.company')
1612 company_currency = False
1615 factor = self.read(cr, uid, ids[0], ['factor_tax'])['factor_tax']
1617 company_currency = company_obj.read(cr, uid, [company_id], ['currency_id'])[0]['currency_id'][0]
1618 if currency_id and company_currency:
1619 amount = cur_obj.compute(cr, uid, currency_id, company_currency, amount*factor, context={'date': date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1620 return {'value': {'tax_amount': amount}}
1628 def compute(self, cr, uid, invoice_id, context=None):
1630 tax_obj = self.pool.get('account.tax')
1631 cur_obj = self.pool.get('res.currency')
1632 inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
1633 cur = inv.currency_id
1634 company_currency = inv.company_id.currency_id.id
1636 for line in inv.invoice_line:
1637 for tax in tax_obj.compute_all(cr, uid, line.invoice_line_tax_id, (line.price_unit* (1-(line.discount or 0.0)/100.0)), line.quantity, line.product_id, inv.partner_id)['taxes']:
1638 tax['price_unit'] = cur_obj.round(cr, uid, cur, tax['price_unit'])
1640 val['invoice_id'] = inv.id
1641 val['name'] = tax['name']
1642 val['amount'] = tax['amount']
1643 val['manual'] = False
1644 val['sequence'] = tax['sequence']
1645 val['base'] = tax['price_unit'] * line['quantity']
1647 if inv.type in ('out_invoice','in_invoice'):
1648 val['base_code_id'] = tax['base_code_id']
1649 val['tax_code_id'] = tax['tax_code_id']
1650 val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['base_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1651 val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['tax_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1652 val['account_id'] = tax['account_collected_id'] or line.account_id.id
1653 val['account_analytic_id'] = tax['account_analytic_collected_id']
1655 val['base_code_id'] = tax['ref_base_code_id']
1656 val['tax_code_id'] = tax['ref_tax_code_id']
1657 val['base_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['base'] * tax['ref_base_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1658 val['tax_amount'] = cur_obj.compute(cr, uid, inv.currency_id.id, company_currency, val['amount'] * tax['ref_tax_sign'], context={'date': inv.date_invoice or time.strftime('%Y-%m-%d')}, round=False)
1659 val['account_id'] = tax['account_paid_id'] or line.account_id.id
1660 val['account_analytic_id'] = tax['account_analytic_paid_id']
1662 key = (val['tax_code_id'], val['base_code_id'], val['account_id'], val['account_analytic_id'])
1663 if not key in tax_grouped:
1664 tax_grouped[key] = val
1666 tax_grouped[key]['amount'] += val['amount']
1667 tax_grouped[key]['base'] += val['base']
1668 tax_grouped[key]['base_amount'] += val['base_amount']
1669 tax_grouped[key]['tax_amount'] += val['tax_amount']
1671 for t in tax_grouped.values():
1672 t['base'] = cur_obj.round(cr, uid, cur, t['base'])
1673 t['amount'] = cur_obj.round(cr, uid, cur, t['amount'])
1674 t['base_amount'] = cur_obj.round(cr, uid, cur, t['base_amount'])
1675 t['tax_amount'] = cur_obj.round(cr, uid, cur, t['tax_amount'])
1678 def move_line_get(self, cr, uid, invoice_id):
1680 cr.execute('SELECT * FROM account_invoice_tax WHERE invoice_id=%s', (invoice_id,))
1681 for t in cr.dictfetchall():
1682 if not t['amount'] \
1683 and not t['tax_code_id'] \
1684 and not t['tax_amount']:
1689 'price_unit': t['amount'],
1691 'price': t['amount'] or 0.0,
1692 'account_id': t['account_id'],
1693 'tax_code_id': t['tax_code_id'],
1694 'tax_amount': t['tax_amount'],
1695 'account_analytic_id': t['account_analytic_id'],
1699 account_invoice_tax()
1702 class res_partner(osv.osv):
1703 """ Inherits partner and adds invoice information in the partner form """
1704 _inherit = 'res.partner'
1706 'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
1709 def copy(self, cr, uid, id, default=None, context=None):
1710 default = default or {}
1711 default.update({'invoice_ids' : []})
1712 return super(res_partner, self).copy(cr, uid, id, default, context)
1716 class mail_message(osv.osv):
1717 _name = 'mail.message'
1718 _inherit = 'mail.message'
1720 def _postprocess_sent_message(self, cr, uid, message, context=None):
1721 if message.model == 'account.invoice':
1722 self.pool.get('account.invoice').write(cr, uid, [message.res_id], {'sent':True}, context=context)
1723 return super(mail_message, self)._postprocess_sent_message(cr, uid, message=message, context=context)
1726 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: