[IMP] account: acrual expenses/revenues

=======
Purpose
=======
Accruals are revenues and expenses that are incurred during an accounting period for which no invoices or payments were received or made.

=> We suggest a wizard allowing recognizing the Expense/Revenue in a different period keeping tax return in the invoice period.

Example:
A vendor bill received in February which was for the December consumption.
You should recognize the expense in December but the tax return is still due to February.

1) Regular Vendor Bill in february
Account          D          C
600 ACHAT    10000
411 TVA       2100
440 FOUR                12100

2) Action opening a wizard that allows specifying when we have to move the expense.
- Targeted date, accrued account (expense, revenue, taxes, ...)
- Action from account.move.line ?

3) Wizard result
a) The expense account is replaced by the accrued expense account in the original vendor bill journal entry (february)
Compte           D          C
444 FAE      10000
411 TVA       2100
440 FOUR                12100

b) A new accrual journal entry is created for the original expense account in december
Compte           D          C
600 ACHAT    10000
444 FAE                 10000

Was task 2006617

closes odoo/odoo#36053

Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
This commit is contained in:
wan
2019-08-26 12:16:27 +00:00
committed by qdp-odoo
parent 9cc67ea77b
commit 60dadc8ecd
7 changed files with 202 additions and 0 deletions
+1
View File
@@ -24,6 +24,7 @@ You could use this simplified accounting in case you work with an (external) acc
'data/digest_data.xml',
'views/account_menuitem.xml',
'views/account_payment_view.xml',
'wizard/account_accrual_accounting_view.xml',
'wizard/account_unreconcile_view.xml',
'wizard/account_move_reversal_view.xml',
'views/account_move_views.xml',
+5
View File
@@ -3568,6 +3568,11 @@ class AccountMoveLine(models.Model):
action['domain'] = [('id', 'in', ids)]
return action
def action_accrual_entry(self):
[action] = self.env.ref('account.account_accrual_accounting_wizard_action').read()
action['context'] = self.env.context
return action
@api.model
def _get_suspense_moves_domain(self):
return [
+5
View File
@@ -107,6 +107,11 @@ Best Regards,'''))
# Needed in the Point of Sale
account_default_pos_receivable_account_id = fields.Many2one('account.account', string="Default PoS Receivable Account")
# Accrual Accounting
expense_accrual_account_id = fields.Many2one('account.account', help="Account used to move the period of an expense", domain=[('internal_group', '=', 'liability'), ('reconcile', '=', True)])
revenue_accrual_account_id = fields.Many2one('account.account', help="Account used to move the period of a revenue", domain=[('internal_group', '=', 'asset'), ('reconcile', '=', True)])
accrual_default_journal_id = fields.Many2one('account.journal', help="Journal used by default for moving the period of an entry", domain="[('type', '=', 'general')]")
@api.constrains('account_opening_move_id', 'fiscalyear_last_day', 'fiscalyear_last_month')
def _check_fiscalyear_last_day(self):
# if the user explicitly chooses the 29th of February we allow it:
@@ -983,6 +983,14 @@
<field name="tag">manual_reconciliation_view</field>
</record>
<record id="action_accrual_entry" model="ir.actions.server">
<field name="name">Change Period</field>
<field name="model_id" ref="account.model_account_move_line"/>
<field name="binding_model_id" ref="account.model_account_move_line"/>
<field name="state">code</field>
<field name="code">action = records.action_accrual_entry()</field>
</record>
<act_window
id="action_move_line_select_tax_audit"
name="Journal Items for Tax Audit"
+1
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_accrual_accounting
from . import account_unreconcile
from . import account_validate_account_move
from . import pos_box
@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class AccrualAccountingWizard(models.TransientModel):
_name = 'account.accrual.accounting.wizard'
_description = 'Create accrual entry.'
date = fields.Date(required=True)
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
account_type = fields.Selection([('income', 'Revenue'), ('expense', 'Expense')])
active_move_line_ids = fields.Many2many('account.move.line')
journal_id = fields.Many2one('account.journal', required=True, related="company_id.accrual_default_journal_id", readonly=False)
expense_accrual_account = fields.Many2one('account.account', related="company_id.expense_accrual_account_id", readonly=False)
revenue_accrual_account = fields.Many2one('account.account', related="company_id.revenue_accrual_account_id", readonly=False)
percentage = fields.Float("Percentage", default=100.0)
total_amount = fields.Monetary(compute="_compute_total_amount", currency_field='company_currency_id')
company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
@api.constrains('percentage')
def _constraint_percentage(self):
for record in self:
if not (0.0 < record.percentage <= 100.0):
raise UserError(_("Percentage must be between 0 and 100"))
@api.depends('percentage', 'active_move_line_ids')
def _compute_total_amount(self):
for record in self:
record.total_amount = sum(record.active_move_line_ids.mapped(lambda l: record.percentage * (l.debit + l.credit) / 100))
@api.model
def default_get(self, fields):
if self.env.context.get('active_model') != 'account.move.line' or not self.env.context.get('active_ids'):
raise UserError(_('This can only be used on journal items'))
rec = super(AccrualAccountingWizard, self).default_get(fields)
active_move_line_ids = self.env['account.move.line'].browse(self.env.context['active_ids'])
rec['active_move_line_ids'] = active_move_line_ids.ids
if any(move_line.reconciled for move_line in active_move_line_ids):
raise UserError(_('You can only change the period for items that are not yet reconciled.'))
if any(line.account_id.user_type_id != active_move_line_ids[0].account_id.user_type_id for line in active_move_line_ids):
raise UserError(_('All accounts on the lines must be from the same type.'))
account_types_allowed = self.env.ref('account.data_account_type_expenses') + self.env.ref('account.data_account_type_revenue') + self.env.ref('account.data_account_type_other_income')
if active_move_line_ids[0].account_id.user_type_id not in account_types_allowed:
raise UserError(_('You can only change the period for items in these types of accounts: ') + ", ".join(account_types_allowed.mapped('name')))
rec['account_type'] = active_move_line_ids[0].account_id.user_type_id.internal_group
return rec
def amend_entries(self):
move_data = []
to_reconcile = []
message_data = []
#gather the origin account on seleced journal items
origin_accounts = {}
for aml in self.active_move_line_ids:
origin_accounts[aml.id] = aml.account_id.id
#set the accrual account on the selected journal items
self.active_move_line_ids.write({'account_id': (self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account).id})
#create the accrual move(s) and reconcile accrual items together
for aml in self.active_move_line_ids:
reported_debit = aml.company_id.currency_id.round((self.percentage / 100) * aml.debit)
reported_credit = aml.company_id.currency_id.round((self.percentage / 100) * aml.credit)
ref = _('Accrual Adjusting Entry ({percent}% recognized) for invoice: {name}').format(percent=self.percentage, name=aml.move_id.name)
move_data = [{
'date': self.date,
'ref': ref,
'journal_id': self.journal_id.id,
'line_ids': [
(0, 0, {
'name': aml.name,
'debit': reported_debit,
'credit': reported_credit,
'account_id': origin_accounts[aml.id],
'partner_id': aml.partner_id.id,
}),
(0, 0, {
'name': ref,
'debit': reported_credit,
'credit': reported_debit,
'account_id': (self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account).id,
'partner_id': aml.partner_id.id,
}),
],
}]
message_data = [(_('Accrual Adjusting Entry ({percent}% recognized) for invoice:') + ' <a href=# data-oe-model=account.move data-oe-id={id}>{name}</a>').format(
percent=self.percentage,
id=aml.move_id.id,
name=aml.move_id.name,
)]
#in case the percentage to recognize is not 100%, another entry is created to keep the remaining balance in the same account at the original date
if self.percentage < 100:
ref = _('Accrual Adjusting Entry ({percent}% recognized) for invoice: {name}').format(percent=100 - self.percentage, name=aml.move_id.name)
move_data.append({
'date': aml.move_id.date,
'ref': ref,
'journal_id': self.journal_id.id,
'line_ids': [
(0, 0, {
'name': aml.name,
'debit': aml.debit - reported_debit,
'credit': aml.credit - reported_credit,
'account_id': origin_accounts[aml.id],
'partner_id': aml.partner_id.id,
}),
(0, 0, {
'name': ref,
'debit': aml.credit - reported_credit,
'credit': aml.debit - reported_debit,
'account_id': (self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account).id,
'partner_id': aml.partner_id.id,
}),
],
})
message_data.append((_('Accrual Adjusting Entry ({percent}% recognized) for invoice:') + ' <a href=# data-oe-model=account.move data-oe-id={id}>{name}</a>').format(
percent=self.percentage,
id=aml.move_id.id,
name=aml.move_id.name,
))
created_ids = self.env['account.move'].create(move_data)
created_ids.post()
for move, msg in zip(created_ids, message_data):
move.message_post(body=msg)
#reconcile journal items on the accrual account
to_reconcile = created_ids.line_ids.filtered(lambda line: line.account_id in (self.revenue_accrual_account + self.expense_accrual_account)) + aml
to_reconcile.reconcile()
#open the generated entries
action = {
'name': _('Generated Entries'),
'domain': [('id', 'in', created_ids.ids)],
'res_model': 'account.move',
'view_mode': 'tree,form',
'type': 'ir.actions.act_window',
'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (False, 'form')],
}
if len(created_ids) == 1:
action.update({'view_mode': 'form', 'res_id': created_ids.id})
return action
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_accrual_accounting_wizard_form_view" model="ir.ui.view">
<field name="name">account.accrual.accounting.wizard.form</field>
<field name="model">account.accrual.accounting.wizard</field>
<field name="arch" type="xml">
<form>
<field name="account_type" invisible="1"/>
<field name="company_id" invisible="1"/>
<group>
<group>
<field name="date" string="Recognize on"/>
<field name="expense_accrual_account" string="Accrued Account" attrs="{'invisible': [('account_type', '!=', 'expense')], 'required': [('account_type', '=', 'expense')]}"/>
<field name="revenue_accrual_account" string="Accrued Account" attrs="{'invisible': [('account_type', '!=', 'income')], 'required': [('account_type', '=', 'income')]}"/>
</group>
<group>
<field name="percentage" string="Percent (%)"/>
<field name="total_amount" string="Amount"/>
<field name="journal_id" domain="[('type', '=', 'general')]"/>
</group>
</group>
<footer>
<button string="Create Journal Entry" name="amend_entries" type="object" class="oe_highlight"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="account_accrual_accounting_wizard_action" model="ir.actions.act_window">
<field name="name">Change Period for the expense/revenue recognition</field>
<field name="res_model">account.accrual.accounting.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>