X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Faccount%2Faccount_invoice.py;h=55ba1058d588f3f48f859cee64c4f7bc02c69ca6;hb=f166daf0652aa54037cf1baa5d81a7ae758f5e6e;hp=c9cf90789cb659077ed32cde97e3b7928f365805;hpb=49597f0e66a29fcd39c0c2bd98d6474e7f98d4fd;p=odoo%2Fodoo.git diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index c9cf907..55ba105 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -22,6 +22,7 @@ import time from lxml import etree import openerp.addons.decimal_precision as dp +import openerp.exceptions from openerp import netsvc from openerp import pooler @@ -52,9 +53,12 @@ class account_invoice(osv.osv): company_id = context.get('company_id', user.company_id.id) type2journal = {'out_invoice': 'sale', 'in_invoice': 'purchase', 'out_refund': 'sale_refund', 'in_refund': 'purchase_refund'} journal_obj = self.pool.get('account.journal') - res = journal_obj.search(cr, uid, [('type', '=', type2journal.get(type_inv, 'sale')), - ('company_id', '=', company_id)], - limit=1) + domain = [('company_id', '=', company_id)] + if isinstance(type_inv, list): + domain.append(('type', 'in', [type2journal.get(type) for type in type_inv if type2journal.get(type)])) + else: + domain.append(('type', '=', type2journal.get(type_inv, 'sale'))) + res = journal_obj.search(cr, uid, domain, limit=1) return res and res[0] or False def _get_currency(self, cr, uid, context=None): @@ -70,7 +74,7 @@ class account_invoice(osv.osv): tt = type2journal.get(type_inv, 'sale') result = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=',tt)], context=context) if not result: - raise osv.except_osv(_('No Analytic Journal !'),_("You must define an analytic journal of type '%s'!") % (tt,)) + raise osv.except_osv(_('No Analytic Journal!'),_("You must define an analytic journal of type '%s'!") % (tt,)) return result[0] def _get_type(self, cr, uid, context=None): @@ -91,13 +95,45 @@ class account_invoice(osv.osv): return [('none', _('Free Reference'))] def _amount_residual(self, cr, uid, ids, name, args, context=None): + """Function of the field residua. It computes the residual amount (balance) for each invoice""" + if context is None: + context = {} + ctx = context.copy() result = {} + currency_obj = self.pool.get('res.currency') for invoice in self.browse(cr, uid, ids, context=context): + nb_inv_in_partial_rec = max_invoice_id = 0 result[invoice.id] = 0.0 if invoice.move_id: - for m in invoice.move_id.line_id: - if m.account_id.type in ('receivable','payable'): - result[invoice.id] += m.amount_residual_currency + for aml in invoice.move_id.line_id: + if aml.account_id.type in ('receivable','payable'): + if aml.currency_id and aml.currency_id.id == invoice.currency_id.id: + result[invoice.id] += aml.amount_residual_currency + else: + ctx['date'] = aml.date + result[invoice.id] += currency_obj.compute(cr, uid, aml.company_id.currency_id.id, invoice.currency_id.id, aml.amount_residual, context=ctx) + + if aml.reconcile_partial_id.line_partial_ids: + #we check if the invoice is partially reconciled and if there are other invoices + #involved in this partial reconciliation (and we sum these invoices) + for line in aml.reconcile_partial_id.line_partial_ids: + if line.invoice and invoice.type == line.invoice.type: + nb_inv_in_partial_rec += 1 + #store the max invoice id as for this invoice we will make a balance instead of a simple division + max_invoice_id = max(max_invoice_id, line.invoice.id) + if nb_inv_in_partial_rec: + #if there are several invoices in a partial reconciliation, we split the residual by the number + #of invoice to have a sum of residual amounts that matches the partner balance + new_value = currency_obj.round(cr, uid, invoice.currency_id, result[invoice.id] / nb_inv_in_partial_rec) + if invoice.id == max_invoice_id: + #if it's the last the invoice of the bunch of invoices partially reconciled together, we make a + #balance to avoid rounding errors + result[invoice.id] = result[invoice.id] - ((nb_inv_in_partial_rec - 1) * new_value) + else: + result[invoice.id] = new_value + + #prevent the residual amount on the invoice to be less than 0 + result[invoice.id] = max(result[invoice.id], 0.0) return result # Give Journal Items related to the payment reconciled to this invoice @@ -304,16 +340,7 @@ class account_invoice(osv.osv): ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'), ] - def _find_partner(self, inv): - ''' - Find the partner for which the accounting entries will be created - ''' - #if the chosen partner is not a company and has a parent company, use the parent for the journal entries - #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait' - part = inv.partner_id - if part.parent_id and not part.is_company: - part = part.parent_id - return part + def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False): @@ -376,18 +403,6 @@ class account_invoice(osv.osv): context['view_id'] = view_id return context - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - try: - return super(account_invoice, self).create(cr, uid, vals, context) - except Exception, e: - if '"journal_id" viol' in e.args[0]: - raise orm.except_orm(_('Configuration Error!'), - _('There is no Sale/Purchase Journal(s) defined.')) - else: - raise orm.except_orm(_('Unknown Error!'), str(e)) - def invoice_print(self, cr, uid, ids, context=None): ''' This function prints the invoice and mark it as sent, so that we can see more easily the next step of the workflow @@ -419,7 +434,7 @@ class account_invoice(osv.osv): try: compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1] except ValueError: - compose_form_id = False + compose_form_id = False ctx = dict(context) ctx.update({ 'default_model': 'account.invoice', @@ -451,11 +466,15 @@ class account_invoice(osv.osv): context = {} invoices = self.read(cr, uid, ids, ['state','internal_number'], context=context) unlink_ids = [] + for t in invoices: - if t['state'] in ('draft', 'cancel') and t['internal_number']== False: - unlink_ids.append(t['id']) + if t['state'] not in ('draft', 'cancel'): + raise openerp.exceptions.Warning(_('You cannot delete an invoice which is not draft or cancelled. You should refund it instead.')) + elif t['internal_number']: + raise openerp.exceptions.Warning(_('You cannot delete an invoice after it has been validated (and received a number). You can set it back to "Draft" state and modify its content, then re-confirm it.')) else: - raise osv.except_osv(_('Invalid Action!'), _('You can not delete an invoice which is not cancelled. You should refund it instead.')) + unlink_ids.append(t['id']) + osv.osv.unlink(self, cr, uid, unlink_ids, context=context) return True @@ -540,11 +559,15 @@ class account_invoice(osv.osv): return result def onchange_payment_term_date_invoice(self, cr, uid, ids, payment_term_id, date_invoice): - res = {} + res = {} + if isinstance(ids, (int, long)): + ids = [ids] if not date_invoice: date_invoice = time.strftime('%Y-%m-%d') if not payment_term_id: - return {'value':{'date_due': date_invoice}} #To make sure the invoice has a due date when no payment term + inv = self.browse(cr, uid, ids[0]) + #To make sure the invoice due date should contain due date which is entered by user when there is no payment term defined + return {'value':{'date_due': inv.date_due and inv.date_due or date_invoice}} pterm_list = self.pool.get('account.payment.term').compute(cr, uid, payment_term_id, value=1, date_ref=date_invoice) if pterm_list: pterm_list = [line[0] for line in pterm_list] @@ -561,6 +584,10 @@ class account_invoice(osv.osv): return {'value': {}} def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id): + #TODO: add the missing context parameter when forward-porting in trunk so we can remove + # this hack! + context = self.pool['res.users'].context_get(cr, uid) + val = {} dom = {} obj_journal = self.pool.get('account.journal') @@ -607,18 +634,17 @@ class account_invoice(osv.osv): obj_l = account_obj.browse(cr, uid, inv_line[2]['account_id']) if obj_l.company_id.id != company_id: raise osv.except_osv(_('Configuration Error!'), - _('Invoice line account\'s company and invoice\'s compnay does not match.')) + _('Invoice line account\'s company and invoice\'s company does not match.')) else: continue if company_id and type: - if type in ('out_invoice'): - journal_type = 'sale' - elif type in ('out_refund'): - journal_type = 'sale_refund' - elif type in ('in_refund'): - journal_type = 'purchase_refund' - else: - journal_type = 'purchase' + journal_mapping = { + 'out_invoice': 'sale', + 'out_refund': 'sale_refund', + 'in_refund': 'purchase_refund', + 'in_invoice': 'purchase', + } + journal_type = journal_mapping[type] journal_ids = obj_journal.search(cr, uid, [('company_id','=',company_id), ('type', '=', journal_type)]) if journal_ids: val['journal_id'] = journal_ids[0] @@ -628,7 +654,12 @@ class account_invoice(osv.osv): if r[1] == 'journal_id' and r[2] in journal_ids: val['journal_id'] = r[2] if not val.get('journal_id', False): - raise osv.except_osv(_('Configuration Error!'), (_('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.') % (journal_type))) + journal_type_map = dict(obj_journal._columns['type'].selection) + journal_type_label = self.pool['ir.translation']._get_source(cr, uid, None, ('code','selection'), + context.get('lang'), + journal_type_map.get(journal_type)) + raise osv.except_osv(_('Configuration Error!'), + _('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.') % ('"%s"' % journal_type_label)) dom = {'journal_id': [('id', 'in', journal_ids)]} else: journal_ids = obj_journal.search(cr, uid, []) @@ -731,7 +762,7 @@ class account_invoice(osv.osv): inv = self.browse(cr, uid, id) cur_obj = self.pool.get('res.currency') - company_currency = inv.company_id.currency_id.id + company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id if inv.type in ('out_invoice', 'in_refund'): sign = 1 else: @@ -745,7 +776,7 @@ class account_invoice(osv.osv): else: ref = self._convert_ref(cr, uid, inv.number) if not inv.journal_id.analytic_journal_id: - raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,)) + raise osv.except_osv(_('No Analytic Journal!'),_("You have to define an analytic journal on the '%s' journal!") % (inv.journal_id.name,)) il['analytic_lines'] = [(0,0, { 'name': il['name'], 'date': inv['date_invoice'], @@ -778,6 +809,7 @@ class account_invoice(osv.osv): return move_lines def check_tax_lines(self, cr, uid, inv, compute_taxes, ait_obj): + company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id if not inv.tax_line: for tax in compute_taxes.values(): ait_obj.create(cr, uid, tax) @@ -791,7 +823,7 @@ class account_invoice(osv.osv): if not key in compute_taxes: raise osv.except_osv(_('Warning!'), _('Global taxes defined, but they are not in invoice lines !')) base = compute_taxes[key]['base'] - if abs(base - tax.base) > inv.company_id.currency_id.rounding: + if abs(base - tax.base) > company_currency.rounding: raise osv.except_osv(_('Warning!'), _('Tax base different!\nClick on compute to update the tax base.')) for key in compute_taxes: if not key in tax_key: @@ -870,7 +902,7 @@ class account_invoice(osv.osv): if not inv.journal_id.sequence_id: raise osv.except_osv(_('Error!'), _('Please define sequence on the journal related to this invoice.')) if not inv.invoice_line: - raise osv.except_osv(_('No Invoice Lines !'), _('Please create some invoice lines.')) + raise osv.except_osv(_('No Invoice Lines!'), _('Please create some invoice lines.')) if inv.move_id: continue @@ -878,7 +910,7 @@ class account_invoice(osv.osv): ctx.update({'lang': inv.partner_id.lang}) if not inv.date_invoice: self.write(cr, uid, [inv.id], {'date_invoice': fields.date.context_today(self,cr,uid,context=context)}, context=ctx) - company_currency = inv.company_id.currency_id.id + company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id # create the analytical lines # one move line per invoice line iml = self._get_analytic_lines(cr, uid, inv.id, context=ctx) @@ -891,7 +923,7 @@ class account_invoice(osv.osv): group_check_total = self.pool.get('res.groups').browse(cr, uid, group_check_total_id, context=context) if group_check_total and uid in [x.id for x in group_check_total.users]: if (inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0)): - raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe encoded total does not match the computed total.')) + raise osv.except_osv(_('Bad Total!'), _('Please verify the price of the invoice!\nThe encoded total does not match the computed total.')) if inv.payment_term: total_fixed = total_percent = 0 @@ -926,7 +958,7 @@ class account_invoice(osv.osv): total, total_currency, iml = self.compute_invoice_totals(cr, uid, inv, company_currency, ref, iml, context=ctx) acc_id = inv.account_id.id - name = inv['name'] or '/' + name = inv['name'] or inv['supplier_invoice_number'] or '/' totlines = False if inv.payment_term: totlines = payment_term_obj.compute(cr, @@ -975,7 +1007,7 @@ class account_invoice(osv.osv): date = inv.date_invoice or time.strftime('%Y-%m-%d') - part = self._find_partner(inv) + part = self.pool.get("res.partner")._find_accounting_partner(inv.partner_id) line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml) @@ -994,7 +1026,8 @@ class account_invoice(osv.osv): 'line_id': line, 'journal_id': journal_id, 'date': date, - 'narration':inv.comment + 'narration': inv.comment, + 'company_id': inv.company_id.id, } period_id = inv.period_id and inv.period_id.id or False ctx.update(company_id=inv.company_id.id, @@ -1125,12 +1158,12 @@ class account_invoice(osv.osv): if not ids: return [] types = { - 'out_invoice': 'Invoice ', - 'in_invoice': 'Sup. Invoice ', - 'out_refund': 'Refund ', - 'in_refund': 'Supplier Refund ', + 'out_invoice': _('Invoice'), + 'in_invoice': _('Supplier Invoice'), + 'out_refund': _('Refund'), + 'in_refund': _('Supplier Refund'), } - 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')] + return [(r['id'], '%s %s' % (r['number'] or types[r['type']], r['name'] or '')) for r in self.read(cr, uid, ids, ['type', 'number', 'name'], context, load='_classic_write')] def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100): if not args: @@ -1145,6 +1178,11 @@ class account_invoice(osv.osv): return self.name_get(cr, user, ids, context) def _refund_cleanup_lines(self, cr, uid, lines, context=None): + """Convert records to dict of values suitable for one2many line creation + + :param list(browse_record) lines: records to convert + :return: list of command tuple for one2many line creation [(0, 0, dict of valueis), ...] + """ clean_lines = [] for line in lines: clean_line = {} @@ -1269,9 +1307,7 @@ class account_invoice(osv.osv): ref = invoice.reference else: ref = self._convert_ref(cr, uid, invoice.number) - partner = invoice.partner_id - if partner.parent_id and not partner.is_company: - partner = partner.parent_id + partner = self.pool['res.partner']._find_accounting_partner(invoice.partner_id) # Pay attention to the sign for both debit/credit AND amount_currency l1 = { 'debit': direction * pay_amount>0 and direction * pay_amount, @@ -1371,6 +1407,7 @@ class account_invoice_line(osv.osv): _name = "account.invoice.line" _description = "Invoice Line" + _order = "invoice_id,sequence,id" _columns = { 'name': fields.text('Description', required=True), 'origin': fields.char('Source Document', size=256, help="Reference of the document that produced this invoice."), @@ -1394,7 +1431,12 @@ class account_invoice_line(osv.osv): # XXX this gets the default account for the user's company, # it should get the default account for the invoice's company # however, the invoice's company does not reach this point - prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context) + if context is None: + context = {} + if context.get('type') in ('out_invoice','out_refund'): + prop = self.pool.get('ir.property').get(cr, uid, 'property_account_income_categ', 'product.category', context=context) + else: + prop = self.pool.get('ir.property').get(cr, uid, 'property_account_expense_categ', 'product.category', context=context) return prop and prop.id or False _defaults = { @@ -1402,6 +1444,7 @@ class account_invoice_line(osv.osv): 'discount': 0.0, 'price_unit': _price_unit_default, 'account_id': _default_account_id, + 'sequence': 10, } def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): @@ -1425,7 +1468,7 @@ class account_invoice_line(osv.osv): context = dict(context) context.update({'company_id': company_id, 'force_company': company_id}) if not partner_id: - raise osv.except_osv(_('No Partner Defined !'),_("You must first select a partner !") ) + raise osv.except_osv(_('No Partner Defined!'),_("You must first select a partner!") ) if not product: if type in ('in_invoice', 'in_refund'): return {'value': {}, 'domain':{'product_uom':[]}} @@ -1484,11 +1527,10 @@ class account_invoice_line(osv.osv): new_price = res_final['value']['price_unit'] * currency.rate res_final['value']['price_unit'] = new_price - if result['uos_id'] != res.uom_id.id: - selected_uom = self.pool.get('product.uom_id').browse(cr, uid, result['uos_id'], context=context) - if res.uom_id.category_id.id == selected_uom.category_id.id: - new_price = res_final['value']['price_unit'] * uom_id.factor_inv - res_final['value']['price_unit'] = new_price + if result['uos_id'] and result['uos_id'] != res.uom_id.id: + selected_uom = self.pool.get('product.uom').browse(cr, uid, result['uos_id'], context=context) + new_price = self.pool.get('product.uom')._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uos_id']) + res_final['value']['price_unit'] = new_price return res_final 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): @@ -1520,8 +1562,7 @@ class account_invoice_line(osv.osv): if context is None: context = {} inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context) - company_currency = inv.company_id.currency_id.id - + company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id for line in inv.invoice_line: mres = self.move_line_get_item(cr, uid, line, context) if not mres: @@ -1666,8 +1707,7 @@ class account_invoice_tax(osv.osv): cur_obj = self.pool.get('res.currency') inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context) cur = inv.currency_id - company_currency = inv.company_id.currency_id.id - + company_currency = self.pool['res.company'].browse(cr, uid, inv.company_id.id).currency_id.id for line in inv.invoice_line: 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']: val={} @@ -1738,6 +1778,18 @@ class res_partner(osv.osv): 'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True), } + def _find_accounting_partner(self, partner): + ''' + Find the partner for which the accounting entries will be created + ''' + # FIXME: after 7.0, to replace by function field partner.commercial_partner_id + + #if the chosen partner is not a company and has a parent company, use the parent for the journal entries + #because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait' + while not partner.is_company and partner.parent_id: + partner = partner.parent_id + return partner + def copy(self, cr, uid, id, default=None, context=None): default = default or {} default.update({'invoice_ids' : []})