[IMP] account: fusion wizards for automatic entries

Task 2227389

The wizards for creating Accrual Entries and Transfer Entries are quite
similar. We are merging them as they share code and business logic.

Entries in the future should not be posted for both wizards, but set to
be auto posted instead.
This commit is contained in:
william
2020-08-05 11:57:10 +00:00
parent 82dc0cb7b9
commit 0ea9254c81
15 changed files with 496 additions and 577 deletions
+1 -2
View File
@@ -23,7 +23,7 @@ You could use this simplified accounting in case you work with an (external) acc
'data/account_data.xml',
'data/digest_data.xml',
'views/account_payment_view.xml',
'wizard/account_accrual_accounting_view.xml',
'wizard/account_automatic_entry_wizard_views.xml',
'wizard/account_unreconcile_view.xml',
'wizard/account_move_reversal_view.xml',
'wizard/account_resequence_views.xml',
@@ -74,7 +74,6 @@ You could use this simplified accounting in case you work with an (external) acc
'wizard/account_invoice_send_views.xml',
'views/account_tax_report_views.xml',
'report/account_hash_integrity_templates.xml',
'wizard/account_transfer_wizard.xml',
'views/res_currency.xml',
'views/account_menuitem.xml',
'views/account_analytic_default_view.xml',
+3 -16
View File
@@ -2750,6 +2750,7 @@ class AccountMoveLine(models.Model):
check_company=True,
tracking=True)
account_internal_type = fields.Selection(related='account_id.user_type_id.type', string="Internal Type", readonly=True)
account_internal_group = fields.Selection(related='account_id.user_type_id.internal_group', string="Internal Group", readonly=True)
account_root_id = fields.Many2one(related='account_id.root_id', string="Account Root", store=True, readonly=True)
sequence = fields.Integer(default=10)
name = fields.Text(string='Label', tracking=True)
@@ -4693,8 +4694,8 @@ 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()
def action_automatic_entry(self):
[action] = self.env.ref('account.account_automatic_entry_wizard_action').read()
action['context'] = self.env.context
return action
@@ -4706,20 +4707,6 @@ class AccountMoveLine(models.Model):
('statement_line_id', '!=', False),
]
def action_transfer_accounts_wizard(self):
wizard = self.env['account.transfer.wizard'].create({})
return {
'name': _("Transfer Entries"),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'account.transfer.wizard',
'res_id': wizard.id,
'target': 'new',
'context': self.env.context,
}
def _get_attachment_domains(self):
self.ensure_one()
domains = [[('res_model', '=', 'account.move'), ('res_id', '=', self.move_id.id)]]
+3 -3
View File
@@ -103,11 +103,11 @@ class ResCompany(models.Model):
# Accrual Accounting
expense_accrual_account_id = fields.Many2one('account.account',
help="Account used to move the period of an expense",
domain="[('internal_group', '=', 'liability'), ('internal_type', 'not in', ('receivable', 'payable')), ('reconcile', '=', True), ('company_id', '=', id)]")
domain="[('internal_group', '=', 'liability'), ('internal_type', 'not in', ('receivable', 'payable')), ('company_id', '=', id)]")
revenue_accrual_account_id = fields.Many2one('account.account',
help="Account used to move the period of a revenue",
domain="[('internal_group', '=', 'asset'), ('internal_type', 'not in', ('receivable', 'payable')), ('reconcile', '=', True), ('company_id', '=', id)]")
accrual_default_journal_id = fields.Many2one('account.journal', help="Journal used by default for moving the period of an entry", domain="[('type', '=', 'general')]")
domain="[('internal_group', '=', 'asset'), ('internal_type', 'not in', ('receivable', 'payable')), ('company_id', '=', id)]")
automatic_entry_default_journal_id = fields.Many2one('account.journal', help="Journal used by default for moving the period of an entry", domain="[('type', '=', 'general')]")
# Technical field to hide country specific fields in company form view
country_code = fields.Char(related='country_id.code')
+1 -2
View File
@@ -125,7 +125,7 @@ access_account_payment,account.payment,model_account_payment,account.group_accou
access_account_payment_register,access.account.payment.register,model_account_payment_register,account.group_account_invoice,1,1,1,0
access_account_bank_statement_closebalance,access.account.bank.statement.closebalance,model_account_bank_statement_closebalance,account.group_account_user,1,1,1,0
access_account_accrual_accounting_wizard,access.account.accrual.accounting.wizard,model_account_accrual_accounting_wizard,account.group_account_user,1,1,1,0
access_account_automatic_entry_wizard,access.account.automatic.entry.wizard,model_account_automatic_entry_wizard,account.group_account_user,1,1,1,0
access_account_unreconcile,access.account.unreconcile,model_account_unreconcile,account.group_account_user,1,1,1,0
access_account_resequence,access.account.resequence.wizard,model_account_resequence_wizard,account.group_account_user,1,1,1,0
access_validate_account_move,access.validate.account.move,model_validate_account_move,account.group_account_invoice,1,1,1,0
@@ -138,7 +138,6 @@ access_account_financial_year_op,access.account.financial.year.op,model_account_
access_account_setup_bank_manual_config,access.account.setup.bank.manual.config,model_account_setup_bank_manual_config,account.group_account_manager,1,1,1,0
access_tax_adjustments_wizard,access.tax.adjustments.wizard,model_tax_adjustments_wizard,account.group_account_user,1,1,1,0
access_account_invoice_send,access.account.invoice.send,model_account_invoice_send,account.group_account_invoice,1,1,1,0
access_account_transfer_wizard,access.account.transfer.wizard,model_account_transfer_wizard,account.group_account_user,1,1,1,0
access_account_analytic_default,account.analytic.default,model_account_analytic_default,account.group_account_readonly,1,0,0,0
access_account_analytic_default_analytic,account.analytic.default analytic,model_account_analytic_default,analytic.group_analytic_accounting,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
125 access_account_invoice_send access.account.invoice.send model_account_invoice_send account.group_account_invoice 1 1 1 0
126 access_account_transfer_wizard access_account_analytic_default access.account.transfer.wizard account.analytic.default model_account_transfer_wizard model_account_analytic_default account.group_account_user account.group_account_readonly 1 1 0 1 0 0
127 access_account_analytic_default access_account_analytic_default_analytic account.analytic.default account.analytic.default analytic model_account_analytic_default account.group_account_readonly analytic.group_analytic_accounting 1 0 0 0
128 access_account_analytic_default_analytic access_account_analytic_default_invoice account.analytic.default analytic account.analytic.default invoice model_account_analytic_default analytic.group_analytic_accounting account.group_account_invoice 1 0 1 0 1 0 1
129
130
131
138
139
140
141
142
143
@@ -1653,8 +1653,9 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
})
move.action_post()
wizard = self.env['account.accrual.accounting.wizard']\
wizard = self.env['account.automatic.entry.wizard']\
.with_context(active_model='account.move.line', active_ids=move.invoice_line_ids.ids).create({
'action': 'change_period',
'date': '2018-01-01',
'percentage': 60,
'journal_id': self.company_data['default_journal_misc'].id,
@@ -1671,7 +1672,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
'reconcile': True,
}).id,
})
wizard_res = wizard.amend_entries()
wizard_res = wizard.do_action()
self.assertInvoiceValues(move, [
{
@@ -2330,8 +2330,9 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
})
move.action_post()
wizard = self.env['account.accrual.accounting.wizard']\
wizard = self.env['account.automatic.entry.wizard']\
.with_context(active_model='account.move.line', active_ids=move.invoice_line_ids.ids).create({
'action': 'change_period',
'date': '2018-01-01',
'percentage': 60,
'journal_id': self.company_data['default_journal_misc'].id,
@@ -2348,7 +2349,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
'reconcile': True,
}).id,
})
wizard_res = wizard.amend_entries()
wizard_res = wizard.do_action()
self.assertInvoiceValues(move, [
{
+12 -8
View File
@@ -196,12 +196,13 @@ class TestTransferWizard(AccountTestInvoicingCommon):
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.transfer.wizard'].with_context(context)) as wizard_form:
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.receivable_account
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.button_transfer()['res_id']
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
payable_transfer = transfer_move.line_ids.filtered(lambda x: x.account_id == self.payable_account)
@@ -221,12 +222,13 @@ class TestTransferWizard(AccountTestInvoicingCommon):
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.transfer.wizard'].with_context(context)) as wizard_form:
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.accounts[4]
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.button_transfer()['res_id']
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
groups = {}
@@ -251,12 +253,13 @@ class TestTransferWizard(AccountTestInvoicingCommon):
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.transfer.wizard'].with_context(context)) as wizard_form:
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.test_currency_account
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.button_transfer()['res_id']
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
destination_line = transfer_move.line_ids.filtered(lambda x: x.account_id == self.test_currency_account)
@@ -273,12 +276,13 @@ class TestTransferWizard(AccountTestInvoicingCommon):
# We use a form to pass the context properly to the depends_context move_line_ids field
context = {'active_model': 'account.move.line', 'active_ids': active_move_lines.ids}
with Form(self.env['account.transfer.wizard'].with_context(context)) as wizard_form:
with Form(self.env['account.automatic.entry.wizard'].with_context(context)) as wizard_form:
wizard_form.action = 'change_account'
wizard_form.destination_account_id = self.receivable_account
wizard_form.journal_id = self.journal
wizard = wizard_form.save()
transfer_move_id = wizard.button_transfer()['res_id']
transfer_move_id = wizard.do_action()['res_id']
transfer_move = self.env['account.move'].browse(transfer_move_id)
destination_lines = transfer_move.line_ids.filtered(lambda x: x.account_id == self.receivable_account)
+16 -14
View File
@@ -758,6 +758,7 @@
<field name="is_rounding_line" invisible="1"/>
<field name="exclude_from_invoice_tab" invisible="1"/>
<field name="account_internal_type" invisible="1"/>
<field name="account_internal_group" invisible="1"/>
<field name="always_set_currency_id" invisible="1"/>
</tree>
<kanban class="o_kanban_mobile">
@@ -837,6 +838,7 @@
<field name="is_rounding_line" invisible="1"/>
<field name="exclude_from_invoice_tab" invisible="1"/>
<field name="account_internal_type" invisible="1"/>
<field name="account_internal_group" invisible="1"/>
<field name="always_set_currency_id" invisible="1"/>
</kanban>
<form>
@@ -949,6 +951,16 @@
<field name="country_id" invisible="1"/>
<field name="tax_tag_ids" widget="many2many_tags" string="Tax Grids" optional="show" domain="[('country_id', '=', country_id), ('applicability', '=', 'taxes')]"/>
<!-- Buttons -->
<button name="action_automatic_entry"
type="object"
icon="fa-calendar"
string="Cut-Off"
aria-label="Change Period"
class="float-right"
attrs="{'invisible': [('account_internal_group', 'not in', ('income', 'expense'))], 'column_invisible': [('parent.move_type', '=', 'entry')]}"
context="{'hide_automatic_options': 1, 'default_action': 'change_period'}"/>
<!-- Others fields -->
<field name="payment_id" invisible="1"/>
@@ -976,6 +988,7 @@
<field name="is_rounding_line" invisible="1"/>
<field name="exclude_from_invoice_tab" invisible="1"/>
<field name="account_internal_type" invisible="1"/>
<field name="account_internal_group" invisible="1"/>
</tree>
<!-- Form view to cover mobile use -->
<form>
@@ -1163,13 +1176,13 @@
<field name="context">{'search_default_account_id': [active_id]}</field>
</record>
<record id="action_accrual_entry" model="ir.actions.server">
<field name="name">Create Accrual Entries</field>
<record id="action_automatic_entry" model="ir.actions.server">
<field name="name">Automatic Entries</field>
<field name="model_id" ref="account.model_account_move_line"/>
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
<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>
<field name="code">action = records.action_automatic_entry()</field>
</record>
<record id="action_account_moves_all_a" model="ir.actions.act_window">
@@ -1408,16 +1421,5 @@ if records:
</field>
</record>
<record model="ir.actions.server" id="action_move_transfer_accounts">
<field name="name">Transfer Entries</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">
if records:
action = records.action_transfer_accounts_wizard()
</field>
</record>
</data>
</odoo>
+1 -2
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_accrual_accounting
from . import account_automatic_entry_wizard
from . import account_unreconcile
from . import account_validate_account_move
from . import pos_box
@@ -15,5 +15,4 @@ from . import setup_wizards
from . import wizard_tax_adjustments
from . import account_invoice_send
from . import base_document_layout
from . import account_transfer_wizard
from . import account_payment_register
@@ -1,248 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.misc import format_date
import json
class AccrualAccountingWizard(models.TransientModel):
_name = 'account.accrual.accounting.wizard'
_description = 'Create accrual entries.'
date = fields.Date(required=True)
company_id = fields.Many2one('res.company', required=True, readonly=True)
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, readonly=False,
domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
related="company_id.accrual_default_journal_id")
expense_accrual_account = fields.Many2one('account.account', readonly=False,
domain="[('company_id', '=', company_id), ('internal_type', 'not in', ('receivable', 'payable')), ('internal_group', '=', 'liability'), ('reconcile', '=', True)]",
related="company_id.expense_accrual_account_id")
revenue_accrual_account = fields.Many2one('account.account', readonly=False,
domain="[('company_id', '=', company_id), ('internal_type', 'not in', ('receivable', 'payable')), ('internal_group', '=', 'asset'), ('reconcile', '=', True)]",
related="company_id.revenue_accrual_account_id")
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')
data = fields.Text(compute="_compute_data")
preview_data = fields.Text(compute="_compute_preview_data")
@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)
if not set(fields) & set(['active_move_line_ids', 'company_id', 'account_type']):
return super().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.state != 'posted' for move in active_move_line_ids.mapped('move_id')):
raise UserError(_('You can only change the period for posted journal items.'))
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.'))
if any(line.company_id != active_move_line_ids[0].company_id for line in active_move_line_ids):
raise UserError(_('All lines must be from the same company.'))
rec['company_id'] = active_move_line_ids[0].company_id.id
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: %s') % ", ".join(account_types_allowed.mapped('name')))
rec['account_type'] = active_move_line_ids[0].account_id.user_type_id.internal_group
return rec
@api.depends('active_move_line_ids', 'journal_id', 'revenue_accrual_account', 'expense_accrual_account', 'percentage', 'date', 'account_type')
def _compute_data(self):
for record in self:
# set the accrual account on the selected journal items
accrual_account = record.revenue_accrual_account if record.account_type == 'income' else record.expense_accrual_account
move_data = {}
# compute the account.move.lines and the total amount per move
for aml in record.active_move_line_ids:
if aml.move_id not in move_data:
move_data[aml.move_id] = {
'total_amount': 0,
'move_vals': {
'new_date': {'line_ids': []},
'original_date': {'line_ids': []},
}
}
# total amount per move is used to build the account.move references and chatter messages
move_data[aml.move_id]['total_amount'] += aml.balance
# account.move.line data
reported_debit = aml.company_id.currency_id.round((record.percentage / 100) * aml.debit)
reported_credit = aml.company_id.currency_id.round((record.percentage / 100) * aml.credit)
if aml.currency_id:
reported_amount_currency = aml.currency_id.round((record.percentage / 100) * aml.amount_currency)
else:
reported_amount_currency = 0.0
move_data[aml.move_id]['move_vals']['new_date']['line_ids'] += [
(0, 0, {
'name': aml.name,
'debit': reported_debit,
'credit': reported_credit,
'amount_currency': reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': aml.account_id.id,
'partner_id': aml.partner_id.id,
}),
(0, 0, {
'name': _('Accrual Adjusting Entry'),
'debit': reported_credit,
'credit': reported_debit,
'amount_currency': -reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': accrual_account.id,
'partner_id': aml.partner_id.id,
}),
]
move_data[aml.move_id]['move_vals']['original_date']['line_ids'] += [
(0, 0, {
'name': aml.name,
'debit': reported_credit,
'credit': reported_debit,
'amount_currency': -reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': aml.account_id.id,
'partner_id': aml.partner_id.id,
}),
(0, 0, {
'name': _('Accrual Adjusting Entry'),
'debit': reported_debit,
'credit': reported_credit,
'amount_currency': reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': accrual_account.id,
'partner_id': aml.partner_id.id,
}),
]
# complete the account.move data
for move in record.active_move_line_ids.move_id:
def format_strings(string):
return string.format(
percent=record.percentage,
name=move.name,
id=move.id,
origin_amount=origin_amount,
date=format_date(self.env, move.date),
new_date=record.date and format_date(self.env, record.date) or _('[Not set]'),
)
origin_amount = abs(move_data[move]['total_amount'])
ref1 = format_strings(_('Adjusting Entry of {name} ({percent}% of {origin_amount} recognized from {date})'))
ref2 = format_strings(_('Adjusting Entry of {name} ({percent}% of {origin_amount} recognized on {new_date})'))
move_data[move]['move_vals']['new_date']['ref'] = ref1
move_data[move]['move_vals']['new_date']['date'] = fields.Date.to_string(record.date)
move_data[move]['move_vals']['new_date']['journal_id'] = record.journal_id.id
move_data[move]['move_vals']['original_date']['ref'] = ref2
move_data[move]['move_vals']['original_date']['date'] = fields.Date.to_string(move.date)
move_data[move]['move_vals']['original_date']['journal_id'] = record.journal_id.id
move_data[move]['log_messages'] = {
'new_date': format_strings(
_('Accrual Adjusting Entry')
+ ' <a href=# data-oe-model=account.move data-oe-id={id}>{name}</a>: '
+ _('{percent}% of {origin_amount} recognized from {date}')
),
'original_date': format_strings(
_('Accrual Adjusting Entry for')
+ ' <a href=# data-oe-model=account.move data-oe-id={id}>{name}</a>: '
+ _('{percent}% of {origin_amount} recognized on {date}')
),
'origin': format_strings(
_('Accrual Adjusting Entries have been created for this invoice')
+ ':<ul><li><a href=# data-oe-model=account.move data-oe-id=%(second_id)d>%(second_name)s</a> '
+ _('cancelling {percent}%% of {origin_amount} ')
+ '</li><li><a href=# data-oe-model=account.move data-oe-id=%(first_id)d>%(first_name)s</a> '
+ _('postponing it to {date}')
+ '</li></ul>'
)
}
move_vals = [m for o in move_data.values() for m in o['move_vals'].values()]
log_messages = [o['log_messages'] for o in move_data.values()]
record.data = json.dumps({
'move_vals': move_vals,
'log_messages': log_messages,
})
@api.depends('data')
def _compute_preview_data(self):
for record in self:
data = json.loads(record.data)
move_vals, log_messages = (data['move_vals'], data['log_messages'])
preview_vals = []
for move in move_vals[:4]:
preview_vals += [self.env['account.move']._move_dict_to_preview_vals(move, record.company_id.currency_id)]
preview_discarded = max(0, len(move_vals) - len(preview_vals))
preview_columns = [
{'field': 'account_id', 'label': _('Account')},
{'field': 'name', 'label': _('Label')},
{'field': 'debit', 'label': _('Debit'), 'class': 'text-right text-nowrap'},
{'field': 'credit', 'label': _('Credit'), 'class': 'text-right text-nowrap'},
]
record.preview_data = json.dumps({
'groups_vals': preview_vals,
'options': {
'discarded_number': _("%d moves", preview_discarded) if preview_discarded else False,
'columns': preview_columns,
},
})
def amend_entries(self):
accrual_account = self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account
data = json.loads(self.data)
move_vals, log_messages = (data['move_vals'], data['log_messages'])
created_moves = self.env['account.move'].create(move_vals)
created_moves.post()
# Reconcile.
index = 0
for move in self.active_move_line_ids.mapped('move_id'):
accrual_moves = created_moves[index:index + 2]
to_reconcile = accrual_moves.mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)
to_reconcile.reconcile()
move.message_post(body=log_messages[index//2]['origin'] % {
'first_id': accrual_moves[0].id,
'first_name': accrual_moves[0].name,
'second_id': accrual_moves[1].id,
'second_name': accrual_moves[1].name,
})
accrual_moves[0].message_post(body=log_messages[index//2]['new_date'])
accrual_moves[1].message_post(body=log_messages[index//2]['original_date'])
index += 2
# open the generated entries
action = {
'name': _('Generated Entries'),
'domain': [('id', 'in', created_moves.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_moves) == 1:
action.update({'view_mode': 'form', 'res_id': created_moves.id})
return action
@@ -1,42 +0,0 @@
<?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"/>
</group>
</group>
<label for="preview_data" string="The following Journal Entries will be generated"/>
<field name="preview_data" widget="grouped_view_widget"/>
<footer>
<button string="Create Journal Entries" 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">Create Accrual Entries 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>
@@ -0,0 +1,398 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.misc import format_date, formatLang
from collections import defaultdict
from itertools import groupby
import json
class AutomaticEntryWizard(models.TransientModel):
_name = 'account.automatic.entry.wizard'
_description = 'Create Automatic Entries'
# General
action = fields.Selection([('change_period', 'Change Period'), ('change_account', 'Change Account')], required=True)
move_data = fields.Text(compute="_compute_move_data", help="JSON value of the moves to be created")
preview_move_data = fields.Text(compute="_compute_preview_move_data", help="JSON value of the data to be displayed in the previewer")
move_line_ids = fields.Many2many('account.move.line')
date = fields.Date(required=True, default=fields.Date.today())
company_id = fields.Many2one('res.company', required=True, readonly=True)
company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
percentage = fields.Float("Percentage", compute='_compute_percentage', readonly=False, store=True, help="Percentage of each line to execute the action on.")
total_amount = fields.Monetary(compute='_compute_total_amount', store=True, readonly=False, currency_field='company_currency_id', help="Total amount impacted by the automatic entry.")
journal_id = fields.Many2one('account.journal', required=True, readonly=False, string="Journal",
domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
related="company_id.automatic_entry_default_journal_id",
help="Journal where to create the entry.")
# change period
account_type = fields.Selection([('income', 'Revenue'), ('expense', 'Expense')], compute='_compute_account_type', store=True)
expense_accrual_account = fields.Many2one('account.account', readonly=False,
domain="[('company_id', '=', company_id), ('internal_type', 'not in', ('receivable', 'payable'))]",
related="company_id.expense_accrual_account_id")
revenue_accrual_account = fields.Many2one('account.account', readonly=False,
domain="[('company_id', '=', company_id), ('internal_type', 'not in', ('receivable', 'payable'))]",
related="company_id.revenue_accrual_account_id")
# change account
destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.")
display_currency_helper = fields.Boolean(string="Currency Conversion Helper", compute='_compute_display_currency_helper', help="Technical field used to indicate whether or not to display the tooltip informing a currency conversion will be performed with the transfer")
@api.constrains('percentage', 'action')
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"))
if record.percentage != 100 and record.action != 'change_period':
print(record.percentage)
raise UserError(_("Percentage can only be set for Change Period method"))
@api.depends('percentage', 'move_line_ids')
def _compute_total_amount(self):
for record in self:
record.total_amount = (record.percentage or 100) * sum(record.move_line_ids.mapped('balance')) / 100
@api.depends('total_amount', 'move_line_ids')
def _compute_percentage(self):
for record in self:
record.percentage = record.total_amount / (sum(record.move_line_ids.mapped('balance')) or record.total_amount) * 100
@api.depends('move_line_ids')
def _compute_account_type(self):
for record in self:
record.account_type = 'income' if sum(record.move_line_ids.mapped('balance')) < 0 else 'expense'
@api.depends('destination_account_id')
def _compute_display_currency_helper(self):
for record in self:
record.display_currency_helper = bool(record.destination_account_id.currency_id)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if not set(fields) & set(['move_line_ids', 'company_id']):
return res
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'))
move_line_ids = self.env['account.move.line'].browse(self.env.context['active_ids'])
res['move_line_ids'] = [(6, 0, move_line_ids.ids)]
if any(move.state != 'posted' for move in move_line_ids.mapped('move_id')):
raise UserError(_('You can only change the period/account for posted journal items.'))
if any(move_line.reconciled for move_line in move_line_ids):
raise UserError(_('You can only change the period/account for items that are not yet reconciled.'))
if any(line.company_id != move_line_ids[0].company_id for line in move_line_ids):
raise UserError(_('You cannot use this wizard on journal entries belonging to different companies.'))
res['company_id'] = move_line_ids[0].company_id.id
allowed_actions = set(dict(self._fields['action'].selection))
if self.env.context.get('default_action'):
allowed_actions = {self.env.context['default_action']}
if any(line.account_id.user_type_id != move_line_ids[0].account_id.user_type_id for line in move_line_ids):
allowed_actions.discard('change_period')
if not allowed_actions:
raise UserError(_('No possible action found with the selected lines.'))
res['action'] = allowed_actions.pop()
return res
def _get_move_dict_vals_change_account(self):
line_vals = []
# Group data from selected move lines
counterpart_balances = defaultdict(lambda: defaultdict(lambda: 0))
grouped_source_lines = defaultdict(lambda: self.env['account.move.line'])
for line in self.move_line_ids.filtered(lambda x: x.account_id != self.destination_account_id):
counterpart_currency = line.currency_id
counterpart_amount_currency = line.amount_currency
if self.destination_account_id.currency_id and self.destination_account_id.currency_id != self.company_id.currency_id:
counterpart_currency = self.destination_account_id.currency_id
counterpart_amount_currency = self.company_id.currency_id._convert(line.balance, self.destination_account_id.currency_id, self.company_id, line.date)
if counterpart_currency:
counterpart_balances[(line.partner_id, counterpart_currency)]['amount_currency'] += counterpart_amount_currency
counterpart_balances[(line.partner_id, counterpart_currency)]['balance'] += line.balance
grouped_source_lines[(line.partner_id, line.currency_id, line.account_id)] += line
# Generate counterpart lines' vals
for (counterpart_partner, counterpart_currency), counterpart_vals in counterpart_balances.items():
source_accounts = self.move_line_ids.mapped('account_id')
counterpart_label = len(source_accounts) == 1 and _("Transfer from %s", source_accounts.display_name) or _("Transfer counterpart")
if not self.company_id.currency_id.is_zero(counterpart_vals['balance']) or (counterpart_currency and not counterpart_currency.is_zero(counterpart_vals['amount_currency'])):
line_vals.append({
'name': counterpart_label,
'debit': counterpart_vals['balance'] > 0 and self.company_id.currency_id.round(counterpart_vals['balance']) or 0,
'credit': counterpart_vals['balance'] < 0 and self.company_id.currency_id.round(-counterpart_vals['balance']) or 0,
'account_id': self.destination_account_id.id,
'partner_id': counterpart_partner.id or None,
'amount_currency': counterpart_currency and counterpart_currency.round((counterpart_vals['balance'] < 0 and -1 or 1) * abs(counterpart_vals['amount_currency'])) or 0,
'currency_id': counterpart_currency.id or None,
})
# Generate change_account lines' vals
for (partner, currency, account), lines in grouped_source_lines.items():
account_balance = sum(line.balance for line in lines)
if not (currency or self.company_id.currency_id).is_zero(account_balance):
account_amount_currency = currency and currency.round(sum(line.amount_currency for line in lines)) or 0
line_vals.append({
'name': _('Transfer to %s', self.destination_account_id.display_name or _('[Not set]')),
'debit': account_balance < 0 and self.company_id.currency_id.round(-account_balance) or 0,
'credit': account_balance > 0 and self.company_id.currency_id.round(account_balance) or 0,
'account_id': account.id,
'partner_id': partner.id or None,
'currency_id': currency.id or None,
'amount_currency': (account_balance > 0 and -1 or 1) * abs(account_amount_currency),
})
return [{
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
'move_type': 'entry',
'journal_id': self.journal_id.id,
'date': fields.Date.to_string(self.date),
'ref': self.destination_account_id.display_name and _("Transfer entry to %s", self.destination_account_id.display_name or ''),
'line_ids': [(0, 0, line) for line in line_vals],
}]
def _get_move_dict_vals_change_period(self):
# set the change_period account on the selected journal items
accrual_account = self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account
move_data = {'new_date': {
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
'move_type': 'entry',
'line_ids': [],
'ref': _('Adjusting Entry'),
'date': fields.Date.to_string(self.date),
'journal_id': self.journal_id.id,
}}
# complete the account.move data
for date, grouped_lines in groupby(self.move_line_ids, lambda m: m.move_id.date):
grouped_lines = list(grouped_lines)
amount = sum(l.balance for l in grouped_lines)
move_data[date] = {
'currency_id': self.journal_id.currency_id.id or self.journal_id.company_id.currency_id.id,
'move_type': 'entry',
'line_ids': [],
'ref': self._format_strings(_('Adjusting Entry of {date} ({percent:f}% recognized on {new_date})'), grouped_lines[0].move_id, amount),
'date': fields.Date.to_string(date),
'journal_id': self.journal_id.id,
}
# compute the account.move.lines and the total amount per move
for aml in self.move_line_ids:
# account.move.line data
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)
if aml.currency_id:
reported_amount_currency = aml.currency_id.round((self.percentage / 100) * aml.amount_currency)
else:
reported_amount_currency = 0.0
move_data['new_date']['line_ids'] += [
(0, 0, {
'name': aml.name,
'debit': reported_debit,
'credit': reported_credit,
'amount_currency': reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': aml.account_id.id,
'partner_id': aml.partner_id.id,
}),
(0, 0, {
'name': _('Adjusting Entry'),
'debit': reported_credit,
'credit': reported_debit,
'amount_currency': -reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': accrual_account.id,
'partner_id': aml.partner_id.id,
}),
]
move_data[aml.move_id.date]['line_ids'] += [
(0, 0, {
'name': aml.name,
'debit': reported_credit,
'credit': reported_debit,
'amount_currency': -reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': aml.account_id.id,
'partner_id': aml.partner_id.id,
}),
(0, 0, {
'name': _('Adjusting Entry'),
'debit': reported_debit,
'credit': reported_credit,
'amount_currency': reported_amount_currency,
'currency_id': aml.currency_id.id,
'account_id': accrual_account.id,
'partner_id': aml.partner_id.id,
}),
]
move_vals = [m for m in move_data.values()]
return move_vals
@api.depends('move_line_ids', 'journal_id', 'revenue_accrual_account', 'expense_accrual_account', 'percentage', 'date', 'account_type', 'action', 'destination_account_id')
def _compute_move_data(self):
for record in self:
if record.action == 'change_period':
if any(line.account_id.user_type_id != record.move_line_ids[0].account_id.user_type_id for line in record.move_line_ids):
raise UserError(_('All accounts on the lines must be of the same type.'))
if record.action == 'change_period':
record.move_data = json.dumps(record._get_move_dict_vals_change_period())
elif record.action == 'change_account':
record.move_data = json.dumps(record._get_move_dict_vals_change_account())
@api.depends('move_data')
def _compute_preview_move_data(self):
for record in self:
preview_columns = [
{'field': 'account_id', 'label': _('Account')},
{'field': 'name', 'label': _('Label')},
{'field': 'debit', 'label': _('Debit'), 'class': 'text-right text-nowrap'},
{'field': 'credit', 'label': _('Credit'), 'class': 'text-right text-nowrap'},
]
if record.action == 'change_account':
preview_columns[2:2] = [{'field': 'partner_id', 'label': _('Partner')}]
move_vals = json.loads(record.move_data)
preview_vals = []
for move in move_vals[:4]:
preview_vals += [self.env['account.move']._move_dict_to_preview_vals(move, record.company_id.currency_id)]
preview_discarded = max(0, len(move_vals) - len(preview_vals))
record.preview_move_data = json.dumps({
'groups_vals': preview_vals,
'options': {
'discarded_number': _("%d moves", preview_discarded) if preview_discarded else False,
'columns': preview_columns,
},
})
def do_action(self):
move_vals = json.loads(self.move_data)
if self.action == 'change_period':
return self._do_action_change_period(move_vals)
elif self.action == 'change_account':
return self._do_action_change_account(move_vals)
def _do_action_change_period(self, move_vals):
accrual_account = self.revenue_accrual_account if self.account_type == 'income' else self.expense_accrual_account
created_moves = self.env['account.move'].create(move_vals)
created_moves._post()
destination_move = created_moves[0]
destination_messages = []
for move in self.move_line_ids.move_id:
amount = sum((self.move_line_ids._origin & move.line_ids).mapped('balance'))
accrual_move = created_moves[1:].filtered(lambda m: m.date == move.date)
if accrual_account.reconcile:
to_reconcile = (accrual_move + destination_move).mapped('line_ids').filtered(lambda line: line.account_id == accrual_account)
to_reconcile.reconcile()
move.message_post(body=self._format_strings(_('Adjusting Entries have been created for this invoice:<ul><li>%(link1)s cancelling '
'{percent:f}%% of {amount}</li><li>%(link0)s postponing it to {new_date}</li></ul>',
link0=self._format_move_link(destination_move),
link1=self._format_move_link(accrual_move),
), move, amount))
destination_messages += [self._format_strings(_('Adjusting Entry {link}: {percent:f}% of {amount} recognized from {date}'), move, amount)]
accrual_move.message_post(body=self._format_strings(_('Adjusting Entry for {link}: {percent:f}% of {amount} recognized on {new_date}'), move, amount))
destination_move.message_post(body='<br/>\n'.join(destination_messages))
# open the generated entries
action = {
'name': _('Generated Entries'),
'domain': [('id', 'in', created_moves.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_moves) == 1:
action.update({'view_mode': 'form', 'res_id': created_moves.id})
return action
def _do_action_change_account(self, move_vals):
new_move = self.env['account.move'].create(move_vals)
new_move._post()
# Group lines
grouped_lines = defaultdict(lambda: self.env['account.move.line'])
destination_lines = self.move_line_ids.filtered(lambda x: x.account_id == self.destination_account_id)
for line in self.move_line_ids - destination_lines:
grouped_lines[(line.partner_id, line.currency_id, line.account_id)] += line
# Reconcile
for (partner, currency, account), lines in grouped_lines.items():
if account.reconcile:
to_reconcile = lines + new_move.line_ids.filtered(lambda x: x.account_id == account and x.partner_id == partner and x.currency_id == currency)
to_reconcile.reconcile()
if destination_lines and self.destination_account_id.reconcile:
to_reconcile = destination_lines + new_move.line_ids.filtered(lambda x: x.account_id == self.destination_account_id and x.partner_id == partner and x.currency_id == currency)
to_reconcile.reconcile()
# Log the operation on source moves
acc_transfer_per_move = defaultdict(lambda: defaultdict(lambda: 0)) # dict(move, dict(account, balance))
for line in self.move_line_ids:
acc_transfer_per_move[line.move_id][line.account_id] += line.balance
for move, balances_per_account in acc_transfer_per_move.items():
message_to_log = self._format_transfer_source_log(balances_per_account, new_move)
if message_to_log:
move.message_post(body=message_to_log)
# Log on target move as well
new_move.message_post(body=self._format_new_transfer_move_log(acc_transfer_per_move))
return {
'name': _("Transfer"),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'account.move',
'res_id': new_move.id,
}
# Transfer utils
def _format_new_transfer_move_log(self, acc_transfer_per_move):
format = _("<li>{amount} ({debit_credit}) from {link}, <strong>%(account_source_name)s</strong></li>")
rslt = _("This entry transfers the following amounts to <strong>%(destination)s</strong> <ul>", destination=self.destination_account_id.display_name)
for move, balances_per_account in acc_transfer_per_move.items():
for account, balance in balances_per_account.items():
if account != self.destination_account_id: # Otherwise, logging it here is confusing for the user
rslt += self._format_strings(format, move, balance) % {'account_source_name': account.display_name}
rslt += '</ul>'
return rslt
def _format_transfer_source_log(self, balances_per_account, transfer_move):
transfer_format = _("<li>{amount} ({debit_credit}) from <strong>%s</strong> were transferred to <strong>{account_target_name}</strong> by {link}</li>")
content = ''
for account, balance in balances_per_account.items():
if account != self.destination_account_id:
content += self._format_strings(transfer_format, transfer_move, balance) % account.display_name
return content and '<ul>' + content + '</ul>' or None
def _format_move_link(self, move):
move_link_format = "<a href=# data-oe-model=account.move data-oe-id={move_id}>{move_name}</a>"
return move_link_format.format(move_id=move.id, move_name=move.name)
def _format_strings(self, string, move, amount):
return string.format(
percent=self.percentage,
name=move.name,
id=move.id,
amount=formatLang(self.env, abs(amount), currency_obj=self.company_id.currency_id),
debit_credit=amount < 0 and _('C') or _('D'),
link=self._format_move_link(move),
date=format_date(self.env, move.date),
new_date=self.date and format_date(self.env, self.date) or _('[Not set]'),
account_target_name=self.destination_account_id.display_name,
)
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_automatic_entry_wizard_form" model="ir.ui.view">
<field name="name">account.automatic.entry.wizard.form</field>
<field name="model">account.automatic.entry.wizard</field>
<field name="arch" type="xml">
<form>
<field name="account_type" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="display_currency_helper" invisible="1"/>
<div attrs="{'invisible': [('display_currency_helper', '=', False)]}" class="alert alert-info text-center" role="status">
The selected destination account is set to use a specific currency. Every entry transferred to it will be converted into this currency, causing
the loss of any pre-existing foreign currency amount.
</div>
<field name="action" invisible="context.get('hide_automatic_options')" widget="radio" options="{'horizontal': true}"/>
<group>
<group attrs="{'invisible': [('action', '!=', 'change_period')]}">
<field name="date" string="Recognition Date"/>
<field name="expense_accrual_account" string="Accrued Account"
attrs="{'invisible': [('account_type', '!=', 'expense')], 'required': [('account_type', '=', 'expense'), ('action', '=', 'change_period')]}"/>
<field name="revenue_accrual_account" string="Accrued Account"
attrs="{'invisible': [('account_type', '!=', 'income')], 'required': [('account_type', '=', 'income'), ('action', '=', 'change_period')]}"/>
</group>
<group attrs="{'invisible': [('action', '!=', 'change_account')]}">
<field name="date" string="Transfer Date"/>
<field name="destination_account_id" attrs="{'required': [('action', '=', 'change_account')]}" domain="[('company_id', '=', company_id)]"/>
</group>
<group>
<label for="total_amount" string="Adjusting Amount" attrs="{'invisible': [('action', '!=', 'change_period')]}"/>
<div attrs="{'invisible': [('action', '!=', 'change_period')]}">
<field name="percentage" style="width:40% !important" class="oe_inline" attrs="{'readonly': [('action', '!=', 'change_period')]}"/>%<span class="px-3"></span>(<field name="total_amount" class="oe_inline"/>)
</div>
<field name="total_amount" readonly="1" attrs="{'invisible': [('action', '=', 'change_period')]}"/>
<field name="journal_id"/>
</group>
</group>
<label for="preview_move_data" string="The following Journal Entries will be generated"/>
<field name="preview_move_data" widget="grouped_view_widget"/>
<footer>
<button string="Create Journal Entries" name="do_action" type="object" class="oe_highlight"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="account_automatic_entry_wizard_action" model="ir.actions.act_window">
<field name="name">Create Automatic Entries for selected Journal Items</field>
<field name="res_model">account.automatic.entry.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>
@@ -1,195 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.misc import format_date, formatLang
from collections import defaultdict
import json
class AccountTransferWizard(models.TransientModel):
_name = 'account.transfer.wizard'
_description = "Account Transfer Wizard"
move_line_ids = fields.One2many(string="Journal Items", comodel_name='account.move.line', compute="_retrieve_fields_from_context", help="Journal entries to transfer accounts from.")
destination_account_id = fields.Many2one(string="To", comodel_name='account.account', help="Account to transfer to.")
journal_id = fields.Many2one(string="Journal", comodel_name='account.journal', help="Journal where to create the transfer entry.")
date = fields.Date(string="Date", help="Date to be set on the transfer entry.", default=fields.Date.today)
company_id = fields.Many2one(string="Company", comodel_name='res.company', compute='_retrieve_fields_from_context')
display_currency_helper = fields.Boolean(string="Currency Conversion Helper", compute='_compute_display_currency_helper', help="Technical field used to indicate whether or not to display the tooltip informing a currency conversion will be performed with the transfer")
aml_preview_data = fields.Text(string="AML Preview Data", compute='_compute_aml_preview_data', help="Preview JSON data for aml preview widget")
@api.depends_context('active_model')
def _retrieve_fields_from_context(self):
if self._context.get('active_model') == 'account.move.line':
aml = self.env['account.move.line'].browse(self._context.get('active_ids', []))
company = aml.mapped('company_id')
if len(company) > 1:
raise UserError(_("You cannot create a transfer for entries belonging to different companies."))
for record in self:
record.move_line_ids = aml
record.company_id = company
else:
raise UserError(_("Transfer wizard should only be called on account.move.line objects"))
@api.depends('destination_account_id')
def _compute_display_currency_helper(self):
for record in self:
record.display_currency_helper = bool(record.destination_account_id.currency_id)
@api.depends('destination_account_id')
def _compute_aml_preview_data(self):
for record in self:
record.aml_preview_data = json.dumps({
'groups_vals': [self.env['account.move']._move_dict_to_preview_vals(self._get_move_to_create_dict(), record.company_id.currency_id)],
'options': {
'columns': [
{'field': 'account_id', 'label': _('Account')},
{'field': 'name', 'label': _('Label')},
{'field': 'partner_id', 'label': _('Partner')},
{'field': 'debit', 'label': _('Debit'), 'class': 'text-right text-nowrap'},
{'field': 'credit', 'label': _('Credit'), 'class': 'text-right text-nowrap'},
],
},
})
def _get_move_to_create_dict(self):
line_vals = []
# Group data from selected move lines
counterpart_balances = defaultdict(lambda: defaultdict(lambda: 0))
grouped_source_lines = defaultdict(lambda: self.env['account.move.line'])
for line in self.move_line_ids.filtered(lambda x: x.account_id != self.destination_account_id):
counterpart_currency = line.currency_id
counterpart_amount_currency = line.amount_currency
if self.destination_account_id.currency_id and self.destination_account_id.currency_id != self.company_id.currency_id:
counterpart_currency = self.destination_account_id.currency_id
counterpart_amount_currency = self.company_id.currency_id._convert(line.balance, self.destination_account_id.currency_id, self.company_id, line.date)
if counterpart_currency:
counterpart_balances[(line.partner_id, counterpart_currency)]['amount_currency'] += counterpart_amount_currency
counterpart_balances[(line.partner_id, counterpart_currency)]['balance'] += line.balance
grouped_source_lines[(line.partner_id, line.currency_id, line.account_id)] += line
# Generate counterpart lines' vals
for (counterpart_partner, counterpart_currency), counterpart_vals in counterpart_balances.items():
source_accounts = self.move_line_ids.mapped('account_id')
counterpart_label = len(source_accounts) == 1 and _("Transfer from %s", source_accounts.display_name) or _("Transfer counterpart")
if not self.company_id.currency_id.is_zero(counterpart_vals['balance']) or (counterpart_currency and not counterpart_currency.is_zero(counterpart_vals['amount_currency'])):
line_vals.append({
'name': counterpart_label,
'debit': counterpart_vals['balance'] > 0 and self.company_id.currency_id.round(counterpart_vals['balance']) or 0,
'credit': counterpart_vals['balance'] < 0 and self.company_id.currency_id.round(-counterpart_vals['balance']) or 0,
'account_id': self.destination_account_id.id,
'partner_id': counterpart_partner.id or None,
'amount_currency': counterpart_currency and counterpart_currency.round((counterpart_vals['balance'] < 0 and -1 or 1) * abs(counterpart_vals['amount_currency'])) or 0,
'currency_id': counterpart_currency.id or None,
})
# Generate transfer lines' vals
for (partner, currency, account), lines in grouped_source_lines.items():
account_balance = sum(line.balance for line in lines)
if not (currency or self.company_id.currency_id).is_zero(account_balance):
account_amount_currency = currency and currency.round(sum(line.amount_currency for line in lines)) or 0
line_vals.append({
'name': _('Transfer to %s') % (self.destination_account_id.display_name or _('Destination Account')),
'debit': account_balance < 0 and self.company_id.currency_id.round(-account_balance) or 0,
'credit': account_balance > 0 and self.company_id.currency_id.round(account_balance) or 0,
'account_id': account.id,
'partner_id': partner.id or None,
'currency_id': currency.id or None,
'amount_currency': (account_balance > 0 and -1 or 1) * abs(account_amount_currency),
})
return {
'journal_id': self.journal_id.id,
'date': self.date,
'ref': self.destination_account_id.display_name and _("Transfer entry to %s", self.destination_account_id.display_name) or '',
'line_ids': [(0, 0, line) for line in line_vals],
}
def button_transfer(self):
new_move = self.env['account.move'].create(self._get_move_to_create_dict())
new_move.post()
# Group lines
grouped_lines = defaultdict(lambda: self.env['account.move.line'])
destination_lines = self.move_line_ids.filtered(lambda x: x.account_id == self.destination_account_id)
for line in self.move_line_ids - destination_lines:
grouped_lines[(line.partner_id, line.currency_id, line.account_id)] += line
# Reconcile
for (partner, currency, account), lines in grouped_lines.items():
if account.reconcile:
to_reconcile = lines + new_move.line_ids.filtered(lambda x: x.account_id == account and x.partner_id == partner and x.currency_id == currency)
to_reconcile.reconcile()
if destination_lines and self.destination_account_id.reconcile:
to_reconcile = destination_lines + new_move.line_ids.filtered(lambda x: x.account_id == self.destination_account_id and x.partner_id == partner and x.currency_id == currency)
to_reconcile.reconcile()
# Log the operation on source moves
acc_transfer_per_move = defaultdict(lambda: defaultdict(lambda: 0)) # dict(move, dict(account, balance))
for line in self.move_line_ids:
acc_transfer_per_move[line.move_id][line.account_id] += line.balance
for move, balances_per_account in acc_transfer_per_move.items():
message_to_log = self._format_source_log(balances_per_account, new_move)
if message_to_log:
move.message_post(body=message_to_log)
# Log on target move as well
new_move.message_post(body=self._format_new_move_log(acc_transfer_per_move))
return {
'name': _("Transfer"),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'account.move',
'res_id': new_move.id,
}
def _format_new_move_log(self, acc_transfer_per_move):
format = _("%(amount)s %(currency_symbol)s (%(debit_credit)s) from %(link)s, <strong>%(account_source_name)s</strong>")
rslt = _("This entry transfers the following amounts to <strong>%(destination)s</strong> <ul>") % {'destination': self.destination_account_id.display_name}
for move, balances_per_account in acc_transfer_per_move.items():
for account, balance in balances_per_account.items():
if account != self.destination_account_id: # Otherwise, logging it here is confusing for the user
rslt += ('<li>' + format +'</li>') % {
'amount': abs(balance),
'debit_credit': balance < 0 and _('C') or _('D'),
'currency_symbol': self.company_id.currency_id.symbol,
'account_source_name': account.display_name,
'link': self._format_move_link(move),
}
rslt += '</ul>'
return rslt
def _format_source_log(self, balances_per_account, transfer_move):
transfer_format = _("%(amount)s %(currency_symbol)s (%(debit_credit)s) from <strong>%(account_source_name)s</strong> were transferred to <strong>%(account_target_name)s</strong> by %(link)s")
content = ''
for account, balance in balances_per_account.items():
if account != self.destination_account_id:
content += ('<li>' + transfer_format + '</li>') % {
'amount': abs(balance),
'debit_credit': balance < 0 and _('C') or _('D'),
'currency_symbol': self.company_id.currency_id.symbol,
'account_source_name': account.display_name,
'account_target_name': self.destination_account_id.display_name,
'link': self._format_move_link(transfer_move),
}
return content and '<ul>' + content + '</ul>' or None
def _format_move_link(self, move):
move_link_format = "<a href=# data-oe-model=account.move data-oe-id=%(move_id)s>%(move_name)s</a>"
return move_link_format % {'move_id': move.id, 'move_name': move.name}
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_move_transfer_accounts_wizard" model="ir.ui.view">
<field name="name">account.transfer.wizard.form</field>
<field name="model">account.transfer.wizard</field>
<field name="arch" type="xml">
<form>
<field name="display_currency_helper" invisible="1"/>
<div attrs="{'invisible': [('display_currency_helper', '=', False)]}" class="alert alert-info text-center" role="status">
The selected destination account is set to use a specific currency. Every entry transferred to it will be converted into this currency, causing
the loss of any pre-existing foreign currency amount.
</div>
<group>
<group>
<field name="company_id" invisible="1"/>
<field name="destination_account_id" required="1" domain="[('company_id', '=', company_id)]"/>
<field name="date" required="1"/>
</group>
<group>
<field name="journal_id" required="1" domain="[('type', '=', 'general'), ('company_id', '=', company_id)]"/>
</group>
</group>
<p><strong>The following journal entry will be generated</strong></p>
<field name="aml_preview_data" widget="grouped_view_widget"/>
<footer>
<button string="Transfer Entries" name="button_transfer" type="object" class="oe_highlight"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>