diff options
| -rw-r--r-- | addons/account/__manifest__.py | 3 | ||||
| -rw-r--r-- | addons/account/models/account_move.py | 19 | ||||
| -rw-r--r-- | addons/account/models/company.py | 6 | ||||
| -rw-r--r-- | addons/account/security/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | addons/account/tests/test_account_move_in_invoice.py | 5 | ||||
| -rw-r--r-- | addons/account/tests/test_account_move_out_invoice.py | 5 | ||||
| -rw-r--r-- | addons/account/tests/test_transfer_wizard.py | 20 | ||||
| -rw-r--r-- | addons/account/views/account_move_views.xml | 30 | ||||
| -rw-r--r-- | addons/account/wizard/__init__.py | 3 | ||||
| -rw-r--r-- | addons/account/wizard/account_accrual_accounting.py | 248 | ||||
| -rw-r--r-- | addons/account/wizard/account_accrual_accounting_view.xml | 42 | ||||
| -rw-r--r-- | addons/account/wizard/account_automatic_entry_wizard.py | 398 | ||||
| -rw-r--r-- | addons/account/wizard/account_automatic_entry_wizard_views.xml | 55 | ||||
| -rw-r--r-- | addons/account/wizard/account_transfer_wizard.py | 195 | ||||
| -rw-r--r-- | addons/account/wizard/account_transfer_wizard.xml | 41 |
15 files changed, 496 insertions, 577 deletions
diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 1ec5bdde556..0ebd0cac36f 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -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', diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 621cbb22b23..fccccb49ae6 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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)]] diff --git a/addons/account/models/company.py b/addons/account/models/company.py index acebbb209bf..2f3f1e5864c 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -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') diff --git a/addons/account/security/ir.model.access.csv b/addons/account/security/ir.model.access.csv index 4725df73507..8020b7d06ec 100644 --- a/addons/account/security/ir.model.access.csv +++ b/addons/account/security/ir.model.access.csv @@ -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 diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index 69c85523809..bd8e9a09b42 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -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, [ { diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index b18edefe04e..e77eff26027 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -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, [ { diff --git a/addons/account/tests/test_transfer_wizard.py b/addons/account/tests/test_transfer_wizard.py index 76a393f790d..03b76235398 100644 --- a/addons/account/tests/test_transfer_wizard.py +++ b/addons/account/tests/test_transfer_wizard.py @@ -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) diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 09f700adb78..8738b921ce7 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -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> diff --git a/addons/account/wizard/__init__.py b/addons/account/wizard/__init__.py index a130d327938..b88c746d3f5 100644 --- a/addons/account/wizard/__init__.py +++ b/addons/account/wizard/__init__.py @@ -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 diff --git a/addons/account/wizard/account_accrual_accounting.py b/addons/account/wizard/account_accrual_accounting.py deleted file mode 100644 index 2f22ac54e11..00000000000 --- a/addons/account/wizard/account_accrual_accounting.py +++ /dev/null @@ -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 diff --git a/addons/account/wizard/account_accrual_accounting_view.xml b/addons/account/wizard/account_accrual_accounting_view.xml deleted file mode 100644 index 98b88d94530..00000000000 --- a/addons/account/wizard/account_accrual_accounting_view.xml +++ /dev/null @@ -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> diff --git a/addons/account/wizard/account_automatic_entry_wizard.py b/addons/account/wizard/account_automatic_entry_wizard.py new file mode 100644 index 00000000000..920e209c5a5 --- /dev/null +++ b/addons/account/wizard/account_automatic_entry_wizard.py @@ -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, + ) diff --git a/addons/account/wizard/account_automatic_entry_wizard_views.xml b/addons/account/wizard/account_automatic_entry_wizard_views.xml new file mode 100644 index 00000000000..6b22bd3fa61 --- /dev/null +++ b/addons/account/wizard/account_automatic_entry_wizard_views.xml @@ -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> diff --git a/addons/account/wizard/account_transfer_wizard.py b/addons/account/wizard/account_transfer_wizard.py deleted file mode 100644 index 709adc51f74..00000000000 --- a/addons/account/wizard/account_transfer_wizard.py +++ /dev/null @@ -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} diff --git a/addons/account/wizard/account_transfer_wizard.xml b/addons/account/wizard/account_transfer_wizard.xml deleted file mode 100644 index 052bf8cdc9b..00000000000 --- a/addons/account/wizard/account_transfer_wizard.xml +++ /dev/null @@ -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> |
