[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:
@@ -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',
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user