diff --git a/addons/at_accounting/__init__.py b/addons/at_accounting/__init__.py deleted file mode 100644 index e50ab2e..0000000 --- a/addons/at_accounting/__init__.py +++ /dev/null @@ -1,105 +0,0 @@ -from . import models -from . import wizard -from . import controllers - -from odoo import Command - -import logging - -_logger = logging.getLogger(__name__) - - -def _at_accounting_post_init(env): - country_code = env.company.country_id.code - if country_code: - module_list = [] - - sepa_zone = env.ref('base.sepa_zone', raise_if_not_found=False) - sepa_zone_country_codes = sepa_zone and sepa_zone.mapped('country_ids.code') or [] - - if country_code in sepa_zone_country_codes: - module_list.extend(['account_iso20022', 'account_bank_statement_import_camt']) - - module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')]) - if module_ids: - module_ids.sudo().button_install() - - for company in env['res.company'].search([('chart_template', '!=', False)], order="parent_path"): - ChartTemplate = env['account.chart.template'].with_company(company) - ChartTemplate._load_data({ - 'res.company': ChartTemplate._get_account_accountant_res_company(company.chart_template), - }) - - country_code = env.company.country_id.code - if country_code: - module_list = [] - - if country_code in ('AU', 'CA', 'US'): - module_list.append('account_reports_cash_basis') - - module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')]) - if module_ids: - module_ids.sudo().button_install() - - for company in env['res.company'].search([]): - company.account_tax_periodicity_journal_id = company._get_default_misc_journal() - company.account_tax_periodicity_journal_id.show_on_dashboard = True - company._initiate_account_onboardings() - - -def uninstall_hook(env): - group_basic = env.ref('account.group_account_basic') - group_manager = env.ref('account.group_account_manager') - if group_basic: - group_basic.write({ - 'users': [Command.clear()], - 'category_id': env.ref("base.module_category_hidden").id, - }) - group_manager.write({ - 'implied_ids': [Command.unlink(group_basic.id)], - }) - - - try: - group_user = env.ref("account.group_account_user") - group_user.write({ - 'name': "Show Full Accounting Features", - 'implied_ids': [(3, env.ref('account.group_account_invoice').id)], - 'category_id': env.ref("base.module_category_hidden").id, - }) - group_readonly = env.ref("account.group_account_readonly") - group_readonly.write({ - 'name': "Show Full Accounting Features - Readonly", - 'category_id': env.ref("base.module_category_hidden").id, - }) - except ValueError as e: - _logger.warning(e) - - try: - group_manager = env.ref("account.group_account_manager") - group_manager.write({'name': "Billing Manager", - 'implied_ids': [(4, env.ref("account.group_account_invoice").id), - (3, env.ref("account.group_account_readonly").id), - (3, env.ref("account.group_account_user").id)]}) - except ValueError as e: - _logger.warning(e) - - # make the account_accountant features disappear (magic) - env.ref("account.group_account_user").write({'users': [(5, False, False)]}) - env.ref("account.group_account_readonly").write({'users': [(5, False, False)]}) - - - invoicing_menu = env.ref("account.menu_finance") - menus_to_move = [ - "account.menu_finance_receivables", - "account.menu_finance_payables", - "account.menu_finance_entries", - "account.menu_finance_reports", - "account.menu_finance_configuration", - "account.menu_board_journal_1", - ] - for menu_xmlids in menus_to_move: - try: - env.ref(menu_xmlids).parent_id = invoicing_menu - except ValueError as e: - _logger.warning(e) diff --git a/addons/at_accounting/__manifest__.py b/addons/at_accounting/__manifest__.py deleted file mode 100644 index 8bef4f7..0000000 --- a/addons/at_accounting/__manifest__.py +++ /dev/null @@ -1,178 +0,0 @@ -{ - 'name': "Odoo 18 Vera Accounting", - 'version': "18.0.1.7", - 'category': 'Accounting/Accounting', - 'sequence': 1, - 'summary': "A complete accounting toolkit for Odoo 18 Community with advanced reports, wizards, and workflows.", - 'description': """ -======================================================== -Your Complete Professional Accounting Suite for Odoo 18 -======================================================== - -Transform your Odoo 18 Community into a powerful, professional-grade financial management system. This module provides the advanced tools and deep financial insights that growing businesses need to thrive. - -Move beyond standard accounting with a comprehensive toolkit designed to improve accuracy, streamline workflows, and empower you to make smarter, data-driven decisions. - -Key Features: -------------- -* **Advanced Reporting Engine:** Generate a full suite of essential financial reports on-demand, including: - * Profit and Loss (P&L) - * Balance Sheet - * Cash Flow Statement - * Aged Receivables & Payables - * General Ledger & Partner Ledger - * Trial Balance - * Comprehensive Tax Reports - * ...and many more. - -* **Interactive Financial Dashboards:** Visualize your financial health with intuitive and dynamic dashboard components, bringing your numbers to life. - -* **Streamlined Bank Reconciliation:** Utilize an enhanced bank reconciliation widget and powerful auto-reconciliation wizards to manage your statements faster and more accurately. - -* **Powerful Accounting Wizards:** Simplify complex processes with guided, user-friendly wizards for tasks like Fiscal Year Closing, Multicurrency Revaluation, and Report Exporting. - -* **Guided User Tours:** Onboard your team quickly and ensure they can leverage all the powerful new features with integrated user tours. - -* **Professional PDF Exports:** Create clean, professionally formatted PDF documents for all your financial reports, ready for sharing with stakeholders. - -Empower Your Finance Team -------------------------- -This module provides your team with the information they need, right where they need it. Reduce manual work, eliminate errors, and give your accountants the tools they need to perform at their best. - """, - 'icon': 'static/description/icon.png', - 'author': 'Vera Software', - "website": "https://www.erp-tradepro.com/", - 'support': 'vera@Software.com', - 'maintainer': 'Vera Software', - 'depends': ['account','web_tour', 'stock_account', 'base_import'], - 'data': [ - 'data/ir_cron.xml', - 'data/digest_data.xml', - 'data/at_accounting_tour.xml', - 'data/at_accounting_data.xml', - 'data/pdf_export_templates.xml', - 'data/balance_sheet.xml', - 'data/cash_flow_report.xml', - 'data/executive_summary.xml', - 'data/profit_and_loss.xml', - 'data/bank_reconciliation_report.xml', - 'data/aged_partner_balance.xml', - 'data/general_ledger.xml', - 'data/trial_balance.xml', - 'data/sales_report.xml', - 'data/partner_ledger.xml', - 'data/multicurrency_revaluation_report.xml', - 'data/deferred_reports.xml', - 'data/journal_report.xml', - 'data/generic_tax_report.xml', - 'views/account_report_view.xml', - 'data/account_report_actions.xml', - 'data/report_send_cron.xml', - 'data/menuitems.xml', - 'data/mail_activity_type_data.xml', - 'data/mail_templates.xml', - - 'security/ir.model.access.csv', - 'security/at_accounting_security.xml', - 'security/accounting_security.xml', - - 'views/account_account_views.xml', - 'views/account_fiscal_year_view.xml', - 'views/account_journal_dashboard_views.xml', - 'views/account_move_views.xml', - 'views/account_payment_views.xml', - 'views/account_reconcile_views.xml', - 'views/account_reconcile_model_views.xml', - 'views/at_accounting_menuitems.xml', - 'views/digest_views.xml', - 'views/res_config_settings_views.xml', - 'views/product_views.xml', - 'views/bank_rec_widget_views.xml', - 'views/report_invoice.xml', - 'views/partner_views.xml', - 'views/account_activity.xml', - 'views/account_tax_views.xml', - 'views/mail_activity_views.xml', - 'views/report_template.xml', - 'views/res_company_views.xml', - 'views/res_partner_views.xml', - - 'wizard/account_change_lock_date.xml', - 'wizard/account_auto_reconcile_wizard.xml', - 'wizard/account_reconcile_wizard.xml', - 'wizard/reconcile_model_wizard.xml', - 'wizard/account_report_send.xml', - 'wizard/multicurrency_revaluation.xml', - 'wizard/report_export_wizard.xml', - 'wizard/account_report_file_download_error_wizard.xml', - 'wizard/fiscal_year.xml', - 'wizard/mail_activity_schedule_views.xml', - 'security/at_account_asset_security.xml', - 'security/ir.model.access.csv', - 'wizard/asset_modify_views.xml', - # 'views/account_account_views.xml', - 'views/account_asset_views.xml', - 'views/account_asset_group_views.xml', - # 'views/account_move_views.xml', - 'data/assets_reports.xml', - 'data/account_report_actions_depr.xml', - 'views/account_bank_statement_import_view.xml', - - ], - 'demo': [ - 'demo/at_accounting_demo.xml', - 'demo/partner_bank.xml', - - ], - 'installable': True, - 'application': True, - 'post_init_hook': '_at_accounting_post_init', - 'uninstall_hook': "uninstall_hook", - 'license': 'OPL-1', - 'assets':{ - 'web.assets_backend': [ - 'at_accounting/static/src/js/tours/at_accounting.js', - 'at_accounting/static/src/components/**/*', - 'at_accounting/static/src/**/*.xml', - 'at_accounting/static/src/js/**/*', - 'at_accounting/static/src/widgets/**/*', - # Root-level JS files not covered by the patterns above - 'at_accounting/static/src/account_bank_statement_import_model.js', - 'at_accounting/static/src/bank_statement_csv_import_action.js', - 'at_accounting/static/src/bank_statement_csv_import_model.js', - # Bank reconciliation view files (outside /components/) - 'at_accounting/static/src/bank_reconciliation/**/*', - # Backend-only SCSS (excludes PDF stylesheets and dark-mode files) - 'at_accounting/static/src/scss/account_asset.scss', - ], - 'web.assets_unit_tests': [ - 'at_accounting/static/tests/**/*', - ('remove', 'at_accounting/static/tests/tours/**/*'), - 'at_accounting/static/tests/*.js', - 'at_accounting/static/tests/account_report/**/*.js', - ], - 'web.assets_tests': [ - 'at_accounting/static/tests/tours/**/*', - ], - 'at_accounting.assets_pdf_export': [ - ('include', 'web._assets_helpers'), - 'web/static/src/scss/pre_variables.scss', - 'web/static/lib/bootstrap/scss/_variables.scss', - 'web/static/lib/bootstrap/scss/_variables-dark.scss', - 'web/static/lib/bootstrap/scss/_maps.scss', - ('include', 'web._assets_bootstrap_backend'), - 'web/static/fonts/fonts.scss', - 'at_accounting/static/src/scss/**/*', - ], - 'web.report_assets_common': [ - 'at_accounting/static/src/scss/account_pdf_export_template.scss', - ], - 'web.assets_web_dark': [ - 'at_accounting/static/src/scss/*.dark.scss', - ], - 'web.qunit_suite_tests': [ - 'at_accounting/static/tests/legacy/*.js', - ], - }, - 'images': ['static/description/banner.png'], -} \ No newline at end of file diff --git a/addons/at_accounting/__pycache__/__init__.cpython-312.pyc b/addons/at_accounting/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 8725f9b..0000000 Binary files a/addons/at_accounting/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/controllers/__init__.py b/addons/at_accounting/controllers/__init__.py deleted file mode 100644 index 12a7e52..0000000 --- a/addons/at_accounting/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import main diff --git a/addons/at_accounting/controllers/main.py b/addons/at_accounting/controllers/main.py deleted file mode 100644 index efe0bcc..0000000 --- a/addons/at_accounting/controllers/main.py +++ /dev/null @@ -1,89 +0,0 @@ -import werkzeug -from werkzeug.exceptions import InternalServerError - -from odoo.addons.at_accounting.models.account_report import AccountReportFileDownloadException -from odoo.addons.account.controllers.download_docs import _get_headers -from odoo import http -from odoo.models import check_method_name -from odoo.http import content_disposition, request -from odoo.tools.misc import html_escape - -import json - - -class AccountReportController(http.Controller): - - @http.route('/at_accounting', type='http', auth='user', methods=['POST'], csrf=False) - def get_report(self, options, file_generator, **kwargs): - uid = request.uid - options = json.loads(options) - - allowed_company_ids = request.env['account.report'].get_report_company_ids(options) - if not allowed_company_ids: - company_str = request.cookies.get('cids', str(request.env.user.company_id.id)) - allowed_company_ids = [int(str_id) for str_id in company_str.split('-')] - - report = request.env['account.report'].with_user(uid).with_context(allowed_company_ids=allowed_company_ids).browse(options['report_id']) - - try: - check_method_name(file_generator) - generated_file_data = report.dispatch_report_action(options, file_generator) - file_content = generated_file_data['file_content'] - file_type = generated_file_data['file_type'] - response_headers = self._get_response_headers(file_type, generated_file_data['file_name'], file_content) - - if file_type == 'xlsx': - response = request.make_response(None, headers=response_headers) - response.stream.write(file_content) - else: - response = request.make_response(file_content, headers=response_headers) - - if file_type in ('zip', 'xaf'): - # Adding direct_passthrough to the response and giving it a file - # as content means that we will stream the content of the file to the user - # Which will prevent having the whole file in memory - response.direct_passthrough = True - - return response - except AccountReportFileDownloadException as e: - if e.content: - e.content['file_content'] = e.content['file_content'].decode() - data = { - 'name': type(e).__name__, - 'arguments': [e.errors, e.content], - } - raise InternalServerError(response=self._generate_response(data)) from e - except Exception as e: # noqa: BLE001 - data = http.serialize_exception(e) - raise InternalServerError(response=self._generate_response(data)) from e - - def _generate_response(self, data): - error = { - 'code': 200, - 'message': 'Odoo Server Error', - 'data': data, - } - return request.make_response(html_escape(json.dumps(error))) - - def _get_response_headers(self, file_type, file_name, file_content): - headers = [ - ('Content-Type', request.env['account.report'].get_export_mime_type(file_type)), - ('Content-Disposition', content_disposition(file_name)), - ] - - if file_type in ('xml', 'txt', 'csv', 'kvr', 'csv'): - headers.append(('Content-Length', len(file_content))) - - return headers - - @http.route('/at_accounting/download_attachments/', type='http', auth='user') - def download_report_attachments(self, attachments): - attachments.check_access('read') - assert all(attachment.res_id and attachment.res_model == 'res.partner' for attachment in attachments) - if len(attachments) == 1: - headers = _get_headers(attachments.name, attachments.mimetype, attachments.raw) - return request.make_response(attachments.raw, headers) - else: - content = attachments._build_zip_from_attachments() - headers = _get_headers('attachments.zip', 'zip', content) - return request.make_response(content, headers) diff --git a/addons/at_accounting/data/account_report_actions.xml b/addons/at_accounting/data/account_report_actions.xml deleted file mode 100644 index 5b1bd82..0000000 --- a/addons/at_accounting/data/account_report_actions.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - Cash Flow Statement - account_report - - - - - Balance Sheet - account_report - balance-sheet - - - - - Executive Summary - account_report - executive-summary - - - - - Profit and Loss - account_report - profit-and-loss - - - - - Tax Return - account_report - tax-report - - - - - Journal Audit - account_report - journal-report - - - - - General Ledger - account_report - general-ledger - - - - - Unrealized Currency Gains/Losses - account_report - - - - - Aged Receivable - account_report - aged-receivable - - - - - Aged Payable - account_report - aged-payable - - - - - Trial Balance - account_report - trial-balance - - - - - Partner Ledger - account_report - partner-ledger - - - - - EC Sales List - account_report - - - - - Deferred Expense - account_report - deferred-expense - - - - Deferred Revenue - account_report - deferred-revenue - - - - - - - - - - - - - - - - - Create Menu Item - - - code - form - - if records: - action = records._create_menu_item_for_report() - - - - - Accounting Reports - account.report - list,form - - - - - - Horizontal Groups - account.report.horizontal.group - list,form - - - - - Bank Reconciliation - account_report - - - - - Financial Budgets - account.report.budget - list,form - - - - - - diff --git a/addons/at_accounting/data/account_report_actions_depr.xml b/addons/at_accounting/data/account_report_actions_depr.xml deleted file mode 100644 index 49c483a..0000000 --- a/addons/at_accounting/data/account_report_actions_depr.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - Depreciation Schedule - account_report - - - - diff --git a/addons/at_accounting/data/aged_partner_balance.xml b/addons/at_accounting/data/aged_partner_balance.xml deleted file mode 100644 index 71f6585..0000000 --- a/addons/at_accounting/data/aged_partner_balance.xml +++ /dev/null @@ -1,326 +0,0 @@ - - - - Aged Receivable - - - - - receivable - never - - selector - today - - - - Invoice Date - invoice_date - date - - - - Amount Currency - amount_currency - - - Currency - currency - string - - - Account - account_name - string - - - At Date - period0 - - - - Period 1 - period1 - - - - Period 2 - period2 - - - - Period 3 - period3 - - - - Period 4 - period4 - - - - Older - period5 - - - - Total - total - - - - - - Aged Receivable - partner_id, id - - - invoice_date - custom - _report_custom_engine_aged_receivable - invoice_date - - - - amount_currency - custom - _report_custom_engine_aged_receivable - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_aged_receivable - currency_id - - - currency - custom - _report_custom_engine_aged_receivable - currency - - - - account_name - custom - _report_custom_engine_aged_receivable - account_name - - - - period0 - custom - _report_custom_engine_aged_receivable - period0 - - - - period1 - custom - _report_custom_engine_aged_receivable - period1 - - - - period2 - custom - _report_custom_engine_aged_receivable - period2 - - - - period3 - custom - _report_custom_engine_aged_receivable - period3 - - - - period4 - custom - _report_custom_engine_aged_receivable - period4 - - - - period5 - custom - _report_custom_engine_aged_receivable - period5 - - - - total - custom - _report_custom_engine_aged_receivable - total - - - - - - - - - Aged Payable - - - - - payable - never - - selector - today - - - - Invoice Date - invoice_date - date - - - - Amount Currency - amount_currency - - - Currency - currency - string - - - Account - account_name - string - - - At Date - period0 - - - - Period 1 - period1 - - - - Period 2 - period2 - - - - Period 3 - period3 - - - - Period 4 - period4 - - - - Older - period5 - - - - Total - total - - - - - - Aged Payable - partner_id, id - - - invoice_date - custom - _report_custom_engine_aged_payable - invoice_date - - - - amount_currency - custom - _report_custom_engine_aged_payable - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_aged_payable - currency_id - - - currency - custom - _report_custom_engine_aged_payable - currency - - - - account_name - custom - _report_custom_engine_aged_payable - account_name - - - - period0 - custom - _report_custom_engine_aged_payable - period0 - - - - period1 - custom - _report_custom_engine_aged_payable - period1 - - - - period2 - custom - _report_custom_engine_aged_payable - period2 - - - - period3 - custom - _report_custom_engine_aged_payable - period3 - - - - period4 - custom - _report_custom_engine_aged_payable - period4 - - - - period5 - custom - _report_custom_engine_aged_payable - period5 - - - - total - custom - _report_custom_engine_aged_payable - total - - - - - - - diff --git a/addons/at_accounting/data/assets_reports.xml b/addons/at_accounting/data/assets_reports.xml deleted file mode 100644 index cc8f6b3..0000000 --- a/addons/at_accounting/data/assets_reports.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Depreciation Schedule - optional - - - - - - - - Acquisition Date - acquisition_date - date - - - First Depreciation - first_depreciation - date - - - Method - method - string - - - Duration / Rate - duration_rate - string - - - date from - assets_date_from - - - + - assets_plus - - - - - assets_minus - - - date to - assets_date_to - - - date from - depre_date_from - - - + - depre_plus - - - - - depre_minus - - - date to - depre_date_to - - - book_value - balance - - - - diff --git a/addons/at_accounting/data/at_accounting_data.xml b/addons/at_accounting/data/at_accounting_data.xml deleted file mode 100644 index 7ff3b96..0000000 --- a/addons/at_accounting/data/at_accounting_data.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/at_accounting/data/at_accounting_tour.xml b/addons/at_accounting/data/at_accounting_tour.xml deleted file mode 100644 index 3cf1caf..0000000 --- a/addons/at_accounting/data/at_accounting_tour.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - account_accountant_tour - 50 - Good job! You went through all steps of this tour. -
See how to manage your customer invoices in the Customers/Invoices menu - ]]>
-
-
diff --git a/addons/at_accounting/data/balance_sheet.xml b/addons/at_accounting/data/balance_sheet.xml deleted file mode 100644 index c53b6e7..0000000 --- a/addons/at_accounting/data/balance_sheet.xml +++ /dev/null @@ -1,285 +0,0 @@ - - - - Balance Sheet - - - - - selector - today - - - - Balance - balance - - - - - ASSETS - 0 - TA - left - CA.balance + FA.balance + PNCA.balance - - - Current Assets - CA - BA.balance + REC.balance + CAS.balance + PRE.balance - - - Bank and Cash Accounts - BA - account_id - - sum([('account_id.account_type', '=', 'asset_cash')]) - - - Receivables - REC - account_id - - sum([('account_id.account_type', '=', 'asset_receivable'), ('account_id.non_trade', '=', False)]) - - - Current Assets - CAS - account_id - - sum(['|', ('account_id.account_type', '=', 'asset_current'), '&', ('account_id.account_type', '=', 'asset_receivable'), ('account_id.non_trade', '=', True)]) - - - Prepayments - PRE - account_id - - sum([('account_id.account_type', '=', 'asset_prepayments')]) - - - - - Plus Fixed Assets - FA - account_id - - sum([('account_id.account_type', '=', 'asset_fixed')]) - - - Plus Non-current Assets - PNCA - account_id - - sum([('account_id.account_type', '=', 'asset_non_current')]) - - - - - LIABILITIES - 0 - L - right - - - balance - aggregation - CL.balance + NL.balance - - - - - - Current Liabilities - CL - - - balance - aggregation - CL1.balance + CL2.balance - - - - - - Current Liabilities - CL1 - account_id - - - - balance - domain - - -sum - - - - - - Payables - CL2 - account_id - - - - balance - domain - - -sum - - - - - - - - Plus Non-current Liabilities - NL - account_id - - - - balance - domain - - -sum - - - - - - - - EQUITY - 0 - EQ - right - UNAFFECTED_EARNINGS.balance + RETAINED_EARNINGS.balance - - - Unallocated Earnings - UNAFFECTED_EARNINGS - CURR_YEAR_EARNINGS.balance + PREV_YEAR_EARNINGS.balance - - - Current Year Unallocated Earnings - CURR_YEAR_EARNINGS - - - - pnl - aggregation - NEP.balance - from_fiscalyear - cross_report - - - alloc - domain - - from_fiscalyear - -sum - - - balance - aggregation - CURR_YEAR_EARNINGS.pnl + CURR_YEAR_EARNINGS.alloc - - - - - Previous Years Unallocated Earnings - PREV_YEAR_EARNINGS - - - allocated_earnings - domain - - -sum - from_beginning - - - balance_domain - domain - - -sum - from_beginning - - - balance - aggregation - PREV_YEAR_EARNINGS.balance_domain + PREV_YEAR_EARNINGS.allocated_earnings - CURR_YEAR_EARNINGS.balance - - - - - - - Retained Earnings - RETAINED_EARNINGS - CURR_RETAINED_EARNINGS.balance + PREV_RETAINED_EARNINGS.balance - - - - - Current Year Retained Earnings - CURR_RETAINED_EARNINGS - account_id - - - - balance - domain - - -sum - from_fiscalyear - - - - - Previous Years Retained Earnings - PREV_RETAINED_EARNINGS - - - total - domain - - -sum - - - balance - aggregation - PREV_RETAINED_EARNINGS.total - CURR_RETAINED_EARNINGS.balance - - - - - - - - - LIABILITIES + EQUITY - 0 - LE - right - - - balance - aggregation - L.balance + EQ.balance - - - - - - OFF BALANCE SHEET ACCOUNTS - 0 - OS - account_id - - - -sum([('account_id.account_type', '=', 'off_balance')]) - - - - diff --git a/addons/at_accounting/data/bank_reconciliation_report.xml b/addons/at_accounting/data/bank_reconciliation_report.xml deleted file mode 100644 index 27163ac..0000000 --- a/addons/at_accounting/data/bank_reconciliation_report.xml +++ /dev/null @@ -1,474 +0,0 @@ - - - - Bank Reconciliation Report - - - - by_default - - today - - - - Date - date - date - - - Label - label - string - - - Amount Currency - amount_currency - monetary - - - Currency - currency - string - - - Amount - amount - monetary - - - - - Balance of Bank - balance_bank - 0 - - - amount - aggregation - last_statement_balance.amount + transaction_without_statement.amount + misc_operations.amount - - - - _currency_amount - custom - _report_custom_engine_forced_currency_amount - amount_currency_id - - - - - Last statement balance - last_statement_balance - - - amount - custom - _report_custom_engine_last_statement_balance_amount - amount - - - - _currency_amount - custom - _report_custom_engine_last_statement_balance_amount - amount_currency_id - - - - - Including Unreconciled Receipts - last_statement_receipts - id - - - - date - custom - _report_custom_engine_unreconciled_last_statement_receipts - date - - - - label - custom - _report_custom_engine_unreconciled_last_statement_receipts - label - - - - amount_currency - custom - _report_custom_engine_unreconciled_last_statement_receipts - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_unreconciled_last_statement_receipts - amount_currency_currency_id - - - currency - custom - _report_custom_engine_unreconciled_last_statement_receipts - currency - - - - amount - custom - _report_custom_engine_unreconciled_last_statement_receipts - amount - - - - _currency_amount - custom - _report_custom_engine_unreconciled_last_statement_receipts - amount_currency_id - - - - - Including Unreconciled Payments - last_statement_payments - id - - - - date - custom - _report_custom_engine_unreconciled_last_statement_payments - date - - - - label - custom - _report_custom_engine_unreconciled_last_statement_payments - label - - - - amount_currency - custom - _report_custom_engine_unreconciled_last_statement_payments - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_unreconciled_last_statement_payments - amount_currency_currency_id - - - currency - custom - _report_custom_engine_unreconciled_last_statement_payments - currency - - - - amount - custom - _report_custom_engine_unreconciled_last_statement_payments - amount - - - - _currency_amount - custom - _report_custom_engine_unreconciled_last_statement_payments - amount_currency_id - - - - - - - Transactions without statement - transaction_without_statement - - - amount - custom - _report_custom_engine_transaction_without_statement_amount - amount - - - - _currency_amount - custom - _report_custom_engine_transaction_without_statement_amount - amount_currency_id - - - - - Including Unreconciled Receipts - unreconciled_receipt - id - - - - date - custom - _report_custom_engine_unreconciled_receipts - date - - - - label - custom - _report_custom_engine_unreconciled_receipts - label - - - - amount_currency - custom - _report_custom_engine_unreconciled_receipts - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_unreconciled_receipts - amount_currency_currency_id - - - currency - custom - _report_custom_engine_unreconciled_receipts - currency - - - - amount - custom - _report_custom_engine_unreconciled_receipts - amount - - - - _currency_amount - custom - _report_custom_engine_unreconciled_receipts - amount_currency_id - - - - - Including Unreconciled Payments - unreconciled_payments - id - - - - date - custom - _report_custom_engine_unreconciled_payments - date - - - - label - custom - _report_custom_engine_unreconciled_payments - label - - - - amount_currency - custom - _report_custom_engine_unreconciled_payments - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_unreconciled_payments - amount_currency_currency_id - - - currency - custom - _report_custom_engine_unreconciled_payments - currency - - - - amount - custom - _report_custom_engine_unreconciled_payments - amount - - - - _currency_amount - custom - _report_custom_engine_unreconciled_payments - amount_currency_id - - - - - - - Misc. operations - misc_operations - - - amount - custom - _report_custom_engine_misc_operations - amount - - - - _currency_amount - custom - _report_custom_engine_misc_operations - amount_currency_id - - - - - - - Outstanding Receipts/Payments - 0 - - - amount - aggregation - outstanding_receipts.amount + outstanding_payments.amount - - - - _currency_amount - custom - _report_custom_engine_forced_currency_amount - amount_currency_id - - - - - (+) Outstanding Receipts - outstanding_receipts - id - - - - date - custom - _report_custom_engine_outstanding_receipts - date - - - - label - custom - _report_custom_engine_outstanding_receipts - label - - - - amount_currency - custom - _report_custom_engine_outstanding_receipts - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_outstanding_receipts - amount_currency_currency_id - - - currency - custom - _report_custom_engine_outstanding_receipts - currency - - - - amount - custom - _report_custom_engine_outstanding_receipts - amount - - - - _currency_amount - custom - _report_custom_engine_outstanding_receipts - amount_currency_id - - - - - (-) Outstanding Payments - outstanding_payments - id - - - - date - custom - _report_custom_engine_outstanding_payments - date - - - - label - custom - _report_custom_engine_outstanding_payments - label - - - - amount_currency - custom - _report_custom_engine_outstanding_payments - amount_currency - - - - _currency_amount_currency - custom - _report_custom_engine_outstanding_payments - amount_currency_currency_id - - - currency - custom - _report_custom_engine_outstanding_payments - currency - - - - amount - custom - _report_custom_engine_outstanding_payments - amount - - - - _currency_amount - custom - _report_custom_engine_outstanding_payments - amount_currency_id - - - - - - - - diff --git a/addons/at_accounting/data/cash_flow_report.xml b/addons/at_accounting/data/cash_flow_report.xml deleted file mode 100644 index 8a6ceab..0000000 --- a/addons/at_accounting/data/cash_flow_report.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - Cash Flow Statement - - - - - selector - current - - - - Balance - balance - - - - diff --git a/addons/at_accounting/data/deferred_reports.xml b/addons/at_accounting/data/deferred_reports.xml deleted file mode 100644 index 305bca2..0000000 --- a/addons/at_accounting/data/deferred_reports.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Deferred Expense Report - - - - - selector - - by_default - previous_month - - - - - Current - current - - - - - - Deferred Revenue Report - - - - - selector - - by_default - previous_month - - - - - Current - current - - - - diff --git a/addons/at_accounting/data/digest_data.xml b/addons/at_accounting/data/digest_data.xml deleted file mode 100644 index 829a01a..0000000 --- a/addons/at_accounting/data/digest_data.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - True - - - - - Tip: Bulk update journal items - 900 - - -
- Tip: Bulk update journal items -

From any list view, select multiple records and the list becomes editable. If you update a cell, selected records are updated all at once. Use this feature to update multiple journal entries from the General Ledger, or any Journal view.

- -
-
-
- - Tip: Find an Accountant or register your Accounting Firm - 1000 - - -
- Tip: Find an Accountant or register your Accounting Firm -

Click here to find an accountant or if you want to list out your accounting services on Odoo

-

- Find an Accountant - Register your Accounting Firm -

-
-
-
-
-
diff --git a/addons/at_accounting/data/executive_summary.xml b/addons/at_accounting/data/executive_summary.xml deleted file mode 100644 index 635af9e..0000000 --- a/addons/at_accounting/data/executive_summary.xml +++ /dev/null @@ -1,395 +0,0 @@ - - - - Executive Summary - selector - this_year - - - Balance - balance - - - - - Cash - 0 - - - Cash received - CR - - - balance - domain - - sum - - - - - - Cash spent - CS - - - balance - domain - - sum - - - - - - - Cash surplus - - - balance - aggregation - CR.balance + CS.balance - - - - - - Closing bank balance - - - balance - domain - - from_beginning - sum - - - - - - - - Profitability - 0 - - - Revenue - - - balance - aggregation - REV.balance - strict_range - cross_report - - - - - - Cost of Revenue - EXEC_COS - - - balance - aggregation - COS.balance - strict_range - cross_report - - - - - - - Gross profit - - - balance - aggregation - GRP.balance - strict_range - cross_report - - - - - - Expenses - - - balance - aggregation - EXP.balance - strict_range - cross_report - - - - - - - Net Profit - EXEC_NEP - - - balance - aggregation - NEP.balance - strict_range - cross_report - - - - - - - - Balance Sheet - 0 - - - Receivables - DEB - - - balance - domain - - from_beginning - sum - - - - - - Payables - CRE - - - balance - domain - - from_beginning - sum - - - - - - - Net assets - EXEC_SUMMARY_NA - - - balance - aggregation - TA.balance - L.balance - from_beginning - cross_report - - - - - - - - Performance - 0 - - - Gross profit margin (gross profit / operating income) - GPMARGIN0 - - - balance - aggregation - GPMARGIN0.grp / GPMARGIN0.opinc * 100 - ignore_zero_division - percentage - - - - grp - aggregation - GRP.balance - strict_range - cross_report - - - opinc - aggregation - REV.balance - strict_range - cross_report - - - - - Net profit margin (net profit / income) - NPMARGIN0 - - - balance - aggregation - NPMARGIN0.nep / NPMARGIN0.inc * 100 - ignore_zero_division - percentage - - - - nep - aggregation - NEP.balance - strict_range - cross_report - - - inc - aggregation - INC.balance - strict_range - cross_report - - - - - Return on investments (net profit / assets) - ROI - - - balance - aggregation - ROI.nep / ROI.ta * 100 - ignore_zero_division - percentage - - - - nep - aggregation - NEP.balance - strict_range - cross_report - - - ta - aggregation - TA.balance - from_beginning - cross_report - - - - - - - Position - 0 - - - Average debtors days - AVG_DEBT_DAYS - - - balance - aggregation - DEB.balance / AVG_DEBT_DAYS.opinc * AVG_DEBT_DAYS.NDays - ignore_zero_division - - float - - - - opinc - aggregation - REV.balance - strict_range - cross_report - - - NDays - custom - _report_custom_engine_executive_summary_ndays - - - - - - Average creditors days - AVG_CRED_DAYS - - - balance - aggregation - -CRE.balance / (AVG_CRED_DAYS.cos + AVG_CRED_DAYS.exp) * AVG_CRED_DAYS.NDays - ignore_zero_division - - float - - - - cos - aggregation - COS.balance - strict_range - cross_report - - - exp - aggregation - EXP.balance - strict_range - cross_report - - - NDays - custom - _report_custom_engine_executive_summary_ndays - - - - - - Short term cash forecast - - - balance - aggregation - DEB.balance + CRE.balance - - - - - - Current assets to liabilities - CATL - - - balance - aggregation - CATL.ca / CATL.cl - ignore_zero_division - float - - - - ca - aggregation - CA.balance - from_beginning - cross_report - - - cl - aggregation - CL.balance - from_beginning - cross_report - - - - - - - - diff --git a/addons/at_accounting/data/general_ledger.xml b/addons/at_accounting/data/general_ledger.xml deleted file mode 100644 index b6f1c52..0000000 --- a/addons/at_accounting/data/general_ledger.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - General Ledger - - - - selector - - never - this_month - - - - - - Date - date - date - - - Communication - communication - string - - - Partner - partner_name - string - - - Currency - amount_currency - - - Debit - debit - - - Credit - credit - - - Balance - balance - - - - diff --git a/addons/at_accounting/data/generic_tax_report.xml b/addons/at_accounting/data/generic_tax_report.xml deleted file mode 100644 index 3da471f..0000000 --- a/addons/at_accounting/data/generic_tax_report.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/addons/at_accounting/data/ir_cron.xml b/addons/at_accounting/data/ir_cron.xml deleted file mode 100644 index 3398c92..0000000 --- a/addons/at_accounting/data/ir_cron.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Try to reconcile automatically your statement lines - - code - model._cron_try_auto_reconcile_statement_lines(batch_size=100) - 1 - days - - diff --git a/addons/at_accounting/data/journal_report.xml b/addons/at_accounting/data/journal_report.xml deleted file mode 100644 index eb70ce1..0000000 --- a/addons/at_accounting/data/journal_report.xml +++ /dev/null @@ -1,235 +0,0 @@ - - - - Journal Report - - - - never - - - never - this_year - - - - Code - code - string - - - Debit - debit - - - Credit - credit - - - Balance - balance - - - - - Name - journal_id, account_id - 0 - - - code - custom - _report_custom_engine_journal_report - code - - - debit - custom - _report_custom_engine_journal_report - debit - - - credit - custom - _report_custom_engine_journal_report - credit - - - balance - custom - _report_custom_engine_journal_report - balance - - - - - - - - - - - - diff --git a/addons/at_accounting/data/mail_activity_type_data.xml b/addons/at_accounting/data/mail_activity_type_data.xml deleted file mode 100644 index 8141398..0000000 --- a/addons/at_accounting/data/mail_activity_type_data.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Tax Report - Tax Report - tax_report - account.journal - suggest - - - - Pay Tax - Tax is ready to be paid - tax_report - 0 - days - previous_activity - account.move - suggest - - - - Tax Report Ready - Tax report is ready to be sent to the administration - tax_report - 0 - days - current_date - account.move - suggest - - - diff --git a/addons/at_accounting/data/mail_templates.xml b/addons/at_accounting/data/mail_templates.xml deleted file mode 100644 index b4af9e0..0000000 --- a/addons/at_accounting/data/mail_templates.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - Customer Statement - - {{ object._get_followup_responsible().email_formatted }} - {{ (object.company_id or object._get_followup_responsible().company_id).name }} Statement - {{ object.commercial_company_name }} - -
-

- Dear (), - Dear , -
- Please find enclosed the statement of your account. -
- Do not hesitate to contact us if you have any questions. -
- Sincerely, -
- -

-
-
- {{ object.lang }} - -
-
diff --git a/addons/at_accounting/data/menuitems.xml b/addons/at_accounting/data/menuitems.xml deleted file mode 100644 index 2e99abd..0000000 --- a/addons/at_accounting/data/menuitems.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/at_accounting/data/menuitems_asset.xml b/addons/at_accounting/data/menuitems_asset.xml deleted file mode 100644 index 88ffc1e..0000000 --- a/addons/at_accounting/data/menuitems_asset.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/addons/at_accounting/data/multicurrency_revaluation_report.xml b/addons/at_accounting/data/multicurrency_revaluation_report.xml deleted file mode 100644 index c719dcc..0000000 --- a/addons/at_accounting/data/multicurrency_revaluation_report.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - Unrealized Currency Gains/Losses - - - previous_month - - - - Balance in Foreign Currency - balance_currency - - - Balance at Operation Rate - balance_operation - - - Balance at Current Rate - balance_current - - - Adjustment - adjustment - - - - - Accounts To Adjust - multicurrency_included - currency_id, account_id, id - - - balance_currency - custom - _report_custom_engine_multi_currency_revaluation_to_adjust - balance_currency - - - _currency_balance_currency - custom - _report_custom_engine_multi_currency_revaluation_to_adjust - currency_id - - - balance_operation - custom - _report_custom_engine_multi_currency_revaluation_to_adjust - balance_operation - - - - balance_current - custom - _report_custom_engine_multi_currency_revaluation_to_adjust - balance_current - - - - adjustment - custom - _report_custom_engine_multi_currency_revaluation_to_adjust - adjustment - - - - - - - Excluded Accounts - currency_id, account_id, id - - - balance_currency - custom - _report_custom_engine_multi_currency_revaluation_excluded - balance_currency - - - _currency_balance_currency - custom - _report_custom_engine_multi_currency_revaluation_excluded - currency_id - - - balance_operation - custom - _report_custom_engine_multi_currency_revaluation_excluded - balance_operation - - - balance_current - custom - _report_custom_engine_multi_currency_revaluation_excluded - balance_current - - - adjustment - custom - _report_custom_engine_multi_currency_revaluation_excluded - adjustment - - - - - - diff --git a/addons/at_accounting/data/partner_ledger.xml b/addons/at_accounting/data/partner_ledger.xml deleted file mode 100644 index 8b1d142..0000000 --- a/addons/at_accounting/data/partner_ledger.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - Partner Ledger - - both - - - - - selector - never - this_year - - - - - - Journal - journal_code - string - - - Account - account_code - string - - - Invoice Date - invoice_date - date - - - Due Date - date_maturity - date - - - Matching - matching_number - string - - - Debit - debit - - - Credit - credit - - - Amount - amount - - - Amount Currency - amount_currency - - - Balance - balance - - - - diff --git a/addons/at_accounting/data/pdf_export_templates.xml b/addons/at_accounting/data/pdf_export_templates.xml deleted file mode 100644 index 9688117..0000000 --- a/addons/at_accounting/data/pdf_export_templates.xml +++ /dev/null @@ -1,335 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/addons/at_accounting/data/profit_and_loss.xml b/addons/at_accounting/data/profit_and_loss.xml deleted file mode 100644 index 80a978f..0000000 --- a/addons/at_accounting/data/profit_and_loss.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - Profit and Loss - - - - selector - - this_year - - - Balance - balance - - - - - Revenue - REV - 1 - account_id - - - - balance - domain - - -sum - - - - - Less Costs of Revenue - COS - 1 - account_id - - - - balance - domain - - sum - - - - - - Gross Profit - GRP - 0 - - - balance - aggregation - REV.balance - COS.balance - - - - - Less Operating Expenses - EXP - 1 - account_id - - - - balance - domain - - sum - - - - - - Operating Income (or Loss) - 0 - INC - - - balance - aggregation - REV.balance - COS.balance - EXP.balance - - - - - Plus Other Income - OIN - 1 - account_id - - - - balance - domain - - -sum - - - - - Less Other Expenses - OEXP - 1 - account_id - - - - balance - domain - - sum - - - - - - Net Profit - 0 - NEP - - - balance - aggregation - REV.balance + OIN.balance - COS.balance - EXP.balance - OEXP.balance - - - - - - diff --git a/addons/at_accounting/data/report_send_cron.xml b/addons/at_accounting/data/report_send_cron.xml deleted file mode 100644 index 3582846..0000000 --- a/addons/at_accounting/data/report_send_cron.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Send account reports automatically - - code - model._cron_account_report_send(job_count=20) - - 1 - days - - diff --git a/addons/at_accounting/data/sales_report.xml b/addons/at_accounting/data/sales_report.xml deleted file mode 100644 index bb46c9e..0000000 --- a/addons/at_accounting/data/sales_report.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - Generic EC Sales List - - - - - - - selector - previous_month - - - - - - Country Code - country_code - string - - - - VAT Number - vat_number - string - - - - Amount - balance - - - - - diff --git a/addons/at_accounting/data/trial_balance.xml b/addons/at_accounting/data/trial_balance.xml deleted file mode 100644 index e252a28..0000000 --- a/addons/at_accounting/data/trial_balance.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - Trial Balance - - - - selector - - by_default - never - this_month - - - - - Debit - debit - - - Credit - credit - - - - diff --git a/addons/at_accounting/demo/at_accounting_demo.xml b/addons/at_accounting/demo/at_accounting_demo.xml deleted file mode 100644 index cde676c..0000000 --- a/addons/at_accounting/demo/at_accounting_demo.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - Odoo Office - - - - Asset - 5 Years - none - 1000 - - - - - open - - - - diff --git a/addons/at_accounting/demo/partner_bank.xml b/addons/at_accounting/demo/partner_bank.xml deleted file mode 100644 index ab15c79..0000000 --- a/addons/at_accounting/demo/partner_bank.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - BE68539007547034 - - - - - - 00987654322 - - - - - - 10987654320 - - - - - - 10987654322 - - - - - - diff --git a/addons/at_accounting/i18n/ar.po b/addons/at_accounting/i18n/ar.po deleted file mode 100644 index f02059e..0000000 --- a/addons/at_accounting/i18n/ar.po +++ /dev/null @@ -1,5332 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * at_accounting -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 18.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-30 00:00+0000\n" -"PO-Revision-Date: 2026-03-30 00:00+0000\n" -"Last-Translator: \n" -"Language-Team: Arabic\n" -"Language: ar\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" - - - - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__total_depreciation_entries_count -msgid "# Depreciation Entries" -msgstr "# قيود الإهلاك" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__gross_increase_count -msgid "# Gross Increases" -msgstr "# الزيادات الإجمالية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__depreciation_entries_count -msgid "# Posted Depreciation Entries" -msgstr "# قيود الإهلاك المرحّلة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(asset)s: Disposal" -msgstr "%(asset)s: التصرف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(asset)s: Sale" -msgstr "%(asset)s: البيع" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "%(display_name_html)s with an open amount of %(open_amount)s will be fully reconciled by the transaction." -msgstr "%(display_name_html)s بمبلغ مفتوح %(open_amount)s سيتم مطابقته بالكامل بواسطة المعاملة." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "%(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s." -msgstr "%(display_name_html)s بمبلغ مفتوح %(open_amount)s سيتم تخفيضه بمقدار %(amount)s." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "%(journal)s - %(account)s" -msgstr "%(journal)s - %(account)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(months)s m" -msgstr "%(months)s شهر" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "%(move_line)s (%(current)s of %(total)s)" -msgstr "%(move_line)s (%(current)s من %(total)s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%(names)s and %(remaining)s others" -msgstr "%(names)s و %(remaining)s آخرون" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%(names)s and one other" -msgstr "%(names)s وواحد آخر" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "%(report_label)s: %(period)s" -msgstr "%(report_label)s: %(period)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(years)s y" -msgstr "%(years)s سنة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "%d transactions had already been imported and were ignored." -msgstr "تم استيراد %d معاملة مسبقاً وتم تجاهلها." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/budget.py:0 -#, python-format -msgid "%s (copy)" -msgstr "%s (نسخة)" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "%s Future entries will be recomputed to depreciate the asset following the changes." -msgstr "%s سيتم إعادة حساب القيود المستقبلية لإهلاك الأصل وفقاً للتغييرات." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%s is not a numeric value" -msgstr "%s ليست قيمة رقمية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "%s: Depreciation" -msgstr "%s: إهلاك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'external' engine does not support groupby, limit nor offset." -msgstr "المحرك 'external' لا يدعم التجميع أو الحد أو الإزاحة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'Open General Ledger' caret option is only available form report lines targetting accounts." -msgstr "خيار 'فتح دفتر الأستاذ العام' متاح فقط لأسطر التقارير التي تستهدف الحسابات." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'View Bank Statement' caret option is only available for report lines targeting bank statements." -msgstr "خيار 'عرض كشف الحساب البنكي' متاح فقط لأسطر التقارير التي تستهدف كشوفات البنك." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "(%s lines)" -msgstr "(%s أسطر)" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding_receipts -msgid "(+) Outstanding Receipts" -msgstr "(+) إيصالات معلقة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding_payments -msgid "(-) Outstanding Payments" -msgstr "(-) مدفوعات معلقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "(1 line)" -msgstr "(سطر واحد)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "(No %s)" -msgstr "(بدون %s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "(No Group)" -msgstr "(بدون مجموعة)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.assets_report_assets_plus -#: model:account.report.column,name:at_accounting.assets_report_depre_plus -msgid "+" -msgstr "+" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.assets_report_assets_minus -#: model:account.report.column,name:at_accounting.assets_report_depre_minus -msgid "-" -msgstr "-" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "-> Reconcile" -msgstr "-> مطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "-> Refresh" -msgstr "-> تحديث" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "1 transaction had already been imported and was ignored." -msgstr "تم استيراد معاملة واحدة مسبقاً وتم تجاهلها." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s." -msgstr "سيتم ترحيل قيد إهلاك في تاريخ %(date)s وما يشمله." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
%(extra_text)s" -msgstr "سيتم ترحيل قيد إهلاك في تاريخ %(date)s وما يشمله.
%(extra_text)s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
%(extra_text)s Future entries will be recomputed to depreciate the asset following the changes." -msgstr "سيتم ترحيل قيد إهلاك في تاريخ %(date)s وما يشمله.
%(extra_text)s سيتم إعادة حساب القيود المستقبلية لإهلاك الأصل وفقاً للتغييرات." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
A disposal entry will be posted on the %(account_type)s account %(account)s." -msgstr "سيتم ترحيل قيد إهلاك في تاريخ %(date)s وما يشمله.
سيتم ترحيل قيد تصرف على حساب %(account_type)s %(account)s." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
A second entry will neutralize the original income and post the outcome of this sale on account %(account)s." -msgstr "سيتم ترحيل قيد إهلاك في تاريخ %(date)s وما يشمله.
سيتم ترحيل قيد ثانٍ لإلغاء الإيراد الأصلي وترحيل نتيجة هذا البيع على حساب %(account)s." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %s." -msgstr "سيتم ترحيل قيد إهلاك في تاريخ %s وما يشمله." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A document linked to %(move_line_name)s has been deleted: %(link)s" -msgstr "تم حذف مستند مرتبط بـ %(move_line_name)s: %(link)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A document linked to this move has been deleted: %s" -msgstr "تم حذف مستند مرتبط بهذه الحركة: %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A gross increase has been created: %(link)s" -msgstr "تم إنشاء زيادة إجمالية: %(link)s" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/widgets/account_report_x2many/account_report_x2many.js:0 -msgid "A line with a 'Group By' value cannot have children." -msgstr "لا يمكن أن يحتوي السطر ذو قيمة 'تجميع حسب' على عناصر فرعية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A non deductible tax value of %(tax_value)s was added to %(name)s's initial value of %(purchase_value)s" -msgstr "تمت إضافة قيمة ضريبة غير قابلة للخصم بقيمة %(tax_value)s إلى القيمة الأولية لـ %(name)s البالغة %(purchase_value)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A non deductible tax value of %(tax_value)s was added to %(name)s\\" -msgstr "تمت إضافة قيمة ضريبة غير قابلة للخصم بقيمة %(tax_value)s إلى %(name)s\\" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "A tax unit can only be created between companies sharing the same main currency." -msgstr "يمكن إنشاء وحدة ضريبية فقط بين الشركات التي تشترك في نفس العملة الرئيسية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "A tax unit must contain a minimum of two companies. You might want to delete the unit." -msgstr "يجب أن تحتوي الوحدة الضريبية على شركتين كحد أدنى. قد ترغب في حذف الوحدة." - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.auto.reconcile.wizard" -msgstr "access.account.auto.reconcile.wizard" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.change.lock.date" -msgstr "access.account.change.lock.date" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.multicurrency.revaluation.wizard" -msgstr "access.account.multicurrency.revaluation.wizard" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.reconcile.wizard" -msgstr "access.account.reconcile.wizard" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.report.send" -msgstr "access.account.report.send" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.secure.entries.wizard" -msgstr "access.account.secure.entries.wizard" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account_reports.export.wizard" -msgstr "access.account_reports.export.wizard" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account_reports.export.wizard.format" -msgstr "access.account_reports.export.wizard.format" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.asset.modify" -msgstr "access.asset.modify" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.bank.rec.widget" -msgstr "access.bank.rec.widget" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.bank.rec.widget.line" -msgstr "access.bank.rec.widget.line" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access_account_tax_unit_manager" -msgstr "access_account_tax_unit_manager" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access_account_tax_unit_readonly" -msgstr "access_account_tax_unit_readonly" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_account_name -#: model:account.report.column,name:at_accounting.aged_receivable_report_account_name -#: model:account.report.column,name:at_accounting.partner_ledger_report_account_code -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__account_id -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__account_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Account" -msgstr "حساب" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_auto_reconcile_wizard -msgid "Account automatic reconciliation wizard" -msgstr "معالج المطابقة التلقائية للحسابات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Account Code" -msgstr "رمز الحساب" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Account Code / Tag" -msgstr "رمز الحساب / العلامة" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_group_tree -msgid "Account Groups" -msgstr "مجموعات الحسابات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Account Label" -msgstr "تسمية الحساب" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reconcile_wizard -msgid "Account reconciliation wizard" -msgstr "معالج مطابقة الحسابات" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_tax_report_handler -msgid "Account Report Handler for Tax Reports" -msgstr "معالج تقارير الحسابات لتقارير الضرائب" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_send -msgid "Account Report Send" -msgstr "إرسال تقرير الحساب" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.account_tag_action -msgid "Account Tags" -msgstr "علامات الحسابات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__transfer_from_account_id -msgid "Account Transfer From" -msgstr "تحويل من حساب" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_depreciation_id -msgid "Account used in the depreciation entries, to decrease the asset value." -msgstr "الحساب المستخدم في قيود الإهلاك لتخفيض قيمة الأصل." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_depreciation_expense_id -msgid "Account used in the periodical entries, to record a part of the asset as expense." -msgstr "الحساب المستخدم في القيود الدورية لتسجيل جزء من الأصل كمصروف." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_asset_id -msgid "Account used to record the purchase of the asset at its original price." -msgstr "الحساب المستخدم لتسجيل شراء الأصل بسعره الأصلي." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__gain_account_id -msgid "Account used to write the journal item in case of gain" -msgstr "الحساب المستخدم لكتابة بند اليومية في حالة الربح" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__loss_account_id -msgid "Account used to write the journal item in case of loss" -msgstr "الحساب المستخدم لكتابة بند اليومية في حالة الخسارة" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.account_report_annotation" -msgstr "account.account_report_annotation" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.account_report_annotation_readonly" -msgstr "account.account_report_annotation_readonly" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.asset" -msgstr "account.asset" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.asset.group" -msgstr "account.asset.group" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.fiscal.year.manager" -msgstr "account.fiscal.year.manager" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.fiscal.year.user" -msgstr "account.fiscal.year.user" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.ac.user" -msgstr "account.report.budget.ac.user" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.item.ac.user" -msgstr "account.report.budget.item.ac.user" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.item.readonly" -msgstr "account.report.budget.item.readonly" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.readonly" -msgstr "account.report.budget.readonly" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.file.download.error.wizard" -msgstr "account.report.file.download.error.wizard" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.ac.user" -msgstr "account.report.horizontal.group.ac.user" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.readonly" -msgstr "account.report.horizontal.group.readonly" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.rule.ac.user" -msgstr "account.report.horizontal.group.rule.ac.user" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.rule.readonly" -msgstr "account.report.horizontal.group.rule.readonly" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounting" -msgstr "المحاسبة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_budget -msgid "Accounting Report Budget" -msgstr "ميزانية التقارير المحاسبية" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_budget_item -msgid "Accounting Report Budget Item" -msgstr "بند ميزانية التقارير المحاسبية" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_tree -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_tree -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounting Reports" -msgstr "التقارير المحاسبية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__account_ids -msgid "Accounts" -msgstr "حسابات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Accounts coverage" -msgstr "تغطية الحسابات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounts Coverage Report" -msgstr "تقرير تغطية الحسابات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.multicurrency_revaluation_to_adjust -msgid "Accounts To Adjust" -msgstr "حسابات للتعديل" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_acquisition_date -msgid "Acquisition Date" -msgstr "تاريخ الاقتناء" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__modify_action -msgid "Action" -msgstr "إجراء" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Add an internal note" -msgstr "إضافة ملاحظة داخلية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Add contacts to notify..." -msgstr "إضافة جهات اتصال للإشعار..." - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_adjustment -msgid "Adjustment" -msgstr "تعديل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "Adjustment Entry" -msgstr "قيد التعديل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Advance payments made to suppliers" -msgstr "دفعات مقدمة للموردين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Advance Payments received from customers" -msgstr "دفعات مقدمة مستلمة من العملاء" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Advanced" -msgstr "متقدم" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "After importing three bills for a vendor without making changes, your ERP will suggest automatically validating future bills..." -msgstr "بعد استيراد ثلاث فواتير لمورد بدون إجراء تغييرات، سيقترح نظام ERP الخاص بك التحقق تلقائياً من الفواتير المستقبلية..." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "After the data extraction, check and validate the bill. If no vendor has been found, add one before validating." -msgstr "بعد استخراج البيانات، تحقق من الفاتورة وصادق عليها. إذا لم يتم العثور على مورد، أضف واحداً قبل المصادقة." - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_partner_balance_report_handler -msgid "Aged Partner Balance Custom Handler" -msgstr "معالج مخصص لرصيد الشركاء المستحق" - -#. module: at_accounting -#: model:account.report,name:at_accounting.aged_payable_report -#: model:account.report.line,name:at_accounting.aged_payable_line -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_aged_payable -msgid "Aged Payable" -msgstr "ذمم دائنة مستحقة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_payable_report_handler -msgid "Aged Payable Custom Handler" -msgstr "معالج مخصص للذمم الدائنة المستحقة" - -#. module: at_accounting -#: model:account.report,name:at_accounting.aged_receivable_report -#: model:account.report.line,name:at_accounting.aged_receivable_line -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_aged_receivable -msgid "Aged Receivable" -msgstr "ذمم مدينة مستحقة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_receivable_report_handler -msgid "Aged Receivable Custom Handler" -msgstr "معالج مخصص للذمم المدينة المستحقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/sales_report/filters/filters.js:0 -msgid "All" -msgstr "الكل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "All Journals" -msgstr "جميع دفاتر اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "All Payable" -msgstr "جميع المستحقات الدائنة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "All Receivable" -msgstr "جميع المستحقات المدينة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_model_autocomplete_ids -msgid "All reconciliation models" -msgstr "جميع نماذج المطابقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "All Report Variants" -msgstr "جميع متغيرات التقارير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be from the same account" -msgstr "يجب أن تكون جميع الأسطر من نفس الحساب" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be from the same company" -msgstr "يجب أن تكون جميع الأسطر من نفس الشركة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be posted" -msgstr "يجب ترحيل جميع الأسطر" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__allow_partials -msgid "Allow partials" -msgstr "السماح بالمطابقة الجزئية" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.group_fiscal_year -msgid "Allow to define fiscal years of more or less than a year" -msgstr "السماح بتحديد سنوات مالية أكثر أو أقل من سنة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__aml -msgid "aml" -msgstr "aml" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_amount -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_amount -#: model:account.report.column,name:at_accounting.partner_ledger_amount -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__amount_currency -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__amount -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount" -msgstr "مبلغ" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_amount_currency -#: model:account.report.column,name:at_accounting.aged_receivable_report_amount_currency -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_amount_currency -#: model:account.report.column,name:at_accounting.partner_ledger_report_amount_currency -msgid "Amount Currency" -msgstr "مبلغ بالعملة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount Due" -msgstr "المبلغ المستحق" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount Due (in currency)" -msgstr "المبلغ المستحق (بالعملة)" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__amount -msgid "Amount in company currency" -msgstr "المبلغ بعملة الشركة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Amount in Currency" -msgstr "المبلغ بالعملة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "Amount in currency: %s" -msgstr "المبلغ بالعملة: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Lakhs" -msgstr "المبالغ باللاكات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Millions" -msgstr "المبالغ بالملايين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Thousands" -msgstr "المبالغ بالآلاف" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__parent_id -msgid "An asset has a parent when it is the result of gaining value" -msgstr "يكون للأصل أصل رئيسي عندما يكون نتيجة لزيادة في القيمة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "An asset has been created for this move:" -msgstr "تم إنشاء أصل لهذه الحركة:" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "An asset will be created for the value increase of the asset.
" -msgstr "سيتم إنشاء أصل لزيادة قيمة الأصل.
" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "An entry will transfer %(amount)s from %(from_account)s to %(to_account)s." -msgstr "سيقوم قيد بتحويل %(amount)s من %(from_account)s إلى %(to_account)s." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Analytic" -msgstr "تحليلي" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__hard_lock_date -msgid "Any entry up to and including that date will be postponed to a later time, in accordance with its journal sequence. This lock date is irreversible and does not allow any exception." -msgstr "سيتم تأجيل أي قيد حتى وبما في ذلك هذا التاريخ إلى وقت لاحق، وفقاً لتسلسل دفتر اليومية الخاص به. تاريخ القفل هذا لا رجعة فيه ولا يسمح بأي استثناء." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__fiscalyear_lock_date -msgid "Any entry up to and including that date will be postponed to a later time, in accordance with its journal's sequence." -msgstr "سيتم تأجيل أي قيد حتى وبما في ذلك هذا التاريخ إلى وقت لاحق، وفقاً لتسلسل دفتر اليومية الخاص به." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__tax_lock_date -msgid "Any entry with taxes up to and including that date will be postponed to a later time, in accordance with its journal's sequence. The tax lock date is automatically set when the tax closing entry is posted." -msgstr "سيتم تأجيل أي قيد بضرائب حتى وبما في ذلك هذا التاريخ إلى وقت لاحق، وفقاً لتسلسل دفتر اليومية الخاص به. يتم تعيين تاريخ قفل الضرائب تلقائياً عند ترحيل قيد إقفال الضرائب." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__purchase_lock_date -msgid "Any purchase entry prior to and including this date will be postponed to a later date, in accordance with its journal's sequence." -msgstr "سيتم تأجيل أي قيد شراء قبل وبما في ذلك هذا التاريخ إلى تاريخ لاحق، وفقاً لتسلسل دفتر اليومية الخاص به." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__sale_lock_date -msgid "Any sales entry prior to and including this date will be postponed to a later date, in accordance with its journal's sequence." -msgstr "سيتم تأجيل أي قيد مبيعات قبل وبما في ذلك هذا التاريخ إلى تاريخ لاحق، وفقاً لتسلسل دفتر اليومية الخاص به." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "AP %s" -msgstr "ذمم دائنة %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "AR %s" -msgstr "ذمم مدينة %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Archived" -msgstr "مؤرشف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "As of %s" -msgstr "اعتباراً من %s" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Ascending" -msgstr "تصاعدي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__asset_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset" -msgstr "أصل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Account" -msgstr "حساب الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Asset Cancelled" -msgstr "تم إلغاء الأصل" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_asset_counterpart_id -msgid "Asset Counterpart Account" -msgstr "حساب مقابل الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Asset created" -msgstr "تم إنشاء الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Asset created from invoice: %s" -msgstr "تم إنشاء الأصل من الفاتورة: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset disposed. %s" -msgstr "تم التخلص من الأصل. %s" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset_group -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__asset_group_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Group" -msgstr "مجموعة الأصول" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Model" -msgstr "نموذج الأصل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Model name" -msgstr "اسم نموذج الأصل" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_asset_model_form -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Models" -msgstr "نماذج الأصول" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_move_type -msgid "Asset Move Type" -msgstr "نوع حركة الأصل" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__name -msgid "Asset Name" -msgstr "اسم الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset paused. %s" -msgstr "تم إيقاف الأصل مؤقتاً. %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset sold. %s" -msgstr "تم بيع الأصل. %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Asset unpaused. %s" -msgstr "تم استئناف الأصل. %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Values" -msgstr "قيم الأصول" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset(s)" -msgstr "أصل/أصول" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset -msgid "Asset/Revenue Recognition" -msgstr "الأصول/الاعتراف بالإيرادات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_total_assets0 -msgid "ASSETS" -msgstr "الأصول" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.actions.act_window,name:at_accounting.action_account_asset_form -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets" -msgstr "أصول" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_finance_config_assets -msgid "Assets and Revenues" -msgstr "الأصول والإيرادات" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets in closed state" -msgstr "أصول في حالة مغلقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets in draft and open states" -msgstr "أصول في حالة مسودة أو مفتوحة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset_report_handler -msgid "Assets Report Custom Handler" -msgstr "معالج مخصص لتقرير الأصول" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period0 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period0 -msgid "At Date" -msgstr "في التاريخ" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Atleast one asset (%s) couldn't be set as running because it lacks any required information" -msgstr "لم يتم تعيين أصل واحد على الأقل (%s) كنشط لأنه يفتقر إلى المعلومات المطلوبة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Attach a file" -msgstr "إرفاق ملف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Audit" -msgstr "تدقيق" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.account_reports_audit_reports_menu -msgid "Audit Reports" -msgstr "تقارير التدقيق" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__auto_balance -msgid "auto_balance" -msgstr "auto_balance" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automate Asset" -msgstr "أتمتة الأصل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automatic Accounting" -msgstr "محاسبة تلقائية" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_auto_reconcile_wizard.py:0 -msgid "Automatically Reconciled Entries" -msgstr "قيود تمت مطابقتها تلقائياً" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automation" -msgstr "أتمتة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_avgcre0 -msgid "Average creditors days" -msgstr "متوسط أيام الدائنين" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_avdebt0 -msgid "Average debtors days" -msgstr "متوسط أيام المدينين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "B: %s" -msgstr "ر: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.balance_sheet_balance -#: model:account.report.column,name:at_accounting.cash_flow_report_balance -#: model:account.report.column,name:at_accounting.executive_summary_column -#: model:account.report.column,name:at_accounting.general_ledger_report_balance -#: model:account.report.column,name:at_accounting.journal_report_balance -#: model:account.report.column,name:at_accounting.partner_ledger_report_balance -#: model:account.report.column,name:at_accounting.profit_and_loss_column -msgid "Balance" -msgstr "رصيد" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_current -msgid "Balance at Current Rate" -msgstr "الرصيد بالسعر الحالي" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_operation -msgid "Balance at Operation Rate" -msgstr "الرصيد بسعر العملية" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_currency -msgid "Balance in Foreign Currency" -msgstr "الرصيد بالعملة الأجنبية" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -#, python-format -msgid "Balance of '%s'" -msgstr "رصيد '%s'" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.balance_bank -msgid "Balance of Bank" -msgstr "رصيد البنك" - -#. module: at_accounting -#: model:account.report,name:at_accounting.balance_sheet -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_balancesheet0 -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_balance_sheet -msgid "Balance Sheet" -msgstr "الميزانية العمومية" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_balance_sheet_report_handler -msgid "Balance Sheet Custom Handler" -msgstr "معالج مخصص للميزانية العمومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax advance payment account" -msgstr "رصيد حساب الدفعات المقدمة للضرائب" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax current account (payable)" -msgstr "رصيد حساب الضرائب الجاري (دائن)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax current account (receivable)" -msgstr "رصيد حساب الضرائب الجاري (مدين)" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_bank_view0 -msgid "Bank and Cash Accounts" -msgstr "حسابات البنك والنقد" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_transactions -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_transactions_kanban -msgid "Bank Reconciliation" -msgstr "مطابقة البنك" - -#. module: at_accounting -#: model:account.report,name:at_accounting.bank_reconciliation_report -msgid "Bank Reconciliation Report" -msgstr "تقرير مطابقة البنك" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_bank_reconciliation_report_handler -msgid "Bank Reconciliation Report Custom Handler" -msgstr "معالج مخصص لتقرير مطابقة البنك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Bank Statement" -msgstr "كشف حساب بنكي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "Bank Statement %s.pdf" -msgstr "كشف حساب بنكي %s.pdf" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "Bank Statement.pdf" -msgstr "كشف حساب بنكي.pdf" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Base Amount" -msgstr "المبلغ الأساسي" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Based on" -msgstr "بناءً على" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__daily_computation -msgid "Based on days per period" -msgstr "بناءً على الأيام لكل فترة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Before" -msgstr "قبل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Bills" -msgstr "فواتير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__book_value -msgid "Book Value" -msgstr "القيمة الدفترية" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_balance -msgid "book_value" -msgstr "القيمة_الدفترية" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_user -msgid "Bookkeeper" -msgstr "محاسب" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__budget_id -msgid "Budget" -msgstr "ميزانية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Budget Items" -msgstr "بنود الميزانية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Budget items can only be edited from account lines." -msgstr "لا يمكن تعديل بنود الميزانية إلا من أسطر الحسابات." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Budget Name" -msgstr "اسم الميزانية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Cancel" -msgstr "إلغاء" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Cancel Asset" -msgstr "إلغاء الأصل" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__cancelled -msgid "Cancelled" -msgstr "ملغي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Cannot audit tax from another model than account.tax." -msgstr "لا يمكن تدقيق الضريبة من نموذج آخر غير account.tax." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Cannot find in which journal import this statement. Please manually select a journal." -msgstr "لا يمكن العثور على دفتر اليومية لاستيراد هذا الكشف. يرجى تحديد دفتر يومية يدوياً." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Cannot generate carryover values for all fiscal positions at once!" -msgstr "لا يمكن إنشاء قيم الترحيل لجميع الأوضاع المالية دفعة واحدة!" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Carryover adjustment for tax unit" -msgstr "تعديل الترحيل للوحدة الضريبية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Carryover can only be generated for a single column group." -msgstr "يمكن إنشاء الترحيل فقط لمجموعة أعمدة واحدة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Carryover from %(date_from)s to %(date_to)s" -msgstr "ترحيل من %(date_from)s إلى %(date_to)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Carryover lines for: %s" -msgstr "أسطر الترحيل لـ: %s" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash0 -msgid "Cash" -msgstr "نقد" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash and cash equivalents, beginning of period" -msgstr "النقد وما يعادله، بداية الفترة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash and cash equivalents, closing balance" -msgstr "النقد وما يعادله، الرصيد الختامي" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_cash_flow_report_handler -msgid "Cash Flow Report Custom Handler" -msgstr "معالج مخصص لتقرير التدفق النقدي" - -#. module: at_accounting -#: model:account.report,name:at_accounting.cash_flow_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_cash_flow -msgid "Cash Flow Statement" -msgstr "قائمة التدفق النقدي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from financing activities" -msgstr "التدفقات النقدية من الأنشطة التمويلية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from investing & extraordinary activities" -msgstr "التدفقات النقدية من الأنشطة الاستثمارية والاستثنائية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from operating activities" -msgstr "التدفقات النقدية من الأنشطة التشغيلية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from unclassified activities" -msgstr "التدفقات النقدية من الأنشطة غير المصنفة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash in" -msgstr "نقد وارد" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash out" -msgstr "نقد صادر" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash paid for operating activities" -msgstr "النقد المدفوع للأنشطة التشغيلية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_received0 -msgid "Cash received" -msgstr "نقد مستلم" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash received from operating activities" -msgstr "النقد المستلم من الأنشطة التشغيلية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_spent0 -msgid "Cash spent" -msgstr "نقد منفق" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_surplus0 -msgid "Cash surplus" -msgstr "فائض نقدي" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_change_lock_date -msgid "Change Lock Date" -msgstr "تغيير تاريخ القفل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Characteristics" -msgstr "الخصائص" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_reconcile_wizard__to_check -msgid "Check if you are not certain of all the information of the counterpart." -msgstr "تحقق إذا لم تكن متأكداً من جميع معلومات الطرف المقابل." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Check Partner(s) Email(s)" -msgstr "التحقق من بريد الشريك/الشركاء" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method -msgid "" -"Choose the method to use to compute the amount of depreciation lines.\n" -" * Straight Line: Calculated on basis of: Gross Value / Duration\n" -" * Declining: Calculated on basis of: Residual Value * Declining Factor\n" -" * Declining then Straight Line: Like Declining but with a minimum depreciation value equal to the straight line value.\n" -msgstr "اختر الطريقة المستخدمة لحساب مبلغ أسطر الإهلاك.\n * القسط الثابت: يُحسب على أساس: القيمة الإجمالية / المدة\n * القسط المتناقص: يُحسب على أساس: القيمة المتبقية * عامل التناقص\n * المتناقص ثم الثابت: مثل المتناقص ولكن بحد أدنى لقيمة الإهلاك يساوي قيمة القسط الثابت.\n" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_auto_reconcile_wizard__search_mode__zero_balance -msgid "Clear Account" -msgstr "تصفية الحساب" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "Click \"New\" or upload a %s." -msgstr "انقر على \"جديد\" أو قم بتحميل %s." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Click on a fetched bank transaction to start the reconciliation process." -msgstr "انقر على معاملة بنكية مجلوبة لبدء عملية المطابقة." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Close" -msgstr "إغلاق" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__close -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Closed" -msgstr "مغلق" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_closing_bank_balance0 -msgid "Closing bank balance" -msgstr "رصيد البنك الختامي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Closing Entry" -msgstr "قيد الإقفال" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.journal_report_code -msgid "Code" -msgstr "رمز" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Columns" -msgstr "أعمدة" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.general_ledger_report_communication -msgid "Communication" -msgstr "الاتصال" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__company_ids -msgid "Companies" -msgstr "شركات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__company_id -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__company_id -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__company_id -msgid "Company" -msgstr "شركة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -#, python-format -msgid "Company %(company)s already belongs to a tax unit in %(country)s. A company can at most be part of one tax unit per country." -msgstr "الشركة %(company)s تنتمي بالفعل إلى وحدة ضريبية في %(country)s. يمكن للشركة أن تكون جزءاً من وحدة ضريبية واحدة على الأكثر لكل بلد." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Company Currency" -msgstr "عملة الشركة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__company_currency_id -msgid "Company currency" -msgstr "عملة الشركة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Company Only" -msgstr "الشركة فقط" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Company Settings" -msgstr "إعدادات الشركة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__prorata_computation_type -msgid "Computation" -msgstr "الحساب" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_asset_compute_depreciations -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Compute Depreciation" -msgstr "حساب الإهلاك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Configure start dates" -msgstr "تكوين تواريخ البدء" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_config_settings.py:0 -msgid "Configure your start dates" -msgstr "تكوين تواريخ البدء الخاصة بك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Configure your tax accounts" -msgstr "تكوين حسابات الضرائب الخاصة بك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -#, python-format -msgid "Configure your TAX accounts - %s" -msgstr "تكوين حسابات الضرائب - %s" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_asset_run -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Confirm" -msgstr "تأكيد" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Confirm the transaction." -msgstr "تأكيد المعاملة." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Connect your bank and get your latest transactions." -msgstr "اربط حسابك البنكي واحصل على أحدث معاملاتك." - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__constant_periods -msgid "Constant Periods" -msgstr "فترات ثابتة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_body -msgid "Contents" -msgstr "المحتويات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_direct_costs0 -msgid "Cost of Revenue" -msgstr "تكلفة الإيرادات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Could not expand term %(term)s while evaluating formula %(unexpanded_formula)s" -msgstr "لم يتم توسيع المصطلح %(term)s أثناء تقييم الصيغة %(unexpanded_formula)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "" -"Could not make sense of the given file.\n" -"Did you install the module to support this type of file?\n" -msgstr "لم يتم فهم الملف المعطى.\nهل قمت بتثبيت الوحدة لدعم هذا النوع من الملفات?\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Could not make sense of the given file.\\nDid you install the module to support this type of file?" -msgstr "لم يتم فهم الملف المعطى.\\nهل قمت بتثبيت الوحدة لدعم هذا النوع من الملفات؟" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Could not parse account_code formula from token '%s'" -msgstr "لم يتم تحليل صيغة account_code من الرمز '%s'" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Counterpart Values" -msgstr "قيم الطرف المقابل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__country_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Country" -msgstr "البلد" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_country -msgid "Country Code" -msgstr "رمز البلد" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Create a new transaction." -msgstr "إنشاء معاملة جديدة." - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_aml_to_asset -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Create Asset" -msgstr "إنشاء أصل" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_create_composite_report_list -msgid "Create Composite Report" -msgstr "إنشاء تقرير مركب" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Create Entry" -msgstr "إنشاء قيد" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_create_report_menu -msgid "Create Menu Item" -msgstr "إنشاء عنصر قائمة" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_form_bank_rec_widget -msgid "Create Statement" -msgstr "إنشاء كشف" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Create your first vendor bill.

Tip: If you don't have one on hand, use our sample bill." -msgstr "أنشئ أول فاتورة مورد لك.

نصيحة: إذا لم يكن لديك واحدة، استخدم فاتورة العينة الخاصة بنا." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_credit -#: model:account.report.column,name:at_accounting.journal_report_credit -#: model:account.report.column,name:at_accounting.partner_ledger_report_credit -#: model:account.report.column,name:at_accounting.trial_balance_report_credit -msgid "Credit" -msgstr "دائن" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_depreciated_value -msgid "Cumulative Depreciation" -msgstr "الإهلاك المتراكم" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_currency -#: model:account.report.column,name:at_accounting.aged_receivable_report_currency -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_currency -#: model:account.report.column,name:at_accounting.general_ledger_report_amount_currency -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Currency" -msgstr "عملة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -#, python-format -msgid "Currency Rates (%s)" -msgstr "أسعار العملات (%s)" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_currency_id -msgid "Currency to use for reconciliation" -msgstr "العملة المستخدمة للمطابقة" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.deferred_expense_current -#: model:account.report.column,name:at_accounting.deferred_revenue_current -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Current" -msgstr "حالي" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_assets0 -#: model:account.report.line,name:at_accounting.account_financial_report_current_assets_view0 -msgid "Current Assets" -msgstr "الأصول المتداولة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_ca_to_l0 -msgid "Current assets to liabilities" -msgstr "الأصول المتداولة إلى الالتزامات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__current_hard_lock_date -msgid "Current Hard Lock" -msgstr "القفل الصارم الحالي" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities0 -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities1 -msgid "Current Liabilities" -msgstr "الالتزامات المتداولة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Current Values" -msgstr "القيم الحالية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings_line_1 -msgid "Current Year Retained Earnings" -msgstr "الأرباح المحتجزة للسنة الحالية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_current_year_earnings0 -msgid "Current Year Unallocated Earnings" -msgstr "الأرباح غير الموزعة للسنة الحالية" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -msgid "Custom engine _report_custom_engine_last_statement_balance_amount does not support groupby" -msgstr "المحرك المخصص _report_custom_engine_last_statement_balance_amount لا يدعم التجميع" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__invoice_ids -msgid "Customer Invoice" -msgstr "فاتورة عميل" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "Customer/Vendor" -msgstr "عميل/مورد" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_date -#: model:account.report.column,name:at_accounting.general_ledger_report_date -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__date -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__date -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Date" -msgstr "تاريخ" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_end_date -msgid "Date at which the deferred expense/revenue ends" -msgstr "التاريخ الذي ينتهي فيه المصروف/الإيراد المؤجل" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_start_date -msgid "Date at which the deferred expense/revenue starts" -msgstr "التاريخ الذي يبدأ فيه المصروف/الإيراد المؤجل" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Date cannot be empty" -msgstr "لا يمكن أن يكون التاريخ فارغاً" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_date_from -#: model:account.report.column,name:at_accounting.assets_report_depre_date_from -msgid "date from" -msgstr "من تاريخ" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_depreciation_beginning_date -msgid "Date of the beginning of the depreciation" -msgstr "تاريخ بداية الإهلاك" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_assets_date_to -#: model:account.report.column,name:at_accounting.assets_report_depre_date_to -msgid "date to" -msgstr "إلى تاريخ" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_debit -#: model:account.report.column,name:at_accounting.journal_report_debit -#: model:account.report.column,name:at_accounting.partner_ledger_report_debit -#: model:account.report.column,name:at_accounting.trial_balance_report_debit -msgid "Debit" -msgstr "مدين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Dec. then Straight" -msgstr "متناقص ثم ثابت" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__degressive -msgid "Declining" -msgstr "متناقص" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_progress_factor -msgid "Declining Factor" -msgstr "عامل التناقص" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__degressive_then_linear -msgid "Declining then Straight Line" -msgstr "متناقص ثم قسط ثابت" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Deductible" -msgstr "قابل للخصم" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Deferral of %s" -msgstr "تأجيل %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_move_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred Entries" -msgstr "قيود مؤجلة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_entry_type -msgid "Deferred Entry Type" -msgstr "نوع القيد المؤجل" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__deferred_entry_type__expense -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_deferred_expense -msgid "Deferred Expense" -msgstr "مصروف مؤجل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred expense" -msgstr "مصروف مؤجل" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_expense_report_handler -msgid "Deferred Expense Custom Handler" -msgstr "معالج مخصص للمصروفات المؤجلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred expense entries:" -msgstr "قيود المصروفات المؤجلة:" - -#. module: at_accounting -#: model:account.report,name:at_accounting.deferred_expense_report -msgid "Deferred Expense Report" -msgstr "تقرير المصروفات المؤجلة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_report_handler -msgid "Deferred Expense Report Custom Handler" -msgstr "معالج مخصص لتقرير المصروفات المؤجلة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__deferred_entry_type__revenue -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_deferred_revenue -msgid "Deferred Revenue" -msgstr "إيراد مؤجل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred revenue" -msgstr "إيراد مؤجل" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_revenue_report_handler -msgid "Deferred Revenue Custom Handler" -msgstr "معالج مخصص للإيرادات المؤجلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred revenue entries:" -msgstr "قيود الإيرادات المؤجلة:" - -#. module: at_accounting -#: model:account.report,name:at_accounting.deferred_revenue_report -msgid "Deferred Revenue Report" -msgstr "تقرير الإيرادات المؤجلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Define fiscal years of more or less than one year" -msgstr "تحديد سنوات مالية أكثر أو أقل من سنة واحدة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Definition" -msgstr "التعريف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Depending moves" -msgstr "حركات تابعة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deposits" -msgstr "إيداعات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__value_residual -msgid "Depreciable Amount" -msgstr "المبلغ القابل للإهلاك" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__value_residual -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_remaining_value -msgid "Depreciable Value" -msgstr "القيمة القابلة للإهلاك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciated Amount" -msgstr "المبلغ المُهلَك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__depreciation_value -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__depreciation -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation" -msgstr "إهلاك" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_depreciation_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_depreciation_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Account" -msgstr "حساب الإهلاك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Board" -msgstr "جدول الإهلاك" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Depreciation board modified %s" -msgstr "تم تعديل جدول الإهلاك %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Date" -msgstr "تاريخ الإهلاك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Depreciation entry %(name)s posted (%(value)s)" -msgstr "تم ترحيل قيد الإهلاك %(name)s (%(value)s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Depreciation entry %(name)s reversed (%(value)s)" -msgstr "تم عكس قيد الإهلاك %(name)s (%(value)s)" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__depreciation_move_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Lines" -msgstr "أسطر الإهلاك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Method" -msgstr "طريقة الإهلاك" - -#. module: at_accounting -#: model:account.report,name:at_accounting.assets_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_assets -msgid "Depreciation Schedule" -msgstr "جدول الإهلاك" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Descending" -msgstr "تنازلي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Difference from rounding taxes" -msgstr "الفرق من تقريب الضرائب" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discard" -msgstr "تجاهل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discount Amount" -msgstr "مبلغ الخصم" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discount Date" -msgstr "تاريخ الخصم" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__disposal -msgid "Disposal" -msgstr "تصرف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Disposal Move" -msgstr "حركة التصرف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Disposal Moves" -msgstr "حركات التصرف" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Dispose" -msgstr "تصرف" - -#. module: at_accounting -#: code:addons/at_accounting/models/digest.py:0 -msgid "Do not have access, skip this data for user's digest email" -msgstr "لا يوجد صلاحية، تخطي هذه البيانات لبريد ملخص المستخدم" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Document" -msgstr "مستند" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__doc_name -msgid "Documents Name" -msgstr "اسم المستندات" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Domestic" -msgstr "محلي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__checkbox_download -msgid "Download" -msgstr "تحميل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Download Anyway" -msgstr "تحميل على أي حال" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Download the Data Inalterability Check Report" -msgstr "تحميل تقرير التحقق من عدم قابلية تغيير البيانات" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__draft -msgid "Draft" -msgstr "مسودة" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "Draft Entries" -msgstr "قيود مسودة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Due" -msgstr "مستحق" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_date_maturity -msgid "Due Date" -msgstr "تاريخ الاستحقاق" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_number -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__method_number -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Duration" -msgstr "المدة" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_duration_rate -msgid "Duration / Rate" -msgstr "المدة / المعدل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "e.g. Bank Fees" -msgstr "مثال: رسوم بنكية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "e.g. Laptop iBook" -msgstr "مثال: حاسوب محمول iBook" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__early_payment -msgid "early_payment" -msgstr "دفعة_مبكرة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_ec_sales_report_handler -msgid "EC Sales Report Custom Handler" -msgstr "معالج مخصص لتقرير مبيعات الاتحاد الأوروبي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "EC tax on non EC countries" -msgstr "ضريبة الاتحاد الأوروبي على البلدان خارج الاتحاد" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "EC tax on same country" -msgstr "ضريبة الاتحاد الأوروبي على نفس البلد" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__edit_mode_amount_currency -msgid "Edit mode amount" -msgstr "مبلغ وضع التحرير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Editing a manual report line is not allowed in multivat setup when displaying data from all fiscal positions." -msgstr "لا يُسمح بتحرير سطر تقرير يدوي في إعداد ضريبة القيمة المضافة المتعددة عند عرض البيانات من جميع الأوضاع المالية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Editing a manual report line is not allowed when multiple companies are selected." -msgstr "لا يُسمح بتحرير سطر تقرير يدوي عند تحديد شركات متعددة." - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__checkbox_send_mail -msgid "Email" -msgstr "البريد الإلكتروني" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_template_id -msgid "Email template" -msgstr "قالب البريد الإلكتروني" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Enable automatic accounting entries for stock movements" -msgstr "تفعيل القيود المحاسبية التلقائية لحركات المخزون" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Enable Sections" -msgstr "تفعيل الأقسام" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -msgid "End Balance" -msgstr "الرصيد النهائي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__date_to -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_end_date -msgid "End Date" -msgstr "تاريخ الانتهاء" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Month" -msgstr "نهاية الشهر" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Quarter" -msgstr "نهاية الربع" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Year" -msgstr "نهاية السنة" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_fiscal_year__date_to -msgid "Ending Date, included in the fiscal year." -msgstr "تاريخ الانتهاء، مشمول في السنة المالية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "entries" -msgstr "قيود" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Entries with partners with no VAT" -msgstr "قيود مع شركاء بدون ضريبة قيمة مضافة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "entry" -msgstr "قيد" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_equity0 -msgid "EQUITY" -msgstr "حقوق الملكية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Error message" -msgstr "رسالة خطأ" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_applies_to -msgid "Exception applies" -msgstr "الاستثناء ينطبق" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_duration -msgid "Exception Duration" -msgstr "مدة الاستثناء" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_needed -msgid "Exception needed" -msgstr "الاستثناء مطلوب" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_reason -msgid "Exception Reason" -msgstr "سبب الاستثناء" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -#, python-format -msgid "Exchange Difference: %s" -msgstr "فرق الصرف: %s" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__exchange_diff -msgid "exchange_diff" -msgstr "فرق_الصرف" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Exclude Bank lines" -msgstr "استبعاد أسطر البنك" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.multicurrency_revaluation_excluded -msgid "Excluded Accounts" -msgstr "حسابات مستبعدة" - -#. module: at_accounting -#: model:account.report,name:at_accounting.executive_summary -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_exec_summary -msgid "Executive Summary" -msgstr "الملخص التنفيذي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_depreciation_expense_id -#: model:ir.model.fields,field_description:at_accounting.field_account_multicurrency_revaluation_wizard__expense_provision_account_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_depreciation_expense_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Expense Account" -msgstr "حساب المصروفات" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Expense Provision for %s" -msgstr "مخصص المصروفات لـ %s" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_expenses0 -msgid "Expenses" -msgstr "مصروفات" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Export" -msgstr "تصدير" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reports_export_wizard_format -msgid "Export format for accounting's reports" -msgstr "تنسيق التصدير للتقارير المحاسبية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__export_format_ids -msgid "Export to" -msgstr "تصدير إلى" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reports_export_wizard -msgid "Export wizard for accounting's reports" -msgstr "معالج التصدير للتقارير المحاسبية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Expression" -msgstr "تعبير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Expression labelled '%(label)s' of line '%(line)s' is being overwritten when computing the current report." -msgstr "التعبير المسمى '%(label)s' للسطر '%(line)s' يتم استبداله عند حساب التقرير الحالي." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s does not exist on account.move.line, and is not supported by this report's custom handler." -msgstr "الحقل %s غير موجود في account.move.line، وغير مدعوم من المعالج المخصص لهذا التقرير." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s does not exist on account.move.line." -msgstr "الحقل %s غير موجود في account.move.line." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s of account.move.line is not stored, and hence cannot be used in a groupby expression" -msgstr "الحقل %s في account.move.line غير مخزن، ولذلك لا يمكن استخدامه في تعبير التجميع" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field 'Custom Handler Model' can only reference records inheriting from [%s]." -msgstr "الحقل 'نموذج المعالج المخصص' يمكنه فقط الإشارة إلى السجلات الموروثة من [%s]." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "File Download Errors" -msgstr "أخطاء تحميل الملفات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Filters" -msgstr "فلاتر" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_budget_tree -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_budget_tree -msgid "Financial Budgets" -msgstr "الميزانيات المالية" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_first_depreciation -msgid "First Depreciation" -msgstr "الإهلاك الأول" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__fpos_synced -msgid "Fiscal Positions Synchronised" -msgstr "تمت مزامنة الأوضاع المالية" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_fiscal_year -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Year" -msgstr "سنة مالية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Year 2018" -msgstr "السنة المالية 2018" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.actions_account_fiscal_year -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Years" -msgstr "سنوات مالية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_asset_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fixed Asset Account" -msgstr "حساب الأصول الثابتة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__1h -msgid "for 1 hour" -msgstr "لمدة ساعة واحدة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__15min -msgid "for 15 minutes" -msgstr "لمدة 15 دقيقة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__24h -msgid "for 24 hours" -msgstr "لمدة 24 ساعة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__5min -msgid "for 5 minutes" -msgstr "لمدة 5 دقائق" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_applies_to__everyone -msgid "for everyone" -msgstr "للجميع" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_applies_to__me -msgid "for me" -msgstr "لي" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Foreign currencies adjustment entry as of %s" -msgstr "قيد تعديل العملات الأجنبية اعتباراً من %s" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__forever -msgid "forever" -msgstr "للأبد" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__from_date -msgid "From" -msgstr "من" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "From %(date_from)s\\nto %(date_to)s" -msgstr "من %(date_from)s\\nإلى %(date_to)s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "From Trade Payable accounts" -msgstr "من حسابات الذمم الدائنة التجارية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "From Trade Receivable accounts" -msgstr "من حسابات الذمم المدينة التجارية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__fun_param -msgid "Function Parameter" -msgstr "معامل الدالة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__fun_to_call -msgid "Function to Call" -msgstr "الدالة للاستدعاء" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Future Activities" -msgstr "أنشطة مستقبلية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "G %s" -msgstr "ز %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "gain" -msgstr "ربح" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "gain/loss" -msgstr "ربح/خسارة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "General Account Properties" -msgstr "خصائص الحساب العام" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -#: model:account.report,name:at_accounting.general_ledger_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_general_ledger -msgid "General Ledger" -msgstr "دفتر الأستاذ العام" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_general_ledger_report_handler -msgid "General Ledger Custom Handler" -msgstr "معالج مخصص لدفتر الأستاذ العام" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Generate Entries" -msgstr "إنشاء قيود" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Generate entry" -msgstr "إنشاء قيد" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/report_export_wizard.py:0 -msgid "Generated Documents" -msgstr "مستندات مُنشأة" - -#. module: at_accounting -#: model:account.report,name:at_accounting.generic_ec_sales_report -msgid "Generic EC Sales List" -msgstr "قائمة مبيعات الاتحاد الأوروبي العامة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler -msgid "Generic Tax Report Custom Handler" -msgstr "معالج مخصص لتقرير الضرائب العام" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler_account_tax -msgid "Generic Tax Report Custom Handler (Account -> Tax)" -msgstr "معالج مخصص لتقرير الضرائب العام (حساب -> ضريبة)" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler_tax_account -msgid "Generic Tax Report Custom Handler (Tax -> Account)" -msgstr "معالج مخصص لتقرير الضرائب العام (ضريبة -> حساب)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Global Tax Summary" -msgstr "ملخص الضرائب العام" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Go to Apps" -msgstr "الذهاب إلى التطبيقات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Goods" -msgstr "بضائع" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Grid" -msgstr "شبكة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Gross Increase" -msgstr "زيادة إجمالية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_asset_id -msgid "Gross Increase Account" -msgstr "حساب الزيادة الإجمالية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__gross_increase_value -msgid "Gross Increase Value" -msgstr "قيمة الزيادة الإجمالية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_gross_profit0 -msgid "Gross Profit" -msgstr "الربح الإجمالي" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_gross_profit0 -msgid "Gross profit" -msgstr "الربح الإجمالي" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_gpmargin0 -msgid "Gross profit margin (gross profit / operating income)" -msgstr "هامش الربح الإجمالي (الربح الإجمالي / الدخل التشغيلي)" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group By" -msgstr "تجميع حسب" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group By..." -msgstr "تجميع حسب..." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group Name" -msgstr "اسم المجموعة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "Grouped Deferral Entry of %s" -msgstr "قيد التأجيل المجمع لـ %s" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__hard_lock_date -msgid "Hard Lock" -msgstr "قفل صارم" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Horizontal Group" -msgstr "مجموعة أفقية" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_horizontal_groups -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_horizontal_groups -msgid "Horizontal Groups" -msgstr "مجموعات أفقية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "How often tax returns have to be made" -msgstr "عدد مرات تقديم الإقرارات الضريبية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Impact On Grid" -msgstr "التأثير على الشبكة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/bank_statement_csv_import_action.js:0 -msgid "Import Bank Statement" -msgstr "استيراد كشف حساب بنكي" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Import File" -msgstr "استيراد ملف" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_bank_statement__unique_import_id -msgid "Import ID" -msgstr "معرف الاستيراد" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/account_bank_statement_import_model.js:0 -msgid "Import Template for Bank Statements" -msgstr "قالب استيراد كشوفات البنك" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__already_depreciated_amount_import -msgid "In case of an import from another software, you might need to use this field to have the right depreciation table report. This is the value that was already depreciated with entries not computed from this model" -msgstr "في حالة الاستيراد من برنامج آخر، قد تحتاج إلى استخدام هذا الحقل للحصول على تقرير جدول الإهلاك الصحيح. هذه هي القيمة التي تم إهلاكها بالفعل بقيود لم يتم حسابها من هذا النموذج" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "In Currency" -msgstr "بالعملة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Inactive" -msgstr "غير نشط" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Including Analytic Simulations" -msgstr "بما في ذلك المحاكاة التحليلية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.no_statement_unreconciled_payments -#: model:account.report.line,name:at_accounting.unreconciled_last_statement_payments -msgid "Including Unreconciled Payments" -msgstr "بما في ذلك المدفوعات غير المطابقة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.no_statement_unreconciled_receipt -#: model:account.report.line,name:at_accounting.unreconciled_last_statement_receipts -msgid "Including Unreconciled Receipts" -msgstr "بما في ذلك الإيصالات غير المطابقة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_multicurrency_revaluation_wizard__income_provision_account_id -msgid "Income Account" -msgstr "حساب الإيرادات" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Income Provision for %s" -msgstr "مخصص الإيرادات لـ %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Incoming" -msgstr "وارد" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Inconsistent data: more than one external value at the same date for a 'most_recent' external line." -msgstr "بيانات غير متسقة: أكثر من قيمة خارجية في نفس التاريخ لسطر خارجي 'most_recent'." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Inconsistent report_id in options dictionary. Options says %(options_report)s; report is %(report)s." -msgstr "report_id غير متسق في قاموس الخيارات. الخيارات تقول %(options_report)s; التقرير هو %(report)s." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -msgid "Inconsistent Statements" -msgstr "كشوفات غير متسقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_config_settings.py:0 -#, python-format -msgid "Incorrect fiscal year date: day is out of range for month. Month: %(month)s; Day: %(day)s" -msgstr "تاريخ سنة مالية غير صحيح: اليوم خارج النطاق للشهر. الشهر: %(month)s; اليوم: %(day)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -msgid "Initial Balance" -msgstr "الرصيد الافتتاحي" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/aged_partner_balance/filters.js:0 -msgid "Intervals cannot be smaller than 1" -msgstr "لا يمكن أن تكون الفترات أصغر من 1" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid domain formula in expression \"%(expression)s\" of line \"%(line)s\": %(formula)s" -msgstr "صيغة نطاق غير صالحة في التعبير \"%(expression)s\" من السطر \"%(line)s\": %(formula)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid method \"%s\"" -msgstr "طريقة غير صالحة \"%s\"" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invalid statements" -msgstr "كشوفات غير صالحة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid subformula in expression \"%(expression)s\" of line \"%(line)s\": %(subformula)s" -msgstr "صيغة فرعية غير صالحة في التعبير \"%(expression)s\" من السطر \"%(line)s\": %(subformula)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid token '%(token)s' in account_codes formula '%(formula)s'" -msgstr "رمز غير صالح '%(token)s' في صيغة account_codes '%(formula)s'" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_invoice_date -#: model:account.report.column,name:at_accounting.aged_receivable_report_invoice_date -#: model:account.report.column,name:at_accounting.partner_ledger_report_invoicing_date -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invoice Date" -msgstr "تاريخ الفاتورة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invoice lines" -msgstr "أسطر الفاتورة" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_basic -msgid "Invoicing & Banks" -msgstr "الفوترة والبنوك" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__is_write_off_required -msgid "Is a write-off move required to reconcile" -msgstr "هل يتطلب الأمر حركة شطب للمطابقة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__is_transfer_required -msgid "Is an account transfer required" -msgstr "هل يتطلب الأمر تحويل حساب" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__transfer_warning_message -msgid "Is an account transfer required to reconcile" -msgstr "هل يتطلب الأمر تحويل حساب للمطابقة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__lock_date_violated_warning_message -msgid "Is the date violating the lock date of moves" -msgstr "هل التاريخ ينتهك تاريخ قفل الحركات" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "It is not possible to decrease or remove the Hard Lock Date." -msgstr "لا يمكن تقليل أو إزالة تاريخ القفل الصارم." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__salvage_value -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__salvage_value_pct -msgid "It is the amount you plan to have that you cannot depreciate." -msgstr "هو المبلغ الذي تخطط للاحتفاظ به ولا يمكنك إهلاكه." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "It seems there is some depending closing move to be posted" -msgstr "يبدو أن هناك حركة إقفال تابعة يجب ترحيلها" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "It's not possible to select a budget with the horizontal group feature." -msgstr "لا يمكن تحديد ميزانية مع ميزة المجموعة الأفقية." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "It's not possible to select a horizontal group with the budget feature." -msgstr "لا يمكن تحديد مجموعة أفقية مع ميزة الميزانية." - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__item_ids -msgid "Items" -msgstr "بنود" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_journal_code -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__journal_id -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__journal_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal" -msgstr "دفتر يومية" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_ja -msgid "Journal Audit" -msgstr "تدقيق دفتر اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Journal Entries" -msgstr "قيود اليومية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Entry" -msgstr "قيد يومية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Item" -msgstr "بند يومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__original_move_line_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Items" -msgstr "بنود اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Journal Items for Tax Audit" -msgstr "بنود اليومية للتدقيق الضريبي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Journal Items of %(account)s should have a label in order to generate an asset" -msgstr "يجب أن تحتوي بنود اليومية لـ %(account)s على تسمية لإنشاء أصل" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_move_line_posted_unreconciled -msgid "Journal Items to reconcile" -msgstr "بنود اليومية للمطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal items where matching number isn't set" -msgstr "بنود اليومية حيث رقم المطابقة غير محدد" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal items where the account allows reconciliation no matter the residual amount" -msgstr "بنود اليومية حيث يسمح الحساب بالمطابقة بغض النظر عن المبلغ المتبقي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Journal items with archived tax tags" -msgstr "بنود اليومية مع علامات ضريبية مؤرشفة" - -#. module: at_accounting -#: model:account.report,name:at_accounting.journal_report -msgid "Journal Report" -msgstr "تقرير دفتر اليومية" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_journal_report_handler -msgid "Journal Report Custom Handler" -msgstr "معالج مخصص لتقرير دفتر اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Journals" -msgstr "دفاتر اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_label -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__label -msgid "Label" -msgstr "التسمية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_lang -msgid "Lang" -msgstr "اللغة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Last Day" -msgstr "اليوم الأخير" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.last_statement_balance -msgid "Last statement balance" -msgstr "رصيد آخر كشف" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Last Statement balance + Transactions since statement" -msgstr "رصيد آخر كشف + المعاملات منذ الكشف" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Late Activities" -msgstr "أنشطة متأخرة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Later" -msgstr "لاحقاً" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Latest Statement" -msgstr "أحدث كشف" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Legal signatory" -msgstr "الموقع القانوني" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_cost_sales0 -msgid "Less Costs of Revenue" -msgstr "ناقص تكاليف الإيرادات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_expense0 -msgid "Less Operating Expenses" -msgstr "ناقص المصروفات التشغيلية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_depreciation0 -msgid "Less Other Expenses" -msgstr "ناقص المصروفات الأخرى" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Let's go back to the dashboard." -msgstr "لنعد إلى لوحة التحكم." - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_liabilities_view0 -msgid "LIABILITIES" -msgstr "الالتزامات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_liabilities_and_equity_view0 -msgid "LIABILITIES + EQUITY" -msgstr "الالتزامات + حقوق الملكية" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_bank_rec_widget_line -msgid "Line of the bank reconciliation widget" -msgstr "سطر أداة مطابقة البنك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Linear" -msgstr "خطي" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Lines" -msgstr "أسطر" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__linked_assets_ids -msgid "Linked Assets" -msgstr "أصول مرتبطة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__liquidity -msgid "liquidity" -msgstr "سيولة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Load more..." -msgstr "تحميل المزيد..." - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_change_lock_date -msgid "Lock Dates" -msgstr "تواريخ القفل" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date -msgid "Lock Everything" -msgstr "قفل كل شيء" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date_for_everyone -msgid "Lock Everything For Everyone" -msgstr "قفل كل شيء للجميع" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date_for_me -msgid "Lock Everything For Me" -msgstr "قفل كل شيء لي" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_view_account_change_lock_date -msgid "Lock Journal Entries" -msgstr "قفل قيود اليومية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date -msgid "Lock Purchases" -msgstr "قفل المشتريات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date_for_everyone -msgid "Lock Purchases For Everyone" -msgstr "قفل المشتريات للجميع" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date_for_me -msgid "Lock Purchases For Me" -msgstr "قفل المشتريات لي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date -msgid "Lock Sales" -msgstr "قفل المبيعات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date_for_everyone -msgid "Lock Sales For Everyone" -msgstr "قفل المبيعات للجميع" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date_for_me -msgid "Lock Sales For Me" -msgstr "قفل المبيعات لي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date -msgid "Lock Tax Return" -msgstr "قفل الإقرار الضريبي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date_for_everyone -msgid "Lock Tax Return For Everyone" -msgstr "قفل الإقرار الضريبي للجميع" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date_for_me -msgid "Lock Tax Return For Me" -msgstr "قفل الإقرار الضريبي لي" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "loss" -msgstr "خسارة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__main_company_id -msgid "Main Company" -msgstr "الشركة الرئيسية" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__main_company_id -msgid "Main company of this unit; the one actually reporting and paying the taxes." -msgstr "الشركة الرئيسية لهذه الوحدة؛ التي تقوم فعلياً بالإبلاغ ودفع الضرائب." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Make Adjustment Entry" -msgstr "إنشاء قيد تعديل" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_bank_statement_import_csv.py:0 -msgid "Make sure that an Amount or Debit and Credit is in the file." -msgstr "تأكد من وجود مبلغ أو مدين ودائن في الملف." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Manage Items" -msgstr "إدارة البنود" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_file_download_error_wizard -msgid "Manage the file generation errors from report exports." -msgstr "إدارة أخطاء إنشاء الملفات من تصدير التقارير." - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__manual -msgid "manual" -msgstr "يدوي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "Manual (or import %(import_formats)s)" -msgstr "يدوي (أو استيراد %(import_formats)s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Manual value" -msgstr "قيمة يدوية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Manual values" -msgstr "قيم يدوية" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/list_view_switcher.js:0 -msgid "Match" -msgstr "مطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Matched" -msgstr "مُطابَق" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_payment.py:0 -msgid "Matched Transactions" -msgstr "معاملات مُطابَقة" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_matching_number -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Matching" -msgstr "مطابقة" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__company_ids -msgid "Members of this unit" -msgstr "أعضاء هذه الوحدة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Memo" -msgstr "مذكرة" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_first_method -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method -msgid "Method" -msgstr "طريقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Method '%(method_name)s' must start with the '%(prefix)s' prefix." -msgstr "الطريقة '%(method_name)s' يجب أن تبدأ بالبادئة '%(prefix)s'." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "Misc" -msgstr "متفرقات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.misc_operations -msgid "Misc. operations" -msgstr "عمليات متفرقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_csv.py:0 -msgid "Mixing CSV files with other file types is not allowed." -msgstr "لا يُسمح بخلط ملفات CSV مع أنواع ملفات أخرى." - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__model_id -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__model -msgid "Model" -msgstr "نموذج" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify" -msgstr "تعديل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model,name:at_accounting.model_asset_modify -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify Asset" -msgstr "تعديل الأصل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify Depreciation" -msgstr "تعديل الإهلاك" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Month" -msgstr "شهر" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Months" -msgstr "أشهر" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__move_line_ids -msgid "Move lines to reconcile" -msgstr "أسطر الحركات للمطابقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Multi-ledger" -msgstr "دفاتر متعددة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_multicurrency_revaluation_report_handler -msgid "Multicurrency Revaluation Report Custom Handler" -msgstr "معالج مخصص لتقرير إعادة تقييم العملات المتعددة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_multicurrency_revaluation_wizard -msgid "Multicurrency Revaluation Wizard" -msgstr "معالج إعادة تقييم العملات المتعددة" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "" -"Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \n" -" %(closing_entries)s\n" -msgstr "توجد عدة قيود إقفال ضريبي مسودة للوضع المالي %(position)s بعد %(period_start)s. يجب أن يكون هناك واحد على الأكثر. \n %(closing_entries)s\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \\n %(closing_entries)s" -msgstr "توجد عدة قيود إقفال ضريبي مسودة للموقع المالي %(position)s بعد %(period_start)s. يجب أن يكون هناك واحد على الأكثر. \\n %(closing_entries)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "" -"Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \n" -" %(closing_entries)s\n" -msgstr "توجد عدة قيود إقفال ضريبي مسودة لمنطقتك المحلية بعد %(period_start)s. يجب أن يكون هناك واحد على الأكثر. \n %(closing_entries)s\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \\n %(closing_entries)s" -msgstr "توجد عدة قيود إقفال ضريبي مسودة لمنطقتك المحلية بعد %(period_start)s. يجب أن يكون هناك واحد على الأكثر. \\n %(closing_entries)s" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_report_send__mode__multi -msgid "Multiple Recipients" -msgstr "مستلمون متعددون" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "n/a" -msgstr "غ/م" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.line,name:at_accounting.journal_report_line -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__name -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__name -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__name -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__name -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Name" -msgstr "الاسم" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_reports_export_wizard_format__doc_name -msgid "Name to give to the generated documents." -msgstr "الاسم المراد إعطاؤه للمستندات المُنشأة." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Navigate easily through reports and see what is behind the numbers" -msgstr "تنقل بسهولة عبر التقارير وانظر ما وراء الأرقام" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__negative_revaluation -msgid "Negative revaluation" -msgstr "إعادة تقييم سلبية" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_net_assets0 -msgid "Net assets" -msgstr "صافي الأصول" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__net_gain_on_sale -msgid "Net gain on sale" -msgstr "صافي الربح من البيع" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Net increase in cash and cash equivalents" -msgstr "صافي الزيادة في النقد وما يعادله" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_profit0 -#: model:account.report.line,name:at_accounting.account_financial_report_net_profit0 -msgid "Net Profit" -msgstr "صافي الربح" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_npmargin0 -msgid "Net profit margin (net profit / income)" -msgstr "هامش صافي الربح (صافي الربح / الدخل)" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__net_gain_on_sale -msgid "Net value of gain or loss on sale of an asset" -msgstr "صافي قيمة الربح أو الخسارة من بيع أصل" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__value_residual -msgid "New residual amount for the asset" -msgstr "المبلغ المتبقي الجديد للأصل" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__salvage_value -msgid "New salvage amount for the asset" -msgstr "قيمة الإنقاذ الجديدة للأصل" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_form_bank_rec_widget -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "New Transaction" -msgstr "معاملة جديدة" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__new_aml -msgid "new_aml" -msgstr "new_aml" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No" -msgstr "لا" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -msgid "No adjustment needed" -msgstr "لا حاجة للتعديل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "No attachment was provided" -msgstr "لم يتم توفير مرفق" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "No currency found matching '%s'." -msgstr "لم يتم العثور على عملة مطابقة لـ '%s'." - -#. module: at_accounting -#: code:addons/at_accounting/models/chart_template.py:0 -msgid "No default miscellaneous journal could be found for the active company" -msgstr "لم يتم العثور على دفتر يومية متفرقات افتراضي للشركة النشطة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "No entry to generate." -msgstr "لا يوجد قيد لإنشائه." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No Journal" -msgstr "بدون دفتر يومية" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__none -msgid "No Prorata" -msgstr "بدون توزيع نسبي" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -msgid "No provision needed was found." -msgstr "لم يتم العثور على مخصص مطلوب." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "No statement" -msgstr "بدون كشف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "No transactions matching your filters were found." -msgstr "لم يتم العثور على معاملات مطابقة لفلاترك." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No VAT number associated with your company. Please define one." -msgstr "لا يوجد رقم ضريبة قيمة مضافة مرتبط بشركتك. يرجى تحديد واحد." - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__non_deductible_tax_value -msgid "Non Deductible Tax Value" -msgstr "قيمة الضريبة غير القابلة للخصم" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Non Trade Partners" -msgstr "شركاء غير تجاريين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Non Trade Payable" -msgstr "ذمم دائنة غير تجارية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Non Trade Receivable" -msgstr "ذمم مدينة غير تجارية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Non-Deductible" -msgstr "غير قابل للخصم" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/sales_report/filters/filters.js:0 -msgid "None" -msgstr "لا شيء" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__salvage_value -msgid "Not Depreciable Amount" -msgstr "المبلغ غير القابل للإهلاك" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__salvage_value -msgid "Not Depreciable Value" -msgstr "القيمة غير القابلة للإهلاك" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__salvage_value_pct -msgid "Not Depreciable Value Percent" -msgstr "نسبة القيمة غير القابلة للإهلاك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Not locked" -msgstr "غير مقفل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Not Matched" -msgstr "غير مُطابَق" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Not Started" -msgstr "لم يبدأ" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__name -msgid "Note" -msgstr "ملاحظة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Notes" -msgstr "ملاحظات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "Nothing to do here!" -msgstr "لا شيء للقيام به هنا!" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Now, we'll create your first invoice (accountant)" -msgstr "الآن، سننشئ فاتورتك الأولى (محاسب)" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__gross_increase_count -msgid "Number of assets made to increase the value of the asset" -msgstr "عدد الأصول التي تم إنشاؤها لزيادة قيمة الأصل" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_number_days -msgid "Number of days" -msgstr "عدد الأيام" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__total_depreciation_entries_count -msgid "Number of depreciation entries (posted or not)" -msgstr "عدد قيود الإهلاك (مرحّلة أو غير مرحّلة)" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Number of Depreciations" -msgstr "عدد الإهلاكات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_period -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__method_period -msgid "Number of Months in a Period" -msgstr "عدد الأشهر في الفترة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Number of periods cannot be smaller than 1" -msgstr "لا يمكن أن يكون عدد الفترات أقل من 1" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/aged_partner_balance/filters.js:0 -msgid "Odoo Warning" -msgstr "تحذير أودو" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_off_sheet -msgid "OFF BALANCE SHEET ACCOUNTS" -msgstr "حسابات خارج الميزانية" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period5 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period5 -msgid "Older" -msgstr "أقدم" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__paused -msgid "On Hold" -msgstr "معلق" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/report_export_wizard.py:0 -msgid "One of the formats chosen can not be exported in the DMS" -msgstr "لا يمكن تصدير أحد التنسيقات المختارة في نظام إدارة المستندات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "Only Billing Administrators are allowed to change lock dates!" -msgstr "مسؤولو الفوترة فقط مسموح لهم بتغيير تواريخ القفل!" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_csv.py:0 -msgid "Only one CSV file can be selected." -msgstr "يمكن تحديد ملف CSV واحد فقط." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Open Asset" -msgstr "فتح الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -#, python-format -msgid "Open balance of %(amount)s" -msgstr "رصيد مفتوح بقيمة %(amount)s" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_reports_customer_statements -msgid "Open Customer Statements" -msgstr "فتح كشوفات العملاء" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_operating_income0 -msgid "Operating Income (or Loss)" -msgstr "الدخل التشغيلي (أو الخسارة)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Options" -msgstr "خيارات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Original Deferred Entries" -msgstr "قيود مؤجلة أصلية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_original_move_ids -msgid "Original Invoices" -msgstr "فواتير أصلية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__original_value -msgid "Original Value" -msgstr "القيمة الأصلية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Originator Tax" -msgstr "ضريبة المنشأ" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Outgoing" -msgstr "صادر" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding -msgid "Outstanding Receipts/Payments" -msgstr "إيصالات/مدفوعات معلقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Parent Asset" -msgstr "الأصل الرئيسي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__report_id -msgid "Parent Report Id" -msgstr "معرف التقرير الرئيسي" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__export_wizard_id -msgid "Parent Wizard" -msgstr "المعالج الرئيسي" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_partner_name -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__to_partner_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Partner" -msgstr "شريك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Partner Categories" -msgstr "فئات الشركاء" - -#. module: at_accounting -#: model:account.report,name:at_accounting.partner_ledger_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_partner_ledger -msgid "Partner Ledger" -msgstr "دفتر أستاذ الشركاء" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_partner_ledger_report_handler -msgid "Partner Ledger Custom Handler" -msgstr "معالج مخصص لدفتر أستاذ الشركاء" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Partner(s) should have an email address." -msgstr "يجب أن يكون للشريك/الشركاء عنوان بريد إلكتروني." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__partner_ids -msgid "Partners" -msgstr "شركاء" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Pause" -msgstr "إيقاف مؤقت" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Pay tax: %s" -msgstr "دفع الضريبة: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_aged_partner_balance.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payable" -msgstr "دائن" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Payable tax amount" -msgstr "مبلغ الضريبة المستحقة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities_payable -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_creditors0 -msgid "Payables" -msgstr "الذمم الدائنة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payment Matching" -msgstr "مطابقة المدفوعات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__payment_state_before_switch -msgid "Payment State Before Switch" -msgstr "حالة الدفع قبل التبديل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payments" -msgstr "مدفوعات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "PDF" -msgstr "PDF" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_auto_reconcile_wizard__search_mode__one_to_one -msgid "Perfect Match" -msgstr "مطابقة تامة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_performance0 -msgid "Performance" -msgstr "الأداء" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Period" -msgstr "فترة" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period1 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period1 -msgid "Period 1" -msgstr "الفترة 1" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period2 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period2 -msgid "Period 2" -msgstr "الفترة 2" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period3 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period3 -msgid "Period 3" -msgstr "الفترة 3" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period4 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period4 -msgid "Period 4" -msgstr "الفترة 4" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Period length" -msgstr "طول الفترة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Periodicity" -msgstr "الدورية" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Periods" -msgstr "فترات" - -#. module: at_accounting -#: code:addons/at_accounting/models/budget.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Please enter a valid budget name." -msgstr "يرجى إدخال اسم ميزانية صالح." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Please select a mail template to send multiple statements." -msgstr "يرجى تحديد قالب بريد لإرسال كشوفات متعددة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Please select the main company and its branches in the company selector to proceed." -msgstr "يرجى تحديد الشركة الرئيسية وفروعها في محدد الشركة للمتابعة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Please set the deferred accounts in the accounting settings." -msgstr "يرجى تعيين الحسابات المؤجلة في إعدادات المحاسبة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Please set the deferred journal in the accounting settings." -msgstr "يرجى تعيين دفتر اليومية المؤجل في إعدادات المحاسبة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Please specify the accounts necessary for the Tax Closing Entry." -msgstr "يرجى تحديد الحسابات اللازمة لقيد إقفال الضرائب." - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_fixed_assets_view0 -msgid "Plus Fixed Assets" -msgstr "زائد الأصول الثابتة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_non_current_assets_view0 -msgid "Plus Non-current Assets" -msgstr "زائد الأصول غير المتداولة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_non_current_liabilities0 -msgid "Plus Non-current Liabilities" -msgstr "زائد الالتزامات غير المتداولة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_other_income0 -msgid "Plus Other Income" -msgstr "زائد الإيرادات الأخرى" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_position0 -msgid "Position" -msgstr "الموقع" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__positive_revaluation -msgid "Positive revaluation" -msgstr "إعادة تقييم إيجابية" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Posted Entries" -msgstr "قيود مرحّلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Predict vendor bill product" -msgstr "توقع منتج فاتورة المورد" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_prepayements0 -msgid "Prepayments" -msgstr "دفعات مقدمة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings_line_2 -msgid "Previous Years Retained Earnings" -msgstr "أرباح محتجزة للسنوات السابقة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_previous_year_earnings0 -msgid "Previous Years Unallocated Earnings" -msgstr "أرباح غير موزعة للسنوات السابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Print & Send" -msgstr "طباعة وإرسال" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Proceed" -msgstr "متابعة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Product" -msgstr "منتج" - -#. module: at_accounting -#: model:account.report,name:at_accounting.profit_and_loss -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_profit_and_loss -msgid "Profit and Loss" -msgstr "الأرباح والخسائر" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_profitability0 -msgid "Profitability" -msgstr "الربحية" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Properties" -msgstr "الخصائص" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__prorata_date -msgid "Prorata Date" -msgstr "تاريخ التوزيع النسبي" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Provision for %(for_cur)s (1 %(comp_cur)s = %(rate)s %(for_cur)s)" -msgstr "مخصص لـ %(for_cur)s (1 %(comp_cur)s = %(rate)s %(for_cur)s)" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__purchase -msgid "Purchase" -msgstr "شراء" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Quarter" -msgstr "ربع سنة" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "Re-evaluate" -msgstr "إعادة التقييم" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_readonly -msgid "Read-only" -msgstr "للقراءة فقط" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reason..." -msgstr "السبب..." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_aged_partner_balance.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Receivable" -msgstr "مدين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Receivable tax amount" -msgstr "مبلغ الضريبة المستحقة للتحصيل" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_debtors0 -#: model:account.report.line,name:at_accounting.account_financial_report_receivable0 -msgid "Receivables" -msgstr "الذمم المدينة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_partner_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Recipients" -msgstr "المستلمون" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__search_mode -#: model:ir.ui.menu,name:at_accounting.menu_account_reconcile -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile" -msgstr "مطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile & open" -msgstr "مطابقة وفتح" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_account_id -msgid "Reconcile Account" -msgstr "حساب المطابقة" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_open_auto_reconcile_wizard -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile automatically" -msgstr "مطابقة تلقائية" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_auto_reconcile_wizard__search_mode -msgid "Reconcile journal items with opposite balance or clear accounts with a zero balance" -msgstr "مطابقة بنود اليومية ذات الرصيد المعاكس أو تصفية الحسابات ذات الرصيد الصفري" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_model_id -msgid "Reconciliation model" -msgstr "نموذج المطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Record cost of goods sold in your journal entries" -msgstr "تسجيل تكلفة البضاعة المباعة في قيود اليومية" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__linked_asset_ids -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Assets" -msgstr "أصول ذات صلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Purchase(s)" -msgstr "مشتريات ذات صلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Sale(s)" -msgstr "مبيعات ذات صلة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reminder" -msgstr "تذكير" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__account_report_id -msgid "Report" -msgstr "تقرير" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Report Line" -msgstr "سطر التقرير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Report lines mentioning the account code" -msgstr "أسطر التقرير التي تذكر رمز الحساب" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Report Name" -msgstr "اسم التقرير" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reporting" -msgstr "التقارير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Reset to running" -msgstr "إعادة التعيين إلى نشط" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Residual" -msgstr "متبقي" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Residual in Currency" -msgstr "المتبقي بالعملة" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Resume" -msgstr "استئناف" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Resume Depreciation" -msgstr "استئناف الإهلاك" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings0 -msgid "Retained Earnings" -msgstr "الأرباح المحتجزة" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_return_investment0 -msgid "Return on investments (net profit / assets)" -msgstr "العائد على الاستثمارات (صافي الربح / الأصول)" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_income0 -#: model:account.report.line,name:at_accounting.account_financial_report_revenue0 -msgid "Revenue" -msgstr "إيرادات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "Reversal of Grouped Deferral Entry of %s" -msgstr "عكس قيد التأجيل المجمع لـ %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Reversal of: %s" -msgstr "عكس: %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "Reverse the depreciation entries posted in the future in order to modify the depreciation" -msgstr "عكس قيود الإهلاك المرحّلة في المستقبل لتعديل الإهلاك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Root Report" -msgstr "التقرير الجذري" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_bank_statement_import_csv.py:0 -msgid "Rows must be sorted by date." -msgstr "يجب فرز الصفوف حسب التاريخ." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Run manually" -msgstr "تشغيل يدوي" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__open -msgid "Running" -msgstr "نشط" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__sale -msgid "Sale" -msgstr "بيع" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save" -msgstr "حفظ" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save & Close" -msgstr "حفظ وإغلاق" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save & New" -msgstr "حفظ وجديد" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save as Model" -msgstr "حفظ كنموذج" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Save model" -msgstr "حفظ النموذج" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Search Journal Items to Reconcile" -msgstr "البحث عن بنود اليومية للمطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Sections" -msgstr "أقسام" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Sell" -msgstr "بيع" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -msgid "Send" -msgstr "إرسال" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -msgid "Send Partner Ledgers" -msgstr "إرسال دفاتر أستاذ الشركاء" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Send tax report: %s" -msgstr "إرسال تقرير الضرائب: %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Sending statements" -msgstr "إرسال الكشوفات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__sequence -msgid "Sequence" -msgstr "تسلسل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Services" -msgstr "خدمات" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Set an amount." -msgstr "تعيين مبلغ." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set as Checked" -msgstr "تعيين كمُراجَع" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Set the payment reference." -msgstr "تعيين مرجع الدفع." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set to Draft" -msgstr "تعيين كمسودة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set to Running" -msgstr "تعيين كنشط" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.account.menu_account_config -msgid "Settings" -msgstr "إعدادات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_st_cash_forecast0 -msgid "Short term cash forecast" -msgstr "توقعات النقد قصيرة الأجل" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Show all records which has next action date is before today" -msgstr "عرض جميع السجلات التي تاريخ إجرائها التالي قبل اليوم" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__show_draft_entries_warning -msgid "Show Draft Entries Warning" -msgstr "عرض تحذير القيود المسودة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__signing_user -msgid "Signer" -msgstr "الموقّع" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_report_send__mode__single -msgid "Single Recipient" -msgstr "مستلم واحد" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Some fields are missing %s" -msgstr "بعض الحقول مفقودة %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Some required values are missing" -msgstr "بعض القيم المطلوبة مفقودة" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__date_from -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_start_date -msgid "Start Date" -msgstr "تاريخ البدء" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_fiscal_year__date_from -msgid "Start Date, included in the fiscal year." -msgstr "تاريخ البدء، مشمول في السنة المالية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Starting Balance" -msgstr "الرصيد الافتتاحي" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__prorata_date -msgid "Starting date of the period used in the prorata calculation of the first depreciation" -msgstr "تاريخ بداية الفترة المستخدمة في حساب التوزيع النسبي للإهلاك الأول" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_bank_statement_attachment -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Statement" -msgstr "كشف" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Statement Line" -msgstr "سطر الكشف" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Statements are being sent in the background." -msgstr "يتم إرسال الكشوفات في الخلفية." - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__state -msgid "Status" -msgstr "الحالة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Stock Valuation" -msgstr "تقييم المخزون" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__linear -msgid "Straight Line" -msgstr "قسط ثابت" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_subject -msgid "Subject" -msgstr "الموضوع" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Subject..." -msgstr "الموضوع..." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Suggestions" -msgstr "اقتراحات" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__book_value -msgid "Sum of the depreciable value, the salvage value and the book value of all value increase items" -msgstr "مجموع القيمة القابلة للإهلاك، قيمة الإنقاذ والقيمة الدفترية لجميع بنود زيادة القيمة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "T: %s" -msgstr "ض: %s" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__tax_id -msgid "Tax" -msgstr "ضريبة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Tax Amount" -msgstr "مبلغ الضريبة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -msgid "Tax Declaration" -msgstr "إقرار ضريبي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Grids" -msgstr "شبكات الضرائب" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__vat -msgid "Tax ID" -msgstr "الرقم الضريبي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Tax Paid Adjustment" -msgstr "تعديل الضريبة المدفوعة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Tax Received Adjustment" -msgstr "تعديل الضريبة المستلمة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Report" -msgstr "تقرير الضرائب" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_gt -msgid "Tax Return" -msgstr "الإقرار الضريبي" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -msgid "Tax return" -msgstr "الإقرار الضريبي" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Return Periodicity" -msgstr "دورية الإقرار الضريبي" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_tax_unit -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Unit" -msgstr "وحدة ضريبية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -#, python-format -msgid "tax unit [%s]" -msgstr "وحدة ضريبية [%s]" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_view_tax_units -msgid "Tax Units" -msgstr "وحدات ضريبية" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__tax_line -msgid "tax_line" -msgstr "سطر_الضريبة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Taxes" -msgstr "ضرائب" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Taxes Applied" -msgstr "الضرائب المطبقة" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__fpos_synced -msgid "Technical field indicating whether Fiscal Positions exist for all companies in the unit" -msgstr "حقل تقني يشير إلى ما إذا كانت الأوضاع المالية موجودة لجميع الشركات في الوحدة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_ir_actions_account_report_download -msgid "Technical model for accounting report downloads" -msgstr "نموذج تقني لتنزيلات التقارير المحاسبية" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/ellipsis/ellipsis.js:0 -msgid "Text copied" -msgstr "تم نسخ النص" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The account %(exp_acc)s has been credited by %(exp_delta)s," -msgstr "تم قيد الحساب %(exp_acc)s بمبلغ %(exp_delta)s،" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The account %(exp_acc)s has been credited by %(exp_delta)s, while the account %(dep_acc)s has been debited by %(dep_delta)s. This corresponds to %(move_count)s cancelled %(word)s:" -msgstr "تم قيد الحساب %(exp_acc)s بمبلغ %(exp_delta)s، بينما تم خصم الحساب %(dep_acc)s بمبلغ %(dep_delta)s. هذا يتوافق مع %(move_count)s %(word)s ملغاة:" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "The account of this statement (%(account)s) is not the same as the journal (%(journal)s)." -msgstr "حساب هذا الكشف (%(account)s) ليس نفس دفتر اليومية (%(journal)s)." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "The Accounts Coverage Report is not available for this report." -msgstr "تقرير تغطية الحسابات غير متاح لهذا التقرير." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single credit line should be strictly negative." -msgstr "يجب أن يكون مبلغ شطب سطر دائن واحد سالباً تماماً." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single debit line should be strictly positive." -msgstr "يجب أن يكون مبلغ شطب سطر مدين واحد موجباً تماماً." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single line cannot be 0." -msgstr "لا يمكن أن يكون مبلغ شطب سطر واحد صفراً." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method_period -#: model:ir.model.fields,help:at_accounting.field_asset_modify__method_period -msgid "The amount of time between two depreciations" -msgstr "الفترة الزمنية بين إهلاكين" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s)." -msgstr "المبلغ الذي أدخلته (%(entered_amount)s) لا يتطابق مع قيمة الشراء ذي الصلة (%(purchase_value)s)." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s). Please make sure this is what you want." -msgstr "المبلغ الذي أدخلته (%(entered_amount)s) لا يتطابق مع قيمة الشراء ذي الصلة (%(purchase_value)s). يرجى التأكد من أن هذا ما تريده." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/line_name/popover_line/annotation_popover_line.js:0 -msgid "The annotation shouldn't have an empty value." -msgstr "لا يجب أن تكون قيمة التعليق فارغة." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__asset_id -msgid "The asset to be modified by this wizard" -msgstr "الأصل الذي سيتم تعديله بواسطة هذا المعالج" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "The attachments of the tax report can be found on the closing entry of the representative company." -msgstr "يمكن العثور على مرفقات تقرير الضرائب في قيد الإقفال للشركة الممثلة." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__children_ids -msgid "The children are the gains in value of this asset" -msgstr "الأصول الفرعية هي مكاسب قيمة هذا الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#, python-format -msgid "The column '%s' is not available for this report." -msgstr "العمود '%s' غير متاح لهذا التقرير." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "The country detected for this VAT number does not match the one set on this Tax Unit." -msgstr "البلد المكتشف لرقم ضريبة القيمة المضافة هذا لا يتطابق مع البلد المحدد في هذه الوحدة الضريبية." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__country_id -msgid "The country in which this tax unit is used to group your companies' tax reports declaration." -msgstr "البلد الذي تُستخدم فيه هذه الوحدة الضريبية لتجميع إقرارات التقارير الضريبية لشركاتك." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "The currency of the bank statement (%(code)s) is not the same as the currency of the journal (%(journal)s)." -msgstr "عملة كشف الحساب البنكي (%(code)s) ليست نفس عملة دفتر اليومية (%(journal)s)." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "The currency rate cannot be equal to zero" -msgstr "لا يمكن أن يكون سعر صرف العملة صفراً" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "The currently selected dates don't match a tax period. The closing entry will be created for the closest-matching period according to your periodicity setup." -msgstr "التواريخ المحددة حالياً لا تتطابق مع فترة ضريبية. سيتم إنشاء قيد الإقفال لأقرب فترة مطابقة وفقاً لإعداد الدورية الخاص بك." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "The date you set violates the lock date of one of your entry. It will be overriden by the following date : %(replacement_date)s" -msgstr "التاريخ الذي حددته ينتهك تاريخ قفل أحد قيودك. سيتم استبداله بالتاريخ التالي: %(replacement_date)s" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_move_ids -msgid "The deferred entries created by this invoice" -msgstr "القيود المؤجلة التي أنشأتها هذه الفاتورة" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__invoice_ids -msgid "The disposal invoice is needed in order to generate the closing journal entry." -msgstr "فاتورة التصرف مطلوبة لإنشاء قيد الإقفال." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The email address is unknown on the partner" -msgstr "عنوان البريد الإلكتروني غير معروف للشريك" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "The ending date must not be prior to the starting date." -msgstr "يجب ألا يكون تاريخ الانتهاء قبل تاريخ البدء." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "The following files could not be imported:" -msgstr "لم يتم استيراد الملفات التالية:" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "The following files could not be imported:\\n" -msgstr "لم يتم استيراد الملفات التالية:\\n" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__vat -msgid "The identifier to be used when submitting a report for this unit." -msgstr "المعرف المستخدم عند تقديم تقرير لهذه الوحدة." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "The invoice %(display_name_html)s with an open amount of %(open_amount)s will be entirely paid by the transaction." -msgstr "الفاتورة %(display_name_html)s بمبلغ مفتوح %(open_amount)s سيتم دفعها بالكامل بواسطة المعاملة." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "The invoice %(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s." -msgstr "الفاتورة %(display_name_html)s بمبلغ مفتوح %(open_amount)s سيتم تخفيضها بمقدار %(amount)s." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The invoices up to this date will not be taken into account as accounting entries" -msgstr "الفواتير حتى هذا التاريخ لن تؤخذ في الاعتبار كقيود محاسبية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "The main company of a tax unit has to be part of it." -msgstr "يجب أن تكون الشركة الرئيسية للوحدة الضريبية جزءاً منها." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method_number -msgid "The number of depreciations needed to depreciate your asset" -msgstr "عدد الإهلاكات اللازمة لإهلاك أصلك" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_original_move_ids -msgid "The original invoices that created the deferred entries" -msgstr "الفواتير الأصلية التي أنشأت القيود المؤجلة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "The remaining value on the last depreciation line must be 0" -msgstr "يجب أن تكون القيمة المتبقية في آخر سطر إهلاك صفراً" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The system will try to predict the product on vendor bill lines based on the label of the line" -msgstr "سيحاول النظام توقع المنتج في أسطر فاتورة المورد بناءً على تسمية السطر" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "The used operator is not supported for this expression." -msgstr "المشغل المستخدم غير مدعوم لهذا التعبير." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "There are currently reports waiting to be sent, please try again later." -msgstr "توجد تقارير تنتظر الإرسال حالياً، يرجى المحاولة مرة أخرى لاحقاً." - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__invoice_line_ids -msgid "There are multiple lines that could be the related to this asset" -msgstr "توجد أسطر متعددة قد تكون مرتبطة بهذا الأصل" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "There are unposted depreciations prior to the selected operation date, please deal with them first." -msgstr "توجد إهلاكات غير مرحّلة قبل تاريخ العملية المحدد، يرجى معالجتها أولاً." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account exists in the Chart of Accounts but is not mentioned in any line of the report" -msgstr "هذا الحساب موجود في شجرة الحسابات ولكنه غير مذكور في أي سطر من التقرير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported in a line of the report but does not exist in the Chart of Accounts" -msgstr "هذا الحساب مذكور في سطر من التقرير ولكنه غير موجود في شجرة الحسابات" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported in multiple lines of the report" -msgstr "هذا الحساب مذكور في أسطر متعددة من التقرير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported multiple times on the same line of the report" -msgstr "هذا الحساب مذكور عدة مرات في نفس سطر التقرير" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "This allows you to choose the position of totals in your financial reports." -msgstr "يتيح لك هذا اختيار موضع الإجماليات في تقاريرك المالية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "This bank transaction has been automatically validated using the reconciliation model '%s'." -msgstr "تم التحقق من معاملة البنك هذه تلقائياً باستخدام نموذج المطابقة '%s'." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "This bank transaction is locked up tighter than a squirrel in a nut factory! You can't hit the reset button on it. So, do you want to \\" -msgstr "هذه المعاملة البنكية مقفلة بإحكام! لا يمكنك إعادة تعيينها. لذا، هل تريد \\" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "This can only be used on journal items" -msgstr "يمكن استخدام هذا فقط على بنود اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "" -"This file doesn't contain any statement for account %s.\n" -"If it contains transactions for more than one account, it must be imported on each of them.\n" -msgstr "هذا الملف لا يحتوي على أي كشف للحساب %s.\nإذا كان يحتوي على معاملات لأكثر من حساب واحد، يجب استيراده على كل منها.\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "" -"This file doesn't contain any transaction for account %s.\n" -"If it contains transactions for more than one account, it must be imported on each of them.\n" -msgstr "هذا الملف لا يحتوي على أي معاملة للحساب %s.\nإذا كان يحتوي على معاملات لأكثر من حساب واحد، يجب استيراده على كل منها.\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "This file doesn\\" -msgstr "هذا الملف لا\\" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/widgets/account_report_x2many/account_report_x2many.js:0 -msgid "This line and all its children will be deleted. Are you sure you want to proceed?" -msgstr "سيتم حذف هذا السطر وجميع عناصره الفرعية. هل أنت متأكد أنك تريد المتابعة؟" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "This option hides lines with a value of 0" -msgstr "هذا الخيار يخفي الأسطر ذات القيمة 0" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_reconcile_model_line.py:0 -msgid "This reconciliation model can't be used in the manual reconciliation widget because its" -msgstr "لا يمكن استخدام نموذج المطابقة هذا في أداة المطابقة اليدوية لأن" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_reconcile_model_line.py:0 -msgid "This reconciliation model can't be used in the manual reconciliation widget because its configuration is not adapted" -msgstr "لا يمكن استخدام نموذج المطابقة هذا في أداة المطابقة اليدوية لأن إعداداته غير متوافقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This report already has a menuitem." -msgstr "هذا التقرير لديه عنصر قائمة بالفعل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "This subformula references an unknown expression: %s" -msgstr "هذه الصيغة الفرعية تشير إلى تعبير غير معروف: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This tag is reported in a line of the report but is not linked to any account of the Chart of Accounts" -msgstr "هذه العلامة مذكورة في سطر من التقرير ولكنها غير مرتبطة بأي حساب في شجرة الحسابات" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__to_date -msgid "To" -msgstr "إلى" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__to_check -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "To Check" -msgstr "للمراجعة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "To enhance authenticity, add a signature to your invoices" -msgstr "لتعزيز المصداقية، أضف توقيعاً إلى فواتيرك" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Today Activities" -msgstr "أنشطة اليوم" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_total -#: model:account.report.column,name:at_accounting.aged_receivable_report_total -msgid "Total" -msgstr "إجمالي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Total %s" -msgstr "إجمالي %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Balance" -msgstr "إجمالي الرصيد" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Credit" -msgstr "إجمالي الدائن" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Debit" -msgstr "إجمالي المدين" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Residual" -msgstr "إجمالي المتبقي" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Residual in Currency" -msgstr "إجمالي المتبقي بالعملة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Trade Partners" -msgstr "شركاء تجاريون" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Transaction" -msgstr "معاملة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Transactions" -msgstr "معاملات" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.transaction_without_statement -msgid "Transactions without statement" -msgstr "معاملات بدون كشف" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "Transfer from %s" -msgstr "تحويل من %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "Transfer to %s" -msgstr "تحويل إلى %s" - -#. module: at_accounting -#: model:account.report,name:at_accounting.trial_balance_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_coa -msgid "Trial Balance" -msgstr "ميزان المراجعة" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_trial_balance_report_handler -msgid "Trial Balance Custom Handler" -msgstr "معالج مخصص لميزان المراجعة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Triangular" -msgstr "مثلث" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to dispatch an action on a report not compatible with the provided options." -msgstr "محاولة إرسال إجراء على تقرير غير متوافق مع الخيارات المقدمة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Trying to expand a group for a line which was not generated by a report line: %s" -msgstr "محاولة توسيع مجموعة لسطر لم يتم إنشاؤه بواسطة سطر تقرير: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to expand a line without an expansion function." -msgstr "محاولة توسيع سطر بدون دالة توسيع." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to expand groupby results on lines without a groupby value." -msgstr "محاولة توسيع نتائج التجميع على أسطر بدون قيمة تجميع." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Turn as an asset" -msgstr "تحويل إلى أصل" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_type -msgid "Type of the account" -msgstr "نوع الحساب" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_unaffected_earnings0 -msgid "Unallocated Earnings" -msgstr "أرباح غير موزعة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Unknown" -msgstr "غير معروف" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Unknown bound criterium: %s" -msgstr "معيار حد غير معروف: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Unknown date scope: %s" -msgstr "نطاق تاريخ غير معروف: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Unknown Partner" -msgstr "شريك غير معروف" - -#. module: at_accounting -#: model:account.report,name:at_accounting.multicurrency_revaluation_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_multicurrency_revaluation -msgid "Unrealized Currency Gains/Losses" -msgstr "مكاسب/خسائر عملة غير محققة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Unreconciled" -msgstr "غير مطابق" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Unreconciled Entries" -msgstr "قيود غير مطابقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -msgid "Unreconciled statements lines" -msgstr "أسطر كشوفات غير مطابقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Validate" -msgstr "مصادقة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Value at Import" -msgstr "القيمة عند الاستيراد" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Value decrease for: %(asset)s" -msgstr "انخفاض قيمة لـ: %(asset)s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Value increase for: %(asset)s" -msgstr "زيادة قيمة لـ: %(asset)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Vat closing from %(date_from)s to %(date_to)s" -msgstr "إقفال ضريبة القيمة المضافة من %(date_from)s إلى %(date_to)s" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_vat -msgid "VAT Number" -msgstr "رقم ضريبة القيمة المضافة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "VAT Periodicity" -msgstr "دورية ضريبة القيمة المضافة" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/list_view_switcher.js:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "View" -msgstr "عرض" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Bank Statement" -msgstr "عرض كشف الحساب البنكي" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Journal Entry" -msgstr "عرض قيد اليومية" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "View Partner" -msgstr "عرض الشريك" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "View Partner(s)" -msgstr "عرض الشريك/الشركاء" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Payment" -msgstr "عرض الدفعة" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "View Reconciled Entries" -msgstr "عرض القيود المطابقة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "View successfully imported statements" -msgstr "عرض الكشوفات المستوردة بنجاح" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Warning for the Original Value of %s" -msgstr "تحذير للقيمة الأصلية لـ %s" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__state -msgid "" -"When an asset is created, the status is 'Draft'.\n" -"If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" -"The 'On Hold' status can be set manually when you want to pause the depreciation of an asset for some time.\n" -"You can manually close an asset when the depreciation is over.\n" -"By cancelling an asset, all depreciation entries will be reversed\n" -msgstr "عند إنشاء أصل، تكون الحالة 'مسودة'.\nإذا تم تأكيد الأصل، تصبح الحالة 'نشط' ويمكن ترحيل أسطر الإهلاك في المحاسبة.\nيمكن تعيين حالة 'معلق' يدوياً عندما تريد إيقاف إهلاك أصل لبعض الوقت.\nيمكنك إغلاق أصل يدوياً عند انتهاء الإهلاك.\nعند إلغاء أصل، سيتم عكس جميع قيود الإهلاك\n" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "When ticked, totals and subtotals appear below the sections of the report" -msgstr "عند التحديد، تظهر الإجماليات والمجاميع الفرعية أسفل أقسام التقرير" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "With Draft Entries" -msgstr "مع قيود مسودة" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "With residual" -msgstr "مع متبقي" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "Write-Off" -msgstr "شطب" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "Write-Off Entry" -msgstr "قيد الشطب" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Wrong format for if_other_expr_above/if_other_expr_below formula: %s" -msgstr "تنسيق خاطئ لصيغة if_other_expr_above/if_other_expr_below: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#, python-format -msgid "Wrong ID for general ledger line to expand: %s" -msgstr "معرف خاطئ لسطر دفتر الأستاذ العام للتوسيع: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#, python-format -msgid "Wrong ID for partner ledger line to expand: %s" -msgstr "معرف خاطئ لسطر دفتر أستاذ الشركاء للتوسيع: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "XLSX" -msgstr "XLSX" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Year" -msgstr "سنة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Yes" -msgstr "نعم" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "You already have imported that file." -msgstr "لقد استوردت هذا الملف بالفعل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years." -msgstr "لا يمكن أن يكون هناك تداخل بين سنتين ماليتين، يرجى تصحيح تواريخ البدء و/أو الانتهاء لسنواتك المالية." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "You can only reconcile entries with up to two different accounts: %s" -msgstr "يمكنك فقط مطابقة القيود بحد أقصى حسابين مختلفين: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "You can't hit the reset button on a secured bank transaction." -msgstr "لا يمكنك إعادة تعيين معاملة بنكية مؤمنة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You can't open a tax report from a move without a VAT closing date." -msgstr "لا يمكنك فتح تقرير ضريبي من حركة بدون تاريخ إقفال ضريبة القيمة المضافة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You can't post an entry related to a draft asset. Please post the asset before." -msgstr "لا يمكنك ترحيل قيد مرتبط بأصل مسودة. يرجى ترحيل الأصل أولاً." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You can't re-evaluate the asset before the lock date." -msgstr "لا يمكنك إعادة تقييم الأصل قبل تاريخ القفل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot add or remove bills when the asset is already running or closed." -msgstr "لا يمكنك إضافة أو إزالة فواتير عندما يكون الأصل نشطاً أو مغلقاً بالفعل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move_line.py:0 -msgid "You cannot add taxes on a tax closing move line." -msgstr "لا يمكنك إضافة ضرائب على سطر حركة إقفال الضرائب." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot archive a record that is not closed" -msgstr "لا يمكنك أرشفة سجل غير مغلق" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot automate the journal entry for an asset that has a running gross increase. Please use 'Dispose' on the increase(s)." -msgstr "لا يمكنك أتمتة قيد اليومية لأصل له زيادة إجمالية نشطة. يرجى استخدام 'تصرف' على الزيادة/الزيادات." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "You cannot change the account for a deferred line in %(move_name)s if it has already been deferred." -msgstr "لا يمكنك تغيير الحساب لسطر مؤجل في %(move_name)s إذا تم تأجيله بالفعل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot create a deferred entry with a start date but no end date." -msgstr "لا يمكنك إنشاء قيد مؤجل بتاريخ بدء بدون تاريخ انتهاء." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot create a deferred entry with a start date later than the end date." -msgstr "لا يمكنك إنشاء قيد مؤجل بتاريخ بدء لاحق لتاريخ الانتهاء." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot create an asset from lines containing credit and debit on the account or with a null amount" -msgstr "لا يمكنك إنشاء أصل من أسطر تحتوي على دائن ومدين في الحساب أو بمبلغ صفري" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "You cannot delete a document that is in %s state." -msgstr "لا يمكنك حذف مستند في حالة %s." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot delete an asset linked to posted entries." -msgstr "لا يمكنك حذف أصل مرتبط بقيود مرحّلة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "" -"You cannot delete an asset linked to posted entries.\n" -"You should either confirm the asset, then, sell or dispose of it, or cancel the linked journal entries.\n" -msgstr "لا يمكنك حذف أصل مرتبط بقيود مرحّلة.\nيجب إما تأكيد الأصل، ثم بيعه أو التخلص منه، أو إلغاء قيود اليومية المرتبطة.\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot dispose of an asset before the lock date." -msgstr "لا يمكنك التخلص من أصل قبل تاريخ القفل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot generate deferred entries for a miscellaneous journal entry." -msgstr "لا يمكنك إنشاء قيود مؤجلة لقيد يومية متفرقات." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "You cannot generate entries for a period that does not end at the end of the month." -msgstr "لا يمكنك إنشاء قيود لفترة لا تنتهي في نهاية الشهر." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "You cannot generate entries for a period that is locked." -msgstr "لا يمكنك إنشاء قيود لفترة مقفلة." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "You cannot have a fiscal year on a child company." -msgstr "لا يمكن أن يكون للشركة الفرعية سنة مالية." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as another closing entry has been posted at a later date." -msgstr "لا يمكنك إعادة قيد الإقفال هذا إلى مسودة، حيث تم ترحيل قيد إقفال آخر في تاريخ لاحق." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a" -msgstr "لا يمكنك إعادة قيد الإقفال هذا إلى مسودة، حيث سيحذف قيم الترحيل التي تؤثر على تقرير الضرائب لـ" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a locked period. To do this, you first need to modify you tax return lock date." -msgstr "لا يمكنك إعادة قيد الإقفال هذا إلى مسودة، حيث سيحذف قيم الترحيل التي تؤثر على تقرير الضرائب لفترة مقفلة. للقيام بذلك، تحتاج أولاً إلى تعديل تاريخ قفل الإقرار الضريبي." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset to draft an entry related to a posted asset" -msgstr "لا يمكنك إعادة قيد مرتبط بأصل مرحّل إلى مسودة" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset to draft an invoice that is grouped in deferral entry. You can create a credit note instead." -msgstr "لا يمكنك إعادة فاتورة مجمعة في قيد تأجيل إلى مسودة. يمكنك إنشاء إشعار دائن بدلاً من ذلك." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot resume at a date equal to or before the pause date" -msgstr "لا يمكنك الاستئناف في تاريخ يساوي أو قبل تاريخ الإيقاف" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot select the same account as the Depreciation Account" -msgstr "لا يمكنك تحديد نفس الحساب كحساب الإهلاك" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You cannot set a Lock Date in the future." -msgstr "لا يمكنك تعيين تاريخ قفل في المستقبل." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "You have to set a Default Account for the journal: %s" -msgstr "يجب تعيين حساب افتراضي لدفتر اليومية: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to %(btn_start)sfully reconcile%(btn_end)s the document." -msgstr "قد ترغب في %(btn_start)sمطابقة%(btn_end)s المستند بالكامل." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to make a %(btn_start)spartial reconciliation%(btn_end)s instead." -msgstr "قد ترغب في إجراء %(btn_start)sمطابقة جزئية%(btn_end)s بدلاً من ذلك." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to record a %(btn_start)spartial payment%(btn_end)s." -msgstr "قد ترغب في تسجيل %(btn_start)sدفعة جزئية%(btn_end)s." - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to set the invoice as %(btn_start)sfully paid%(btn_end)s." -msgstr "قد ترغب في تعيين الفاتورة كـ %(btn_start)sمدفوعة بالكامل%(btn_end)s." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "You need to activate more than one currency to access this report." -msgstr "تحتاج إلى تفعيل أكثر من عملة واحدة للوصول إلى هذا التقرير." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You need to select a duration for the exception." -msgstr "تحتاج إلى تحديد مدة للاستثناء." - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You need to select who the exception applies to." -msgstr "تحتاج إلى تحديد من ينطبق عليه الاستثناء." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "You uploaded an invalid or empty file." -msgstr "لقد رفعت ملفاً غير صالح أو فارغاً." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "You're about the generate the closing entries of multiple companies at once. Each of them will be created in accordance with its company tax periodicity." -msgstr "أنت على وشك إنشاء قيود الإقفال لعدة شركات دفعة واحدة. سيتم إنشاء كل منها وفقاً لدورية الضرائب الخاصة بشركتها." diff --git a/addons/at_accounting/i18n/at_accounting.pot b/addons/at_accounting/i18n/at_accounting.pot deleted file mode 100644 index 3e189dd..0000000 --- a/addons/at_accounting/i18n/at_accounting.pot +++ /dev/null @@ -1,5329 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * at_accounting -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 18.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-30 12:13+0000\n" -"PO-Revision-Date: 2026-03-30 12:13+0000\n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: \n" - - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__total_depreciation_entries_count -msgid "# Depreciation Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__gross_increase_count -msgid "# Gross Increases" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__depreciation_entries_count -msgid "# Posted Depreciation Entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(asset)s: Disposal" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(asset)s: Sale" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "%(display_name_html)s with an open amount of %(open_amount)s will be fully reconciled by the transaction." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "%(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "%(journal)s - %(account)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(months)s m" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "%(move_line)s (%(current)s of %(total)s)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%(names)s and %(remaining)s others" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%(names)s and one other" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "%(report_label)s: %(period)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(years)s y" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "%d transactions had already been imported and were ignored." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/budget.py:0 -#, python-format -msgid "%s (copy)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "%s Future entries will be recomputed to depreciate the asset following the changes." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%s is not a numeric value" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "%s: Depreciation" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'external' engine does not support groupby, limit nor offset." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'Open General Ledger' caret option is only available form report lines targetting accounts." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'View Bank Statement' caret option is only available for report lines targeting bank statements." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "(%s lines)" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding_receipts -msgid "(+) Outstanding Receipts" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding_payments -msgid "(-) Outstanding Payments" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "(1 line)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "(No %s)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "(No Group)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.assets_report_assets_plus -#: model:account.report.column,name:at_accounting.assets_report_depre_plus -msgid "+" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.assets_report_assets_minus -#: model:account.report.column,name:at_accounting.assets_report_depre_minus -msgid "-" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "-> Reconcile" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "-> Refresh" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "1 transaction had already been imported and was ignored." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
%(extra_text)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
%(extra_text)s Future entries will be recomputed to depreciate the asset following the changes." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
A disposal entry will be posted on the %(account_type)s account %(account)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
A second entry will neutralize the original income and post the outcome of this sale on account %(account)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A document linked to %(move_line_name)s has been deleted: %(link)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A document linked to this move has been deleted: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A gross increase has been created: %(link)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/widgets/account_report_x2many/account_report_x2many.js:0 -msgid "A line with a 'Group By' value cannot have children." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A non deductible tax value of %(tax_value)s was added to %(name)s's initial value of %(purchase_value)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A non deductible tax value of %(tax_value)s was added to %(name)s\\" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "A tax unit can only be created between companies sharing the same main currency." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "A tax unit must contain a minimum of two companies. You might want to delete the unit." -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.auto.reconcile.wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.change.lock.date" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.multicurrency.revaluation.wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.reconcile.wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.report.send" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.secure.entries.wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account_reports.export.wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account_reports.export.wizard.format" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.asset.modify" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.bank.rec.widget" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.bank.rec.widget.line" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access_account_tax_unit_manager" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access_account_tax_unit_readonly" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_account_name -#: model:account.report.column,name:at_accounting.aged_receivable_report_account_name -#: model:account.report.column,name:at_accounting.partner_ledger_report_account_code -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__account_id -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__account_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Account" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_auto_reconcile_wizard -msgid "Account automatic reconciliation wizard" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Account Code" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Account Code / Tag" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_group_tree -msgid "Account Groups" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Account Label" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reconcile_wizard -msgid "Account reconciliation wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_tax_report_handler -msgid "Account Report Handler for Tax Reports" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_send -msgid "Account Report Send" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.account_tag_action -msgid "Account Tags" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__transfer_from_account_id -msgid "Account Transfer From" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_depreciation_id -msgid "Account used in the depreciation entries, to decrease the asset value." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_depreciation_expense_id -msgid "Account used in the periodical entries, to record a part of the asset as expense." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_asset_id -msgid "Account used to record the purchase of the asset at its original price." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__gain_account_id -msgid "Account used to write the journal item in case of gain" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__loss_account_id -msgid "Account used to write the journal item in case of loss" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.account_report_annotation" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.account_report_annotation_readonly" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.asset.group" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.fiscal.year.manager" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.fiscal.year.user" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.ac.user" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.item.ac.user" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.item.readonly" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.readonly" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.file.download.error.wizard" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.ac.user" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.readonly" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.rule.ac.user" -msgstr "" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.rule.readonly" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounting" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_budget -msgid "Accounting Report Budget" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_budget_item -msgid "Accounting Report Budget Item" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_tree -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_tree -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounting Reports" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__account_ids -msgid "Accounts" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Accounts coverage" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounts Coverage Report" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.multicurrency_revaluation_to_adjust -msgid "Accounts To Adjust" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_acquisition_date -msgid "Acquisition Date" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__modify_action -msgid "Action" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Add an internal note" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Add contacts to notify..." -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_adjustment -msgid "Adjustment" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "Adjustment Entry" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Advance payments made to suppliers" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Advance Payments received from customers" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Advanced" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "After importing three bills for a vendor without making changes, your ERP will suggest automatically validating future bills..." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "After the data extraction, check and validate the bill. If no vendor has been found, add one before validating." -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_partner_balance_report_handler -msgid "Aged Partner Balance Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.aged_payable_report -#: model:account.report.line,name:at_accounting.aged_payable_line -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_aged_payable -msgid "Aged Payable" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_payable_report_handler -msgid "Aged Payable Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.aged_receivable_report -#: model:account.report.line,name:at_accounting.aged_receivable_line -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_aged_receivable -msgid "Aged Receivable" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_receivable_report_handler -msgid "Aged Receivable Custom Handler" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/sales_report/filters/filters.js:0 -msgid "All" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "All Journals" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "All Payable" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "All Receivable" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_model_autocomplete_ids -msgid "All reconciliation models" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "All Report Variants" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be from the same account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be from the same company" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be posted" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__allow_partials -msgid "Allow partials" -msgstr "" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.group_fiscal_year -msgid "Allow to define fiscal years of more or less than a year" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__aml -msgid "aml" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_amount -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_amount -#: model:account.report.column,name:at_accounting.partner_ledger_amount -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__amount_currency -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__amount -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_amount_currency -#: model:account.report.column,name:at_accounting.aged_receivable_report_amount_currency -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_amount_currency -#: model:account.report.column,name:at_accounting.partner_ledger_report_amount_currency -msgid "Amount Currency" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount Due" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount Due (in currency)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__amount -msgid "Amount in company currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Amount in Currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "Amount in currency: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Lakhs" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Millions" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Thousands" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__parent_id -msgid "An asset has a parent when it is the result of gaining value" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "An asset has been created for this move:" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "An asset will be created for the value increase of the asset.
" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "An entry will transfer %(amount)s from %(from_account)s to %(to_account)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Analytic" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__hard_lock_date -msgid "Any entry up to and including that date will be postponed to a later time, in accordance with its journal sequence. This lock date is irreversible and does not allow any exception." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__fiscalyear_lock_date -msgid "Any entry up to and including that date will be postponed to a later time, in accordance with its journal's sequence." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__tax_lock_date -msgid "Any entry with taxes up to and including that date will be postponed to a later time, in accordance with its journal's sequence. The tax lock date is automatically set when the tax closing entry is posted." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__purchase_lock_date -msgid "Any purchase entry prior to and including this date will be postponed to a later date, in accordance with its journal's sequence." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__sale_lock_date -msgid "Any sales entry prior to and including this date will be postponed to a later date, in accordance with its journal's sequence." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "AP %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "AR %s" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Archived" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "As of %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Ascending" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__asset_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Asset Cancelled" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_asset_counterpart_id -msgid "Asset Counterpart Account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Asset created" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Asset created from invoice: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset disposed. %s" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset_group -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__asset_group_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Group" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Model" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Model name" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_asset_model_form -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Models" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_move_type -msgid "Asset Move Type" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__name -msgid "Asset Name" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset paused. %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset sold. %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Asset unpaused. %s" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Values" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset(s)" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset -msgid "Asset/Revenue Recognition" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_total_assets0 -msgid "ASSETS" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.actions.act_window,name:at_accounting.action_account_asset_form -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_finance_config_assets -msgid "Assets and Revenues" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets in closed state" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets in draft and open states" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset_report_handler -msgid "Assets Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period0 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period0 -msgid "At Date" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Atleast one asset (%s) couldn't be set as running because it lacks any required information" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Attach a file" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Audit" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.account_reports_audit_reports_menu -msgid "Audit Reports" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__auto_balance -msgid "auto_balance" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automate Asset" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automatic Accounting" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_auto_reconcile_wizard.py:0 -msgid "Automatically Reconciled Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automation" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_avgcre0 -msgid "Average creditors days" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_avdebt0 -msgid "Average debtors days" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "B: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.balance_sheet_balance -#: model:account.report.column,name:at_accounting.cash_flow_report_balance -#: model:account.report.column,name:at_accounting.executive_summary_column -#: model:account.report.column,name:at_accounting.general_ledger_report_balance -#: model:account.report.column,name:at_accounting.journal_report_balance -#: model:account.report.column,name:at_accounting.partner_ledger_report_balance -#: model:account.report.column,name:at_accounting.profit_and_loss_column -msgid "Balance" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_current -msgid "Balance at Current Rate" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_operation -msgid "Balance at Operation Rate" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_currency -msgid "Balance in Foreign Currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -#, python-format -msgid "Balance of '%s'" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.balance_bank -msgid "Balance of Bank" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.balance_sheet -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_balancesheet0 -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_balance_sheet -msgid "Balance Sheet" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_balance_sheet_report_handler -msgid "Balance Sheet Custom Handler" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax advance payment account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax current account (payable)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax current account (receivable)" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_bank_view0 -msgid "Bank and Cash Accounts" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_transactions -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_transactions_kanban -msgid "Bank Reconciliation" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.bank_reconciliation_report -msgid "Bank Reconciliation Report" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_bank_reconciliation_report_handler -msgid "Bank Reconciliation Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Bank Statement" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "Bank Statement %s.pdf" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "Bank Statement.pdf" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Base Amount" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Based on" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__daily_computation -msgid "Based on days per period" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Before" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Bills" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__book_value -msgid "Book Value" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_balance -msgid "book_value" -msgstr "" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_user -msgid "Bookkeeper" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__budget_id -msgid "Budget" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Budget Items" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Budget items can only be edited from account lines." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Budget Name" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Cancel" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Cancel Asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__cancelled -msgid "Cancelled" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Cannot audit tax from another model than account.tax." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Cannot find in which journal import this statement. Please manually select a journal." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Cannot generate carryover values for all fiscal positions at once!" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Carryover adjustment for tax unit" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Carryover can only be generated for a single column group." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Carryover from %(date_from)s to %(date_to)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Carryover lines for: %s" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash0 -msgid "Cash" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash and cash equivalents, beginning of period" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash and cash equivalents, closing balance" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_cash_flow_report_handler -msgid "Cash Flow Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.cash_flow_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_cash_flow -msgid "Cash Flow Statement" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from financing activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from investing & extraordinary activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from operating activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from unclassified activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash in" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash out" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash paid for operating activities" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_received0 -msgid "Cash received" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash received from operating activities" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_spent0 -msgid "Cash spent" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_surplus0 -msgid "Cash surplus" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_change_lock_date -msgid "Change Lock Date" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Characteristics" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_reconcile_wizard__to_check -msgid "Check if you are not certain of all the information of the counterpart." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Check Partner(s) Email(s)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method -msgid "" -"Choose the method to use to compute the amount of depreciation lines.\n" -" * Straight Line: Calculated on basis of: Gross Value / Duration\n" -" * Declining: Calculated on basis of: Residual Value * Declining Factor\n" -" * Declining then Straight Line: Like Declining but with a minimum depreciation value equal to the straight line value.\n" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_auto_reconcile_wizard__search_mode__zero_balance -msgid "Clear Account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "Click \"New\" or upload a %s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Click on a fetched bank transaction to start the reconciliation process." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Close" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__close -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Closed" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_closing_bank_balance0 -msgid "Closing bank balance" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Closing Entry" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.journal_report_code -msgid "Code" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Columns" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.general_ledger_report_communication -msgid "Communication" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__company_ids -msgid "Companies" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__company_id -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__company_id -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__company_id -msgid "Company" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -#, python-format -msgid "Company %(company)s already belongs to a tax unit in %(country)s. A company can at most be part of one tax unit per country." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Company Currency" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__company_currency_id -msgid "Company currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Company Only" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Company Settings" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__prorata_computation_type -msgid "Computation" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_asset_compute_depreciations -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Compute Depreciation" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Configure start dates" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_config_settings.py:0 -msgid "Configure your start dates" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Configure your tax accounts" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -#, python-format -msgid "Configure your TAX accounts - %s" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_asset_run -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Confirm" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Confirm the transaction." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Connect your bank and get your latest transactions." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__constant_periods -msgid "Constant Periods" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_body -msgid "Contents" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_direct_costs0 -msgid "Cost of Revenue" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Could not expand term %(term)s while evaluating formula %(unexpanded_formula)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "" -"Could not make sense of the given file.\n" -"Did you install the module to support this type of file?\n" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Could not make sense of the given file.\\nDid you install the module to support this type of file?" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Could not parse account_code formula from token '%s'" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Counterpart Values" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__country_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Country" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_country -msgid "Country Code" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Create a new transaction." -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_aml_to_asset -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Create Asset" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_create_composite_report_list -msgid "Create Composite Report" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Create Entry" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_create_report_menu -msgid "Create Menu Item" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_form_bank_rec_widget -msgid "Create Statement" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Create your first vendor bill.

Tip: If you don’t have one on hand, use our sample bill." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_credit -#: model:account.report.column,name:at_accounting.journal_report_credit -#: model:account.report.column,name:at_accounting.partner_ledger_report_credit -#: model:account.report.column,name:at_accounting.trial_balance_report_credit -msgid "Credit" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_depreciated_value -msgid "Cumulative Depreciation" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_currency -#: model:account.report.column,name:at_accounting.aged_receivable_report_currency -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_currency -#: model:account.report.column,name:at_accounting.general_ledger_report_amount_currency -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -#, python-format -msgid "Currency Rates (%s)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_currency_id -msgid "Currency to use for reconciliation" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.deferred_expense_current -#: model:account.report.column,name:at_accounting.deferred_revenue_current -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Current" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_assets0 -#: model:account.report.line,name:at_accounting.account_financial_report_current_assets_view0 -msgid "Current Assets" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_ca_to_l0 -msgid "Current assets to liabilities" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__current_hard_lock_date -msgid "Current Hard Lock" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities0 -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities1 -msgid "Current Liabilities" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Current Values" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings_line_1 -msgid "Current Year Retained Earnings" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_current_year_earnings0 -msgid "Current Year Unallocated Earnings" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -msgid "Custom engine _report_custom_engine_last_statement_balance_amount does not support groupby" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__invoice_ids -msgid "Customer Invoice" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "Customer/Vendor" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_date -#: model:account.report.column,name:at_accounting.general_ledger_report_date -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__date -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__date -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Date" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_end_date -msgid "Date at which the deferred expense/revenue ends" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_start_date -msgid "Date at which the deferred expense/revenue starts" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Date cannot be empty" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_date_from -#: model:account.report.column,name:at_accounting.assets_report_depre_date_from -msgid "date from" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_depreciation_beginning_date -msgid "Date of the beginning of the depreciation" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_assets_date_to -#: model:account.report.column,name:at_accounting.assets_report_depre_date_to -msgid "date to" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_debit -#: model:account.report.column,name:at_accounting.journal_report_debit -#: model:account.report.column,name:at_accounting.partner_ledger_report_debit -#: model:account.report.column,name:at_accounting.trial_balance_report_debit -msgid "Debit" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Dec. then Straight" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__degressive -msgid "Declining" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_progress_factor -msgid "Declining Factor" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__degressive_then_linear -msgid "Declining then Straight Line" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Deductible" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Deferral of %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_move_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_entry_type -msgid "Deferred Entry Type" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__deferred_entry_type__expense -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_deferred_expense -msgid "Deferred Expense" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred expense" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_expense_report_handler -msgid "Deferred Expense Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred expense entries:" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.deferred_expense_report -msgid "Deferred Expense Report" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_report_handler -msgid "Deferred Expense Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__deferred_entry_type__revenue -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_deferred_revenue -msgid "Deferred Revenue" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred revenue" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_revenue_report_handler -msgid "Deferred Revenue Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred revenue entries:" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.deferred_revenue_report -msgid "Deferred Revenue Report" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Define fiscal years of more or less than one year" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Definition" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Depending moves" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deposits" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__value_residual -msgid "Depreciable Amount" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__value_residual -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_remaining_value -msgid "Depreciable Value" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciated Amount" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__depreciation_value -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__depreciation -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_depreciation_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_depreciation_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Account" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Board" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Depreciation board modified %s" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Date" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Depreciation entry %(name)s posted (%(value)s)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Depreciation entry %(name)s reversed (%(value)s)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__depreciation_move_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Lines" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Method" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.assets_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_assets -msgid "Depreciation Schedule" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Descending" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Difference from rounding taxes" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discard" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discount Amount" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discount Date" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__disposal -msgid "Disposal" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Disposal Move" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Disposal Moves" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Dispose" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/digest.py:0 -msgid "Do not have access, skip this data for user's digest email" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Document" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__doc_name -msgid "Documents Name" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Domestic" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__checkbox_download -msgid "Download" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Download Anyway" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Download the Data Inalterability Check Report" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__draft -msgid "Draft" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "Draft Entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Due" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_date_maturity -msgid "Due Date" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_number -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__method_number -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Duration" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_duration_rate -msgid "Duration / Rate" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "e.g. Bank Fees" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "e.g. Laptop iBook" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__early_payment -msgid "early_payment" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_ec_sales_report_handler -msgid "EC Sales Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "EC tax on non EC countries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "EC tax on same country" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__edit_mode_amount_currency -msgid "Edit mode amount" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Editing a manual report line is not allowed in multivat setup when displaying data from all fiscal positions." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Editing a manual report line is not allowed when multiple companies are selected." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__checkbox_send_mail -msgid "Email" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_template_id -msgid "Email template" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Enable automatic accounting entries for stock movements" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Enable Sections" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -msgid "End Balance" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__date_to -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_end_date -msgid "End Date" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Month" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Quarter" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Year" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_fiscal_year__date_to -msgid "Ending Date, included in the fiscal year." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Entries with partners with no VAT" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "entry" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_equity0 -msgid "EQUITY" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Error message" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_applies_to -msgid "Exception applies" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_duration -msgid "Exception Duration" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_needed -msgid "Exception needed" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_reason -msgid "Exception Reason" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -#, python-format -msgid "Exchange Difference: %s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__exchange_diff -msgid "exchange_diff" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Exclude Bank lines" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.multicurrency_revaluation_excluded -msgid "Excluded Accounts" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.executive_summary -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_exec_summary -msgid "Executive Summary" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_depreciation_expense_id -#: model:ir.model.fields,field_description:at_accounting.field_account_multicurrency_revaluation_wizard__expense_provision_account_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_depreciation_expense_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Expense Account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Expense Provision for %s" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_expenses0 -msgid "Expenses" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Export" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reports_export_wizard_format -msgid "Export format for accounting's reports" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__export_format_ids -msgid "Export to" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reports_export_wizard -msgid "Export wizard for accounting's reports" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Expression" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Expression labelled '%(label)s' of line '%(line)s' is being overwritten when computing the current report." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s does not exist on account.move.line, and is not supported by this report's custom handler." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s does not exist on account.move.line." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s of account.move.line is not stored, and hence cannot be used in a groupby expression" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field 'Custom Handler Model' can only reference records inheriting from [%s]." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "File Download Errors" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Filters" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_budget_tree -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_budget_tree -msgid "Financial Budgets" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_first_depreciation -msgid "First Depreciation" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__fpos_synced -msgid "Fiscal Positions Synchronised" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_fiscal_year -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Year" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Year 2018" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.actions_account_fiscal_year -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Years" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_asset_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fixed Asset Account" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__1h -msgid "for 1 hour" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__15min -msgid "for 15 minutes" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__24h -msgid "for 24 hours" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__5min -msgid "for 5 minutes" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_applies_to__everyone -msgid "for everyone" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_applies_to__me -msgid "for me" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Foreign currencies adjustment entry as of %s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__forever -msgid "forever" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__from_date -msgid "From" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "From %(date_from)s\\nto %(date_to)s" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "From Trade Payable accounts" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "From Trade Receivable accounts" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__fun_param -msgid "Function Parameter" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__fun_to_call -msgid "Function to Call" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Future Activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "G %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "gain" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "gain/loss" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "General Account Properties" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -#: model:account.report,name:at_accounting.general_ledger_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_general_ledger -msgid "General Ledger" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_general_ledger_report_handler -msgid "General Ledger Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Generate Entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Generate entry" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/report_export_wizard.py:0 -msgid "Generated Documents" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.generic_ec_sales_report -msgid "Generic EC Sales List" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler -msgid "Generic Tax Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler_account_tax -msgid "Generic Tax Report Custom Handler (Account -> Tax)" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler_tax_account -msgid "Generic Tax Report Custom Handler (Tax -> Account)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Global Tax Summary" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Go to Apps" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Goods" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Grid" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Gross Increase" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_asset_id -msgid "Gross Increase Account" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__gross_increase_value -msgid "Gross Increase Value" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_gross_profit0 -msgid "Gross Profit" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_gross_profit0 -msgid "Gross profit" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_gpmargin0 -msgid "Gross profit margin (gross profit / operating income)" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group By" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group By..." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group Name" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "Grouped Deferral Entry of %s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__hard_lock_date -msgid "Hard Lock" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Horizontal Group" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_horizontal_groups -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_horizontal_groups -msgid "Horizontal Groups" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "How often tax returns have to be made" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Impact On Grid" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/bank_statement_csv_import_action.js:0 -msgid "Import Bank Statement" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Import File" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_bank_statement__unique_import_id -msgid "Import ID" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/account_bank_statement_import_model.js:0 -msgid "Import Template for Bank Statements" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__already_depreciated_amount_import -msgid "In case of an import from another software, you might need to use this field to have the right depreciation table report. This is the value that was already depreciated with entries not computed from this model" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "In Currency" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Inactive" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Including Analytic Simulations" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.no_statement_unreconciled_payments -#: model:account.report.line,name:at_accounting.unreconciled_last_statement_payments -msgid "Including Unreconciled Payments" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.no_statement_unreconciled_receipt -#: model:account.report.line,name:at_accounting.unreconciled_last_statement_receipts -msgid "Including Unreconciled Receipts" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_multicurrency_revaluation_wizard__income_provision_account_id -msgid "Income Account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Income Provision for %s" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Incoming" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Inconsistent data: more than one external value at the same date for a 'most_recent' external line." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Inconsistent report_id in options dictionary. Options says %(options_report)s; report is %(report)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -msgid "Inconsistent Statements" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_config_settings.py:0 -#, python-format -msgid "Incorrect fiscal year date: day is out of range for month. Month: %(month)s; Day: %(day)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -msgid "Initial Balance" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/aged_partner_balance/filters.js:0 -msgid "Intervals cannot be smaller than 1" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid domain formula in expression \"%(expression)s\" of line \"%(line)s\": %(formula)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid method “%s”" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invalid statements" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid subformula in expression \"%(expression)s\" of line \"%(line)s\": %(subformula)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid token '%(token)s' in account_codes formula '%(formula)s'" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_invoice_date -#: model:account.report.column,name:at_accounting.aged_receivable_report_invoice_date -#: model:account.report.column,name:at_accounting.partner_ledger_report_invoicing_date -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invoice Date" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invoice lines" -msgstr "" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_basic -msgid "Invoicing & Banks" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__is_write_off_required -msgid "Is a write-off move required to reconcile" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__is_transfer_required -msgid "Is an account transfer required" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__transfer_warning_message -msgid "Is an account transfer required to reconcile" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__lock_date_violated_warning_message -msgid "Is the date violating the lock date of moves" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "It is not possible to decrease or remove the Hard Lock Date." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__salvage_value -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__salvage_value_pct -msgid "It is the amount you plan to have that you cannot depreciate." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "It seems there is some depending closing move to be posted" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "It's not possible to select a budget with the horizontal group feature." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "It's not possible to select a horizontal group with the budget feature." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__item_ids -msgid "Items" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_journal_code -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__journal_id -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__journal_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_ja -msgid "Journal Audit" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Journal Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Entry" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Item" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__original_move_line_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Items" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Journal Items for Tax Audit" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Journal Items of %(account)s should have a label in order to generate an asset" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_move_line_posted_unreconciled -msgid "Journal Items to reconcile" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal items where matching number isn't set" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal items where the account allows reconciliation no matter the residual amount" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Journal items with archived tax tags" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.journal_report -msgid "Journal Report" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_journal_report_handler -msgid "Journal Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Journals" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_label -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__label -msgid "Label" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_lang -msgid "Lang" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Last Day" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.last_statement_balance -msgid "Last statement balance" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Last Statement balance + Transactions since statement" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Late Activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Later" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Latest Statement" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Legal signatory" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_cost_sales0 -msgid "Less Costs of Revenue" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_expense0 -msgid "Less Operating Expenses" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_depreciation0 -msgid "Less Other Expenses" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Let’s go back to the dashboard." -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_liabilities_view0 -msgid "LIABILITIES" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_liabilities_and_equity_view0 -msgid "LIABILITIES + EQUITY" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_bank_rec_widget_line -msgid "Line of the bank reconciliation widget" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Linear" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Lines" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__linked_assets_ids -msgid "Linked Assets" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__liquidity -msgid "liquidity" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Load more..." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_change_lock_date -msgid "Lock Dates" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date -msgid "Lock Everything" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date_for_everyone -msgid "Lock Everything For Everyone" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date_for_me -msgid "Lock Everything For Me" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_view_account_change_lock_date -msgid "Lock Journal Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date -msgid "Lock Purchases" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date_for_everyone -msgid "Lock Purchases For Everyone" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date_for_me -msgid "Lock Purchases For Me" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date -msgid "Lock Sales" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date_for_everyone -msgid "Lock Sales For Everyone" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date_for_me -msgid "Lock Sales For Me" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date -msgid "Lock Tax Return" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date_for_everyone -msgid "Lock Tax Return For Everyone" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date_for_me -msgid "Lock Tax Return For Me" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "loss" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__main_company_id -msgid "Main Company" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__main_company_id -msgid "Main company of this unit; the one actually reporting and paying the taxes." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Make Adjustment Entry" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_bank_statement_import_csv.py:0 -msgid "Make sure that an Amount or Debit and Credit is in the file." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Manage Items" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_file_download_error_wizard -msgid "Manage the file generation errors from report exports." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__manual -msgid "manual" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "Manual (or import %(import_formats)s)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Manual value" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Manual values" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/list_view_switcher.js:0 -msgid "Match" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Matched" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_payment.py:0 -msgid "Matched Transactions" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_matching_number -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Matching" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__company_ids -msgid "Members of this unit" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Memo" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_first_method -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method -msgid "Method" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Method '%(method_name)s' must start with the '%(prefix)s' prefix." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "Misc" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.misc_operations -msgid "Misc. operations" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_csv.py:0 -msgid "Mixing CSV files with other file types is not allowed." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__model_id -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__model -msgid "Model" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model,name:at_accounting.model_asset_modify -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify Asset" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify Depreciation" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Month" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Months" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__move_line_ids -msgid "Move lines to reconcile" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Multi-ledger" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_multicurrency_revaluation_report_handler -msgid "Multicurrency Revaluation Report Custom Handler" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_multicurrency_revaluation_wizard -msgid "Multicurrency Revaluation Wizard" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "" -"Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \n" -" %(closing_entries)s\n" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \\n %(closing_entries)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "" -"Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \n" -" %(closing_entries)s\n" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \\n %(closing_entries)s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_report_send__mode__multi -msgid "Multiple Recipients" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "n/a" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.line,name:at_accounting.journal_report_line -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__name -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__name -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__name -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__name -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Name" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_reports_export_wizard_format__doc_name -msgid "Name to give to the generated documents." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Navigate easily through reports and see what is behind the numbers" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__negative_revaluation -msgid "Negative revaluation" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_net_assets0 -msgid "Net assets" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__net_gain_on_sale -msgid "Net gain on sale" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Net increase in cash and cash equivalents" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_profit0 -#: model:account.report.line,name:at_accounting.account_financial_report_net_profit0 -msgid "Net Profit" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_npmargin0 -msgid "Net profit margin (net profit / income)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__net_gain_on_sale -msgid "Net value of gain or loss on sale of an asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__value_residual -msgid "New residual amount for the asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__salvage_value -msgid "New salvage amount for the asset" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_form_bank_rec_widget -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "New Transaction" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__new_aml -msgid "new_aml" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -msgid "No adjustment needed" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "No attachment was provided" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "No currency found matching '%s'." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/chart_template.py:0 -msgid "No default miscellaneous journal could be found for the active company" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "No entry to generate." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No Journal" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__none -msgid "No Prorata" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -msgid "No provision needed was found." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "No statement" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "No transactions matching your filters were found." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No VAT number associated with your company. Please define one." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__non_deductible_tax_value -msgid "Non Deductible Tax Value" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Non Trade Partners" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Non Trade Payable" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Non Trade Receivable" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Non-Deductible" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/sales_report/filters/filters.js:0 -msgid "None" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__salvage_value -msgid "Not Depreciable Amount" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__salvage_value -msgid "Not Depreciable Value" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__salvage_value_pct -msgid "Not Depreciable Value Percent" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Not locked" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Not Matched" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Not Started" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__name -msgid "Note" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Notes" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "Nothing to do here!" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Now, we'll create your first invoice (accountant)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__gross_increase_count -msgid "Number of assets made to increase the value of the asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_number_days -msgid "Number of days" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__total_depreciation_entries_count -msgid "Number of depreciation entries (posted or not)" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Number of Depreciations" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_period -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__method_period -msgid "Number of Months in a Period" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Number of periods cannot be smaller than 1" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/aged_partner_balance/filters.js:0 -msgid "Odoo Warning" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_off_sheet -msgid "OFF BALANCE SHEET ACCOUNTS" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period5 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period5 -msgid "Older" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__paused -msgid "On Hold" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/report_export_wizard.py:0 -msgid "One of the formats chosen can not be exported in the DMS" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "Only Billing Administrators are allowed to change lock dates!" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_csv.py:0 -msgid "Only one CSV file can be selected." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Open Asset" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -#, python-format -msgid "Open balance of %(amount)s" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_reports_customer_statements -msgid "Open Customer Statements" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_operating_income0 -msgid "Operating Income (or Loss)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Options" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Original Deferred Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_original_move_ids -msgid "Original Invoices" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__original_value -msgid "Original Value" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Originator Tax" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Outgoing" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding -msgid "Outstanding Receipts/Payments" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Parent Asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__report_id -msgid "Parent Report Id" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__export_wizard_id -msgid "Parent Wizard" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_partner_name -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__to_partner_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Partner" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Partner Categories" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.partner_ledger_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_partner_ledger -msgid "Partner Ledger" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_partner_ledger_report_handler -msgid "Partner Ledger Custom Handler" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Partner(s) should have an email address." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__partner_ids -msgid "Partners" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Pause" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Pay tax: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_aged_partner_balance.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payable" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Payable tax amount" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities_payable -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_creditors0 -msgid "Payables" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payment Matching" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__payment_state_before_switch -msgid "Payment State Before Switch" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payments" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "PDF" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_auto_reconcile_wizard__search_mode__one_to_one -msgid "Perfect Match" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_performance0 -msgid "Performance" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Period" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period1 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period1 -msgid "Period 1" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period2 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period2 -msgid "Period 2" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period3 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period3 -msgid "Period 3" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period4 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period4 -msgid "Period 4" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Period length" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Periodicity" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Periods" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/budget.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Please enter a valid budget name." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Please select a mail template to send multiple statements." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Please select the main company and its branches in the company selector to proceed." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Please set the deferred accounts in the accounting settings." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Please set the deferred journal in the accounting settings." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Please specify the accounts necessary for the Tax Closing Entry." -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_fixed_assets_view0 -msgid "Plus Fixed Assets" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_non_current_assets_view0 -msgid "Plus Non-current Assets" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_non_current_liabilities0 -msgid "Plus Non-current Liabilities" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_other_income0 -msgid "Plus Other Income" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_position0 -msgid "Position" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__positive_revaluation -msgid "Positive revaluation" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Posted Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Predict vendor bill product" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_prepayements0 -msgid "Prepayments" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings_line_2 -msgid "Previous Years Retained Earnings" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_previous_year_earnings0 -msgid "Previous Years Unallocated Earnings" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Print & Send" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Proceed" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Product" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.profit_and_loss -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_profit_and_loss -msgid "Profit and Loss" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_profitability0 -msgid "Profitability" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Properties" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__prorata_date -msgid "Prorata Date" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Provision for %(for_cur)s (1 %(comp_cur)s = %(rate)s %(for_cur)s)" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__purchase -msgid "Purchase" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Quarter" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "Re-evaluate" -msgstr "" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_readonly -msgid "Read-only" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reason..." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_aged_partner_balance.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Receivable" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Receivable tax amount" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_debtors0 -#: model:account.report.line,name:at_accounting.account_financial_report_receivable0 -msgid "Receivables" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_partner_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Recipients" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__search_mode -#: model:ir.ui.menu,name:at_accounting.menu_account_reconcile -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile & open" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_account_id -msgid "Reconcile Account" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_open_auto_reconcile_wizard -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile automatically" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_auto_reconcile_wizard__search_mode -msgid "Reconcile journal items with opposite balance or clear accounts with a zero balance" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_model_id -msgid "Reconciliation model" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Record cost of goods sold in your journal entries" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__linked_asset_ids -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Assets" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Purchase(s)" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Sale(s)" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reminder" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__account_report_id -msgid "Report" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Report Line" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Report lines mentioning the account code" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Report Name" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reporting" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Reset to running" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Residual" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Residual in Currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Resume" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Resume Depreciation" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings0 -msgid "Retained Earnings" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_return_investment0 -msgid "Return on investments (net profit / assets)" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_income0 -#: model:account.report.line,name:at_accounting.account_financial_report_revenue0 -msgid "Revenue" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "Reversal of Grouped Deferral Entry of %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Reversal of: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "Reverse the depreciation entries posted in the future in order to modify the depreciation" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Root Report" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_bank_statement_import_csv.py:0 -msgid "Rows must be sorted by date." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Run manually" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__open -msgid "Running" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__sale -msgid "Sale" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save & Close" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save & New" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save as Model" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Save model" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Search Journal Items to Reconcile" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Sections" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Sell" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -msgid "Send" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -msgid "Send Partner Ledgers" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Send tax report: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Sending statements" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__sequence -msgid "Sequence" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Services" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Set an amount." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set as Checked" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Set the payment reference." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set to Draft" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set to Running" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.account.menu_account_config -msgid "Settings" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_st_cash_forecast0 -msgid "Short term cash forecast" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Show all records which has next action date is before today" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__show_draft_entries_warning -msgid "Show Draft Entries Warning" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__signing_user -msgid "Signer" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_report_send__mode__single -msgid "Single Recipient" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Some fields are missing %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Some required values are missing" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__date_from -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_start_date -msgid "Start Date" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_fiscal_year__date_from -msgid "Start Date, included in the fiscal year." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Starting Balance" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__prorata_date -msgid "Starting date of the period used in the prorata calculation of the first depreciation" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_bank_statement_attachment -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Statement" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Statement Line" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Statements are being sent in the background." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__state -msgid "Status" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Stock Valuation" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__linear -msgid "Straight Line" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_subject -msgid "Subject" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Subject..." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Suggestions" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__book_value -msgid "Sum of the depreciable value, the salvage value and the book value of all value increase items" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "T: %s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__tax_id -msgid "Tax" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Tax Amount" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -msgid "Tax Declaration" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Grids" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__vat -msgid "Tax ID" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Tax Paid Adjustment" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Tax Received Adjustment" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Report" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_gt -msgid "Tax Return" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -msgid "Tax return" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Return Periodicity" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_tax_unit -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Unit" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -#, python-format -msgid "tax unit [%s]" -msgstr "" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_view_tax_units -msgid "Tax Units" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__tax_line -msgid "tax_line" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Taxes" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Taxes Applied" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__fpos_synced -msgid "Technical field indicating whether Fiscal Positions exist for all companies in the unit" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_ir_actions_account_report_download -msgid "Technical model for accounting report downloads" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/ellipsis/ellipsis.js:0 -msgid "Text copied" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The account %(exp_acc)s has been credited by %(exp_delta)s," -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The account %(exp_acc)s has been credited by %(exp_delta)s, while the account %(dep_acc)s has been debited by %(dep_delta)s. This corresponds to %(move_count)s cancelled %(word)s:" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "The account of this statement (%(account)s) is not the same as the journal (%(journal)s)." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "The Accounts Coverage Report is not available for this report." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single credit line should be strictly negative." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single debit line should be strictly positive." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single line cannot be 0." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method_period -#: model:ir.model.fields,help:at_accounting.field_asset_modify__method_period -msgid "The amount of time between two depreciations" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s)." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s). Please make sure this is what you want." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/line_name/popover_line/annotation_popover_line.js:0 -msgid "The annotation shouldn't have an empty value." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__asset_id -msgid "The asset to be modified by this wizard" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "The attachments of the tax report can be found on the closing entry of the representative company." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__children_ids -msgid "The children are the gains in value of this asset" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#, python-format -msgid "The column '%s' is not available for this report." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "The country detected for this VAT number does not match the one set on this Tax Unit." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__country_id -msgid "The country in which this tax unit is used to group your companies' tax reports declaration." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "The currency of the bank statement (%(code)s) is not the same as the currency of the journal (%(journal)s)." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "The currency rate cannot be equal to zero" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "The currently selected dates don't match a tax period. The closing entry will be created for the closest-matching period according to your periodicity setup." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "The date you set violates the lock date of one of your entry. It will be overriden by the following date : %(replacement_date)s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_move_ids -msgid "The deferred entries created by this invoice" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__invoice_ids -msgid "The disposal invoice is needed in order to generate the closing journal entry." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The email address is unknown on the partner" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "The ending date must not be prior to the starting date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "The following files could not be imported:" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "The following files could not be imported:\\n" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__vat -msgid "The identifier to be used when submitting a report for this unit." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "The invoice %(display_name_html)s with an open amount of %(open_amount)s will be entirely paid by the transaction." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "The invoice %(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s." -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The invoices up to this date will not be taken into account as accounting entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "The main company of a tax unit has to be part of it." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method_number -msgid "The number of depreciations needed to depreciate your asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_original_move_ids -msgid "The original invoices that created the deferred entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "The remaining value on the last depreciation line must be 0" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The system will try to predict the product on vendor bill lines based on the label of the line" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "The used operator is not supported for this expression." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "There are currently reports waiting to be sent, please try again later." -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__invoice_line_ids -msgid "There are multiple lines that could be the related to this asset" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "There are unposted depreciations prior to the selected operation date, please deal with them first." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account exists in the Chart of Accounts but is not mentioned in any line of the report" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported in a line of the report but does not exist in the Chart of Accounts" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported in multiple lines of the report" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported multiple times on the same line of the report" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "This allows you to choose the position of totals in your financial reports." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "This bank transaction has been automatically validated using the reconciliation model '%s'." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "This bank transaction is locked up tighter than a squirrel in a nut factory! You can't hit the reset button on it. So, do you want to \\" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "This can only be used on journal items" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "" -"This file doesn't contain any statement for account %s.\n" -"If it contains transactions for more than one account, it must be imported on each of them.\n" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "" -"This file doesn't contain any transaction for account %s.\n" -"If it contains transactions for more than one account, it must be imported on each of them.\n" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "This file doesn\\" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/widgets/account_report_x2many/account_report_x2many.js:0 -msgid "This line and all its children will be deleted. Are you sure you want to proceed?" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "This option hides lines with a value of 0" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_reconcile_model_line.py:0 -msgid "This reconciliation model can't be used in the manual reconciliation widget because its" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_reconcile_model_line.py:0 -msgid "This reconciliation model can't be used in the manual reconciliation widget because its configuration is not adapted" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This report already has a menuitem." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "This subformula references an unknown expression: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This tag is reported in a line of the report but is not linked to any account of the Chart of Accounts" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__to_date -msgid "To" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__to_check -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "To Check" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "To enhance authenticity, add a signature to your invoices" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Today Activities" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_total -#: model:account.report.column,name:at_accounting.aged_receivable_report_total -msgid "Total" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Total %s" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Balance" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Credit" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Debit" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Residual" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Residual in Currency" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Trade Partners" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Transaction" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Transactions" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.transaction_without_statement -msgid "Transactions without statement" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "Transfer from %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "Transfer to %s" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.trial_balance_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_coa -msgid "Trial Balance" -msgstr "" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_trial_balance_report_handler -msgid "Trial Balance Custom Handler" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Triangular" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to dispatch an action on a report not compatible with the provided options." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Trying to expand a group for a line which was not generated by a report line: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to expand a line without an expansion function." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to expand groupby results on lines without a groupby value." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Turn as an asset" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_type -msgid "Type of the account" -msgstr "" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_unaffected_earnings0 -msgid "Unallocated Earnings" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Unknown" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Unknown bound criterium: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Unknown date scope: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Unknown Partner" -msgstr "" - -#. module: at_accounting -#: model:account.report,name:at_accounting.multicurrency_revaluation_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_multicurrency_revaluation -msgid "Unrealized Currency Gains/Losses" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Unreconciled" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Unreconciled Entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -msgid "Unreconciled statements lines" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Validate" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Value at Import" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Value decrease for: %(asset)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Value increase for: %(asset)s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Vat closing from %(date_from)s to %(date_to)s" -msgstr "" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_vat -msgid "VAT Number" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "VAT Periodicity" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/list_view_switcher.js:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "View" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Bank Statement" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Journal Entry" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "View Partner" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "View Partner(s)" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Payment" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "View Reconciled Entries" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "View successfully imported statements" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Warning for the Original Value of %s" -msgstr "" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__state -msgid "" -"When an asset is created, the status is 'Draft'.\n" -"If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" -"The 'On Hold' status can be set manually when you want to pause the depreciation of an asset for some time.\n" -"You can manually close an asset when the depreciation is over.\n" -"By cancelling an asset, all depreciation entries will be reversed\n" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "When ticked, totals and subtotals appear below the sections of the report" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "With Draft Entries" -msgstr "" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "With residual" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "Write-Off" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "Write-Off Entry" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Wrong format for if_other_expr_above/if_other_expr_below formula: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#, python-format -msgid "Wrong ID for general ledger line to expand: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#, python-format -msgid "Wrong ID for partner ledger line to expand: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "XLSX" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Year" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Yes" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "You already have imported that file." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "You can only reconcile entries with up to two different accounts: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "You can't hit the reset button on a secured bank transaction." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You can't open a tax report from a move without a VAT closing date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You can't post an entry related to a draft asset. Please post the asset before." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You can't re-evaluate the asset before the lock date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot add or remove bills when the asset is already running or closed." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move_line.py:0 -msgid "You cannot add taxes on a tax closing move line." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot archive a record that is not closed" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot automate the journal entry for an asset that has a running gross increase. Please use 'Dispose' on the increase(s)." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "You cannot change the account for a deferred line in %(move_name)s if it has already been deferred." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot create a deferred entry with a start date but no end date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot create a deferred entry with a start date later than the end date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot create an asset from lines containing credit and debit on the account or with a null amount" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "You cannot delete a document that is in %s state." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot delete an asset linked to posted entries." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "" -"You cannot delete an asset linked to posted entries.\n" -"You should either confirm the asset, then, sell or dispose of it, or cancel the linked journal entries.\n" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot dispose of an asset before the lock date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot generate deferred entries for a miscellaneous journal entry." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "You cannot generate entries for a period that does not end at the end of the month." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "You cannot generate entries for a period that is locked." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "You cannot have a fiscal year on a child company." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as another closing entry has been posted at a later date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a locked period. To do this, you first need to modify you tax return lock date." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset to draft an entry related to a posted asset" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset to draft an invoice that is grouped in deferral entry. You can create a credit note instead." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot resume at a date equal to or before the pause date" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot select the same account as the Depreciation Account" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You cannot set a Lock Date in the future." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "You have to set a Default Account for the journal: %s" -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to %(btn_start)sfully reconcile%(btn_end)s the document." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to make a %(btn_start)spartial reconciliation%(btn_end)s instead." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to record a %(btn_start)spartial payment%(btn_end)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to set the invoice as %(btn_start)sfully paid%(btn_end)s." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "You need to activate more than one currency to access this report." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You need to select a duration for the exception." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You need to select who the exception applies to." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "You uploaded an invalid or empty file." -msgstr "" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "You're about the generate the closing entries of multiple companies at once. Each of them will be created in accordance with its company tax periodicity." -msgstr "" diff --git a/addons/at_accounting/i18n/zh_CN.po b/addons/at_accounting/i18n/zh_CN.po deleted file mode 100644 index 41fe9f7..0000000 --- a/addons/at_accounting/i18n/zh_CN.po +++ /dev/null @@ -1,5309 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * at_accounting -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 18.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-30 00:00+0000\n" -"PO-Revision-Date: 2026-03-30 00:00+0000\n" -"Last-Translator: \n" -"Language-Team: Chinese (Simplified)\n" -"Language: zh_CN\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__total_depreciation_entries_count -msgid "# Depreciation Entries" -msgstr "# 折旧分录" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__gross_increase_count -msgid "# Gross Increases" -msgstr "# 原值增加" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__depreciation_entries_count -msgid "# Posted Depreciation Entries" -msgstr "# 已过账折旧分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(asset)s: Disposal" -msgstr "%(asset)s: 处置" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(asset)s: Sale" -msgstr "%(asset)s: 销售" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "%(display_name_html)s with an open amount of %(open_amount)s will be fully reconciled by the transaction." -msgstr "%(display_name_html)s 未结金额 %(open_amount)s 将通过该交易完全对账。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "%(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s." -msgstr "%(display_name_html)s 未结金额 %(open_amount)s 将减少 %(amount)s。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "%(journal)s - %(account)s" -msgstr "%(journal)s - %(account)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(months)s m" -msgstr "%(months)s 月" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "%(move_line)s (%(current)s of %(total)s)" -msgstr "%(move_line)s (%(current)s / %(total)s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%(names)s and %(remaining)s others" -msgstr "%(names)s 和其他 %(remaining)s 个" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%(names)s and one other" -msgstr "%(names)s 和另外一个" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "%(report_label)s: %(period)s" -msgstr "%(report_label)s: %(period)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "%(years)s y" -msgstr "%(years)s 年" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "%d transactions had already been imported and were ignored." -msgstr "%d 笔交易已导入,已忽略。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/budget.py:0 -#, python-format -msgid "%s (copy)" -msgstr "%s (副本)" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "%s Future entries will be recomputed to depreciate the asset following the changes." -msgstr "%s 未来分录将根据变更重新计算资产折旧。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "%s is not a numeric value" -msgstr "%s 不是数值" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "%s: Depreciation" -msgstr "%s: 折旧" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'external' engine does not support groupby, limit nor offset." -msgstr "'external' 引擎不支持 groupby、limit 或 offset。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'Open General Ledger' caret option is only available form report lines targetting accounts." -msgstr "'打开总账'选项仅适用于针对账户的报表行。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "'View Bank Statement' caret option is only available for report lines targeting bank statements." -msgstr "'查看银行对账单'选项仅适用于针对银行对账单的报表行。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "(%s lines)" -msgstr "(%s 行)" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding_receipts -msgid "(+) Outstanding Receipts" -msgstr "(+) 未结收款" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding_payments -msgid "(-) Outstanding Payments" -msgstr "(-) 未结付款" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "(1 line)" -msgstr "(1 行)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "(No %s)" -msgstr "(无 %s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "(No Group)" -msgstr "(无分组)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.assets_report_assets_plus -#: model:account.report.column,name:at_accounting.assets_report_depre_plus -msgid "+" -msgstr "+" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.assets_report_assets_minus -#: model:account.report.column,name:at_accounting.assets_report_depre_minus -msgid "-" -msgstr "-" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "-> Reconcile" -msgstr "-> 对账" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "-> Refresh" -msgstr "-> 刷新" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "1 transaction had already been imported and was ignored." -msgstr "1 笔交易已导入,已忽略。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s." -msgstr "折旧分录将在 %(date)s 及之后过账。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
%(extra_text)s" -msgstr "折旧分录将在 %(date)s 及之后过账。
%(extra_text)s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
%(extra_text)s Future entries will be recomputed to depreciate the asset following the changes." -msgstr "折旧分录将在 %(date)s 及之后过账。
%(extra_text)s 未来分录将根据变更重新计算资产折旧。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
A disposal entry will be posted on the %(account_type)s account %(account)s." -msgstr "折旧分录将在 %(date)s 及之后过账。
处置分录将过账到 %(account_type)s 账户 %(account)s。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %(date)s.
A second entry will neutralize the original income and post the outcome of this sale on account %(account)s." -msgstr "折旧分录将在 %(date)s 及之后过账。
第二笔分录将冲销原始收入,并将此次销售结果过账到账户 %(account)s。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A depreciation entry will be posted on and including the date %s." -msgstr "折旧分录将在 %s 及之后过账。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A document linked to %(move_line_name)s has been deleted: %(link)s" -msgstr "与 %(move_line_name)s 关联的单据已删除:%(link)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A document linked to this move has been deleted: %s" -msgstr "与此凭证关联的单据已删除:%s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "A gross increase has been created: %(link)s" -msgstr "已创建原值增加:%(link)s" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/widgets/account_report_x2many/account_report_x2many.js:0 -msgid "A line with a 'Group By' value cannot have children." -msgstr "带有'分组依据'值的行不能有子项。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A non deductible tax value of %(tax_value)s was added to %(name)s's initial value of %(purchase_value)s" -msgstr "不可抵扣税额 %(tax_value)s 已添加到 %(name)s 的初始值 %(purchase_value)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "A non deductible tax value of %(tax_value)s was added to %(name)s\\" -msgstr "不可抵扣税额 %(tax_value)s 已添加到 %(name)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "A tax unit can only be created between companies sharing the same main currency." -msgstr "税务单元只能在共享相同主货币的公司之间创建。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "A tax unit must contain a minimum of two companies. You might want to delete the unit." -msgstr "税务单元必须至少包含两家公司。您可能需要删除该单元。" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.auto.reconcile.wizard" -msgstr "访问.账户.自动.对账.向导" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.change.lock.date" -msgstr "访问.账户.更改.锁定.日期" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.multicurrency.revaluation.wizard" -msgstr "访问.账户.多币种.重估.向导" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.reconcile.wizard" -msgstr "访问.账户.对账.向导" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.report.send" -msgstr "访问.账户.报表.发送" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account.secure.entries.wizard" -msgstr "访问.账户.安全.分录.向导" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account_reports.export.wizard" -msgstr "访问.账户报表.导出.向导" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.account_reports.export.wizard.format" -msgstr "访问.账户报表.导出.向导.格式" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.asset.modify" -msgstr "访问.资产.修改" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.bank.rec.widget" -msgstr "访问.银行.对账.控件" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access.bank.rec.widget.line" -msgstr "访问.银行.对账.控件.行" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access_account_tax_unit_manager" -msgstr "访问_账户_税务单元_管理员" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "access_account_tax_unit_readonly" -msgstr "访问_账户_税务单元_只读" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_account_name -#: model:account.report.column,name:at_accounting.aged_receivable_report_account_name -#: model:account.report.column,name:at_accounting.partner_ledger_report_account_code -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__account_id -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__account_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Account" -msgstr "账户" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_auto_reconcile_wizard -msgid "Account automatic reconciliation wizard" -msgstr "账户自动对账向导" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Account Code" -msgstr "账户代码" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Account Code / Tag" -msgstr "账户代码 / 标签" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_group_tree -msgid "Account Groups" -msgstr "账户组" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Account Label" -msgstr "账户标签" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reconcile_wizard -msgid "Account reconciliation wizard" -msgstr "账户对账向导" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_tax_report_handler -msgid "Account Report Handler for Tax Reports" -msgstr "税务报表的账户报表处理器" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_send -msgid "Account Report Send" -msgstr "账户报表发送" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.account_tag_action -msgid "Account Tags" -msgstr "账户标签" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__transfer_from_account_id -msgid "Account Transfer From" -msgstr "转出账户" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_depreciation_id -msgid "Account used in the depreciation entries, to decrease the asset value." -msgstr "用于折旧分录的账户,以减少资产价值。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_depreciation_expense_id -msgid "Account used in the periodical entries, to record a part of the asset as expense." -msgstr "用于定期分录的账户,以将部分资产记录为费用。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__account_asset_id -msgid "Account used to record the purchase of the asset at its original price." -msgstr "用于按原价记录资产购买的账户。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__gain_account_id -msgid "Account used to write the journal item in case of gain" -msgstr "用于在产生收益时写入日记账分录的账户" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__loss_account_id -msgid "Account used to write the journal item in case of loss" -msgstr "用于在产生损失时写入日记账分录的账户" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.account_report_annotation" -msgstr "账户.账户报表注释" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.account_report_annotation_readonly" -msgstr "账户.账户报表注释_只读" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.asset" -msgstr "账户.资产" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.asset.group" -msgstr "账户.资产.组" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.fiscal.year.manager" -msgstr "账户.会计年度.管理员" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.fiscal.year.user" -msgstr "账户.会计年度.用户" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.ac.user" -msgstr "账户.报表.预算.会计.用户" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.item.ac.user" -msgstr "账户.报表.预算.项目.会计.用户" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.item.readonly" -msgstr "账户.报表.预算.项目.只读" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.budget.readonly" -msgstr "账户.报表.预算.只读" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.file.download.error.wizard" -msgstr "账户.报表.文件.下载.错误.向导" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.ac.user" -msgstr "账户.报表.横向.组.会计.用户" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.readonly" -msgstr "账户.报表.横向.组.只读" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.rule.ac.user" -msgstr "账户.报表.横向.组.规则.会计.用户" - -#. module: at_accounting -#: model:ir.model.access,name:at_accounting -msgid "account.report.horizontal.group.rule.readonly" -msgstr "账户.报表.横向.组.规则.只读" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounting" -msgstr "会计" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_budget -msgid "Accounting Report Budget" -msgstr "会计报表预算" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_budget_item -msgid "Accounting Report Budget Item" -msgstr "会计报表预算项目" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_tree -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_tree -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounting Reports" -msgstr "会计报表" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__account_ids -msgid "Accounts" -msgstr "账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Accounts coverage" -msgstr "账户覆盖率" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Accounts Coverage Report" -msgstr "账户覆盖率报表" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.multicurrency_revaluation_to_adjust -msgid "Accounts To Adjust" -msgstr "待调整账户" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_acquisition_date -msgid "Acquisition Date" -msgstr "购置日期" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__modify_action -msgid "Action" -msgstr "操作" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Add an internal note" -msgstr "添加内部备注" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Add contacts to notify..." -msgstr "添加要通知的联系人..." - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_adjustment -msgid "Adjustment" -msgstr "调整" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "Adjustment Entry" -msgstr "调整分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Advance payments made to suppliers" -msgstr "支付给供应商的预付款" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Advance Payments received from customers" -msgstr "从客户收到的预收款" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Advanced" -msgstr "高级" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "After importing three bills for a vendor without making changes, your ERP will suggest automatically validating future bills..." -msgstr "在为供应商导入三张账单而不做更改后,您的 ERP 将建议自动验证未来的账单..." - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "After the data extraction, check and validate the bill. If no vendor has been found, add one before validating." -msgstr "数据提取后,检查并验证账单。如果未找到供应商,请在验证前添加一个。" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_partner_balance_report_handler -msgid "Aged Partner Balance Custom Handler" -msgstr "账龄合作伙伴余额自定义处理器" - -#. module: at_accounting -#: model:account.report,name:at_accounting.aged_payable_report -#: model:account.report.line,name:at_accounting.aged_payable_line -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_aged_payable -msgid "Aged Payable" -msgstr "账龄应付" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_payable_report_handler -msgid "Aged Payable Custom Handler" -msgstr "账龄应付自定义处理器" - -#. module: at_accounting -#: model:account.report,name:at_accounting.aged_receivable_report -#: model:account.report.line,name:at_accounting.aged_receivable_line -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_aged_receivable -msgid "Aged Receivable" -msgstr "账龄应收" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_aged_receivable_report_handler -msgid "Aged Receivable Custom Handler" -msgstr "账龄应收自定义处理器" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/sales_report/filters/filters.js:0 -msgid "All" -msgstr "全部" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "All Journals" -msgstr "所有日记账" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "All Payable" -msgstr "所有应付" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "All Receivable" -msgstr "所有应收" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_model_autocomplete_ids -msgid "All reconciliation models" -msgstr "所有对账模型" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "All Report Variants" -msgstr "所有报表变体" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be from the same account" -msgstr "所有明细行应来自同一账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be from the same company" -msgstr "所有明细行应来自同一公司" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "All the lines should be posted" -msgstr "所有明细行应已过账" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__allow_partials -msgid "Allow partials" -msgstr "允许部分" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.group_fiscal_year -msgid "Allow to define fiscal years of more or less than a year" -msgstr "允许定义超过或少于一年的会计年度" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__aml -msgid "aml" -msgstr "aml" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_amount -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_amount -#: model:account.report.column,name:at_accounting.partner_ledger_amount -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__amount_currency -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__amount -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount" -msgstr "金额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_amount_currency -#: model:account.report.column,name:at_accounting.aged_receivable_report_amount_currency -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_amount_currency -#: model:account.report.column,name:at_accounting.partner_ledger_report_amount_currency -msgid "Amount Currency" -msgstr "货币金额" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount Due" -msgstr "应付金额" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Amount Due (in currency)" -msgstr "应付金额(货币)" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__amount -msgid "Amount in company currency" -msgstr "公司货币金额" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Amount in Currency" -msgstr "货币金额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "Amount in currency: %s" -msgstr "货币金额:%s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Lakhs" -msgstr "金额(十万)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Millions" -msgstr "金额(百万)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Amounts in Thousands" -msgstr "金额(千)" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__parent_id -msgid "An asset has a parent when it is the result of gaining value" -msgstr "当资产是增值的结果时,它有一个父资产" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "An asset has been created for this move:" -msgstr "已为此凭证创建资产:" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "An asset will be created for the value increase of the asset.
" -msgstr "将为资产增值创建一项资产。
" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "An entry will transfer %(amount)s from %(from_account)s to %(to_account)s." -msgstr "一笔分录将从 %(from_account)s 转移 %(amount)s 到 %(to_account)s。" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Analytic" -msgstr "分析" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__hard_lock_date -msgid "Any entry up to and including that date will be postponed to a later time, in accordance with its journal sequence. This lock date is irreversible and does not allow any exception." -msgstr "截至该日期(含)的任何分录将根据其日记账序列推迟到以后的时间。此锁定日期不可逆转,不允许任何例外。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__fiscalyear_lock_date -msgid "Any entry up to and including that date will be postponed to a later time, in accordance with its journal's sequence." -msgstr "截至该日期(含)的任何分录将根据其日记账序列推迟到以后的时间。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__tax_lock_date -msgid "Any entry with taxes up to and including that date will be postponed to a later time, in accordance with its journal's sequence. The tax lock date is automatically set when the tax closing entry is posted." -msgstr "截至该日期(含)的任何含税分录将根据其日记账序列推迟到以后的时间。税务锁定日期在税务结转分录过账时自动设置。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__purchase_lock_date -msgid "Any purchase entry prior to and including this date will be postponed to a later date, in accordance with its journal's sequence." -msgstr "在此日期(含)之前的任何采购分录将根据其日记账序列推迟到以后的日期。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_change_lock_date__sale_lock_date -msgid "Any sales entry prior to and including this date will be postponed to a later date, in accordance with its journal's sequence." -msgstr "在此日期(含)之前的任何销售分录将根据其日记账序列推迟到以后的日期。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "AP %s" -msgstr "应付 %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "AR %s" -msgstr "应收 %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Archived" -msgstr "已归档" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "As of %s" -msgstr "截至 %s" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Ascending" -msgstr "升序" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__asset_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset" -msgstr "资产" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Account" -msgstr "资产账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Asset Cancelled" -msgstr "资产已取消" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_asset_counterpart_id -msgid "Asset Counterpart Account" -msgstr "资产对应账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Asset created" -msgstr "资产已创建" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Asset created from invoice: %s" -msgstr "从发票创建的资产:%s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset disposed. %s" -msgstr "资产已处置。%s" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset_group -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__asset_group_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Group" -msgstr "资产组" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Model" -msgstr "资产模型" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Model name" -msgstr "资产模型名称" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_asset_model_form -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Models" -msgstr "资产模型" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_move_type -msgid "Asset Move Type" -msgstr "资产凭证类型" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__name -msgid "Asset Name" -msgstr "资产名称" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset paused. %s" -msgstr "资产已暂停。%s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Asset sold. %s" -msgstr "资产已售出。%s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Asset unpaused. %s" -msgstr "资产已恢复。%s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset Values" -msgstr "资产价值" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Asset(s)" -msgstr "资产" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset -msgid "Asset/Revenue Recognition" -msgstr "资产/收入确认" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_total_assets0 -msgid "ASSETS" -msgstr "资产" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.actions.act_window,name:at_accounting.action_account_asset_form -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets" -msgstr "资产" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_finance_config_assets -msgid "Assets and Revenues" -msgstr "资产和收入" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets in closed state" -msgstr "已关闭状态的资产" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Assets in draft and open states" -msgstr "草稿和运行状态的资产" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_asset_report_handler -msgid "Assets Report Custom Handler" -msgstr "资产报表自定义处理器" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period0 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period0 -msgid "At Date" -msgstr "截止日期" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Atleast one asset (%s) couldn't be set as running because it lacks any required information" -msgstr "至少有一项资产 (%s) 无法设置为运行状态,因为它缺少必要信息" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Attach a file" -msgstr "附加文件" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Audit" -msgstr "审计" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.account_reports_audit_reports_menu -msgid "Audit Reports" -msgstr "审计报表" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__auto_balance -msgid "auto_balance" -msgstr "自动平衡" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automate Asset" -msgstr "自动化资产" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automatic Accounting" -msgstr "自动记账" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_auto_reconcile_wizard.py:0 -msgid "Automatically Reconciled Entries" -msgstr "自动对账的分录" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Automation" -msgstr "自动化" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_avgcre0 -msgid "Average creditors days" -msgstr "平均应付天数" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_avdebt0 -msgid "Average debtors days" -msgstr "平均应收天数" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "B: %s" -msgstr "B: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.column,name:at_accounting.balance_sheet_balance -#: model:account.report.column,name:at_accounting.cash_flow_report_balance -#: model:account.report.column,name:at_accounting.executive_summary_column -#: model:account.report.column,name:at_accounting.general_ledger_report_balance -#: model:account.report.column,name:at_accounting.journal_report_balance -#: model:account.report.column,name:at_accounting.partner_ledger_report_balance -#: model:account.report.column,name:at_accounting.profit_and_loss_column -msgid "Balance" -msgstr "余额" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_current -msgid "Balance at Current Rate" -msgstr "按当前汇率的余额" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_operation -msgid "Balance at Operation Rate" -msgstr "按操作汇率的余额" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.multicurrency_revaluation_report_balance_currency -msgid "Balance in Foreign Currency" -msgstr "外币余额" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -#, python-format -msgid "Balance of '%s'" -msgstr "'%s' 的余额" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.balance_bank -msgid "Balance of Bank" -msgstr "银行余额" - -#. module: at_accounting -#: model:account.report,name:at_accounting.balance_sheet -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_balancesheet0 -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_balance_sheet -msgid "Balance Sheet" -msgstr "资产负债表" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_balance_sheet_report_handler -msgid "Balance Sheet Custom Handler" -msgstr "资产负债表自定义处理器" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax advance payment account" -msgstr "税款预付账户余额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax current account (payable)" -msgstr "当期税款账户余额(应付)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Balance tax current account (receivable)" -msgstr "当期税款账户余额(应收)" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_bank_view0 -msgid "Bank and Cash Accounts" -msgstr "银行和现金账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_transactions -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_transactions_kanban -msgid "Bank Reconciliation" -msgstr "银行对账" - -#. module: at_accounting -#: model:account.report,name:at_accounting.bank_reconciliation_report -msgid "Bank Reconciliation Report" -msgstr "银行对账报表" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_bank_reconciliation_report_handler -msgid "Bank Reconciliation Report Custom Handler" -msgstr "银行对账报表自定义处理器" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Bank Statement" -msgstr "银行对账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "Bank Statement %s.pdf" -msgstr "银行对账单 %s.pdf" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "Bank Statement.pdf" -msgstr "银行对账单.pdf" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Base Amount" -msgstr "基础金额" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Based on" -msgstr "基于" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__daily_computation -msgid "Based on days per period" -msgstr "基于每期天数" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Before" -msgstr "之前" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Bills" -msgstr "账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__book_value -msgid "Book Value" -msgstr "账面价值" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_balance -msgid "book_value" -msgstr "账面价值" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_user -msgid "Bookkeeper" -msgstr "记账员" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__budget_id -msgid "Budget" -msgstr "预算" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Budget Items" -msgstr "预算项目" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Budget items can only be edited from account lines." -msgstr "预算项目只能从账户行编辑。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Budget Name" -msgstr "预算名称" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Cancel" -msgstr "取消" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Cancel Asset" -msgstr "取消资产" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__cancelled -msgid "Cancelled" -msgstr "已取消" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Cannot audit tax from another model than account.tax." -msgstr "无法从 account.tax 以外的模型审计税务。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Cannot find in which journal import this statement. Please manually select a journal." -msgstr "无法找到导入此对账单的日记账。请手动选择一个日记账。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Cannot generate carryover values for all fiscal positions at once!" -msgstr "无法一次性为所有财务状况生成结转值!" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Carryover adjustment for tax unit" -msgstr "税务单元的结转调整" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Carryover can only be generated for a single column group." -msgstr "结转只能为单个列组生成。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Carryover from %(date_from)s to %(date_to)s" -msgstr "从 %(date_from)s 到 %(date_to)s 的结转" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Carryover lines for: %s" -msgstr "%s 的结转行" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash0 -msgid "Cash" -msgstr "现金" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash and cash equivalents, beginning of period" -msgstr "期初现金及现金等价物" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash and cash equivalents, closing balance" -msgstr "期末现金及现金等价物余额" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_cash_flow_report_handler -msgid "Cash Flow Report Custom Handler" -msgstr "现金流量报表自定义处理器" - -#. module: at_accounting -#: model:account.report,name:at_accounting.cash_flow_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_cash_flow -msgid "Cash Flow Statement" -msgstr "现金流量表" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from financing activities" -msgstr "筹资活动产生的现金流量" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from investing & extraordinary activities" -msgstr "投资及非常规活动产生的现金流量" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from operating activities" -msgstr "经营活动产生的现金流量" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash flows from unclassified activities" -msgstr "未分类活动产生的现金流量" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash in" -msgstr "现金流入" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash out" -msgstr "现金流出" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash paid for operating activities" -msgstr "经营活动支付的现金" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_received0 -msgid "Cash received" -msgstr "收到的现金" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Cash received from operating activities" -msgstr "经营活动收到的现金" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_spent0 -msgid "Cash spent" -msgstr "支出的现金" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_cash_surplus0 -msgid "Cash surplus" -msgstr "现金盈余" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_change_lock_date -msgid "Change Lock Date" -msgstr "更改锁定日期" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Characteristics" -msgstr "特征" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_reconcile_wizard__to_check -msgid "Check if you are not certain of all the information of the counterpart." -msgstr "如果您不确定对方的所有信息,请勾选。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Check Partner(s) Email(s)" -msgstr "检查合作伙伴邮箱" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method -msgid "Choose the method to use to compute the amount of depreciation lines.\n * Straight Line: Calculated on basis of: Gross Value / Duration\n * Declining: Calculated on basis of: Residual Value * Declining Factor\n * Declining then Straight Line: Like Declining but with a minimum depreciation value equal to the straight line value.\n" -msgstr "选择计算折旧行金额的方法。\n * 直线法:按以下方式计算:原值 / 期限\n * 余额递减法:按以下方式计算:残值 * 递减系数\n * 先递减后直线法:类似递减法,但最低折旧值等于直线法的值。\n" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_auto_reconcile_wizard__search_mode__zero_balance -msgid "Clear Account" -msgstr "清理账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "Click \"New\" or upload a %s." -msgstr "点击\u201c新建\u201d或上传 %s。" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Click on a fetched bank transaction to start the reconciliation process." -msgstr "点击获取的银行交易以开始对账流程。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Close" -msgstr "关闭" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__close -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Closed" -msgstr "已关闭" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_closing_bank_balance0 -msgid "Closing bank balance" -msgstr "银行期末余额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Closing Entry" -msgstr "结转分录" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.journal_report_code -msgid "Code" -msgstr "代码" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Columns" -msgstr "列" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.general_ledger_report_communication -msgid "Communication" -msgstr "备注" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__company_ids -msgid "Companies" -msgstr "公司" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__company_id -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__company_id -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__company_id -msgid "Company" -msgstr "公司" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -#, python-format -msgid "Company %(company)s already belongs to a tax unit in %(country)s. A company can at most be part of one tax unit per country." -msgstr "公司 %(company)s 已属于 %(country)s 的一个税务单元。每个公司在每个国家最多只能属于一个税务单元。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Company Currency" -msgstr "公司货币" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__company_currency_id -msgid "Company currency" -msgstr "公司货币" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Company Only" -msgstr "仅公司" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Company Settings" -msgstr "公司设置" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__prorata_computation_type -msgid "Computation" -msgstr "计算" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_asset_compute_depreciations -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Compute Depreciation" -msgstr "计算折旧" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Configure start dates" -msgstr "配置开始日期" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_config_settings.py:0 -msgid "Configure your start dates" -msgstr "配置您的开始日期" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Configure your tax accounts" -msgstr "配置您的税务账户" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -#, python-format -msgid "Configure your TAX accounts - %s" -msgstr "配置您的税务账户 - %s" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_asset_run -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Confirm" -msgstr "确认" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Confirm the transaction." -msgstr "确认交易。" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Connect your bank and get your latest transactions." -msgstr "连接您的银行并获取最新交易。" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__constant_periods -msgid "Constant Periods" -msgstr "固定期间" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_body -msgid "Contents" -msgstr "内容" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_direct_costs0 -msgid "Cost of Revenue" -msgstr "营业成本" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Could not expand term %(term)s while evaluating formula %(unexpanded_formula)s" -msgstr "在计算公式 %(unexpanded_formula)s 时无法展开术语 %(term)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Could not make sense of the given file.\nDid you install the module to support this type of file?\n" -msgstr "无法解析给定的文件。\n您是否安装了支持此类文件的模块?\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Could not make sense of the given file.\\nDid you install the module to support this type of file?" -msgstr "无法解析给定的文件。您是否安装了支持此类文件的模块?" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Could not parse account_code formula from token '%s'" -msgstr "无法从标记 '%s' 解析 account_code 公式" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Counterpart Values" -msgstr "对方值" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__country_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Country" -msgstr "国家" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_country -msgid "Country Code" -msgstr "国家代码" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Create a new transaction." -msgstr "创建新交易。" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_aml_to_asset -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Create Asset" -msgstr "创建资产" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_create_composite_report_list -msgid "Create Composite Report" -msgstr "创建组合报表" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Create Entry" -msgstr "创建分录" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_create_report_menu -msgid "Create Menu Item" -msgstr "创建菜单项" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_form_bank_rec_widget -msgid "Create Statement" -msgstr "创建对账单" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Create your first vendor bill.

Tip: If you don't have one on hand, use our sample bill." -msgstr "创建您的第一张供应商账单。

提示:如果您手头没有,请使用我们的示例账单。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_credit -#: model:account.report.column,name:at_accounting.journal_report_credit -#: model:account.report.column,name:at_accounting.partner_ledger_report_credit -#: model:account.report.column,name:at_accounting.trial_balance_report_credit -msgid "Credit" -msgstr "贷方" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_depreciated_value -msgid "Cumulative Depreciation" -msgstr "累计折旧" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_currency -#: model:account.report.column,name:at_accounting.aged_receivable_report_currency -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_currency -#: model:account.report.column,name:at_accounting.general_ledger_report_amount_currency -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Currency" -msgstr "货币" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -#, python-format -msgid "Currency Rates (%s)" -msgstr "货币汇率 (%s)" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_currency_id -msgid "Currency to use for reconciliation" -msgstr "用于对账的货币" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.deferred_expense_current -#: model:account.report.column,name:at_accounting.deferred_revenue_current -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Current" -msgstr "当前" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_assets0 -#: model:account.report.line,name:at_accounting.account_financial_report_current_assets_view0 -msgid "Current Assets" -msgstr "流动资产" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_ca_to_l0 -msgid "Current assets to liabilities" -msgstr "流动资产与负债比" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__current_hard_lock_date -msgid "Current Hard Lock" -msgstr "当前硬锁定" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities0 -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities1 -msgid "Current Liabilities" -msgstr "流动负债" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Current Values" -msgstr "当前值" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings_line_1 -msgid "Current Year Retained Earnings" -msgstr "本年留存收益" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_current_year_earnings0 -msgid "Current Year Unallocated Earnings" -msgstr "本年未分配利润" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -msgid "Custom engine _report_custom_engine_last_statement_balance_amount does not support groupby" -msgstr "自定义引擎 _report_custom_engine_last_statement_balance_amount 不支持 groupby" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__invoice_ids -msgid "Customer Invoice" -msgstr "客户发票" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "Customer/Vendor" -msgstr "客户/供应商" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_date -#: model:account.report.column,name:at_accounting.general_ledger_report_date -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__date -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__date -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Date" -msgstr "日期" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_end_date -msgid "Date at which the deferred expense/revenue ends" -msgstr "递延费用/收入结束的日期" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_start_date -msgid "Date at which the deferred expense/revenue starts" -msgstr "递延费用/收入开始的日期" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Date cannot be empty" -msgstr "日期不能为空" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_date_from -#: model:account.report.column,name:at_accounting.assets_report_depre_date_from -msgid "date from" -msgstr "开始日期" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_depreciation_beginning_date -msgid "Date of the beginning of the depreciation" -msgstr "折旧开始日期" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_assets_date_to -#: model:account.report.column,name:at_accounting.assets_report_depre_date_to -msgid "date to" -msgstr "结束日期" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_debit -#: model:account.report.column,name:at_accounting.journal_report_debit -#: model:account.report.column,name:at_accounting.partner_ledger_report_debit -#: model:account.report.column,name:at_accounting.trial_balance_report_debit -msgid "Debit" -msgstr "借方" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Dec. then Straight" -msgstr "先递减后直线" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__degressive -msgid "Declining" -msgstr "余额递减" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_progress_factor -msgid "Declining Factor" -msgstr "递减系数" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__degressive_then_linear -msgid "Declining then Straight Line" -msgstr "先余额递减后直线" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Deductible" -msgstr "可抵扣" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Deferral of %s" -msgstr "%s 的递延" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_move_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred Entries" -msgstr "递延分录" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_entry_type -msgid "Deferred Entry Type" -msgstr "递延分录类型" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__deferred_entry_type__expense -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_deferred_expense -msgid "Deferred Expense" -msgstr "递延费用" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred expense" -msgstr "递延费用" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_expense_report_handler -msgid "Deferred Expense Custom Handler" -msgstr "递延费用自定义处理器" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred expense entries:" -msgstr "递延费用分录:" - -#. module: at_accounting -#: model:account.report,name:at_accounting.deferred_expense_report -msgid "Deferred Expense Report" -msgstr "递延费用报表" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_report_handler -msgid "Deferred Expense Report Custom Handler" -msgstr "递延费用报表自定义处理器" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__deferred_entry_type__revenue -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_deferred_revenue -msgid "Deferred Revenue" -msgstr "递延收入" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred revenue" -msgstr "递延收入" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_deferred_revenue_report_handler -msgid "Deferred Revenue Custom Handler" -msgstr "递延收入自定义处理器" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deferred revenue entries:" -msgstr "递延收入分录:" - -#. module: at_accounting -#: model:account.report,name:at_accounting.deferred_revenue_report -msgid "Deferred Revenue Report" -msgstr "递延收入报表" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Define fiscal years of more or less than one year" -msgstr "定义超过或少于一年的会计年度" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Definition" -msgstr "定义" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Depending moves" -msgstr "依赖凭证" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Deposits" -msgstr "存款" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__value_residual -msgid "Depreciable Amount" -msgstr "可折旧金额" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__value_residual -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_remaining_value -msgid "Depreciable Value" -msgstr "可折旧价值" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciated Amount" -msgstr "已折旧金额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__depreciation_value -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__depreciation -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation" -msgstr "折旧" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_depreciation_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_depreciation_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Account" -msgstr "折旧账户" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Board" -msgstr "折旧计划表" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Depreciation board modified %s" -msgstr "折旧计划表已修改 %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Date" -msgstr "折旧日期" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Depreciation entry %(name)s posted (%(value)s)" -msgstr "折旧分录 %(name)s 已过账 (%(value)s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Depreciation entry %(name)s reversed (%(value)s)" -msgstr "折旧分录 %(name)s 已冲销 (%(value)s)" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__depreciation_move_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Lines" -msgstr "折旧明细行" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Depreciation Method" -msgstr "折旧方法" - -#. module: at_accounting -#: model:account.report,name:at_accounting.assets_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_assets -msgid "Depreciation Schedule" -msgstr "折旧计划" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Descending" -msgstr "降序" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Difference from rounding taxes" -msgstr "税额四舍五入差异" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discard" -msgstr "放弃" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discount Amount" -msgstr "折扣金额" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Discount Date" -msgstr "折扣日期" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__disposal -msgid "Disposal" -msgstr "处置" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Disposal Move" -msgstr "处置分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Disposal Moves" -msgstr "处置分录" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Dispose" -msgstr "处置" - -#. module: at_accounting -#: code:addons/at_accounting/models/digest.py:0 -msgid "Do not have access, skip this data for user's digest email" -msgstr "无访问权限,跳过用户摘要邮件中的此数据" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Document" -msgstr "单据" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__doc_name -msgid "Documents Name" -msgstr "文档名称" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Domestic" -msgstr "国内" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__checkbox_download -msgid "Download" -msgstr "下载" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Download Anyway" -msgstr "仍然下载" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Download the Data Inalterability Check Report" -msgstr "下载数据不可篡改性检查报告" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__draft -msgid "Draft" -msgstr "草稿" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "Draft Entries" -msgstr "草稿分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Due" -msgstr "到期" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_date_maturity -msgid "Due Date" -msgstr "到期日" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_number -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__method_number -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Duration" -msgstr "期限" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_duration_rate -msgid "Duration / Rate" -msgstr "期限 / 费率" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "e.g. Bank Fees" -msgstr "例如 银行手续费" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "e.g. Laptop iBook" -msgstr "例如 笔记本电脑 iBook" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__early_payment -msgid "early_payment" -msgstr "early_payment" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_ec_sales_report_handler -msgid "EC Sales Report Custom Handler" -msgstr "欧盟销售报表自定义处理器" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "EC tax on non EC countries" -msgstr "非欧盟国家的欧盟税" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "EC tax on same country" -msgstr "同一国家的欧盟税" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__edit_mode_amount_currency -msgid "Edit mode amount" -msgstr "编辑模式金额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Editing a manual report line is not allowed in multivat setup when displaying data from all fiscal positions." -msgstr "在多增值税设置中显示所有财务状况数据时,不允许编辑手动报表行。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Editing a manual report line is not allowed when multiple companies are selected." -msgstr "选择多家公司时,不允许编辑手动报表行。" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__checkbox_send_mail -msgid "Email" -msgstr "电子邮件" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_template_id -msgid "Email template" -msgstr "邮件模板" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Enable automatic accounting entries for stock movements" -msgstr "启用库存移动的自动会计分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Enable Sections" -msgstr "启用分节" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -msgid "End Balance" -msgstr "期末余额" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__date_to -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_end_date -msgid "End Date" -msgstr "结束日期" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Month" -msgstr "月末" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Quarter" -msgstr "季末" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "End of Year" -msgstr "年末" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_fiscal_year__date_to -msgid "Ending Date, included in the fiscal year." -msgstr "结束日期,包含在会计年度内。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "entries" -msgstr "分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Entries with partners with no VAT" -msgstr "无增值税号合作伙伴的分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "entry" -msgstr "分录" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_equity0 -msgid "EQUITY" -msgstr "权益" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Error message" -msgstr "错误信息" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_applies_to -msgid "Exception applies" -msgstr "例外适用于" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_duration -msgid "Exception Duration" -msgstr "例外时长" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_needed -msgid "Exception needed" -msgstr "需要例外" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__exception_reason -msgid "Exception Reason" -msgstr "例外原因" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -#, python-format -msgid "Exchange Difference: %s" -msgstr "汇兑差异: %s" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__exchange_diff -msgid "exchange_diff" -msgstr "exchange_diff" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Exclude Bank lines" -msgstr "排除银行明细" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.multicurrency_revaluation_excluded -msgid "Excluded Accounts" -msgstr "排除的账户" - -#. module: at_accounting -#: model:account.report,name:at_accounting.executive_summary -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_exec_summary -msgid "Executive Summary" -msgstr "执行摘要" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_depreciation_expense_id -#: model:ir.model.fields,field_description:at_accounting.field_account_multicurrency_revaluation_wizard__expense_provision_account_id -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_depreciation_expense_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Expense Account" -msgstr "费用账户" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Expense Provision for %s" -msgstr "%s 的费用准备" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_expenses0 -msgid "Expenses" -msgstr "费用" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Export" -msgstr "导出" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reports_export_wizard_format -msgid "Export format for accounting's reports" -msgstr "会计报表导出格式" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__export_format_ids -msgid "Export to" -msgstr "导出到" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_reports_export_wizard -msgid "Export wizard for accounting's reports" -msgstr "会计报表导出向导" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Expression" -msgstr "表达式" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Expression labelled '%(label)s' of line '%(line)s' is being overwritten when computing the current report." -msgstr "计算当前报表时,'%(line)s' 行中标记为 '%(label)s' 的表达式正在被覆盖。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s does not exist on account.move.line, and is not supported by this report's custom handler." -msgstr "字段 %s 在 account.move.line 上不存在,且此报表的自定义处理器不支持该字段。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s does not exist on account.move.line." -msgstr "字段 %s 在 account.move.line 上不存在。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field %s of account.move.line is not stored, and hence cannot be used in a groupby expression" -msgstr "account.move.line 的字段 %s 未存储,因此不能在 groupby 表达式中使用" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Field 'Custom Handler Model' can only reference records inheriting from [%s]." -msgstr "字段 '自定义处理器模型' 只能引用继承自 [%s] 的记录。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "File Download Errors" -msgstr "文件下载错误" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Filters" -msgstr "筛选" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_budget_tree -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_budget_tree -msgid "Financial Budgets" -msgstr "财务预算" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_first_depreciation -msgid "First Depreciation" -msgstr "首次折旧" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__fpos_synced -msgid "Fiscal Positions Synchronised" -msgstr "财务状况已同步" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_fiscal_year -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Year" -msgstr "会计年度" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Year 2018" -msgstr "会计年度 2018" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.actions_account_fiscal_year -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fiscal Years" -msgstr "会计年度" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_asset_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Fixed Asset Account" -msgstr "固定资产账户" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__1h -msgid "for 1 hour" -msgstr "1小时" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__15min -msgid "for 15 minutes" -msgstr "15分钟" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__24h -msgid "for 24 hours" -msgstr "24小时" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__5min -msgid "for 5 minutes" -msgstr "5分钟" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_applies_to__everyone -msgid "for everyone" -msgstr "所有人" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_applies_to__me -msgid "for me" -msgstr "仅自己" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Foreign currencies adjustment entry as of %s" -msgstr "截至 %s 的外币调整分录" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_change_lock_date__exception_duration__forever -msgid "forever" -msgstr "永久" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__from_date -msgid "From" -msgstr "从" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "From %(date_from)s\\nto %(date_to)s" -msgstr "从 %(date_from)s\\n到 %(date_to)s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "From Trade Payable accounts" -msgstr "从应付账款账户" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "From Trade Receivable accounts" -msgstr "从应收账款账户" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__fun_param -msgid "Function Parameter" -msgstr "函数参数" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__fun_to_call -msgid "Function to Call" -msgstr "调用函数" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Future Activities" -msgstr "未来活动" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "G %s" -msgstr "G %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "gain" -msgstr "收益" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "gain/loss" -msgstr "收益/损失" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "General Account Properties" -msgstr "一般账户属性" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -#: model:account.report,name:at_accounting.general_ledger_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_general_ledger -msgid "General Ledger" -msgstr "总账" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_general_ledger_report_handler -msgid "General Ledger Custom Handler" -msgstr "总账自定义处理器" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Generate Entries" -msgstr "生成分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Generate entry" -msgstr "生成分录" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/report_export_wizard.py:0 -msgid "Generated Documents" -msgstr "已生成文档" - -#. module: at_accounting -#: model:account.report,name:at_accounting.generic_ec_sales_report -msgid "Generic EC Sales List" -msgstr "通用欧盟销售清单" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler -msgid "Generic Tax Report Custom Handler" -msgstr "通用税务报表自定义处理器" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler_account_tax -msgid "Generic Tax Report Custom Handler (Account -> Tax)" -msgstr "通用税务报表自定义处理器 (账户 -> 税)" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_generic_tax_report_handler_tax_account -msgid "Generic Tax Report Custom Handler (Tax -> Account)" -msgstr "通用税务报表自定义处理器 (税 -> 账户)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Global Tax Summary" -msgstr "全局税务汇总" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "Go to Apps" -msgstr "前往应用" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Goods" -msgstr "货物" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Grid" -msgstr "网格" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Gross Increase" -msgstr "原值增加" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__account_asset_id -msgid "Gross Increase Account" -msgstr "原值增加账户" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__gross_increase_value -msgid "Gross Increase Value" -msgstr "原值增加值" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_gross_profit0 -msgid "Gross Profit" -msgstr "毛利润" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_gross_profit0 -msgid "Gross profit" -msgstr "毛利润" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_gpmargin0 -msgid "Gross profit margin (gross profit / operating income)" -msgstr "毛利率 (毛利润 / 营业收入)" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group By" -msgstr "分组" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group By..." -msgstr "分组..." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Group Name" -msgstr "组名称" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "Grouped Deferral Entry of %s" -msgstr "%s 的分组递延分录" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__hard_lock_date -msgid "Hard Lock" -msgstr "硬锁定" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Horizontal Group" -msgstr "水平分组" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_account_report_horizontal_groups -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_horizontal_groups -msgid "Horizontal Groups" -msgstr "水平分组" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "How often tax returns have to be made" -msgstr "税务申报频率" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Impact On Grid" -msgstr "对网格的影响" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/bank_statement_csv_import_action.js:0 -msgid "Import Bank Statement" -msgstr "导入银行对账单" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Import File" -msgstr "导入文件" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_bank_statement__unique_import_id -msgid "Import ID" -msgstr "导入ID" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/account_bank_statement_import_model.js:0 -msgid "Import Template for Bank Statements" -msgstr "银行对账单导入模板" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__already_depreciated_amount_import -msgid "In case of an import from another software, you might need to use this field to have the right depreciation table report. This is the value that was already depreciated with entries not computed from this model" -msgstr "从其他软件导入时,您可能需要使用此字段以获得正确的折旧表报告。这是已通过非本模型计算的分录进行折旧的值" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "In Currency" -msgstr "按币种" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Inactive" -msgstr "未启用" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Including Analytic Simulations" -msgstr "包含分析模拟" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.no_statement_unreconciled_payments -#: model:account.report.line,name:at_accounting.unreconciled_last_statement_payments -msgid "Including Unreconciled Payments" -msgstr "包含未对账付款" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.no_statement_unreconciled_receipt -#: model:account.report.line,name:at_accounting.unreconciled_last_statement_receipts -msgid "Including Unreconciled Receipts" -msgstr "包含未对账收款" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_multicurrency_revaluation_wizard__income_provision_account_id -msgid "Income Account" -msgstr "收入账户" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Income Provision for %s" -msgstr "%s 的收入准备" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Incoming" -msgstr "收入" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Inconsistent data: more than one external value at the same date for a 'most_recent' external line." -msgstr "数据不一致:'most_recent' 外部行在同一日期存在多个外部值。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Inconsistent report_id in options dictionary. Options says %(options_report)s; report is %(report)s." -msgstr "选项字典中的 report_id 不一致。选项显示 %(options_report)s;报表是 %(report)s。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -msgid "Inconsistent Statements" -msgstr "不一致的对账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_config_settings.py:0 -#, python-format -msgid "Incorrect fiscal year date: day is out of range for month. Month: %(month)s; Day: %(day)s" -msgstr "会计年度日期不正确:日期超出月份范围。月份:%(month)s;日期:%(day)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -msgid "Initial Balance" -msgstr "期初余额" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/aged_partner_balance/filters.js:0 -msgid "Intervals cannot be smaller than 1" -msgstr "间隔不能小于1" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid domain formula in expression \"%(expression)s\" of line \"%(line)s\": %(formula)s" -msgstr "行 \"%(line)s\" 中表达式 \"%(expression)s\" 的域公式无效:%(formula)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid method “%s”" -msgstr "无效的方法 “%s”" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invalid statements" -msgstr "无效的对账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid subformula in expression \"%(expression)s\" of line \"%(line)s\": %(subformula)s" -msgstr "行 \"%(line)s\" 中表达式 \"%(expression)s\" 的子公式无效:%(subformula)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Invalid token '%(token)s' in account_codes formula '%(formula)s'" -msgstr "account_codes 公式 '%(formula)s' 中的令牌 '%(token)s' 无效" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_invoice_date -#: model:account.report.column,name:at_accounting.aged_receivable_report_invoice_date -#: model:account.report.column,name:at_accounting.partner_ledger_report_invoicing_date -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invoice Date" -msgstr "发票日期" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Invoice lines" -msgstr "发票明细" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_basic -msgid "Invoicing & Banks" -msgstr "开票与银行" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__is_write_off_required -msgid "Is a write-off move required to reconcile" -msgstr "是否需要核销分录进行对账" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__is_transfer_required -msgid "Is an account transfer required" -msgstr "是否需要账户转账" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__transfer_warning_message -msgid "Is an account transfer required to reconcile" -msgstr "是否需要账户转账进行对账" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__lock_date_violated_warning_message -msgid "Is the date violating the lock date of moves" -msgstr "日期是否违反分录的锁定日期" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "It is not possible to decrease or remove the Hard Lock Date." -msgstr "无法减少或移除硬锁定日期。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__salvage_value -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__salvage_value_pct -msgid "It is the amount you plan to have that you cannot depreciate." -msgstr "这是您计划保留且不能折旧的金额。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "It seems there is some depending closing move to be posted" -msgstr "似乎有一些依赖的结账分录需要过账" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "It's not possible to select a budget with the horizontal group feature." -msgstr "无法同时选择预算和水平分组功能。" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "It's not possible to select a horizontal group with the budget feature." -msgstr "无法同时选择水平分组和预算功能。" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__item_ids -msgid "Items" -msgstr "项目" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_journal_code -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__journal_id -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__journal_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal" -msgstr "日记账" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_ja -msgid "Journal Audit" -msgstr "日记账审计" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Journal Entries" -msgstr "日记账分录" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Entry" -msgstr "日记账分录" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Item" -msgstr "日记账明细" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_trial_balance_report.py:0 -#: code:addons/at_accounting/models/bank_reconciliation_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__original_move_line_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal Items" -msgstr "日记账明细" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Journal Items for Tax Audit" -msgstr "税务审计的日记账明细" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Journal Items of %(account)s should have a label in order to generate an asset" -msgstr "%(account)s 的日记账明细应有标签才能生成资产" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_move_line_posted_unreconciled -msgid "Journal Items to reconcile" -msgstr "待对账的日记账明细" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal items where matching number isn't set" -msgstr "未设置匹配号的日记账明细" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Journal items where the account allows reconciliation no matter the residual amount" -msgstr "无论余额如何,该账户允许对账的日记账明细" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Journal items with archived tax tags" -msgstr "带有已归档税务标签的日记账明细" - -#. module: at_accounting -#: model:account.report,name:at_accounting.journal_report -msgid "Journal Report" -msgstr "日记账报表" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_journal_report_handler -msgid "Journal Report Custom Handler" -msgstr "日记账报表自定义处理器" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Journals" -msgstr "日记账" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#: model:account.report.column,name:at_accounting.bank_reconciliation_report_label -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__label -msgid "Label" -msgstr "标签" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_lang -msgid "Lang" -msgstr "语言" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Last Day" -msgstr "最后一天" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.last_statement_balance -msgid "Last statement balance" -msgstr "上期对账单余额" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Last Statement balance + Transactions since statement" -msgstr "上期对账单余额 + 对账单后的交易" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Late Activities" -msgstr "逾期活动" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Later" -msgstr "稍后" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Latest Statement" -msgstr "最新对账单" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Legal signatory" -msgstr "法定签署人" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_cost_sales0 -msgid "Less Costs of Revenue" -msgstr "减:营业成本" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_expense0 -msgid "Less Operating Expenses" -msgstr "减:营业费用" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_depreciation0 -msgid "Less Other Expenses" -msgstr "减:其他费用" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Let's go back to the dashboard." -msgstr "让我们回到仪表板。" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_liabilities_view0 -msgid "LIABILITIES" -msgstr "负债" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_liabilities_and_equity_view0 -msgid "LIABILITIES + EQUITY" -msgstr "负债 + 权益" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_bank_rec_widget_line -msgid "Line of the bank reconciliation widget" -msgstr "银行对账小组件的行" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Linear" -msgstr "线性" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Lines" -msgstr "明细行" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__linked_assets_ids -msgid "Linked Assets" -msgstr "关联资产" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__liquidity -msgid "liquidity" -msgstr "liquidity" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Load more..." -msgstr "加载更多..." - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_change_lock_date -msgid "Lock Dates" -msgstr "锁定日期" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date -msgid "Lock Everything" -msgstr "锁定全部" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date_for_everyone -msgid "Lock Everything For Everyone" -msgstr "为所有人锁定全部" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__fiscalyear_lock_date_for_me -msgid "Lock Everything For Me" -msgstr "为我锁定全部" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_view_account_change_lock_date -msgid "Lock Journal Entries" -msgstr "锁定日记账分录" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date -msgid "Lock Purchases" -msgstr "锁定采购" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date_for_everyone -msgid "Lock Purchases For Everyone" -msgstr "为所有人锁定采购" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__purchase_lock_date_for_me -msgid "Lock Purchases For Me" -msgstr "为我锁定采购" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date -msgid "Lock Sales" -msgstr "锁定销售" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date_for_everyone -msgid "Lock Sales For Everyone" -msgstr "为所有人锁定销售" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__sale_lock_date_for_me -msgid "Lock Sales For Me" -msgstr "为我锁定销售" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date -msgid "Lock Tax Return" -msgstr "锁定税务申报" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date_for_everyone -msgid "Lock Tax Return For Everyone" -msgstr "为所有人锁定税务申报" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__tax_lock_date_for_me -msgid "Lock Tax Return For Me" -msgstr "为我锁定税务申报" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "loss" -msgstr "损失" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__main_company_id -msgid "Main Company" -msgstr "主公司" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__main_company_id -msgid "Main company of this unit; the one actually reporting and paying the taxes." -msgstr "本单位的主公司;实际报税和缴税的公司。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Make Adjustment Entry" -msgstr "创建调整分录" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_bank_statement_import_csv.py:0 -msgid "Make sure that an Amount or Debit and Credit is in the file." -msgstr "请确保文件中包含金额或借方和贷方。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Manage Items" -msgstr "管理项目" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_report_file_download_error_wizard -msgid "Manage the file generation errors from report exports." -msgstr "管理报表导出的文件生成错误。" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__manual -msgid "manual" -msgstr "manual" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "Manual (or import %(import_formats)s)" -msgstr "手动 (或导入 %(import_formats)s)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Manual value" -msgstr "手动值" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Manual values" -msgstr "手动值" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/list_view_switcher.js:0 -msgid "Match" -msgstr "匹配" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Matched" -msgstr "已匹配" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_payment.py:0 -msgid "Matched Transactions" -msgstr "已匹配交易" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.partner_ledger_report_matching_number -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Matching" -msgstr "匹配" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__company_ids -msgid "Members of this unit" -msgstr "本单位的成员" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Memo" -msgstr "备忘录" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.assets_report_first_method -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method -msgid "Method" -msgstr "方法" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Method '%(method_name)s' must start with the '%(prefix)s' prefix." -msgstr "方法 '%(method_name)s' 必须以 '%(prefix)s' 前缀开头。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "Misc" -msgstr "杂项" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.misc_operations -msgid "Misc. operations" -msgstr "杂项操作" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_csv.py:0 -msgid "Mixing CSV files with other file types is not allowed." -msgstr "不允许将CSV文件与其他文件类型混合。" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__model_id -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__model -msgid "Model" -msgstr "模型" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify" -msgstr "修改" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.model,name:at_accounting.model_asset_modify -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify Asset" -msgstr "修改资产" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Modify Depreciation" -msgstr "修改折旧" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Month" -msgstr "月" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Months" -msgstr "月" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__move_line_ids -msgid "Move lines to reconcile" -msgstr "待对账的分录行" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Multi-ledger" -msgstr "多账簿" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_multicurrency_revaluation_report_handler -msgid "Multicurrency Revaluation Report Custom Handler" -msgstr "多币种重估报表自定义处理器" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_multicurrency_revaluation_wizard -msgid "Multicurrency Revaluation Wizard" -msgstr "多币种重估向导" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \n %(closing_entries)s\n" -msgstr "财务状况 %(position)s 在 %(period_start)s 之后存在多个草稿税务结账分录。最多应该只有一个。\n %(closing_entries)s\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \\n %(closing_entries)s" -msgstr "财务状况 %(position)s 在 %(period_start)s 之后存在多个草稿税务结账分录。最多应该只有一个。\\n %(closing_entries)s" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \n %(closing_entries)s\n" -msgstr "您的国内地区在 %(period_start)s 之后存在多个草稿税务结账分录。最多应该只有一个。\n %(closing_entries)s\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -#, python-format -msgid "Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \\n %(closing_entries)s" -msgstr "您的国内地区在 %(period_start)s 之后存在多个草稿税务结账分录。最多应该只有一个。\\n %(closing_entries)s" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_report_send__mode__multi -msgid "Multiple Recipients" -msgstr "多个收件人" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "n/a" -msgstr "不适用" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:account.report.line,name:at_accounting.journal_report_line -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__name -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__name -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__name -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__name -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Name" -msgstr "名称" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_reports_export_wizard_format__doc_name -msgid "Name to give to the generated documents." -msgstr "生成文档的名称。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Navigate easily through reports and see what is behind the numbers" -msgstr "轻松浏览报表并了解数字背后的内容" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__negative_revaluation -msgid "Negative revaluation" -msgstr "负重估" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_net_assets0 -msgid "Net assets" -msgstr "净资产" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__net_gain_on_sale -msgid "Net gain on sale" -msgstr "销售净收益" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_cash_flow_report.py:0 -msgid "Net increase in cash and cash equivalents" -msgstr "现金及现金等价物净增加额" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_profit0 -#: model:account.report.line,name:at_accounting.account_financial_report_net_profit0 -msgid "Net Profit" -msgstr "净利润" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_npmargin0 -msgid "Net profit margin (net profit / income)" -msgstr "净利率 (净利润 / 收入)" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__net_gain_on_sale -msgid "Net value of gain or loss on sale of an asset" -msgstr "资产销售收益或损失的净值" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__value_residual -msgid "New residual amount for the asset" -msgstr "资产的新残余金额" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__salvage_value -msgid "New salvage amount for the asset" -msgstr "资产的新残值金额" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_bank_statement_line_form_bank_rec_widget -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "New Transaction" -msgstr "新交易" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__new_aml -msgid "new_aml" -msgstr "new_aml" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No" -msgstr "否" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -msgid "No adjustment needed" -msgstr "无需调整" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "No attachment was provided" -msgstr "未提供附件" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "No currency found matching '%s'." -msgstr "未找到匹配 '%s' 的货币。" - -#. module: at_accounting -#: code:addons/at_accounting/models/chart_template.py:0 -msgid "No default miscellaneous journal could be found for the active company" -msgstr "未找到活动公司的默认杂项日记账" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "No entry to generate." -msgstr "没有可生成的分录。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No Journal" -msgstr "无日记账" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__prorata_computation_type__none -msgid "No Prorata" -msgstr "无按比例" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -msgid "No provision needed was found." -msgstr "未找到需要的准备。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "No statement" -msgstr "无对账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "No transactions matching your filters were found." -msgstr "未找到匹配您筛选条件的交易。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "No VAT number associated with your company. Please define one." -msgstr "您的公司未关联增值税号。请定义一个。" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__non_deductible_tax_value -msgid "Non Deductible Tax Value" -msgstr "不可抵扣税额" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Non Trade Partners" -msgstr "非贸易合作伙伴" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Non Trade Payable" -msgstr "非贸易应付款" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Non Trade Receivable" -msgstr "非贸易应收款" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Non-Deductible" -msgstr "不可抵扣" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/sales_report/filters/filters.js:0 -msgid "None" -msgstr "无" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__salvage_value -msgid "Not Depreciable Amount" -msgstr "不可折旧金额" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__salvage_value -msgid "Not Depreciable Value" -msgstr "不可折旧值" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__salvage_value_pct -msgid "Not Depreciable Value Percent" -msgstr "不可折旧值百分比" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Not locked" -msgstr "未锁定" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Not Matched" -msgstr "未匹配" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Not Started" -msgstr "未开始" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__name -msgid "Note" -msgstr "备注" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Notes" -msgstr "备注" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -msgid "Nothing to do here!" -msgstr "此处无需操作!" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Now, we'll create your first invoice (accountant)" -msgstr "现在,我们将创建您的第一张发票 (会计)" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__gross_increase_count -msgid "Number of assets made to increase the value of the asset" -msgstr "用于增加资产价值的资产数量" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_number_days -msgid "Number of days" -msgstr "天数" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__total_depreciation_entries_count -msgid "Number of depreciation entries (posted or not)" -msgstr "折旧分录数量 (已过账或未过账)" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Number of Depreciations" -msgstr "折旧次数" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__method_period -#: model:ir.model.fields,field_description:at_accounting.field_asset_modify__method_period -msgid "Number of Months in a Period" -msgstr "每期月数" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Number of periods cannot be smaller than 1" -msgstr "期数不能小于1" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: code:addons/at_accounting/static/src/components/aged_partner_balance/filters.js:0 -msgid "Odoo Warning" -msgstr "Odoo 警告" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_off_sheet -msgid "OFF BALANCE SHEET ACCOUNTS" -msgstr "表外账户" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period5 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period5 -msgid "Older" -msgstr "更早" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__paused -msgid "On Hold" -msgstr "暂停" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/report_export_wizard.py:0 -msgid "One of the formats chosen can not be exported in the DMS" -msgstr "所选格式之一无法导出到DMS" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "Only Billing Administrators are allowed to change lock dates!" -msgstr "只有账单管理员才能更改锁定日期!" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_csv.py:0 -msgid "Only one CSV file can be selected." -msgstr "只能选择一个CSV文件。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Open Asset" -msgstr "打开资产" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -#, python-format -msgid "Open balance of %(amount)s" -msgstr "未结余额 %(amount)s" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_account_reports_customer_statements -msgid "Open Customer Statements" -msgstr "打开客户对账单" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_operating_income0 -msgid "Operating Income (or Loss)" -msgstr "营业收入 (或亏损)" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Options" -msgstr "选项" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Original Deferred Entries" -msgstr "原始递延分录" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_original_move_ids -msgid "Original Invoices" -msgstr "原始发票" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__original_value -msgid "Original Value" -msgstr "原值" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Originator Tax" -msgstr "原始税" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Outgoing" -msgstr "支出" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.outstanding -msgid "Outstanding Receipts/Payments" -msgstr "未结收款/付款" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Parent Asset" -msgstr "父资产" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__report_id -msgid "Parent Report Id" -msgstr "父报表ID" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reports_export_wizard_format__export_wizard_id -msgid "Parent Wizard" -msgstr "父向导" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -#: model:account.report.column,name:at_accounting.general_ledger_report_partner_name -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__to_partner_id -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Partner" -msgstr "合作伙伴" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Partner Categories" -msgstr "合作伙伴类别" - -#. module: at_accounting -#: model:account.report,name:at_accounting.partner_ledger_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_partner_ledger -msgid "Partner Ledger" -msgstr "合作伙伴明细账" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_partner_ledger_report_handler -msgid "Partner Ledger Custom Handler" -msgstr "合作伙伴明细账自定义处理器" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Partner(s) should have an email address." -msgstr "合作伙伴应有电子邮件地址。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__partner_ids -msgid "Partners" -msgstr "合作伙伴" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Pause" -msgstr "暂停" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Pay tax: %s" -msgstr "缴税: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_aged_partner_balance.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payable" -msgstr "应付" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Payable tax amount" -msgstr "应付税额" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_current_liabilities_payable -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_creditors0 -msgid "Payables" -msgstr "应付款" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payment Matching" -msgstr "付款匹配" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__payment_state_before_switch -msgid "Payment State Before Switch" -msgstr "切换前的付款状态" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Payments" -msgstr "付款" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "PDF" -msgstr "PDF" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_auto_reconcile_wizard__search_mode__one_to_one -msgid "Perfect Match" -msgstr "完美匹配" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_performance0 -msgid "Performance" -msgstr "绩效" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Period" -msgstr "期间" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period1 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period1 -msgid "Period 1" -msgstr "第1期" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period2 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period2 -msgid "Period 2" -msgstr "第2期" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period3 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period3 -msgid "Period 3" -msgstr "第3期" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.aged_payable_report_period4 -#: model:account.report.column,name:at_accounting.aged_receivable_report_period4 -msgid "Period 4" -msgstr "第4期" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Period length" -msgstr "期间长度" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Periodicity" -msgstr "周期性" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Periods" -msgstr "期数" - -#. module: at_accounting -#: code:addons/at_accounting/models/budget.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Please enter a valid budget name." -msgstr "请输入有效的预算名称。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Please select a mail template to send multiple statements." -msgstr "请选择邮件模板以发送多个对账单。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Please select the main company and its branches in the company selector to proceed." -msgstr "请在公司选择器中选择主公司及其分支机构以继续。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Please set the deferred accounts in the accounting settings." -msgstr "请在会计设置中设置递延账户。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Please set the deferred journal in the accounting settings." -msgstr "请在会计设置中设置递延日记账。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Please specify the accounts necessary for the Tax Closing Entry." -msgstr "请指定税务结账分录所需的账户。" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_fixed_assets_view0 -msgid "Plus Fixed Assets" -msgstr "加:固定资产" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_non_current_assets_view0 -msgid "Plus Non-current Assets" -msgstr "加:非流动资产" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_non_current_liabilities0 -msgid "Plus Non-current Liabilities" -msgstr "加:非流动负债" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_other_income0 -msgid "Plus Other Income" -msgstr "加:其他收入" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_position0 -msgid "Position" -msgstr "状况" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__positive_revaluation -msgid "Positive revaluation" -msgstr "正重估" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Posted Entries" -msgstr "已过账分录" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Predict vendor bill product" -msgstr "预测供应商账单产品" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_prepayements0 -msgid "Prepayments" -msgstr "预付款" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings_line_2 -msgid "Previous Years Retained Earnings" -msgstr "以前年度留存收益" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_previous_year_earnings0 -msgid "Previous Years Unallocated Earnings" -msgstr "以前年度未分配收益" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Print & Send" -msgstr "打印并发送" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Proceed" -msgstr "继续" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "Product" -msgstr "产品" - -#. module: at_accounting -#: model:account.report,name:at_accounting.profit_and_loss -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_profit_and_loss -msgid "Profit and Loss" -msgstr "利润表" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_profitability0 -msgid "Profitability" -msgstr "盈利能力" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Properties" -msgstr "属性" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__prorata_date -msgid "Prorata Date" -msgstr "按比例日期" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Provision for %(for_cur)s (1 %(comp_cur)s = %(rate)s %(for_cur)s)" -msgstr "%(for_cur)s 的准备 (1 %(comp_cur)s = %(rate)s %(for_cur)s)" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__purchase -msgid "Purchase" -msgstr "采购" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Quarter" -msgstr "季度" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "Re-evaluate" -msgstr "重新评估" - -#. module: at_accounting -#: model:res.groups,name:at_accounting.account.group_account_readonly -msgid "Read-only" -msgstr "只读" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reason..." -msgstr "原因..." - -#. module: at_accounting -#: code:addons/at_accounting/models/account_aged_partner_balance.py:0 -#: code:addons/at_accounting/models/account_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Receivable" -msgstr "应收" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Receivable tax amount" -msgstr "应收税额" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_debtors0 -#: model:account.report.line,name:at_accounting.account_financial_report_receivable0 -msgid "Receivables" -msgstr "应收款" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_partner_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Recipients" -msgstr "收件人" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__search_mode -#: model:ir.ui.menu,name:at_accounting.menu_account_reconcile -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile" -msgstr "对账" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile & open" -msgstr "对账并打开" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_account_id -msgid "Reconcile Account" -msgstr "对账账户" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_open_auto_reconcile_wizard -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reconcile automatically" -msgstr "自动对账" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_auto_reconcile_wizard__search_mode -msgid "Reconcile journal items with opposite balance or clear accounts with a zero balance" -msgstr "对账相反余额的日记账明细或清除零余额账户" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__reco_model_id -msgid "Reconciliation model" -msgstr "对账模型" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Record cost of goods sold in your journal entries" -msgstr "在日记账分录中记录销售成本" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__linked_asset_ids -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__asset_ids -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Assets" -msgstr "关联资产" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Purchase(s)" -msgstr "关联采购" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Related Sale(s)" -msgstr "关联销售" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reminder" -msgstr "提醒" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__account_report_id -msgid "Report" -msgstr "报表" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Report Line" -msgstr "报表行" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Report lines mentioning the account code" -msgstr "提及账户代码的报表行" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Report Name" -msgstr "报表名称" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Reporting" -msgstr "报告" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Reset to running" -msgstr "重置为运行中" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Residual" -msgstr "残余" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Residual in Currency" -msgstr "按币种的残余" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Resume" -msgstr "继续" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Resume Depreciation" -msgstr "继续折旧" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_retained_earnings0 -msgid "Retained Earnings" -msgstr "留存收益" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_return_investment0 -msgid "Return on investments (net profit / assets)" -msgstr "投资回报率 (净利润 / 资产)" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_income0 -#: model:account.report.line,name:at_accounting.account_financial_report_revenue0 -msgid "Revenue" -msgstr "收入" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#, python-format -msgid "Reversal of Grouped Deferral Entry of %s" -msgstr "%s 的分组递延分录冲销" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/multicurrency_revaluation.py:0 -#, python-format -msgid "Reversal of: %s" -msgstr "冲销: %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "Reverse the depreciation entries posted in the future in order to modify the depreciation" -msgstr "冲销未来已过账的折旧分录以修改折旧" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Root Report" -msgstr "根报表" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_bank_statement_import_csv.py:0 -msgid "Rows must be sorted by date." -msgstr "行必须按日期排序。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Run manually" -msgstr "手动运行" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__state__open -msgid "Running" -msgstr "运行中" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_move_line__asset_move_type__sale -msgid "Sale" -msgstr "销售" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save" -msgstr "保存" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save & Close" -msgstr "保存并关闭" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save & New" -msgstr "保存并新建" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Save as Model" -msgstr "保存为模型" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Save model" -msgstr "保存模型" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Search Journal Items to Reconcile" -msgstr "搜索待对账的日记账明细" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Sections" -msgstr "分节" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Sell" -msgstr "销售" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -msgid "Send" -msgstr "发送" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -msgid "Send Partner Ledgers" -msgstr "发送合作伙伴明细账" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Send tax report: %s" -msgstr "发送税务报表: %s" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Sending statements" -msgstr "正在发送对账单" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_budget_item__sequence -msgid "Sequence" -msgstr "序列" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Services" -msgstr "服务" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Set an amount." -msgstr "设置金额。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set as Checked" -msgstr "设为已检查" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/js/tours/at_accounting.js:0 -msgid "Set the payment reference." -msgstr "设置付款参考。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set to Draft" -msgstr "设为草稿" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Set to Running" -msgstr "设为运行中" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.account.menu_account_config -msgid "Settings" -msgstr "设置" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_report_executivesummary_st_cash_forecast0 -msgid "Short term cash forecast" -msgstr "短期现金预测" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Show all records which has next action date is before today" -msgstr "显示所有下一个操作日期在今天之前的记录" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_change_lock_date__show_draft_entries_warning -msgid "Show Draft Entries Warning" -msgstr "显示草稿分录警告" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__signing_user -msgid "Signer" -msgstr "签署人" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_report_send__mode__single -msgid "Single Recipient" -msgstr "单个收件人" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Some fields are missing %s" -msgstr "缺少一些字段 %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "Some required values are missing" -msgstr "缺少一些必填值" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_fiscal_year__date_from -#: model:ir.model.fields,field_description:at_accounting.field_account_move_line__deferred_start_date -msgid "Start Date" -msgstr "开始日期" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_fiscal_year__date_from -msgid "Start Date, included in the fiscal year." -msgstr "开始日期,包含在会计年度内。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Starting Balance" -msgstr "期初余额" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__prorata_date -msgid "Starting date of the period used in the prorata calculation of the first depreciation" -msgstr "用于首次折旧按比例计算的期间开始日期" - -#. module: at_accounting -#: model:ir.actions.server,name:at_accounting.action_bank_statement_attachment -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Statement" -msgstr "对账单" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Statement Line" -msgstr "对账单行" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "Statements are being sent in the background." -msgstr "对账单正在后台发送。" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__state -msgid "Status" -msgstr "状态" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Stock Valuation" -msgstr "库存估值" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__account_asset_report_handler__method__linear -msgid "Straight Line" -msgstr "直线法" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_report_send__mail_subject -msgid "Subject" -msgstr "主题" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Subject..." -msgstr "主题..." - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Suggestions" -msgstr "建议" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__book_value -msgid "Sum of the depreciable value, the salvage value and the book value of all value increase items" -msgstr "可折旧值、残值和所有增值项目账面价值的总和" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#, python-format -msgid "T: %s" -msgstr "T: %s" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__tax_id -msgid "Tax" -msgstr "税" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Tax Amount" -msgstr "税额" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -msgid "Tax Declaration" -msgstr "税务申报" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Grids" -msgstr "税务网格" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_tax_unit__vat -msgid "Tax ID" -msgstr "税号" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Tax Paid Adjustment" -msgstr "已缴税调整" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "Tax Received Adjustment" -msgstr "已收税调整" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Report" -msgstr "税务报表" - -#. module: at_accounting -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_gt -msgid "Tax Return" -msgstr "税务申报" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -msgid "Tax return" -msgstr "税务申报" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Return Periodicity" -msgstr "税务申报周期" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_tax_unit -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Tax Unit" -msgstr "税务单位" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -#, python-format -msgid "tax unit [%s]" -msgstr "税务单位 [%s]" - -#. module: at_accounting -#: model:ir.actions.act_window,name:at_accounting.action_view_tax_units -msgid "Tax Units" -msgstr "税务单位" - -#. module: at_accounting -#: model:ir.model.fields.selection,name:at_accounting.selection__bank_rec_widget_line__flag__tax_line -msgid "tax_line" -msgstr "tax_line" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/static/src/components/bank_reconciliation/kanban.js:0 -msgid "Taxes" -msgstr "税" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal_report.py:0 -msgid "Taxes Applied" -msgstr "已应用税" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__fpos_synced -msgid "Technical field indicating whether Fiscal Positions exist for all companies in the unit" -msgstr "技术字段,指示单位中所有公司是否存在财务状况" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_ir_actions_account_report_download -msgid "Technical model for accounting report downloads" -msgstr "会计报表下载的技术模型" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/ellipsis/ellipsis.js:0 -msgid "Text copied" -msgstr "文本已复制" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The account %(exp_acc)s has been credited by %(exp_delta)s," -msgstr "账户 %(exp_acc)s 已贷记 %(exp_delta)s," - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The account %(exp_acc)s has been credited by %(exp_delta)s, while the account %(dep_acc)s has been debited by %(dep_delta)s. This corresponds to %(move_count)s cancelled %(word)s:" -msgstr "账户 %(exp_acc)s 已贷记 %(exp_delta)s,而账户 %(dep_acc)s 已借记 %(dep_delta)s。这对应 %(move_count)s 个已取消的 %(word)s:" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "The account of this statement (%(account)s) is not the same as the journal (%(journal)s)." -msgstr "此对账单的账户 (%(account)s) 与日记账 (%(journal)s) 不同。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "The Accounts Coverage Report is not available for this report." -msgstr "账户覆盖报表不适用于此报表。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single credit line should be strictly negative." -msgstr "单个贷方行的核销金额应严格为负数。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single debit line should be strictly positive." -msgstr "单个借方行的核销金额应严格为正数。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "The amount of the write-off of a single line cannot be 0." -msgstr "单行核销金额不能为0。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method_period -#: model:ir.model.fields,help:at_accounting.field_asset_modify__method_period -msgid "The amount of time between two depreciations" -msgstr "两次折旧之间的时间间隔" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s)." -msgstr "您输入的金额 (%(entered_amount)s) 与关联采购的值 (%(purchase_value)s) 不匹配。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s). Please make sure this is what you want." -msgstr "您输入的金额 (%(entered_amount)s) 与关联采购的值 (%(purchase_value)s) 不匹配。请确认这是您想要的。" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/line_name/popover_line/annotation_popover_line.js:0 -msgid "The annotation shouldn't have an empty value." -msgstr "注释不应为空值。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__asset_id -msgid "The asset to be modified by this wizard" -msgstr "此向导要修改的资产" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "The attachments of the tax report can be found on the closing entry of the representative company." -msgstr "税务报表的附件可以在代表公司的 结账分录 上找到。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__children_ids -msgid "The children are the gains in value of this asset" -msgstr "子项是此资产的增值" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#, python-format -msgid "The column '%s' is not available for this report." -msgstr "列 '%s' 不适用于此报表。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "The country detected for this VAT number does not match the one set on this Tax Unit." -msgstr "此增值税号检测到的国家/地区与此税务单位设置的不匹配。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__country_id -msgid "The country in which this tax unit is used to group your companies' tax reports declaration." -msgstr "此税务单位用于分组您公司税务报表申报的国家/地区。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "The currency of the bank statement (%(code)s) is not the same as the currency of the journal (%(journal)s)." -msgstr "银行对账单的货币 (%(code)s) 与日记账的货币 (%(journal)s) 不同。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "The currency rate cannot be equal to zero" -msgstr "货币汇率不能等于零" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "The currently selected dates don't match a tax period. The closing entry will be created for the closest-matching period according to your periodicity setup." -msgstr "当前选择的日期与税务期间不匹配。结账分录将根据您的周期设置为最接近的匹配期间创建。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "The date you set violates the lock date of one of your entry. It will be overriden by the following date : %(replacement_date)s" -msgstr "您设置的日期违反了某个分录的锁定日期。它将被以下日期覆盖: %(replacement_date)s" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_move_ids -msgid "The deferred entries created by this invoice" -msgstr "此发票创建的递延分录" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__invoice_ids -msgid "The disposal invoice is needed in order to generate the closing journal entry." -msgstr "需要处置发票才能生成结账日记账分录。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The email address is unknown on the partner" -msgstr "合作伙伴的电子邮件地址未知" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "The ending date must not be prior to the starting date." -msgstr "结束日期不能早于开始日期。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "The following files could not be imported:" -msgstr "以下文件无法导入:" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "The following files could not be imported:\\n" -msgstr "以下文件无法导入:\\n" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_tax_unit__vat -msgid "The identifier to be used when submitting a report for this unit." -msgstr "提交此单位报表时使用的标识符。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "The invoice %(display_name_html)s with an open amount of %(open_amount)s will be entirely paid by the transaction." -msgstr "发票 %(display_name_html)s 未结金额 %(open_amount)s 将通过此交易完全支付。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "The invoice %(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s." -msgstr "发票 %(display_name_html)s 未结金额 %(open_amount)s 将减少 %(amount)s。" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The invoices up to this date will not be taken into account as accounting entries" -msgstr "此日期之前的发票将不作为会计分录计入" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_tax.py:0 -msgid "The main company of a tax unit has to be part of it." -msgstr "税务单位的主公司必须是其组成部分。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__method_number -msgid "The number of depreciations needed to depreciate your asset" -msgstr "折旧您的资产所需的折旧次数" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_move_line__deferred_original_move_ids -msgid "The original invoices that created the deferred entries" -msgstr "创建递延分录的原始发票" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "The remaining value on the last depreciation line must be 0" -msgstr "最后一条折旧行的剩余价值必须为0" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "The system will try to predict the product on vendor bill lines based on the label of the line" -msgstr "系统将根据行标签尝试预测供应商账单行上的产品" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "The used operator is not supported for this expression." -msgstr "此表达式不支持所使用的运算符。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "There are currently reports waiting to be sent, please try again later." -msgstr "当前有报表等待发送,请稍后重试。" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_asset_modify__invoice_line_ids -msgid "There are multiple lines that could be the related to this asset" -msgstr "有多行可能与此资产相关" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "There are unposted depreciations prior to the selected operation date, please deal with them first." -msgstr "在所选操作日期之前存在未过账的折旧,请先处理它们。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account exists in the Chart of Accounts but is not mentioned in any line of the report" -msgstr "此账户存在于会计科目表中,但未在报表的任何行中提及" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported in a line of the report but does not exist in the Chart of Accounts" -msgstr "此账户在报表的某行中报告,但不存在于会计科目表中" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported in multiple lines of the report" -msgstr "此账户在报表的多行中报告" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This account is reported multiple times on the same line of the report" -msgstr "此账户在报表的同一行中被多次报告" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "This allows you to choose the position of totals in your financial reports." -msgstr "此选项允许您选择财务报表中合计的位置。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_bank_statement.py:0 -#, python-format -msgid "This bank transaction has been automatically validated using the reconciliation model '%s'." -msgstr "此银行交易已使用对账模型 '%s' 自动验证。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "This bank transaction is locked up tighter than a squirrel in a nut factory! You can't hit the reset button on it. So, do you want to \\" -msgstr "此银行交易已被牢牢锁定!您无法重置它。那么,您想要 \\" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "This can only be used on journal items" -msgstr "这只能用于日记账明细" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "This file doesn't contain any statement for account %s.\nIf it contains transactions for more than one account, it must be imported on each of them.\n" -msgstr "此文件不包含账户 %s 的任何对账单。\n如果它包含多个账户的交易,必须在每个账户上分别导入。\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "This file doesn't contain any transaction for account %s.\nIf it contains transactions for more than one account, it must be imported on each of them.\n" -msgstr "此文件不包含账户 %s 的任何交易。\n如果它包含多个账户的交易,必须在每个账户上分别导入。\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "This file doesn\\" -msgstr "此文件不\\" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/widgets/account_report_x2many/account_report_x2many.js:0 -msgid "This line and all its children will be deleted. Are you sure you want to proceed?" -msgstr "此行及其所有子项将被删除。您确定要继续吗?" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "This option hides lines with a value of 0" -msgstr "此选项隐藏值为0的行" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_reconcile_model_line.py:0 -msgid "This reconciliation model can't be used in the manual reconciliation widget because its" -msgstr "此对账模型无法在手动对账小组件中使用,因为其" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_reconcile_model_line.py:0 -msgid "This reconciliation model can't be used in the manual reconciliation widget because its configuration is not adapted" -msgstr "此对账模型无法在手动对账小组件中使用,因为其配置不适用" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This report already has a menuitem." -msgstr "此报表已有菜单项。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "This subformula references an unknown expression: %s" -msgstr "此子公式引用了未知表达式: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "This tag is reported in a line of the report but is not linked to any account of the Chart of Accounts" -msgstr "此标签在报表的某行中报告,但未链接到会计科目表的任何账户" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_auto_reconcile_wizard__to_date -msgid "To" -msgstr "到" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_reconcile_wizard__to_check -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "To Check" -msgstr "待检查" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "To enhance authenticity, add a signature to your invoices" -msgstr "为增强真实性,请在发票上添加签名" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Today Activities" -msgstr "今日活动" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#: code:addons/at_accounting/models/account_journal_report.py:0 -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -#: model:account.report.column,name:at_accounting.aged_payable_report_total -#: model:account.report.column,name:at_accounting.aged_receivable_report_total -msgid "Total" -msgstr "合计" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Total %s" -msgstr "合计 %s" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Balance" -msgstr "余额合计" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Credit" -msgstr "贷方合计" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Debit" -msgstr "借方合计" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Residual" -msgstr "残余合计" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Total Residual in Currency" -msgstr "按币种的残余合计" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Trade Partners" -msgstr "贸易合作伙伴" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Transaction" -msgstr "交易" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Transactions" -msgstr "交易" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.transaction_without_statement -msgid "Transactions without statement" -msgstr "无对账单的交易" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "Transfer from %s" -msgstr "从 %s 转入" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "Transfer to %s" -msgstr "转出到 %s" - -#. module: at_accounting -#: model:account.report,name:at_accounting.trial_balance_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_coa -msgid "Trial Balance" -msgstr "试算平衡表" - -#. module: at_accounting -#: model:ir.model,name:at_accounting.model_account_trial_balance_report_handler -msgid "Trial Balance Custom Handler" -msgstr "试算平衡表自定义处理器" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Triangular" -msgstr "三角" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to dispatch an action on a report not compatible with the provided options." -msgstr "尝试在与提供的选项不兼容的报表上调度操作。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Trying to expand a group for a line which was not generated by a report line: %s" -msgstr "尝试展开一个不是由报表行生成的行的分组: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to expand a line without an expansion function." -msgstr "尝试展开没有展开函数的行。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Trying to expand groupby results on lines without a groupby value." -msgstr "尝试在没有 groupby 值的行上展开 groupby 结果。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "Turn as an asset" -msgstr "转为资产" - -#. module: at_accounting -#: model:ir.model.fields,field_description:at_accounting.field_account_asset_report_handler__account_type -msgid "Type of the account" -msgstr "账户类型" - -#. module: at_accounting -#: model:account.report.line,name:at_accounting.account_financial_unaffected_earnings0 -msgid "Unallocated Earnings" -msgstr "未分配收益" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Unknown" -msgstr "未知" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Unknown bound criterium: %s" -msgstr "未知的边界条件: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Unknown date scope: %s" -msgstr "未知的日期范围: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -msgid "Unknown Partner" -msgstr "未知合作伙伴" - -#. module: at_accounting -#: model:account.report,name:at_accounting.multicurrency_revaluation_report -#: model:ir.ui.menu,name:at_accounting.menu_action_account_report_multicurrency_revaluation -msgid "Unrealized Currency Gains/Losses" -msgstr "未实现货币收益/损失" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Unreconciled" -msgstr "未对账" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Unreconciled Entries" -msgstr "未对账分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/res_company.py:0 -msgid "Unreconciled statements lines" -msgstr "未对账的对账单行" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Validate" -msgstr "验证" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "Value at Import" -msgstr "导入时的值" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Value decrease for: %(asset)s" -msgstr "%(asset)s 的值减少" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -#, python-format -msgid "Value increase for: %(asset)s" -msgstr "%(asset)s 的值增加" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "Vat closing from %(date_from)s to %(date_to)s" -msgstr "从 %(date_from)s 到 %(date_to)s 的增值税结账" - -#. module: at_accounting -#: model:account.report.column,name:at_accounting.account_financial_report_ec_sales_vat -msgid "VAT Number" -msgstr "增值税号" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "VAT Periodicity" -msgstr "增值税周期" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/bank_reconciliation/list_view_switcher.js:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "View" -msgstr "查看" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Bank Statement" -msgstr "查看银行对账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Journal Entry" -msgstr "查看日记账分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/models/account_sales_report.py:0 -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "View Partner" -msgstr "查看合作伙伴" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_report_send.py:0 -msgid "View Partner(s)" -msgstr "查看合作伙伴" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "View Payment" -msgstr "查看付款" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "View Reconciled Entries" -msgstr "查看已对账分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "View successfully imported statements" -msgstr "查看成功导入的对账单" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "Warning for the Original Value of %s" -msgstr "%s 原值的警告" - -#. module: at_accounting -#: model:ir.model.fields,help:at_accounting.field_account_asset_report_handler__state -msgid "When an asset is created, the status is 'Draft'.\nIf the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\nThe 'On Hold' status can be set manually when you want to pause the depreciation of an asset for some time.\nYou can manually close an asset when the depreciation is over.\nBy cancelling an asset, all depreciation entries will be reversed\n" -msgstr "当创建资产时,状态为 '草稿'。\n如果资产被确认,状态变为 '运行中',折旧行可以过账到会计中。\n'暂停' 状态可以在您想要暂时停止资产折旧时手动设置。\n当折旧结束时,您可以手动关闭资产。\n通过取消资产,所有折旧分录将被冲销\n" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "When ticked, totals and subtotals appear below the sections of the report" -msgstr "勾选时,合计和小计显示在报表分节下方" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "With Draft Entries" -msgstr "包含草稿分录" - -#. module: at_accounting -#: model:ir.ui.view,arch_db:at_accounting.view -msgid "With residual" -msgstr "有残余" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "Write-Off" -msgstr "核销" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -msgid "Write-Off Entry" -msgstr "核销分录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -#, python-format -msgid "Wrong format for if_other_expr_above/if_other_expr_below formula: %s" -msgstr "if_other_expr_above/if_other_expr_below 公式格式错误: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_general_ledger.py:0 -#, python-format -msgid "Wrong ID for general ledger line to expand: %s" -msgstr "要展开的总账行 ID 错误: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_partner_ledger.py:0 -#, python-format -msgid "Wrong ID for partner ledger line to expand: %s" -msgstr "要展开的合作伙伴明细账行 ID 错误: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "XLSX" -msgstr "XLSX" - -#. module: at_accounting -#: code:addons/at_accounting/static/src/components/account_report/filters/filters.js:0 -msgid "Year" -msgstr "年" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_report.py:0 -msgid "Yes" -msgstr "是" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "You already have imported that file." -msgstr "您已经导入过该文件。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years." -msgstr "两个会计年度不能重叠,请更正您的会计年度的开始和/或结束日期。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_reconcile_wizard.py:0 -#, python-format -msgid "You can only reconcile entries with up to two different accounts: %s" -msgstr "您只能对账最多两个不同账户的分录: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget.py:0 -msgid "You can't hit the reset button on a secured bank transaction." -msgstr "您无法重置已安全的银行交易。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You can't open a tax report from a move without a VAT closing date." -msgstr "您无法从没有增值税结账日期的分录打开税务报表。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You can't post an entry related to a draft asset. Please post the asset before." -msgstr "您无法过账与草稿资产相关的分录。请先过账资产。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You can't re-evaluate the asset before the lock date." -msgstr "您无法在锁定日期之前重新评估资产。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot add or remove bills when the asset is already running or closed." -msgstr "当资产已运行或关闭时,您无法添加或删除账单。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move_line.py:0 -msgid "You cannot add taxes on a tax closing move line." -msgstr "您无法在税务结账分录行上添加税。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot archive a record that is not closed" -msgstr "您无法归档未关闭的记录" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot automate the journal entry for an asset that has a running gross increase. Please use 'Dispose' on the increase(s)." -msgstr "您无法为具有运行中的原值增加的资产自动创建日记账分录。请对增加项使用 '处置'。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -#, python-format -msgid "You cannot change the account for a deferred line in %(move_name)s if it has already been deferred." -msgstr "如果 %(move_name)s 中的递延行已被递延,您无法更改其账户。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot create a deferred entry with a start date but no end date." -msgstr "您无法创建有开始日期但无结束日期的递延分录。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot create a deferred entry with a start date later than the end date." -msgstr "您无法创建开始日期晚于结束日期的递延分录。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot create an asset from lines containing credit and debit on the account or with a null amount" -msgstr "您无法从账户上包含借方和贷方的行或金额为零的行创建资产" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -#, python-format -msgid "You cannot delete a document that is in %s state." -msgstr "您无法删除处于 %s 状态的文档。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot delete an asset linked to posted entries." -msgstr "您无法删除链接到已过账分录的资产。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot delete an asset linked to posted entries.\nYou should either confirm the asset, then, sell or dispose of it, or cancel the linked journal entries.\n" -msgstr "您无法删除链接到已过账分录的资产。\n您应该确认资产,然后出售或处置它,或取消链接的日记账分录。\n" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_asset.py:0 -msgid "You cannot dispose of an asset before the lock date." -msgstr "您无法在锁定日期之前处置资产。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot generate deferred entries for a miscellaneous journal entry." -msgstr "您无法为杂项日记账分录生成递延分录。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "You cannot generate entries for a period that does not end at the end of the month." -msgstr "您无法为不在月末结束的期间生成分录。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_deferred_reports.py:0 -msgid "You cannot generate entries for a period that is locked." -msgstr "您无法为已锁定的期间生成分录。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_fiscal_year.py:0 -msgid "You cannot have a fiscal year on a child company." -msgstr "您无法在子公司上设置会计年度。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as another closing entry has been posted at a later date." -msgstr "您无法将此结账分录重置为草稿,因为另一个结账分录已在较晚日期过账。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a" -msgstr "您无法将此结账分录重置为草稿,因为这将删除影响税务报表的结转值" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a locked period. To do this, you first need to modify you tax return lock date." -msgstr "您无法将此结账分录重置为草稿,因为这将删除影响已锁定期间税务报表的结转值。要执行此操作,您首先需要修改税务申报锁定日期。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset to draft an entry related to a posted asset" -msgstr "您无法将与已过账资产相关的分录重置为草稿" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_move.py:0 -msgid "You cannot reset to draft an invoice that is grouped in deferral entry. You can create a credit note instead." -msgstr "您无法将分组在递延分录中的发票重置为草稿。您可以创建贷项通知单代替。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot resume at a date equal to or before the pause date" -msgstr "您无法在等于或早于暂停日期的日期继续" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/asset_modify.py:0 -msgid "You cannot select the same account as the Depreciation Account" -msgstr "您无法选择与折旧账户相同的账户" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You cannot set a Lock Date in the future." -msgstr "您无法设置未来的锁定日期。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -#, python-format -msgid "You have to set a Default Account for the journal: %s" -msgstr "您必须为日记账设置默认账户: %s" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to %(btn_start)sfully reconcile%(btn_end)s the document." -msgstr "您可能想要 %(btn_start)s完全对账%(btn_end)s 该文档。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to make a %(btn_start)spartial reconciliation%(btn_end)s instead." -msgstr "您可能想要改为 %(btn_start)s部分对账%(btn_end)s。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to record a %(btn_start)spartial payment%(btn_end)s." -msgstr "您可能想要记录 %(btn_start)s部分付款%(btn_end)s。" - -#. module: at_accounting -#: code:addons/at_accounting/models/bank_rec_widget_line.py:0 -#, python-format -msgid "You might want to set the invoice as %(btn_start)sfully paid%(btn_end)s." -msgstr "您可能想要将发票设置为 %(btn_start)s已完全支付%(btn_end)s。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_multicurrency_revaluation_report.py:0 -msgid "You need to activate more than one currency to access this report." -msgstr "您需要启用多个货币才能访问此报表。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You need to select a duration for the exception." -msgstr "您需要为例外选择一个时长。" - -#. module: at_accounting -#: code:addons/at_accounting/wizard/account_change_lock_date.py:0 -msgid "You need to select who the exception applies to." -msgstr "您需要选择例外适用于谁。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_journal.py:0 -msgid "You uploaded an invalid or empty file." -msgstr "您上传了无效或空的文件。" - -#. module: at_accounting -#: code:addons/at_accounting/models/account_generic_tax_report.py:0 -msgid "You're about the generate the closing entries of multiple companies at once. Each of them will be created in accordance with its company tax periodicity." -msgstr "您即将同时生成多家公司的结账分录。每个分录将根据其公司税务周期创建。" diff --git a/addons/at_accounting/models/__init__.py b/addons/at_accounting/models/__init__.py deleted file mode 100644 index 0186f30..0000000 --- a/addons/at_accounting/models/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -from . import account_account -from . import account_bank_statement -from . import account_chart_template -from . import account_fiscal_year -from . import account_journal_dashboard -from . import account_move -from . import account_payment -from . import account_reconcile_model -from . import account_reconcile_model_line -from . import account_tax -from . import digest -from . import res_config_settings -from . import res_company -from . import bank_rec_widget -from . import bank_rec_widget_line -from . import ir_ui_menu -from . import res_currency -from . import res_partner -from . import account_report -from . import account_analytic_report -from . import bank_reconciliation_report -from . import account_general_ledger -from . import account_generic_tax_report -from . import account_journal_report -from . import account_cash_flow_report -from . import account_deferred_reports -from . import account_multicurrency_revaluation_report -from . import account_move_line -from . import account_trial_balance_report -from . import account_aged_partner_balance -from . import account_partner_ledger -from . import mail_activity -from . import mail_activity_type -from . import chart_template -from . import ir_actions -from . import account_sales_report -from . import executive_summary_report -from . import budget -from . import balance_sheet -from . import account_fiscal_position -from . import account_asset,account_journal -from . import account_journal_csv \ No newline at end of file diff --git a/addons/at_accounting/models/__pycache__/__init__.cpython-312.pyc b/addons/at_accounting/models/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d11938e..0000000 Binary files a/addons/at_accounting/models/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_account.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_account.cpython-312.pyc deleted file mode 100644 index fbd6ed8..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_account.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_aged_partner_balance.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_aged_partner_balance.cpython-312.pyc deleted file mode 100644 index 88b9117..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_aged_partner_balance.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_asset.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_asset.cpython-312.pyc deleted file mode 100644 index fcacb2a..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_asset.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_bank_statement.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_bank_statement.cpython-312.pyc deleted file mode 100644 index baadafc..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_bank_statement.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_chart_template.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_chart_template.cpython-312.pyc deleted file mode 100644 index db11af4..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_chart_template.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_deferred_reports.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_deferred_reports.cpython-312.pyc deleted file mode 100644 index 0a39367..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_deferred_reports.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_fiscal_position.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_fiscal_position.cpython-312.pyc deleted file mode 100644 index 0586d86..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_fiscal_position.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_fiscal_year.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_fiscal_year.cpython-312.pyc deleted file mode 100644 index fdbffc0..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_fiscal_year.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_general_ledger.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_general_ledger.cpython-312.pyc deleted file mode 100644 index 0a85883..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_general_ledger.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_generic_tax_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_generic_tax_report.cpython-312.pyc deleted file mode 100644 index c86f09c..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_generic_tax_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_journal.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_journal.cpython-312.pyc deleted file mode 100644 index a65e9ce..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_journal.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_journal_csv.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_journal_csv.cpython-312.pyc deleted file mode 100644 index 2a983a2..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_journal_csv.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_journal_dashboard.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_journal_dashboard.cpython-312.pyc deleted file mode 100644 index 8213a3e..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_journal_dashboard.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_journal_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_journal_report.cpython-312.pyc deleted file mode 100644 index fd50454..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_journal_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_move.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_move.cpython-312.pyc deleted file mode 100644 index bc9f0a6..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_move.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_move_line.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_move_line.cpython-312.pyc deleted file mode 100644 index 8199928..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_move_line.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_multicurrency_revaluation_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_multicurrency_revaluation_report.cpython-312.pyc deleted file mode 100644 index 408897e..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_multicurrency_revaluation_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_partner_ledger.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_partner_ledger.cpython-312.pyc deleted file mode 100644 index 6dafb95..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_partner_ledger.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_payment.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_payment.cpython-312.pyc deleted file mode 100644 index 75f31c7..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_payment.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_reconcile_model.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_reconcile_model.cpython-312.pyc deleted file mode 100644 index c322d7b..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_reconcile_model.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_reconcile_model_line.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_reconcile_model_line.cpython-312.pyc deleted file mode 100644 index c27a3bc..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_reconcile_model_line.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_report.cpython-312.pyc deleted file mode 100644 index 7000862..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_sales_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_sales_report.cpython-312.pyc deleted file mode 100644 index 397e1e2..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_sales_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/account_trial_balance_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/account_trial_balance_report.cpython-312.pyc deleted file mode 100644 index 0cd3bcc..0000000 Binary files a/addons/at_accounting/models/__pycache__/account_trial_balance_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/balance_sheet.cpython-312.pyc b/addons/at_accounting/models/__pycache__/balance_sheet.cpython-312.pyc deleted file mode 100644 index 5c31791..0000000 Binary files a/addons/at_accounting/models/__pycache__/balance_sheet.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/bank_rec_widget.cpython-312.pyc b/addons/at_accounting/models/__pycache__/bank_rec_widget.cpython-312.pyc deleted file mode 100644 index 5454871..0000000 Binary files a/addons/at_accounting/models/__pycache__/bank_rec_widget.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/bank_rec_widget_line.cpython-312.pyc b/addons/at_accounting/models/__pycache__/bank_rec_widget_line.cpython-312.pyc deleted file mode 100644 index 03115c4..0000000 Binary files a/addons/at_accounting/models/__pycache__/bank_rec_widget_line.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/bank_reconciliation_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/bank_reconciliation_report.cpython-312.pyc deleted file mode 100644 index d874480..0000000 Binary files a/addons/at_accounting/models/__pycache__/bank_reconciliation_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/budget.cpython-312.pyc b/addons/at_accounting/models/__pycache__/budget.cpython-312.pyc deleted file mode 100644 index 582ed31..0000000 Binary files a/addons/at_accounting/models/__pycache__/budget.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/chart_template.cpython-312.pyc b/addons/at_accounting/models/__pycache__/chart_template.cpython-312.pyc deleted file mode 100644 index f2632c0..0000000 Binary files a/addons/at_accounting/models/__pycache__/chart_template.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/digest.cpython-312.pyc b/addons/at_accounting/models/__pycache__/digest.cpython-312.pyc deleted file mode 100644 index 357d08f..0000000 Binary files a/addons/at_accounting/models/__pycache__/digest.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/executive_summary_report.cpython-312.pyc b/addons/at_accounting/models/__pycache__/executive_summary_report.cpython-312.pyc deleted file mode 100644 index a405fa2..0000000 Binary files a/addons/at_accounting/models/__pycache__/executive_summary_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/ir_actions.cpython-312.pyc b/addons/at_accounting/models/__pycache__/ir_actions.cpython-312.pyc deleted file mode 100644 index 8c809d2..0000000 Binary files a/addons/at_accounting/models/__pycache__/ir_actions.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/ir_ui_menu.cpython-312.pyc b/addons/at_accounting/models/__pycache__/ir_ui_menu.cpython-312.pyc deleted file mode 100644 index a3ad574..0000000 Binary files a/addons/at_accounting/models/__pycache__/ir_ui_menu.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/mail_activity.cpython-312.pyc b/addons/at_accounting/models/__pycache__/mail_activity.cpython-312.pyc deleted file mode 100644 index 24e0480..0000000 Binary files a/addons/at_accounting/models/__pycache__/mail_activity.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/mail_activity_type.cpython-312.pyc b/addons/at_accounting/models/__pycache__/mail_activity_type.cpython-312.pyc deleted file mode 100644 index 14f6b2f..0000000 Binary files a/addons/at_accounting/models/__pycache__/mail_activity_type.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/res_company.cpython-312.pyc b/addons/at_accounting/models/__pycache__/res_company.cpython-312.pyc deleted file mode 100644 index d2f208f..0000000 Binary files a/addons/at_accounting/models/__pycache__/res_company.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/res_config_settings.cpython-312.pyc b/addons/at_accounting/models/__pycache__/res_config_settings.cpython-312.pyc deleted file mode 100644 index 7087115..0000000 Binary files a/addons/at_accounting/models/__pycache__/res_config_settings.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/__pycache__/res_partner.cpython-312.pyc b/addons/at_accounting/models/__pycache__/res_partner.cpython-312.pyc deleted file mode 100644 index 3e1c66e..0000000 Binary files a/addons/at_accounting/models/__pycache__/res_partner.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/models/account_account.py b/addons/at_accounting/models/account_account.py deleted file mode 100644 index 0cfa2ec..0000000 --- a/addons/at_accounting/models/account_account.py +++ /dev/null @@ -1,45 +0,0 @@ -import ast -from odoo import api, fields, models, _ - - -class AccountAccount(models.Model): - _inherit = "account.account" - - def action_open_reconcile(self): - self.ensure_one() - # Open reconciliation view for this account - action_values = self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_move_line_posted_unreconciled') - domain = ast.literal_eval(action_values['domain']) - domain.append(('account_id', '=', self.id)) - action_values['domain'] = domain - return action_values - - exclude_provision_currency_ids = fields.Many2many('res.currency', relation='account_account_exclude_res_currency_provision', help="Whether or not we have to make provisions for the selected foreign currencies.") - budget_item_ids = fields.One2many(comodel_name='account.report.budget.item', inverse_name='account_id') # To use it in the domain when adding accounts from the report - - asset_model_ids = fields.Many2many( - 'account.asset', - domain=[('state', '=', 'model')], - help="An asset wil be created for each asset model when this account is used on a vendor bill or a refund", - tracking=True, - ) - create_asset = fields.Selection([('no', 'No'), ('draft', 'Create in draft'), ('validate', 'Create and validate')], - required=True, default='no', tracking=True) - # specify if the account can generate asset depending on it's type. It is used in the account form view - can_create_asset = fields.Boolean(compute="_compute_can_create_asset") - form_view_ref = fields.Char(compute='_compute_can_create_asset') - # decimal quantities are not supported, quantities are rounded to the lower int - multiple_assets_per_line = fields.Boolean(string='Multiple Assets per Line', default=False, tracking=True, - help="Multiple asset items will be generated depending on the bill line quantity instead of 1 global asset.") - - @api.depends('account_type') - def _compute_can_create_asset(self): - for record in self: - record.can_create_asset = record.account_type in ('asset_fixed', 'asset_non_current') - record.form_view_ref = 'at_accountingview_account_asset_form' - - @api.onchange('create_asset') - def _onchange_multiple_assets_per_line(self): - for record in self: - if record.create_asset == 'no': - record.multiple_assets_per_line = False diff --git a/addons/at_accounting/models/account_aged_partner_balance.py b/addons/at_accounting/models/account_aged_partner_balance.py deleted file mode 100644 index 89d224d..0000000 --- a/addons/at_accounting/models/account_aged_partner_balance.py +++ /dev/null @@ -1,446 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import datetime - -from odoo import models, fields, _ -from odoo.tools import SQL -from odoo.tools.misc import format_date - -from dateutil.relativedelta import relativedelta -from itertools import chain - - -class AgedPartnerBalanceCustomHandler(models.AbstractModel): - _name = 'account.aged.partner.balance.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Aged Partner Balance Custom Handler' - - def _get_custom_display_config(self): - return { - 'css_custom_class': 'aged_partner_balance', - 'templates': { - 'AccountReportLineName': 'at_accounting.AgedPartnerBalanceLineName', - }, - 'components': { - 'AccountReportFilters': 'at_accounting.AgedPartnerBalanceFilters', - }, - } - - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - hidden_columns = set() - - options['multi_currency'] = report.env.user.has_group('base.group_multi_currency') - options['show_currency'] = options['multi_currency'] and (previous_options or {}).get('show_currency', False) - if not options['show_currency']: - hidden_columns.update(['amount_currency', 'currency']) - - options['show_account'] = (previous_options or {}).get('show_account', False) - if not options['show_account']: - hidden_columns.add('account_name') - - options['columns'] = [ - column for column in options['columns'] - if column['expression_label'] not in hidden_columns - ] - - default_order_column = { - 'expression_label': 'invoice_date', - 'direction': 'ASC', - } - - options['order_column'] = previous_options.get('order_column') or default_order_column - options['aging_based_on'] = previous_options.get('aging_based_on') or 'base_on_maturity_date' - options['aging_interval'] = previous_options.get('aging_interval') or 30 - - # Set aging column names - interval = options['aging_interval'] - for column in options['columns']: - if column['expression_label'].startswith('period'): - period_number = int(column['expression_label'].replace('period', '')) - 1 - if 0 <= period_number < 4: - column['name'] = f'{interval * period_number + 1}-{interval * (period_number + 1)}' - - def _custom_line_postprocessor(self, report, options, lines): - partner_lines_map = {} - - # Sort line dicts by partner - for line in lines: - model, model_id = report._get_model_info_from_id(line['id']) - if model == 'res.partner': - partner_lines_map[model_id] = line - - if partner_lines_map: - for partner, line_dict in zip( - self.env['res.partner'].browse(partner_lines_map), - partner_lines_map.values() - ): - line_dict['trust'] = partner.with_company(partner.company_id or self.env.company).trust - - return lines - - def _report_custom_engine_aged_receivable(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._aged_partner_report_custom_engine_common(options, 'asset_receivable', current_groupby, next_groupby, offset=offset, limit=limit) - - def _report_custom_engine_aged_payable(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._aged_partner_report_custom_engine_common(options, 'liability_payable', current_groupby, next_groupby, offset=offset, limit=limit) - - def _aged_partner_report_custom_engine_common(self, options, internal_type, current_groupby, next_groupby, offset=0, limit=None): - report = self.env['account.report'].browse(options['report_id']) - report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - def minus_days(date_obj, days): - return fields.Date.to_string(date_obj - relativedelta(days=days)) - - aging_date_field = SQL.identifier('invoice_date') if options['aging_based_on'] == 'base_on_invoice_date' else SQL.identifier('date_maturity') - date_to = fields.Date.from_string(options['date']['date_to']) - interval = options['aging_interval'] - periods = [(False, fields.Date.to_string(date_to))] - # Since we added the first period in the list we have to do one less iteration - nb_periods = len([column for column in options['columns'] if column['expression_label'].startswith('period')]) - 1 - for i in range(nb_periods): - start_date = minus_days(date_to, (interval * i) + 1) - # The last element of the list will have False for the end date - end_date = minus_days(date_to, interval * (i + 1)) if i < nb_periods - 1 else False - periods.append((start_date, end_date)) - - def build_result_dict(report, query_res_lines): - rslt = {f'period{i}': 0 for i in range(len(periods))} - - for query_res in query_res_lines: - for i in range(len(periods)): - period_key = f'period{i}' - rslt[period_key] += query_res[period_key] - - if current_groupby == 'id': - query_res = query_res_lines[0] # We're grouping by id, so there is only 1 element in query_res_lines anyway - currency = self.env['res.currency'].browse(query_res['currency_id'][0]) if len(query_res['currency_id']) == 1 else None - rslt.update({ - 'invoice_date': query_res['invoice_date'][0] if len(query_res['invoice_date']) == 1 else None, - 'due_date': query_res['due_date'][0] if len(query_res['due_date']) == 1 else None, - 'amount_currency': query_res['amount_currency'], - 'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None, - 'currency': currency.display_name if currency else None, - 'account_name': query_res['account_name'][0] if len(query_res['account_name']) == 1 else None, - 'total': None, - 'has_sublines': query_res['aml_count'] > 0, - - # Needed by the custom_unfold_all_batch_data_generator, to speed-up unfold_all - 'partner_id': query_res['partner_id'][0] if query_res['partner_id'] else None, - }) - else: - rslt.update({ - 'invoice_date': None, - 'due_date': None, - 'amount_currency': None, - 'currency_id': None, - 'currency': None, - 'account_name': None, - 'total': sum(rslt[f'period{i}'] for i in range(len(periods))), - 'has_sublines': False, - }) - - return rslt - - # Build period table - period_table_format = ('(VALUES %s)' % ','.join("(%s, %s, %s)" for period in periods)) - params = list(chain.from_iterable( - (period[0] or None, period[1] or None, i) - for i, period in enumerate(periods) - )) - period_table = SQL(period_table_format, *params) - - # Build query - query = report._get_report_query(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)]) - account_alias = query.left_join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - - always_present_groupby = SQL("period_table.period_index") - if current_groupby: - select_from_groupby = SQL("%s AS grouping_key,", SQL.identifier("account_move_line", current_groupby)) - groupby_clause = SQL("%s, %s", SQL.identifier("account_move_line", current_groupby), always_present_groupby) - else: - select_from_groupby = SQL() - groupby_clause = always_present_groupby - multiplicator = -1 if internal_type == 'liability_payable' else 1 - select_period_query = SQL(',').join( - SQL(""" - CASE WHEN period_table.period_index = %(period_index)s - THEN %(multiplicator)s * SUM(%(balance_select)s) - ELSE 0 END AS %(column_name)s - """, - period_index=i, - multiplicator=multiplicator, - column_name=SQL.identifier(f"period{i}"), - balance_select=report._currency_table_apply_rate(SQL( - "account_move_line.balance - COALESCE(part_debit.amount, 0) + COALESCE(part_credit.amount, 0)" - )), - ) - for i in range(len(periods)) - ) - - tail_query = report._get_engine_query_tail(offset, limit) - query = SQL( - """ - WITH period_table(date_start, date_stop, period_index) AS (%(period_table)s) - - SELECT - %(select_from_groupby)s - %(multiplicator)s * ( - SUM(account_move_line.amount_currency) - - COALESCE(SUM(part_debit.debit_amount_currency), 0) - + COALESCE(SUM(part_credit.credit_amount_currency), 0) - ) AS amount_currency, - ARRAY_AGG(DISTINCT account_move_line.partner_id) AS partner_id, - ARRAY_AGG(account_move_line.payment_id) AS payment_id, - ARRAY_AGG(DISTINCT move.invoice_date) AS invoice_date, - ARRAY_AGG(DISTINCT COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date)) AS report_date, - ARRAY_AGG(DISTINCT %(account_code)s) AS account_name, - ARRAY_AGG(DISTINCT COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date)) AS due_date, - ARRAY_AGG(DISTINCT account_move_line.currency_id) AS currency_id, - COUNT(account_move_line.id) AS aml_count, - ARRAY_AGG(%(account_code)s) AS account_code, - %(select_period_query)s - - FROM %(table_references)s - - JOIN account_journal journal ON journal.id = account_move_line.journal_id - JOIN account_move move ON move.id = account_move_line.move_id - %(currency_table_join)s - - LEFT JOIN LATERAL ( - SELECT - SUM(part.amount) AS amount, - SUM(part.debit_amount_currency) AS debit_amount_currency, - part.debit_move_id - FROM account_partial_reconcile part - WHERE part.max_date <= %(date_to)s AND part.debit_move_id = account_move_line.id - GROUP BY part.debit_move_id - ) part_debit ON TRUE - - LEFT JOIN LATERAL ( - SELECT - SUM(part.amount) AS amount, - SUM(part.credit_amount_currency) AS credit_amount_currency, - part.credit_move_id - FROM account_partial_reconcile part - WHERE part.max_date <= %(date_to)s AND part.credit_move_id = account_move_line.id - GROUP BY part.credit_move_id - ) part_credit ON TRUE - - JOIN period_table ON - ( - period_table.date_start IS NULL - OR COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date) <= DATE(period_table.date_start) - ) - AND - ( - period_table.date_stop IS NULL - OR COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date) >= DATE(period_table.date_stop) - ) - - WHERE %(search_condition)s - - GROUP BY %(groupby_clause)s - - HAVING - ROUND(SUM(%(having_debit)s), %(currency_precision)s) != 0 - OR ROUND(SUM(%(having_credit)s), %(currency_precision)s) != 0 - - ORDER BY %(groupby_clause)s - - %(tail_query)s - """, - account_code=account_code, - period_table=period_table, - select_from_groupby=select_from_groupby, - select_period_query=select_period_query, - multiplicator=multiplicator, - aging_date_field=aging_date_field, - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(options), - date_to=date_to, - search_condition=query.where_clause, - groupby_clause=groupby_clause, - having_debit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance > 0 THEN account_move_line.balance else 0 END - COALESCE(part_debit.amount, 0)")), - having_credit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance < 0 THEN -account_move_line.balance else 0 END - COALESCE(part_credit.amount, 0)")), - currency_precision=self.env.company.currency_id.decimal_places, - tail_query=tail_query, - ) - - self._cr.execute(query) - query_res_lines = self._cr.dictfetchall() - - if not current_groupby: - return build_result_dict(report, query_res_lines) - else: - rslt = [] - - all_res_per_grouping_key = {} - for query_res in query_res_lines: - grouping_key = query_res['grouping_key'] - all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res) - - for grouping_key, query_res_lines in all_res_per_grouping_key.items(): - rslt.append((grouping_key, build_result_dict(report, query_res_lines))) - - return rslt - - def open_journal_items(self, options, params): - params['view_ref'] = 'account.view_move_line_tree_grouped_partner' - options_for_audit = {**options, 'date': {**options['date'], 'date_from': None}} - report = self.env['account.report'].browse(options['report_id']) - action = report.open_journal_items(options=options_for_audit, params=params) - action.get('context', {}).update({'search_default_group_by_account': 0, 'search_default_group_by_partner': 1}) - return action - - def open_partner_ledger(self, options, params): - report = self.env['account.report'].browse(options['report_id']) - record_model, record_id = report._get_model_info_from_id(params.get('line_id')) - return self.env[record_model].browse(record_id).open_partner_ledger() - - def _common_custom_unfold_all_batch_data_generator(self, internal_type, report, options, lines_to_expand_by_function): - rslt = {} # In the form {full_sub_groupby_key: all_column_group_expression_totals for this groupby computation} - report_periods = 6 # The report has 6 periods - - for expand_function_name, lines_to_expand in lines_to_expand_by_function.items(): - for line_to_expand in lines_to_expand: # In standard, this loop will execute only once - if expand_function_name == '_report_expand_unfoldable_line_with_groupby': - report_line_id = report._get_res_id_from_line_id(line_to_expand['id'], 'account.report.line') - expressions_to_evaluate = report.line_ids.expression_ids.filtered(lambda x: x.report_line_id.id == report_line_id and x.engine == 'custom') - - if not expressions_to_evaluate: - continue - - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - # Get all aml results by partner - aml_data_by_partner = {} - for aml_id, aml_result in self._aged_partner_report_custom_engine_common(column_group_options, internal_type, 'id', None): - aml_result['aml_id'] = aml_id - aml_data_by_partner.setdefault(aml_result['partner_id'], []).append(aml_result) - - # Iterate on results by partner to generate the content of the column group - partner_expression_totals = rslt.setdefault(f"[{report_line_id}]=>partner_id", {})\ - .setdefault(column_group_key, {expression: {'value': []} for expression in expressions_to_evaluate}) - for partner_id, aml_data_list in aml_data_by_partner.items(): - partner_values = self._prepare_partner_values() - for i in range(report_periods): - partner_values[f'period{i}'] = 0 - - # Build expression totals under the right key - partner_aml_expression_totals = rslt.setdefault(f"[{report_line_id}]partner_id:{partner_id}=>id", {})\ - .setdefault(column_group_key, {expression: {'value': []} for expression in expressions_to_evaluate}) - for aml_data in aml_data_list: - for i in range(report_periods): - period_value = aml_data[f'period{i}'] - partner_values[f'period{i}'] += period_value - partner_values['total'] += period_value - - for expression in expressions_to_evaluate: - partner_aml_expression_totals[expression]['value'].append( - (aml_data['aml_id'], aml_data[expression.subformula]) - ) - - for expression in expressions_to_evaluate: - partner_expression_totals[expression]['value'].append( - (partner_id, partner_values[expression.subformula]) - ) - - return rslt - - def _prepare_partner_values(self): - return { - 'invoice_date': None, - 'due_date': None, - 'amount_currency': None, - 'currency_id': None, - 'currency': None, - 'account_name': None, - 'total': 0, - } - - def aged_partner_balance_audit(self, options, params, journal_type): - """ Open a list of invoices/bills and/or deferral entries for the clicked cell - :param dict options: the report's `options` - :param dict params: a dict containing: - `calling_line_dict_id`: line id containing the optional account of the cell - `expression_label`: the expression label of the cell - """ - report = self.env['account.report'].browse(options['report_id']) - action = self.env['ir.actions.actions']._for_xml_id('account.action_amounts_to_settle') - journal_type_to_exclude = {'purchase': 'sale', 'sale': 'purchase'} - if options: - domain = [ - ('account_id.reconcile', '=', True), - ('journal_id.type', '!=', journal_type_to_exclude.get(journal_type)), - *self._build_domain_from_period(options, params['expression_label']), - *report._get_options_domain(options, 'from_beginning'), - *report._get_audit_line_groupby_domain(params['calling_line_dict_id']), - ] - action['domain'] = domain - return action - - def _build_domain_from_period(self, options, period): - if period != "total" and period[-1].isdigit(): - period_number = int(period[-1]) - if period_number == 0: - domain = [('date_maturity', '>=', options['date']['date_to'])] - else: - options_date_to = datetime.datetime.strptime(options['date']['date_to'], '%Y-%m-%d') - period_end = options_date_to - datetime.timedelta(30*(period_number-1)+1) - period_start = options_date_to - datetime.timedelta(30*(period_number)) - domain = [('date_maturity', '>=', period_start), ('date_maturity', '<=', period_end)] - if period_number == 5: - domain = [('date_maturity', '<=', period_end)] - else: - domain = [] - return domain - -class AgedPayableCustomHandler(models.AbstractModel): - _name = 'account.aged.payable.report.handler' - _inherit = 'account.aged.partner.balance.report.handler' - _description = 'Aged Payable Custom Handler' - - def open_journal_items(self, options, params): - payable_account_type = {'id': 'trade_payable', 'name': _("Payable"), 'selected': True} - - if 'account_type' in options: - options['account_type'].append(payable_account_type) - else: - options['account_type'] = [payable_account_type] - - return super().open_journal_items(options, params) - - def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function): - # We only optimize the unfold all if the groupby value of the report has not been customized. Else, we'll just run the full computation - if self.env.ref('at_accounting.aged_payable_line').groupby.replace(' ', '') == 'partner_id,id': - return self._common_custom_unfold_all_batch_data_generator('liability_payable', report, options, lines_to_expand_by_function) - return {} - - def action_audit_cell(self, options, params): - return super().aged_partner_balance_audit(options, params, 'purchase') - -class AgedReceivableCustomHandler(models.AbstractModel): - _name = 'account.aged.receivable.report.handler' - _inherit = 'account.aged.partner.balance.report.handler' - _description = 'Aged Receivable Custom Handler' - - def open_journal_items(self, options, params): - receivable_account_type = {'id': 'trade_receivable', 'name': _("Receivable"), 'selected': True} - - if 'account_type' in options: - options['account_type'].append(receivable_account_type) - else: - options['account_type'] = [receivable_account_type] - - return super().open_journal_items(options, params) - - def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function): - # We only optimize the unfold all if the groupby value of the report has not been customized. Else, we'll just run the full computation - if self.env.ref('at_accounting.aged_receivable_line').groupby.replace(' ', '') == 'partner_id,id': - return self._common_custom_unfold_all_batch_data_generator('asset_receivable', report, options, lines_to_expand_by_function) - return {} - - def action_audit_cell(self, options, params): - return super().aged_partner_balance_audit(options, params, 'sale') diff --git a/addons/at_accounting/models/account_analytic_report.py b/addons/at_accounting/models/account_analytic_report.py deleted file mode 100644 index 6ff264d..0000000 --- a/addons/at_accounting/models/account_analytic_report.py +++ /dev/null @@ -1,267 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, fields, api, osv -from odoo.addons.web.controllers.utils import clean_action -from odoo.tools import SQL, Query - - -class AccountReport(models.AbstractModel): - _inherit = 'account.report' - - filter_analytic_groupby = fields.Boolean( - string="Analytic Group By", - compute=lambda x: x._compute_report_option_filter('filter_analytic_groupby'), readonly=False, store=True, depends=['root_report_id'], - ) - - def _get_options_initializers_forced_sequence_map(self): - """ Force the sequence for the init_options so columns headers are already generated but not the columns - So, between _init_options_column_headers and _init_options_columns""" - sequence_map = super(AccountReport, self)._get_options_initializers_forced_sequence_map() - sequence_map[self._init_options_analytic_groupby] = 995 - return sequence_map - - def _init_options_analytic_groupby(self, options, previous_options): - if not self.filter_analytic_groupby: - return - enable_analytic_accounts = self.env.user.has_group('analytic.group_analytic_accounting') - if not enable_analytic_accounts: - return - - options['display_analytic_groupby'] = True - options['display_analytic_plan_groupby'] = True - - options['include_analytic_without_aml'] = previous_options.get('include_analytic_without_aml', False) - previous_analytic_accounts = previous_options.get('analytic_accounts_groupby', []) - analytic_account_ids = [int(x) for x in previous_analytic_accounts] - selected_analytic_accounts = self.env['account.analytic.account'].with_context(active_test=False).search( - [('id', 'in', analytic_account_ids)]) - options['analytic_accounts_groupby'] = selected_analytic_accounts.ids - options['selected_analytic_account_groupby_names'] = selected_analytic_accounts.mapped('name') - - previous_analytic_plans = previous_options.get('analytic_plans_groupby', []) - analytic_plan_ids = [int(x) for x in previous_analytic_plans] - selected_analytic_plans = self.env['account.analytic.plan'].search([('id', 'in', analytic_plan_ids)]) - options['analytic_plans_groupby'] = selected_analytic_plans.ids - options['selected_analytic_plan_groupby_names'] = selected_analytic_plans.mapped('name') - - self._create_column_analytic(options) - - def _init_options_readonly_query(self, options, previous_options): - super()._init_options_readonly_query(options, previous_options) - options['readonly_query'] = options['readonly_query'] and not options.get('analytic_groupby_option') - - def _create_column_analytic(self, options): - """ Creates the analytic columns for each plan or account in the filters. - This will duplicate all previous columns and adding the analytic accounts in the domain of the added columns. - - The analytic_groupby_option is used so the table used is the shadowed table. - The domain on analytic_distribution can just use simple comparison as the column of the shadowed - table will simply be filled with analytic_account_ids. - """ - analytic_headers = [] - plans = self.env['account.analytic.plan'].browse(options.get('analytic_plans_groupby')) - for plan in plans: - account_list = [] - accounts = self.env['account.analytic.account'].search([('plan_id', 'child_of', plan.id)]) - for account in accounts: - account_list.append(account.id) - analytic_headers.append({ - 'name': plan.name, - 'forced_options': { - 'analytic_groupby_option': True, - 'analytic_accounts_list': tuple(account_list), # Analytic accounts used in the domain to filter the lines. - } - }) - - accounts = self.env['account.analytic.account'].browse(options.get('analytic_accounts_groupby')) - for account in accounts: - analytic_headers.append({ - 'name': account.name, - 'forced_options': { - 'analytic_groupby_option': True, - 'analytic_accounts_list': (account.id,), - } - }) - if analytic_headers: - has_selected_budgets = any([budget for budget in options.get('budgets', []) if budget['selected']]) - if has_selected_budgets: - # if budget is selected, then analytic headers are placed on the same header level - options['column_headers'][-1] = analytic_headers + options['column_headers'][-1] - else: - # We add the analytic layer to the column_headers before creating the columns - analytic_headers.append({'name': ''}) - - options['column_headers'] = [ - *options['column_headers'], - analytic_headers, - ] - - @api.model - def _prepare_lines_for_analytic_groupby(self): - """Prepare the analytic_temp_account_move_line - - This method should be used once before all the SQL queries using the - table account_move_line for the analytic columns for the financial reports. - It will create a new table with the schema of account_move_line table, but with - the data from account_analytic_line. - - We inherit the schema of account_move_line, make the correspondence between - account_move_line fields and account_analytic_line fields and put NULL for those - who don't exist in account_analytic_line. - We also drop the NOT NULL constraints for fields who are not required in account_analytic_line. - """ - self.env.cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='analytic_temp_account_move_line'") - if self.env.cr.fetchone(): - return - - project_plan, other_plans = self.env['account.analytic.plan']._get_all_plans() - analytic_cols = SQL(", ").join(SQL('"account_analytic_line".%s', SQL.identifier(n._column_name())) for n in (project_plan + other_plans)) - analytic_distribution_equivalent = SQL('to_jsonb(UNNEST(ARRAY[%s]))', analytic_cols) - - change_equivalence_dict = { - 'id': SQL("account_analytic_line.id"), - 'balance': SQL("-amount"), - 'display_type': 'product', - 'parent_state': 'posted', - 'account_id': SQL.identifier("general_account_id"), - 'debit': SQL("CASE WHEN (amount < 0) THEN -amount else 0 END"), - 'credit': SQL("CASE WHEN (amount > 0) THEN amount else 0 END"), - 'analytic_distribution': analytic_distribution_equivalent, - } - - all_stored_aml_fields = { - field - for field, attrs in self.env['account.move.line'].fields_get().items() - if attrs['type'] not in ['many2many', 'one2many'] and attrs.get('store') - } - - for aml_field in all_stored_aml_fields: - if aml_field not in change_equivalence_dict: - change_equivalence_dict[aml_field] = SQL('"account_move_line".%s', SQL.identifier(aml_field)) - - stored_aml_fields, fields_to_insert = self.env['account.move.line']._prepare_aml_shadowing_for_report(change_equivalence_dict) - - query = SQL(""" - -- Create a temporary table, dropping not null constraints because we're not filling those columns - CREATE TEMPORARY TABLE IF NOT EXISTS analytic_temp_account_move_line () inherits (account_move_line) ON COMMIT DROP; - ALTER TABLE analytic_temp_account_move_line NO INHERIT account_move_line; - ALTER TABLE analytic_temp_account_move_line DROP CONSTRAINT IF EXISTS account_move_line_check_amount_currency_balance_sign; - ALTER TABLE analytic_temp_account_move_line ALTER COLUMN move_id DROP NOT NULL; - ALTER TABLE analytic_temp_account_move_line ALTER COLUMN currency_id DROP NOT NULL; - - INSERT INTO analytic_temp_account_move_line (%(stored_aml_fields)s) - SELECT %(fields_to_insert)s - FROM account_analytic_line - LEFT JOIN account_move_line - ON account_analytic_line.move_line_id = account_move_line.id - WHERE - account_analytic_line.general_account_id IS NOT NULL; - - -- Create a supporting index to avoid seq.scans - CREATE INDEX IF NOT EXISTS analytic_temp_account_move_line__composite_idx ON analytic_temp_account_move_line (analytic_distribution, journal_id, date, company_id); - -- Update statistics for correct planning - ANALYZE analytic_temp_account_move_line - """, stored_aml_fields=stored_aml_fields, fields_to_insert=fields_to_insert) - - self.env.cr.execute(query) - - def _get_report_query(self, options, date_scope, domain=None) -> Query: - # Override to add the context key which will eventually trigger the shadowing of the table - context_self = self.with_context(account_report_analytic_groupby=options.get('analytic_groupby_option')) - - # We add the domain filter for analytic_distribution here, as the search is not available - query = super(AccountReport, context_self)._get_report_query(options, date_scope, domain) - if options.get('analytic_accounts'): - if 'analytic_accounts_list' in options: - # the table will be `analytic_temp_account_move_line` and thus analytic_distribution will be a single ID - analytic_account_ids = tuple(str(account_id) for account_id in options['analytic_accounts']) - query.add_where(SQL("""account_move_line.analytic_distribution IN %s""", analytic_account_ids)) - else: - # Real `account_move_line` table so real JSON with percentage - analytic_account_ids = [[str(account_id) for account_id in options['analytic_accounts']]] - query.add_where(SQL('%s && %s', analytic_account_ids, self.env['account.move.line']._query_analytic_accounts())) - - return query - - def action_audit_cell(self, options, params): - column_group_options = self._get_column_group_options(options, params['column_group_key']) - - if not column_group_options.get('analytic_groupby_option'): - return super(AccountReport, self).action_audit_cell(options, params) - else: - # Start by getting the domain from the options. Note that this domain is targeting account.move.line - report_line = self.env['account.report.line'].browse(params['report_line_id']) - expression = report_line.expression_ids.filtered(lambda x: x.label == params['expression_label']) - line_domain = self._get_audit_line_domain(column_group_options, expression, params) - # The line domain is made for move lines, so we need some postprocessing to have it work with analytic lines. - domain = [] - AccountAnalyticLine = self.env['account.analytic.line'] - for expression in line_domain: - if len(expression) == 1: # For operators such as '&' or '|' we can juste add them again. - domain.append(expression) - continue - - field, operator, right_term = expression - # On analytic lines, the account.account field is named general_account_id and not account_id. - if field.split('.')[0] == 'account_id': - field = field.replace('account_id', 'general_account_id') - expression = [(field, operator, right_term)] - # Replace the 'analytic_distribution' by the account_id domain as we expect for analytic lines. - elif field == 'analytic_distribution': - expression = [('auto_account_id', 'in', right_term)] - # For other fields not present in on the analytic line model, map them to get the info from the move_line. - # Or ignore these conditions if there is no move lines. - elif field.split('.')[0] not in AccountAnalyticLine._fields: - expression = [(f'move_line_id.{field}', operator, right_term)] - if options.get('include_analytic_without_aml'): - expression = osv.expression.OR([ - [('move_line_id', '=', False)], - expression, - ]) - else: - expression = [expression] # just for the extend - domain.extend(expression) - - action = clean_action(self.env.ref('analytic.account_analytic_line_action_entries')._get_action_dict(), env=self.env) - action['domain'] = domain - return action - - @api.model - def _get_options_journals_domain(self, options): - domain = super(AccountReport, self)._get_options_journals_domain(options) - # Add False to the domain in order to select lines without journals for analytics columns. - if options.get('include_analytic_without_aml'): - domain = osv.expression.OR([ - domain, - [('journal_id', '=', False)], - ]) - return domain - - def _get_options_domain(self, options, date_scope): - self.ensure_one() - domain = super()._get_options_domain(options, date_scope) - - # Get the analytic accounts that we need to filter on from the options and add a domain for them. - if 'analytic_accounts_list' in options: - domain = osv.expression.AND([ - domain, - [('analytic_distribution', 'in', options.get('analytic_accounts_list', []))], - ]) - - return domain - - -class AccountMoveLine(models.Model): - _inherit = "account.move.line" - - def _where_calc(self, domain, active_test=True): - """ In case we need an analytic column in an account_report, we shadow the account_move_line table - with a temp table filled with analytic data, that will be used for the analytic columns. - We do it in this function to only create and fill it once for all computations of a report. - The following analytic columns and computations will just query the shadowed table instead of the real one. - """ - query = super()._where_calc(domain, active_test) - if self.env.context.get('account_report_analytic_groupby') and not self.env.context.get('account_report_cash_basis'): - self.env['account.report']._prepare_lines_for_analytic_groupby() - query._tables['account_move_line'] = SQL.identifier('analytic_temp_account_move_line') - return query diff --git a/addons/at_accounting/models/account_asset.py b/addons/at_accounting/models/account_asset.py deleted file mode 100644 index 753d4e5..0000000 --- a/addons/at_accounting/models/account_asset.py +++ /dev/null @@ -1,1726 +0,0 @@ -import psycopg2 -import datetime -from dateutil.relativedelta import relativedelta -from markupsafe import Markup -from math import copysign - -from odoo import api, Command, fields, models, _ -from odoo.exceptions import UserError, ValidationError -from odoo.tools import float_compare, float_is_zero, formatLang -from odoo.tools.date_utils import end_of - -from odoo.tools import format_date, SQL, Query -from collections import defaultdict - - -DAYS_PER_MONTH = 30 -DAYS_PER_YEAR = DAYS_PER_MONTH * 12 -MAX_NAME_LENGTH = 50 - -class AccountAsset(models.Model): - _name = 'account.asset' - _description = 'Asset/Revenue Recognition' - _inherit = ['mail.thread', 'mail.activity.mixin', 'analytic.mixin'] - - depreciation_entries_count = fields.Integer(compute='_compute_counts', string='# Posted Depreciation Entries') - gross_increase_count = fields.Integer(compute='_compute_counts', string='# Gross Increases', help="Number of assets made to increase the value of the asset") - total_depreciation_entries_count = fields.Integer(compute='_compute_counts', string='# Depreciation Entries', help="Number of depreciation entries (posted or not)") - - name = fields.Char(string='Asset Name', compute='_compute_name', store=True, required=True, readonly=False, tracking=True) - company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) - country_code = fields.Char(related='company_id.account_fiscal_country_id.code') - currency_id = fields.Many2one('res.currency', related='company_id.currency_id', store=True) - state = fields.Selection( - selection=[('model', 'Model'), - ('draft', 'Draft'), - ('open', 'Running'), - ('paused', 'On Hold'), - ('close', 'Closed'), - ('cancelled', 'Cancelled')], - string='Status', - copy=False, - default='draft', - readonly=True, - help="When an asset is created, the status is 'Draft'.\n" - "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" - "The 'On Hold' status can be set manually when you want to pause the depreciation of an asset for some time.\n" - "You can manually close an asset when the depreciation is over.\n" - "By cancelling an asset, all depreciation entries will be reversed") - active = fields.Boolean(default=True) - - # Depreciation params - method = fields.Selection( - selection=[ - ('linear', 'Straight Line'), - ('degressive', 'Declining'), - ('degressive_then_linear', 'Declining then Straight Line') - ], - string='Method', - default='linear', - help="Choose the method to use to compute the amount of depreciation lines.\n" - " * Straight Line: Calculated on basis of: Gross Value / Duration\n" - " * Declining: Calculated on basis of: Residual Value * Declining Factor\n" - " * Declining then Straight Line: Like Declining but with a minimum depreciation value equal to the straight line value." - ) - method_number = fields.Integer(string='Duration', default=5, help="The number of depreciations needed to depreciate your asset") - method_period = fields.Selection([('1', 'Months'), ('12', 'Years')], string='Number of Months in a Period', default='12', - help="The amount of time between two depreciations") - method_progress_factor = fields.Float(string='Declining Factor', default=0.3) - prorata_computation_type = fields.Selection( - selection=[ - ('none', 'No Prorata'), - ('constant_periods', 'Constant Periods'), - ('daily_computation', 'Based on days per period'), - ], - string="Computation", - required=True, default='constant_periods', - ) - prorata_date = fields.Date( - string='Prorata Date', - compute='_compute_prorata_date', store=True, readonly=False, - help='Starting date of the period used in the prorata calculation of the first depreciation', - required=True, precompute=True, - copy=True, - ) - paused_prorata_date = fields.Date(compute='_compute_paused_prorata_date') # number of days to shift the computation of future deprecations - account_asset_id = fields.Many2one( - 'account.account', - string='Fixed Asset Account', - compute='_compute_account_asset_id', - help="Account used to record the purchase of the asset at its original price.", - store=True, readonly=False, - check_company=True, - domain="[('account_type', '!=', 'off_balance')]", - ) - asset_group_id = fields.Many2one('account.asset.group', string='Asset Group', tracking=True, index=True) - account_depreciation_id = fields.Many2one( - comodel_name='account.account', - string='Depreciation Account', - check_company=True, - domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card', 'off_balance')), ('deprecated', '=', False)]", - help="Account used in the depreciation entries, to decrease the asset value." - ) - account_depreciation_expense_id = fields.Many2one( - comodel_name='account.account', - string='Expense Account', - check_company=True, - domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card', 'off_balance')), ('deprecated', '=', False)]", - help="Account used in the periodical entries, to record a part of the asset as expense.", - ) - - journal_id = fields.Many2one( - 'account.journal', - string='Journal', - check_company=True, - domain="[('type', '=', 'general')]", - compute='_compute_journal_id', store=True, readonly=False, - ) - - # Values - original_value = fields.Monetary(string="Original Value", compute='_compute_value', store=True, readonly=False) - book_value = fields.Monetary(string='Book Value', readonly=True, compute='_compute_book_value', recursive=True, store=True, help="Sum of the depreciable value, the salvage value and the book value of all value increase items") - value_residual = fields.Monetary(string='Depreciable Value', compute='_compute_value_residual') - salvage_value = fields.Monetary(string='Not Depreciable Value', - help="It is the amount you plan to have that you cannot depreciate.", - compute="_compute_salvage_value", - store=True, readonly=False) - salvage_value_pct = fields.Float(string='Not Depreciable Value Percent', - help="It is the amount you plan to have that you cannot depreciate.") - total_depreciable_value = fields.Monetary(compute='_compute_total_depreciable_value') - gross_increase_value = fields.Monetary(string="Gross Increase Value", compute="_compute_gross_increase_value", compute_sudo=True) - non_deductible_tax_value = fields.Monetary(string="Non Deductible Tax Value", compute="_compute_non_deductible_tax_value", store=True, readonly=True) - related_purchase_value = fields.Monetary(compute='_compute_related_purchase_value') - - # Links with entries - depreciation_move_ids = fields.One2many('account.move', 'asset_id', string='Depreciation Lines') - original_move_line_ids = fields.Many2many('account.move.line', 'asset_move_line_rel', 'asset_id', 'line_id', string='Journal Items', copy=False) - - asset_properties_definition = fields.PropertiesDefinition('Model Properties') - asset_properties = fields.Properties('Properties', definition='model_id.asset_properties_definition', copy=True) - - # Dates - acquisition_date = fields.Date( - compute='_compute_acquisition_date', store=True, precompute=True, - readonly=False, - copy=True, - ) - disposal_date = fields.Date(readonly=False, compute="_compute_disposal_date", store=True) - - # model-related fields - model_id = fields.Many2one('account.asset', string='Model', change_default=True, domain="[('company_id', '=', company_id)]") - account_type = fields.Selection(string="Type of the account", related='account_asset_id.account_type') - display_account_asset_id = fields.Boolean(compute="_compute_display_account_asset_id") - - # Capital gain - parent_id = fields.Many2one('account.asset', help="An asset has a parent when it is the result of gaining value") - children_ids = fields.One2many('account.asset', 'parent_id', help="The children are the gains in value of this asset") - - # Adapt for import fields - already_depreciated_amount_import = fields.Monetary( - help="In case of an import from another software, you might need to use this field to have the right " - "depreciation table report. This is the value that was already depreciated with entries not computed from this model", - ) - - asset_lifetime_days = fields.Float(compute="_compute_lifetime_days", recursive=True) # total number of days to consider for the computation of an asset depreciation board - asset_paused_days = fields.Float(copy=False) - - net_gain_on_sale = fields.Monetary(string="Net gain on sale", help="Net value of gain or loss on sale of an asset", copy=False) - - linked_assets_ids = fields.One2many( - comodel_name='account.asset', - string="Linked Assets", - compute='_compute_linked_assets', - ) - count_linked_asset = fields.Integer(compute="_compute_linked_assets") - warning_count_assets = fields.Boolean(compute="_compute_linked_assets") - - # ------------------------------------------------------------------------- - # COMPUTE METHODS - # ------------------------------------------------------------------------- - @api.depends('company_id') - def _compute_journal_id(self): - for asset in self: - if asset.journal_id and asset.journal_id.company_id == asset.company_id: - asset.journal_id = asset.journal_id - else: - asset.journal_id = self.env['account.journal'].search([ - *self.env['account.journal']._check_company_domain(asset.company_id), - ('type', '=', 'general'), - ], limit=1) - - @api.depends('salvage_value', 'original_value') - def _compute_total_depreciable_value(self): - for asset in self: - asset.total_depreciable_value = asset.original_value - asset.salvage_value - - @api.depends('original_value', 'model_id') - def _compute_salvage_value(self): - for asset in self: - if asset.model_id.salvage_value_pct != 0.0: - asset.salvage_value = asset.original_value * asset.model_id.salvage_value_pct - - @api.depends('depreciation_move_ids.date', 'state') - def _compute_disposal_date(self): - for asset in self: - if asset.state == 'close': - dates = asset.depreciation_move_ids.filtered(lambda m: m.date).mapped('date') - asset.disposal_date = dates and max(dates) - else: - asset.disposal_date = False - - @api.depends('original_move_line_ids', 'original_move_line_ids.account_id', 'non_deductible_tax_value') - def _compute_value(self): - for record in self: - if not record.original_move_line_ids: - record.original_value = record.original_value or False - continue - if any(line.move_id.state == 'draft' for line in record.original_move_line_ids): - raise UserError(_("All the lines should be posted")) - record.original_value = record.related_purchase_value - if record.non_deductible_tax_value: - record.original_value += record.non_deductible_tax_value - - @api.depends('original_move_line_ids') - @api.depends_context('form_view_ref') - def _compute_display_account_asset_id(self): - for record in self: - # Hide the field when creating an asset model from the CoA. (form_view_ref is set from there) - model_from_coa = self.env.context.get('form_view_ref') and record.state == 'model' - record.display_account_asset_id = not record.original_move_line_ids and not model_from_coa - - @api.depends('account_depreciation_id', 'account_depreciation_expense_id', 'original_move_line_ids') - def _compute_account_asset_id(self): - for record in self: - if record.original_move_line_ids: - if len(record.original_move_line_ids.account_id) > 1: - raise UserError(_("All the lines should be from the same account")) - record.account_asset_id = record.original_move_line_ids.account_id - if not record.account_asset_id: - # Only set a default value, do not erase user inputs - record._onchange_account_depreciation_id() - - @api.depends('original_move_line_ids') - def _compute_analytic_distribution(self): - for asset in self: - distribution_asset = {} - amount_total = sum(asset.original_move_line_ids.mapped("balance")) - if not float_is_zero(amount_total, precision_rounding=asset.currency_id.rounding): - for line in asset.original_move_line_ids._origin: - if line.analytic_distribution: - for account, distribution in line.analytic_distribution.items(): - distribution_asset[account] = distribution_asset.get(account, 0) + distribution * line.balance - for account, distribution_amount in distribution_asset.items(): - distribution_asset[account] = distribution_amount / amount_total - asset.analytic_distribution = distribution_asset if distribution_asset else asset.analytic_distribution - - @api.depends('method_number', 'method_period', 'prorata_computation_type') - def _compute_lifetime_days(self): - for asset in self: - if not asset.parent_id: - if asset.prorata_computation_type == 'daily_computation': - asset.asset_lifetime_days = (asset.prorata_date + relativedelta(months=int(asset.method_period) * asset.method_number) - asset.prorata_date).days - else: - asset.asset_lifetime_days = int(asset.method_period) * asset.method_number * DAYS_PER_MONTH - else: - # if it has a parent, we want the asset to only depreciate on the remaining days left of the parent - if asset.prorata_computation_type == 'daily_computation': - parent_end_date = asset.parent_id.paused_prorata_date + relativedelta(days=int(asset.parent_id.asset_lifetime_days - 1)) - else: - parent_end_date = asset.parent_id.paused_prorata_date + relativedelta( - months=int(asset.parent_id.asset_lifetime_days / DAYS_PER_MONTH), - days=int(asset.parent_id.asset_lifetime_days % DAYS_PER_MONTH) - 1 - ) - asset.asset_lifetime_days = asset._get_delta_days(asset.prorata_date, parent_end_date) - - @api.depends('acquisition_date', 'company_id', 'prorata_computation_type') - def _compute_prorata_date(self): - for asset in self: - if asset.prorata_computation_type == 'none' and asset.acquisition_date: - fiscalyear_date = asset.company_id.compute_fiscalyear_dates(asset.acquisition_date).get('date_from') - asset.prorata_date = fiscalyear_date - else: - asset.prorata_date = asset.acquisition_date - - @api.depends('prorata_date', 'prorata_computation_type', 'asset_paused_days') - def _compute_paused_prorata_date(self): - for asset in self: - if asset.prorata_computation_type == 'daily_computation': - asset.paused_prorata_date = asset.prorata_date + relativedelta(days=asset.asset_paused_days) - else: - asset.paused_prorata_date = asset.prorata_date + relativedelta( - months=int(asset.asset_paused_days / DAYS_PER_MONTH), - days=asset.asset_paused_days % DAYS_PER_MONTH - ) - - @api.depends('original_move_line_ids') - def _compute_related_purchase_value(self): - for asset in self: - related_purchase_value = sum(asset.original_move_line_ids.mapped('balance')) - if asset.account_asset_id.multiple_assets_per_line and len(asset.original_move_line_ids) == 1: - related_purchase_value /= max(1, int(asset.original_move_line_ids.quantity)) - asset.related_purchase_value = related_purchase_value - - @api.depends('original_move_line_ids') - def _compute_acquisition_date(self): - for asset in self: - asset.acquisition_date = asset.acquisition_date or min( - [(aml.invoice_date or aml.date) for aml in asset.original_move_line_ids] + [fields.Date.today()] - ) - - @api.depends('original_move_line_ids') - def _compute_name(self): - for record in self: - record.name = record.name or (record.original_move_line_ids and record.original_move_line_ids[0].name or '') - - @api.depends( - 'original_value', 'salvage_value', 'already_depreciated_amount_import', - 'depreciation_move_ids.state', - 'depreciation_move_ids.depreciation_value', - 'depreciation_move_ids.reversal_move_ids' - ) - def _compute_value_residual(self): - for record in self: - posted_depreciation_moves = record.depreciation_move_ids.filtered(lambda mv: mv.state == 'posted') - record.value_residual = ( - record.original_value - - record.salvage_value - - record.already_depreciated_amount_import - - sum(posted_depreciation_moves.mapped('depreciation_value')) - ) - - @api.depends('value_residual', 'salvage_value', 'children_ids.book_value') - def _compute_book_value(self): - for record in self: - record.book_value = record.value_residual + record.salvage_value + sum(record.children_ids.mapped('book_value')) - if record.state == 'close' and all(move.state == 'posted' for move in record.depreciation_move_ids): - record.book_value -= record.salvage_value - - @api.depends('children_ids.original_value') - def _compute_gross_increase_value(self): - for record in self: - record.gross_increase_value = sum(record.children_ids.mapped('original_value')) - - @api.depends('original_move_line_ids') - def _compute_non_deductible_tax_value(self): - for record in self: - record.non_deductible_tax_value = 0.0 - for line in record.original_move_line_ids: - if line.non_deductible_tax_value: - account = line.account_id - auto_create_multi = account.create_asset != 'no' and account.multiple_assets_per_line - quantity = line.quantity if auto_create_multi else 1 - converted_non_deductible_tax_value = line.currency_id._convert(line.non_deductible_tax_value / quantity, record.currency_id, record.company_id, line.date) - record.non_deductible_tax_value += record.currency_id.round(converted_non_deductible_tax_value) - - @api.depends('depreciation_move_ids.state', 'parent_id') - def _compute_counts(self): - depreciation_per_asset = { - group.id: count - for group, count in self.env['account.move']._read_group( - domain=[ - ('asset_id', 'in', self.ids), - ('state', '=', 'posted'), - ], - groupby=['asset_id'], - aggregates=['__count'], - ) - } - for asset in self: - asset.depreciation_entries_count = depreciation_per_asset.get(asset.id, 0) - asset.total_depreciation_entries_count = len(asset.depreciation_move_ids) - asset.gross_increase_count = len(asset.children_ids) - - @api.depends('original_move_line_ids.asset_ids') - def _compute_linked_assets(self): - for asset in self: - asset.linked_assets_ids = asset.original_move_line_ids.asset_ids - self - asset.count_linked_asset = len(asset.linked_assets_ids) - confirmed_assets = asset.linked_assets_ids.filtered(lambda x: x.state == "open") - # The warning_count_assets is useful to put the smart button in red, in case at least one asset has been confirmed - asset.warning_count_assets = len(confirmed_assets) > 0 - - # ------------------------------------------------------------------------- - # ONCHANGE METHODS - # ------------------------------------------------------------------------- - @api.onchange('account_depreciation_id') - def _onchange_account_depreciation_id(self): - if not self.original_move_line_ids: - if not self.account_asset_id and self.state != 'model': - # Only set a default value since it is visible in the form - self.account_asset_id = self.account_depreciation_id - - @api.onchange('original_value', 'original_move_line_ids') - def _display_original_value_warning(self): - if self.original_move_line_ids: - computed_original_value = self.related_purchase_value + self.non_deductible_tax_value - if self.original_value != computed_original_value: - warning = { - 'title': _("Warning for the Original Value of %s", self.name), - 'message': _("The amount you have entered (%(entered_amount)s) does not match the Related Purchase's value (%(purchase_value)s). " - "Please make sure this is what you want.", - entered_amount=formatLang(self.env, self.original_value, currency_obj=self.currency_id), - purchase_value=formatLang(self.env, computed_original_value, currency_obj=self.currency_id)) - } - return {'warning': warning} - - @api.onchange('original_move_line_ids') - def _onchange_original_move_line_ids(self): - # Force the recompute - self.acquisition_date = False - self._compute_acquisition_date() - - @api.onchange('account_asset_id') - def _onchange_account_asset_id(self): - self.account_depreciation_id = self.account_depreciation_id or self.account_asset_id - - @api.onchange('model_id') - def _onchange_model_id(self): - model = self.model_id - if model: - self.method = model.method - self.method_number = model.method_number - self.method_period = model.method_period - self.method_progress_factor = model.method_progress_factor - self.prorata_computation_type = model.prorata_computation_type - self.analytic_distribution = model.analytic_distribution or self.analytic_distribution - self.account_asset_id = model.account_asset_id - self.account_depreciation_id = model.account_depreciation_id - self.account_depreciation_expense_id = model.account_depreciation_expense_id - self.journal_id = model.journal_id - - @api.onchange('original_value', 'salvage_value', 'acquisition_date', 'method', 'method_progress_factor', 'method_period', - 'method_number', 'prorata_computation_type', 'already_depreciated_amount_import', 'prorata_date',) - def onchange_consistent_board(self): - """ When changing the fields that should change the values of the entries, we unlink the entries, so the - depreciation board is not inconsistent with the values of the asset""" - self.write( - {'depreciation_move_ids': [Command.set([])]} - ) - - # ------------------------------------------------------------------------- - # CONSTRAINT METHODS - # ------------------------------------------------------------------------- - @api.constrains('active', 'state') - def _check_active(self): - for record in self: - if not record.active and record.state not in ('close', 'model'): - raise UserError(_('You cannot archive a record that is not closed')) - - @api.constrains('depreciation_move_ids') - def _check_depreciations(self): - for asset in self: - if ( - asset.state == 'open' - and asset.depreciation_move_ids - and not asset.currency_id.is_zero( - asset.depreciation_move_ids.sorted(lambda x: (x.date, x.id))[-1].asset_remaining_value - ) - ): - raise UserError(_("The remaining value on the last depreciation line must be 0")) - - @api.constrains('original_move_line_ids') - def _check_related_purchase(self): - for asset in self: - if asset.original_move_line_ids and asset.related_purchase_value == 0: - raise UserError(_("You cannot create an asset from lines containing credit and debit on the account or with a null amount")) - if asset.state not in ('model', 'draft'): - raise UserError(_("You cannot add or remove bills when the asset is already running or closed.")) - - # ------------------------------------------------------------------------- - # LOW-LEVEL METHODS - # ------------------------------------------------------------------------- - @api.ondelete(at_uninstall=True) - def _unlink_if_model_or_draft(self): - for asset in self: - if asset.state in ['open', 'paused', 'close']: - raise UserError(_( - 'You cannot delete a document that is in %s state.', - dict(self._fields['state']._description_selection(self.env)).get(asset.state) - )) - - posted_amount = len(asset.depreciation_move_ids.filtered(lambda x: x.state == 'posted')) - if posted_amount > 0: - raise UserError(_('You cannot delete an asset linked to posted entries.' - '\nYou should either confirm the asset, then, sell or dispose of it,' - ' or cancel the linked journal entries.')) - - def unlink(self): - for asset in self: - for line in asset.original_move_line_ids: - if line.name: - body = _('A document linked to %(move_line_name)s has been deleted: %(link)s', - move_line_name=line.name, - link=asset._get_html_link(), - ) - else: - body = _('A document linked to this move has been deleted: %s', - asset._get_html_link()) - line.move_id.message_post(body=body) - if len(line.move_id.asset_ids) == 1: - line.move_id.asset_move_type = False - return super(AccountAsset, self).unlink() - - def copy_data(self, default=None): - vals_list = super().copy_data(default) - for asset, vals in zip(self, vals_list): - if asset.state == 'model': - vals['state'] = 'model' - vals['name'] = _('%s (copy)', asset.name) - vals['account_asset_id'] = asset.account_asset_id.id - return vals_list - - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if 'state' in vals and vals['state'] != 'draft' and not (set(vals) - set({'account_depreciation_id', 'account_depreciation_expense_id', 'journal_id'})): - raise UserError(_("Some required values are missing")) - if self._context.get('default_state') != 'model' and vals.get('state') != 'model': - vals['state'] = 'draft' - new_recs = super(AccountAsset, self.with_context(mail_create_nolog=True)).create(vals_list) - # if original_value is passed in vals, make sure the right value is set (as a different original_value may have been computed by _compute_value()) - for i, vals in enumerate(vals_list): - if 'original_value' in vals: - new_recs[i].original_value = vals['original_value'] - if self.env.context.get('original_asset'): - # When original_asset is set, only one asset is created since its from the form view - original_asset = self.env['account.asset'].browse(self.env.context.get('original_asset')) - original_asset.model_id = new_recs - return new_recs - - def write(self, vals): - result = super().write(vals) - for asset in self: - for move in asset.depreciation_move_ids: - if move.state == 'draft' and 'analytic_distribution' in vals: - # Only draft entries to avoid recreating all the analytic items - move.line_ids.analytic_distribution = vals['analytic_distribution'] - lock_date = move.company_id._get_user_fiscal_lock_date(asset.journal_id) - if move.date > lock_date: - if 'account_depreciation_id' in vals: - # ::2 (0, 2, 4, ...) because we want all first lines of the depreciation entries, which corresponds to the - # lines with account_depreciation_id as account - move.line_ids[::2].account_id = vals['account_depreciation_id'] - if 'account_depreciation_expense_id' in vals: - # 1::2 (1, 3, 5, ...) because we want all second lines of the depreciation entries, which corresponds to the - # lines with account_depreciation_expense_id as account - move.line_ids[1::2].account_id = vals['account_depreciation_expense_id'] - if 'journal_id' in vals: - move.journal_id = vals['journal_id'] - return result - - # ------------------------------------------------------------------------- - # BOARD COMPUTATION - # ------------------------------------------------------------------------- - def _get_linear_amount(self, days_before_period, days_until_period_end, total_depreciable_value): - - amount_expected_previous_period = total_depreciable_value * days_before_period / self.asset_lifetime_days - amount_after_expected = total_depreciable_value * days_until_period_end / self.asset_lifetime_days - number_days_for_period = days_until_period_end - days_before_period - # In case of a decrease, we need to lower the amount of the depreciation with the amount of the decrease - # spread over the remaining lifetime - amount_of_decrease_spread_over_period = [ - number_days_for_period * mv.depreciation_value / (self.asset_lifetime_days - self._get_delta_days(self.paused_prorata_date, mv.asset_depreciation_beginning_date)) - for mv in self.depreciation_move_ids.filtered(lambda mv: mv.asset_value_change) - ] - computed_linear_amount = self.currency_id.round(amount_after_expected - self.currency_id.round(amount_expected_previous_period) - sum(amount_of_decrease_spread_over_period)) - return computed_linear_amount - - def _compute_board_amount(self, residual_amount, period_start_date, period_end_date, days_already_depreciated, - days_left_to_depreciated, residual_declining, start_yearly_period=None, total_lifetime_left=None, - residual_at_compute=None, start_recompute_date=None): - - def _get_max_between_linear_and_degressive(linear_amount, effective_start_date=start_yearly_period): - """ - Compute the degressive amount that could be depreciated and returns the biggest between it and linear_amount - The degressive amount corresponds to the difference between what should have been depreciated at the end of - the period and the residual_amount (to deal with rounding issues at the end of each month) - """ - fiscalyear_dates = self.company_id.compute_fiscalyear_dates(period_end_date) - days_in_fiscalyear = self._get_delta_days(fiscalyear_dates['date_from'], fiscalyear_dates['date_to']) - - degressive_total_value = residual_declining * (1 - self.method_progress_factor * self._get_delta_days(effective_start_date, period_end_date) / days_in_fiscalyear) - degressive_amount = residual_amount - degressive_total_value - return self._degressive_linear_amount(residual_amount, degressive_amount, linear_amount) - - if float_is_zero(self.asset_lifetime_days, 2) or float_is_zero(residual_amount, 2): - return 0, 0 - - days_until_period_end = self._get_delta_days(self.paused_prorata_date, period_end_date) - days_before_period = self._get_delta_days(self.paused_prorata_date, period_start_date + relativedelta(days=-1)) - days_before_period = max(days_before_period, 0) # if disposed before the beginning of the asset for example - number_days = days_until_period_end - days_before_period - - # The amount to depreciate are computed by computing how much the asset should be depreciated at the end of the - # period minus how much difference it is actually depreciated. It is done that way to avoid having the last move to take - # every single small difference that could appear over the time with the classic computation method. - if self.method == 'linear': - if total_lifetime_left and float_compare(total_lifetime_left, 0, 2) > 0: - computed_linear_amount = residual_amount - residual_at_compute * (1 - self._get_delta_days(start_recompute_date, period_end_date) / total_lifetime_left) - else: - computed_linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, self.total_depreciable_value) - amount = min(computed_linear_amount, residual_amount, key=abs) - elif self.method == 'degressive': - # Linear amount - # We first calculate the total linear amount for the period left from the beginning of the year - # to get the linear amount for the period in order to avoid big delta at the end of the period - effective_start_date = max(start_yearly_period, self.paused_prorata_date) if start_yearly_period else self.paused_prorata_date - days_left_from_beginning_of_year = self._get_delta_days(effective_start_date, period_start_date - relativedelta(days=1)) + days_left_to_depreciated - expected_remaining_value_with_linear = residual_declining - residual_declining * self._get_delta_days(effective_start_date, period_end_date) / days_left_from_beginning_of_year - linear_amount = residual_amount - expected_remaining_value_with_linear - - amount = _get_max_between_linear_and_degressive(linear_amount, effective_start_date) - elif self.method == 'degressive_then_linear': - if not self.parent_id: - linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, self.total_depreciable_value) - else: - # we want to know the amount before the reeval for the parent so the child can follow the same curve, - # so it transitions from degressive to linear at the same moment - parent_moves = self.parent_id.depreciation_move_ids.filtered(lambda mv: mv.date <= self.prorata_date).sorted(key=lambda mv: (mv.date, mv.id)) - parent_cumulative_depreciation = parent_moves[-1].asset_depreciated_value if parent_moves else self.parent_id.already_depreciated_amount_import - parent_depreciable_value = parent_moves[-1].asset_remaining_value if parent_moves else self.parent_id.total_depreciable_value - if self.currency_id.is_zero(parent_depreciable_value): - linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, self.total_depreciable_value) - else: - # To have the same curve as the parent, we need to have the equivalent amount before the reeval. - # The child's depreciable value corresponds to the amount that is left to depreciate for the parent. - # So, we use the proportion between them to compute the equivalent child's total to depreciate. - # We use it then with the duration of the parent to compute the depreciation amount - depreciable_value = self.total_depreciable_value * (1 + parent_cumulative_depreciation/parent_depreciable_value) - linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, depreciable_value) * self.asset_lifetime_days / self.parent_id.asset_lifetime_days - - amount = _get_max_between_linear_and_degressive(linear_amount) - - amount = max(amount, 0) if self.currency_id.compare_amounts(residual_amount, 0) > 0 else min(amount, 0) - amount = self._get_depreciation_amount_end_of_lifetime(residual_amount, amount, days_until_period_end) - - return number_days, self.currency_id.round(amount) - - def compute_depreciation_board(self, date=False): - # Need to unlink draft moves before adding new ones because if we create new moves before, it will cause an error - self.depreciation_move_ids.filtered(lambda mv: mv.state == 'draft' and (mv.date >= date if date else True)).unlink() - - new_depreciation_moves_data = [] - for asset in self: - new_depreciation_moves_data.extend(asset._recompute_board(date)) - - new_depreciation_moves = self.env['account.move'].create(new_depreciation_moves_data) - new_depreciation_moves_to_post = new_depreciation_moves.filtered(lambda move: move.asset_id.state == 'open') - # In case of the asset is in running mode, we post in the past and set to auto post move in the future - new_depreciation_moves_to_post._post() - - def _recompute_board(self, start_depreciation_date=False): - self.ensure_one() - # All depreciation moves that are posted - posted_depreciation_move_ids = self.depreciation_move_ids.filtered( - lambda mv: mv.state == 'posted' and not mv.asset_value_change - ).sorted(key=lambda mv: (mv.date, mv.id)) - - imported_amount = self.already_depreciated_amount_import - residual_amount = self.value_residual - sum(self.depreciation_move_ids.filtered(lambda mv: mv.state == 'draft').mapped('depreciation_value')) - if not posted_depreciation_move_ids: - residual_amount += imported_amount - residual_declining = residual_at_compute = residual_amount - # start_yearly_period is needed in the 'degressive' and 'degressive_then_linear' methods to compute the amount when the period is monthly - start_recompute_date = start_depreciation_date = start_yearly_period = start_depreciation_date or self.paused_prorata_date - - last_day_asset = self._get_last_day_asset() - final_depreciation_date = self._get_end_period_date(last_day_asset) - total_lifetime_left = self._get_delta_days(start_depreciation_date, last_day_asset) - - depreciation_move_values = [] - if not float_is_zero(self.value_residual, precision_rounding=self.currency_id.rounding): - while not self.currency_id.is_zero(residual_amount) and start_depreciation_date < final_depreciation_date: - period_end_depreciation_date = self._get_end_period_date(start_depreciation_date) - period_end_fiscalyear_date = self.company_id.compute_fiscalyear_dates(period_end_depreciation_date).get('date_to') - lifetime_left = self._get_delta_days(start_depreciation_date, last_day_asset) - - days, amount = self._compute_board_amount(residual_amount, start_depreciation_date, period_end_depreciation_date, False, lifetime_left, residual_declining, start_yearly_period, total_lifetime_left, residual_at_compute, start_recompute_date) - residual_amount -= amount - - if not posted_depreciation_move_ids: - # self.already_depreciated_amount_import management. - # Subtracts the imported amount from the first depreciation moves until we reach it - # (might skip several depreciation entries) - if abs(imported_amount) <= abs(amount): - amount -= imported_amount - imported_amount = 0 - else: - imported_amount -= amount - amount = 0 - - if self.method == 'degressive_then_linear' and final_depreciation_date < period_end_depreciation_date: - period_end_depreciation_date = final_depreciation_date - - if not float_is_zero(amount, precision_rounding=self.currency_id.rounding): - # For deferred revenues, we should invert the amounts. - depreciation_move_values.append(self.env['account.move']._prepare_move_for_asset_depreciation({ - 'amount': amount, - 'asset_id': self, - 'depreciation_beginning_date': start_depreciation_date, - 'date': period_end_depreciation_date, - 'asset_number_days': days, - })) - - if period_end_depreciation_date == period_end_fiscalyear_date: - start_yearly_period = self.company_id.compute_fiscalyear_dates(period_end_depreciation_date).get('date_from') + relativedelta(years=1) - residual_declining = residual_amount - - start_depreciation_date = period_end_depreciation_date + relativedelta(days=1) - - return depreciation_move_values - - def _get_end_period_date(self, start_depreciation_date): - """Get the end of the period in which the depreciation is posted. - - Can be the end of the month if the asset is depreciated monthly, or the end of the fiscal year is it is depreciated yearly. - """ - self.ensure_one() - fiscalyear_date = self.company_id.compute_fiscalyear_dates(start_depreciation_date).get('date_to') - period_end_depreciation_date = fiscalyear_date if start_depreciation_date <= fiscalyear_date else fiscalyear_date + relativedelta(years=1) - - if self.method_period == '1': # If method period is set to monthly computation - max_day_in_month = end_of(datetime.date(start_depreciation_date.year, start_depreciation_date.month, 1), 'month').day - period_end_depreciation_date = min(start_depreciation_date.replace(day=max_day_in_month), period_end_depreciation_date) - return period_end_depreciation_date - - def _get_delta_days(self, start_date, end_date): - """Compute how many days there are between 2 dates. - - The computation is different if the asset is in daily_computation or not. - """ - self.ensure_one() - if self.prorata_computation_type == 'daily_computation': - # Compute how many days there are between 2 dates using a daily_computation method - return (end_date - start_date).days + 1 - else: - # Compute how many days there are between 2 dates counting 30 days per month - # Get how many days there are in the start date month - start_date_days_month = end_of(start_date, 'month').day - # Get how many days there are in the start date month (e.g: June 20th: (30 * (30 - 20 + 1)) / 30 = 11) - start_prorata = (start_date_days_month - start_date.day + 1) / start_date_days_month - # Get how many days there are in the end date month (e.g: You're the August 14th: (14 * 30) / 31 = 13.548387096774194) - end_prorata = end_date.day / end_of(end_date, 'month').day - # Compute how many days there are between these 2 dates - # e.g: 13.548387096774194 + 11 + 360 * (2020 - 2020) + 30 * (8 - 6 - 1) = 24.548387096774194 + 360 * 0 + 30 * 1 = 54.548387096774194 day - return sum(( - start_prorata * DAYS_PER_MONTH, - end_prorata * DAYS_PER_MONTH, - (end_date.year - start_date.year) * DAYS_PER_YEAR, - (end_date.month - start_date.month - 1) * DAYS_PER_MONTH - )) - - def _get_last_day_asset(self): - this = self.parent_id if self.parent_id else self - return this.paused_prorata_date + relativedelta(months=int(this.method_period) * this.method_number, days=-1) - - # ------------------------------------------------------------------------- - # PUBLIC ACTIONS - # ------------------------------------------------------------------------- - - def action_open_linked_assets(self): - action = self.linked_assets_ids.open_asset(['list', 'form']) - action.get('context', {}).update({ - 'from_linked_assets': 0, - }) - return action - - def action_asset_modify(self): - """ Returns an action opening the asset modification wizard. - """ - self.ensure_one() - new_wizard = self.env['asset.modify'].create({ - 'asset_id': self.id, - 'modify_action': 'resume' if self.env.context.get('resume_after_pause') else 'dispose', - }) - return { - 'name': _('Modify Asset'), - 'view_mode': 'form', - 'res_model': 'asset.modify', - 'type': 'ir.actions.act_window', - 'target': 'new', - 'res_id': new_wizard.id, - 'context': self.env.context, - } - - def action_save_model(self): - return { - 'name': _('Save model'), - 'views': [[self.env.ref('at_accountingview_account_asset_form').id, "form"]], - 'res_model': 'account.asset', - 'type': 'ir.actions.act_window', - 'context': { - 'default_state': 'model', - 'default_account_asset_id': self.account_asset_id.id, - 'default_account_depreciation_id': self.account_depreciation_id.id, - 'default_account_depreciation_expense_id': self.account_depreciation_expense_id.id, - 'default_journal_id': self.journal_id.id, - 'default_method': self.method, - 'default_method_number': self.method_number, - 'default_method_period': self.method_period, - 'default_method_progress_factor': self.method_progress_factor, - 'default_prorata_date': self.prorata_date, - 'default_prorata_computation_type': self.prorata_computation_type, - 'default_analytic_distribution': self.analytic_distribution, - 'original_asset': self.id, - } - } - - def open_entries(self): - return { - 'name': _('Journal Entries'), - 'view_mode': 'list,form', - 'res_model': 'account.move', - 'search_view_id': [self.env.ref('account.view_account_move_filter').id, 'search'], - 'views': [(self.env.ref('account.view_move_tree').id, 'list'), (False, 'form')], - 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', self.depreciation_move_ids.ids)], - 'context': dict(self._context, create=False), - } - - def open_related_entries(self): - return { - 'name': _('Journal Items'), - 'view_mode': 'list,form', - 'res_model': 'account.move.line', - 'view_id': False, - 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', self.original_move_line_ids.ids)], - } - - def open_increase(self): - result = { - 'name': _('Gross Increase'), - 'view_mode': 'list,form', - 'res_model': 'account.asset', - 'context': {**self.env.context, 'create': False}, - 'view_id': False, - 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', self.children_ids.ids)], - 'views': [(False, 'list'), (False, 'form')], - } - if len(self.children_ids) == 1: - result['views'] = [(False, 'form')] - result['res_id'] = self.children_ids.id - return result - - def open_parent_id(self): - result = { - 'name': _('Parent Asset'), - 'view_mode': 'form', - 'res_model': 'account.asset', - 'type': 'ir.actions.act_window', - 'res_id': self.parent_id.id, - 'views': [(False, 'form')], - } - return result - - def validate(self): - fields = [ - 'method', - 'method_number', - 'method_period', - 'method_progress_factor', - 'salvage_value', - 'original_move_line_ids', - ] - ref_tracked_fields = self.env['account.asset'].fields_get(fields) - self.write({'state': 'open'}) - for asset in self: - tracked_fields = ref_tracked_fields.copy() - if asset.method == 'linear': - del tracked_fields['method_progress_factor'] - dummy, tracking_value_ids = asset._mail_track(tracked_fields, dict.fromkeys(fields)) - asset_name = (_('Asset created'), _('An asset has been created for this move:')) - msg = asset_name[1] + ' ' + asset._get_html_link() - asset.message_post(body=asset_name[0], tracking_value_ids=tracking_value_ids) - for move_id in asset.original_move_line_ids.mapped('move_id'): - move_id.message_post(body=msg) - try: - if not asset.depreciation_move_ids: - asset.compute_depreciation_board() - asset._check_depreciations() - asset.depreciation_move_ids.filtered(lambda move: move.state != 'posted')._post() - except psycopg2.errors.CheckViolation: - raise ValidationError(_("Atleast one asset (%s) couldn't be set as running because it lacks any required information", asset.name)) - - if asset.account_asset_id.create_asset == 'no': - asset._post_non_deductible_tax_value() - - def set_to_close(self, invoice_line_ids, date=None, message=None): - self.ensure_one() - disposal_date = date or fields.Date.today() - if disposal_date <= self.company_id._get_user_fiscal_lock_date(self.journal_id): - raise UserError(_("You cannot dispose of an asset before the lock date.")) - if invoice_line_ids and self.children_ids.filtered(lambda a: a.state in ('draft', 'open') or a.value_residual > 0): - raise UserError(_("You cannot automate the journal entry for an asset that has a running gross increase. Please use 'Dispose' on the increase(s).")) - full_asset = self + self.children_ids - full_asset.state = 'close' - move_ids = full_asset._get_disposal_moves([invoice_line_ids] * len(full_asset), disposal_date) - for asset in full_asset: - asset.message_post(body= - _('Asset sold. %s', message if message else "") - if invoice_line_ids else - _('Asset disposed. %s', message if message else "") - ) - - selling_price = abs(sum(invoice_line.balance for invoice_line in invoice_line_ids)) - self.net_gain_on_sale = self.currency_id.round(selling_price - self.book_value) - - if move_ids: - name = _('Disposal Move') - view_mode = 'form' - if len(move_ids) > 1: - name = _('Disposal Moves') - view_mode = 'list,form' - return { - 'name': name, - 'view_mode': view_mode, - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'target': 'current', - 'res_id': move_ids[0], - 'domain': [('id', 'in', move_ids)] - } - - def set_to_cancelled(self): - for asset in self: - posted_moves = asset.depreciation_move_ids.filtered(lambda m: ( - not m.reversal_move_ids - and not m.reversed_entry_id - and m.state == 'posted' - )) - if posted_moves: - depreciation_change = sum(posted_moves.line_ids.mapped( - lambda l: l.debit if l.account_id == asset.account_depreciation_expense_id else 0.0 - )) - acc_depreciation_change = sum(posted_moves.line_ids.mapped( - lambda l: l.credit if l.account_id == asset.account_depreciation_id else 0.0 - )) - entries = Markup('
').join(posted_moves.sorted('date').mapped(lambda m: - f'{m.ref} - {m.date} - ' - f'{formatLang(self.env, m.depreciation_value, currency_obj=m.currency_id)} - ' - f'{m.name}' - )) - asset._cancel_future_moves(datetime.date.min) - msg = _('Asset Cancelled') + Markup('
') + \ - _('The account %(exp_acc)s has been credited by %(exp_delta)s, ' - 'while the account %(dep_acc)s has been debited by %(dep_delta)s. ' - 'This corresponds to %(move_count)s cancelled %(word)s:', - exp_acc=asset.account_depreciation_expense_id.display_name, - exp_delta=formatLang(self.env, depreciation_change, currency_obj=asset.currency_id), - dep_acc=asset.account_depreciation_id.display_name, - dep_delta=formatLang(self.env, acc_depreciation_change, currency_obj=asset.currency_id), - move_count=len(posted_moves), - word=_('entries') if len(posted_moves) > 1 else _('entry'), - ) + Markup('
') + entries - asset._message_log(body=msg) - else: - asset._message_log(body=_('Asset Cancelled')) - asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft').with_context(force_delete=True).unlink() - asset.asset_paused_days = 0 - asset.write({'state': 'cancelled'}) - - def set_to_draft(self): - self.write({'state': 'draft'}) - - def set_to_running(self): - if self.depreciation_move_ids and not max(self.depreciation_move_ids, key=lambda m: (m.date, m.id)).asset_remaining_value == 0: - self.env['asset.modify'].create({'asset_id': self.id, 'name': _('Reset to running')}).modify() - self.write({ - 'state': 'open', - 'net_gain_on_sale': 0 - }) - - def resume_after_pause(self): - """ Sets an asset in 'paused' state back to 'open'. - A Depreciation line is created automatically to remove from the - depreciation amount the proportion of time spent - in pause in the current period. - """ - self.ensure_one() - return self.with_context(resume_after_pause=True).action_asset_modify() - - def pause(self, pause_date, message=None): - """ Sets an 'open' asset in 'paused' state, generating first a depreciation - line corresponding to the ratio of time spent within the current depreciation - period before putting the asset in pause. This line and all the previous - unposted ones are then posted. - """ - self.ensure_one() - self._create_move_before_date(pause_date) - self.write({'state': 'paused'}) - self.message_post(body=_("Asset paused. %s", message if message else "")) - - def open_asset(self, view_mode): - if len(self) == 1: - view_mode = ['form'] - views = [v for v in [(False, 'list'), (False, 'form')] if v[1] in view_mode] - ctx = dict(self._context) - ctx.pop('default_move_type', None) - action = { - 'name': _('Asset'), - 'view_mode': ','.join(view_mode), - 'type': 'ir.actions.act_window', - 'res_id': self.id if 'list' not in view_mode else False, - 'res_model': 'account.asset', - 'views': views, - 'domain': [('id', 'in', self.ids)], - 'context': ctx - } - return action - - # ------------------------------------------------------------------------- - # HELPER METHODS - # ------------------------------------------------------------------------- - def _insert_depreciation_line(self, amount, beginning_depreciation_date, depreciation_date, days_depreciated): - """ Inserts a new line in the depreciation board, shifting the sequence of - all the following lines from one unit. - :param amount: The depreciation amount of the new line. - :param label: The name to give to the new line. - :param date: The date to give to the new line. - """ - self.ensure_one() - AccountMove = self.env['account.move'] - - return AccountMove.create(AccountMove._prepare_move_for_asset_depreciation({ - 'amount': amount, - 'asset_id': self, - 'depreciation_beginning_date': beginning_depreciation_date, - 'date': depreciation_date, - 'asset_number_days': days_depreciated, - })) - - def _post_non_deductible_tax_value(self): - # If the asset has a non-deductible tax, the value is posted in the chatter to explain why - # the original value does not match the related purchase(s). - if self.non_deductible_tax_value: - currency = self.env.company.currency_id - msg = _('A non deductible tax value of %(tax_value)s was added to %(name)s\'s initial value of %(purchase_value)s', - tax_value=formatLang(self.env, self.non_deductible_tax_value, currency_obj=currency), - name=self.name, - purchase_value=formatLang(self.env, self.related_purchase_value, currency_obj=currency)) - self.message_post(body=msg) - - def _create_move_before_date(self, date): - """Cancel all the moves after the given date and replace them by a new one. - - The new depreciation/move is depreciating the residual value. - """ - all_move_dates_before_date = (self.depreciation_move_ids.filtered( - lambda x: - x.date <= date - and not x.reversal_move_ids - and not x.reversed_entry_id - and x.state == 'posted' - ).sorted('date')).mapped('date') - - beginning_fiscal_year = self.company_id.compute_fiscalyear_dates(date).get('date_from') if self.method != 'linear' else False - first_fiscalyear_move = self.env['account.move'] - if all_move_dates_before_date: - last_move_date_not_reversed = max(all_move_dates_before_date) - # We don't know when begins the period that the move is supposed to cover - # So, we use the earliest beginning of a move that comes after the last move not cancelled - future_moves_beginning_date = self.depreciation_move_ids.filtered( - lambda m: m.date > last_move_date_not_reversed and ( - not m.reversal_move_ids and not m.reversed_entry_id and m.state == 'posted' - or m.state == 'draft' - ) - ).mapped('asset_depreciation_beginning_date') - beginning_depreciation_date = min(future_moves_beginning_date) if future_moves_beginning_date else self.paused_prorata_date - - if self.method != 'linear': - # In degressive and degressive_then_linear, we need to find the first move of the fiscal year that comes after the last move not cancelled - # in order to correctly compute the moves just before and after the pause date - first_moves = self.depreciation_move_ids.filtered( - lambda m: m.asset_depreciation_beginning_date >= beginning_fiscal_year and ( - not m.reversal_move_ids and not m.reversed_entry_id and m.state == 'posted' - or m.state == 'draft' - ) - ).sorted(lambda m: (m.asset_depreciation_beginning_date, m.id)) - first_fiscalyear_move = next(iter(first_moves), first_fiscalyear_move) - else: - beginning_depreciation_date = self.paused_prorata_date - - residual_declining = first_fiscalyear_move.asset_remaining_value + first_fiscalyear_move.depreciation_value - self._cancel_future_moves(date) - - imported_amount = self.already_depreciated_amount_import if not all_move_dates_before_date else 0 - value_residual = self.value_residual + self.already_depreciated_amount_import if not all_move_dates_before_date else self.value_residual - residual_declining = residual_declining or value_residual - - last_day_asset = self._get_last_day_asset() - lifetime_left = self._get_delta_days(beginning_depreciation_date, last_day_asset) - days_depreciated, amount = self._compute_board_amount(self.value_residual, beginning_depreciation_date, date, False, lifetime_left, residual_declining, beginning_fiscal_year, lifetime_left, value_residual, beginning_depreciation_date) - - if abs(imported_amount) <= abs(amount): - amount -= imported_amount - if not float_is_zero(amount, precision_rounding=self.currency_id.rounding): - new_line = self._insert_depreciation_line(amount, beginning_depreciation_date, date, days_depreciated) - new_line._post() - - def _cancel_future_moves(self, date): - """Cancel all the depreciation entries after the date given as parameter. - - When possible, it will reset those to draft before unlinking them, reverse them otherwise. - - :param date: date after which the moves are deleted/reversed - """ - for asset in self: - obsolete_moves = asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft' or ( - not m.reversal_move_ids - and not m.reversed_entry_id - and m.state == 'posted' - and m.date > date - )) - obsolete_moves._unlink_or_reverse() - - def _get_disposal_moves(self, invoice_lines_list, disposal_date): - """Create the move for the disposal of an asset. - - :param invoice_lines_list: list of recordset of `account.move.line` - Each element of the list corresponds to one record of `self` - These lines are used to generate the disposal move - :param disposal_date: the date of the disposal - """ - def get_line(name, asset, amount, account): - return (0, 0, { - 'name': name, - 'account_id': account.id, - 'balance': -amount, - 'analytic_distribution': analytic_distribution, - 'currency_id': asset.currency_id.id, - 'amount_currency': -asset.company_id.currency_id._convert( - from_amount=amount, - to_currency=asset.currency_id, - company=asset.company_id, - date=disposal_date, - ) - }) - - move_ids = [] - assert len(self) == len(invoice_lines_list) - for asset, invoice_line_ids in zip(self, invoice_lines_list): - asset._create_move_before_date(disposal_date) - - analytic_distribution = asset.analytic_distribution - - dict_invoice = {} - invoice_amount = 0 - - initial_amount = asset.original_value - initial_account = asset.original_move_line_ids.account_id if len(asset.original_move_line_ids.account_id) == 1 else asset.account_asset_id - - all_lines_before_disposal = asset.depreciation_move_ids.filtered(lambda x: x.date <= disposal_date) - depreciated_amount = asset.currency_id.round(copysign( - sum(all_lines_before_disposal.mapped('depreciation_value')) + asset.already_depreciated_amount_import, - -initial_amount, - )) - depreciation_account = asset.account_depreciation_id - for invoice_line in invoice_line_ids: - dict_invoice[invoice_line.account_id] = copysign(invoice_line.balance, -initial_amount) + dict_invoice.get(invoice_line.account_id, 0) - invoice_amount += copysign(invoice_line.balance, -initial_amount) - list_accounts = [(amount, account) for account, amount in dict_invoice.items()] - difference = -initial_amount - depreciated_amount - invoice_amount - difference_account = asset.company_id.gain_account_id if difference > 0 else asset.company_id.loss_account_id - line_datas = [(initial_amount, initial_account), (depreciated_amount, depreciation_account)] + list_accounts + [(difference, difference_account)] - name = _("%(asset)s: Disposal", asset=asset.name) if not invoice_line_ids else _("%(asset)s: Sale", asset=asset.name) - vals = { - 'asset_id': asset.id, - 'ref': name, - 'asset_depreciation_beginning_date': disposal_date, - 'date': disposal_date, - 'journal_id': asset.journal_id.id, - 'move_type': 'entry', - 'asset_move_type': 'disposal' if not invoice_line_ids else 'sale', - 'line_ids': [get_line(name, asset, amount, account) for amount, account in line_datas if account], - } - asset.write({'depreciation_move_ids': [(0, 0, vals)]}) - move_ids += self.env['account.move'].search([('asset_id', '=', asset.id), ('state', '=', 'draft')]).ids - - return move_ids - - def _degressive_linear_amount(self, residual_amount, degressive_amount, linear_amount): - if self.currency_id.compare_amounts(residual_amount, 0) > 0: - return max(degressive_amount, linear_amount) - else: - return min(degressive_amount, linear_amount) - - def _get_depreciation_amount_end_of_lifetime(self, residual_amount, amount, days_until_period_end): - if abs(residual_amount) < abs(amount) or days_until_period_end >= self.asset_lifetime_days: - # If the residual amount is less than the computed amount, we keep the residual amount - # If total_days is greater or equals to asset lifetime days, it should mean that - # the asset will finish in this period and the value for this period is equal to the residual amount. - amount = residual_amount - return amount - - def _get_own_book_value(self, date=None): - self.ensure_one() - return (self._get_residual_value_at_date(date) if date else self.value_residual) + self.salvage_value - - def _get_residual_value_at_date(self, date): - """ Computes the theoretical value of the asset at a specific date. - - :param date: the date at which we want the asset's value - :return: the value at date of the asset without taking reverse entries into account (as it should be in a "normal" flow of the asset) - """ - current_and_previous_depreciation = self.depreciation_move_ids.filtered( - lambda mv: - mv.asset_depreciation_beginning_date < date - and not mv.reversed_entry_id - ).sorted('asset_depreciation_beginning_date', reverse=True) - if not current_and_previous_depreciation: - return 0 - - if len(current_and_previous_depreciation) > 1: - previous_value_residual = current_and_previous_depreciation[1].asset_remaining_value - else: - # If there is only one depreciation, we take the original depreciation value - previous_value_residual = self.original_value - self.salvage_value - self.already_depreciated_amount_import - - # We compare the amount_residuals of the depreciations before and during the given date. - # It applies the ratio of the period (to-given-date / total-days-of-the-period) to the amount of the depreciation. - cur_depr_end_date = self._get_end_period_date(date) - current_depreciation = current_and_previous_depreciation[0] - cur_depr_beg_date = current_depreciation.asset_depreciation_beginning_date - - rate = self._get_delta_days(cur_depr_beg_date, date) / self._get_delta_days(cur_depr_beg_date, cur_depr_end_date) - lost_value_at_date = (previous_value_residual - current_depreciation.asset_remaining_value) * rate - residual_value_at_date = self.currency_id.round(previous_value_residual - lost_value_at_date) - if self.currency_id.compare_amounts(self.original_value, 0) > 0: - return max(residual_value_at_date, 0) - else: - return min(residual_value_at_date, 0) - -class AccountAssetGroup(models.Model): - _name = 'account.asset.group' - _description = 'Asset Group' - _order = 'name' - - name = fields.Char("Name", index="trigram") - company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) - linked_asset_ids = fields.One2many('account.asset', 'asset_group_id', string='Related Assets') - count_linked_assets = fields.Integer(compute='_compute_count_linked_asset') - - @api.depends('linked_asset_ids') - def _compute_count_linked_asset(self): - count_per_asset_group = { - asset_group.id: count - for asset_group, count in self.env['account.asset']._read_group( - domain=[ - ('asset_group_id', 'in', self.ids), - ], - groupby=['asset_group_id'], - aggregates=['__count'], - ) - } - for asset_group in self: - asset_group.count_linked_assets = count_per_asset_group.get(asset_group.id, 0) - - def action_open_linked_assets(self): - self.ensure_one() - return { - 'name': self.name, - 'view_mode': 'list,form', - 'res_model': 'account.asset', - 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', self.linked_asset_ids.ids)], - } - -class AssetsReportCustomHandler(models.AbstractModel): - _name = 'account.asset.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Assets Report Custom Handler' - - def _get_custom_display_config(self): - return { - 'client_css_custom_class': 'depreciation_schedule', - 'templates': { - 'AccountReportFilters': 'at_accountingDepreciationScheduleFilters', - } - } - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - lines, totals_by_column_group = self._generate_report_lines_without_grouping(report, options) - # add the groups by grouping_field - if options['assets_grouping_field'] != 'none': - lines = self._group_by_field(report, lines, options) - else: - lines = report._regroup_lines_by_name_prefix(options, lines, '_report_expand_unfoldable_line_assets_report_prefix_group', 0) - - # add the total line - total_columns = [] - for column_data in options['columns']: - col_value = totals_by_column_group[column_data['column_group_key']].get(column_data['expression_label']) - col_value = col_value if column_data.get('figure_type') == 'monetary' else '' - - total_columns.append(report._build_column_dict(col_value, column_data, options=options)) - - if lines: - lines.append({ - 'id': report._get_generic_line_id(None, None, markup='total'), - 'level': 1, - 'name': _('Total'), - 'columns': total_columns, - 'unfoldable': False, - 'unfolded': False, - }) - - return [(0, line) for line in lines] - - def _generate_report_lines_without_grouping(self, report, options, prefix_to_match=None, parent_id=None, forced_account_id=None): - # construct a dictionary: - # {(account_id, asset_id, asset_group_id): {col_group_key: {expression_label_1: value, expression_label_2: value, ...}}} - all_asset_ids = set() - all_lines_data = {} - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - # the lines returned are already sorted by account_id! - lines_query_results = self._query_lines(column_group_options, prefix_to_match=prefix_to_match, forced_account_id=forced_account_id) - for account_id, asset_id, asset_group_id, cols_by_expr_label in lines_query_results: - line_id = (account_id, asset_id, asset_group_id) - all_asset_ids.add(asset_id) - if line_id not in all_lines_data: - all_lines_data[line_id] = {column_group_key: []} - all_lines_data[line_id][column_group_key] = cols_by_expr_label - - column_names = [ - 'assets_date_from', 'assets_plus', 'assets_minus', 'assets_date_to', 'depre_date_from', - 'depre_plus', 'depre_minus', 'depre_date_to', 'balance' - ] - totals_by_column_group = defaultdict(lambda: dict.fromkeys(column_names, 0.0)) - - # Browse all the necessary assets in one go, to minimize the number of queries - assets_cache = {asset.id: asset for asset in self.env['account.asset'].browse(all_asset_ids)} - - # construct the lines, 1 at a time - lines = [] - company_currency = self.env.company.currency_id - column_expression = self.env['account.report.expression'] - for (account_id, asset_id, asset_group_id), col_group_totals in all_lines_data.items(): - all_columns = [] - for column_data in options['columns']: - col_group_key = column_data['column_group_key'] - expr_label = column_data['expression_label'] - if col_group_key not in col_group_totals or expr_label not in col_group_totals[col_group_key]: - all_columns.append(report._build_column_dict(None, None)) - continue - - col_value = col_group_totals[col_group_key][expr_label] - col_data = None if col_value is None else column_data - - all_columns.append(report._build_column_dict(col_value, col_data, options=options, column_expression=column_expression, currency=company_currency)) - - # add to the total line - if column_data['figure_type'] == 'monetary': - totals_by_column_group[column_data['column_group_key']][column_data['expression_label']] += col_value - - name = assets_cache[asset_id].name - line = { - 'id': report._get_generic_line_id('account.asset', asset_id, parent_line_id=parent_id), - 'level': 2, - 'name': name, - 'columns': all_columns, - 'unfoldable': False, - 'unfolded': False, - 'caret_options': 'account_asset_line', - 'assets_account_id': account_id, - 'assets_asset_group_id': asset_group_id, - } - if parent_id: - line['parent_id'] = parent_id - if len(name) >= MAX_NAME_LENGTH: - line['title_hover'] = name - lines.append(line) - - return lines, totals_by_column_group - - def _caret_options_initializer(self): - # Use 'caret_option_open_record_form' defined in account_reports rather than a custom function - return { - 'account_asset_line': [ - {'name': _("Open Asset"), 'action': 'caret_option_open_record_form'}, - ] - } - - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - column_group_options_map = report._split_options_per_column_group(options) - - for col in options['columns']: - column_group_options = column_group_options_map[col['column_group_key']] - # Dynamic naming of columns containing dates - if col['expression_label'] == 'balance': - col['name'] = '' # The column label will be displayed in the subheader - if col['expression_label'] in ['assets_date_from', 'depre_date_from']: - col['name'] = format_date(self.env, column_group_options['date']['date_from']) - elif col['expression_label'] in ['assets_date_to', 'depre_date_to']: - col['name'] = format_date(self.env, column_group_options['date']['date_to']) - - options['custom_columns_subheaders'] = [ - {"name": _("Characteristics"), "colspan": 4}, - {"name": _("Assets"), "colspan": 4}, - {"name": _("Depreciation"), "colspan": 4}, - {"name": _("Book Value"), "colspan": 1} - ] - - # Group by account by default - options['assets_grouping_field'] = previous_options.get('assets_grouping_field') or 'account_id' - # If group by account is activated, activate the hierarchy (which will group by account group as well) if - # the company has at least one account group, otherwise only group by account - has_account_group = self.env['account.group'].search_count([('company_id', '=', self.env.company.id)], limit=1) - hierarchy_activated = previous_options.get('hierarchy', True) - options['hierarchy'] = has_account_group and hierarchy_activated or False - - def _query_lines(self, options, prefix_to_match=None, forced_account_id=None): - """ - Returns a list of tuples: [(asset_id, account_id, asset_group_id, [{expression_label: value}])] - """ - lines = [] - asset_lines = self._query_values(options, prefix_to_match=prefix_to_match, forced_account_id=forced_account_id) - - # Assign the gross increases sub assets to their main asset (parent) - parent_lines = [] - children_lines = defaultdict(list) - for al in asset_lines: - if al['parent_id']: - children_lines[al['parent_id']] += [al] - else: - parent_lines += [al] - - for al in parent_lines: - - asset_children_lines = children_lines[al['asset_id']] - asset_parent_values = self._get_parent_asset_values(options, al, asset_children_lines) - - # Format the data - columns_by_expr_label = { - "acquisition_date": al["asset_acquisition_date"] and format_date(self.env, al["asset_acquisition_date"]) or "", # Characteristics - "first_depreciation": al["asset_date"] and format_date(self.env, al["asset_date"]) or "", - "method": (al["asset_method"] == "linear" and _("Linear")) or (al["asset_method"] == "degressive" and _("Declining")) or _("Dec. then Straight"), - **asset_parent_values - } - - lines.append((al['account_id'], al['asset_id'], al['asset_group_id'], columns_by_expr_label)) - return lines - - def _get_parent_asset_values(self, options, asset_line, asset_children_lines): - """ Compute the values needed for the depreciation schedule for each parent asset - Overridden in l10n_ro_saft.account_general_ledger""" - - # Compute the depreciation rate string - if asset_line['asset_method'] == 'linear' and asset_line['asset_method_number']: # some assets might have 0 depreciation because they don't lose value - total_months = int(asset_line['asset_method_number']) * int(asset_line['asset_method_period']) - months = total_months % 12 - years = total_months // 12 - asset_depreciation_rate = " ".join(part for part in [ - years and _("%(years)s y", years=years), - months and _("%(months)s m", months=months), - ] if part) - elif asset_line['asset_method'] == 'linear': - asset_depreciation_rate = '0.00 %' - else: - asset_depreciation_rate = ('{:.2f} %').format(float(asset_line['asset_method_progress_factor']) * 100) - - # Manage the opening of the asset - opening = (asset_line['asset_acquisition_date'] or asset_line['asset_date']) < fields.Date.to_date(options['date']['date_from']) - - # Get the main values of the board for the asset - depreciation_opening = asset_line['depreciated_before'] - depreciation_add = asset_line['depreciated_during'] - depreciation_minus = 0.0 - - asset_disposal_value = ( - asset_line['asset_disposal_value'] - if ( - asset_line['asset_disposal_date'] - and asset_line['asset_disposal_date'] <= fields.Date.to_date(options['date']['date_to']) - ) - else 0.0 - ) - - asset_opening = asset_line['asset_original_value'] if opening else 0.0 - asset_add = 0.0 if opening else asset_line['asset_original_value'] - asset_minus = 0.0 - asset_salvage_value = asset_line.get('asset_salvage_value', 0.0) - - # Add the main values of the board for all the sub assets (gross increases) - for child in asset_children_lines: - depreciation_opening += child['depreciated_before'] - depreciation_add += child['depreciated_during'] - - opening = (child['asset_acquisition_date'] or child['asset_date']) < fields.Date.to_date(options['date']['date_from']) - asset_opening += child['asset_original_value'] if opening else 0.0 - asset_add += 0.0 if opening else child['asset_original_value'] - - # Compute the closing values - asset_closing = asset_opening + asset_add - asset_minus - depreciation_closing = depreciation_opening + depreciation_add - depreciation_minus - asset_currency = self.env['res.currency'].browse(asset_line['asset_currency_id']) - - # Manage the closing of the asset - if ( - asset_line['asset_state'] == 'close' - and asset_line['asset_disposal_date'] - and asset_line['asset_disposal_date'] <= fields.Date.to_date(options['date']['date_to']) - and asset_currency.compare_amounts(depreciation_closing, asset_closing - asset_salvage_value) == 0 - ): - depreciation_add -= asset_disposal_value - depreciation_minus += depreciation_closing - asset_disposal_value - depreciation_closing = 0.0 - asset_minus += asset_closing - asset_closing = 0.0 - - # Manage negative assets (credit notes) - if asset_currency.compare_amounts(asset_line['asset_original_value'], 0) < 0: - asset_add, asset_minus = -asset_minus, -asset_add - depreciation_add, depreciation_minus = -depreciation_minus, -depreciation_add - - return { - 'duration_rate': asset_depreciation_rate, - 'asset_disposal_value': asset_disposal_value, - 'assets_date_from': asset_opening, - 'assets_plus': asset_add, - 'assets_minus': asset_minus, - 'assets_date_to': asset_closing, - 'depre_date_from': depreciation_opening, - 'depre_plus': depreciation_add, - 'depre_minus': depreciation_minus, - 'depre_date_to': depreciation_closing, - 'balance': asset_closing - depreciation_closing, - } - - def _group_by_field(self, report, lines, options): - """ - This function adds the grouping lines on top of each group of account.asset - It iterates over the lines, change the line_id of each line to include the account.account.id and the - account.asset.id. - """ - if not lines: - return lines - - line_vals_per_grouping_field_id = {} - parent_model = 'account.account' if options['assets_grouping_field'] == 'account_id' else 'account.asset.group' - for line in lines: - parent_id = line.get('assets_account_id') if options['assets_grouping_field'] == 'account_id' else line.get('assets_asset_group_id') - - model, res_id = report._get_model_info_from_id(line['id']) - - # replace the line['id'] to add the parent id - line['id'] = report._build_line_id([ - (None, parent_model, parent_id), - (None, 'account.asset', res_id) - ]) - - is_parent_in_unfolded_lines = any( - report._get_model_info_from_id(unfolded_line_id) == (parent_model, parent_id) - for unfolded_line_id in options.get('unfolded_lines') - ) - line_vals_per_grouping_field_id.setdefault(parent_id, { - # We don't assign a name to the line yet, so that we can batch the browsing of the parent objects - 'id': report._build_line_id([(None, parent_model, parent_id)]), - 'columns': [], # Filled later - 'unfoldable': True, - 'unfolded': is_parent_in_unfolded_lines or options.get('unfold_all'), - 'level': 1, - - # This value is stored here for convenience; it will be removed from the result - 'group_lines': [], - })['group_lines'].append(line) - - # Generate the result - rslt_lines = [] - idx_monetary_columns = [idx_col for idx_col, col in enumerate(options['columns']) if col['figure_type'] == 'monetary'] - parent_recordset = self.env[parent_model].browse(line_vals_per_grouping_field_id.keys()) - - for parent_field in parent_recordset: - parent_line_vals = line_vals_per_grouping_field_id[parent_field.id] - if options['assets_grouping_field'] == 'account_id': - parent_line_vals['name'] = f"{parent_field.code} {parent_field.name}" - else: - parent_line_vals['name'] = parent_field.name or _('(No %s)', parent_field._description) - - rslt_lines.append(parent_line_vals) - - group_totals = {column_index: 0 for column_index in idx_monetary_columns} - group_lines = report._regroup_lines_by_name_prefix( - options, - parent_line_vals.pop('group_lines'), - '_report_expand_unfoldable_line_assets_report_prefix_group', - parent_line_vals['level'], - parent_line_dict_id=parent_line_vals['id'], - ) - - for parent_subline in group_lines: - # Add this line to the group totals - for column_index in idx_monetary_columns: - group_totals[column_index] += parent_subline['columns'][column_index].get('no_format', 0) - - # Setup the parent and add the line to the result - parent_subline['parent_id'] = parent_line_vals['id'] - rslt_lines.append(parent_subline) - - # Add totals (columns) to the parent line - for column_index in range(len(options['columns'])): - parent_line_vals['columns'].append(report._build_column_dict( - group_totals.get(column_index, ''), - options['columns'][column_index], - options=options, - )) - - return rslt_lines - - def _query_values(self, options, prefix_to_match=None, forced_account_id=None): - "Get the data from the database" - - self.env['account.move.line'].check_access('read') - self.env['account.asset'].check_access('read') - - query = Query(self.env, alias='asset', table=SQL.identifier('account_asset')) - account_alias = query.join(lhs_alias='asset', lhs_column='account_asset_id', rhs_table='account_account', rhs_column='id', link='account_asset_id') - query.add_join('LEFT JOIN', alias='move', table='account_move', condition=SQL(f""" - move.asset_id = asset.id AND move.state {"!= 'cancel'" if options.get('all_entries') else "= 'posted'"} - """)) - - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - account_id = SQL.identifier(account_alias, 'id') - - if prefix_to_match: - query.add_where(SQL("asset.name ILIKE %s", f"{prefix_to_match}%")) - if forced_account_id: - query.add_where(SQL("%s = %s", account_id, forced_account_id)) - - analytic_account_ids = [] - if options.get('analytic_accounts') and not any(x in options.get('analytic_accounts_list', []) for x in options['analytic_accounts']): - analytic_account_ids += [[str(account_id) for account_id in options['analytic_accounts']]] - if options.get('analytic_accounts_list'): - analytic_account_ids += [[str(account_id) for account_id in options.get('analytic_accounts_list')]] - if analytic_account_ids: - query.add_where(SQL('%s && %s', analytic_account_ids, self.env['account.asset']._query_analytic_accounts('asset'))) - - selected_journals = tuple(journal['id'] for journal in options.get('journals', []) if journal['model'] == 'account.journal' and journal['selected']) - if selected_journals: - query.add_where(SQL("asset.journal_id in %s", selected_journals)) - - sql = SQL( - """ - SELECT asset.id AS asset_id, - asset.parent_id AS parent_id, - asset.name AS asset_name, - asset.asset_group_id AS asset_group_id, - asset.original_value AS asset_original_value, - asset.currency_id AS asset_currency_id, - COALESCE(asset.salvage_value, 0) as asset_salvage_value, - MIN(move.date) AS asset_date, - asset.disposal_date AS asset_disposal_date, - asset.acquisition_date AS asset_acquisition_date, - asset.method AS asset_method, - asset.method_number AS asset_method_number, - asset.method_period AS asset_method_period, - asset.method_progress_factor AS asset_method_progress_factor, - asset.state AS asset_state, - asset.company_id AS company_id, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_id)s AS account_id, - COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date < %(date_from)s), 0) + COALESCE(asset.already_depreciated_amount_import, 0) AS depreciated_before, - COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date BETWEEN %(date_from)s AND %(date_to)s), 0) AS depreciated_during, - COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date BETWEEN %(date_from)s AND %(date_to)s AND move.asset_number_days IS NULL), 0) AS asset_disposal_value - FROM %(from_clause)s - WHERE %(where_clause)s - AND asset.company_id in %(company_ids)s - AND (asset.acquisition_date <= %(date_to)s OR move.date <= %(date_to)s) - AND (asset.disposal_date >= %(date_from)s OR asset.disposal_date IS NULL) - AND (asset.state not in ('model', 'draft', 'cancelled') OR (asset.state = 'draft' AND %(include_draft)s)) - AND asset.active = 't' - GROUP BY asset.id, account_id, account_code, account_name - ORDER BY account_code, asset.acquisition_date, asset.id; - """, - account_code=account_code, - account_name=account_name, - account_id=account_id, - date_from=options['date']['date_from'], - date_to=options['date']['date_to'], - from_clause=query.from_clause, - where_clause=query.where_clause or SQL('TRUE'), - company_ids=tuple(self.env['account.report'].get_report_company_ids(options)), - include_draft=options.get('all_entries', False), - ) - - self._cr.execute(sql) - results = self._cr.dictfetchall() - return results - - def _report_expand_unfoldable_line_assets_report_prefix_group(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None): - matched_prefix = self.env['account.report']._get_prefix_groups_matched_prefix_from_line_id(line_dict_id) - report = self.env['account.report'].browse(options['report_id']) - - lines, _totals_by_column_group = self._generate_report_lines_without_grouping( - report, - options, - prefix_to_match=matched_prefix, - parent_id=line_dict_id, - forced_account_id=self.env['account.report']._get_res_id_from_line_id(line_dict_id, 'account.account'), - ) - - lines = report._regroup_lines_by_name_prefix( - options, - lines, - '_report_expand_unfoldable_line_assets_report_prefix_group', - len(matched_prefix), - matched_prefix=matched_prefix, - parent_line_dict_id=line_dict_id, - ) - - return { - 'lines': lines, - 'offset_increment': len(lines), - 'has_more': False, - } - -class AssetsReport(models.Model): - _inherit = 'account.report' - - def _get_caret_option_view_map(self): - view_map = super()._get_caret_option_view_map() - view_map['account.asset.line'] = 'at_accountingview_account_asset_expense_form' - return view_map \ No newline at end of file diff --git a/addons/at_accounting/models/account_bank_statement.py b/addons/at_accounting/models/account_bank_statement.py deleted file mode 100644 index a2bd336..0000000 --- a/addons/at_accounting/models/account_bank_statement.py +++ /dev/null @@ -1,248 +0,0 @@ -import logging - -from odoo import _, api, fields, models -from odoo.addons.base.models.res_bank import sanitize_account_number -from odoo.exceptions import UserError -from odoo.tools import html2plaintext -from odoo.osv import expression -from dateutil.relativedelta import relativedelta -from itertools import product -from lxml import etree -from markupsafe import Markup - -_logger = logging.getLogger(__name__) - -class AccountBankStatement(models.Model): - _name = "account.bank.statement" - _inherit = ['mail.thread.main.attachment', 'account.bank.statement'] - - def action_open_bank_reconcile_widget(self): - self.ensure_one() - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - name=self.name, - default_context={ - 'search_default_statement_id': self.id, - 'search_default_journal_id': self.journal_id.id, - }, - extra_domain=[('statement_id', '=', self.id)] - ) - - def action_generate_attachment(self): - ir_actions_report_sudo = self.env['ir.actions.report'].sudo() - statement_report_action = self.env.ref('account.action_report_account_statement') - for statement in self: - statement_report = statement_report_action.sudo() - content, _content_type = ir_actions_report_sudo._render_qweb_pdf(statement_report, res_ids=statement.ids) - statement.attachment_ids |= self.env['ir.attachment'].create({ - 'name': _("Bank Statement %s.pdf", statement.name) if statement.name else _("Bank Statement.pdf"), - 'type': 'binary', - 'mimetype': 'application/pdf', - 'raw': content, - 'res_model': statement._name, - 'res_id': statement.id, - }) - return statement_report_action.report_action(docids=self) - -class AccountBankStatementLine(models.Model): - _inherit = 'account.bank.statement.line' - - cron_last_check = fields.Datetime() - - def action_save_close(self): - return {'type': 'ir.actions.act_window_close'} - - def action_save_new(self): - action = self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_bank_statement_line_form_bank_rec_widget') - action['context'] = {'default_journal_id': self._context['default_journal_id']} - return action - - #################################################### - # RECONCILIATION PROCESS - #################################################### - - @api.model - def _action_open_bank_reconciliation_widget(self, extra_domain=None, default_context=None, name=None, kanban_first=True): - action_reference = 'at_accounting.action_bank_statement_line_transactions' + ('_kanban' if kanban_first else '') - action = self.env['ir.actions.act_window']._for_xml_id(action_reference) - - action.update({ - 'name': name or _("Bank Reconciliation"), - 'context': default_context or {}, - 'domain': [('state', '!=', 'cancel')] + (extra_domain or []), - }) - - return action - - def action_open_recon_st_line(self): - self.ensure_one() - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - name=self.name, - default_context={ - 'default_statement_id': self.statement_id.id, - 'default_journal_id': self.journal_id.id, - 'default_st_line_id': self.id, - 'search_default_id': self.id, - }, - ) - - def _cron_try_auto_reconcile_statement_lines(self, batch_size=None, limit_time=0): - def _compute_st_lines_to_reconcile(configured_company): - # Find the bank statement lines that are not reconciled and try to reconcile them automatically. - # The ones that are never be processed by the CRON before are processed first. - remaining_line_id = None - limit = batch_size + 1 if batch_size else None - domain = [ - ('is_reconciled', '=', False), - ('create_date', '>', start_time.date() - relativedelta(months=3)), - ('company_id', 'in', configured_company.ids), - ] - st_lines = self.search(domain, limit=limit, order="cron_last_check ASC NULLS FIRST, id") - if batch_size and len(st_lines) > batch_size: - remaining_line_id = st_lines[batch_size].id - st_lines = st_lines[:batch_size] - return st_lines, remaining_line_id - - start_time = fields.Datetime.now() - - configured_company = children_company = self.env['account.reconcile.model'].search_fetch([ - ('auto_reconcile', '=', True), - ('rule_type', 'in', ('writeoff_suggestion', 'invoice_matching')), - ], ['company_id']).company_id - if not configured_company: - return - while children_company := children_company.child_ids: - configured_company += children_company - - st_lines, remaining_line_id = (self, None) if self else _compute_st_lines_to_reconcile(configured_company) - - nb_auto_reconciled_lines = 0 - for index, st_line in enumerate(st_lines): - if limit_time and fields.Datetime.now().timestamp() - start_time.timestamp() > limit_time: - remaining_line_id = st_line.id - st_lines = st_lines[:index] - break - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_trigger_matching_rules() - if wizard.state == 'valid' and wizard.matching_rules_allow_auto_reconcile: - try: - wizard._action_validate() - if st_line.is_reconciled: - st_line.move_id.message_post(body=_( - "This bank transaction has been automatically validated using the reconciliation model '%s'.", - ', '.join(st_line.move_id.line_ids.reconcile_model_id.mapped('name')), - )) - nb_auto_reconciled_lines += 1 - except UserError as e: - _logger.info("Failed to auto reconcile statement line %s due to user error: %s", - st_line.id, - str(e) - ) - continue - - st_lines.write({'cron_last_check': start_time}) - - if remaining_line_id: - remaining_st_line = self.env['account.bank.statement.line'].browse(remaining_line_id) - if nb_auto_reconciled_lines or not remaining_st_line.cron_last_check: - self.env.ref('at_accounting.auto_reconcile_bank_statement_line')._trigger() - - def _retrieve_partner(self): - self.ensure_one() - - - if self.partner_id: - return self.partner_id - - - if self.account_number: - account_number_nums = sanitize_account_number(self.account_number) - if account_number_nums: - domain = [('sanitized_acc_number', 'ilike', account_number_nums)] - for extra_domain in ([('company_id', 'parent_of', self.company_id.id)], [('company_id', '=', False)]): - bank_accounts = self.env['res.partner.bank'].search(extra_domain + domain) - if len(bank_accounts.partner_id) == 1: - return bank_accounts.partner_id - else: - # We have several partner with same account, possibly some archived partner - # so try to filter out inactive partner and if one remains, select this one - bank_accounts = bank_accounts.filtered(lambda bacc: bacc.partner_id.active) - if len(bank_accounts) == 1: - return bank_accounts.partner_id - - - if self.partner_name: - - domains = product( - [ - ('complete_name', '=ilike', self.partner_name), - ('complete_name', 'ilike', self.partner_name), - ], - [ - ('company_id', 'parent_of', self.company_id.id), - ('company_id', '=', False), - ], - ) - for domain in domains: - partner = self.env['res.partner'].search(list(domain) + [('parent_id', '=', False)], limit=2) - if len(partner) == 1: - return partner - # Retrieve the partner from the 'reconcile models'. - rec_models = self.env['account.reconcile.model'].search([ - *self.env['account.reconcile.model']._check_company_domain(self.company_id), - ('rule_type', '!=', 'writeoff_button'), - ]) - for rec_model in rec_models: - partner = rec_model._get_partner_from_mapping(self) - if partner and rec_model._is_applicable_for(self, partner): - return partner - - return self.env['res.partner'] - - def _get_st_line_strings_for_matching(self, allowed_fields=None): - self.ensure_one() - - st_line_text_values = [] - if not allowed_fields or 'payment_ref' in allowed_fields: - if self.payment_ref: - st_line_text_values.append(self.payment_ref) - if not allowed_fields or 'narration' in allowed_fields: - value = html2plaintext(self.narration or "") - if value: - st_line_text_values.append(value) - if not allowed_fields or 'ref' in allowed_fields: - if self.ref: - st_line_text_values.append(self.ref) - return st_line_text_values - - def _get_default_amls_matching_domain(self): - # EXTENDS account - domain = super()._get_default_amls_matching_domain() - - categories = self.env['product.category'].search([ - '|', - ('property_stock_account_input_categ_id', '!=', False), - ('property_stock_account_output_categ_id', '!=', False) - ]) - accounts = (categories.mapped('property_stock_account_input_categ_id') + - categories.mapped('property_stock_account_output_categ_id')) - if accounts: - return expression.AND([domain, [('account_id', 'not in', tuple(set(accounts.ids)))]]) - return domain - - # Ensure transactions can be imported only once (if the import format provides unique transaction ids) - unique_import_id = fields.Char(string='Import ID', readonly=True, copy=False) - - _sql_constraints = [ - ('unique_import_id', 'unique (unique_import_id)', 'A bank account transactions can be imported only once!') - ] - - def _action_open_bank_reconciliation_widget(self, extra_domain=None, default_context=None, name=None, - kanban_first=True): - res = super()._action_open_bank_reconciliation_widget(extra_domain, default_context, name, kanban_first) - res['help'] = Markup("

{}

{}
{}

").format( - _('Nothing to do here!'), - _('No transactions matching your filters were found.'), - _('Click "New" or upload a %s.', - ", ".join(self.env['account.journal']._get_bank_statements_available_import_formats())), - ) - return res \ No newline at end of file diff --git a/addons/at_accounting/models/account_cash_flow_report.py b/addons/at_accounting/models/account_cash_flow_report.py deleted file mode 100644 index 255ff09..0000000 --- a/addons/at_accounting/models/account_cash_flow_report.py +++ /dev/null @@ -1,712 +0,0 @@ -from odoo import models, _ -from odoo.tools import SQL, Query - - -class CashFlowReportCustomHandler(models.AbstractModel): - _name = 'account.cash.flow.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Cash Flow Report Custom Handler' - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - # Compute the cash flow report using the direct method: https://www.investopedia.com/terms/d/direct_method.asp - lines = [] - - layout_data = self._get_layout_data() - report_data = self._get_report_data(report, options, layout_data) - - for layout_line_id, layout_line_data in layout_data.items(): - lines.append((0, self._get_layout_line(report, options, layout_line_id, layout_line_data, report_data))) - - if layout_line_id in report_data and 'aml_groupby_account' in report_data[layout_line_id]: - aml_data_values = report_data[layout_line_id]['aml_groupby_account'].values() - - aml_data_values_with_account_code = [] - aml_data_values_without_account_code = [] - - for aml_data in aml_data_values: - if aml_data['account_code'] is not None: - aml_data_values_with_account_code.append(aml_data) - else: - aml_data_values_without_account_code.append(aml_data) - - for aml_data in (sorted(aml_data_values_with_account_code, key=lambda x: x['account_code']) - + aml_data_values_without_account_code): - lines.append((0, self._get_aml_line(report, options, aml_data))) - - unexplained_difference_line = self._get_unexplained_difference_line(report, options, report_data) - - if unexplained_difference_line: - lines.append((0, unexplained_difference_line)) - - return lines - - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - report._init_options_journals(options, previous_options=previous_options, additional_journals_domain=[('type', 'in', ('bank', 'cash', 'general'))]) - - def _get_report_data(self, report, options, layout_data): - report_data = {} - - payment_account_ids = self._get_account_ids(report, options) - if not payment_account_ids: - return report_data - - # Compute 'Cash and cash equivalents, beginning of period' - for aml_data in self._compute_liquidity_balance(report, options, payment_account_ids, 'to_beginning_of_period'): - self._add_report_data('opening_balance', aml_data, layout_data, report_data) - self._add_report_data('closing_balance', aml_data, layout_data, report_data) - - # Compute 'Cash and cash equivalents, closing balance' - for aml_data in self._compute_liquidity_balance(report, options, payment_account_ids, 'strict_range'): - self._add_report_data('closing_balance', aml_data, layout_data, report_data) - - tags_ids = self._get_tags_ids() - cashflow_tag_ids = self._get_cashflow_tag_ids() - - # Process liquidity moves - for aml_groupby_account in self._get_liquidity_moves(report, options, payment_account_ids, cashflow_tag_ids): - for aml_data in aml_groupby_account.values(): - self._dispatch_aml_data(tags_ids, aml_data, layout_data, report_data) - - # Process reconciled moves - for aml_groupby_account in self._get_reconciled_moves(report, options, payment_account_ids, cashflow_tag_ids): - for aml_data in aml_groupby_account.values(): - self._dispatch_aml_data(tags_ids, aml_data, layout_data, report_data) - - return report_data - - def _add_report_data(self, layout_line_id, aml_data, layout_data, report_data): - """ - Add or update the report_data dictionnary with aml_data. - - report_data is a dictionnary where the keys are keys from _cash_flow_report_get_layout_data() (used for mapping) - and the values can contain 2 dictionnaries: - * (required) 'balance' where the key is the column_group_key and the value is the balance of the line - * (optional) 'aml_groupby_account' where the key is an account_id and the values are the aml data - """ - def _report_update_parent(layout_line_id, aml_column_group_key, aml_balance, layout_data, report_data): - # Update the balance in report_data of the parent of the layout_line_id recursively (Stops when the line has no parent) - if 'parent_line_id' in layout_data[layout_line_id]: - parent_line_id = layout_data[layout_line_id]['parent_line_id'] - - report_data.setdefault(parent_line_id, {'balance': {}}) - report_data[parent_line_id]['balance'].setdefault(aml_column_group_key, 0.0) - report_data[parent_line_id]['balance'][aml_column_group_key] += aml_balance - - _report_update_parent(parent_line_id, aml_column_group_key, aml_balance, layout_data, report_data) - - aml_column_group_key = aml_data['column_group_key'] - aml_account_id = aml_data['account_id'] - aml_account_code = aml_data['account_code'] - aml_account_name = aml_data['account_name'] - aml_balance = aml_data['balance'] - aml_account_tag = aml_data.get('account_tag_id', None) - - if self.env.company.currency_id.is_zero(aml_balance): - return - - report_data.setdefault(layout_line_id, { - 'balance': {}, - 'aml_groupby_account': {}, - }) - - report_data[layout_line_id]['aml_groupby_account'].setdefault(aml_account_id, { - 'parent_line_id': layout_line_id, - 'account_id': aml_account_id, - 'account_code': aml_account_code, - 'account_name': aml_account_name, - 'account_tag_id': aml_account_tag, - 'level': layout_data[layout_line_id]['level'] + 1, - 'balance': {}, - }) - - report_data[layout_line_id]['balance'].setdefault(aml_column_group_key, 0.0) - report_data[layout_line_id]['balance'][aml_column_group_key] += aml_balance - - report_data[layout_line_id]['aml_groupby_account'][aml_account_id]['balance'].setdefault(aml_column_group_key, 0.0) - report_data[layout_line_id]['aml_groupby_account'][aml_account_id]['balance'][aml_column_group_key] += aml_balance - - _report_update_parent(layout_line_id, aml_column_group_key, aml_balance, layout_data, report_data) - - def _get_tags_ids(self): - ''' Get a dict to pass on to _dispatch_aml_data containing information mapping account tags to report lines. ''' - return { - 'operating': self.env.ref('account.account_tag_operating').id, - 'investing': self.env.ref('account.account_tag_investing').id, - 'financing': self.env.ref('account.account_tag_financing').id, - } - - def _get_cashflow_tag_ids(self): - ''' Get the list of account tags that are relevant for the cash flow report. ''' - return self._get_tags_ids().values() - - def _dispatch_aml_data(self, tags_ids, aml_data, layout_data, report_data): - # Dispatch the aml_data in the correct layout_line - if aml_data['account_account_type'] == 'asset_receivable': - self._add_report_data('advance_payments_customer', aml_data, layout_data, report_data) - elif aml_data['account_account_type'] == 'liability_payable': - self._add_report_data('advance_payments_suppliers', aml_data, layout_data, report_data) - elif aml_data['balance'] < 0: - if aml_data['account_tag_id'] == tags_ids['operating']: - self._add_report_data('paid_operating_activities', aml_data, layout_data, report_data) - elif aml_data['account_tag_id'] == tags_ids['investing']: - self._add_report_data('investing_activities_cash_out', aml_data, layout_data, report_data) - elif aml_data['account_tag_id'] == tags_ids['financing']: - self._add_report_data('financing_activities_cash_out', aml_data, layout_data, report_data) - else: - self._add_report_data('unclassified_activities_cash_out', aml_data, layout_data, report_data) - elif aml_data['balance'] > 0: - if aml_data['account_tag_id'] == tags_ids['operating']: - self._add_report_data('received_operating_activities', aml_data, layout_data, report_data) - elif aml_data['account_tag_id'] == tags_ids['investing']: - self._add_report_data('investing_activities_cash_in', aml_data, layout_data, report_data) - elif aml_data['account_tag_id'] == tags_ids['financing']: - self._add_report_data('financing_activities_cash_in', aml_data, layout_data, report_data) - else: - self._add_report_data('unclassified_activities_cash_in', aml_data, layout_data, report_data) - - # ------------------------------------------------------------------------- - # QUERIES - # ------------------------------------------------------------------------- - def _get_account_ids(self, report, options): - ''' Retrieve all accounts to be part of the cash flow statement and also the accounts making them. - - :param options: The report options. - :return: payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal. - ''' - # Fetch liquidity accounts: - # Accounts being used by at least one bank/cash journal. - selected_journal_ids = [j['id'] for j in report._get_options_journals(options)] - - where_clause = "account_journal.id IN %s" if selected_journal_ids else "account_journal.type IN ('bank', 'cash', 'general')" - where_params = [tuple(selected_journal_ids)] if selected_journal_ids else [] - - self._cr.execute(f''' - SELECT - array_remove(ARRAY_AGG(DISTINCT account_account.id), NULL), - array_remove(ARRAY_AGG(DISTINCT account_payment_method_line.payment_account_id), NULL) - FROM account_journal - JOIN res_company - ON account_journal.company_id = res_company.id - LEFT JOIN account_payment_method_line - ON account_journal.id = account_payment_method_line.journal_id - LEFT JOIN account_account - ON account_journal.default_account_id = account_account.id - AND account_account.account_type IN ('asset_cash', 'liability_credit_card') - WHERE {where_clause} - ''', where_params) - - res = self._cr.fetchall()[0] - payment_account_ids = set((res[0] or []) + (res[1] or [])) - - if not payment_account_ids: - return () - - return tuple(payment_account_ids) - - def _get_move_ids_query(self, report, payment_account_ids, column_group_options) -> SQL: - ''' Get all liquidity moves to be part of the cash flow statement. - :param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal. - :return: query: The SQL query to retrieve the move IDs. - ''' - - query = report._get_report_query(column_group_options, 'strict_range', [('account_id', 'in', list(payment_account_ids))]) - return SQL( - ''' - SELECT - array_agg(DISTINCT account_move_line.move_id) AS move_id - FROM %(table_references)s - WHERE %(search_condition)s - ''', - table_references=query.from_clause, - search_condition=query.where_clause, - ) - - def _compute_liquidity_balance(self, report, options, payment_account_ids, date_scope): - ''' Compute the balance of all liquidity accounts to populate the following sections: - 'Cash and cash equivalents, beginning of period' and 'Cash and cash equivalents, closing balance'. - - :param options: The report options. - :param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal. - :return: A list of tuple (account_id, account_code, account_name, balance). - ''' - queries = [] - - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - query = report._get_report_query(column_group_options, date_scope, domain=[('account_id', 'in', payment_account_ids)]) - account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - - queries.append(SQL( - ''' - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.account_id, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - GROUP BY account_move_line.account_id, account_code, account_name - ''', - column_group_key=column_group_key, - account_code=account_code, - account_name=account_name, - table_references=query.from_clause, - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=report._currency_table_aml_join(column_group_options), - search_condition=query.where_clause, - )) - - self._cr.execute(SQL(' UNION ALL ').join(queries)) - - return self._cr.dictfetchall() - - def _get_liquidity_moves(self, report, options, payment_account_ids, cash_flow_tag_ids): - ''' Fetch all information needed to compute lines from liquidity moves. - The difficulty is to represent only the not-reconciled part of balance. - - :param options: The report options. - :param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal. - :return: A list of tuple (account_id, account_code, account_name, account_type, amount). - ''' - - reconciled_aml_groupby_account = {} - - queries = [] - - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - move_ids_query = self._get_move_ids_query(report, payment_account_ids, column_group_options) - query = Query(self.env, 'account_move_line') - account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - account_type = SQL.identifier(account_alias, 'account_type') - - queries.append(SQL( - ''' - (WITH payment_move_ids AS (%(move_ids_query)s) - -- Credit amount of each account - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.account_id, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_type)s AS account_account_type, - account_account_account_tag.account_account_tag_id AS account_tag_id, - SUM(%(partial_amount_select)s) AS balance - FROM %(from_clause)s - %(currency_table_join)s - LEFT JOIN account_partial_reconcile - ON account_partial_reconcile.credit_move_id = account_move_line.id - LEFT JOIN account_account_account_tag - ON account_account_account_tag.account_account_id = account_move_line.account_id - AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s - WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND account_move_line.account_id NOT IN %(payment_account_ids)s - AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s - GROUP BY account_move_line.company_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id - - UNION ALL - - -- Debit amount of each account - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.account_id, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_type)s AS account_account_type, - account_account_account_tag.account_account_tag_id AS account_tag_id, - -SUM(%(partial_amount_select)s) AS balance - FROM %(from_clause)s - %(currency_table_join)s - LEFT JOIN account_partial_reconcile - ON account_partial_reconcile.debit_move_id = account_move_line.id - LEFT JOIN account_account_account_tag - ON account_account_account_tag.account_account_id = account_move_line.account_id - AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s - WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND account_move_line.account_id NOT IN %(payment_account_ids)s - AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s - GROUP BY account_move_line.company_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id - - UNION ALL - - -- Total amount of each account - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.account_id AS account_id, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_type)s AS account_account_type, - account_account_account_tag.account_account_tag_id AS account_tag_id, - SUM(%(aml_balance_select)s) AS balance - FROM %(from_clause)s - %(currency_table_join)s - LEFT JOIN account_account_account_tag - ON account_account_account_tag.account_account_id = account_move_line.account_id - AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s - WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND account_move_line.account_id NOT IN %(payment_account_ids)s - GROUP BY account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id) - ''', - column_group_key=column_group_key, - move_ids_query=move_ids_query, - account_code=account_code, - account_name=account_name, - account_type=account_type, - from_clause=query.from_clause, - currency_table_join=report._currency_table_aml_join(column_group_options), - partial_amount_select=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")), - aml_balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - cash_flow_tag_ids=tuple(cash_flow_tag_ids), - payment_account_ids=payment_account_ids, - date_from=column_group_options['date']['date_from'], - date_to=column_group_options['date']['date_to'], - )) - - self._cr.execute(SQL(' UNION ALL ').join(queries)) - - for aml_data in self._cr.dictfetchall(): - reconciled_aml_groupby_account.setdefault(aml_data['account_id'], {}) - reconciled_aml_groupby_account[aml_data['account_id']].setdefault(aml_data['column_group_key'], { - 'column_group_key': aml_data['column_group_key'], - 'account_id': aml_data['account_id'], - 'account_code': aml_data['account_code'], - 'account_name': aml_data['account_name'], - 'account_account_type': aml_data['account_account_type'], - 'account_tag_id': aml_data['account_tag_id'], - 'balance': 0.0, - }) - - reconciled_aml_groupby_account[aml_data['account_id']][aml_data['column_group_key']]['balance'] -= aml_data['balance'] - - return list(reconciled_aml_groupby_account.values()) - - def _get_reconciled_moves(self, report, options, payment_account_ids, cash_flow_tag_ids): - ''' Retrieve all moves being not a liquidity move to be shown in the cash flow statement. - Each amount must be valued at the percentage of what is actually paid. - E.g. An invoice of 1000 being paid at 50% must be valued at 500. - - :param options: The report options. - :param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal. - :return: A list of tuple (account_id, account_code, account_name, account_type, amount). - ''' - - reconciled_account_ids = {column_group_key: set() for column_group_key in options['column_groups']} - reconciled_percentage_per_move = {column_group_key: {} for column_group_key in options['column_groups']} - currency_table = report._get_currency_table(options) - - queries = [] - - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - move_ids_query = self._get_move_ids_query(report, payment_account_ids, column_group_options) - - queries.append(SQL( - ''' - (WITH payment_move_ids AS (%(move_ids_query)s) - SELECT - %(column_group_key)s AS column_group_key, - debit_line.move_id, - debit_line.account_id, - SUM(%(partial_amount)s) AS balance - FROM account_move_line AS credit_line - LEFT JOIN account_partial_reconcile - ON account_partial_reconcile.credit_move_id = credit_line.id - JOIN %(currency_table)s - ON account_currency_table.company_id = account_partial_reconcile.company_id - AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway - INNER JOIN account_move_line AS debit_line - ON debit_line.id = account_partial_reconcile.debit_move_id - WHERE credit_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND credit_line.account_id NOT IN %(payment_account_ids)s - AND credit_line.credit > 0.0 - AND debit_line.move_id NOT IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s - GROUP BY debit_line.move_id, debit_line.account_id - - UNION ALL - - SELECT - %(column_group_key)s AS column_group_key, - credit_line.move_id, - credit_line.account_id, - -SUM(%(partial_amount)s) AS balance - FROM account_move_line AS debit_line - LEFT JOIN account_partial_reconcile - ON account_partial_reconcile.debit_move_id = debit_line.id - JOIN %(currency_table)s - ON account_currency_table.company_id = account_partial_reconcile.company_id - AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway - INNER JOIN account_move_line AS credit_line - ON credit_line.id = account_partial_reconcile.credit_move_id - WHERE debit_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND debit_line.account_id NOT IN %(payment_account_ids)s - AND debit_line.debit > 0.0 - AND credit_line.move_id NOT IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids) - AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s - GROUP BY credit_line.move_id, credit_line.account_id) - ''', - move_ids_query=move_ids_query, - column_group_key=column_group_key, - payment_account_ids=payment_account_ids, - date_from=column_group_options['date']['date_from'], - date_to=column_group_options['date']['date_to'], - currency_table=currency_table, - partial_amount=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")), - )) - - self._cr.execute(SQL(' UNION ALL ').join(queries)) - - for aml_data in self._cr.dictfetchall(): - reconciled_percentage_per_move[aml_data['column_group_key']].setdefault(aml_data['move_id'], {}) - reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']].setdefault(aml_data['account_id'], [0.0, 0.0]) - reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']][aml_data['account_id']][0] += aml_data['balance'] - - reconciled_account_ids[aml_data['column_group_key']].add(aml_data['account_id']) - - if not reconciled_percentage_per_move: - return [] - - queries = [] - - for column in options['columns']: - queries.append(SQL( - ''' - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.move_id, - account_move_line.account_id, - SUM(%(balance_select)s) AS balance - FROM account_move_line - JOIN %(currency_table)s - ON account_currency_table.company_id = account_move_line.company_id - AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway - WHERE account_move_line.move_id IN %(move_ids)s - AND account_move_line.account_id IN %(account_ids)s - GROUP BY account_move_line.move_id, account_move_line.account_id - ''', - column_group_key=column['column_group_key'], - currency_table=currency_table, - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - move_ids=tuple(reconciled_percentage_per_move[column['column_group_key']].keys()) or (None,), - account_ids=tuple(reconciled_account_ids[column['column_group_key']]) or (None,) - )) - - self._cr.execute(SQL(' UNION ALL ').join(queries)) - - for aml_data in self._cr.dictfetchall(): - if aml_data['account_id'] in reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']]: - reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']][aml_data['account_id']][1] += aml_data['balance'] - - reconciled_aml_per_account = {} - - queries = [] - - query = Query(self.env, 'account_move_line') - account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - account_type = SQL.identifier(account_alias, 'account_type') - - for column in options['columns']: - queries.append(SQL( - ''' - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.move_id, - account_move_line.account_id, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_type)s AS account_account_type, - account_account_account_tag.account_account_tag_id AS account_tag_id, - SUM(%(balance_select)s) AS balance - FROM %(from_clause)s - %(currency_table_join)s - LEFT JOIN account_account_account_tag - ON account_account_account_tag.account_account_id = account_move_line.account_id - AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s - WHERE account_move_line.move_id IN %(move_ids)s - GROUP BY account_move_line.move_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id - ''', - column_group_key=column['column_group_key'], - account_code=account_code, - account_name=account_name, - account_type=account_type, - from_clause=query.from_clause, - currency_table_join=report._currency_table_aml_join(options), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - cash_flow_tag_ids=tuple(cash_flow_tag_ids), - move_ids=tuple(reconciled_percentage_per_move[column['column_group_key']].keys()) or (None,) - )) - - self._cr.execute(SQL(' UNION ALL ').join(queries)) - - for aml_data in self._cr.dictfetchall(): - aml_column_group_key = aml_data['column_group_key'] - aml_move_id = aml_data['move_id'] - aml_account_id = aml_data['account_id'] - aml_account_code = aml_data['account_code'] - aml_account_name = aml_data['account_name'] - aml_account_account_type = aml_data['account_account_type'] - aml_account_tag_id = aml_data['account_tag_id'] - aml_balance = aml_data['balance'] - - # Compute the total reconciled for the whole move. - total_reconciled_amount = 0.0 - total_amount = 0.0 - - for reconciled_amount, amount in reconciled_percentage_per_move[aml_column_group_key][aml_move_id].values(): - total_reconciled_amount += reconciled_amount - total_amount += amount - - # Compute matched percentage for each account. - if total_amount and aml_account_id not in reconciled_percentage_per_move[aml_column_group_key][aml_move_id]: - # Lines being on reconciled moves but not reconciled with any liquidity move must be valued at the - # percentage of what is actually paid. - reconciled_percentage = total_reconciled_amount / total_amount - aml_balance *= reconciled_percentage - elif not total_amount and aml_account_id in reconciled_percentage_per_move[aml_column_group_key][aml_move_id]: - # The total amount to reconcile is 0. In that case, only add entries being on these accounts. Otherwise, - # this special case will lead to an unexplained difference equivalent to the reconciled amount on this - # account. - # E.g: - # - # Liquidity move: - # Account | Debit | Credit - # -------------------------------------- - # Bank | | 100 - # Receivable | 100 | - # - # Reconciled move: <- reconciled_amount=100, total_amount=0.0 - # Account | Debit | Credit - # -------------------------------------- - # Receivable | | 200 - # Receivable | 200 | <- Only the reconciled part of this entry must be added. - aml_balance = -reconciled_percentage_per_move[aml_column_group_key][aml_move_id][aml_account_id][0] - else: - # Others lines are not considered. - continue - - reconciled_aml_per_account.setdefault(aml_account_id, {}) - reconciled_aml_per_account[aml_account_id].setdefault(aml_column_group_key, { - 'column_group_key': aml_column_group_key, - 'account_id': aml_account_id, - 'account_code': aml_account_code, - 'account_name': aml_account_name, - 'account_account_type': aml_account_account_type, - 'account_tag_id': aml_account_tag_id, - 'balance': 0.0, - }) - - reconciled_aml_per_account[aml_account_id][aml_column_group_key]['balance'] -= aml_balance - - return list(reconciled_aml_per_account.values()) - - # ------------------------------------------------------------------------- - # COLUMNS / LINES - # ------------------------------------------------------------------------- - def _get_layout_data(self): - # Indentation of the following dict reflects the structure of the report. - return { - 'opening_balance': {'name': _('Cash and cash equivalents, beginning of period'), 'level': 0}, - 'net_increase': {'name': _('Net increase in cash and cash equivalents'), 'level': 0, 'unfolded': True}, - 'operating_activities': {'name': _('Cash flows from operating activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True}, - 'advance_payments_customer': {'name': _('Advance Payments received from customers'), 'level': 4, 'parent_line_id': 'operating_activities'}, - 'received_operating_activities': {'name': _('Cash received from operating activities'), 'level': 4, 'parent_line_id': 'operating_activities'}, - 'advance_payments_suppliers': {'name': _('Advance payments made to suppliers'), 'level': 4, 'parent_line_id': 'operating_activities'}, - 'paid_operating_activities': {'name': _('Cash paid for operating activities'), 'level': 4, 'parent_line_id': 'operating_activities'}, - 'investing_activities': {'name': _('Cash flows from investing & extraordinary activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True}, - 'investing_activities_cash_in': {'name': _('Cash in'), 'level': 4, 'parent_line_id': 'investing_activities'}, - 'investing_activities_cash_out': {'name': _('Cash out'), 'level': 4, 'parent_line_id': 'investing_activities'}, - 'financing_activities': {'name': _('Cash flows from financing activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True}, - 'financing_activities_cash_in': {'name': _('Cash in'), 'level': 4, 'parent_line_id': 'financing_activities'}, - 'financing_activities_cash_out': {'name': _('Cash out'), 'level': 4, 'parent_line_id': 'financing_activities'}, - 'unclassified_activities': {'name': _('Cash flows from unclassified activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True}, - 'unclassified_activities_cash_in': {'name': _('Cash in'), 'level': 4, 'parent_line_id': 'unclassified_activities'}, - 'unclassified_activities_cash_out': {'name': _('Cash out'), 'level': 4, 'parent_line_id': 'unclassified_activities'}, - 'closing_balance': {'name': _('Cash and cash equivalents, closing balance'), 'level': 0}, - } - - def _get_layout_line(self, report, options, layout_line_id, layout_line_data, report_data): - line_id = report._get_generic_line_id(None, None, markup=layout_line_id) - unfoldable = 'aml_groupby_account' in report_data[layout_line_id] if layout_line_id in report_data else False - - column_values = [] - - for column in options['columns']: - expression_label = column['expression_label'] - column_group_key = column['column_group_key'] - - value = report_data[layout_line_id][expression_label].get(column_group_key, 0.0) if layout_line_id in report_data else 0.0 - - column_values.append(report._build_column_dict(value, column, options=options)) - - return { - 'id': line_id, - 'name': layout_line_data['name'], - 'level': layout_line_data['level'], - 'class': layout_line_data.get('class', ''), - 'columns': column_values, - 'unfoldable': unfoldable, - 'unfolded': line_id in options['unfolded_lines'] or layout_line_data.get('unfolded') or (options.get('unfold_all') and unfoldable), - } - - def _get_aml_line(self, report, options, aml_data): - parent_line_id = report._get_generic_line_id(None, None, aml_data['parent_line_id']) - line_id = report._get_generic_line_id('account.account', aml_data['account_id'], parent_line_id=parent_line_id) - - column_values = [] - - for column in options['columns']: - expression_label = column['expression_label'] - column_group_key = column['column_group_key'] - - value = aml_data[expression_label].get(column_group_key, 0.0) - - column_values.append(report._build_column_dict(value, column, options=options)) - - return { - 'id': line_id, - 'name': f"{aml_data['account_code']} {aml_data['account_name']}" if aml_data['account_code'] else aml_data['account_name'], - 'caret_options': 'account.account', - 'level': aml_data['level'], - 'parent_id': parent_line_id, - 'columns': column_values, - } - - def _get_unexplained_difference_line(self, report, options, report_data): - unexplained_difference = False - column_values = [] - - for column in options['columns']: - expression_label = column['expression_label'] - column_group_key = column['column_group_key'] - - opening_balance = report_data['opening_balance'][expression_label].get(column_group_key, 0.0) if 'opening_balance' in report_data else 0.0 - closing_balance = report_data['closing_balance'][expression_label].get(column_group_key, 0.0) if 'closing_balance' in report_data else 0.0 - net_increase = report_data['net_increase'][expression_label].get(column_group_key, 0.0) if 'net_increase' in report_data else 0.0 - - balance = closing_balance - opening_balance - net_increase - - if not self.env.company.currency_id.is_zero(balance): - unexplained_difference = True - - column_values.append(report._build_column_dict( - balance, - { - 'figure_type': 'monetary', - 'expression_label': 'balance', - }, - options=options, - )) - - if unexplained_difference: - return { - 'id': report._get_generic_line_id(None, None, markup='unexplained_difference'), - 'name': 'Unexplained Difference', - 'level': 1, - 'columns': column_values, - } diff --git a/addons/at_accounting/models/account_chart_template.py b/addons/at_accounting/models/account_chart_template.py deleted file mode 100644 index 8020fd9..0000000 --- a/addons/at_accounting/models/account_chart_template.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo.addons.account.models.chart_template import template -from odoo import models - -class AccountChartTemplate(models.AbstractModel): - _inherit = 'account.chart.template' - - def _get_account_accountant_res_company(self, chart_template): - # Called when installing the Accountant module - company = self.env.company - data = self._get_chart_template_data(chart_template) - company_data = data['res.company'].get(company.id, {}) - - # Pre-reload to ensure the necessary xmlids for the load exist in case they were deleted or not created yet. - required_data = {k: v for k, v in data.items() if k in ['account.journal', 'account.account']} - self._pre_reload_data(company, data['template_data'], required_data) - - return { - company.id: { - 'deferred_expense_journal_id': company.deferred_expense_journal_id.id or company_data.get('deferred_expense_journal_id'), - 'deferred_revenue_journal_id': company.deferred_revenue_journal_id.id or company_data.get('deferred_revenue_journal_id'), - 'deferred_expense_account_id': company.deferred_expense_account_id.id or company_data.get('deferred_expense_account_id'), - 'deferred_revenue_account_id': company.deferred_revenue_account_id.id or company_data.get('deferred_revenue_account_id'), - } - } - - def _get_chart_template_data(self, chart_template): - # OVERRIDE chart template to process the default values for deferred journal and accounts. - - data = super()._get_chart_template_data(chart_template) - - for _company_id, company_data in data['res.company'].items(): - company_data['deferred_expense_journal_id'] = ( - company_data.get('deferred_expense_journal_id') - or next((xid for xid, d in data['account.journal'].items() if d['type'] == 'general'), None) - ) - - company_data['deferred_revenue_journal_id'] = ( - company_data.get('deferred_revenue_journal_id') - or next((xid for xid, d in data['account.journal'].items() if d['type'] == 'general'), None) - ) - - company_data['deferred_expense_account_id'] = ( - company_data.get('deferred_expense_account_id') - or next((xid for xid, d in data['account.account'].items() if d['account_type'] == 'asset_current'), None) - ) - - company_data['deferred_revenue_account_id'] = ( - company_data.get('deferred_revenue_account_id') - or next((xid for xid, d in data['account.account'].items() if d['account_type'] == 'liability_current'), None) - ) - - return data diff --git a/addons/at_accounting/models/account_deferred_reports.py b/addons/at_accounting/models/account_deferred_reports.py deleted file mode 100644 index 7d13bbf..0000000 --- a/addons/at_accounting/models/account_deferred_reports.py +++ /dev/null @@ -1,581 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import calendar -from collections import defaultdict -from dateutil.relativedelta import relativedelta - -from odoo import models, fields, _, api, Command -from odoo.exceptions import UserError -from odoo.tools import groupby, SQL -from odoo.addons.at_accounting.models.account_move import DEFERRED_DATE_MIN, DEFERRED_DATE_MAX - - -class DeferredReportCustomHandler(models.AbstractModel): - _name = 'account.deferred.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Deferred Expense Report Custom Handler' - - def _get_deferred_report_type(self): - raise NotImplementedError("This method is not implemented in the deferred report handler.") - - ############################################ - # DEFERRED COMMON (DISPLAY AND GENERATION) # - ############################################ - - def _get_domain(self, report, options, filter_already_generated=False, filter_not_started=False): - domain = report._get_options_domain(options, "from_beginning") - account_types = ('expense', 'expense_depreciation', 'expense_direct_cost') if self._get_deferred_report_type() == 'expense' else ('income', 'income_other') - domain += [ - ('account_id.account_type', 'in', account_types), - ('deferred_start_date', '!=', False), - ('deferred_end_date', '!=', False), - ('deferred_end_date', '>=', options['date']['date_from']), - ('move_id.date', '<=', options['date']['date_to']), - ] - domain += [ # Exclude if entirely inside the period - '!', '&', '&', '&', '&', '&', - ('deferred_start_date', '>=', options['date']['date_from']), - ('deferred_start_date', '<=', options['date']['date_to']), - ('deferred_end_date', '>=', options['date']['date_from']), - ('deferred_end_date', '<=', options['date']['date_to']), - ('move_id.date', '>=', options['date']['date_from']), - ('move_id.date', '<=', options['date']['date_to']), - ] - if filter_already_generated: - domain += [ - ('deferred_end_date', '>=', options['date']['date_from']), - '!', - '&', - ('move_id.deferred_move_ids.date', '=', options['date']['date_to']), - ('move_id.deferred_move_ids.state', '=', 'posted'), - ] - if filter_not_started: - domain += [('deferred_start_date', '>', options['date']['date_to'])] - return domain - - @api.model - def _get_select(self): - account_name = self.env['account.account']._field_to_sql('account_move_line__account_id', 'name') - return [ - SQL("account_move_line.id AS line_id"), - SQL("account_move_line.account_id AS account_id"), - SQL("account_move_line.partner_id AS partner_id"), - SQL("account_move_line.product_id AS product_id"), - SQL("account_move_line__product_template_id.categ_id AS product_category_id"), - SQL("account_move_line.name AS line_name"), - SQL("account_move_line.deferred_start_date AS deferred_start_date"), - SQL("account_move_line.deferred_end_date AS deferred_end_date"), - SQL("account_move_line.deferred_end_date - account_move_line.deferred_start_date AS diff_days"), - SQL("account_move_line.balance AS balance"), - SQL("account_move_line.analytic_distribution AS analytic_distribution"), - SQL("account_move_line__move_id.id as move_id"), - SQL("account_move_line__move_id.name AS move_name"), - SQL("%s AS account_name", account_name), - ] - - def _get_lines(self, report, options, filter_already_generated=False): - domain = self._get_domain(report, options, filter_already_generated) - query = report._get_report_query(options, domain=domain, date_scope='from_beginning') - select_clause = SQL(', ').join(self._get_select()) - - query = SQL( - """ - SELECT %(select_clause)s - FROM %(table_references)s - LEFT JOIN product_product AS account_move_line__product_id ON account_move_line.product_id = account_move_line__product_id.id - LEFT JOIN product_template AS account_move_line__product_template_id ON account_move_line__product_id.product_tmpl_id = account_move_line__product_template_id.id - WHERE %(search_condition)s - ORDER BY account_move_line.deferred_start_date, account_move_line.id - """, - select_clause=select_clause, - table_references=query.from_clause, - search_condition=query.where_clause, - ) - - self.env.cr.execute(query) - res = self.env.cr.dictfetchall() - return res - - @api.model - def _get_grouping_fields_deferred_lines(self, filter_already_generated=False, grouping_field='account_id'): - return (grouping_field,) - - @api.model - def _group_by_deferred_fields(self, line, filter_already_generated=False, grouping_field='account_id'): - return tuple(line[k] for k in self._get_grouping_fields_deferred_lines(filter_already_generated, grouping_field)) - - @api.model - def _get_grouping_fields_deferral_lines(self): - return () - - @api.model - def _group_by_deferral_fields(self, line): - return tuple(line[k] for k in self._get_grouping_fields_deferral_lines()) - - @api.model - def _group_deferred_amounts_by_grouping_field(self, deferred_amounts_by_line, periods, is_reverse, filter_already_generated=False, grouping_field='account_id'): - """ - Groups the deferred amounts by account and computes the totals for each account for each period. - And the total for all accounts for each period. - E.g. (where period1 = (date1, date2, label1), period2 = (date2, date3, label2), ...) - { - self._get_grouping_keys_deferred_lines(): { - 'account_id': account1, 'amount_total': 600, period_1: 200, period_2: 400 - }, - self._get_grouping_keys_deferred_lines(): { - 'account_id': account2, 'amount_total': 700, period_1: 300, period_2: 400 - }, - }, {'totals_aggregated': 1300, period_1: 500, period_2: 800} - """ - deferred_amounts_by_line = groupby(deferred_amounts_by_line, key=lambda x: self._group_by_deferred_fields(x, filter_already_generated, grouping_field)) - totals_per_key = {} # {key: {**self._get_grouping_fields_deferral_lines(), total, before, current, later}} - totals_aggregated_by_period = {period: 0 for period in periods + ['totals_aggregated']} - sign = 1 if is_reverse else -1 - for key, lines_per_key in deferred_amounts_by_line: - lines_per_key = list(lines_per_key) - current_key_totals = self._get_current_key_totals_dict(lines_per_key, sign) - totals_aggregated_by_period['totals_aggregated'] += current_key_totals['amount_total'] - for period in periods: - current_key_totals[period] = sign * sum(line[period] for line in lines_per_key) - totals_aggregated_by_period[period] += self.env.company.currency_id.round(current_key_totals[period]) - totals_per_key[key] = current_key_totals - return totals_per_key, totals_aggregated_by_period - - ########################### - # DEFERRED REPORT DISPLAY # - ########################### - - def _get_custom_display_config(self): - return { - 'templates': { - 'AccountReportFilters': 'at_accounting.DeferredFilters', - }, - } - - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - - options_per_col_group = report._split_options_per_column_group(options) - for column_dict in options['columns']: - column_options = options_per_col_group[column_dict['column_group_key']] - column_dict['name'] = column_options['date']['string'] - column_dict['date_from'] = column_options['date']['date_from'] - column_dict['date_to'] = column_options['date']['date_to'] - - options['columns'] = list(reversed(options['columns'])) - total_column = [{ - **options['columns'][0], - 'name': _('Total'), - 'expression_label': 'total', - 'date_from': DEFERRED_DATE_MIN, - 'date_to': DEFERRED_DATE_MAX, - }] - not_started_column = [{ - **options['columns'][0], - 'name': _('Not Started'), - 'expression_label': 'not_started', - 'date_from': options['columns'][-1]['date_to'], - 'date_to': DEFERRED_DATE_MAX, - }] - before_column = [{ - **options['columns'][0], - 'name': _('Before'), - 'expression_label': 'before', - 'date_from': DEFERRED_DATE_MIN, - 'date_to': options['columns'][0]['date_from'], - }] - later_column = [{ - **options['columns'][0], - 'name': _('Later'), - 'expression_label': 'later', - 'date_from': options['columns'][-1]['date_to'], - 'date_to': DEFERRED_DATE_MAX, - }] - options['columns'] = total_column + not_started_column + before_column + options['columns'] + later_column - options['column_headers'] = [] - options['deferred_report_type'] = self._get_deferred_report_type() - options['deferred_grouping_field'] = previous_options.get('deferred_grouping_field') or 'account_id' - if ( - self._get_deferred_report_type() == 'expense' and self.env.company.generate_deferred_expense_entries_method == 'manual' - or self._get_deferred_report_type() == 'revenue' and self.env.company.generate_deferred_revenue_entries_method == 'manual' - ): - options['buttons'].append({'name': _('Generate entry'), 'action': 'action_generate_entry', 'sequence': 80, 'always_show': True}) - - def action_audit_cell(self, options, params): - """ Open a list of invoices/bills and/or deferral entries for the clicked cell in a deferred report. - - Specifically, we show the following lines, grouped by their journal entry, filtered by the column date bounds: - - Total: Lines of all invoices/bills being deferred in the current period - - Not Started: Lines of all deferral entries for which the original invoice/bill date is before or in the - current period, but the deferral only starts after the current period, as well as the lines of - their original invoices/bills - - Before: Lines of all deferral entries with a date before the current period, created by invoices/bills also - being deferred in the current period, as well as the lines of their original invoices/bills - - Current: Lines of all deferral entries in the current period, as well as these of their original - invoices/bills - - Later: Lines of all deferral entries with a date after the current period, created by invoices/bills also - being deferred in the current period, as well as the lines of their original invoices/bills - - :param dict options: the report's `options` - :param dict params: a dict containing: - `calling_line_dict_id`: line id containing the optional account of the cell - `column_group_id`: the column group id of the cell - `expression_label`: the expression label of the cell - """ - report = self.env['account.report'].browse(options['report_id']) - column_values = next( - (column for column in options['columns'] if ( - column['column_group_key'] == params.get('column_group_key') - and column['expression_label'] == params.get('expression_label') - )), - None - ) - if not column_values: - return - - column_date_from = fields.Date.to_date(column_values['date_from']) - column_date_to = fields.Date.to_date(column_values['date_to']) - report_date_from = fields.Date.to_date(options['date']['date_from']) - report_date_to = fields.Date.to_date(options['date']['date_to']) - - # Corrections for comparisons - if column_values['expression_label'] in ('not_started', 'later'): - # Not Started and Later period start one day after `report_date_to` - column_date_from = report_date_to + relativedelta(days=1) - if column_values['expression_label'] == 'before': - # Before period ends one day before `report_date_from` - column_date_to = report_date_from - relativedelta(days=1) - - # calling_line_dict_id is of the format `~account.report~15|~account.account~25` - _grouping_model, grouping_record_id = report._get_model_info_from_id(params.get('calling_line_dict_id')) - - # Find the original lines to be deferred in the report period - original_move_lines_domain = self._get_domain( - report, options, filter_not_started=column_values['expression_label'] == 'not_started' - ) - if grouping_record_id: - # We're auditing a specific account, so we only want moves containing this account - original_move_lines_domain.append((options['deferred_grouping_field'], '=', grouping_record_id)) - # We're getting all lines from the concerned moves. They are filtered later for flexibility. - original_move = self.env['account.move.line'].search(original_move_lines_domain).move_id - - # For the Total period only show the original move lines - line_ids = original_move.line_ids.ids - - # Show both the original move lines and deferral move lines for all other periods - if not column_values['expression_label'] == 'total': - line_ids += original_move.deferred_move_ids.line_ids.ids - - return { - 'type': 'ir.actions.act_window', - 'name': _('Deferred Entries'), - 'res_model': 'account.move.line', - 'domain': [('id', 'in', line_ids)], - 'views': [(self.env.ref('at_accounting.view_deferred_entries_tree').id, 'list')], - # Most filters are set here to allow auditing flexibility to the user - 'context': { - 'search_default_pl_accounts': True, - f'search_default_{options["deferred_grouping_field"]}': grouping_record_id, - 'date_from': column_date_from, - 'date_to': column_date_to, - 'search_default_date_between': True, - 'expand': True, - } - } - - def _caret_options_initializer(self): - return { - 'deferred_caret': [ - {'name': _("Journal Items"), 'action': 'open_journal_items'}, - ], - } - - def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings): - already_generated = ( - ( - self._get_deferred_report_type() == 'expense' and self.env.company.generate_deferred_expense_entries_method == 'manual' - or self._get_deferred_report_type() == 'revenue' and self.env.company.generate_deferred_revenue_entries_method == 'manual' - ) - and self.env['account.move'].search_count( - report._get_generated_deferral_entries_domain(options) - ) - ) - if already_generated: - warnings['at_accounting.deferred_report_warning_already_posted'] = {'alert_type': 'warning'} - - def open_journal_items(self, options, params): - report = self.env['account.report'].browse(options['report_id']) - record_model, record_id = report._get_model_info_from_id(params.get('line_id')) - domain = self._get_domain(report, options) - if record_model == 'account.account' and record_id: - domain += [('account_id', '=', record_id)] - elif record_model == 'product.product' and record_id: - domain += [('product_id', '=', record_id)] - elif record_model == 'product.category' and record_id: - domain += [('product_category_id', '=', record_id)] - return { - 'type': 'ir.actions.act_window', - 'name': _("Deferred Entries"), - 'res_model': 'account.move.line', - 'domain': domain, - 'views': [(self.env.ref('at_accounting.view_deferred_entries_tree').id, 'list')], - 'context': { - 'search_default_group_by_move': True, - 'expand': True, - } - } - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - def get_columns(totals): - return [ - { - **report._build_column_dict( - totals[( - fields.Date.to_date(column['date_from']), - fields.Date.to_date(column['date_to']), - column['expression_label'] - )], - column, - options=options, - currency=self.env.company.currency_id, - ), - 'auditable': True, - } - for column in options['columns'] - ] - - lines = self._get_lines(report, options) - periods = [ - ( - fields.Date.from_string(column['date_from']), - fields.Date.from_string(column['date_to']), - column['expression_label'], - ) - for column in options['columns'] - ] - deferred_amounts_by_line = self.env['account.move']._get_deferred_amounts_by_line(lines, periods, self._get_deferred_report_type()) - totals_per_grouping_field, totals_all_grouping_field = self._group_deferred_amounts_by_grouping_field( - deferred_amounts_by_line=deferred_amounts_by_line, - periods=periods, - is_reverse=self._get_deferred_report_type() == 'expense', - filter_already_generated=False, - grouping_field=options['deferred_grouping_field'], - ) - - report_lines = [] - grouping_model = self.env['account.move.line'][options['deferred_grouping_field']]._name - for totals_grouping_field in totals_per_grouping_field.values(): - grouping_record = self.env[grouping_model].browse(totals_grouping_field[options['deferred_grouping_field']]) - grouping_field_description = self.env['account.move.line'][options['deferred_grouping_field']]._description - if options['deferred_grouping_field'] == 'product_id': - grouping_field_description = _("Product") - grouping_name = grouping_record.display_name or _("(No %s)", grouping_field_description) - report_lines.append((0, { - 'id': report._get_generic_line_id(grouping_model, grouping_record.id), - 'name': grouping_name, - 'caret_options': 'deferred_caret', - 'level': 1, - 'columns': get_columns(totals_grouping_field), - })) - if totals_per_grouping_field: - report_lines.append((0, { - 'id': report._get_generic_line_id(None, None, markup='total'), - 'name': 'Total', - 'level': 1, - 'columns': get_columns(totals_all_grouping_field), - })) - - return report_lines - - ####################### - # DEFERRED GENERATION # - ####################### - - def action_generate_entry(self, options): - new_deferred_moves = self._generate_deferral_entry(options) - return { - 'name': _('Deferred Entries'), - 'type': 'ir.actions.act_window', - 'views': [(False, "list"), (False, "form")], - 'domain': [('id', 'in', new_deferred_moves.ids)], - 'res_model': 'account.move', - 'context': { - 'search_default_group_by_move': True, - 'expand': True, - }, - 'target': 'current', - } - - def _generate_deferral_entry(self, options): - journal = self.env.company.deferred_expense_journal_id if self._get_deferred_report_type() == "expense" else self.env.company.deferred_revenue_journal_id - if not journal: - raise UserError(_("Please set the deferred journal in the accounting settings.")) - date_from = fields.Date.to_date(DEFERRED_DATE_MIN) - date_to = fields.Date.from_string(options['date']['date_to']) - if date_to.day != calendar.monthrange(date_to.year, date_to.month)[1]: - raise UserError(_("You cannot generate entries for a period that does not end at the end of the month.")) - if self.env.company._get_violated_lock_dates(date_to, False, journal): - raise UserError(_("You cannot generate entries for a period that is locked.")) - options['all_entries'] = False # We only want to create deferrals for posted moves - report = self.env["account.report"].browse(options["report_id"]) - self.env['account.move.line'].flush_model() - lines = self._get_lines(report, options, filter_already_generated=True) - deferral_entry_period = self.env['account.report']._get_dates_period(date_from, date_to, 'range', period_type='month') - ref = _("Grouped Deferral Entry of %s", deferral_entry_period['string']) - ref_rev = _("Reversal of Grouped Deferral Entry of %s", deferral_entry_period['string']) - deferred_account = self.env.company.deferred_expense_account_id if self._get_deferred_report_type() == 'expense' else self.env.company.deferred_revenue_account_id - move_lines, original_move_ids = self._get_deferred_lines(lines, deferred_account, (date_from, date_to, 'current'), self._get_deferred_report_type() == 'expense', ref) - if not move_lines: - raise UserError(_("No entry to generate.")) - - deferred_move = self.env['account.move'].with_context(skip_account_deprecation_check=True).create({ - 'move_type': 'entry', - 'deferred_original_move_ids': [Command.set(original_move_ids)], - 'journal_id': journal.id, - 'date': date_to, - 'auto_post': 'at_date', - 'ref': ref, - }) - # We write the lines after creation, to make sure the `deferred_original_move_ids` is set. - # This way we can avoid adding taxes for deferred moves. - deferred_move.write({'line_ids': move_lines}) - reverse_move = deferred_move._reverse_moves() - reverse_move.write({ - 'date': deferred_move.date + relativedelta(days=1), - 'ref': ref_rev, - }) - reverse_move.line_ids.name = ref_rev - new_deferred_moves = deferred_move + reverse_move - # We create the relation (original deferred move, deferral entry) - # using SQL. This avoids a MemoryError using the ORM which will - # load huge amounts of moves in memory for nothing - self.env.cr.execute_values(""" - INSERT INTO account_move_deferred_rel(original_move_id, deferred_move_id) - VALUES %s - ON CONFLICT DO NOTHING - """, [ - (original_move_id, deferral_move.id) - for original_move_id in original_move_ids - for deferral_move in new_deferred_moves - ]) - (deferred_move + reverse_move)._post(soft=True) - return new_deferred_moves - - @api.model - def _get_current_key_totals_dict(self, lines_per_key, sign): - return { - 'account_id': lines_per_key[0]['account_id'], - 'product_id': lines_per_key[0]['product_id'], - 'product_category_id': lines_per_key[0]['product_category_id'], - 'amount_total': sign * sum(line['balance'] for line in lines_per_key), - 'move_ids': {line['move_id'] for line in lines_per_key}, - } - - @api.model - def _get_deferred_lines(self, lines, deferred_account, period, is_reverse, ref): - """ - Returns a list of Command objects to create the deferred lines of a single given period. - And the move_ids of the original lines that created these deferred - (to keep track of the original invoice in the deferred entries). - """ - if not deferred_account: - raise UserError(_("Please set the deferred accounts in the accounting settings.")) - deferred_amounts_by_line = self.env['account.move']._get_deferred_amounts_by_line(lines, [period], is_reverse) - deferred_amounts_by_key, deferred_amounts_totals = self._group_deferred_amounts_by_grouping_field(deferred_amounts_by_line, [period], is_reverse, filter_already_generated=True) - if deferred_amounts_totals['totals_aggregated'] == deferred_amounts_totals[period]: - return [], set() - - # compute analytic distribution to populate on deferred lines - # structure: {self._get_grouping_keys_deferred_lines(): [analytic distribution]} - # dict of keys: self._get_grouping_keys_deferred_lines() - # values: dict of keys: "account.analytic.account.id" (string) - # values: float - anal_dist_by_key = defaultdict(lambda: defaultdict(float)) - # using another var for the analytic distribution of the deferral account - deferred_anal_dist = defaultdict(lambda: defaultdict(float)) - for line in lines: - if not line['analytic_distribution']: - continue - # Analytic distribution should be computed from the lines with the same _get_grouping_keys_deferred_lines(), except for - # the deferred line with the deferral account which will use _get_grouping_fields_deferral_lines() - full_ratio = (line['balance'] / deferred_amounts_totals['totals_aggregated']) if deferred_amounts_totals['totals_aggregated'] else 0 - key_amount = deferred_amounts_by_key.get(self._group_by_deferred_fields(line, True)) - key_ratio = (line['balance'] / key_amount['amount_total']) if key_amount and key_amount['amount_total'] else 0 - - for account_id, distribution in line['analytic_distribution'].items(): - anal_dist_by_key[self._group_by_deferred_fields(line, True)][account_id] += distribution * key_ratio - deferred_anal_dist[self._group_by_deferral_fields(line)][account_id] += distribution * full_ratio - - remaining_balance = 0 - deferred_lines = [] - original_move_ids = set() - for key, line in deferred_amounts_by_key.items(): - for balance in (-line['amount_total'], line[period]): - if balance != 0 and line[period] != line['amount_total']: - original_move_ids |= line['move_ids'] - deferred_balance = self.env.company.currency_id.round((1 if is_reverse else -1) * balance) - deferred_lines.append( - Command.create( - self.env['account.move.line']._get_deferred_lines_values( - account_id=line['account_id'], - balance=deferred_balance, - ref=ref, - analytic_distribution=anal_dist_by_key[key] or False, - line=line, - ) - ) - ) - remaining_balance += deferred_balance - - grouped_by_key = { - key: list(value) - for key, value in groupby( - deferred_amounts_by_key.values(), - key=self._group_by_deferral_fields, - ) - } - deferral_lines = [] - for key, lines_per_key in grouped_by_key.items(): - balance = 0 - for line in lines_per_key: - if line[period] != line['amount_total']: - balance += self.env.company.currency_id.round((1 if is_reverse else -1) * (line['amount_total'] - line[period])) - deferral_lines.append( - Command.create( - self.env['account.move.line']._get_deferred_lines_values( - account_id=deferred_account.id, - balance=balance, - ref=ref, - analytic_distribution=deferred_anal_dist[key] or False, - line=lines_per_key[0], - ) - ) - ) - remaining_balance += balance - - if not self.env.company.currency_id.is_zero(remaining_balance): - deferral_lines.append( - Command.create({ - 'account_id': deferred_account.id, - 'balance': -remaining_balance, - 'name': ref, - }) - ) - return deferred_lines + deferral_lines, original_move_ids - - -class DeferredExpenseCustomHandler(models.AbstractModel): - _name = 'account.deferred.expense.report.handler' - _inherit = 'account.deferred.report.handler' - _description = 'Deferred Expense Custom Handler' - - def _get_deferred_report_type(self): - return 'expense' - - -class DeferredRevenueCustomHandler(models.AbstractModel): - _name = 'account.deferred.revenue.report.handler' - _inherit = 'account.deferred.report.handler' - _description = 'Deferred Revenue Custom Handler' - - def _get_deferred_report_type(self): - return 'revenue' diff --git a/addons/at_accounting/models/account_fiscal_position.py b/addons/at_accounting/models/account_fiscal_position.py deleted file mode 100644 index bcba747..0000000 --- a/addons/at_accounting/models/account_fiscal_position.py +++ /dev/null @@ -1,19 +0,0 @@ -from odoo import models - - -class AccountFiscalPosition(models.Model): - _inherit = 'account.fiscal.position' - - def _inverse_foreign_vat(self): - # EXTENDS account - super()._inverse_foreign_vat() - for fpos in self: - if fpos.foreign_vat: - fpos._create_draft_closing_move_for_foreign_vat() - - def _create_draft_closing_move_for_foreign_vat(self): - self.ensure_one() - existing_draft_closings = self.env['account.move'].search([('tax_closing_report_id', '!=', False), ('state', '=', 'draft')]) - for closing_date, entries in existing_draft_closings.grouped('date').items(): - for entry in entries: - self.company_id._get_and_update_tax_closing_moves(closing_date, entry.tax_closing_report_id, fiscal_positions=self) diff --git a/addons/at_accounting/models/account_fiscal_year.py b/addons/at_accounting/models/account_fiscal_year.py deleted file mode 100644 index 64169f7..0000000 --- a/addons/at_accounting/models/account_fiscal_year.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo.exceptions import ValidationError -from odoo import api, fields, models, _ - - -from datetime import datetime - - -class AccountFiscalYear(models.Model): - _name = 'account.fiscal.year' - _description = 'Fiscal Year' - - name = fields.Char(string='Name', required=True) - date_from = fields.Date(string='Start Date', required=True, - help='Start Date, included in the fiscal year.') - date_to = fields.Date(string='End Date', required=True, - help='Ending Date, included in the fiscal year.') - company_id = fields.Many2one('res.company', string='Company', required=True, - default=lambda self: self.env.company) - - @api.constrains('date_from', 'date_to', 'company_id') - def _check_dates(self): - ''' - Check interleaving between fiscal years. - There are 3 cases to consider: - - s1 s2 e1 e2 - ( [----)----] - - s2 s1 e2 e1 - [----(----] ) - - s1 s2 e2 e1 - ( [----] ) - ''' - for fy in self: - # Starting date must be prior to the ending date - date_from = fy.date_from - date_to = fy.date_to - if date_to < date_from: - raise ValidationError(_('The ending date must not be prior to the starting date.')) - if fy.company_id.parent_id: - raise ValidationError(_('You cannot have a fiscal year on a child company.')) - - domain = [ - ('id', '!=', fy.id), - ('company_id', '=', fy.company_id.id), - '|', '|', - '&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_from), - '&', ('date_from', '<=', fy.date_to), ('date_to', '>=', fy.date_to), - '&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_to), - ] - - if self.search_count(domain) > 0: - raise ValidationError(_('You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years.')) diff --git a/addons/at_accounting/models/account_general_ledger.py b/addons/at_accounting/models/account_general_ledger.py deleted file mode 100644 index a86fd3d..0000000 --- a/addons/at_accounting/models/account_general_ledger.py +++ /dev/null @@ -1,744 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import json - -from odoo import models, fields, api, _ -from odoo.tools.misc import format_date -from odoo.tools import get_lang, SQL -from odoo.exceptions import UserError - -from datetime import timedelta -from collections import defaultdict - - -class GeneralLedgerCustomHandler(models.AbstractModel): - _name = 'account.general.ledger.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'General Ledger Custom Handler' - - def _get_custom_display_config(self): - return { - 'templates': { - 'AccountReportLineName': 'at_accounting.GeneralLedgerLineName', - }, - } - - def _custom_options_initializer(self, report, options, previous_options): - # Remove multi-currency columns if needed - super()._custom_options_initializer(report, options, previous_options=previous_options) - if self.env.user.has_group('base.group_multi_currency'): - options['multi_currency'] = True - else: - options['columns'] = [ - column for column in options['columns'] - if column['expression_label'] != 'amount_currency' - ] - - # Automatically unfold the report when printing it, unless some specific lines have been unfolded - options['unfold_all'] = (options['export_mode'] == 'print' and not options.get('unfolded_lines')) or options['unfold_all'] - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - lines = [] - date_from = fields.Date.from_string(options['date']['date_from']) - company_currency = self.env.company.currency_id - - totals_by_column_group = defaultdict(lambda: {'debit': 0, 'credit': 0, 'balance': 0}) - for account, column_group_results in self._query_values(report, options): - eval_dict = {} - has_lines = False - for column_group_key, results in column_group_results.items(): - account_sum = results.get('sum', {}) - account_un_earn = results.get('unaffected_earnings', {}) - - account_debit = account_sum.get('debit', 0.0) + account_un_earn.get('debit', 0.0) - account_credit = account_sum.get('credit', 0.0) + account_un_earn.get('credit', 0.0) - account_balance = account_sum.get('balance', 0.0) + account_un_earn.get('balance', 0.0) - - eval_dict[column_group_key] = { - 'amount_currency': account_sum.get('amount_currency', 0.0) + account_un_earn.get('amount_currency', 0.0), - 'debit': account_debit, - 'credit': account_credit, - 'balance': account_balance, - } - - max_date = account_sum.get('max_date') - has_lines = has_lines or (max_date and max_date >= date_from) - - totals_by_column_group[column_group_key]['debit'] += account_debit - totals_by_column_group[column_group_key]['credit'] += account_credit - totals_by_column_group[column_group_key]['balance'] += account_balance - - lines.append(self._get_account_title_line(report, options, account, has_lines, eval_dict)) - - # Report total line. - for totals in totals_by_column_group.values(): - totals['balance'] = company_currency.round(totals['balance']) - - # Tax Declaration lines. - journal_options = report._get_options_journals(options) - if len(options['column_groups']) == 1 and len(journal_options) == 1 and journal_options[0]['type'] in ('sale', 'purchase'): - lines += self._tax_declaration_lines(report, options, journal_options[0]['type']) - - # Total line - lines.append(self._get_total_line(report, options, totals_by_column_group)) - - return [(0, line) for line in lines] - - def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function): - account_ids_to_expand = [] - for line_dict in lines_to_expand_by_function.get('_report_expand_unfoldable_line_general_ledger', []): - model, model_id = report._get_model_info_from_id(line_dict['id']) - if model == 'account.account': - account_ids_to_expand.append(model_id) - - limit_to_load = report.load_more_limit if report.load_more_limit and not options.get('export_mode') else None - has_more_per_account_id = {} - - unlimited_aml_results_per_account_id = self._get_aml_values(report, options, account_ids_to_expand)[0] - if limit_to_load: - # Apply the load_more_limit. - # load_more_limit cannot be passed to the call to _get_aml_values, otherwise it won't be applied per account but on the whole result. - # We gain perf from batching, but load every result ; then we need to filter them. - - aml_results_per_account_id = {} - for account_id, account_aml_results in unlimited_aml_results_per_account_id.items(): - account_values = {} - for key, value in account_aml_results.items(): - if len(account_values) == limit_to_load: - has_more_per_account_id[account_id] = True - break - account_values[key] = value - aml_results_per_account_id[account_id] = account_values - else: - aml_results_per_account_id = unlimited_aml_results_per_account_id - - return { - 'initial_balances': self._get_initial_balance_values(report, account_ids_to_expand, options), - 'aml_results': aml_results_per_account_id, - 'has_more': has_more_per_account_id, - } - - def _tax_declaration_lines(self, report, options, tax_type): - labels_replacement = { - 'debit': _("Base Amount"), - 'credit': _("Tax Amount"), - } - - rslt = [{ - 'id': report._get_generic_line_id(None, None, markup='tax_decl_header_1'), - 'name': _('Tax Declaration'), - 'columns': [{} for column in options['columns']], - 'level': 1, - 'unfoldable': False, - 'unfolded': False, - }, { - 'id': report._get_generic_line_id(None, None, markup='tax_decl_header_2'), - 'name': _('Name'), - 'columns': [{'name': labels_replacement.get(col['expression_label'], '')} for col in options['columns']], - 'level': 3, - 'unfoldable': False, - 'unfolded': False, - }] - - # Call the generic tax report - generic_tax_report = self.env.ref('account.generic_tax_report') - tax_report_options = generic_tax_report.get_options({**options, 'selected_variant_id': generic_tax_report.id, 'forced_domain': [('tax_line_id.type_tax_use', '=', tax_type)]}) - tax_report_lines = generic_tax_report._get_lines(tax_report_options) - tax_type_parent_line_id = generic_tax_report._get_generic_line_id(None, None, markup=tax_type) - - for tax_report_line in tax_report_lines: - if tax_report_line.get('parent_id') == tax_type_parent_line_id: - original_columns = tax_report_line['columns'] - row_column_map = { - 'debit': original_columns[0], - 'credit': original_columns[1], - } - - tax_report_line['columns'] = [row_column_map.get(col['expression_label'], {}) for col in options['columns']] - rslt.append(tax_report_line) - - return rslt - - def _query_values(self, report, options): - """ Executes the queries, and performs all the computations. - - :return: [(record, values_by_column_group), ...], where - - record is an account.account record. - - values_by_column_group is a dict in the form {column_group_key: values, ...} - - column_group_key is a string identifying a column group, as in options['column_groups'] - - values is a list of dictionaries, one per period containing: - - sum: {'debit': float, 'credit': float, 'balance': float} - - (optional) initial_balance: {'debit': float, 'credit': float, 'balance': float} - - (optional) unaffected_earnings: {'debit': float, 'credit': float, 'balance': float} - """ - # Execute the queries and dispatch the results. - query = self._get_query_sums(report, options) - - if not query: - return [] - - groupby_accounts = {} - groupby_companies = {} - - self._cr.execute(query) - for res in self._cr.dictfetchall(): - # No result to aggregate. - if res['groupby'] is None: - continue - - column_group_key = res['column_group_key'] - key = res['key'] - if key == 'sum': - groupby_accounts.setdefault(res['groupby'], {col_group_key: {} for col_group_key in options['column_groups']}) - groupby_accounts[res['groupby']][column_group_key][key] = res - - elif key == 'initial_balance': - groupby_accounts.setdefault(res['groupby'], {col_group_key: {} for col_group_key in options['column_groups']}) - groupby_accounts[res['groupby']][column_group_key][key] = res - - elif key == 'unaffected_earnings': - groupby_companies.setdefault(res['groupby'], {col_group_key: {} for col_group_key in options['column_groups']}) - groupby_companies[res['groupby']][column_group_key] = res - - # Affect the unaffected earnings to the first fetched account of type 'account.data_unaffected_earnings'. - # It's less costly to fetch all candidate accounts in a single search and then iterate it. - if groupby_companies: - unaffected_earnings_accounts = self.env['account.account'].search([ - ('display_name', 'ilike', options.get('filter_search_bar')), - *self.env['account.account']._check_company_domain(list(groupby_companies.keys())), - ('account_type', '=', 'equity_unaffected'), - ]) - for company_id, groupby_company in groupby_companies.items(): - if equity_unaffected_account := unaffected_earnings_accounts.filtered(lambda a: self.env['res.company'].browse(company_id).root_id in a.company_ids): - for column_group_key in options['column_groups']: - groupby_accounts.setdefault( - equity_unaffected_account.id, - {col_group_key: {'unaffected_earnings': {}} for col_group_key in options['column_groups']}, - ) - if unaffected_earnings := groupby_company.get(column_group_key): - if groupby_accounts[equity_unaffected_account.id][column_group_key].get('unaffected_earnings'): - for key in ['amount_currency', 'debit', 'credit', 'balance']: - groupby_accounts[equity_unaffected_account.id][column_group_key]['unaffected_earnings'][key] += unaffected_earnings[key] - else: - groupby_accounts[equity_unaffected_account.id][column_group_key]['unaffected_earnings'] = unaffected_earnings - - # Retrieve the accounts to browse. - # groupby_accounts.keys() contains all account ids affected by: - # - the amls in the current period. - # - the amls affecting the initial balance. - # - the unaffected earnings allocation. - # Note a search is done instead of a browse to preserve the table ordering. - if groupby_accounts: - accounts = self.env['account.account'].search([('id', 'in', list(groupby_accounts.keys()))]) - else: - accounts = [] - - return [(account, groupby_accounts[account.id]) for account in accounts] - - def _get_query_sums(self, report, options) -> SQL: - """ Construct a query retrieving all the aggregated sums to build the report. It includes: - - sums for all accounts. - - sums for the initial balances. - - sums for the unaffected earnings. - - sums for the tax declaration. - :return: query as SQL object - """ - options_by_column_group = report._split_options_per_column_group(options) - - queries = [] - - # ============================================ - # 1) Get sums for all accounts. - # ============================================ - for column_group_key, options_group in options_by_column_group.items(): - - # Sum is computed including the initial balance of the accounts configured to do so, unless a special option key is used - # (this is required for trial balance, which is based on general ledger) - sum_date_scope = 'strict_range' if options_group.get('general_ledger_strict_range') else 'from_beginning' - - query_domain = [] - - if not options_group.get('general_ledger_strict_range'): - date_from = fields.Date.from_string(options_group['date']['date_from']) - current_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_from) - query_domain += [ - '|', - ('date', '>=', current_fiscalyear_dates['date_from']), - ('account_id.include_initial_balance', '=', True), - ] - - if options_group.get('export_mode') == 'print' and options_group.get('filter_search_bar'): - query_domain.append(('account_id', 'ilike', options_group['filter_search_bar'])) - - if options_group.get('include_current_year_in_unaff_earnings'): - query_domain += [('account_id.include_initial_balance', '=', True)] - - query = report._get_report_query(options_group, sum_date_scope, domain=query_domain) - queries.append(SQL( - """ - SELECT - account_move_line.account_id AS groupby, - 'sum' AS key, - MAX(account_move_line.date) AS max_date, - %(column_group_key)s AS column_group_key, - COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency, - SUM(%(debit_select)s) AS debit, - SUM(%(credit_select)s) AS credit, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - GROUP BY account_move_line.account_id - """, - column_group_key=column_group_key, - table_references=query.from_clause, - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=report._currency_table_aml_join(options_group), - search_condition=query.where_clause, - )) - - # ============================================ - # 2) Get sums for the unaffected earnings. - # ============================================ - if not options_group.get('general_ledger_strict_range'): - unaff_earnings_domain = [('account_id.include_initial_balance', '=', False)] - - # The period domain is expressed as: - # [ - # ('date' <= fiscalyear['date_from'] - 1), - # ('account_id.include_initial_balance', '=', False), - # ] - - new_options = self._get_options_unaffected_earnings(options_group) - query = report._get_report_query(new_options, 'strict_range', domain=unaff_earnings_domain) - queries.append(SQL( - """ - SELECT - account_move_line.company_id AS groupby, - 'unaffected_earnings' AS key, - NULL AS max_date, - %(column_group_key)s AS column_group_key, - COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency, - SUM(%(debit_select)s) AS debit, - SUM(%(credit_select)s) AS credit, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - GROUP BY account_move_line.company_id - """, - column_group_key=column_group_key, - table_references=query.from_clause, - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=report._currency_table_aml_join(options_group), - search_condition=query.where_clause, - )) - - return SQL(" UNION ALL ").join(queries) - - def _get_options_unaffected_earnings(self, options): - ''' Create options used to compute the unaffected earnings. - The unaffected earnings are the amount of benefits/loss that have not been allocated to - another account in the previous fiscal years. - The resulting dates domain will be: - [ - ('date' <= fiscalyear['date_from'] - 1), - ('account_id.include_initial_balance', '=', False), - ] - :param options: The report options. - :return: A copy of the options. - ''' - new_options = options.copy() - new_options.pop('filter_search_bar', None) - fiscalyear_dates = self.env.company.compute_fiscalyear_dates(fields.Date.from_string(options['date']['date_from'])) - - # Trial balance uses the options key, general ledger does not - new_date_to = fields.Date.from_string(new_options['date']['date_to']) if options.get('include_current_year_in_unaff_earnings') else fiscalyear_dates['date_from'] - timedelta(days=1) - - new_options['date'] = self.env['account.report']._get_dates_period(None, new_date_to, 'single') - - return new_options - - def _get_aml_values(self, report, options, expanded_account_ids, offset=0, limit=None): - rslt = {account_id: {} for account_id in expanded_account_ids} - aml_query = self._get_query_amls(report, options, expanded_account_ids, offset=offset, limit=limit) - self._cr.execute(aml_query) - aml_results_number = 0 - has_more = False - for aml_result in self._cr.dictfetchall(): - aml_results_number += 1 - if aml_results_number == limit: - has_more = True - break - - # For asset_receivable the name will already contains the ref with the _compute_name - if aml_result['ref'] and aml_result['account_type'] != 'asset_receivable': - aml_result['communication'] = f"{aml_result['ref']} - {aml_result['name']}" - else: - aml_result['communication'] = aml_result['name'] - - # The same aml can return multiple results when using account_report_cash_basis module, if the receivable/payable - # is reconciled with multiple payments. In this case, the date shown for the move lines actually corresponds to the - # reconciliation date. In order to keep distinct lines in this case, we include date in the grouping key. - aml_key = (aml_result['id'], aml_result['date']) - - account_result = rslt[aml_result['account_id']] - if not aml_key in account_result: - account_result[aml_key] = {col_group_key: {} for col_group_key in options['column_groups']} - - already_present_result = account_result[aml_key][aml_result['column_group_key']] - if already_present_result: - # In case the same move line gives multiple results at the same date, add them. - # This does not happen in standard GL report, but could because of custom shadowing of account.move.line, - # such as the one done in account_report_cash_basis (if the payable/receivable line is reconciled twice at the same date). - already_present_result['debit'] += aml_result['debit'] - already_present_result['credit'] += aml_result['credit'] - already_present_result['balance'] += aml_result['balance'] - already_present_result['amount_currency'] += aml_result['amount_currency'] - else: - account_result[aml_key][aml_result['column_group_key']] = aml_result - - return rslt, has_more - - def _get_query_amls(self, report, options, expanded_account_ids, offset=0, limit=None) -> SQL: - """ Construct a query retrieving the account.move.lines when expanding a report line with or without the load - more. - :param options: The report options. - :param expanded_account_ids: The account.account ids corresponding to consider. If None, match every account. - :param offset: The offset of the query (used by the load more). - :param limit: The limit of the query (used by the load more). - :return: (query, params) - """ - additional_domain = [('account_id', 'in', expanded_account_ids)] if expanded_account_ids is not None else None - queries = [] - journal_name = self.env['account.journal']._field_to_sql('journal', 'name') - for column_group_key, group_options in report._split_options_per_column_group(options).items(): - # Get sums for the account move lines. - # period: [('date' <= options['date_to']), ('date', '>=', options['date_from'])] - query = report._get_report_query(group_options, domain=additional_domain, date_scope='strict_range') - account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - account_type = self.env['account.account']._field_to_sql(account_alias, 'account_type') - - query = SQL( - ''' - SELECT - account_move_line.id, - account_move_line.date, - account_move_line.date_maturity, - account_move_line.name, - account_move_line.ref, - account_move_line.company_id, - account_move_line.account_id, - account_move_line.payment_id, - account_move_line.partner_id, - account_move_line.currency_id, - account_move_line.amount_currency, - COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date, - account_move_line.date AS date, - %(debit_select)s AS debit, - %(credit_select)s AS credit, - %(balance_select)s AS balance, - move.name AS move_name, - company.currency_id AS company_currency_id, - partner.name AS partner_name, - move.move_type AS move_type, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_type)s AS account_type, - journal.code AS journal_code, - %(journal_name)s AS journal_name, - full_rec.id AS full_rec_name, - %(column_group_key)s AS column_group_key - FROM %(table_references)s - JOIN account_move move ON move.id = account_move_line.move_id - %(currency_table_join)s - LEFT JOIN res_company company ON company.id = account_move_line.company_id - LEFT JOIN res_partner partner ON partner.id = account_move_line.partner_id - LEFT JOIN account_journal journal ON journal.id = account_move_line.journal_id - LEFT JOIN account_full_reconcile full_rec ON full_rec.id = account_move_line.full_reconcile_id - WHERE %(search_condition)s - ORDER BY account_move_line.date, account_move_line.move_name, account_move_line.id - ''', - account_code=account_code, - account_name=account_name, - account_type=account_type, - journal_name=journal_name, - column_group_key=column_group_key, - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(group_options), - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - search_condition=query.where_clause, - ) - queries.append(query) - - full_query = SQL(" UNION ALL ").join(SQL("(%s)", query) for query in queries) - - if offset: - full_query = SQL('%s OFFSET %s ', full_query, offset) - if limit: - full_query = SQL('%s LIMIT %s ', full_query, limit) - - return full_query - - def _get_initial_balance_values(self, report, account_ids, options): - """ - Get sums for the initial balance. - """ - queries = [] - for column_group_key, options_group in report._split_options_per_column_group(options).items(): - new_options = self._get_options_initial_balance(options_group) - domain = [ - ('account_id', 'in', account_ids), - ] - if not new_options.get('general_ledger_strict_range'): - domain += [ - '|', - ('date', '>=', new_options['date']['date_from']), - ('account_id.include_initial_balance', '=', True), - ] - if new_options.get('include_current_year_in_unaff_earnings'): - domain += [('account_id.include_initial_balance', '=', True)] - query = report._get_report_query(new_options, 'from_beginning', domain=domain) - queries.append(SQL( - """ - SELECT - account_move_line.account_id AS groupby, - 'initial_balance' AS key, - NULL AS max_date, - %(column_group_key)s AS column_group_key, - COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency, - SUM(%(debit_select)s) AS debit, - SUM(%(credit_select)s) AS credit, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - GROUP BY account_move_line.account_id - """, - column_group_key=column_group_key, - table_references=query.from_clause, - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=report._currency_table_aml_join(options_group), - search_condition=query.where_clause, - )) - - self._cr.execute(SQL(" UNION ALL ").join(queries)) - - init_balance_by_col_group = { - account_id: {column_group_key: {} for column_group_key in options['column_groups']} - for account_id in account_ids - } - for result in self._cr.dictfetchall(): - init_balance_by_col_group[result['groupby']][result['column_group_key']] = result - - accounts = self.env['account.account'].browse(account_ids) - return { - account.id: (account, init_balance_by_col_group[account.id]) - for account in accounts - } - - def _get_options_initial_balance(self, options): - """ Create options used to compute the initial balances. - The initial balances depict the current balance of the accounts at the beginning of - the selected period in the report. - The resulting dates domain will be: - [ - ('date' <= options['date_from'] - 1), - '|', - ('date' >= fiscalyear['date_from']), - ('account_id.include_initial_balance', '=', True) - ] - :param options: The report options. - :return: A copy of the options. - """ - #pylint: disable=sql-injection - new_options = options.copy() - date_to = new_options['comparison']['periods'][-1]['date_from'] if new_options.get('comparison', {}).get('periods') else new_options['date']['date_from'] - new_date_to = fields.Date.from_string(date_to) - timedelta(days=1) - - # Date from computation - # We have two case: - # 1) We are choosing a date that starts at the beginning of a fiscal year and we want the initial period to be - # the previous fiscal year - # 2) We are choosing a date that starts in the middle of a fiscal year and in that case we want the initial period - # to be the beginning of the fiscal year - date_from = fields.Date.from_string(new_options['date']['date_from']) - current_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_from) - - if date_from == current_fiscalyear_dates['date_from']: - # We want the previous fiscal year - previous_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_from - timedelta(days=1)) - new_date_from = previous_fiscalyear_dates['date_from'] - include_current_year_in_unaff_earnings = True - else: - # We want the current fiscal year - new_date_from = current_fiscalyear_dates['date_from'] - include_current_year_in_unaff_earnings = False - - new_options['date'] = self.env['account.report']._get_dates_period( - new_date_from, - new_date_to, - 'range', - ) - new_options['include_current_year_in_unaff_earnings'] = include_current_year_in_unaff_earnings - - return new_options - - #################################################### - # COLUMN/LINE HELPERS - #################################################### - def _get_account_title_line(self, report, options, account, has_lines, eval_dict): - line_columns = [] - for column in options['columns']: - col_value = eval_dict.get(column['column_group_key'], {}).get(column['expression_label']) - col_expr_label = column['expression_label'] - - value = None if col_value is None or (col_expr_label == 'amount_currency' and not account.currency_id) else col_value - - line_columns.append(report._build_column_dict( - value, - column, - options=options, - currency=account.currency_id if col_expr_label == 'amount_currency' else None, - )) - - line_id = report._get_generic_line_id('account.account', account.id) - is_in_unfolded_lines = any( - report._get_res_id_from_line_id(line_id, 'account.account') == account.id - for line_id in options.get('unfolded_lines') - ) - return { - 'id': line_id, - 'name': account.display_name, - 'columns': line_columns, - 'level': 1, - 'unfoldable': has_lines, - 'unfolded': has_lines and (is_in_unfolded_lines or options.get('unfold_all')), - 'expand_function': '_report_expand_unfoldable_line_general_ledger', - } - - def _get_aml_line(self, report, parent_line_id, options, eval_dict, init_bal_by_col_group): - line_columns = [] - for column in options['columns']: - col_expr_label = column['expression_label'] - col_value = eval_dict[column['column_group_key']].get(col_expr_label) - col_currency = None - - if col_value is not None: - if col_expr_label == 'amount_currency': - col_currency = self.env['res.currency'].browse(eval_dict[column['column_group_key']]['currency_id']) - col_value = None if col_currency == self.env.company.currency_id else col_value - elif col_expr_label == 'balance': - col_value += (init_bal_by_col_group[column['column_group_key']] or 0) - - line_columns.append(report._build_column_dict( - col_value, - column, - options=options, - currency=col_currency, - )) - - aml_id = None - move_name = None - caret_type = None - for column_group_dict in eval_dict.values(): - aml_id = column_group_dict.get('id', '') - if aml_id: - if column_group_dict.get('payment_id'): - caret_type = 'account.payment' - else: - caret_type = 'account.move.line' - move_name = column_group_dict['move_name'] - date = str(column_group_dict.get('date', '')) - break - - return { - 'id': report._get_generic_line_id('account.move.line', aml_id, parent_line_id=parent_line_id, markup=date), - 'caret_options': caret_type, - 'parent_id': parent_line_id, - 'name': move_name, - 'columns': line_columns, - 'level': 3, - } - - @api.model - def _get_total_line(self, report, options, eval_dict): - line_columns = [] - for column in options['columns']: - col_value = eval_dict[column['column_group_key']].get(column['expression_label']) - col_value = None if col_value is None else col_value - - line_columns.append(report._build_column_dict(col_value, column, options=options)) - - return { - 'id': report._get_generic_line_id(None, None, markup='total'), - 'name': _('Total'), - 'level': 1, - 'columns': line_columns, - } - - def caret_option_audit_tax(self, options, params): - return self.env['account.generic.tax.report.handler'].caret_option_audit_tax(options, params) - - def _report_expand_unfoldable_line_general_ledger(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None): - def init_load_more_progress(line_dict): - return { - column['column_group_key']: line_col.get('no_format', 0) - for column, line_col in zip(options['columns'], line_dict['columns']) - if column['expression_label'] == 'balance' - } - - report = self.env.ref('at_accounting.general_ledger_report') - model, model_id = report._get_model_info_from_id(line_dict_id) - - if model != 'account.account': - raise UserError(_("Wrong ID for general ledger line to expand: %s", line_dict_id)) - - lines = [] - - # Get initial balance - if offset == 0: - if unfold_all_batch_data: - account, init_balance_by_col_group = unfold_all_batch_data['initial_balances'][model_id] - else: - account, init_balance_by_col_group = self._get_initial_balance_values(report, [model_id], options)[model_id] - - initial_balance_line = report._get_partner_and_general_ledger_initial_balance_line(options, line_dict_id, init_balance_by_col_group, account.currency_id) - - if initial_balance_line: - lines.append(initial_balance_line) - - # For the first expansion of the line, the initial balance line gives the progress - progress = init_load_more_progress(initial_balance_line) - - # Get move lines - limit_to_load = report.load_more_limit + 1 if report.load_more_limit and options['export_mode'] != 'print' else None - if unfold_all_batch_data: - aml_results = unfold_all_batch_data['aml_results'][model_id] - has_more = unfold_all_batch_data['has_more'].get(model_id, False) - else: - aml_results, has_more = self._get_aml_values(report, options, [model_id], offset=offset, limit=limit_to_load) - aml_results = aml_results[model_id] - - next_progress = progress - for aml_result in aml_results.values(): - new_line = self._get_aml_line(report, line_dict_id, options, aml_result, next_progress) - lines.append(new_line) - next_progress = init_load_more_progress(new_line) - - return { - 'lines': lines, - 'offset_increment': report.load_more_limit, - 'has_more': has_more, - 'progress': next_progress, - } diff --git a/addons/at_accounting/models/account_generic_tax_report.py b/addons/at_accounting/models/account_generic_tax_report.py deleted file mode 100644 index b9e9475..0000000 --- a/addons/at_accounting/models/account_generic_tax_report.py +++ /dev/null @@ -1,1221 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import ast -from collections import defaultdict - -from odoo import models, api, fields, Command, _ -from odoo.addons.web.controllers.utils import clean_action -from odoo.exceptions import UserError, RedirectWarning -from odoo.osv import expression -from odoo.tools import SQL - - -class AccountTaxReportHandler(models.AbstractModel): - _name = 'account.tax.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Account Report Handler for Tax Reports' - - # This model is needed for the Closing Entry button to be available for all reports, including the generic one - # With this, custom tax reports don't need to inherit from the generic tax report - - def _custom_options_initializer(self, report, options, previous_options): - optional_periods = { - 'monthly': 'month', - 'trimester': 'quarter', - 'year': 'year', - } - - options['buttons'].append({'name': _('Closing Entry'), 'action': 'action_periodic_vat_entries', 'sequence': 110, 'always_show': True}) - self._enable_export_buttons_for_common_vat_groups_in_branches(options) - - day, month = self.env.company._get_tax_closing_start_date_attributes(report) - periodicity = self.env.company._get_tax_periodicity(report) - options['tax_periodicity'] = { - 'periodicity': periodicity, - 'months_per_period': self.env.company._get_tax_periodicity_months_delay(report), - 'start_day': day, - 'start_month': month, - } - - options['show_tax_period_filter'] = periodicity not in optional_periods or day != 1 or month != 1 - if not options['show_tax_period_filter']: - period_type = optional_periods[periodicity] - options['date']['filter'] = options['date']['filter'].replace('tax_period', period_type) - options['date']['period_type'] = options['date']['period_type'].replace('tax_period', period_type) - - def _get_custom_display_config(self): - display_config = defaultdict(dict) - display_config['templates']['AccountReportFilters'] = 'at_accounting.GenericTaxReportFiltersCustomizable' - return display_config - - def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings): - if 'at_accounting.common_warning_draft_in_period' in warnings: - # Recompute the warning 'common_warning_draft_in_period' to not include tax closing entries in the banner of unposted moves - if not self.env['account.move'].search_count( - [('state', '=', 'draft'), ('date', '<=', options['date']['date_to']), - ('tax_closing_report_id', '=', False)], - limit=1, - ): - warnings.pop('at_accounting.common_warning_draft_in_period') - - # Chek the use of inactive tags in the period - query = report._get_report_query(options, 'strict_range') - rows = self.env.execute_query(SQL(""" - SELECT 1 - FROM %s - JOIN account_account_tag_account_move_line_rel aml_tag - ON account_move_line.id = aml_tag.account_move_line_id - JOIN account_account_tag tag - ON aml_tag.account_account_tag_id = tag.id - WHERE %s - AND NOT tag.active - LIMIT 1 - """, query.from_clause, query.where_clause)) - if rows: - warnings['at_accounting.tax_report_warning_inactive_tags'] = {} - - - # ------------------------------------------------------------------------- - # TAX CLOSING - # ------------------------------------------------------------------------- - - def _is_period_equal_to_options(self, report, options): - options_date_to = fields.Date.from_string(options['date']['date_to']) - options_date_from = fields.Date.from_string(options['date']['date_from']) - date_from, date_to = self.env.company._get_tax_closing_period_boundaries(options_date_to, report) - return date_from == options_date_from and date_to == options_date_to - - def action_periodic_vat_entries(self, options, from_post=False): - report = self.env['account.report'].browse(options['report_id']) - if (options['date']['period_type'] != 'tax_period' and not self._is_period_equal_to_options(report, - options)) and not self.env.context.get( - 'override_tax_closing_warning'): - if len(options['companies']) > 1 and (report.filter_multi_company != 'tax_units' or not ( - report.country_id and options['available_tax_units'])): - message = _( - "You're about the generate the closing entries of multiple companies at once. Each of them will be created in accordance with its company tax periodicity.") - else: - message = _( - "The currently selected dates don't match a tax period. The closing entry will be created for the closest-matching period according to your periodicity setup.") - - return { - 'type': 'ir.actions.client', - 'tag': 'at_accounting.redirect_action', - 'target': 'new', - 'params': { - 'depending_action': self.with_context( - {'override_tax_closing_warning': True}).action_periodic_vat_entries(options), - 'message': message, - 'button_text': _("Proceed"), - }, - 'context': { - 'dialog_size': 'medium', - 'override_tax_closing_warning': True, - }, - } - - moves = self._get_periodic_vat_entries(options, from_post=from_post) - # Make the action for the retrieved move and return it. - action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_journal_line") - action = clean_action(action, env=self.env) - action.pop('domain', None) - - if len(moves) == 1: - action['views'] = [(self.env.ref('account.view_move_form').id, 'form')] - action['res_id'] = moves.id - else: - action['domain'] = [('id', 'in', moves.ids)] - action['context'] = dict(ast.literal_eval(action['context'])) - action['context'].pop('search_default_posted', None) - return action - - def _get_periodic_vat_entries(self, options, from_post=False): - report = self.env['account.report'].browse(options['report_id']) - - # When integer_rounding is available, we always want it for tax closing (as it means it's a legal requirement) - if options.get('integer_rounding'): - options['integer_rounding_enabled'] = True - - # Return action to open form view of newly created entry - moves = self.env['account.move'] - - # Get all companies impacting the report. - companies = self.env['res.company'].browse(report.get_report_company_ids(options)) - - companies_moves = self._get_tax_closing_entries_for_closed_period(report, options, companies, posted_only=False) - moves += companies_moves - moves += self._generate_tax_closing_entries(report, options, companies=companies - companies_moves.company_id, from_post=from_post) - - return moves - - def _generate_tax_closing_entries(self, report, options, closing_moves=None, companies=None, from_post=False): - """Generates and/or updates VAT closing entries. - - This method computes the content of the tax closing in the following way: - - Search on all tax lines in the given period, group them by tax_group (each tax group might have its own - tax receivable/payable account). - - Create a move line that balances each tax account and add the difference in the correct receivable/payable - account. Also take into account amounts already paid via advance tax payment account. - - The tax closing is done so that an individual move is created per available VAT number: so, one for each - foreign vat fiscal position (each with fiscal_position_id set to this fiscal position), and one for the domestic - position (with fiscal_position_id = None). The moves created by this function hence depends on the content of the - options dictionary, and what fiscal positions are accepted by it. - - :param options: the tax report options dict to use to make the closing. - :param closing_moves: If provided, closing moves to update the content from. - They need to be compatible with the provided options (if they have a fiscal_position_id, for example). - :param companies: optional params, the companies given will be used instead of taking all the companies impacting - the report. - :return: The closing moves. - """ - if companies is None: - companies = self.env['res.company'].browse(report.get_report_company_ids(options)) - - if closing_moves is None: - closing_moves = self.env['account.move'] - - end_date = fields.Date.from_string(options['date']['date_to']) - - closing_moves_by_company = defaultdict(lambda: self.env['account.move']) - - companies_without_closing = companies.filtered(lambda company: company not in closing_moves.company_id) - if closing_moves: - for move in closing_moves.filtered(lambda x: x.state == 'draft'): - closing_moves_by_company[move.company_id] |= move - - for company in companies_without_closing: - include_domestic, fiscal_positions = self._get_fpos_info_for_tax_closing(company, report, options) - company_closing_moves = company._get_and_update_tax_closing_moves(end_date, report, fiscal_positions=fiscal_positions, include_domestic=include_domestic) - closing_moves_by_company[company] = company_closing_moves - closing_moves += company_closing_moves - - for company, company_closing_moves in closing_moves_by_company.items(): - - # First gather the countries for which the closing is being done - countries = self.env['res.country'] - for move in company_closing_moves: - if move.fiscal_position_id.foreign_vat: - countries |= move.fiscal_position_id.country_id - else: - countries |= company.account_fiscal_country_id - - # Check the tax groups from the company for any misconfiguration in these countries - if self.env['account.tax.group']._check_misconfigured_tax_groups(company, countries): - self._redirect_to_misconfigured_tax_groups(company, countries) - - for move in company_closing_moves: - # When coming from post and that the current move is the closing of the current company we don't want to - # write on it again - if from_post and move == closing_moves_by_company.get(self.env.company): - continue - - # get tax entries by tax_group for the period defined in options - move_options = {**options, 'fiscal_position': move.fiscal_position_id.id if move.fiscal_position_id else 'domestic'} - line_ids_vals, tax_group_subtotal = self._compute_vat_closing_entry(company, move_options) - - line_ids_vals += self._add_tax_group_closing_items(tax_group_subtotal, move) - - if move.line_ids: - line_ids_vals += [Command.delete(aml.id) for aml in move.line_ids] - - move_vals = {} - if line_ids_vals: - move_vals['line_ids'] = line_ids_vals - move.write(move_vals) - - return closing_moves - - def _get_tax_closing_entries_for_closed_period(self, report, options, companies, posted_only=True): - """ Fetch the closing entries related to the given companies for the currently selected tax report period. - Only used when the selected period already has a tax lock date impacting it, and assuming that these periods - all have a tax closing entry. - :param report: The tax report for which we are getting the closing entries. - :param options: the tax report options dict needed to get the period end date and fiscal position info. - :param companies: a recordset of companies for which the period has already been closed. - :return: The closing moves. - """ - closing_moves = self.env['account.move'] - for company in companies: - _dummy, period_end = company._get_tax_closing_period_boundaries(fields.Date.from_string(options['date']['date_to']), report) - include_domestic, fiscal_positions = self._get_fpos_info_for_tax_closing(company, report, options) - fiscal_position_ids = fiscal_positions.ids + ([False] if include_domestic else []) - state_domain = ('state', '=', 'posted') if posted_only else ('state', '!=', 'cancel') - closing_moves += self.env['account.move'].search([ - ('company_id', '=', company.id), - ('fiscal_position_id', 'in', fiscal_position_ids), - ('date', '=', period_end), - ('tax_closing_report_id', '=', options['report_id']), - state_domain, - ], limit=1) - - return closing_moves - - @api.model - def _compute_vat_closing_entry(self, company, options): - """Compute the VAT closing entry. - - This method returns the one2many commands to balance the tax accounts for the selected period, and - a dictionnary that will help balance the different accounts set per tax group. - """ - self = self.with_company(company) # Needed to handle access to property fields correctly - - # first, for each tax group, gather the tax entries per tax and account - self.env['account.tax'].flush_model(['name', 'tax_group_id']) - self.env['account.tax.repartition.line'].flush_model(['use_in_tax_closing']) - self.env['account.move.line'].flush_model(['account_id', 'debit', 'credit', 'move_id', 'tax_line_id', 'date', 'company_id', 'display_type', 'parent_state']) - self.env['account.move'].flush_model(['state']) - - new_options = { - **options, - 'all_entries': False, - 'date': dict(options['date']), - } - - report = self.env['account.report'].browse(options['report_id']) - period_start, period_end = company._get_tax_closing_period_boundaries(fields.Date.from_string(options['date']['date_to']), report) - new_options['date']['date_from'] = fields.Date.to_string(period_start) - new_options['date']['date_to'] = fields.Date.to_string(period_end) - new_options['date']['period_type'] = 'custom' - new_options['date']['filter'] = 'custom' - new_options = report.with_context(allowed_company_ids=company.ids).get_options(previous_options=new_options) - # Force the use of the fiscal position from the original options (_get_options sets the fiscal - # position to 'all' when the report is the generic tax report) - new_options['fiscal_position'] = options['fiscal_position'] - - query = self.env.ref('account.generic_tax_report')._get_report_query( - new_options, - 'strict_range', - domain=self._get_vat_closing_entry_additional_domain() - ) - - # Check whether it is multilingual, in order to get the translation from the JSON value if present - tax_name = self.env['account.tax']._field_to_sql('tax', 'name') - - query = SQL( - """ - SELECT "account_move_line".tax_line_id as tax_id, - tax.tax_group_id as tax_group_id, - %(tax_name)s as tax_name, - "account_move_line".account_id, - COALESCE(SUM("account_move_line".balance), 0) as amount - FROM account_tax tax, account_tax_repartition_line repartition, %(table_references)s - WHERE %(search_condition)s - AND tax.id = "account_move_line".tax_line_id - AND repartition.id = "account_move_line".tax_repartition_line_id - AND repartition.use_in_tax_closing - GROUP BY tax.tax_group_id, "account_move_line".tax_line_id, tax.name, "account_move_line".account_id - """, - tax_name=tax_name, - table_references=query.from_clause, - search_condition=query.where_clause, - ) - self.env.cr.execute(query) - results = self.env.cr.dictfetchall() - results = self._postprocess_vat_closing_entry_results(company, new_options, results) - - tax_group_ids = [r['tax_group_id'] for r in results] - tax_groups = {} - for tg, result in zip(self.env['account.tax.group'].browse(tax_group_ids), results): - if tg not in tax_groups: - tax_groups[tg] = {} - if result.get('tax_id') not in tax_groups[tg]: - tax_groups[tg][result.get('tax_id')] = [] - tax_groups[tg][result.get('tax_id')].append((result.get('tax_name'), result.get('account_id'), result.get('amount'))) - - # then loop on previous results to - # * add the lines that will balance their sum per account - # * make the total per tax group's account triplet - # (if 2 tax groups share the same 3 accounts, they should consolidate in the vat closing entry) - move_vals_lines = [] - tax_group_subtotal = {} - currency = self.env.company.currency_id - for tg, values in tax_groups.items(): - total = 0 - # ignore line that have no property defined on tax group - if not tg.tax_receivable_account_id or not tg.tax_payable_account_id: - continue - for dummy, value in values.items(): - for v in value: - tax_name, account_id, amt = v - # Line to balance - move_vals_lines.append((0, 0, {'name': tax_name, 'debit': abs(amt) if amt < 0 else 0, 'credit': amt if amt > 0 else 0, 'account_id': account_id})) - total += amt - - if not currency.is_zero(total): - # Add total to correct group - key = (tg.advance_tax_payment_account_id.id or False, tg.tax_receivable_account_id.id, tg.tax_payable_account_id.id) - - if tax_group_subtotal.get(key): - tax_group_subtotal[key] += total - else: - tax_group_subtotal[key] = total - - # If the tax report is completely empty, we add two 0-valued lines, using the first in in and out - # account id we find on the taxes. - if len(move_vals_lines) == 0: - rep_ln_in = self.env['account.tax.repartition.line'].search([ - *self.env['account.tax.repartition.line']._check_company_domain(company), - ('account_id.deprecated', '=', False), - ('repartition_type', '=', 'tax'), - ('document_type', '=', 'invoice'), - ('tax_id.type_tax_use', '=', 'purchase') - ], limit=1) - rep_ln_out = self.env['account.tax.repartition.line'].search([ - *self.env['account.tax.repartition.line']._check_company_domain(company), - ('account_id.deprecated', '=', False), - ('repartition_type', '=', 'tax'), - ('document_type', '=', 'invoice'), - ('tax_id.type_tax_use', '=', 'sale') - ], limit=1) - - if rep_ln_out.account_id and rep_ln_in.account_id: - move_vals_lines = [ - Command.create({ - 'name': _('Tax Received Adjustment'), - 'debit': 0, - 'credit': 0.0, - 'account_id': rep_ln_out.account_id.id - }), - - Command.create({ - 'name': _('Tax Paid Adjustment'), - 'debit': 0.0, - 'credit': 0, - 'account_id': rep_ln_in.account_id.id - }) - ] - - return move_vals_lines, tax_group_subtotal - - def _get_vat_closing_entry_additional_domain(self): - return [] - - def _postprocess_vat_closing_entry_results(self, company, options, results): - # Override this to, for example, apply a rounding to the lines of the closing entry - return results - - def _vat_closing_entry_results_rounding(self, company, options, results, rounding_accounts, vat_results_summary): - """ - Apply the rounding from the tax report by adding a line to the end of the query results - representing the sum of the roundings on each line of the tax report. - """ - # Ignore if the rounding accounts cannot be found - if not rounding_accounts.get('profit') or not rounding_accounts.get('loss'): - return results - - total_amount = 0.0 - tax_group_id = None - - for line in results: - total_amount += line['amount'] - # The accounts on the tax group ids from the results should be uniform, - # but we choose the greatest id so that the line appears last on the entry. - tax_group_id = line['tax_group_id'] - - report = self.env['account.report'].browse(options['report_id']) - - for line in report._get_lines(options): - model, record_id = report._get_model_info_from_id(line['id']) - - if model != 'account.report.line': - continue - - for (operation_type, report_line_id, column_expression_label) in vat_results_summary: - for column in line['columns']: - if record_id != report_line_id or column['expression_label'] != column_expression_label: - continue - - # We accept 3 types of operations: - # 1) due and 2) deductible - This is used for reports that have lines for the payable vat and - # lines for the reclaimable vat. - # 3) total - This is used for reports that have a single line with the payable/reclaimable vat. - if operation_type in {'due', 'total'}: - total_amount += column['no_format'] - elif operation_type == 'deductible': - total_amount -= column['no_format'] - - currency = company.currency_id - total_difference = currency.round(total_amount) - - if not currency.is_zero(total_difference): - results.append({ - 'tax_name': _('Difference from rounding taxes'), - 'amount': total_difference * -1, - 'tax_group_id': tax_group_id, - 'account_id': rounding_accounts['profit'].id if total_difference < 0 else rounding_accounts['loss'].id - }) - - return results - - @api.model - def _add_tax_group_closing_items(self, tax_group_subtotal, closing_move): - """Transform the parameter tax_group_subtotal dictionnary into one2many commands. - - Used to balance the tax group accounts for the creation of the vat closing entry. - """ - def _add_line(account, name, company_currency): - self.env.cr.execute(sql_account, ( - account, - closing_move.date, - closing_move.company_id.id, - )) - result = self.env.cr.dictfetchone() - advance_balance = result.get('balance') or 0 - # Deduct/Add advance payment - if not company_currency.is_zero(advance_balance): - line_ids_vals.append((0, 0, { - 'name': name, - 'debit': abs(advance_balance) if advance_balance < 0 else 0, - 'credit': abs(advance_balance) if advance_balance > 0 else 0, - 'account_id': account - })) - return advance_balance - - currency = closing_move.company_id.currency_id - sql_account = ''' - SELECT SUM(aml.balance) AS balance - FROM account_move_line aml - LEFT JOIN account_move move ON move.id = aml.move_id - WHERE aml.account_id = %s - AND aml.date <= %s - AND move.state = 'posted' - AND aml.company_id = %s - ''' - line_ids_vals = [] - # keep track of already balanced account, as one can be used in several tax group - account_already_balanced = [] - for key, value in tax_group_subtotal.items(): - total = value - # Search if any advance payment done for that configuration - if key[0] and key[0] not in account_already_balanced: - total += _add_line(key[0], _('Balance tax advance payment account'), currency) - account_already_balanced.append(key[0]) - if key[1] and key[1] not in account_already_balanced: - total += _add_line(key[1], _('Balance tax current account (receivable)'), currency) - account_already_balanced.append(key[1]) - if key[2] and key[2] not in account_already_balanced: - total += _add_line(key[2], _('Balance tax current account (payable)'), currency) - account_already_balanced.append(key[2]) - # Balance on the receivable/payable tax account - if not currency.is_zero(total): - line_ids_vals.append(Command.create({ - 'name': _('Payable tax amount') if total < 0 else _('Receivable tax amount'), - 'debit': total if total > 0 else 0, - 'credit': abs(total) if total < 0 else 0, - 'account_id': key[2] if total < 0 else key[1] - })) - return line_ids_vals - - @api.model - def _redirect_to_misconfigured_tax_groups(self, company, countries): - """ Raises a RedirectWarning informing the user his tax groups are missing configuration - for a given company, redirecting him to the list view of account.tax.group, filtered - accordingly to the provided countries. - """ - need_config_action = { - 'type': 'ir.actions.act_window', - 'name': 'Tax groups', - 'res_model': 'account.tax.group', - 'view_mode': 'list', - 'views': [[False, 'list']], - 'domain': ['|', ('country_id', 'in', countries.ids), ('country_id', '=', False)] - } - - raise RedirectWarning( - _('Please specify the accounts necessary for the Tax Closing Entry.'), - need_config_action, - _('Configure your TAX accounts - %s', company.display_name), - ) - - def _get_fpos_info_for_tax_closing(self, company, report, options): - """ Returns the fiscal positions information to use to generate the tax closing - for this company, with the provided options. - - :return: (include_domestic, fiscal_positions), where fiscal positions is a recordset - and include_domestic is a boolean telling whether or not the domestic closing - (i.e. the one without any fiscal position) must also be performed - """ - if options['fiscal_position'] == 'domestic': - fiscal_positions = self.env['account.fiscal.position'] - elif options['fiscal_position'] == 'all': - fiscal_positions = self.env['account.fiscal.position'].search([ - *self.env['account.fiscal.position']._check_company_domain(company), - ('foreign_vat', '!=', False), - ]) - else: - fpos_ids = [options['fiscal_position']] - fiscal_positions = self.env['account.fiscal.position'].browse(fpos_ids) - - if options['fiscal_position'] == 'all': - fiscal_country = company.account_fiscal_country_id - include_domestic = not fiscal_positions \ - or not report.country_id \ - or fiscal_country == fiscal_positions[0].country_id - else: - include_domestic = options['fiscal_position'] == 'domestic' - - return include_domestic, fiscal_positions - - def _get_amls_with_archived_tags_domain(self, options): - domain = [ - ('tax_tag_ids.active', '=', False), - ('parent_state', '=', 'posted'), - ('date', '>=', options['date']['date_from']), - ] - if options['date']['mode'] == 'single': - domain.append(('date', '<=', options['date']['date_to'])) - return domain - - def action_open_amls_with_archived_tags(self, options, params=None): - return { - 'name': _("Journal items with archived tax tags"), - 'type': 'ir.actions.act_window', - 'res_model': 'account.move.line', - 'domain': self._get_amls_with_archived_tags_domain(options), - 'context': {'active_test': False}, - 'views': [(self.env.ref('at_accounting.view_archived_tag_move_tree').id, 'list')], - } - - -class GenericTaxReportCustomHandler(models.AbstractModel): - _name = 'account.generic.tax.report.handler' - _inherit = 'account.tax.report.handler' - _description = 'Generic Tax Report Custom Handler' - - def _get_custom_display_config(self): - parent_config = super()._get_custom_display_config() - parent_config['css_custom_class'] = 'generic_tax_report' - parent_config['templates']['AccountReportLineName'] = 'at_accounting.TaxReportLineName' - - return parent_config - - def _custom_options_initializer(self, report, options, previous_options=None): - super()._custom_options_initializer(report, options, previous_options=previous_options) - - # We are on the generic tax report (no country) and the user can not change the fiscal position so we show them all. - if not report.country_id and len(options['available_vat_fiscal_positions']) <= (0 if options['allow_domestic'] else 1) and len(options['companies']) <= 1: - options['allow_domestic'] = False - options['fiscal_position'] = 'all' - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - return self._get_dynamic_lines(report, options, 'default', warnings) - - def _caret_options_initializer(self): - return { - 'generic_tax_report': [ - {'name': _("Audit"), 'action': 'caret_option_audit_tax'}, - ] - } - - def _get_dynamic_lines(self, report, options, grouping, warnings=None): - """ Compute the report lines for the generic tax report. - - :param options: The report options. - :return: A list of lines, each one being a python dictionary. - """ - options_by_column_group = report._split_options_per_column_group(options) - - # Compute tax_base_amount / tax_amount for each selected groupby. - if grouping == 'tax_account': - groupby_fields = [('src_tax', 'type_tax_use'), ('src_tax', 'id'), ('account', 'id')] - comodels = [None, 'account.tax', 'account.account'] - elif grouping == 'account_tax': - groupby_fields = [('src_tax', 'type_tax_use'), ('account', 'id'), ('src_tax', 'id')] - comodels = [None, 'account.account', 'account.tax'] - else: - groupby_fields = [('src_tax', 'type_tax_use'), ('src_tax', 'id')] - comodels = [None, 'account.tax'] - - if grouping in ('tax_account', 'account_tax'): - tax_amount_hierarchy = self._read_generic_tax_report_amounts(report, options_by_column_group, groupby_fields) - else: - tax_amount_hierarchy = self._read_generic_tax_report_amounts_no_tax_details(report, options, options_by_column_group) - - - # Fetch involved records in order to ensure all lines are sorted according the comodel order. - # To do so, we compute 'sorting_map_list' allowing to retrieve each record by id and the order - # to be used. - record_ids_gb = [set() for dummy in groupby_fields] - - def populate_record_ids_gb_recursively(node, level=0): - for k, v in node.items(): - if k: - record_ids_gb[level].add(k) - if v.get('children'): - populate_record_ids_gb_recursively(v['children'], level=level + 1) - - populate_record_ids_gb_recursively(tax_amount_hierarchy) - - sorting_map_list = [] - for i, comodel in enumerate(comodels): - if comodel: - # Relational records. - records = self.env[comodel].with_context(active_test=False).search([('id', 'in', tuple(record_ids_gb[i]))]) - sorting_map = {r.id: (r, j) for j, r in enumerate(records)} - sorting_map_list.append(sorting_map) - else: - # src_tax_type_tax_use. - selection = self.env['account.tax']._fields['type_tax_use'].selection - sorting_map_list.append({v[0]: (v, j) for j, v in enumerate(selection) if v[0] in record_ids_gb[i]}) - - # Compute report lines. - lines = [] - self._populate_lines_recursively( - report, - options, - lines, - sorting_map_list, - groupby_fields, - tax_amount_hierarchy, - warnings=warnings, - ) - return lines - - - # ------------------------------------------------------------------------- - # GENERIC TAX REPORT COMPUTATION (DYNAMIC LINES) - # ------------------------------------------------------------------------- - - @api.model - def _read_generic_tax_report_amounts_no_tax_details(self, report, options, options_by_column_group): - # Fetch the group of taxes. - # If all child taxes have a 'none' type_tax_use, all amounts are aggregated and only the group appears on the report. - company_ids = report.get_report_company_ids(options) - company_domain = self.env['account.tax']._check_company_domain(company_ids) - company_where_query = self.env['account.tax'].with_context(active_test=False)._where_calc(company_domain) - self._cr.execute(SQL( - ''' - SELECT - account_tax.id, - account_tax.type_tax_use, - ARRAY_AGG(child_tax.id) AS child_tax_ids, - ARRAY_AGG(DISTINCT child_tax.type_tax_use) AS child_types - FROM account_tax_filiation_rel account_tax_rel - JOIN account_tax ON account_tax.id = account_tax_rel.parent_tax - JOIN account_tax child_tax ON child_tax.id = account_tax_rel.child_tax - WHERE account_tax.amount_type = 'group' - AND %s - GROUP BY account_tax.id - ''', company_where_query.where_clause or SQL("TRUE") - )) - group_of_taxes_info = {} - child_to_group_of_taxes = {} - for row in self._cr.dictfetchall(): - row['to_expand'] = row['child_types'] != ['none'] - group_of_taxes_info[row['id']] = row - for child_id in row['child_tax_ids']: - child_to_group_of_taxes[child_id] = row['id'] - - results = defaultdict(lambda: { # key: type_tax_use - 'base_amount': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_amount': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_non_deductible': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_deductible': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_due': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'children': defaultdict(lambda: { # key: tax_id - 'base_amount': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_amount': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_non_deductible': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_deductible': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_due': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - }), - }) - - for column_group_key, options in options_by_column_group.items(): - query = report._get_report_query(options, 'strict_range') - - # Fetch the base amounts. - self._cr.execute(SQL( - ''' - SELECT - tax.id AS tax_id, - tax.type_tax_use AS tax_type_tax_use, - src_group_tax.id AS src_group_tax_id, - src_group_tax.type_tax_use AS src_group_tax_type_tax_use, - src_tax.id AS src_tax_id, - src_tax.type_tax_use AS src_tax_type_tax_use, - SUM(account_move_line.balance) AS base_amount - FROM %(table_references)s - JOIN account_move_line_account_tax_rel tax_rel ON account_move_line.id = tax_rel.account_move_line_id - JOIN account_tax tax ON tax.id = tax_rel.account_tax_id - LEFT JOIN account_tax src_tax ON src_tax.id = account_move_line.tax_line_id - LEFT JOIN account_tax src_group_tax ON src_group_tax.id = account_move_line.group_tax_id - WHERE %(search_condition)s - AND ( - /* CABA */ - account_move_line__move_id.always_tax_exigible - OR account_move_line__move_id.tax_cash_basis_rec_id IS NOT NULL - OR tax.tax_exigibility != 'on_payment' - ) - AND ( - ( - /* Tax lines affecting the base of others. */ - account_move_line.tax_line_id IS NOT NULL - AND ( - src_tax.type_tax_use IN ('sale', 'purchase') - OR src_group_tax.type_tax_use IN ('sale', 'purchase') - ) - ) - OR - ( - /* For regular base lines. */ - account_move_line.tax_line_id IS NULL - AND tax.type_tax_use IN ('sale', 'purchase') - ) - ) - GROUP BY tax.id, src_group_tax.id, src_tax.id - ORDER BY src_group_tax.sequence, src_group_tax.id, src_tax.sequence, src_tax.id, tax.sequence, tax.id - ''', - table_references=query.from_clause, - search_condition=query.where_clause, - )) - - group_of_taxes_with_extra_base_amount = set() - for row in self._cr.dictfetchall(): - is_tax_line = bool(row['src_tax_id']) - if is_tax_line: - if row['src_group_tax_id'] \ - and not group_of_taxes_info[row['src_group_tax_id']]['to_expand'] \ - and row['tax_id'] in group_of_taxes_info[row['src_group_tax_id']]['child_tax_ids']: - # Suppose a base of 1000 with a group of taxes 20% affect + 10%. - # The base of the group of taxes must be 1000, not 1200 because the group of taxes is not - # expanded. So the tax lines affecting the base of its own group of taxes are ignored. - pass - elif row['tax_type_tax_use'] == 'none' and child_to_group_of_taxes.get(row['tax_id']): - # The tax line is affecting the base of a 'none' tax belonging to a group of taxes. - # In that case, the amount is accounted as an extra base for that group. However, we need to - # account it only once. - # For example, suppose a tax 10% affect base of subsequent followed by a group of taxes - # 20% + 30%. On a base of 1000.0, the tax line for 10% will affect the base of 20% + 30%. - # However, this extra base must be accounted only once since the base of the group of taxes - # must be 1100.0 and not 1200.0. - group_tax_id = child_to_group_of_taxes[row['tax_id']] - if group_tax_id not in group_of_taxes_with_extra_base_amount: - group_tax_info = group_of_taxes_info[group_tax_id] - results[group_tax_info['type_tax_use']]['children'][group_tax_id]['base_amount'][column_group_key] += row['base_amount'] - group_of_taxes_with_extra_base_amount.add(group_tax_id) - else: - tax_type_tax_use = row['src_group_tax_type_tax_use'] or row['src_tax_type_tax_use'] - results[tax_type_tax_use]['children'][row['tax_id']]['base_amount'][column_group_key] += row['base_amount'] - else: - if row['tax_id'] in group_of_taxes_info and group_of_taxes_info[row['tax_id']]['to_expand']: - # Expand the group of taxes since it contains at least one tax with a type != 'none'. - group_info = group_of_taxes_info[row['tax_id']] - for child_tax_id in group_info['child_tax_ids']: - results[group_info['type_tax_use']]['children'][child_tax_id]['base_amount'][column_group_key] += row['base_amount'] - else: - results[row['tax_type_tax_use']]['children'][row['tax_id']]['base_amount'][column_group_key] += row['base_amount'] - - # Fetch the tax amounts. - - select_deductible = join_deductible = group_by_deductible = SQL() - if options.get('account_journal_report_tax_deductibility_columns'): - select_deductible = SQL(""", repartition.use_in_tax_closing AS trl_tax_closing - , SIGN(repartition.factor_percent) AS trl_factor""") - join_deductible = SQL("""JOIN account_tax_repartition_line repartition - ON account_move_line.tax_repartition_line_id = repartition.id""") - group_by_deductible = SQL(', repartition.use_in_tax_closing, SIGN(repartition.factor_percent)') - - self._cr.execute(SQL( - ''' - SELECT - tax.id AS tax_id, - tax.type_tax_use AS tax_type_tax_use, - group_tax.id AS group_tax_id, - group_tax.type_tax_use AS group_tax_type_tax_use, - SUM(account_move_line.balance) AS tax_amount - %(select_deductible)s - FROM %(table_references)s - JOIN account_tax tax ON tax.id = account_move_line.tax_line_id - %(join_deductible)s - LEFT JOIN account_tax group_tax ON group_tax.id = account_move_line.group_tax_id - WHERE %(search_condition)s - AND ( - /* CABA */ - account_move_line__move_id.always_tax_exigible - OR account_move_line__move_id.tax_cash_basis_rec_id IS NOT NULL - OR tax.tax_exigibility != 'on_payment' - ) - AND ( - (group_tax.id IS NULL AND tax.type_tax_use IN ('sale', 'purchase')) - OR - (group_tax.id IS NOT NULL AND group_tax.type_tax_use IN ('sale', 'purchase')) - ) - GROUP BY tax.id, group_tax.id %(group_by_deductible)s - ''', - select_deductible=select_deductible, - table_references=query.from_clause, - join_deductible=join_deductible, - search_condition=query.where_clause, - group_by_deductible=group_by_deductible, - )) - - for row in self._cr.dictfetchall(): - # Manage group of taxes. - # In case the group of taxes is mixing multiple taxes having a type_tax_use != 'none', consider - # them instead of the group. - tax_id = row['tax_id'] - if row['group_tax_id']: - tax_type_tax_use = row['group_tax_type_tax_use'] - if not group_of_taxes_info[row['group_tax_id']]['to_expand']: - tax_id = row['group_tax_id'] - else: - tax_type_tax_use = row['group_tax_type_tax_use'] or row['tax_type_tax_use'] - - results[tax_type_tax_use]['tax_amount'][column_group_key] += row['tax_amount'] - results[tax_type_tax_use]['children'][tax_id]['tax_amount'][column_group_key] += row['tax_amount'] - - if options.get('account_journal_report_tax_deductibility_columns'): - tax_detail_label = False - if row['trl_factor'] > 0 and tax_type_tax_use == 'purchase': - tax_detail_label = 'tax_deductible' if row['trl_tax_closing'] else 'tax_non_deductible' - elif row['trl_tax_closing'] and (row['trl_factor'] > 0, tax_type_tax_use) in ((False, 'purchase'), (True, 'sale')): - tax_detail_label = 'tax_due' - - if tax_detail_label: - results[tax_type_tax_use][tax_detail_label][column_group_key] += row['tax_amount'] * row['trl_factor'] - results[tax_type_tax_use]['children'][tax_id][tax_detail_label][column_group_key] += row['tax_amount'] * row['trl_factor'] - - return results - - def _read_generic_tax_report_amounts(self, report, options_by_column_group, groupby_fields): - """ Read the tax details to compute the tax amounts. - - :param options_list: The list of report options, one for each period. - :param groupby_fields: A list of tuple (alias, field) representing the way the amounts must be grouped. - :return: A dictionary mapping each groupby key (e.g. a tax_id) to a sub dictionary containing: - - base_amount: The tax base amount expressed in company's currency. - tax_amount The tax amount expressed in company's currency. - children: The children nodes following the same pattern as the current dictionary. - """ - fetch_group_of_taxes = False - - select_clause_list = [] - groupby_query_list = [] - for alias, field in groupby_fields: - select_clause_list.append(SQL("%s AS %s", SQL.identifier(alias, field), SQL.identifier(f'{alias}_{field}'))) - groupby_query_list.append(SQL.identifier(alias, field)) - - # Fetch both info from the originator tax and the child tax to manage the group of taxes. - if alias == 'src_tax': - select_clause_list.append(SQL("%s AS %s", SQL.identifier('tax', field), SQL.identifier(f'tax_{field}'))) - groupby_query_list.append(SQL.identifier('tax', field)) - fetch_group_of_taxes = True - - # Fetch the group of taxes. - # If all children taxes are 'none', all amounts are aggregated and only the group will appear on the report. - # If some children taxes are not 'none', the children are displayed. - group_of_taxes_to_expand = set() - if fetch_group_of_taxes: - group_of_taxes = self.env['account.tax'].with_context(active_test=False).search([('amount_type', '=', 'group')]) - for group in group_of_taxes: - if set(group.children_tax_ids.mapped('type_tax_use')) != {'none'}: - group_of_taxes_to_expand.add(group.id) - - res = {} - for column_group_key, options in options_by_column_group.items(): - query = report._get_report_query(options, 'strict_range') - tax_details_query = self.env['account.move.line']._get_query_tax_details(query.from_clause, query.where_clause) - - # Avoid adding multiple times the same base amount sharing the same grouping_key. - # It could happen when dealing with group of taxes for example. - row_keys = set() - - self._cr.execute(SQL( - ''' - SELECT - %(select_clause)s, - trl.document_type = 'refund' AS is_refund, - SUM(CASE WHEN tdr.display_type = 'rounding' THEN 0 ELSE tdr.base_amount END) AS base_amount, - SUM(tdr.tax_amount) AS tax_amount - FROM (%(tax_details_query)s) AS tdr - JOIN account_tax_repartition_line trl ON trl.id = tdr.tax_repartition_line_id - JOIN account_tax tax ON tax.id = tdr.tax_id - JOIN account_tax src_tax ON - src_tax.id = COALESCE(tdr.group_tax_id, tdr.tax_id) - AND src_tax.type_tax_use IN ('sale', 'purchase') - JOIN account_account account ON account.id = tdr.base_account_id - WHERE tdr.tax_exigible - GROUP BY tdr.tax_repartition_line_id, trl.document_type, %(groupby_query)s - ORDER BY src_tax.sequence, src_tax.id, tax.sequence, tax.id - ''', - select_clause=SQL(',').join(select_clause_list), - tax_details_query=tax_details_query, - groupby_query=SQL(',').join(groupby_query_list), - )) - - for row in self._cr.dictfetchall(): - node = res - - # tuple of values used to prevent adding multiple times the same base amount. - cumulated_row_key = [row['is_refund']] - - for alias, field in groupby_fields: - grouping_key = f'{alias}_{field}' - - # Manage group of taxes. - # In case the group of taxes is mixing multiple taxes having a type_tax_use != 'none', consider - # them instead of the group. - if grouping_key == 'src_tax_id' and row['src_tax_id'] in group_of_taxes_to_expand: - # Add the originator group to the grouping key, to make sure that its base amount is not - # treated twice, for hybrid cases where a tax is both used in a group and independently. - cumulated_row_key.append(row[grouping_key]) - - # Ensure the child tax is used instead of the group. - grouping_key = 'tax_id' - - row_key = row[grouping_key] - cumulated_row_key.append(row_key) - cumulated_row_key_tuple = tuple(cumulated_row_key) - - node.setdefault(row_key, { - 'base_amount': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'tax_amount': {column_group_key: 0.0 for column_group_key in options['column_groups']}, - 'children': {}, - }) - sub_node = node[row_key] - - # Add amounts. - if cumulated_row_key_tuple not in row_keys: - sub_node['base_amount'][column_group_key] += row['base_amount'] - sub_node['tax_amount'][column_group_key] += row['tax_amount'] - - node = sub_node['children'] - row_keys.add(cumulated_row_key_tuple) - - return res - - def _populate_lines_recursively(self, report, options, lines, sorting_map_list, groupby_fields, values_node, index=0, type_tax_use=None, parent_line_id=None, warnings=None): - ''' Populate the list of report lines passed as parameter recursively. At this point, every amounts is already - fetched for every periods and every groupby. - - :param options: The report options. - :param lines: The list of report lines to populate. - :param sorting_map_list: A list of dictionary mapping each encountered key with a weight to sort the results. - :param index: The index of the current element to process (also equals to the level into the hierarchy). - :param groupby_fields: A list of tuple defining in which way tax amounts should be grouped. - :param values_node: The node containing the amounts and children into the hierarchy. - :param type_tax_use: The type_tax_use of the tax. - :param parent_line_id: The line id of the parent line (if any) - :param warnings The warnings dictionnary. - ''' - if index == len(groupby_fields): - return - - alias, field = groupby_fields[index] - groupby_key = f'{alias}_{field}' - - # Sort the keys in order to add the lines in the same order as the records. - sorting_map = sorting_map_list[index] - sorted_keys = sorted(list(values_node.keys()), key=lambda x: sorting_map[x][1]) - - for key in sorted_keys: - - # Compute 'type_tax_use' with the first grouping since 'src_tax_type_tax_use' is always - # the first one. - if groupby_key == 'src_tax_type_tax_use': - type_tax_use = key - sign = -1 if type_tax_use == 'sale' else 1 - - # Prepare columns. - tax_amount_dict = values_node[key] - columns = [] - tax_base_amounts = tax_amount_dict['base_amount'] - tax_amounts = tax_amount_dict['tax_amount'] - - for column in options['columns']: - tax_base_amount = tax_base_amounts[column['column_group_key']] - tax_amount = tax_amounts[column['column_group_key']] - - expr_label = column.get('expression_label') - - if expr_label == 'net': - col_value = sign * tax_base_amount if index == len(groupby_fields) - 1 else '' - - if expr_label == 'tax': - col_value = sign * tax_amount - - columns.append(report._build_column_dict(col_value, column, options=options)) - - # Add the non-deductible, deductible and due tax amounts. - if expr_label == 'tax' and options.get('account_journal_report_tax_deductibility_columns'): - for deduct_type in ('tax_non_deductible', 'tax_deductible', 'tax_due'): - columns.append(report._build_column_dict( - col_value=sign * tax_amount_dict[deduct_type][column['column_group_key']], - col_data={ - 'figure_type': 'monetary', - 'column_group_key': column['column_group_key'], - 'expression_label': deduct_type, - }, - options=options, - )) - - # Prepare line. - default_vals = { - 'columns': columns, - 'level': index if index == 0 else index + 1, - 'unfoldable': False, - } - report_line = self._build_report_line(report, options, default_vals, groupby_key, sorting_map[key][0], parent_line_id, warnings) - - if groupby_key == 'src_tax_id': - report_line['caret_options'] = 'generic_tax_report' - - lines.append((0, report_line)) - - # Process children recursively. - self._populate_lines_recursively( - report, - options, - lines, - sorting_map_list, - groupby_fields, - tax_amount_dict.get('children'), - index=index + 1, - type_tax_use=type_tax_use, - parent_line_id=report_line['id'], - warnings=warnings, - ) - - def _build_report_line(self, report, options, default_vals, groupby_key, value, parent_line_id, warnings=None): - """ Build the report line accordingly to its type. - :param options: The report options. - :param default_vals: The pre-computed report line values. - :param groupby_key: The grouping_key record. - :param value: The value that could be a record. - :param parent_line_id The line id of the parent line (if any, can be None otherwise) - :param warnings: The warnings dictionary. - :return: A python dictionary. - """ - report_line = dict(default_vals) - if parent_line_id is not None: - report_line['parent_id'] = parent_line_id - - if groupby_key == 'src_tax_type_tax_use': - type_tax_use_option = value - report_line['id'] = report._get_generic_line_id(None, None, markup=type_tax_use_option[0], parent_line_id=parent_line_id) - report_line['name'] = type_tax_use_option[1] - - elif groupby_key == 'src_tax_id': - tax = value - report_line['id'] = report._get_generic_line_id(tax._name, tax.id, parent_line_id=parent_line_id) - - if tax.amount_type == 'percent': - report_line['name'] = f"{tax.name} ({tax.amount}%)" - - if warnings is not None: - self._check_line_consistency(report, options, report_line, tax, warnings) - elif tax.amount_type == 'fixed': - report_line['name'] = f"{tax.name} ({tax.amount})" - else: - report_line['name'] = tax.name - - if options.get('multi-company'): - report_line['name'] = f"{report_line['name']} - {tax.company_id.display_name}" - - elif groupby_key == 'account_id': - account = value - report_line['id'] = report._get_generic_line_id(account._name, account.id, parent_line_id=parent_line_id) - - if options.get('multi-company'): - report_line['name'] = f"{account.display_name} - {account.company_id.display_name}" - else: - report_line['name'] = account.display_name - - return report_line - - def _check_line_consistency(self, report, options, report_line, tax, warnings=None): - tax_applied = tax.amount * sum(tax.invoice_repartition_line_ids.filtered(lambda tax_rep: tax_rep.repartition_type == 'tax').mapped('factor')) / 100 - - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - net_value = next((col['no_format'] for col in report_line['columns'] if col['column_group_key'] == column_group_key and col['expression_label'] == 'net'), 0) - tax_value = next((col['no_format'] for col in report_line['columns'] if col['column_group_key'] == column_group_key and col['expression_label'] == 'tax'), 0) - - if net_value == '': # noqa: PLC1901 - continue - - currency = self.env.company.currency_id - computed_tax_amount = float(net_value or 0) * tax_applied - is_inconsistent = currency.compare_amounts(computed_tax_amount, tax_value) - - if is_inconsistent: - error = abs(abs(tax_value) - abs(computed_tax_amount)) / float(net_value or 1) - - # Error is bigger than 0.1%. We can not ignore it. - if error > 0.001: - report_line['alert'] = True - warnings['at_accounting.tax_report_warning_lines_consistency'] = {'alert_type': 'danger'} - - return - - # ------------------------------------------------------------------------- - # BUTTONS & CARET OPTIONS - # ------------------------------------------------------------------------- - - def caret_option_audit_tax(self, options, params): - report = self.env['account.report'].browse(options['report_id']) - model, tax_id = report._get_model_info_from_id(params['line_id']) - - if model != 'account.tax': - raise UserError(_("Cannot audit tax from another model than account.tax.")) - - tax = self.env['account.tax'].browse(tax_id) - - if tax.amount_type == 'group': - tax_affecting_base_domain = [ - ('tax_ids', 'in', tax.children_tax_ids.ids), - ('tax_repartition_line_id', '!=', False), - ] - else: - tax_affecting_base_domain = [ - ('tax_ids', '=', tax.id), - ('tax_ids.type_tax_use', '=', tax.type_tax_use), - ('tax_repartition_line_id', '!=', False), - ] - - domain = report._get_options_domain(options, 'strict_range') + expression.OR(( - # Base lines - [ - ('tax_ids', 'in', tax.ids), - ('tax_ids.type_tax_use', '=', tax.type_tax_use), - ('tax_repartition_line_id', '=', False), - ], - # Tax lines - [ - ('group_tax_id', '=', tax.id) if tax.amount_type == 'group' else ('tax_line_id', '=', tax.id), - ], - # Tax lines acting as base lines - tax_affecting_base_domain, - )) - - ctx = self._context.copy() - ctx.update({'search_default_group_by_account': 2, 'expand': 1}) - - return { - 'type': 'ir.actions.act_window', - 'name': _('Journal Items for Tax Audit'), - 'res_model': 'account.move.line', - 'views': [[self.env.ref('account.view_move_line_tax_audit_tree').id, 'list']], - 'domain': domain, - 'context': ctx, - } - - -class GenericTaxReportCustomHandlerAT(models.AbstractModel): - _name = 'account.generic.tax.report.handler.account.tax' - _inherit = 'account.generic.tax.report.handler' - _description = 'Generic Tax Report Custom Handler (Account -> Tax)' - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - return super()._get_dynamic_lines(report, options, 'account_tax', warnings) - - -class GenericTaxReportCustomHandlerTA(models.AbstractModel): - _name = 'account.generic.tax.report.handler.tax.account' - _inherit = 'account.generic.tax.report.handler' - _description = 'Generic Tax Report Custom Handler (Tax -> Account)' - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - return super()._get_dynamic_lines(report, options, 'tax_account', warnings) diff --git a/addons/at_accounting/models/account_journal.py b/addons/at_accounting/models/account_journal.py deleted file mode 100644 index 114ea1d..0000000 --- a/addons/at_accounting/models/account_journal.py +++ /dev/null @@ -1,299 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import models, tools, _ -from odoo.addons.base.models.res_bank import sanitize_account_number -from odoo.exceptions import UserError, RedirectWarning - - -class AccountJournal(models.Model): - _inherit = "account.journal" - - def _get_bank_statements_available_import_formats(self): - """ Returns a list of strings representing the supported import formats. - """ - return [] - - def __get_bank_statements_available_sources(self): - rslt = super(AccountJournal, self).__get_bank_statements_available_sources() - formats_list = self._get_bank_statements_available_import_formats() - if formats_list: - formats_list.sort() - import_formats_str = ', '.join(formats_list) - rslt.append(("file_import", _("Manual (or import %(import_formats)s)", import_formats=import_formats_str))) - return rslt - - def create_document_from_attachment(self, attachment_ids=None): - journal = self or self.browse(self.env.context.get('default_journal_id')) - if journal.type in ('bank', 'credit', 'cash'): - attachments = self.env['ir.attachment'].browse(attachment_ids) - if not attachments: - raise UserError(_("No attachment was provided")) - return journal._import_bank_statement(attachments) - return super().create_document_from_attachment(attachment_ids) - - def _import_bank_statement(self, attachments): - """ Process the file chosen in the wizard, create bank statement(s) and go to reconciliation. """ - if any(not a.raw for a in attachments): - raise UserError(_("You uploaded an invalid or empty file.")) - - statement_ids_all = [] - notifications_all = {} - errors = {} - # Let the appropriate implementation module parse the file and return the required data - # The active_id is passed in context in case an implementation module requires information about the wizard state (see QIF) - for attachment in attachments: - try: - currency_code, account_number, stmts_vals = self._parse_bank_statement_file(attachment) - # Check raw data - self._check_parsed_data(stmts_vals, account_number) - # Try to find the currency and journal in odoo - journal = self._find_additional_data(currency_code, account_number) - # If no journal found, ask the user about creating one - if not journal.default_account_id: - raise UserError(_('You have to set a Default Account for the journal: %s', journal.name)) - # Prepare statement data to be used for bank statements creation - stmts_vals = self._complete_bank_statement_vals(stmts_vals, journal, account_number, attachment) - # Create the bank statements - statement_ids, dummy, notifications = self._create_bank_statements(stmts_vals) - statement_ids_all.extend(statement_ids) - - # Now that the import worked out, set it as the bank_statements_source of the journal - if journal.bank_statements_source != 'file_import': - # Use sudo() because only 'account.group_account_manager' - # has write access on 'account.journal', but 'account.group_account_user' - # must be able to import bank statement files - journal.sudo().bank_statements_source = 'file_import' - - msg = "" - for notif in notifications: - msg += ( - f"{notif['message']}" - ) - if notifications: - notifications_all[attachment.name] = msg - except (UserError, RedirectWarning) as e: - errors[attachment.name] = e.args[0] - - statements = self.env['account.bank.statement'].browse(statement_ids_all) - line_to_reconcile = statements.line_ids - if line_to_reconcile: - # 'limit_time_real_cron' defaults to -1. - # Manual fallback applied for non-POSIX systems where this key is disabled (set to None). - cron_limit_time = tools.config['limit_time_real_cron'] or -1 - limit_time = cron_limit_time if 0 < cron_limit_time < 180 else 180 - line_to_reconcile._cron_try_auto_reconcile_statement_lines(limit_time=limit_time) - - result = self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - extra_domain=[('statement_id', 'in', statements.ids)], - default_context={ - 'search_default_not_matched': True, - 'default_journal_id': statements[:1].journal_id.id, - 'notifications': notifications_all, - }, - ) - - if errors: - error_msg = _("The following files could not be imported:\n") - error_msg += "\n".join([f"- {attachment_name}: {msg}" for attachment_name, msg in errors.items()]) - if statements: - self.env.cr.commit() # save the correctly uploaded statements to the db before raising the errors - raise RedirectWarning(error_msg, result, _('View successfully imported statements')) - else: - raise UserError(error_msg) - return result - - def _parse_bank_statement_file(self, attachment) -> tuple: - """ Each module adding a file support must extends this method. It processes the file if it can, returns super otherwise, resulting in a chain of responsability. - This method parses the given file and returns the data required by the bank statement import process, as specified below. - rtype: triplet (if a value can't be retrieved, use None) - - currency code: string (e.g: 'EUR') - The ISO 4217 currency code, case insensitive - - account number: string (e.g: 'BE1234567890') - The number of the bank account which the statement belongs to - - bank statements data: list of dict containing (optional items marked by o) : - - 'name': string (e.g: '000000123') - - 'date': date (e.g: 2013-06-26) - -o 'balance_start': float (e.g: 8368.56) - -o 'balance_end_real': float (e.g: 8888.88) - - 'transactions': list of dict containing : - - 'name': string (e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01') - - 'date': date - - 'amount': float - - 'unique_import_id': string - -o 'account_number': string - Will be used to find/create the res.partner.bank in odoo - -o 'note': string - -o 'partner_name': string - -o 'ref': string - """ - raise RedirectWarning( - message=_("Could not make sense of the given file.\nDid you install the module to support this type of file?"), - action=self.env.ref('base.open_module_tree').id, - button_text=_("Go to Apps"), - additional_context={ - 'search_default_name': 'account_bank_statement_import', - 'search_default_extra': True, - }, - ) - - def _check_parsed_data(self, stmts_vals, account_number): - """ Basic and structural verifications """ - if len(stmts_vals) == 0: - raise UserError(_( - 'This file doesn\'t contain any statement for account %s.\nIf it contains transactions for more than one account, it must be imported on each of them.', - account_number, - )) - - no_st_line = True - for vals in stmts_vals: - if vals['transactions'] and len(vals['transactions']) > 0: - no_st_line = False - break - if no_st_line: - raise UserError(_( - 'This file doesn\'t contain any transaction for account %s.\nIf it contains transactions for more than one account, it must be imported on each of them.', - account_number, - )) - - def _statement_import_check_bank_account(self, account_number): - # Needed for CH to accommodate for non-unique account numbers - sanitized_acc_number = self.bank_account_id.sanitized_acc_number.split(" ")[0] - # Needed for BNP France - if len(sanitized_acc_number) == 27 and len(account_number) == 11 and sanitized_acc_number[:2].upper() == "FR": - return sanitized_acc_number[14:-2] == account_number - - # Needed for Credit Lyonnais (LCL) - if len(sanitized_acc_number) == 27 and len(account_number) == 7 and sanitized_acc_number[:2].upper() == "FR": - return sanitized_acc_number[18:-2] == account_number - - return sanitized_acc_number == account_number - - def _find_additional_data(self, currency_code, account_number): - """ Look for the account.journal using values extracted from the - statement and make sure it's consistent. - """ - company_currency = self.env.company.currency_id - currency = None - sanitized_account_number = sanitize_account_number(account_number) - - if currency_code: - currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1) - if not currency: - raise UserError(_("No currency found matching '%s'.", currency_code)) - if currency == company_currency: - currency = False - - journal = self - if account_number: - # No bank account on the journal : create one from the account number of the statement - if journal and not journal.bank_account_id: - journal.set_bank_account(account_number) - # No journal passed to the wizard : try to find one using the account number of the statement - elif not journal: - journal = self.search([('bank_account_id.sanitized_acc_number', '=', sanitized_account_number)]) - if not journal: - # Sometimes the bank returns only part of the full account number (e.g. local account number instead of full IBAN) - partial_match = self.search([('bank_account_id.sanitized_acc_number', 'ilike', sanitized_account_number)]) - if len(partial_match) == 1: - journal = partial_match - # Already a bank account on the journal : check it's the same as on the statement - else: - if not self._statement_import_check_bank_account(sanitized_account_number): - raise UserError(_('The account of this statement (%(account)s) is not the same as the journal (%(journal)s).', account=account_number, journal=journal.bank_account_id.acc_number)) - - # If importing into an existing journal, its currency must be the same as the bank statement - if journal: - journal_currency = journal.currency_id or journal.company_id.currency_id - if currency is None: - currency = journal_currency - if currency and currency != journal_currency: - statement_cur_code = not currency and company_currency.name or currency.name - journal_cur_code = not journal_currency and company_currency.name or journal_currency.name - raise UserError(_('The currency of the bank statement (%(code)s) is not the same as the currency of the journal (%(journal)s).', code=statement_cur_code, journal=journal_cur_code)) - - if not journal: - raise UserError(_('Cannot find in which journal import this statement. Please manually select a journal.')) - return journal - - def _complete_bank_statement_vals(self, stmts_vals, journal, account_number, attachment): - for st_vals in stmts_vals: - if not st_vals.get('reference'): - st_vals['reference'] = attachment.name - for line_vals in st_vals['transactions']: - line_vals['journal_id'] = journal.id - unique_import_id = line_vals.get('unique_import_id') - if unique_import_id: - sanitized_account_number = sanitize_account_number(account_number) - line_vals['unique_import_id'] = (sanitized_account_number and sanitized_account_number + '-' or '') + str(journal.id) + '-' + unique_import_id - - if not line_vals.get('partner_bank_id'): - # Find the partner and his bank account or create the bank account. The partner selected during the - # reconciliation process will be linked to the bank when the statement is closed. - identifying_string = line_vals.get('account_number') - if identifying_string: - if line_vals.get('partner_id'): - partner_bank = self.env['res.partner.bank'].search([ - ('acc_number', '=', identifying_string), - ('partner_id', '=', line_vals['partner_id']) - ]) - else: - partner_bank = self.env['res.partner.bank'].search([ - ('acc_number', '=', identifying_string), - ('company_id', 'in', (False, journal.company_id.id)) - ]) - # If multiple partners share the same account number, do not try to guess and just avoid setting it - if partner_bank and len(partner_bank) == 1: - line_vals['partner_bank_id'] = partner_bank.id - line_vals['partner_id'] = partner_bank.partner_id.id - return stmts_vals - - def _create_bank_statements(self, stmts_vals, raise_no_imported_file=True): - """ Create new bank statements from imported values, filtering out already imported transactions, and returns data used by the reconciliation widget """ - BankStatement = self.env['account.bank.statement'] - BankStatementLine = self.env['account.bank.statement.line'] - - # Filter out already imported transactions and create statements - statement_ids = [] - statement_line_ids = [] - ignored_statement_lines_import_ids = [] - for st_vals in stmts_vals: - filtered_st_lines = [] - for line_vals in st_vals['transactions']: - if (line_vals['amount'] != 0 - and ('unique_import_id' not in line_vals - or not line_vals['unique_import_id'] - or not bool(BankStatementLine.sudo().search([('unique_import_id', '=', line_vals['unique_import_id'])], limit=1)))): - filtered_st_lines.append(line_vals) - else: - ignored_statement_lines_import_ids.append(line_vals) - if st_vals.get('balance_start') is not None: - st_vals['balance_start'] += float(line_vals['amount']) - - if len(filtered_st_lines) > 0: - # Remove values that won't be used to create records - st_vals.pop('transactions', None) - # Create the statement - st_vals['line_ids'] = [[0, False, line] for line in filtered_st_lines] - statement = BankStatement.with_context(default_journal_id=self.id).create(st_vals) - if not statement.name: - statement.name = st_vals['reference'] - statement_ids.append(statement.id) - statement_line_ids.extend(statement.line_ids.ids) - - # Create the report. - if statement.is_complete and not self._context.get('skip_pdf_attachment_generation'): - statement.action_generate_attachment() - - if len(statement_line_ids) == 0 and raise_no_imported_file: - raise UserError(_('You already have imported that file.')) - - # Prepare import feedback - notifications = [] - num_ignored = len(ignored_statement_lines_import_ids) - if num_ignored > 0: - notifications += [{ - 'type': 'warning', - 'message': _("%d transactions had already been imported and were ignored.", num_ignored) - if num_ignored > 1 - else _("1 transaction had already been imported and was ignored."), - }] - return statement_ids, statement_line_ids, notifications diff --git a/addons/at_accounting/models/account_journal_csv.py b/addons/at_accounting/models/account_journal_csv.py deleted file mode 100644 index 2add424..0000000 --- a/addons/at_accounting/models/account_journal_csv.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import _, models -from odoo.exceptions import UserError - - -class AccountJournal(models.Model): - _inherit = 'account.journal' - - def _get_bank_statements_available_import_formats(self): - rslt = super()._get_bank_statements_available_import_formats() - rslt.extend(['CSV', 'XLS', 'XLSX']) - return rslt - - def _check_file_format(self, filename): - return filename and filename.lower().strip().endswith(('.csv', '.xls', '.xlsx')) - - def _import_bank_statement(self, attachments): - # In case of CSV files, only one file can be imported at a time. - if len(attachments) > 1: - csv = [bool(self._check_file_format(att.name)) for att in attachments] - if True in csv and False in csv: - raise UserError(_('Mixing CSV files with other file types is not allowed.')) - if csv.count(True) > 1: - raise UserError(_('Only one CSV file can be selected.')) - return super()._import_bank_statement(attachments) - - if not self._check_file_format(attachments.name): - return super()._import_bank_statement(attachments) - ctx = dict(self.env.context) - import_wizard = self.env['base_import.import'].create({ - 'res_model': 'account.bank.statement.line', - 'file': attachments.raw, - 'file_name': attachments.name, - 'file_type': attachments.mimetype, - }) - ctx['wizard_id'] = import_wizard.id - ctx['default_journal_id'] = self.id - return { - 'type': 'ir.actions.client', - 'tag': 'import_bank_stmt', - 'params': { - 'model': 'account.bank.statement.line', - 'context': ctx, - 'filename': 'bank_statement_import.csv', - } - } diff --git a/addons/at_accounting/models/account_journal_dashboard.py b/addons/at_accounting/models/account_journal_dashboard.py deleted file mode 100644 index a60310e..0000000 --- a/addons/at_accounting/models/account_journal_dashboard.py +++ /dev/null @@ -1,78 +0,0 @@ -from odoo import models -import ast - -class account_journal(models.Model): - _inherit = "account.journal" - - def action_open_reconcile(self): - self.ensure_one() - - if self.type in ('bank', 'cash', 'credit'): - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - default_context={ - 'default_journal_id': self.id, - 'search_default_journal_id': self.id, - 'search_default_not_matched': True, - }, - ) - else: - # Open reconciliation view for customers/suppliers - return self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_move_line_posted_unreconciled') - - def action_open_to_check(self): - self.ensure_one() - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - default_context={ - 'search_default_to_check': True, - 'search_default_journal_id': self.id, - 'default_journal_id': self.id, - }, - ) - - def action_open_bank_transactions(self): - self.ensure_one() - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - default_context={ - 'search_default_journal_id': self.id, - 'default_journal_id': self.id - }, - kanban_first=False, - ) - - def action_open_reconcile_statement(self): - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - default_context={ - 'search_default_statement_id': self.env.context.get('statement_id'), - }, - ) - - def open_action(self): - # EXTENDS account - # set default action for liquidity journals in dashboard - - if self.type in ('bank', 'cash', 'credit') and not self._context.get('action_name'): - self.ensure_one() - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - extra_domain=[('line_ids.account_id', '=', self.default_account_id.id)], - default_context={ - 'default_journal_id': self.id, - 'search_default_journal_id': self.id, - }, - ) - return super().open_action() - - def _fill_general_dashboard_data(self, dashboard_data): - super()._fill_general_dashboard_data(dashboard_data) - for journal in self.filtered(lambda journal: journal.type == 'general'): - dashboard_data[journal.id]['is_account_tax_periodicity_journal'] = journal == journal.company_id._get_tax_closing_journal() - - def action_open_bank_balance_in_gl(self): - ''' Show the bank balance inside the General Ledger report. - :return: An action opening the General Ledger. - ''' - self.ensure_one() - action = self.env["ir.actions.actions"]._for_xml_id("at_accounting.action_account_report_general_ledger") - - action['context'] = dict(ast.literal_eval(action['context']), default_filter_accounts=self.default_account_id.code) - - return action diff --git a/addons/at_accounting/models/account_journal_report.py b/addons/at_accounting/models/account_journal_report.py deleted file mode 100644 index dba4972..0000000 --- a/addons/at_accounting/models/account_journal_report.py +++ /dev/null @@ -1,1324 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import io -import datetime - -from PIL import ImageFont -from markupsafe import Markup - -from odoo import models, _ -from odoo.tools import SQL -from odoo.tools.misc import xlsxwriter, file_path -from collections import defaultdict - -XLSX_GRAY_200 = '#EEEEEE' -XLSX_BORDER_COLOR = '#B4B4B4' -XLSX_FONT_SIZE_DEFAULT = 8 -XLSX_FONT_SIZE_HEADING = 11 - - -class JournalReportCustomHandler(models.AbstractModel): - _name = "account.journal.report.handler" - _inherit = "account.report.custom.handler" - _description = "Journal Report Custom Handler" - - def _custom_options_initializer(self, report, options, previous_options): - """ Initialize the options for the journal report. """ - - # Initialise the custom option for this report. - options['ignore_totals_below_sections'] = True - options['show_payment_lines'] = previous_options.get('show_payment_lines', True) - - def _get_custom_display_config(self): - return { - 'css_custom_class': 'journal_report', - 'pdf_css_custom_class': 'journal_report_pdf', - 'components': { - 'AccountReportLine': 'at_accounting.JournalReportLine', - }, - 'templates': { - 'AccountReportFilters': 'at_accounting.JournalReportFilters', - 'AccountReportLineName': 'at_accounting.JournalReportLineName', - } - } - - ########################################################################## - # UI - ########################################################################## - - def _report_custom_engine_journal_report(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - - def build_result_dict(current_groupby, query_line): - """ - Creates a line entry used by the custom engine - """ - if current_groupby == 'account_id': - code = query_line['account_code'][0] - elif current_groupby == 'journal_id': - code = query_line['journal_code'][0] - else: - code = None - - result_line_dict = { - 'code': code, - 'credit': query_line['credit'], - 'debit': query_line['debit'], - 'balance': query_line['balance'] if current_groupby == 'account_id' else None - } - return query_line['grouping_key'], result_line_dict - - report = self.env['account.report'].browse(options['report_id']) - report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - # If it is the first line, we want to render our column label - # Since we don't use the one from the base report - if not current_groupby: - return { - 'code': None, - 'debit': None, - 'credit': None, - 'balance': None - } - - query = report._get_report_query(options, 'strict_range') - account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - - groupby_clause = SQL.identifier('account_move_line', current_groupby) - select_from_groupby = SQL('%s AS grouping_key', groupby_clause) - - query = SQL( - """ - SELECT - %(select_from_groupby)s, - ARRAY_AGG(DISTINCT %(account_code)s) AS account_code, - ARRAY_AGG(DISTINCT j.code) AS journal_code, - SUM("account_move_line".debit) AS debit, - SUM("account_move_line".credit) AS credit, - SUM("account_move_line".balance) AS balance - FROM %(table)s - JOIN account_move am ON am.id = account_move_line.move_id - JOIN account_journal j ON j.id = am.journal_id - JOIN res_company cp ON cp.id = am.company_id - WHERE %(case_statement)s AND %(search_conditions)s - GROUP BY %(groupby_clause)s - ORDER BY %(groupby_clause)s - """, - select_from_groupby=select_from_groupby, - account_code=account_code, - table=query.from_clause, - search_conditions=query.where_clause, - case_statement=self._get_payment_lines_filter_case_statement(options), - groupby_clause=groupby_clause - ) - self._cr.execute(query) - query_lines = self._cr.dictfetchall() - result_lines = [] - - for query_line in query_lines: - result_lines.append(build_result_dict(current_groupby, query_line)) - - return result_lines - - def _custom_line_postprocessor(self, report, options, lines): - """ - Process the lines generated by the engine to add metadata and add the tax summary lines - """ - new_lines = [] - - for i, line in enumerate(lines): - new_lines.append(line) - line_id = line['id'] - - line_model, res_id = report._get_model_info_from_id(line_id) - if line_model == 'account.journal': - line['journal_id'] = res_id - elif line_model == 'account.account': - res_ids_map = report._get_res_ids_from_line_id(line_id, ['account.journal', 'account.account']) - line['journal_id'] = res_ids_map['account.journal'] - line['account_id'] = res_ids_map['account.account'] - line['date'] = options['date'] - - journal = self.env['account.journal'].browse(line['journal_id']) - - # If it is the last line of the journal section - # Check if the journal has taxes and if so, add the tax summaries - if (i + 1 == len(lines) or (i + 1 < len(lines) and report._get_model_info_from_id(lines[i + 1]['id'])[0] != 'account.account')) and self._section_has_tax(options, journal.id): - tax_summary_line = { - 'id': report._get_generic_line_id(False, False, parent_line_id=line['parent_id'], markup='tax_report_section'), - 'name': '', - 'parent_id': line['parent_id'], - 'journal_id': journal.id, - 'is_tax_section_line': True, - 'columns': [], - 'colspan': len(options['columns']) + 1, - 'level': 4, - **self._get_tax_summary_section(options, {'id': journal.id, 'type': journal.type}) - } - new_lines.append(tax_summary_line) - - # If we render the first level it means that we need to render - # the global tax summary lines - if report._get_model_info_from_id(lines[0]['id'])[0] == 'account.report.line': - if self._section_has_tax(options, False): - # We only add the global summary line if it has taxes - new_lines.append({ - 'id': report._get_generic_line_id(False, False, markup='tax_report_section_heading'), - 'name': _('Global Tax Summary'), - 'level': 0, - 'columns': [], - 'unfoldable': False, - 'colspan': len(options['columns']) + 1 - # We want it to take the whole line. It makes it easier to unfold it. - }) - summary_line = { - 'id': report._get_generic_line_id(False, False, markup='tax_report_section'), - 'name': '', - 'is_tax_section_line': True, - 'columns': [], - 'colspan': len(options['columns']) + 1, - 'level': 4, - 'class': 'o_account_reports_ja_subtable', - **self._get_tax_summary_section(options) - } - new_lines.append(summary_line) - - return new_lines - - ########################################################################## - # PDF Export - ########################################################################## - - def export_to_pdf(self, options): - """ - Overrides the default export_to_pdf function from account.report to - not use the default lines system since we make a different report - from the UI - """ - report = self.env['account.report'].browse(options['report_id']) - base_url = report.get_base_url() - print_options = { - **report.get_options(previous_options={**options, 'export_mode': 'print'}), - 'css_custom_class': self._get_custom_display_config().get('pdf_css_custom_class', 'journal_report_pdf') - } - rcontext = { - 'mode': 'print', - 'base_url': base_url, - 'company': self.env.company, - } - - footer = self.env['ir.actions.report']._render_template('at_accounting.internal_layout', values=rcontext) - footer = self.env['ir.actions.report']._render_template('web.minimal_layout', values=dict(rcontext, subst=True, body=Markup(footer.decode()))) - - document_data = self._generate_document_data_for_export(report, print_options, 'pdf') - render_values = { - 'report': report, - 'options': print_options, - 'base_url': base_url, - 'document_data': document_data - } - body = self.env['ir.qweb']._render('at_accounting.journal_report_pdf_export_main', render_values) - - action_report = self.env['ir.actions.report'] - pdf_file_stream = io.BytesIO(action_report._run_wkhtmltopdf( - [body], - footer=footer.decode(), - landscape=False, - specific_paperformat_args={ - 'data-report-margin-top': 10, - 'data-report-header-spacing': 10, - 'data-report-margin-bottom': 15, - } - )) - - pdf_result = pdf_file_stream.getvalue() - pdf_file_stream.close() - - return { - 'file_name': report.get_default_report_filename(print_options, 'pdf'), - 'file_content': pdf_result, - 'file_type': 'pdf', - } - - ########################################################################## - # XLSX Export - ########################################################################## - - def export_to_xlsx(self, options, response=None): - """ - Overrides the default XLSX Generation from account.repor to use a custom one. - """ - output = io.BytesIO() - workbook = xlsxwriter.Workbook(output, { - 'in_memory': True, - 'strings_to_formulas': False, - }) - report = self.env['account.report'].search([('id', '=', options['report_id'])], limit=1) - print_options = report.get_options(previous_options={**options, 'export_mode': 'print'}) - document_data = self._generate_document_data_for_export(report, print_options, 'xlsx') - - # We need to use fonts to calculate column width otherwise column width would be ugly - # Using Lato as reference font is a hack and is not recommended. Customer computers don't have this font by default and so - # the generated xlsx wouldn't have this font. Since it is not by default, we preferred using Arial font as default and keep - # Lato as reference for columns width calculations. - fonts = {} - for font_size in (XLSX_FONT_SIZE_HEADING, XLSX_FONT_SIZE_DEFAULT): - fonts[font_size] = defaultdict() - for font_type in ('Reg', 'Bol', 'RegIta', 'BolIta'): - try: - lato_path = f'web/static/fonts/lato/Lato-{font_type}-webfont.ttf' - fonts[font_size][font_type] = ImageFont.truetype(file_path(lato_path), font_size) - except (OSError, FileNotFoundError): - # This won't give great result, but it will work. - fonts[font_size][font_type] = ImageFont.load_default() - - for journal_vals in document_data['journals_vals']: - cursor_x = 0 - cursor_y = 0 - - # Default sheet properties - sheet = workbook.add_worksheet(journal_vals['name'][:31]) - columns = journal_vals['columns'] - - for column in columns: - align = 'left' - if 'o_right_alignment' in column.get('class', ''): - align = 'right' - self._write_cell(cursor_x, cursor_y, column['name'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_HEADING, - True, XLSX_GRAY_200, align, 2, 2) - cursor_x = cursor_x + 1 - - # Set cursor coordinates for the table generation - cursor_y += 1 - cursor_x = 0 - for line in journal_vals['lines'][:-1]: - is_first_aml_line = False - for column in columns: - border_top = 0 if not is_first_aml_line else 1 - align = 'left' - - if line.get(column['label'], {}).get('data'): - data = line[column['label']]['data'] - is_date = isinstance(data, datetime.date) - bold = False - - if 'o_right_alignment' in column.get('class', ''): - align = 'right' - - if line[column['label']].get('class') and 'o_bold' in line[column['label']]['class']: - # if the cell has bold styling, should only be on the first line of each aml - is_first_aml_line = True - border_top = 1 - bold = True - - self._write_cell(cursor_x, cursor_y, data, 1, is_date, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, - bold, 'white', align, 0, border_top, XLSX_BORDER_COLOR) - - else: - # Empty value - self._write_cell(cursor_x, cursor_y, '', 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, False, - 'white', align, 0, border_top, XLSX_BORDER_COLOR) - - cursor_x += 1 - cursor_x = 0 - cursor_y += 1 - - # Draw total line - total_line = journal_vals['lines'][-1] - for column in columns: - data = '' - align = 'left' - - if total_line.get(column['label'], {}).get('data'): - data = total_line[column['label']]['data'] - - if 'o_right_alignment' in column.get('class', ''): - align = 'right' - - self._write_cell(cursor_x, cursor_y, data, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, - XLSX_GRAY_200, align, 2, 2) - cursor_x += 1 - - cursor_x = 0 - - sheet.set_default_row(20) - sheet.set_row(0, 30) - - # Tax tables drawing - if journal_vals.get('tax_summary'): - self._write_tax_summaries_to_sheet(report, workbook, sheet, fonts, len(columns) + 1, 1, journal_vals['tax_summary']) - - if document_data.get('global_tax_summary'): - self._write_tax_summaries_to_sheet( - report, - workbook, - workbook.add_worksheet(_('Global Tax Summary')[:31]), - fonts, - 0, - 0, - document_data['global_tax_summary'] - ) - - workbook.close() - output.seek(0) - generated_file = output.read() - output.close() - - return { - 'file_name': report.get_default_report_filename(options, 'xlsx'), - 'file_content': generated_file, - 'file_type': 'xlsx', - } - - def _write_cell(self, x, y, value, colspan, datetime, report, fonts, workbook, sheet, font_size, bold=False, - bg_color='white', align='left', border_bottom=0, border_top=0, border_color='0x000000'): - """ - Write a value to a specific cell in the sheet with specific styling - - This helps to not create style format for every use case - - :param x: The x coordinate of the cell to write in - :param y: The y coordinate of the cell to write in - :param value: The value to write - :param colspan: The number of columns to extend - :param datetime: True if the value is a date else False - :param report: The current report - :param fonts: The fonts used to calculate the size of each cells. We use Lato because we cannot get Arial but, we write in Arial since we cannot embed Lato on the worksheet - :param workbook: The workbook currently using - :param sheet: The sheet from the workbook to write on - :param font_size: The font size to write with - :param bold: True if the written value should be bold default: False - :param bg_color: The background color of the cell in hex or string ex: '#fff' default: 'white' - :param align: The alignement of the text ex: 'left', 'right', 'center' default: 'left' - :param border_bottom: The width of the bottom border default: 0 - :param border_top: The width of the top border default: 0 - :param border_color: The color of the borders in hex or string default: '0x000' - """ - style = workbook.add_format({ - 'font_name': 'Arial', - 'font_size': font_size, - 'bold': bold, - 'bg_color': bg_color, - 'align': align, - 'bottom': border_bottom, - 'top': border_top, - 'border_color': border_color, - }) - - if colspan == 1: - if datetime: - style.set_num_format('yyyy-mm-dd') - sheet.write_datetime(y, x, value, style) - else: - # Some account_move_lines cells can have multiple lines: one for the title then some additional lines for text. - # On Xlsx it's better to keep everything on one line so when you click on cell, all the value is shown and not juste the title - if isinstance(value, str): - value = value.replace('\n', ' ') - report._set_xlsx_cell_sizes(sheet, fonts[font_size], x, y, value, style, colspan > 1) - sheet.write(y, x, value, style) - else: - sheet.merge_range(y, x, y, x + colspan - 1, value, style) - - def _write_tax_summaries_to_sheet(self, report, workbook, sheet, fonts, start_x, start_y, tax_summary): - cursor_x = start_x - cursor_y = start_y - - # Tax applied - columns = [] - taxes = tax_summary.get('tax_report_lines') - if taxes: - start_align_right = start_x + 1 - - if len(taxes) > 1: - start_align_right += 1 - columns.append(_('Country')) - - columns += [_('Name'), _('Base Amount'), _('Tax Amount')] - if tax_summary.get('tax_non_deductible_column'): - columns.append(_('Non-Deductible')) - if tax_summary.get('tax_deductible_column'): - columns.append(_('Deductible')) - if tax_summary.get('tax_due_column'): - columns.append(_('Due')) - - # Draw Tax Applied Table - # Write tax applied header amd columns - self._write_cell(cursor_x, cursor_y, _('Taxes Applied'), len(columns), False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_HEADING, True, 'white', 'left', 2) - cursor_y += 1 - for column in columns: - align = 'left' - if cursor_x >= start_align_right: - align = 'right' - self._write_cell(cursor_x, cursor_y, column, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, - XLSX_GRAY_200, align, 2) - cursor_x += 1 - - cursor_x = start_x - cursor_y += 1 - - for country in taxes: - is_country_first_line = True - for tax in taxes[country]: - if len(taxes) > 1: - if is_country_first_line: - is_country_first_line = not is_country_first_line - self._write_cell(cursor_x, cursor_y, country, 1, False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_DEFAULT, True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR) - - cursor_x += 1 - - self._write_cell(cursor_x, cursor_y, tax['name'], 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, - True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR) - self._write_cell(cursor_x + 1, cursor_y, tax['base_amount'], 1, False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - self._write_cell(cursor_x + 2, cursor_y, tax['tax_amount'], 1, False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - cursor_x += 3 - - if tax_summary.get('tax_non_deductible_column'): - self._write_cell(cursor_x, cursor_y, tax['tax_non_deductible'], 1, False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - cursor_x += 1 - - if tax_summary.get('tax_deductible_column'): - self._write_cell(cursor_x, cursor_y, tax['tax_deductible'], 1, False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - cursor_x += 1 - - if tax_summary.get('tax_due_column'): - self._write_cell(cursor_x, cursor_y, tax['tax_due'], 1, False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - - cursor_x = start_x - cursor_y += 1 - - cursor_x = start_x - cursor_y += 2 - - # Tax grids - columns = [] - grids = tax_summary.get('tax_grid_summary_lines') - if grids: - start_align_right = start_x + 1 - if len(grids) > 1: - start_align_right += 1 - columns.append(_('Country')) - - columns += [_('Grid'), _('+'), _('-'), _('Impact On Grid')] - - # Draw Tax Applied Table - # Write tax applied columns and header - self._write_cell(cursor_x, cursor_y, _('Impact On Grid'), len(columns), False, report, fonts, workbook, sheet, - XLSX_FONT_SIZE_HEADING, True, 'white', 'left', 2) - - cursor_y += 1 - for column in columns: - align = 'left' - if cursor_x >= start_align_right: - align = 'right' - self._write_cell(cursor_x, cursor_y, column, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, - XLSX_GRAY_200, align, 2) - cursor_x += 1 - - cursor_x = start_x - cursor_y += 1 - - for country in grids: - is_country_first_line = True - for grid_name in grids[country]: - if len(grids) > 1: - if is_country_first_line: - is_country_first_line = not is_country_first_line - self._write_cell(cursor_x, cursor_y, country, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, - True, 'white', 'left', 1, 0, XLSX_BORDER_COLOR) - - cursor_x += 1 - - self._write_cell(cursor_x, cursor_y, grid_name, 1, False, report, fonts, workbook, sheet, XLSX_FONT_SIZE_DEFAULT, True, - 'white', 'left', 1, 0, XLSX_BORDER_COLOR) - self._write_cell(cursor_x + 1, cursor_y, grids[country][grid_name].get('+', 0), 1, False, report, fonts, workbook, - sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - self._write_cell(cursor_x + 2, cursor_y, grids[country][grid_name].get('-', 0), 1, False, report, fonts, workbook, - sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - self._write_cell(cursor_x + 3, cursor_y, grids[country][grid_name]['impact'], 1, False, report, fonts, workbook, - sheet, XLSX_FONT_SIZE_DEFAULT, False, 'white', 'right', 1, 0, XLSX_BORDER_COLOR) - - cursor_x = start_x - cursor_y += 1 - - ########################################################################## - # Document Data Generation - ########################################################################## - - def _generate_document_data_for_export(self, report, options, export_type='pdf'): - """ - Used to generate all the data needed for the rendering of the export - - :param export_type: The export type the generation need to use can be ('pdf' or 'xslx') - - :return: a dictionnary containing a list of all lines grouped by journals and a dictionnay with the global tax summary lines - - journals_vals (mandatory): List of dictionary containing all the lines, columns, and tax summaries - - lines (mandatory): A list of dict containing all tha data for each lines in format returned by _get_lines_for_journal - - columns (mandatory): A list of columns for this journal returned in the format returned by _get_columns_for_journal - - tax_summary (optional): A dict of data for the tax summaries inside journals in the format returned by _get_tax_summary_section - - global_tax_summary: A dict with the global tax summaries data in the format returned by _get_tax_summary_section - """ - # Ensure that all the data is synchronized with the database before we read it - self.env.flush_all() - query = report._get_report_query(options, 'strict_range') - account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - - query = SQL( - """ - SELECT - account_move_line.id AS move_line_id, - account_move_line.name, - account_move_line.date, - account_move_line.invoice_date, - account_move_line.amount_currency, - account_move_line.tax_base_amount, - account_move_line.currency_id AS move_line_currency, - am.id AS move_id, - am.name AS move_name, - am.journal_id, - am.currency_id AS move_currency, - am.amount_total_in_currency_signed AS amount_currency_total, - am.currency_id != cp.currency_id AS is_multicurrency, - p.name AS partner_name, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - %(account_alias)s.account_type AS account_type, - COALESCE(account_move_line.debit, 0) AS debit, - COALESCE(account_move_line.credit, 0) AS credit, - COALESCE(account_move_line.balance, 0) AS balance, - %(j_name)s AS journal_name, - j.code AS journal_code, - j.type AS journal_type, - cp.currency_id AS company_currency, - CASE WHEN j.type = 'sale' THEN am.payment_reference WHEN j.type = 'purchase' THEN am.ref END AS reference, - array_remove(array_agg(DISTINCT %(tax_name)s), NULL) AS taxes, - array_remove(array_agg(DISTINCT %(tag_name)s), NULL) AS tax_grids - FROM %(table)s - JOIN account_move am ON am.id = account_move_line.move_id - LEFT JOIN res_partner p ON p.id = account_move_line.partner_id - JOIN account_journal j ON j.id = am.journal_id - JOIN res_company cp ON cp.id = am.company_id - LEFT JOIN account_move_line_account_tax_rel aml_at_rel ON aml_at_rel.account_move_line_id = account_move_line.id - LEFT JOIN account_tax parent_tax ON parent_tax.id = aml_at_rel.account_tax_id and parent_tax.amount_type = 'group' - LEFT JOIN account_tax_filiation_rel tax_filiation_rel ON tax_filiation_rel.parent_tax = parent_tax.id - LEFT JOIN account_tax tax ON (tax.id = aml_at_rel.account_tax_id and tax.amount_type != 'group') or tax.id = tax_filiation_rel.child_tax - LEFT JOIN account_account_tag_account_move_line_rel tag_rel ON tag_rel.account_move_line_id = account_move_line.id - LEFT JOIN account_account_tag tag ON tag_rel.account_account_tag_id = tag.id - LEFT JOIN res_currency journal_curr ON journal_curr.id = j.currency_id - WHERE %(case_statement)s AND %(search_conditions)s - GROUP BY "account_move_line".id, am.id, p.id, %(account_alias)s.id, j.id, cp.id, journal_curr.id, account_code, account_name - ORDER BY - CASE j.type - WHEN 'sale' THEN 1 - WHEN 'purchase' THEN 2 - WHEN 'general' THEN 3 - WHEN 'bank' THEN 4 - ELSE 5 - END, - j.sequence, - CASE WHEN am.name = '/' THEN 1 ELSE 0 END, am.date, am.name, - CASE %(account_alias)s.account_type - WHEN 'liability_payable' THEN 1 - WHEN 'asset_receivable' THEN 1 - WHEN 'liability_credit_card' THEN 5 - WHEN 'asset_cash' THEN 5 - ELSE 2 - END, - account_move_line.tax_line_id NULLS FIRST - """, - table=query.from_clause, - case_statement=self._get_payment_lines_filter_case_statement(options), - search_conditions=query.where_clause, - account_code=account_code, - account_name=account_name, - account_alias=SQL.identifier(account_alias), - j_name=self.env['account.journal']._field_to_sql('j', 'name'), - tax_name=self.env['account.tax']._field_to_sql('tax', 'name'), - tag_name=self.env['account.account.tag']._field_to_sql('tag', 'name') - ) - - self._cr.execute(query) - result = {} - - # Grouping by journal_id then move_id - for entry in self._cr.dictfetchall(): - result.setdefault(entry['journal_id'], {}) - result[entry['journal_id']].setdefault(entry['move_id'], []) - result[entry['journal_id']][entry['move_id']].append(entry) - - journals_vals = [] - any_journal_group_has_taxes = False - - for journal_entry_dict in result.values(): - account_move_vals_list = list(journal_entry_dict.values()) - journal_vals = { - 'id': account_move_vals_list[0][0]['journal_id'], - 'name': account_move_vals_list[0][0]['journal_name'], - 'code': account_move_vals_list[0][0]['journal_code'], - 'type': account_move_vals_list[0][0]['journal_type'] - } - - if self._section_has_tax(options, journal_vals['id']): - journal_vals['tax_summary'] = self._get_tax_summary_section(options, journal_vals) - any_journal_group_has_taxes = True - - journal_vals['lines'] = self._get_export_lines_for_journal(report, options, export_type, journal_vals, account_move_vals_list) - journal_vals['columns'] = self._get_columns_for_journal(journal_vals, export_type) - journals_vals.append(journal_vals) - - return { - 'journals_vals': journals_vals, - 'global_tax_summary': self._get_tax_summary_section(options) if any_journal_group_has_taxes else False - } - - def _get_columns_for_journal(self, journal, export_type='pdf'): - """ - Creates a columns list that will be used in this journal for the pdf report - - :return: A list of the columns as dict each having: - - name (mandatory): A string that will be displayed - - label (mandatory): A string used to link lines with the column - - class (optional): A string with css classes that need to be applied to all that column - """ - columns = [ - {'name': _('Document'), 'label': 'document'}, - ] - - # We have different columns regarding we are exporting to a PDF file or an XLSX document - if export_type == 'pdf': - columns.append({'name': _('Account'), 'label': 'account_label'}) - else: - columns.extend([ - {'name': _('Account Code'), 'label': 'account_code'}, - {'name': _('Account Label'), 'label': 'account_label'} - ]) - - columns.extend([ - {'name': _('Name'), 'label': 'name'}, - {'name': _('Debit'), 'label': 'debit', 'class': 'o_right_alignment '}, - {'name': _('Credit'), 'label': 'credit', 'class': 'o_right_alignment '}, - ]) - - if journal.get('tax_summary'): - columns.append( - {'name': _('Taxes'), 'label': 'taxes'}, - ) - if journal['tax_summary'].get('tax_grid_summary_lines'): - columns.append({'name': _('Tax Grids'), 'label': 'tax_grids'}) - - if journal['type'] == 'bank': - columns.append({ - 'name': _('Balance'), - 'label': 'balance', - 'class': 'o_right_alignment ' - }) - - if journal.get('multicurrency_column'): - columns.append({ - 'name': _('Amount Currency'), - 'label': 'amount_currency', - 'class': 'o_right_alignment ' - }) - - return columns - - def _get_export_lines_for_journal(self, report, options, export_type, journal_vals, account_move_vals_list): - """ - Default document lines generation it will generate a list of lines in a format valid for the pdf and xlsx - - If it is a bank journal it will be redirected to _get_lines_for_bank_journal since this type of journals - require more complexity - We want to be as lightweight as possible and not at unnecessary calculations - - :return: A list of lines. Each line is a dict having: - - 'column_label': A dict containing the values for a cell with a key that links to the label of a column - - data (mandatory): The formatted cell value - - class (optional): Additional css classes to apply to the current cell - - line_class (optional): Additional css classes that applies to the entire line - """ - lines = [] - - if journal_vals['type'] == 'bank': - return self._get_export_lines_for_bank_journal(report, options, export_type, journal_vals, account_move_vals_list) - - total_credit = 0 - total_debit = 0 - - for i, account_move_line_vals_list in enumerate(account_move_vals_list): - for j, move_line_entry_vals in enumerate(account_move_line_vals_list): - document = False - if j == 0: - document = move_line_entry_vals['move_name'] - elif j == 1: - document = move_line_entry_vals['date'] - - line = self._get_base_line(report, options, export_type, document, move_line_entry_vals, j, i % 2 != 0, journal_vals.get('tax_summary')) - - total_credit += move_line_entry_vals['credit'] - total_debit += move_line_entry_vals['debit'] - - lines.append(line) - - # Add other currency amout if this move is using multiple currencies - move_vals_entry = account_move_line_vals_list[0] - if move_vals_entry['is_multicurrency']: - amount_currency_name = _( - 'Amount in currency: %s', - report._format_value( - options, - move_vals_entry['amount_currency_total'], - 'monetary', - format_params={'currency_id': move_vals_entry['move_currency']}, - ), - ) - if len(account_move_line_vals_list) <= 2: - lines.append({ - 'document': {'data': amount_currency_name}, - 'line_class': 'o_even ' if i % 2 == 0 else 'o_odd ', - 'amount': {'data': move_vals_entry['amount_currency_total']}, - 'currency_id': {'data': move_vals_entry['move_currency']} - }) - else: - lines[-1]['document'] = {'data': amount_currency_name} - lines[-1]['amount'] = {'data': move_vals_entry['amount_currency_total']} - lines[-1]['currency_id'] = {'data': move_vals_entry['move_currency']} - - # Add an empty line to add a separation between the total section and the data section - lines.append({}) - - total_line = { - 'name': {'data': _('Total')}, - 'debit': {'data': report._format_value(options, total_debit, 'monetary')}, - 'credit': {'data': report._format_value(options, total_credit, 'monetary')}, - } - - lines.append(total_line) - - return lines - - def _get_export_lines_for_bank_journal(self, report, options, export_type, journal_vals, account_moves_vals_list): - """ - Bank journals are more complex and should be calculated separately from other journal types - - :return: A list of lines. Each line is a dict having: - - 'column_label': A dict containing the values for a cell with a key that links to the label of a column - - data (mandatory): The formatted cell value - - class (optional): Additional css classes to apply to the current cell - - line_class (optional): Additional css classes that applies to the entire line - """ - lines = [] - - # Initial balance - current_balance = self._query_bank_journal_initial_balance(options, journal_vals['id']) - lines.append({ - 'name': {'data': _('Starting Balance')}, - 'balance': {'data': report._format_value(options, current_balance, 'monetary')}, - }) - - # Debit and credit accumulators - total_credit = 0 - total_debit = 0 - - for i, account_move_line_vals_list in enumerate(account_moves_vals_list): - is_unreconciled_payment = not any( - line for line in account_move_line_vals_list if line['account_type'] in ('liability_credit_card', 'asset_cash') - ) - - for j, move_line_entry_vals in enumerate(account_move_line_vals_list): - # Do not display bank account lines for bank journals - if move_line_entry_vals['account_type'] not in ('liability_credit_card', 'asset_cash'): - document = '' - if j == 0: - document = f'{move_line_entry_vals["move_name"]} ({move_line_entry_vals["date"]})' - line = self._get_base_line(report, options, export_type, document, move_line_entry_vals, j, i % 2 != 0, journal_vals.get('tax_summary')) - - total_credit += move_line_entry_vals['credit'] - total_debit += move_line_entry_vals['debit'] - - if not is_unreconciled_payment: - # We need to invert the balance since it is a bank journal - line_balance = -move_line_entry_vals['balance'] - current_balance += line_balance - line.update({ - 'balance': { - 'data': report._format_value(options, current_balance, 'monetary'), - 'class': 'o_muted ' if self.env.company.currency_id.is_zero(line_balance) else '' - }, - }) - - if self.env.user.has_group('base.group_multi_currency') and move_line_entry_vals['move_line_currency'] != move_line_entry_vals['company_currency']: - journal_vals['multicurrency_column'] = True - amount_currency = -move_line_entry_vals['amount_currency'] if not is_unreconciled_payment else move_line_entry_vals['amount_currency'] - move_line_currency = self.env['res.currency'].browse(move_line_entry_vals['move_line_currency']) - line.update({ - 'amount_currency': { - 'data': report._format_value( - options, - amount_currency, - 'monetary', - format_params={'currency_id': move_line_currency.id}, - ), - 'class': 'o_muted ' if move_line_currency.is_zero(amount_currency) else '', - } - }) - lines.append(line) - - # Add an empty line to add a separation between the total section and the data section - lines.append({}) - - total_line = { - 'name': {'data': _('Total')}, - 'balance': {'data': report._format_value(options, current_balance, 'monetary')}, - } - lines.append(total_line) - - return lines - - def _get_base_line(self, report, options, export_type, document, line_entry, line_index, even, has_taxes): - """ - Returns the generic part of a line that is used by both '_get_lines_for_journal' and '_get_lines_for_bank_journal' - - :return: A dict with base values for the line - - line_class (mandatory): Css classes that applies to this whole line - - document (mandatory): A dict containing the cell data for the column document - - data (mandatory): The value of the cell formatted - - class (mandatory): css class for this cell - - account (mandatory): A dict containing the cell data for the column account - - data (mandatory): The value of the cell formatted - - account_code (mandatory): A dict containing the cell data for the column account_code - - data (mandatory): The value of the cell formatted - - account_label (mandatory): A dict containing the cell data for the column account_label - - data (mandatory): The value of the cell formatted - - name (mandatory): A dict containing the cell data for the column name - - data (mandatory): The value of the cell formatted - - debit (mandatory): A dict containing the cell data for the column debit - - data (mandatory): The value of the cell formatted - - class (mandatory): css class for this cell - - credit (mandatory): A dict containing the cell data for the column credit - - data (mandatory): The value of the cell formatted - - class (mandatory): css class for this cell - - - taxes(optional): A dict containing the cell data for the column taxes - - data (mandatory): The value of the cell formatted - - tax_grids(optional): A dict containing the cell data for the column taxes - - data (mandatory): The value of the cell formatted - """ - company_currency = self.env.company.currency_id - - name = line_entry['name'] or line_entry['reference'] - account_label = line_entry['partner_name'] or line_entry['account_name'] - if line_entry['partner_name'] and line_entry['account_type'] == 'asset_receivable': - formatted_account_label = _('AR %s', account_label) # AR="Account Receivable" - elif line_entry['partner_name'] and line_entry['account_type'] == 'liability_payable': - formatted_account_label = _('AP %s', account_label) # AP="Account Payable" - else: - account_label = line_entry['account_name'] - formatted_account_label = _('G %s', line_entry["account_code"]) # G="General" - - line = { - 'line_class': 'o_even ' if even else 'o_odd ', - 'document': {'data': document, 'class': 'o_bold ' if line_index == 0 else ''}, - 'account_code': {'data': line_entry['account_code']}, - 'account_label': {'data': account_label if export_type != 'pdf' else formatted_account_label}, - 'name': {'data': name}, - 'debit': { - 'data': report._format_value(options, line_entry['debit'], 'monetary'), - 'class': 'o_muted ' if company_currency.is_zero(line_entry['debit']) else '' - }, - 'credit': { - 'data': report._format_value(options, line_entry['credit'], 'monetary'), - 'class': 'o_muted ' if company_currency.is_zero(line_entry['credit']) else '' - }, - } - - if has_taxes: - tax_val = '' - if line_entry['taxes']: - tax_val = _('T: %s', ', '.join(line_entry['taxes'])) - elif line_entry['tax_base_amount'] is not None: - tax_val = _('B: %s', report._format_value(options, line_entry['tax_base_amount'], 'monetary')) - - line.update({ - 'taxes': {'data': tax_val}, - 'tax_grids': {'data': ', '.join(line_entry['tax_grids'])}, - }) - - return line - - ########################################################################## - # Queries - ########################################################################## - - def _get_payment_lines_filter_case_statement(self, options): - if not options.get('show_payment_lines'): - return SQL( - """ - (j.type != 'bank' OR EXISTS( - SELECT - 1 - FROM account_move_line - JOIN account_account acc ON acc.id = account_move_line.account_id - WHERE account_move_line.move_id = am.id - AND acc.account_type IN ('liability_credit_card', 'asset_cash') - )) - """ - ) - else: - return SQL('TRUE') - - def _query_bank_journal_initial_balance(self, options, journal_id): - report = self.env.ref('at_accounting.journal_report') - query = report._get_report_query(options, 'to_beginning_of_period', domain=[('journal_id', '=', journal_id)]) - query = SQL( - """ - SELECT - COALESCE(SUM(account_move_line.balance), 0) AS balance - FROM %(table)s - JOIN account_journal journal ON journal.id = "account_move_line".journal_id AND account_move_line.account_id = journal.default_account_id - WHERE %(search_conditions)s - GROUP BY journal.id - """, - table=query.from_clause, - search_conditions=query.where_clause, - ) - self._cr.execute(query) - result = self._cr.dictfetchall() - init_balance = result[0]['balance'] if len(result) >= 1 else 0 - return init_balance - - ########################################################################## - # Tax Grids - ########################################################################## - - def _section_has_tax(self, options, journal_id): - report = self.env['account.report'].browse(options.get('report_id')) - aml_has_tax_domain = [('tax_ids', '!=', False)] - if journal_id: - aml_has_tax_domain.append(('journal_id', '=', journal_id)) - aml_has_tax_domain += report._get_options_domain(options, 'strict_range') - return bool(self.env['account.move.line'].search_count(aml_has_tax_domain, limit=1)) - - def _get_tax_summary_section(self, options, journal_vals=None): - """ - Get the journal tax summary if it is passed as parameter. - In case no journal is passed, it will return the global tax summary data - """ - tax_data = { - 'date_from': options.get('date', {}).get('date_from'), - 'date_to': options.get('date', {}).get('date_to'), - } - - if journal_vals: - tax_data['journal_id'] = journal_vals['id'] - tax_data['journal_type'] = journal_vals['type'] - - tax_report_lines = self._get_generic_tax_summary_for_sections(options, tax_data) - tax_non_deductible_column = any(line.get('tax_non_deductible_no_format') for country_vals_list in tax_report_lines.values() for line in country_vals_list) - tax_deductible_column = any(line.get('tax_deductible_no_format') for country_vals_list in tax_report_lines.values() for line in country_vals_list) - tax_due_column = any(line.get('tax_due_no_format') for country_vals_list in tax_report_lines.values() for line in country_vals_list) - extra_columns = int(tax_non_deductible_column) + int(tax_deductible_column) + int(tax_due_column) - - tax_grid_summary_lines = self._get_tax_grids_summary(options, tax_data) - - return { - 'tax_report_lines': tax_report_lines, - 'tax_non_deductible_column': tax_non_deductible_column, - 'tax_deductible_column': tax_deductible_column, - 'tax_due_column': tax_due_column, - 'extra_columns': extra_columns, - 'tax_grid_summary_lines': tax_grid_summary_lines, - } - - def _get_generic_tax_report_options(self, options, data): - """ - Return an option dictionnary set to fetch the reports with the parameters needed for this journal. - The important bits are the journals, date, and fetch the generic tax reports that contains all taxes. - We also provide the information about wether to take all entries or only posted ones. - """ - generic_tax_report = self.env.ref('account.generic_tax_report') - previous_option = options.copy() - # Force the dates to the selected ones. Allows to get it correctly when grouped by months - previous_option.update({ - 'selected_variant_id': generic_tax_report.id, - 'date_from': data.get('date_from'), - 'date_to': data.get('date_to'), - }) - tax_report_options = generic_tax_report.get_options(previous_option) - journal_report = self.env['account.report'].browse(options['report_id']) - tax_report_options['forced_domain'] = tax_report_options.get('forced_domain', []) + journal_report._get_options_domain(options, 'strict_range') - - # Even though it doesn't have a journal selector, we can force a journal in the options to only get the lines for a specific journal. - if data.get('journal_id') or data.get('journal_type'): - tax_report_options['journals'] = [{ - 'id': data.get('journal_id'), - 'model': 'account.journal', - 'type': data.get('journal_type'), - 'selected': True, - }] - - return tax_report_options - - def _get_tax_grids_summary(self, options, data): - """ - Fetches the details of all grids that have been used in the provided journal. - The result is grouped by the country in which the tag exists in case of multivat environment. - Returns a dictionary with the following structure: - { - Country : { - tag_name: {+, -, impact}, - tag_name: {+, -, impact}, - tag_name: {+, -, impact}, - ... - }, - Country : [ - tag_name: {+, -, impact}, - tag_name: {+, -, impact}, - tag_name: {+, -, impact}, - ... - ], - ... - } - """ - report = self.env.ref('at_accounting.journal_report') - # Use the same option as we use to get the tax details, but this time to generate the query used to fetch the - # grid information - tax_report_options = self._get_generic_tax_report_options(options, data) - query = report._get_report_query(tax_report_options, 'strict_range') - country_name = self.env['res.country']._field_to_sql('country', 'name') - tag_name = self.env['account.account.tag']._field_to_sql('tag', 'name') - query = SQL(""" - WITH tag_info (country_name, tag_id, tag_name, tag_sign, balance) AS ( - SELECT - %(country_name)s AS country_name, - tag.id, - %(tag_name)s AS name, - CASE WHEN tag.tax_negate IS TRUE THEN '-' ELSE '+' END, - SUM(COALESCE("account_move_line".balance, 0) - * CASE WHEN "account_move_line".tax_tag_invert THEN -1 ELSE 1 END - ) AS balance - FROM account_account_tag tag - JOIN account_account_tag_account_move_line_rel rel ON tag.id = rel.account_account_tag_id - JOIN res_country country ON country.id = tag.country_id - , %(table_references)s - WHERE %(search_condition)s - AND applicability = 'taxes' - AND "account_move_line".id = rel.account_move_line_id - GROUP BY country_name, tag.id - ) - SELECT - country_name, - tag_id, - REGEXP_REPLACE(tag_name, '^[+-]', '') AS name, -- Remove the sign from the grid name - balance, - tag_sign AS sign - FROM tag_info - ORDER BY country_name, name - """, country_name=country_name, tag_name=tag_name, table_references=query.from_clause, search_condition=query.where_clause) - self._cr.execute(query) - query_res = self.env.cr.fetchall() - - res = {} - opposite = {'+': '-', '-': '+'} - for country_name, tag_id, name, balance, sign in query_res: - res.setdefault(country_name, {}).setdefault(name, {}) - res[country_name][name].setdefault('tag_ids', []).append(tag_id) - res[country_name][name][sign] = report._format_value(options, balance, 'monetary') - - # We need them formatted, to ensure they are displayed correctly in the report. (E.g. 0.0, not 0) - if not opposite[sign] in res[country_name][name]: - res[country_name][name][opposite[sign]] = report._format_value(options, 0, 'monetary') - - res[country_name][name][sign + '_no_format'] = balance - res[country_name][name]['impact'] = report._format_value(options, res[country_name][name].get('+_no_format', 0) - res[country_name][name].get('-_no_format', 0), 'monetary') - - return res - - def _get_generic_tax_summary_for_sections(self, options, data): - """ - Overridden to make use of the generic tax report computation - Works by forcing specific options into the tax report to only get the lines we need. - The result is grouped by the country in which the tag exists in case of multivat environment. - Returns a dictionary with the following structure: - { - Country : [ - {name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}}, - {name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}}, - {name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}}, - ... - ], - Country : [ - {name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}}, - {name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}}, - {name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}}, - ... - ], - ... - } - """ - report = self.env['account.report'].browse(options['report_id']) - tax_report_options = self._get_generic_tax_report_options(options, data) - tax_report_options['account_journal_report_tax_deductibility_columns'] = True - tax_report = self.env.ref('account.generic_tax_report') - tax_report_lines = tax_report._get_lines(tax_report_options) - - tax_values = {} - for tax_report_line in tax_report_lines: - model, line_id = report._parse_line_id(tax_report_line.get('id'))[-1][1:] - if model == 'account.tax': - tax_values[line_id] = { - 'base_amount': tax_report_line['columns'][0]['no_format'], - 'tax_amount': tax_report_line['columns'][1]['no_format'], - 'tax_non_deductible': tax_report_line['columns'][2]['no_format'], - 'tax_deductible': tax_report_line['columns'][3]['no_format'], - 'tax_due': tax_report_line['columns'][4]['no_format'], - } - - # Make the final data dict that will be used by the template, using the taxes information. - taxes = self.env['account.tax'].browse(tax_values.keys()) - res = {} - for tax in taxes: - res.setdefault(tax.country_id.name, []).append({ - 'base_amount': report._format_value(options, tax_values[tax.id]['base_amount'], 'monetary'), - 'tax_amount': report._format_value(options, tax_values[tax.id]['tax_amount'], 'monetary'), - 'tax_non_deductible': report._format_value(options, tax_values[tax.id]['tax_non_deductible'], 'monetary'), - 'tax_non_deductible_no_format': tax_values[tax.id]['tax_non_deductible'], - 'tax_deductible': report._format_value(options, tax_values[tax.id]['tax_deductible'], 'monetary'), - 'tax_deductible_no_format': tax_values[tax.id]['tax_deductible'], - 'tax_due': report._format_value(options, tax_values[tax.id]['tax_due'], 'monetary'), - 'tax_due_no_format': tax_values[tax.id]['tax_due'], - 'name': tax.name, - 'line_id': report._get_generic_line_id('account.tax', tax.id) - }) - - # Return the result, ordered by country name - return dict(sorted(res.items())) - - ########################################################################## - # Actions - ########################################################################## - - def journal_report_tax_tag_template_open_aml(self, options, params=None): - """ returns an action to open a list view of the account.move.line having the selected tax tag """ - tag_ids = params.get('tag_ids') - domain = ( - self.env['account.report'].browse(options['report_id'])._get_options_domain(options, 'strict_range') - + [('tax_tag_ids', 'in', [tag_ids])] - + self.env['account.move.line']._get_tax_exigible_domain() - ) - - return { - 'type': 'ir.actions.act_window', - 'name': _('Journal Items for Tax Audit'), - 'res_model': 'account.move.line', - 'views': [[self.env.ref('account.view_move_line_tax_audit_tree').id, 'list']], - 'domain': domain, - 'context': self.env.context, - } - - def journal_report_action_dropdown_audit_default_tax_report(self, options, params): - return self.env['account.generic.tax.report.handler'].caret_option_audit_tax(options, params) - - def journal_report_action_open_tax_journal_items(self, options, params): - """ - Open the journal items related to the tax on this line. - Take into account the given/options date and group by taxes then account. - :param options: the report options. - :param params: a dict containing the line params. (Dates, name, journal_id, tax_type) - :return: act_window on journal items grouped by tax or tags and accounts. - """ - ctx = { - 'search_default_posted': 0 if options.get('all_entries') else 1, - 'search_default_date_between': 1, - 'date_from': params and params.get('date_from') or options.get('date', {}).get('date_from'), - 'date_to': params and params.get('date_to') or options.get('date', {}).get('date_to'), - 'search_default_journal_id': params.get('journal_id'), - 'expand': 1, - } - if params and params.get('tax_type') == 'tag': - ctx.update({ - 'search_default_group_by_tax_tags': 1, - 'search_default_group_by_account': 2, - }) - elif params and params.get('tax_type') == 'tax': - ctx.update({ - 'search_default_group_by_taxes': 1, - 'search_default_group_by_account': 2, - }) - - if params and 'journal_id' in params: - ctx.update({ - 'search_default_journal_id': [params['journal_id']], - }) - - if options and options.get('journals') and not ctx.get('search_default_journal_id'): - selected_journals = [journal['id'] for journal in options['journals'] if journal.get('selected') and journal['model'] == 'account.journal'] - if len(selected_journals) == 1: - ctx['search_default_journal_id'] = selected_journals - - return { - 'name': params.get('name'), - 'view_mode': 'list,pivot,graph,kanban', - 'res_model': 'account.move.line', - 'views': [(self.env.ref('account.view_move_line_tree').id, 'list')], - 'type': 'ir.actions.act_window', - 'domain': [('display_type', 'not in', ('line_section', 'line_note'))], - 'context': ctx, - } - - def journal_report_action_open_account_move_lines_by_account(self, options, params): - """ - Open a list view of the journal account move lines - corresponding to the date filter and the current account line clicked - :param options: The current options of the report - :param params: The params given from the report UI (journal_id, account_id, date) - :return: act_window on journal items filtered on the current journal and the current account within a date. - """ - report = self.env['account.report'].browse(options['report_id']) - journal = self.env['account.journal'].browse(params['journal_id']) - account = self.env['account.account'].browse(params['account_id']) - - domain = [ - ('journal_id.id', '=', journal.id), - ('account_id.id', '=', account.id), - ] - domain += report._get_options_domain(options, 'strict_range') - - return { - 'type': 'ir.actions.act_window', - 'name': _("%(journal)s - %(account)s", journal=journal.name, account=account.name), - 'res_model': 'account.move.line', - 'views': [[False, 'list']], - 'domain': domain - } - - def journal_report_open_aml_by_move(self, options, params): - report = self.env['account.report'].browse(options['report_id']) - journal = self.env['account.journal'].browse(params['journal_id']) - - context_update = { - 'search_default_group_by_account': 0, - 'show_more_partner_info': 1, - } - - if journal.type in ('bank', 'credit'): - params['view_ref'] = 'at_accounting.view_journal_report_audit_bank_move_line_tree' - context_update['search_default_exclude_bank_lines'] = 1 - else: - params['view_ref'] = 'at_accounting.view_journal_report_audit_move_line_tree' - context_update.update({ - 'search_default_group_by_partner': 1, - 'search_default_group_by_move': 2, - }) - if journal.type in ('sale', 'purchase'): - context_update['search_default_invoices_lines'] = 1 - - action = report.open_journal_items(options=options, params=params) - action.get('context', {}).update(context_update) - return action diff --git a/addons/at_accounting/models/account_move.py b/addons/at_accounting/models/account_move.py deleted file mode 100644 index 7e4ffd7..0000000 --- a/addons/at_accounting/models/account_move.py +++ /dev/null @@ -1,1563 +0,0 @@ -import calendar -from contextlib import contextmanager -from dateutil.relativedelta import relativedelta -import logging -import math -import re -from odoo import fields, models, api, _, Command -from odoo.exceptions import UserError -from odoo.osv import expression -from odoo.tools import SQL, float_compare -import ast -from odoo.addons.account.models.exceptions import TaxClosingNonPostedDependingMovesError -from odoo.tools.misc import format_date -from odoo.tools import date_utils -from odoo.addons.web.controllers.utils import clean_action - -from markupsafe import Markup -from odoo.exceptions import UserError, ValidationError -from odoo.tools.misc import formatLang - - - -_logger = logging.getLogger(__name__) - - -DEFERRED_DATE_MIN = '1900-01-01' -DEFERRED_DATE_MAX = '9999-12-31' - - -class AccountMove(models.Model): - _inherit = "account.move" - - - def _get_invoice_in_payment_state(self): - return 'in_payment' - - payment_state_before_switch = fields.Char(string="Payment State Before Switch", copy=False) - - deferred_move_ids = fields.Many2many( - string="Deferred Entries", - comodel_name='account.move', - relation='account_move_deferred_rel', - column1='original_move_id', - column2='deferred_move_id', - help="The deferred entries created by this invoice", - copy=False, - ) - deferred_original_move_ids = fields.Many2many( - string="Original Invoices", - comodel_name='account.move', - relation='account_move_deferred_rel', - column1='deferred_move_id', - column2='original_move_id', - help="The original invoices that created the deferred entries", - copy=False, - ) - deferred_entry_type = fields.Selection( - string="Deferred Entry Type", - selection=[ - ('expense', 'Deferred Expense'), - ('revenue', 'Deferred Revenue'), - ], - compute='_compute_deferred_entry_type', - copy=False, - ) - - signing_user = fields.Many2one( - string='Signer', - comodel_name='res.users', - compute='_compute_signing_user', store=True, - copy=False, - ) - show_signature_area = fields.Boolean(compute='_compute_signature') - signature = fields.Binary(compute='_compute_signature') # can't be `related`: the sign module might not be there - # used for VAT closing, containing the end date of the period this entry closes - tax_closing_report_id = fields.Many2one(comodel_name='account.report') - # technical field used to know whether to show the tax closing alert or not - tax_closing_alert = fields.Boolean(compute='_compute_tax_closing_alert') - - @api.depends('state', 'move_type', 'invoice_user_id') - def _compute_signing_user(self): - other_moves = self.filtered(lambda move: not move.is_sale_document()) - other_moves.signing_user = False - - is_odoobot_user = self.env.user == self.env.ref('base.user_root') - is_backend_user = self.env.user.has_group('base.group_user') - - for invoice in (self - other_moves).filtered(lambda inv: inv.state == 'posted'): - # signer priority: - # - res.user set in res.settings - # - real backend user posting the invoice - # - if odoobot: the person that initiated the invoice ie: The salesman - # - if invoice initiated by a portal user -> No signature - representative = invoice.company_id.signing_user - # checking `has_group('base.group_user')` ensure we never keep a portal user to sign - if is_odoobot_user: - user_can_sign = invoice.invoice_user_id and invoice.invoice_user_id.has_group('base.group_user') - invoice.signing_user = representative or invoice.invoice_user_id if user_can_sign else False - else: - invoice.signing_user = representative or self.env.user if is_backend_user else False - - @api.depends('state') - def _compute_signature(self): - is_portal_user = self.env.user.has_group('base.group_portal') - # Checking `company_id.sign_invoice` removes the needs to check if the sign module is installed - # Setting it to True through `res.settings` auto install the sign module - moves_not_to_sign = self.filtered( - lambda inv: not inv.company_id.sign_invoice - or inv.state in {'draft', 'cancel'} - or not inv.is_sale_document() - # Allow signature for portal user only if the invoice already went through the send&print workflow - or (is_portal_user and not inv.invoice_pdf_report_id) - ) - moves_not_to_sign.show_signature_area = False - moves_not_to_sign.signature = None - - invoice_with_signature = self - moves_not_to_sign - invoice_with_signature.show_signature_area = True - for invoice in invoice_with_signature: - invoice.signature = invoice.signing_user.sign_signature - - def _post(self, soft=True): - # Deferred management - posted = super()._post(soft) - for move in self: - if move._get_deferred_entries_method() == 'on_validation' and any(move.line_ids.mapped('deferred_start_date')): - move._generate_deferred_entries() - return posted - - def action_post(self): - # EXTENDS 'account' to trigger the CRON auto-reconciling the statement lines. - res = super().action_post() - if self.statement_line_id and not self._context.get('skip_statement_line_cron_trigger'): - self.env.ref('at_accounting.auto_reconcile_bank_statement_line')._trigger() - return res - - def button_draft(self): - if any(len(deferral_move.deferred_original_move_ids) > 1 for deferral_move in self.deferred_move_ids): - raise UserError(_("You cannot reset to draft an invoice that is grouped in deferral entry. You can create a credit note instead.")) - reversed_moves = self.deferred_move_ids._unlink_or_reverse() - if reversed_moves: - for move in reversed_moves: - move.with_context(skip_readonly_check=True).write({ - 'date': move._get_accounting_date(move.date, move._affect_tax_report()), - }) - self.deferred_move_ids |= reversed_moves - return super().button_draft() - - def unlink(self): - # Prevent deferred moves under audit trail restriction from being unlinked - deferral_moves = self.filtered(lambda move: move.company_id.check_account_audit_trail and move.deferred_original_move_ids) - deferral_moves.deferred_original_move_ids.deferred_move_ids = False - deferral_moves._reverse_moves() - return super(AccountMove, self - deferral_moves).unlink() - - # ============================= START - Deferred Management ==================================== - - def _get_deferred_entries_method(self): - self.ensure_one() - if self.is_outbound(): - return self.company_id.generate_deferred_expense_entries_method - return self.company_id.generate_deferred_revenue_entries_method - - @api.depends('deferred_original_move_ids') - def _compute_deferred_entry_type(self): - for move in self: - if move.deferred_original_move_ids: - move.deferred_entry_type = 'expense' if move.deferred_original_move_ids[0].is_outbound() else 'revenue' - else: - move.deferred_entry_type = False - - @api.model - def _get_deferred_diff_dates(self, start, end): - """ - Returns the number of months between two dates [start, end[ - The computation is done by using months of 30 days so that the deferred amount for february - (28-29 days), march (31 days) and april (30 days) are all the same (in case of monthly computation). - See test_deferred_management_get_diff_dates for examples. - """ - if start > end: - start, end = end, start - nb_months = end.month - start.month + 12 * (end.year - start.year) - start_day, end_day = start.day, end.day - if start_day == calendar.monthrange(start.year, start.month)[1]: - start_day = 30 - if end_day == calendar.monthrange(end.year, end.month)[1]: - end_day = 30 - nb_days = end_day - start_day - return (nb_months * 30 + nb_days) / 30 - - @api.model - def _get_deferred_period_amount(self, method, period_start, period_end, line_start, line_end, balance): - """ - Returns the amount to defer for the given period taking into account the deferred method (day/month/full_months). - """ - is_valid_period = period_end > line_start and period_end > period_start - if method == 'day': - amount_per_day = balance / (line_end - line_start).days - return (period_end - period_start).days * amount_per_day if is_valid_period else 0 - elif method == "month": - amount_per_month = balance / self._get_deferred_diff_dates(line_end, line_start) - nb_months_period = self._get_deferred_diff_dates(period_end, period_start) - return nb_months_period * amount_per_month if is_valid_period else 0 - elif method == "full_months": - line_diff = self._get_deferred_diff_dates(line_end, line_start) - period_diff = self._get_deferred_diff_dates(period_end, period_start) - if line_diff < 1: - amount = balance - else: - if line_end.day == calendar.monthrange(line_end.year, line_end.month)[1]: - line_diff = math.ceil(line_diff) - else: - line_diff = math.floor(line_diff) - if period_end.day == calendar.monthrange(period_end.year, period_end.month)[1] or line_end != period_end: - period_diff = math.ceil(period_diff) - else: - period_diff = math.floor(period_diff) - amount_per_month = balance / line_diff - amount = period_diff * amount_per_month - return amount if is_valid_period else 0 - - @api.model - def _get_deferred_amounts_by_line(self, lines, periods, deferred_type): - """ - :return: a list of dictionaries containing the deferred amounts for each line and each period - E.g. (where period1 = (date1, date2, label1), period2 = (date2, date3, label2), ...) - [ - {'account_id': 1, period_1: 100, period_2: 200}, - {'account_id': 1, period_1: 100, period_2: 200}, - {'account_id': 2, period_1: 300, period_2: 400}, - ] - """ - values = [] - for line in lines: - line_start = fields.Date.to_date(line['deferred_start_date']) - line_end = fields.Date.to_date(line['deferred_end_date']) - if line_end < line_start: - # This normally shouldn't happen, but if it does, would cause calculation errors later on. - # To not make the reports crash, we just set both dates to the same day. - # The user should fix the dates manually. - line_end = line_start - - columns = {} - for period in periods: - if period[2] == 'not_started' and line_start <= period[0]: - # The 'Not Started' column only considers lines starting the deferral after the report end date - columns[period] = 0.0 - continue - # periods = [Total, Not Started, Before, ..., Current, ..., Later] - # The dates to calculate the amount for the current period - period_start = max(period[0], line_start) - period_end = min(period[1], line_end) - if ( - period[2] in ('not_started', 'later') and period[0] < line_start - or len(periods) <= 1 - or period[2] not in ('not_started', 'before', 'later') - ): - # We are subtracting 1 day from `period_start` because the start date should be included when: - # - in the 'Not Started' or 'Later' period if the deferral has not started yet (line_start, line_end) - # - we only have one period - # - not in the 'Not Started', 'Before' or 'Later' period - period_start -= relativedelta(days=1) - columns[period] = self._get_deferred_period_amount( - self.env.company.deferred_expense_amount_computation_method if deferred_type == "expense" else self.env.company.deferred_revenue_amount_computation_method, - period_start, period_end, - line_start - relativedelta(days=1), line_end, # -1 because we want to include the start date - line['balance'] - ) - - values.append({ - **self.env['account.move.line']._get_deferred_amounts_by_line_values(line), - **columns, - }) - return values - - @api.model - def _get_deferred_lines(self, line, deferred_account, deferred_type, period, ref, force_balance=None, grouping_field='account_id'): - """ - :return: a list of Command objects to create the deferred lines of a single given period - """ - deferred_amounts = self._get_deferred_amounts_by_line(line, [period], deferred_type)[0] - balance = deferred_amounts[period] if force_balance is None else force_balance - return [ - Command.create({ - **self.env['account.move.line']._get_deferred_lines_values(account.id, coeff * balance, ref, line.analytic_distribution, line), - 'partner_id': line.partner_id.id, - 'product_id': line.product_id.id, - }) - for (account, coeff) in [(deferred_amounts[grouping_field], 1), (deferred_account, -1)] - ] - - def _generate_deferred_entries(self): - """ - Generates the deferred entries for the invoice. - """ - self.ensure_one() - if self.state != 'posted': - return - if self.is_entry(): - raise UserError(_("You cannot generate deferred entries for a miscellaneous journal entry.")) - deferred_type = "expense" if self.is_purchase_document() else "revenue" - deferred_account = self.company_id.deferred_expense_account_id if deferred_type == "expense" else self.company_id.deferred_revenue_account_id - deferred_journal = self.company_id.deferred_expense_journal_id if deferred_type == "expense" else self.company_id.deferred_revenue_journal_id - if not deferred_journal: - raise UserError(_("Please set the deferred journal in the accounting settings.")) - if not deferred_account: - raise UserError(_("Please set the deferred accounts in the accounting settings.")) - - for line in self.line_ids.filtered(lambda l: l.deferred_start_date and l.deferred_end_date): - periods = line._get_deferred_periods() - if not periods: - continue - - ref = _("Deferral of %s", line.move_id.name or '') - - move_vals = { - 'move_type': 'entry', - 'deferred_original_move_ids': [Command.set(line.move_id.ids)], - 'journal_id': deferred_journal.id, - 'company_id': self.company_id.id, - 'partner_id': line.partner_id.id, - 'auto_post': 'at_date', - 'ref': ref, - 'name': False, - } - - # Defer the current invoice - move_fully_deferred = self.create({ - **move_vals, - 'date': line.move_id.date, - }) - # We write the lines after creation, to make sure the `deferred_original_move_ids` is set. - # This way we can avoid adding taxes for deferred moves. - move_fully_deferred.write({ - 'line_ids': [ - Command.create( - self.env['account.move.line']._get_deferred_lines_values(account.id, coeff * line.balance, ref, line.analytic_distribution, line) - ) for (account, coeff) in [(line.account_id, -1), (deferred_account, 1)] - ], - }) - - # Create the deferred entries for the periods [deferred_start_date, deferred_end_date] - deferral_moves = self.create([{ - **move_vals, - 'date': period[1], - } for period in periods]) - remaining_balance = line.balance - for period_index, (period, deferral_move) in enumerate(zip(periods, deferral_moves)): - # For the last deferral move the balance is forced to remaining balance to avoid rounding errors - force_balance = remaining_balance if period_index == len(periods) - 1 else None - # Same as before, to avoid adding taxes for deferred moves. - deferral_move.write({ - 'line_ids': self._get_deferred_lines(line, deferred_account, deferred_type, period, ref, force_balance=force_balance), - }) - remaining_balance -= deferral_move.line_ids[0].balance - # Avoid having deferral moves with a total amount of 0 - if deferral_move.currency_id.is_zero(deferral_move.amount_total): - deferral_moves -= deferral_move - deferral_move.unlink() - - deferred_moves = move_fully_deferred + deferral_moves - if len(deferral_moves) == 1 and move_fully_deferred.date.month == deferral_moves.date.month: - # If, after calculation, we have 2 deferral entries in the same month, it means that - # they simply cancel out each other, so there is no point in creating them. - deferred_moves.unlink() - continue - line.move_id.deferred_move_ids |= deferred_moves - deferred_moves._post(soft=True) - - def open_deferred_entries(self): - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'name': _("Deferred Entries"), - 'res_model': 'account.move.line', - 'domain': [('id', 'in', self.deferred_move_ids.line_ids.ids)], - 'views': [(self.env.ref('at_accounting.view_deferred_entries_tree').id, 'list')], - 'context': { - 'search_default_group_by_move': True, - 'expand': True, - } - } - - def open_deferred_original_entry(self): - self.ensure_one() - action = { - 'type': 'ir.actions.act_window', - 'name': _("Original Deferred Entries"), - 'res_model': 'account.move.line', - 'domain': [('id', 'in', self.deferred_original_move_ids.line_ids.ids)], - 'views': [(False, 'list'), (False, 'form')], - 'context': { - 'search_default_group_by_move': True, - 'expand': True, - } - } - if len(self.deferred_original_move_ids) == 1: - action.update({ - 'res_model': 'account.move', - 'res_id': self.deferred_original_move_ids[0].id, - 'views': [(False, 'form')], - }) - return action - - # ============================= END - Deferred management ====================================== - - def action_open_bank_reconciliation_widget(self): - return self.statement_line_id._action_open_bank_reconciliation_widget( - default_context={ - 'search_default_journal_id': self.statement_line_id.journal_id.id, - 'search_default_statement_line_id': self.statement_line_id.id, - 'default_st_line_id': self.statement_line_id.id, - } - ) - - def action_open_bank_reconciliation_widget_statement(self): - return self.statement_line_id._action_open_bank_reconciliation_widget( - extra_domain=[('statement_id', 'in', self.statement_id.ids)], - ) - - def action_open_business_doc(self): - if self.statement_line_id: - return self.action_open_bank_reconciliation_widget() - else: - action = super().action_open_business_doc() - # prevent propagation of the following keys - action['context'] = action.get('context', {}) | { - 'preferred_aml_value': None, - 'preferred_aml_currency_id': None, - } - return action - - def _get_mail_thread_data_attachments(self): - res = super()._get_mail_thread_data_attachments() - res += self.statement_line_id.statement_id.attachment_ids - return res - - @contextmanager - def _get_edi_creation(self): - with super()._get_edi_creation() as move: - previous_lines = move.invoice_line_ids - yield move.with_context(disable_onchange_name_predictive=True) - for line in move.invoice_line_ids - previous_lines: - line._onchange_name_predictive() - - - def _post(self, soft=True): - # Overridden to create carryover external values and join the pdf of the report when posting the tax closing - for move in self.filtered(lambda m: m.tax_closing_report_id): - report = move.tax_closing_report_id - options = move._get_tax_closing_report_options(move.company_id, move.fiscal_position_id, report, move.date) - move._close_tax_period(report, options) - - return super()._post(soft) - - def action_post(self): - # In the case of a TaxClosingNonPostedDependingMovesError, which can occur when dealing with branches or tax - # units during the closing process, the parent company may have non-posted closing entries from other companies. - # If this exception occurs, we will return an action client that will display a component indicating that there - # are non-posted dependent moves, along with a link to those moves. - # Also, we are not using a RedirectWarning because it will force a rollback on the closing move created for - # depending companies. - try: - res = super().action_post() - except TaxClosingNonPostedDependingMovesError as exception: - return { - "type": "ir.actions.client", - "tag": "at_accounting.redirect_action", - "target": "new", - "name": "Depending Action", - "params": { - "depending_action": exception.args[0], - "message": _("It seems there is some depending closing move to be posted"), - "button_text": _("Depending moves"), - }, - 'context': { - 'dialog_size': 'medium', - } - } - return res - - def button_draft(self): - # Overridden in order to delete the carryover values when resetting the tax closing to draft - super().button_draft() - for closing_move in self.filtered(lambda m: m.tax_closing_report_id): - report = closing_move.tax_closing_report_id - options = closing_move._get_tax_closing_report_options(closing_move.company_id, closing_move.fiscal_position_id, report, closing_move.date) - closing_months_delay = closing_move.company_id._get_tax_periodicity_months_delay(report) - - carryover_values = self.env['account.report.external.value'].search([ - ('carryover_origin_report_line_id', 'in', report.line_ids.ids), - ('date', '=', options['date']['date_to']), - ]) - - carryover_impacted_period_end = fields.Date.from_string(options['date']['date_to']) + relativedelta(months=closing_months_delay) - tax_lock_date = closing_move.company_id.tax_lock_date - if carryover_values and tax_lock_date and tax_lock_date >= carryover_impacted_period_end: - raise UserError(_("You cannot reset this closing entry to draft, as it would delete carryover values impacting the tax report of a " - "locked period. To do this, you first need to modify you tax return lock date.")) - - if self._has_subsequent_posted_closing_moves(): - raise UserError(_("You cannot reset this closing entry to draft, as another closing entry has been posted at a later date.")) - - carryover_values.unlink() - - def _has_subsequent_posted_closing_moves(self): - self.ensure_one() - closing_domains = [ - ('company_id', '=', self.company_id.id), - ('tax_closing_report_id', '!=', False), - ('state', '=', 'posted'), - ('date', '>', self.date), - ('fiscal_position_id', '=', self.fiscal_position_id.id) - ] - return bool(self.env['account.move'].search_count(closing_domains, limit=1)) - - def _get_tax_to_pay_on_closing(self): - self.ensure_one() - tax_payable_accounts = self.env['account.tax.group'].search([ - ('company_id', '=', self.company_id.id), - ]).tax_payable_account_id - payable_lines = self.line_ids.filtered(lambda line: line.account_id in tax_payable_accounts) - return self.currency_id.round(-sum(payable_lines.mapped('balance'))) - - def _action_tax_to_pay_wizard(self): - # hook for l10n tax payment wizard - return self.action_open_tax_report() - - def action_open_tax_report(self): - action = self.env["ir.actions.actions"]._for_xml_id("at_accounting.action_account_report_gt") - if not self.tax_closing_report_id: - raise UserError(_("You can't open a tax report from a move without a VAT closing date.")) - options = self._get_tax_closing_report_options(self.company_id, self.fiscal_position_id, self.tax_closing_report_id, self.date) - # Pass options in context and set ignore_session: true to prevent using session options - action.update({'params': {'options': options, 'ignore_session': True}}) - return action - - def _close_tax_period(self, report, options): - """ Closes tax closing entries. The tax closing activities on them will be marked done, and the next tax closing entry - will be generated or updated (if already existing). Also, a pdf of the tax report at the time of closing - will be posted in the chatter of each move. - - The tax lock date of each move's company will be set to the move's date in case no other draft tax closing - move exists for that company (whatever their foreign VAT fiscal position) before or at that date, meaning that - all the tax closings have been performed so far. - """ - self.ensure_one() - if not self.env.user.has_group('account.group_account_manager'): - raise UserError(_('Only Billing Administrators are allowed to change lock dates!')) - report = self.tax_closing_report_id - options = self._get_tax_closing_report_options(self.company_id, self.fiscal_position_id, report, self.date) - - if not self.fiscal_position_id and (not self.company_id.tax_lock_date or self.date > self.company_id.tax_lock_date): - self.company_id.sudo().tax_lock_date = self.date - self.env['account.report']._generate_default_external_values(options['date']['date_from'], options['date']['date_to'], True) - - sender_company = report._get_sender_company_for_export(options) - company_ids = report.get_report_company_ids(options) - if sender_company == self.company_id: - depending_closings = self.env['account.tax.report.handler']._get_tax_closing_entries_for_closed_period(report, options, self.env['res.company'].browse(company_ids), posted_only=False) - self - depending_closings_to_post = depending_closings.filtered(lambda x: x.state == 'draft') - if depending_closings_to_post: - depending_action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_journal_line") - depending_action = clean_action(depending_action, env=self.env) - - if len(depending_closings_to_post) == 1: - depending_action['views'] = [(self.env.ref('account.view_move_form').id, 'form')] - depending_action['res_id'] = depending_closings_to_post.id - else: - depending_action['domain'] = [('id', 'in', depending_closings_to_post.ids)] - depending_action['context'] = dict(ast.literal_eval(depending_action['context'])) - depending_action['context'].pop('search_default_posted', None) - - # In case of dependent moves, we will raise an error that will be caught in the action_post method. - # When the exception is caught, a component will inform the user that there are some dependent moves - # to be posted and provide a link to these moves. - raise TaxClosingNonPostedDependingMovesError(depending_action) - - # Generate the carryover values. - report.with_context(allowed_company_ids=company_ids)._generate_carryover_external_values(options) - - # Post the message with the attachments (PDF of the report, and possibly an additional export file) - attachments = self._get_vat_report_attachments(report, options) - subject = _( - "Vat closing from %(date_from)s to %(date_to)s", - date_from=format_date(self.env, options['date']['date_from']), - date_to=format_date(self.env, options['date']['date_to']), - ) - self.with_context(no_new_invoice=True).message_post(body=self.ref, subject=subject, attachments=attachments) - - # Log a note on depending closings, redirecting to the main one - for closing_move in depending_closings: - closing_move.message_post( - body=Markup("%s") % _("The attachments of the tax report can be found on the closing entry of the representative company.", self.id), - ) - - # End activity - activity = self.company_id._get_tax_closing_reminder_activity(report.id, self.date, self.fiscal_position_id.id) - if activity: - activity.action_done() - - # Generate next activity - self.company_id._generate_tax_closing_reminder_activity(self.tax_closing_report_id, self.date + relativedelta(days=1), self.fiscal_position_id if self.fiscal_position_id.foreign_vat else None) - - self._close_tax_period_create_activities() - - def _close_tax_period_create_activities(self): - mat_to_send_xml_id = 'at_accounting.mail_activity_type_tax_report_to_be_sent' - mat_to_send = self.env.ref(mat_to_send_xml_id, raise_if_not_found=False) - if not mat_to_send: - # As this is introduced in stable, we ensure data exists by creating them on the fly if needed - mat_to_send = self.env['mail.activity.type'].sudo()._load_records([{ - 'xml_id': mat_to_send_xml_id, - 'noupdate': False, - 'values': { - 'name': 'Tax Report Ready', - 'summary': 'Tax report is ready to be sent to the administration', - 'category': 'tax_report', - 'delay_count': '0', - 'delay_unit': 'days', - 'delay_from': 'current_date', - 'res_model': 'account.move', - 'chaining_type': 'suggest', - } - }]) - mat_to_pay_xml_id = 'at_accounting.mail_activity_type_tax_report_to_pay' - mat_to_pay = self.env.ref(mat_to_pay_xml_id, raise_if_not_found=False) - - act_user = mat_to_send.default_user_id - if act_user and not (self.company_id in act_user.company_ids and act_user.has_group('account.group_account_manager')): - act_user = self.env['res.users'] - - moves_without_send_activity = self.filtered_domain([ - '|', - ('activity_ids', '=', False), - ('activity_ids', 'not any', [('activity_type_id.id', '=', mat_to_send.id)]), - ]) - - for move in moves_without_send_activity: - period_start, period_end = move.company_id._get_tax_closing_period_boundaries(move.date, move.tax_closing_report_id) - period_desc = move.company_id._get_tax_closing_move_description(move.company_id._get_tax_periodicity(move.tax_closing_report_id), period_start, period_end, move.fiscal_position_id, move.tax_closing_report_id) - move.with_context(mail_activity_quick_update=True).activity_schedule( - act_type_xmlid=mat_to_send_xml_id, - summary=_("Send tax report: %s", period_desc), - date_deadline=fields.Date.context_today(move), - user_id=act_user.id or self.env.user.id, - ) - - if mat_to_pay and mat_to_pay not in move.activity_ids.activity_type_id and move._get_tax_to_pay_on_closing() > 0: - move.with_context(mail_activity_quick_update=True).activity_schedule( - act_type_xmlid=mat_to_pay_xml_id, - summary=_("Pay tax: %s", period_desc), - date_deadline=fields.Date.context_today(move), - user_id=act_user.id or self.env.user.id, - ) - - def refresh_tax_entry(self): - for move in self.filtered(lambda m: m.tax_closing_report_id and m.state == 'draft'): - report = move.tax_closing_report_id - options = move._get_tax_closing_report_options(move.company_id, move.fiscal_position_id, report, move.date) - self.env[report.custom_handler_model_name or 'account.generic.tax.report.handler']._generate_tax_closing_entries(report, options, closing_moves=move) - - @api.model - def _get_tax_closing_report_options(self, company, fiscal_position, report, date_inside_period): - _dummy, date_to = company._get_tax_closing_period_boundaries(date_inside_period, report) - - # In case the company submits its report in different regions, a closing entry - # is made for each fiscal position defining a foreign VAT. - # We hence need to make sure to select a tax report in the right country when opening - # the report (in case there are many, we pick the first one available; it doesn't impact the closing) - if fiscal_position and fiscal_position.foreign_vat: - fpos_option = fiscal_position.id - report_country = fiscal_position.country_id - else: - fpos_option = 'domestic' - report_country = company.account_fiscal_country_id - - options = { - 'date': { - 'date_to': fields.Date.to_string(date_to), - 'filter': 'custom_tax_period', - 'mode': 'range', - }, - 'selected_variant_id': report.id, - 'sections_source_id': report.id, - 'fiscal_position': fpos_option, - 'tax_unit': 'company_only', - } - - if report.filter_multi_company == 'tax_units': - # Enforce multicompany if the closing is done for a tax unit - candidate_tax_unit = company.account_tax_unit_ids.filtered(lambda x: x.country_id == report_country) - if candidate_tax_unit: - options['tax_unit'] = candidate_tax_unit.id - company_ids = candidate_tax_unit.company_ids.ids - else: - same_vat_branches = self.env.company._get_branches_with_same_vat() - # Consider the one with the least number of parents (highest in hierarchy) as the active company, coming first - company_ids = same_vat_branches.sorted(lambda x: len(x.parent_ids)).ids - else: - company_ids = self.env.company.ids - - return report.with_context(allowed_company_ids=company_ids).get_options(previous_options=options) - - def _get_vat_report_attachments(self, report, options): - # Fetch pdf - pdf_data = report.export_to_pdf(options) - return [(pdf_data['file_name'], pdf_data['file_content'])] - - def _compute_tax_closing_alert(self): - for move in self: - move.tax_closing_alert = ( - move.state == 'posted' - and move.tax_closing_report_id - and move.company_id.tax_lock_date - and move.company_id.tax_lock_date < move.date - ) - - asset_id = fields.Many2one('account.asset', string='Asset', index=True, ondelete='cascade', copy=False, - domain="[('company_id', '=', company_id)]") - asset_remaining_value = fields.Monetary(string='Depreciable Value', - compute='_compute_depreciation_cumulative_value') - asset_depreciated_value = fields.Monetary(string='Cumulative Depreciation', - compute='_compute_depreciation_cumulative_value') - # true when this move is the result of the changing of value of an asset - asset_value_change = fields.Boolean() - # how many days of depreciation this entry corresponds to - asset_number_days = fields.Integer(string="Number of days", copy=False) # deprecated - asset_depreciation_beginning_date = fields.Date(string="Date of the beginning of the depreciation", - copy=False) # technical field stating when the depreciation associated with this entry has begun - depreciation_value = fields.Monetary( - string="Depreciation", - compute="_compute_depreciation_value", inverse="_inverse_depreciation_value", store=True, - ) - - asset_ids = fields.One2many('account.asset', string='Assets', compute="_compute_asset_ids") - asset_id_display_name = fields.Char( - compute="_compute_asset_ids") # just a button label. That's to avoid a plethora of different buttons defined in xml - count_asset = fields.Integer(compute="_compute_asset_ids") - draft_asset_exists = fields.Boolean(compute="_compute_asset_ids") - asset_move_type = fields.Selection( - selection=[ - ('depreciation', 'Depreciation'), - ('sale', 'Sale'), - ('purchase', 'Purchase'), - ('disposal', 'Disposal'), - ('negative_revaluation', 'Negative revaluation'), - ('positive_revaluation', 'Positive revaluation'), - ], - string='Asset Move Type', - compute='_compute_asset_move_type', store=True, - copy=False, - ) - - # ------------------------------------------------------------------------- - # COMPUTE METHODS - # ------------------------------------------------------------------------- - @api.depends('asset_id', 'depreciation_value', 'asset_id.total_depreciable_value', - 'asset_id.already_depreciated_amount_import', 'state') - def _compute_depreciation_cumulative_value(self): - self.asset_depreciated_value = 0 - self.asset_remaining_value = 0 - - # make sure to protect all the records being assigned, because the - # assignments invoke method write() on non-protected records, which may - # cause an infinite recursion in case method write() needs to read one - # of these fields (like in case of a base automation) - fields = [self._fields['asset_remaining_value'], self._fields['asset_depreciated_value']] - with self.env.protecting(fields, self.asset_id.depreciation_move_ids): - for asset in self.asset_id: - depreciated = 0 - remaining = asset.total_depreciable_value - asset.already_depreciated_amount_import - for move in asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv._origin.id)): - if move.state != 'cancel': - remaining -= move.depreciation_value - depreciated += move.depreciation_value - move.asset_remaining_value = remaining - move.asset_depreciated_value = depreciated - - @api.depends('line_ids.balance') - def _compute_depreciation_value(self): - for move in self: - asset = move.asset_id or move.reversed_entry_id.asset_id # reversed moves are created before being assigned to the asset - if asset: - account_internal_group = 'expense' - asset_depreciation = sum( - move.line_ids.filtered(lambda - l: l.account_id.internal_group == account_internal_group or l.account_id == asset.account_depreciation_expense_id).mapped( - 'balance') - ) - # Special case of closing entry - only disposed assets of type 'purchase' should match this condition - # The condition on len(move.line_ids) is to avoid the case where there is only one depreciation move, and it is not a disposal move - # The condition will be matched because a disposal move from a disposal move will always have more than 2 lines, unlike a normal depreciation move - if any( - line.account_id == asset.account_asset_id - and float_compare(-line.balance, asset.original_value, - precision_rounding=asset.currency_id.rounding) == 0 - for line in move.line_ids - ) and len(move.line_ids) > 2: - asset_depreciation = ( - asset.original_value - - asset.salvage_value - - ( - move.line_ids[1].debit if asset.original_value > 0 else move.line_ids[1].credit - ) * (-1 if asset.original_value < 0 else 1) - ) - else: - asset_depreciation = 0 - move.depreciation_value = asset_depreciation - - @api.depends('asset_id', 'asset_ids') - def _compute_asset_move_type(self): - for move in self: - if move.asset_ids: - move.asset_move_type = 'positive_revaluation' if move.asset_ids.parent_id else 'purchase' - elif not move.asset_move_type or not move.asset_id: - move.asset_move_type = False - - # ------------------------------------------------------------------------- - # INVERSE METHODS - # ------------------------------------------------------------------------- - def _inverse_depreciation_value(self): - for move in self: - asset = move.asset_id - amount = abs(move.depreciation_value) - account = asset.account_depreciation_expense_id - move.write({'line_ids': [ - Command.update(line.id, { - 'balance': amount if line.account_id == account else -amount, - }) - for line in move.line_ids - ]}) - - # ------------------------------------------------------------------------- - # CONSTRAINT METHODS - # ------------------------------------------------------------------------- - @api.constrains('state', 'asset_id') - def _constrains_check_asset_state(self): - for move in self.filtered(lambda mv: mv.asset_id): - asset_id = move.asset_id - if asset_id.state == 'draft' and move.state == 'posted': - raise ValidationError( - _("You can't post an entry related to a draft asset. Please post the asset before.")) - - def _post(self, soft=True): - # OVERRIDE - posted = super()._post(soft) - - # log the post of a depreciation - posted._log_depreciation_asset() - - # look for any asset to create, in case we just posted a bill on an account - # configured to automatically create assets - posted.sudo()._auto_create_asset() - - return posted - - def _reverse_moves(self, default_values_list=None, cancel=False): - if default_values_list is None: - default_values_list = [{} for _i in self] - for move, default_values in zip(self, default_values_list): - # Report the value of this move to the next draft move or create a new one - if move.asset_id: - # Recompute the status of the asset for all depreciations posted after the reversed entry - - first_draft = min(move.asset_id.depreciation_move_ids.filtered(lambda m: m.state == 'draft'), - key=lambda m: m.date, default=None) - if first_draft: - # If there is a draft, simply move/add the depreciation amount here - first_draft.depreciation_value += move.depreciation_value - elif move.asset_id.state != 'close': - # If there was no draft move left, create one. - # Unless the asset is being closed, then the closing move - # takes care of balancing the asset. - last_date = max(move.asset_id.depreciation_move_ids.mapped('date')) - method_period = move.asset_id.method_period - - self.create(self._prepare_move_for_asset_depreciation({ - 'asset_id': move.asset_id, - 'amount': move.depreciation_value, - 'depreciation_beginning_date': last_date + ( - relativedelta(months=1) if method_period == "1" else relativedelta(years=1)), - 'date': last_date + ( - relativedelta(months=1) if method_period == "1" else relativedelta(years=1)), - 'asset_number_days': 0 - })) - - msg = _('Depreciation entry %(name)s reversed (%(value)s)', name=move.name, - value=formatLang(self.env, move.depreciation_value, currency_obj=move.company_id.currency_id)) - move.asset_id.message_post(body=msg) - default_values['asset_id'] = move.asset_id.id - default_values['asset_number_days'] = -move.asset_number_days - default_values['asset_depreciation_beginning_date'] = default_values.get('date', move.date) - - return super(AccountMove, self)._reverse_moves(default_values_list, cancel) - - def button_cancel(self): - # OVERRIDE - res = super(AccountMove, self).button_cancel() - self.env['account.asset'].sudo().search([('original_move_line_ids.move_id', 'in', self.ids)]).write( - {'active': False}) - return res - - def button_draft(self): - for move in self: - if any(asset_id.state != 'draft' for asset_id in move.asset_ids): - raise UserError(_('You cannot reset to draft an entry related to a posted asset')) - # Remove any draft asset that could be linked to the account move being reset to draft - move.asset_ids.filtered(lambda x: x.state == 'draft').unlink() - return super(AccountMove, self).button_draft() - - def _log_depreciation_asset(self): - for move in self.filtered(lambda m: m.asset_id): - asset = move.asset_id - msg = _('Depreciation entry %(name)s posted (%(value)s)', name=move.name, - value=formatLang(self.env, move.depreciation_value, currency_obj=move.company_id.currency_id)) - asset.message_post(body=msg) - - def _auto_create_asset(self): - create_list = [] - invoice_list = [] - auto_validate = [] - for move in self: - if not move.is_invoice(): - continue - - for move_line in move.line_ids: - if ( - move_line.account_id - and (move_line.account_id.can_create_asset) - and move_line.account_id.create_asset != "no" - and not (move_line.currency_id or move.currency_id).is_zero(move_line.price_total) - and not move_line.asset_ids - and not move_line.tax_line_id - and move_line.price_total > 0 - and not (move.move_type in ('out_invoice', - 'out_refund') and move_line.account_id.internal_group == 'asset') - ): - if not move_line.name: - if move_line.product_id: - move_line.name = move_line.product_id.display_name - else: - raise UserError( - _('Journal Items of %(account)s should have a label in order to generate an asset', - account=move_line.account_id.display_name)) - if move_line.account_id.multiple_assets_per_line: - # decimal quantities are not supported, quantities are rounded to the lower int - units_quantity = max(1, int(move_line.quantity)) - else: - units_quantity = 1 - - model_ids = move_line.account_id.asset_model_ids - vals = { - 'name': move_line.name, - 'company_id': move_line.company_id.id, - 'currency_id': move_line.company_currency_id.id, - 'analytic_distribution': move_line.analytic_distribution, - 'original_move_line_ids': [(6, False, move_line.ids)], - 'state': 'draft', - 'acquisition_date': move.invoice_date if not move.reversed_entry_id else move.reversed_entry_id.invoice_date, - } - for model_id in model_ids or [None]: - if model_id: - vals['model_id'] = model_id.id - - auto_validate.extend([move_line.account_id.create_asset == 'validate'] * units_quantity) - invoice_list.extend([move] * units_quantity) - for i in range(1, units_quantity + 1): - if units_quantity > 1: - vals['name'] = _("%(move_line)s (%(current)s of %(total)s)", move_line=move_line.name, - current=i, total=units_quantity) - create_list.extend([vals.copy()]) - - assets = self.env['account.asset'].with_context({}).create(create_list) - for asset, vals, invoice, validate in zip(assets, create_list, invoice_list, auto_validate): - if 'model_id' in vals: - asset._onchange_model_id() - if validate: - asset.validate() - if invoice: - asset.message_post(body=_('Asset created from invoice: %s', invoice._get_html_link())) - asset._post_non_deductible_tax_value() - return assets - - @api.model - def _prepare_move_for_asset_depreciation(self, vals): - missing_fields = {'asset_id', 'amount', 'depreciation_beginning_date', 'date', 'asset_number_days'} - set(vals) - if missing_fields: - raise UserError(_('Some fields are missing %s', ', '.join(missing_fields))) - asset = vals['asset_id'] - analytic_distribution = asset.analytic_distribution - depreciation_date = vals.get('date', fields.Date.context_today(self)) - company_currency = asset.company_id.currency_id - current_currency = asset.currency_id - prec = company_currency.decimal_places - amount_currency = vals['amount'] - amount = current_currency._convert(amount_currency, company_currency, asset.company_id, depreciation_date) - # Keep the partner on the original invoice if there is only one - partner = asset.original_move_line_ids.mapped('partner_id') - partner = partner[:1] if len(partner) <= 1 else self.env['res.partner'] - name = _("%s: Depreciation", asset.name) - move_line_1 = { - 'name': name, - 'partner_id': partner.id, - 'account_id': asset.account_depreciation_id.id, - 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, - 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, - 'currency_id': current_currency.id, - 'amount_currency': -amount_currency, - } - move_line_2 = { - 'name': name, - 'partner_id': partner.id, - 'account_id': asset.account_depreciation_expense_id.id, - 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, - 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, - 'currency_id': current_currency.id, - 'amount_currency': amount_currency, - } - # Only set the 'analytic_distribution' key if there is an analytic distribution on the asset. - # Otherwise, it prevents the computation of the analytic distribution. - if analytic_distribution: - move_line_1['analytic_distribution'] = analytic_distribution - move_line_2['analytic_distribution'] = analytic_distribution - move_vals = { - 'partner_id': partner.id, - 'date': depreciation_date, - 'journal_id': asset.journal_id.id, - 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], - 'asset_id': asset.id, - 'ref': name, - 'asset_depreciation_beginning_date': vals['depreciation_beginning_date'], - 'asset_number_days': vals['asset_number_days'], - 'asset_value_change': vals.get('asset_value_change', False), - 'move_type': 'entry', - 'currency_id': current_currency.id, - 'asset_move_type': vals.get('asset_move_type', 'depreciation'), - 'company_id': asset.company_id.id, - } - return move_vals - - @api.depends('line_ids.asset_ids') - def _compute_asset_ids(self): - for record in self: - record.asset_ids = record.line_ids.asset_ids - record.count_asset = len(record.asset_ids) - record.asset_id_display_name = _('Asset') - record.draft_asset_exists = bool(record.asset_ids.filtered(lambda x: x.state == "draft")) - - def open_asset_view(self): - return self.asset_id.open_asset(['form']) - - def action_open_asset_ids(self): - return self.asset_ids.open_asset(['list', 'form']) - - -class AccountMoveLine(models.Model): - _name = "account.move.line" - _inherit = "account.move.line" - - move_attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment') - - # Deferred management fields - deferred_start_date = fields.Date( - string="Start Date", - compute='_compute_deferred_start_date', store=True, readonly=False, - index='btree_not_null', - copy=False, - help="Date at which the deferred expense/revenue starts" - ) - deferred_end_date = fields.Date( - string="End Date", - index='btree_not_null', - copy=False, - help="Date at which the deferred expense/revenue ends" - ) - has_deferred_moves = fields.Boolean(compute='_compute_has_deferred_moves') - has_abnormal_deferred_dates = fields.Boolean(compute='_compute_has_abnormal_deferred_dates') - - def _order_to_sql(self, order, query, alias=None, reverse=False): - sql_order = super()._order_to_sql(order, query, alias, reverse) - preferred_aml_residual_value = self._context.get('preferred_aml_value') - preferred_aml_currency_id = self._context.get('preferred_aml_currency_id') - if preferred_aml_residual_value and preferred_aml_currency_id and order == self._order: - currency = self.env['res.currency'].browse(preferred_aml_currency_id) - # using round since currency.round(55.55) = 55.550000000000004 - preferred_aml_residual_value = round(preferred_aml_residual_value, currency.decimal_places) - sql_residual_currency = self._field_to_sql(alias or self._table, 'amount_residual_currency', query) - sql_currency = self._field_to_sql(alias or self._table, 'currency_id', query) - return SQL( - "ROUND(%(residual_currency)s, %(decimal_places)s) = %(value)s " - "AND %(currency)s = %(currency_id)s DESC, %(order)s", - residual_currency=sql_residual_currency, - decimal_places=currency.decimal_places, - value=preferred_aml_residual_value, - currency=sql_currency, - currency_id=currency.id, - order=sql_order, - ) - return sql_order - - def copy_data(self, default=None): - data_list = super().copy_data(default=default) - for line, values in zip(self, data_list): - if 'move_reverse_cancel' in self._context: - values['deferred_start_date'] = line.deferred_start_date - values['deferred_end_date'] = line.deferred_end_date - return data_list - - def write(self, vals): - """ Prevent changing the account of a move line when there are already deferral entries. - """ - if 'account_id' in vals: - for line in self: - if ( - line.has_deferred_moves - and line.deferred_start_date - and line.deferred_end_date - and vals['account_id'] != line.account_id.id - ): - raise UserError(_( - "You cannot change the account for a deferred line in %(move_name)s if it has already been deferred.", - move_name=line.move_id.display_name - )) - return super().write(vals) - - # ============================= START - Deferred management ==================================== - def _compute_has_deferred_moves(self): - for line in self: - line.has_deferred_moves = line.move_id.deferred_move_ids - - @api.depends('deferred_start_date', 'deferred_end_date') - def _compute_has_abnormal_deferred_dates(self): - # In the deferred computations, we always assume that both the start and end date are inclusive - # E.g: 1st January -> 31st December is *exactly* 1 year = 12 months - # However, the user may instead put 1st January -> 1st January of next year which is then - # 12 months + 1/30 month = 12.03 months which may result in odd amounts when deferrals are created - # For this reason, we alert the user if we detect such a case - # Other cases were the number of months is not round should not be handled. - for line in self: - line.has_abnormal_deferred_dates = ( - line.deferred_start_date - and line.deferred_end_date - and float_compare( - self.env['account.move']._get_deferred_diff_dates(line.deferred_start_date, line.deferred_end_date + relativedelta(days=1)) % 1, # end date is included - 1 / 30, - precision_digits=2 - ) == 0 - ) - - def _has_deferred_compatible_account(self): - self.ensure_one() - return ( - self.move_id.is_purchase_document() - and - self.account_id.account_type in ('expense', 'expense_depreciation', 'expense_direct_cost') - ) or ( - self.move_id.is_sale_document() - and - self.account_id.account_type in ('income', 'income_other') - ) - - @api.onchange('deferred_start_date') - def _onchange_deferred_start_date(self): - if not self._has_deferred_compatible_account(): - self.deferred_start_date = False - - @api.onchange('deferred_end_date') - def _onchange_deferred_end_date(self): - if not self._has_deferred_compatible_account(): - self.deferred_end_date = False - - @api.depends('deferred_end_date', 'move_id.invoice_date', 'move_id.state') - def _compute_deferred_start_date(self): - for line in self: - if not line.deferred_start_date and line.move_id.invoice_date and line.deferred_end_date: - line.deferred_start_date = line.move_id.invoice_date - - @api.constrains('deferred_start_date', 'deferred_end_date', 'account_id') - def _check_deferred_dates(self): - for line in self: - if line.deferred_start_date and not line.deferred_end_date: - raise UserError(_("You cannot create a deferred entry with a start date but no end date.")) - elif line.deferred_start_date and line.deferred_end_date and line.deferred_start_date > line.deferred_end_date: - raise UserError(_("You cannot create a deferred entry with a start date later than the end date.")) - - @api.model - def _get_deferred_ends_of_month(self, start_date, end_date): - """ - :return: a list of dates corresponding to the end of each month between start_date and end_date. - See test_get_ends_of_month for examples. - """ - dates = [] - while start_date <= end_date: - start_date = start_date + relativedelta(day=31) # Go to end of month - dates.append(start_date) - start_date = start_date + relativedelta(days=1) # Go to first day of next month - return dates - - def _get_deferred_periods(self): - """ - :return: a list of tuples (start_date, end_date) during which the deferred expense/revenue is spread. - If there is only one period containing the move date, it means that we don't need to defer the - expense/revenue since the invoice deferral and its deferred entry will be created on the same day and will - thus cancel each other. - """ - self.ensure_one() - periods = [ - (max(self.deferred_start_date, date.replace(day=1)), min(date, self.deferred_end_date), 'current') - for date in self._get_deferred_ends_of_month(self.deferred_start_date, self.deferred_end_date) - ] - if not periods or len(periods) == 1 and periods[0][0].replace(day=1) == self.date.replace(day=1): - return [] - else: - return periods - - @api.model - def _get_deferred_amounts_by_line_values(self, line): - return { - 'account_id': line['account_id'], - # line either be a dict with ids (coming from SQL query), or a real account.move.line object - 'product_id': line['product_id'] if isinstance(line, dict) else line['product_id'].id, - 'product_category_id': line['product_category_id'] if isinstance(line, dict) else line['product_category_id'].id, - 'balance': line['balance'], - 'move_id': line['move_id'], - } - - @api.model - def _get_deferred_lines_values(self, account_id, balance, ref, analytic_distribution, line=None): - return { - 'account_id': account_id, - # line either be a dict with ids (coming from SQL query), or a real account.move.line object - 'product_id': line['product_id'] if isinstance(line, dict) else line['product_id'].id, - 'product_category_id': line['product_category_id'] if isinstance(line, dict) else line['product_category_id'].id, - 'balance': balance, - 'name': ref, - 'analytic_distribution': analytic_distribution, - } - - # ============================= END - Deferred management ==================================== - - def _get_computed_taxes(self): - if self.move_id.deferred_original_move_ids: - # If this line is part of a deferral move, do not (re)calculate its taxes automatically. - # Doing so might unvoluntarily impact the tax report in deferral moves (if a default tax is set on the account). - return self.tax_ids - return super()._get_computed_taxes() - - def _compute_attachment(self): - for record in self: - record.move_attachment_ids = self.env['ir.attachment'].search(expression.OR(record._get_attachment_domains())) - - def action_reconcile(self): - """ This function is called by the 'Reconcile' button of account.move.line's - list view. It performs reconciliation between the selected lines. - - If the reconciliation can be done directly we do it silently - - Else, if a write-off is required we open the wizard to let the client enter required information - """ - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=self.ids, - ).new({}) - return wizard._action_open_wizard() if (wizard.is_write_off_required or wizard.force_partials) else wizard.reconcile() - - def _get_predict_postgres_dictionary(self): - lang = self._context.get('lang') and self._context.get('lang')[:2] - return {'fr': 'french'}.get(lang, 'english') - - @api.model - def _build_predictive_query(self, move_id, additional_domain=None): - move_query = self.env['account.move']._where_calc([ - ('move_type', '=', move_id.move_type), - ('state', '=', 'posted'), - ('partner_id', '=', move_id.partner_id.id), - ('company_id', '=', move_id.journal_id.company_id.id or self.env.company.id), - ]) - move_query.order = 'account_move.invoice_date' - move_query.limit = int(self.env["ir.config_parameter"].sudo().get_param( - "account.bill.predict.history.limit", - '100', - )) - return self.env['account.move.line']._where_calc([ - ('move_id', 'in', move_query), - ('display_type', '=', 'product'), - ] + (additional_domain or [])) - - @api.model - def _predicted_field(self, name, partner_id, field, query=None, additional_queries=None): - r"""Predict the most likely value based on the previous history. - - This method uses postgres tsvector in order to try to deduce a field of - an invoice line based on the text entered into the name (description) - field and the partner linked. - We only limit the search on the previous 100 entries, which according - to our tests bore the best results. However this limit parameter is - configurable by creating a config parameter with the key: - account.bill.predict.history.limit - - For information, the tests were executed with a dataset of 40 000 bills - from a live database, We split the dataset in 2, removing the 5000 most - recent entries and we tried to use this method to guess the account of - this validation set based on the previous entries. - The result is roughly 90% of success. - - :param field (str): the sql column that has to be predicted. - /!\ it is injected in the query without any checks. - :param query (osv.Query): the query object on account.move.line that is - used to do the ranking, containing the right domain, limit, etc. If - it is omitted, a default query is used. - :param additional_queries (list): can be used in addition to the - default query on account.move.line to fetch data coming from other - tables, to have starting values for instance. - /!\ it is injected in the query without any checks. - """ - if not name or not partner_id: - return False - - psql_lang = self._get_predict_postgres_dictionary() - description = name + ' account_move_line' # give more priority to main query than additional queries - parsed_description = re.sub(r"[*&()|!':<>=%/~@,.;$\[\]]+", " ", description) - parsed_description = ' | '.join(parsed_description.split()) - - try: - main_source = (query if query is not None else self._build_predictive_query(self.move_id)).select( - SQL("%s AS prediction", field), - SQL( - "setweight(to_tsvector(%s, account_move_line.name), 'B') || setweight(to_tsvector('simple', 'account_move_line'), 'A') AS document", - psql_lang - ), - ) - if "(" in field.code: # aggregate function - main_source = SQL("%s %s", main_source, SQL("GROUP BY account_move_line.id, account_move_line.name, account_move_line.partner_id")) - - self.env.cr.execute(SQL(""" - WITH account_move_line AS MATERIALIZED (%(account_move_line)s), - - source AS (%(source)s), - - ranking AS ( - SELECT prediction, ts_rank(source.document, query_plain) AS rank - FROM source, to_tsquery(%(lang)s, %(description)s) query_plain - WHERE source.document @@ query_plain - ) - - SELECT prediction, MAX(rank) AS ranking, COUNT(*) - FROM ranking - GROUP BY prediction - ORDER BY ranking DESC, count DESC - LIMIT 2 - """, - account_move_line=self._build_predictive_query(self.move_id).select(SQL('*')), - source=SQL('(%s)', SQL(') UNION ALL (').join([main_source] + (additional_queries or []))), - lang=psql_lang, - description=parsed_description, - )) - result = self.env.cr.dictfetchall() - if result: - # Only confirm the prediction if it's at least 10% better than the second one - if len(result) > 1 and result[0]['ranking'] < 1.1 * result[1]['ranking']: - return False - return result[0]['prediction'] - except Exception: - # In case there is an error while parsing the to_tsquery (wrong character for example) - # We don't want to have a blocking traceback, instead return False - _logger.exception('Error while predicting invoice line fields') - return False - - def _predict_taxes(self): - field = SQL('array_agg(account_move_line__tax_rel__tax_ids.id ORDER BY account_move_line__tax_rel__tax_ids.id)') - query = self._build_predictive_query(self.move_id) - query.left_join('account_move_line', 'id', 'account_move_line_account_tax_rel', 'account_move_line_id', 'tax_rel') - query.left_join('account_move_line__tax_rel', 'account_tax_id', 'account_tax', 'id', 'tax_ids') - query.add_where('account_move_line__tax_rel__tax_ids.active IS NOT FALSE') - predicted_tax_ids = self._predicted_field(self.name, self.partner_id, field, query) - if predicted_tax_ids == [None]: - return False - if predicted_tax_ids is not False and set(predicted_tax_ids) != set(self.tax_ids.ids): - return predicted_tax_ids - return False - - @api.model - def _predict_specific_tax(self, move, name, partner, amount_type, amount, type_tax_use): - field = SQL('array_agg(account_move_line__tax_rel__tax_ids.id ORDER BY account_move_line__tax_rel__tax_ids.id)') - query = self._build_predictive_query(move) - query.left_join('account_move_line', 'id', 'account_move_line_account_tax_rel', 'account_move_line_id', 'tax_rel') - query.left_join('account_move_line__tax_rel', 'account_tax_id', 'account_tax', 'id', 'tax_ids') - query.add_where(""" - account_move_line__tax_rel__tax_ids.active IS NOT FALSE - AND account_move_line__tax_rel__tax_ids.amount_type = %s - AND account_move_line__tax_rel__tax_ids.type_tax_use = %s - AND account_move_line__tax_rel__tax_ids.amount = %s - """, (amount_type, type_tax_use, amount)) - return self._predicted_field(name, partner, field, query) - - def _predict_product(self): - predict_product = int(self.env['ir.config_parameter'].sudo().get_param('account_predictive_bills.predict_product', '1')) - if predict_product and self.company_id.predict_bill_product: - query = self._build_predictive_query(self.move_id, ['|', ('product_id', '=', False), ('product_id.active', '=', True)]) - predicted_product_id = self._predicted_field(self.name, self.partner_id, SQL('account_move_line.product_id'), query) - if predicted_product_id and predicted_product_id != self.product_id.id: - return predicted_product_id - return False - - def _predict_account(self): - field = SQL('account_move_line.account_id') - if self.move_id.is_purchase_document(True): - excluded_group = 'income' - else: - excluded_group = 'expense' - account_query = self.env['account.account']._where_calc([ - *self.env['account.account']._check_company_domain(self.move_id.company_id or self.env.company), - ('deprecated', '=', False), - ('internal_group', 'not in', (excluded_group, 'off')), - ('account_type', 'not in', ('liability_payable', 'asset_receivable')), - ]) - account_name = self.env['account.account']._field_to_sql('account_account', 'name') - psql_lang = self._get_predict_postgres_dictionary() - additional_queries = [SQL(account_query.select( - SQL("account_account.id AS account_id"), - SQL("setweight(to_tsvector(%(psql_lang)s, %(account_name)s), 'B') AS document", psql_lang=psql_lang, account_name=account_name), - ))] - query = self._build_predictive_query(self.move_id, [('account_id', 'in', account_query)]) - - predicted_account_id = self._predicted_field(self.name, self.partner_id, field, query, additional_queries) - if predicted_account_id and predicted_account_id != self.account_id.id: - return predicted_account_id - return False - - @api.onchange('name') - def _onchange_name_predictive(self): - if ((self.move_id.quick_edit_mode or self.move_id.move_type == 'in_invoice') and self.name and self.display_type == 'product' - and not self.env.context.get('disable_onchange_name_predictive', False)): - - if not self.product_id: - predicted_product_id = self._predict_product() - if predicted_product_id: - # We only update the price_unit, tax_ids and name in case they evaluate to False - protected_fields = ['price_unit', 'tax_ids', 'name'] - to_protect = [self._fields[fname] for fname in protected_fields if self[fname]] - with self.env.protecting(to_protect, self): - self.product_id = predicted_product_id - - # In case no product has been set, the account and taxes - # will not depend on any product and can thus be predicted - if not self.product_id: - # Predict account. - predicted_account_id = self._predict_account() - if predicted_account_id: - self.account_id = predicted_account_id - - # Predict taxes - predicted_tax_ids = self._predict_taxes() - if predicted_tax_ids: - self.tax_ids = [Command.set(predicted_tax_ids)] - - def _read_group_select(self, aggregate_spec, query): - # Enable to use HAVING clause that sum rounded values depending on the - # currency precision settings. Limitation: we only handle a having - # clause of one element with that specific method :sum_rounded. - fname, __, func = models.parse_read_group_spec(aggregate_spec) - if func != 'sum_rounded': - return super()._read_group_select(aggregate_spec, query) - currency_alias = query.make_alias(self._table, 'currency_id') - query.add_join('LEFT JOIN', currency_alias, 'res_currency', SQL( - "%s = %s", - self._field_to_sql(self._table, 'currency_id', query), - SQL.identifier(currency_alias, 'id'), - )) - - return SQL( - 'SUM(ROUND(%s, %s))', - self._field_to_sql(self._table, fname, query), - self.env['res.currency']._field_to_sql(currency_alias, 'decimal_places', query), - ) - - def _read_group_groupby(self, groupby_spec, query): - # enable grouping by :abs_rounded on fields, which is useful when trying - # to match positive and negative amounts - if ':' in groupby_spec: - fname, method = groupby_spec.split(':') - if method == 'abs_rounded': - # rounds with the used currency settings - currency_alias = query.make_alias(self._table, 'currency_id') - query.add_join('LEFT JOIN', currency_alias, 'res_currency', SQL( - "%s = %s", - self._field_to_sql(self._table, 'currency_id', query), - SQL.identifier(currency_alias, 'id'), - )) - - return SQL( - 'ROUND(ABS(%s), %s)', - self._field_to_sql(self._table, fname, query), - self.env['res.currency']._field_to_sql(currency_alias, 'decimal_places', query), - ) - - return super()._read_group_groupby(groupby_spec, query) - - asset_ids = fields.Many2many('account.asset', 'asset_move_line_rel', 'line_id', 'asset_id', string='Related Assets', - copy=False) - non_deductible_tax_value = fields.Monetary(compute='_compute_non_deductible_tax_value', - currency_field='company_currency_id') - - def _get_computed_taxes(self): - if self.move_id.asset_id: - return self.tax_ids - return super()._get_computed_taxes() - - def turn_as_asset(self): - if len(self.company_id) != 1: - raise UserError(_("All the lines should be from the same company")) - if any(line.move_id.state == 'draft' for line in self): - raise UserError(_("All the lines should be posted")) - if any(account != self[0].account_id for account in self.mapped('account_id')): - raise UserError(_("All the lines should be from the same account")) - ctx = self.env.context.copy() - ctx.update({ - 'default_original_move_line_ids': [(6, False, self.env.context['active_ids'])], - 'default_company_id': self.company_id.id, - }) - return { - "name": _("Turn as an asset"), - "type": "ir.actions.act_window", - "res_model": "account.asset", - "views": [[False, "form"]], - "target": "current", - "context": ctx, - } - - @api.depends('tax_ids.invoice_repartition_line_ids') - def _compute_non_deductible_tax_value(self): - """ Handle the specific case of non deductible taxes, - such as "50% Non Déductible - Frais de voiture (Prix Excl.)" in Belgium. - """ - non_deductible_tax_ids = self.tax_ids.invoice_repartition_line_ids.filtered( - lambda line: line.repartition_type == 'tax' and not line.use_in_tax_closing - ).tax_id - - res = {} - if non_deductible_tax_ids: - domain = [('move_id', 'in', self.move_id.ids)] - tax_details_query = self._get_query_tax_details_from_domain(domain) - - self.flush_model() - self._cr.execute(SQL( - ''' - SELECT - tdq.base_line_id, - SUM(tdq.tax_amount_currency) - FROM (%(tax_details_query)s) AS tdq - JOIN account_move_line aml ON aml.id = tdq.tax_line_id - JOIN account_tax_repartition_line trl ON trl.id = tdq.tax_repartition_line_id - WHERE tdq.base_line_id IN %(base_line_ids)s - AND trl.use_in_tax_closing IS FALSE - GROUP BY tdq.base_line_id - ''', - tax_details_query=tax_details_query, - base_line_ids=tuple(self.ids), - )) - - res = {row['base_line_id']: row['sum'] for row in self._cr.dictfetchall()} - - for record in self: - record.non_deductible_tax_value = res.get(record._origin.id, 0.0) diff --git a/addons/at_accounting/models/account_move_line.py b/addons/at_accounting/models/account_move_line.py deleted file mode 100644 index 5de52bc..0000000 --- a/addons/at_accounting/models/account_move_line.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models, fields, _ - -from odoo.exceptions import UserError -from odoo.tools import SQL - -class AccountMoveLine(models.Model): - _name = "account.move.line" - _inherit = "account.move.line" - - exclude_bank_lines = fields.Boolean(compute='_compute_exclude_bank_lines', store=True) - - @api.depends('journal_id') - def _compute_exclude_bank_lines(self): - for move_line in self: - move_line.exclude_bank_lines = move_line.account_id != move_line.journal_id.default_account_id - - @api.constrains('tax_ids', 'tax_tag_ids') - def _check_taxes_on_closing_entries(self): - for aml in self: - if aml.move_id.tax_closing_report_id and (aml.tax_ids or aml.tax_tag_ids): - raise UserError(_("You cannot add taxes on a tax closing move line.")) - - @api.depends('product_id', 'product_uom_id', 'move_id.tax_closing_report_id') - def _compute_tax_ids(self): - """ Some special cases may see accounts used in tax closing having default taxes. - They would trigger the constrains above, which we don't want. Instead, we don't trigger - the tax computation in this case. - """ - # EXTEND account - lines_to_compute = self.filtered(lambda line: not line.move_id.tax_closing_report_id) - (self - lines_to_compute).tax_ids = False - super(AccountMoveLine, lines_to_compute)._compute_tax_ids() - - @api.model - def _prepare_aml_shadowing_for_report(self, change_equivalence_dict): - """ Prepares the fields lists for creating a temporary table shadowing the account_move_line one. - This is used to switch the computation mode of the reports, with analytics or financial budgets, for example. - - :param change_equivalence_dict: A dict, in the form {aml_field: sql_equivalence}, where: - - aml_field: is a string containing the name of field of account.move.line - - sql_equivalence: is the value to use to shadow aml_field. It can be an SQL object; if - it's not, it'll be escaped in the query. - - :return: A tuple of 2 SQL objects, so that: - - The first one is the fields list to pass into the INSERT TO part of the query filling up the temporary table - - The second one contains the field values to insert into the SELECT clause of the same query, in the same order - as in the first element of the returned tuple. - """ - line_fields = self.env['account.move.line'].fields_get() - self.env.cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name='account_move_line'") - stored_fields = {f[0] for f in self.env.cr.fetchall() if f[0] in line_fields} - - fields_to_insert = [] - for fname in stored_fields: - if fname in change_equivalence_dict: - fields_to_insert.append(SQL( - "%(original)s AS %(asname)s", - original=change_equivalence_dict[fname], - asname=SQL('"account_move_line.%s"', SQL(fname)), - )) - else: - line_field = line_fields[fname] - if line_field.get("translate"): - typecast = SQL('jsonb') - else: - typecast = SQL(self.env['account.move.line']._fields[fname].column_type[0]) - - fields_to_insert.append(SQL( - "CAST(NULL AS %(typecast)s) AS %(fname)s", - typecast=typecast, - fname=SQL('"account_move_line.%s"', SQL(fname)), - )) - - return SQL(', ').join(SQL.identifier(fname) for fname in stored_fields), SQL(', ').join(fields_to_insert) diff --git a/addons/at_accounting/models/account_multicurrency_revaluation_report.py b/addons/at_accounting/models/account_multicurrency_revaluation_report.py deleted file mode 100644 index 5c12262..0000000 --- a/addons/at_accounting/models/account_multicurrency_revaluation_report.py +++ /dev/null @@ -1,384 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, fields, api, _ -from odoo.tools import float_is_zero, SQL -from odoo.exceptions import UserError - -from itertools import chain - - -class MulticurrencyRevaluationReportCustomHandler(models.AbstractModel): - """Manage Unrealized Gains/Losses. - - In multi-currencies environments, we need a way to control the risk related - to currencies (in case some are higthly fluctuating) and, in some countries, - some laws also require to create journal entries to record the provisionning - of a probable future expense related to currencies. Hence, people need to - create a journal entry at the beginning of a period, to make visible the - probable expense in reports (and revert it at the end of the period, to - recon the real gain/loss. - """ - _name = 'account.multicurrency.revaluation.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Multicurrency Revaluation Report Custom Handler' - - def _get_custom_display_config(self): - return { - 'components': { - 'AccountReportFilters': 'at_accounting.MulticurrencyRevaluationReportFilters', - }, - 'templates': { - 'AccountReportLineName': 'at_accounting.MulticurrencyRevaluationReportLineName', - }, - } - - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - active_currencies = self.env['res.currency'].search([('active', '=', True)]) - if len(active_currencies) < 2: - raise UserError(_("You need to activate more than one currency to access this report.")) - rates = active_currencies._get_rates(self.env.company, options.get('date').get('date_to')) - # Normalize the rates to the company's currency - company_rate = rates[self.env.company.currency_id.id] - for key in rates.keys(): - rates[key] /= company_rate - - options['currency_rates'] = { - str(currency_id.id): { - 'currency_id': currency_id.id, - 'currency_name': currency_id.name, - 'currency_main': self.env.company.currency_id.name, - 'rate': (rates[currency_id.id] - if not previous_options.get('currency_rates', {}).get(str(currency_id.id), {}).get('rate') else - float(previous_options['currency_rates'][str(currency_id.id)]['rate'])), - } for currency_id in active_currencies - } - - for currency_rates in options['currency_rates'].values(): - if currency_rates['rate'] == 0: - raise UserError(_("The currency rate cannot be equal to zero")) - - options['company_currency'] = options['currency_rates'].pop(str(self.env.company.currency_id.id)) - options['custom_rate'] = any( - not float_is_zero(cr['rate'] - rates[cr['currency_id']], 20) - for cr in options['currency_rates'].values() - ) - - options['multi_currency'] = True - options['buttons'].append({'name': _('Adjustment Entry'), 'sequence': 30, 'action': 'action_multi_currency_revaluation_open_revaluation_wizard', 'always_show': True}) - - def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings): - if len(self.env.companies) > 1: - warnings['at_accounting.multi_currency_revaluation_report_warning_multicompany'] = {'alert_type': 'warning'} - if options['custom_rate']: - warnings['at_accounting.multi_currency_revaluation_report_warning_custom_rate'] = {'alert_type': 'warning'} - - def _custom_line_postprocessor(self, report, options, lines): - line_to_adjust_id = self.env.ref('at_accounting.multicurrency_revaluation_to_adjust').id - line_excluded_id = self.env.ref('at_accounting.multicurrency_revaluation_excluded').id - - rslt = [] - for index, line in enumerate(lines): - res_model_name, res_id = report._get_model_info_from_id(line['id']) - - if res_model_name == 'account.report.line' and ( - (res_id == line_to_adjust_id and report._get_model_info_from_id(lines[index + 1]['id']) == ('account.report.line', line_excluded_id)) or - (res_id == line_excluded_id and index == len(lines) - 1) - ): - # 'To Adjust' and 'Excluded' lines need to be hidden if they have no child - continue - - elif res_model_name == 'res.currency': - # Include the rate in the currency_id group lines - line['name'] = '{for_cur} (1 {comp_cur} = {rate:.6} {for_cur})'.format( - for_cur=line['name'], - comp_cur=self.env.company.currency_id.display_name, - rate=float(options['currency_rates'][str(res_id)]['rate']), - ) - - elif res_model_name == 'account.account': - # Mark the included/excluded lines, so that the custom component templates knows what label to put on them - line['is_included_line'] = report._get_res_id_from_line_id(line['id'], 'account.account') == line_to_adjust_id - - # Inject the related model into the line dict in order to use it on the custom component template on js side to display buttons - line['cur_revaluation_line_model'] = res_model_name - - rslt.append(line) - - return rslt - - def _custom_groupby_line_completer(self, report, options, line_dict): - model_info_from_id = report._get_model_info_from_id(line_dict['id']) - if model_info_from_id[0] == 'res.currency': - line_dict['unfolded'] = True - line_dict['unfoldable'] = False - - def action_multi_currency_revaluation_open_revaluation_wizard(self, options): - """Open the revaluation wizard.""" - form = self.env.ref('at_accounting.view_account_multicurrency_revaluation_wizard', False) - return { - 'name': _("Make Adjustment Entry"), - 'type': 'ir.actions.act_window', - 'res_model': 'account.multicurrency.revaluation.wizard', - 'view_mode': 'form', - 'view_id': form.id, - 'views': [(form.id, 'form')], - 'multi': 'True', - 'target': 'new', - 'context': { - **self._context, - 'multicurrency_revaluation_report_options': options, - }, - } - - # ACTIONS - def action_multi_currency_revaluation_open_general_ledger(self, options, params): - report = self.env['account.report'].browse(options['report_id']) - account_id = report._get_res_id_from_line_id(params['line_id'], 'account.account') - account_line_id = report._get_generic_line_id('account.account', account_id) - general_ledger_options = self.env.ref('at_accounting.general_ledger_report').get_options(options) - general_ledger_options['unfolded_lines'] = [account_line_id] - - general_ledger_action = self.env['ir.actions.actions']._for_xml_id('at_accounting.action_account_report_general_ledger') - general_ledger_action['params'] = { - 'options': general_ledger_options, - 'ignore_session': True, - } - - return general_ledger_action - - def action_multi_currency_revaluation_toggle_provision(self, options, params): - """ Include/exclude an account from the provision. """ - res_ids_map = self.env['account.report']._get_res_ids_from_line_id(params['line_id'], ['res.currency', 'account.account']) - account = self.env['account.account'].browse(res_ids_map['account.account']) - currency = self.env['res.currency'].browse(res_ids_map['res.currency']) - if currency in account.exclude_provision_currency_ids: - account.exclude_provision_currency_ids -= currency - else: - account.exclude_provision_currency_ids += currency - return { - 'type': 'ir.actions.client', - 'tag': 'reload', - } - - def action_multi_currency_revaluation_open_currency_rates(self, options, params=None): - """ Open the currency rate list. """ - currency_id = self.env['account.report']._get_res_id_from_line_id(params['line_id'], 'res.currency') - return { - 'type': 'ir.actions.act_window', - 'name': _('Currency Rates (%s)', self.env['res.currency'].browse(currency_id).display_name), - 'views': [(False, 'list')], - 'res_model': 'res.currency.rate', - 'context': {**self.env.context, **{'default_currency_id': currency_id, 'active_id': currency_id}}, - 'domain': [('currency_id', '=', currency_id)], - } - - def _report_custom_engine_multi_currency_revaluation_to_adjust(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._multi_currency_revaluation_get_custom_lines(options, 'to_adjust', current_groupby, next_groupby, offset=offset, limit=limit) - - def _report_custom_engine_multi_currency_revaluation_excluded(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._multi_currency_revaluation_get_custom_lines(options, 'excluded', current_groupby, next_groupby, offset=offset, limit=limit) - - def _multi_currency_revaluation_get_custom_lines(self, options, line_code, current_groupby, next_groupby, offset=0, limit=None): - def build_result_dict(report, query_res): - return { - 'balance_currency': query_res['balance_currency'] if len(query_res['currency_id']) == 1 else None, - 'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None, - 'balance_operation': query_res['balance_operation'], - 'balance_current': query_res['balance_current'], - 'adjustment': query_res['adjustment'], - 'has_sublines': query_res['aml_count'] > 0, - } - - report = self.env['account.report'].browse(options['report_id']) - report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - # No need to run any SQL if we're computing the main line: it does not display any total - if not current_groupby: - return { - 'balance_currency': None, - 'currency_id': None, - 'balance_operation': None, - 'balance_current': None, - 'adjustment': None, - 'has_sublines': False, - } - - query = "(VALUES {})".format(', '.join("(%s, %s)" for rate in options['currency_rates'])) - params = list(chain.from_iterable((cur['currency_id'], cur['rate']) for cur in options['currency_rates'].values())) - custom_currency_table_query = SQL(query, *params) - date_to = options['date']['date_to'] - select_part_not_an_exchange_move_id = SQL( - """ - NOT EXISTS ( - SELECT 1 - FROM account_partial_reconcile part_exch - WHERE part_exch.exchange_move_id = account_move_line.move_id - AND part_exch.max_date <= %s - ) - """, - date_to - ) - - query = report._get_report_query(options, 'strict_range') - tail_query = report._get_engine_query_tail(offset, limit) - full_query = SQL( - """ - WITH custom_currency_table(currency_id, rate) AS (%(custom_currency_table_query)s) - - -- Final select that gets the following lines: - -- (where there is a change in the rates of currency between the creation of the move and the full payments) - -- - Moves that don't have a payment yet at a certain date - -- - Moves that have a partial but are not fully paid at a certain date - SELECT - subquery.grouping_key, - ARRAY_AGG(DISTINCT(subquery.currency_id)) AS currency_id, - SUM(subquery.balance_currency) AS balance_currency, - SUM(subquery.balance_operation) AS balance_operation, - SUM(subquery.balance_current) AS balance_current, - SUM(subquery.adjustment) AS adjustment, - COUNT(subquery.aml_id) AS aml_count - FROM ( - -- Get moves that have at least one partial at a certain date and are not fully paid at that date - SELECT - """ + (f"account_move_line.{current_groupby} AS grouping_key," if current_groupby else '') + f""" - ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) AS balance_operation, - ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) AS balance_currency, - ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate AS balance_current, - ( - -- adjustment is computed as: balance_current - balance_operation - ROUND( account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate - - ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) - ) AS adjustment, - account_move_line.currency_id AS currency_id, - account_move_line.id AS aml_id - FROM %(table_references)s, - account_account AS account, - res_currency AS aml_currency, - res_currency AS aml_comp_currency, - custom_currency_table, - - -- Get for each move line the amount residual and amount_residual currency - -- both for matched "debit" and matched "credit" the same way as account.move.line - -- '_compute_amount_residual()' method does - -- (using LATERAL greatly reduce the number of lines for which we have to compute it) - LATERAL ( - -- Get sum of matched "debit" amount and amount in currency for related move line at date - SELECT COALESCE(SUM(part.amount), 0.0) AS amount_debit, - ROUND( - SUM(part.debit_amount_currency), - curr.decimal_places - ) AS amount_debit_currency, - 0.0 AS amount_credit, - 0.0 AS amount_credit_currency, - account_move_line.currency_id AS currency_id, - account_move_line.id AS aml_id - FROM account_partial_reconcile part - JOIN res_currency curr ON curr.id = part.debit_currency_id - WHERE account_move_line.id = part.debit_move_id - AND part.max_date <= %(date_to)s - GROUP BY aml_id, - curr.decimal_places - UNION - -- Get sum of matched "credit" amount and amount in currency for related move line at date - SELECT 0.0 AS amount_debit, - 0.0 AS amount_debit_currency, - COALESCE(SUM(part.amount), 0.0) AS amount_credit, - ROUND( - SUM(part.credit_amount_currency), - curr.decimal_places - ) AS amount_credit_currency, - account_move_line.currency_id AS currency_id, - account_move_line.id AS aml_id - FROM account_partial_reconcile part - JOIN res_currency curr ON curr.id = part.credit_currency_id - WHERE account_move_line.id = part.credit_move_id - AND part.max_date <= %(date_to)s - GROUP BY aml_id, - curr.decimal_places - ) AS ara - WHERE %(search_condition)s - AND account_move_line.account_id = account.id - AND account_move_line.currency_id = aml_currency.id - AND account_move_line.company_currency_id = aml_comp_currency.id - AND account_move_line.currency_id = custom_currency_table.currency_id - AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance') - AND ( - account.currency_id != account_move_line.company_currency_id - OR ( - account.account_type IN ('asset_receivable', 'liability_payable') - AND (account_move_line.currency_id != account_move_line.company_currency_id) - ) - ) - AND {'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'} ( - SELECT 1 - FROM account_account_exclude_res_currency_provision - WHERE account_account_id = account_move_line.account_id - AND res_currency_id = account_move_line.currency_id - ) - AND (%(select_part_not_an_exchange_move_id)s) - GROUP BY account_move_line.id, aml_comp_currency.decimal_places, aml_currency.decimal_places, custom_currency_table.rate - HAVING ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) != 0 - OR ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) != 0.0 - - UNION - -- Moves that don't have a payment yet at a certain date - SELECT - """ + (f"account_move_line.{current_groupby} AS grouping_key," if current_groupby else '') + f""" - account_move_line.balance AS balance_operation, - account_move_line.amount_currency AS balance_currency, - account_move_line.amount_currency / custom_currency_table.rate AS balance_current, - account_move_line.amount_currency / custom_currency_table.rate - account_move_line.balance AS adjustment, - account_move_line.currency_id AS currency_id, - account_move_line.id AS aml_id - FROM %(table_references)s - JOIN account_account account ON account_move_line.account_id = account.id - JOIN custom_currency_table ON custom_currency_table.currency_id = account_move_line.currency_id - WHERE %(search_condition)s - AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance') - AND ( - account.currency_id != account_move_line.company_currency_id - OR ( - account.account_type IN ('asset_receivable', 'liability_payable') - AND (account_move_line.currency_id != account_move_line.company_currency_id) - ) - ) - AND {'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'} ( - SELECT 1 - FROM account_account_exclude_res_currency_provision - WHERE account_account_id = account_id - AND res_currency_id = account_move_line.currency_id - ) - AND (%(select_part_not_an_exchange_move_id)s) - AND NOT EXISTS ( - SELECT 1 FROM account_partial_reconcile part - WHERE (part.debit_move_id = account_move_line.id OR part.credit_move_id = account_move_line.id) - AND part.max_date <= %(date_to)s - ) - AND (account_move_line.balance != 0.0 OR account_move_line.amount_currency != 0.0) - - ) subquery - - GROUP BY grouping_key - ORDER BY grouping_key - %(tail_query)s - """, - custom_currency_table_query=custom_currency_table_query, - table_references=query.from_clause, - date_to=date_to, - tail_query=tail_query, - search_condition=query.where_clause, - select_part_not_an_exchange_move_id=select_part_not_an_exchange_move_id, - ) - self._cr.execute(full_query) - query_res_lines = self._cr.dictfetchall() - - if not current_groupby: - return build_result_dict(report, query_res_lines and query_res_lines[0] or {}) - else: - rslt = [] - for query_res in query_res_lines: - grouping_key = query_res['grouping_key'] - rslt.append((grouping_key, build_result_dict(report, query_res))) - return rslt diff --git a/addons/at_accounting/models/account_partner_ledger.py b/addons/at_accounting/models/account_partner_ledger.py deleted file mode 100644 index c133cfb..0000000 --- a/addons/at_accounting/models/account_partner_ledger.py +++ /dev/null @@ -1,771 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models, _, fields -from odoo.exceptions import UserError -from odoo.osv import expression -from odoo.tools import SQL - -from datetime import timedelta -from collections import defaultdict - - -class PartnerLedgerCustomHandler(models.AbstractModel): - _name = 'account.partner.ledger.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Partner Ledger Custom Handler' - - def _get_custom_display_config(self): - return { - 'css_custom_class': 'partner_ledger', - 'components': { - 'AccountReportLineCell': 'at_accounting.PartnerLedgerLineCell', - }, - 'templates': { - 'AccountReportFilters': 'at_accounting.PartnerLedgerFilters', - 'AccountReportLineName': 'at_accounting.PartnerLedgerLineName', - }, - } - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - partner_lines, totals_by_column_group = self._build_partner_lines(report, options) - lines = report._regroup_lines_by_name_prefix(options, partner_lines, '_report_expand_unfoldable_line_partner_ledger_prefix_group', 0) - - # Inject sequence on dynamic lines - lines = [(0, line) for line in lines] - - # Report total line. - lines.append((0, self._get_report_line_total(options, totals_by_column_group))) - - return lines - - def _build_partner_lines(self, report, options, level_shift=0): - lines = [] - - totals_by_column_group = { - column_group_key: { - total: 0.0 - for total in ['debit', 'credit', 'amount', 'balance'] - } - for column_group_key in options['column_groups'] - } - - partners_results = self._query_partners(options) - - search_filter = options.get('filter_search_bar', '') - accept_unknown_in_filter = search_filter.lower() in self._get_no_partner_line_label().lower() - for partner, results in partners_results: - if options['export_mode'] == 'print' and search_filter and not partner and not accept_unknown_in_filter: - # When printing and searching for a specific partner, make it so we only show its lines, not the 'Unknown Partner' one, that would be - # shown in case a misc entry with no partner was reconciled with one of the target partner's entries. - continue - - partner_values = defaultdict(dict) - for column_group_key in options['column_groups']: - partner_sum = results.get(column_group_key, {}) - - partner_values[column_group_key]['debit'] = partner_sum.get('debit', 0.0) - partner_values[column_group_key]['credit'] = partner_sum.get('credit', 0.0) - partner_values[column_group_key]['amount'] = partner_sum.get('amount', 0.0) - partner_values[column_group_key]['balance'] = partner_sum.get('balance', 0.0) - - totals_by_column_group[column_group_key]['debit'] += partner_values[column_group_key]['debit'] - totals_by_column_group[column_group_key]['credit'] += partner_values[column_group_key]['credit'] - totals_by_column_group[column_group_key]['amount'] += partner_values[column_group_key]['amount'] - totals_by_column_group[column_group_key]['balance'] += partner_values[column_group_key]['balance'] - - lines.append(self._get_report_line_partners(options, partner, partner_values, level_shift=level_shift)) - - return lines, totals_by_column_group - - def _report_expand_unfoldable_line_partner_ledger_prefix_group(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None): - report = self.env['account.report'].browse(options['report_id']) - matched_prefix = report._get_prefix_groups_matched_prefix_from_line_id(line_dict_id) - - prefix_domain = [('partner_id.name', '=ilike', f'{matched_prefix}%')] - if self._get_no_partner_line_label().upper().startswith(matched_prefix): - prefix_domain = expression.OR([prefix_domain, [('partner_id', '=', None)]]) - - expand_options = { - **options, - 'forced_domain': options.get('forced_domain', []) + prefix_domain - } - parent_level = len(matched_prefix) * 2 - partner_lines, dummy = self._build_partner_lines(report, expand_options, level_shift=parent_level) - - for partner_line in partner_lines: - partner_line['id'] = report._build_subline_id(line_dict_id, partner_line['id']) - partner_line['parent_id'] = line_dict_id - - lines = report._regroup_lines_by_name_prefix( - options, - partner_lines, - '_report_expand_unfoldable_line_partner_ledger_prefix_group', - parent_level, - matched_prefix=matched_prefix, - parent_line_dict_id=line_dict_id, - ) - - return { - 'lines': lines, - 'offset_increment': len(lines), - 'has_more': False, - } - - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - domain = [] - - company_ids = report.get_report_company_ids(options) - exch_code = self.env['res.company'].browse(company_ids).mapped('currency_exchange_journal_id') - if exch_code: - domain += ['!', '&', '&', '&', ('credit', '=', 0.0), ('debit', '=', 0.0), ('amount_currency', '!=', 0.0), ('journal_id', 'in', exch_code.ids)] - - if options['export_mode'] == 'print' and options.get('filter_search_bar'): - domain += [ - '|', ('matched_debit_ids.debit_move_id.partner_id.name', 'ilike', options['filter_search_bar']), - '|', ('matched_credit_ids.credit_move_id.partner_id.name', 'ilike', options['filter_search_bar']), - ('partner_id.name', 'ilike', options['filter_search_bar']), - ] - - options['forced_domain'] = options.get('forced_domain', []) + domain - - if self.env.user.has_group('base.group_multi_currency'): - options['multi_currency'] = True - - columns_to_hide = [] - options['hide_account'] = (previous_options or {}).get('hide_account', False) - columns_to_hide += ['journal_code', 'account_code', 'matching_number'] if options['hide_account'] else [] - - options['hide_debit_credit'] = (previous_options or {}).get('hide_debit_credit', False) - columns_to_hide += ['debit', 'credit'] if options['hide_debit_credit'] else ['amount'] - - options['columns'] = [col for col in options['columns'] if col['expression_label'] not in columns_to_hide] - - options['buttons'].append({ - 'name': _('Send'), - 'action': 'action_send_statements', - 'sequence': 90, - 'always_show': True, - }) - - def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function): - partner_ids_to_expand = [] - - # Regular case - for line_dict in lines_to_expand_by_function.get('_report_expand_unfoldable_line_partner_ledger', []): - markup, model, model_id = self.env['account.report']._parse_line_id(line_dict['id'])[-1] - if model == 'res.partner': - partner_ids_to_expand.append(model_id) - elif markup == 'no_partner': - partner_ids_to_expand.append(None) - - # In case prefix groups are used - no_partner_line_label = self._get_no_partner_line_label().upper() - partner_prefix_domains = [] - for line_dict in lines_to_expand_by_function.get('_report_expand_unfoldable_line_partner_ledger_prefix_group', []): - prefix = report._get_prefix_groups_matched_prefix_from_line_id(line_dict['id']) - partner_prefix_domains.append([('name', '=ilike', f'{prefix}%')]) - - # amls without partners are regrouped "Unknown Partner", which is also used to create prefix groups - if no_partner_line_label.startswith(prefix): - partner_ids_to_expand.append(None) - - if partner_prefix_domains: - partner_ids_to_expand += self.env['res.partner'].with_context(active_test=False).search(expression.OR(partner_prefix_domains)).ids - - return { - 'initial_balances': self._get_initial_balance_values(partner_ids_to_expand, options) if partner_ids_to_expand else {}, - - # load_more_limit cannot be passed to this call, otherwise it won't be applied per partner but on the whole result. - # We gain perf from batching, but load every result, even if the limit restricts them later. - 'aml_values': self._get_aml_values(options, partner_ids_to_expand) if partner_ids_to_expand else {}, - } - - def _get_report_send_recipients(self, options): - partners = options.get('partner_ids', []) - if not partners: - self._cr.execute(self._get_query_sums(options)) - partners = [row['groupby'] for row in self._cr.dictfetchall() if row['groupby']] - return self.env['res.partner'].browse(partners) - - def action_send_statements(self, options): - template = self.env.ref('at_accounting.email_template_customer_statement', False) - return { - 'name': _("Send Partner Ledgers"), - 'type': 'ir.actions.act_window', - 'views': [[False, 'form']], - 'res_model': 'account.report.send', - 'target': 'new', - 'context': { - 'default_mail_template_id': template.id if template else False, - 'default_report_options': options, - }, - } - - @api.model - def action_open_partner(self, options, params): - dummy, record_id = self.env['account.report']._get_model_info_from_id(params['id']) - return { - 'type': 'ir.actions.act_window', - 'res_model': 'res.partner', - 'res_id': record_id, - 'views': [[False, 'form']], - 'view_mode': 'form', - 'target': 'current', - } - - def _query_partners(self, options): - """ Executes the queries and performs all the computation. - :return: A list of tuple (partner, column_group_values) sorted by the table's model _order: - - partner is a res.parter record. - - column_group_values is a dict(column_group_key, fetched_values), where - - column_group_key is a string identifying a column group, like in options['column_groups'] - - fetched_values is a dictionary containing: - - sum: {'debit': float, 'credit': float, 'balance': float} - - (optional) initial_balance: {'debit': float, 'credit': float, 'balance': float} - - (optional) lines: [line_vals_1, line_vals_2, ...] - """ - def assign_sum(row): - fields_to_assign = ['balance', 'debit', 'credit', 'amount'] - if any(not company_currency.is_zero(row[field]) for field in fields_to_assign): - groupby_partners.setdefault(row['groupby'], defaultdict(lambda: defaultdict(float))) - for field in fields_to_assign: - groupby_partners[row['groupby']][row['column_group_key']][field] += row[field] - - company_currency = self.env.company.currency_id - - # Execute the queries and dispatch the results. - query = self._get_query_sums(options) - - groupby_partners = {} - - self._cr.execute(query) - for res in self._cr.dictfetchall(): - assign_sum(res) - - # Correct the sums per partner, for the lines without partner reconciled with a line having a partner - query = self._get_sums_without_partner(options) - - self._cr.execute(query) - totals = {} - for total_field in ['debit', 'credit', 'amount', 'balance']: - totals[total_field] = {col_group_key: 0 for col_group_key in options['column_groups']} - - for row in self._cr.dictfetchall(): - totals['debit'][row['column_group_key']] += row['debit'] - totals['credit'][row['column_group_key']] += row['credit'] - totals['amount'][row['column_group_key']] += row['amount'] - totals['balance'][row['column_group_key']] += row['balance'] - - if row['groupby'] not in groupby_partners: - continue - - assign_sum(row) - - if None in groupby_partners: - # Debit/credit are inverted for the unknown partner as the computation is made regarding the balance of the known partner - for column_group_key in options['column_groups']: - groupby_partners[None][column_group_key]['debit'] += totals['credit'][column_group_key] - groupby_partners[None][column_group_key]['credit'] += totals['debit'][column_group_key] - groupby_partners[None][column_group_key]['amount'] += totals['amount'][column_group_key] - groupby_partners[None][column_group_key]['balance'] -= totals['balance'][column_group_key] - - # Retrieve the partners to browse. - # groupby_partners.keys() contains all account ids affected by: - # - the amls in the current period. - # - the amls affecting the initial balance. - if groupby_partners: - # Note a search is done instead of a browse to preserve the table ordering. - partners = self.env['res.partner'].with_context(active_test=False).search_fetch([('id', 'in', list(groupby_partners.keys()))], ["id", "name", "trust", "company_registry", "vat"]) - else: - partners = [] - - # Add 'Partner Unknown' if needed - if None in groupby_partners.keys(): - partners = [p for p in partners] + [None] - - return [(partner, groupby_partners[partner.id if partner else None]) for partner in partners] - - def _get_query_sums(self, options) -> SQL: - """ Construct a query retrieving all the aggregated sums to build the report. It includes: - - sums for all partners. - - sums for the initial balances. - :param options: The report options. - :return: query as SQL object - """ - queries = [] - report = self.env.ref('at_accounting.partner_ledger_report') - - # Create the currency table. - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - query = report._get_report_query(column_group_options, 'from_beginning') - queries.append(SQL( - """ - SELECT - account_move_line.partner_id AS groupby, - %(column_group_key)s AS column_group_key, - SUM(%(debit_select)s) AS debit, - SUM(%(credit_select)s) AS credit, - SUM(%(balance_select)s) AS amount, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - GROUP BY account_move_line.partner_id - """, - column_group_key=column_group_key, - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(column_group_options), - search_condition=query.where_clause, - )) - - return SQL(' UNION ALL ').join(queries) - - def _get_initial_balance_values(self, partner_ids, options): - queries = [] - report = self.env.ref('at_accounting.partner_ledger_report') - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - # Get sums for the initial balance. - # period: [('date' <= options['date_from'] - 1)] - new_options = self._get_options_initial_balance(column_group_options) - query = report._get_report_query(new_options, 'from_beginning', domain=[('partner_id', 'in', partner_ids)]) - queries.append(SQL( - """ - SELECT - account_move_line.partner_id, - %(column_group_key)s AS column_group_key, - SUM(%(debit_select)s) AS debit, - SUM(%(credit_select)s) AS credit, - SUM(%(balance_select)s) AS amount, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - GROUP BY account_move_line.partner_id - """, - column_group_key=column_group_key, - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(column_group_options), - search_condition=query.where_clause, - )) - - self._cr.execute(SQL(" UNION ALL ").join(queries)) - - init_balance_by_col_group = { - partner_id: {column_group_key: {} for column_group_key in options['column_groups']} - for partner_id in partner_ids - } - for result in self._cr.dictfetchall(): - init_balance_by_col_group[result['partner_id']][result['column_group_key']] = result - - return init_balance_by_col_group - - def _get_options_initial_balance(self, options): - """ Create options used to compute the initial balances for each partner. - The resulting dates domain will be: - [('date' <= options['date_from'] - 1)] - :param options: The report options. - :return: A copy of the options, modified to match the dates to use to get the initial balances. - """ - new_date_to = fields.Date.from_string(options['date']['date_from']) - timedelta(days=1) - new_date_options = dict(options['date'], date_from=False, date_to=fields.Date.to_string(new_date_to)) - return dict(options, date=new_date_options) - - def _get_sums_without_partner(self, options): - """ Get the sum of lines without partner reconciled with a line with a partner, grouped by partner. Those lines - should be considered as belonging to the partner for the reconciled amount as it may clear some of the partner - invoice/bill and they have to be accounted in the partner balance.""" - queries = [] - report = self.env.ref('at_accounting.partner_ledger_report') - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - query = report._get_report_query(column_group_options, 'from_beginning') - queries.append(SQL( - """ - SELECT - %(column_group_key)s AS column_group_key, - aml_with_partner.partner_id AS groupby, - SUM(%(debit_select)s) AS debit, - SUM(%(credit_select)s) AS credit, - SUM(%(balance_select)s) AS amount, - SUM(%(balance_select)s) AS balance - FROM %(table_references)s - JOIN account_partial_reconcile partial - ON account_move_line.id = partial.debit_move_id OR account_move_line.id = partial.credit_move_id - JOIN account_move_line aml_with_partner ON - (aml_with_partner.id = partial.debit_move_id OR aml_with_partner.id = partial.credit_move_id) - AND aml_with_partner.partner_id IS NOT NULL - %(currency_table_join)s - WHERE partial.max_date <= %(date_to)s AND %(search_condition)s - AND account_move_line.partner_id IS NULL - GROUP BY aml_with_partner.partner_id - """, - column_group_key=column_group_key, - debit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance > 0 THEN 0 ELSE partial.amount END")), - credit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance < 0 THEN 0 ELSE partial.amount END")), - balance_select=report._currency_table_apply_rate(SQL("-SIGN(aml_with_partner.balance) * partial.amount")), - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(column_group_options, aml_alias=SQL("aml_with_partner")), - date_to=column_group_options['date']['date_to'], - search_condition=query.where_clause, - )) - - return SQL(" UNION ALL ").join(queries) - - def _report_expand_unfoldable_line_partner_ledger(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None): - def init_load_more_progress(line_dict): - return { - column['column_group_key']: line_col.get('no_format', 0) - for column, line_col in zip(options['columns'], line_dict['columns']) - if column['expression_label'] == 'balance' - } - - report = self.env.ref('at_accounting.partner_ledger_report') - markup, model, record_id = report._parse_line_id(line_dict_id)[-1] - - if model != 'res.partner': - raise UserError(_("Wrong ID for partner ledger line to expand: %s", line_dict_id)) - - prefix_groups_count = 0 - for markup, dummy1, dummy2 in report._parse_line_id(line_dict_id): - if isinstance(markup, dict) and 'groupby_prefix_group' in markup: - prefix_groups_count += 1 - level_shift = prefix_groups_count * 2 - - lines = [] - - # Get initial balance - if offset == 0: - if unfold_all_batch_data: - init_balance_by_col_group = unfold_all_batch_data['initial_balances'][record_id] - else: - init_balance_by_col_group = self._get_initial_balance_values([record_id], options)[record_id] - initial_balance_line = report._get_partner_and_general_ledger_initial_balance_line(options, line_dict_id, init_balance_by_col_group, level_shift=level_shift) - if initial_balance_line: - lines.append(initial_balance_line) - - # For the first expansion of the line, the initial balance line gives the progress - progress = init_load_more_progress(initial_balance_line) - - limit_to_load = report.load_more_limit + 1 if report.load_more_limit and options['export_mode'] != 'print' else None - - if unfold_all_batch_data: - aml_results = unfold_all_batch_data['aml_values'][record_id] - else: - aml_results = self._get_aml_values(options, [record_id], offset=offset, limit=limit_to_load)[record_id] - - has_more = False - treated_results_count = 0 - next_progress = progress - for result in aml_results: - if options['export_mode'] != 'print' and report.load_more_limit and treated_results_count == report.load_more_limit: - # We loaded one more than the limit on purpose: this way we know we need a "load more" line - has_more = True - break - - new_line = self._get_report_line_move_line(options, result, line_dict_id, next_progress, level_shift=level_shift) - lines.append(new_line) - next_progress = init_load_more_progress(new_line) - treated_results_count += 1 - - return { - 'lines': lines, - 'offset_increment': treated_results_count, - 'has_more': has_more, - 'progress': next_progress - } - - def _get_additional_column_aml_values(self): - """ - Allows customization of additional fields in the partner ledger query. - - This method is intended to be overridden by other modules to add custom fields - to the partner ledger query, e.g. SQL("account_move_line.date AS date,"). - - By default, it returns an empty SQL object. - """ - return SQL() - - def _get_aml_values(self, options, partner_ids, offset=0, limit=None): - rslt = {partner_id: [] for partner_id in partner_ids} - - partner_ids_wo_none = [x for x in partner_ids if x] - directly_linked_aml_partner_clauses = [] - indirectly_linked_aml_partner_clause = SQL('aml_with_partner.partner_id IS NOT NULL') - if None in partner_ids: - directly_linked_aml_partner_clauses.append(SQL('account_move_line.partner_id IS NULL')) - if partner_ids_wo_none: - directly_linked_aml_partner_clauses.append(SQL('account_move_line.partner_id IN %s', tuple(partner_ids_wo_none))) - indirectly_linked_aml_partner_clause = SQL('aml_with_partner.partner_id IN %s', tuple(partner_ids_wo_none)) - directly_linked_aml_partner_clause = SQL('(%s)', SQL(' OR ').join(directly_linked_aml_partner_clauses)) - - queries = [] - journal_name = self.env['account.journal']._field_to_sql('journal', 'name') - report = self.env.ref('at_accounting.partner_ledger_report') - additional_columns = self._get_additional_column_aml_values() - for column_group_key, group_options in report._split_options_per_column_group(options).items(): - query = report._get_report_query(group_options, 'strict_range') - account_alias = query.left_join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') - account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) - account_name = self.env['account.account']._field_to_sql(account_alias, 'name') - - # For the move lines directly linked to this partner - # ruff: noqa: FURB113 - queries.append(SQL( - ''' - SELECT - account_move_line.id, - account_move_line.date_maturity, - account_move_line.name, - account_move_line.ref, - account_move_line.company_id, - account_move_line.account_id, - account_move_line.payment_id, - account_move_line.partner_id, - account_move_line.currency_id, - account_move_line.amount_currency, - account_move_line.matching_number, - %(additional_columns)s - COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date, - %(debit_select)s AS debit, - %(credit_select)s AS credit, - %(balance_select)s AS amount, - %(balance_select)s AS balance, - account_move.name AS move_name, - account_move.move_type AS move_type, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - journal.code AS journal_code, - %(journal_name)s AS journal_name, - %(column_group_key)s AS column_group_key, - 'directly_linked_aml' AS key, - 0 AS partial_id - FROM %(table_references)s - JOIN account_move ON account_move.id = account_move_line.move_id - %(currency_table_join)s - LEFT JOIN res_company company ON company.id = account_move_line.company_id - LEFT JOIN res_partner partner ON partner.id = account_move_line.partner_id - LEFT JOIN account_journal journal ON journal.id = account_move_line.journal_id - WHERE %(search_condition)s AND %(directly_linked_aml_partner_clause)s - ORDER BY account_move_line.date, account_move_line.id - ''', - additional_columns=additional_columns, - debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")), - credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")), - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - account_code=account_code, - account_name=account_name, - journal_name=journal_name, - column_group_key=column_group_key, - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(group_options), - search_condition=query.where_clause, - directly_linked_aml_partner_clause=directly_linked_aml_partner_clause, - )) - - # For the move lines linked to no partner, but reconciled with this partner. They will appear in grey in the report - queries.append(SQL( - ''' - SELECT - account_move_line.id, - account_move_line.date_maturity, - account_move_line.name, - account_move_line.ref, - account_move_line.company_id, - account_move_line.account_id, - account_move_line.payment_id, - aml_with_partner.partner_id, - account_move_line.currency_id, - account_move_line.amount_currency, - account_move_line.matching_number, - %(additional_columns)s - COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date, - %(debit_select)s AS debit, - %(credit_select)s AS credit, - %(balance_select)s AS amount, - %(balance_select)s AS balance, - account_move.name AS move_name, - account_move.move_type AS move_type, - %(account_code)s AS account_code, - %(account_name)s AS account_name, - journal.code AS journal_code, - %(journal_name)s AS journal_name, - %(column_group_key)s AS column_group_key, - 'indirectly_linked_aml' AS key, - partial.id AS partial_id - FROM %(table_references)s - %(currency_table_join)s, - account_partial_reconcile partial, - account_move, - account_move_line aml_with_partner, - account_journal journal - WHERE - (account_move_line.id = partial.debit_move_id OR account_move_line.id = partial.credit_move_id) - AND account_move_line.partner_id IS NULL - AND account_move.id = account_move_line.move_id - AND (aml_with_partner.id = partial.debit_move_id OR aml_with_partner.id = partial.credit_move_id) - AND %(indirectly_linked_aml_partner_clause)s - AND journal.id = account_move_line.journal_id - AND %(account_alias)s.id = account_move_line.account_id - AND %(search_condition)s - AND partial.max_date BETWEEN %(date_from)s AND %(date_to)s - ORDER BY account_move_line.date, account_move_line.id - ''', - additional_columns=additional_columns, - debit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance > 0 THEN 0 ELSE partial.amount END")), - credit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance < 0 THEN 0 ELSE partial.amount END")), - balance_select=report._currency_table_apply_rate(SQL("-SIGN(aml_with_partner.balance) * partial.amount")), - account_code=account_code, - account_name=account_name, - journal_name=journal_name, - column_group_key=column_group_key, - table_references=query.from_clause, - currency_table_join=report._currency_table_aml_join(group_options), - indirectly_linked_aml_partner_clause=indirectly_linked_aml_partner_clause, - account_alias=SQL.identifier(account_alias), - search_condition=query.where_clause, - date_from=group_options['date']['date_from'], - date_to=group_options['date']['date_to'], - )) - - query = SQL(" UNION ALL ").join(SQL("(%s)", query) for query in queries) - - if offset: - query = SQL('%s OFFSET %s ', query, offset) - - if limit: - query = SQL('%s LIMIT %s ', query, limit) - - self._cr.execute(query) - for aml_result in self._cr.dictfetchall(): - if aml_result['key'] == 'indirectly_linked_aml': - - # Append the line to the partner found through the reconciliation. - if aml_result['partner_id'] in rslt: - rslt[aml_result['partner_id']].append(aml_result) - - # Balance it with an additional line in the Unknown Partner section but having reversed amounts. - if None in rslt: - rslt[None].append({ - **aml_result, - 'debit': aml_result['credit'], - 'credit': aml_result['debit'], - 'amount': aml_result['credit'] - aml_result['debit'], - 'balance': -aml_result['balance'], - }) - else: - rslt[aml_result['partner_id']].append(aml_result) - - return rslt - - #################################################### - # COLUMNS/LINES - #################################################### - def _get_report_line_partners(self, options, partner, partner_values, level_shift=0): - company_currency = self.env.company.currency_id - - partner_data = next(iter(partner_values.values())) - unfoldable = not company_currency.is_zero(partner_data.get('debit', 0) or partner_data.get('credit', 0)) - column_values = [] - report = self.env['account.report'].browse(options['report_id']) - for column in options['columns']: - col_expr_label = column['expression_label'] - value = partner_values[column['column_group_key']].get(col_expr_label) - unfoldable = unfoldable or (col_expr_label in ('debit', 'credit', 'amount') and not company_currency.is_zero(value)) - column_values.append(report._build_column_dict(value, column, options=options)) - - - line_id = report._get_generic_line_id('res.partner', partner.id) if partner else report._get_generic_line_id('res.partner', None, markup='no_partner') - - return { - 'id': line_id, - 'name': partner is not None and (partner.name or '')[:128] or self._get_no_partner_line_label(), - 'columns': column_values, - 'level': 1 + level_shift, - 'trust': partner.trust if partner else None, - 'unfoldable': unfoldable, - 'unfolded': line_id in options['unfolded_lines'] or options['unfold_all'], - 'expand_function': '_report_expand_unfoldable_line_partner_ledger', - } - - def _get_no_partner_line_label(self): - return _('Unknown Partner') - - @api.model - def _format_aml_name(self, line_name, move_ref, move_name=None): - ''' Format the display of an account.move.line record. As its very costly to fetch the account.move.line - records, only line_name, move_ref, move_name are passed as parameters to deal with sql-queries more easily. - - :param line_name: The name of the account.move.line record. - :param move_ref: The reference of the account.move record. - :param move_name: The name of the account.move record. - :return: The formatted name of the account.move.line record. - ''' - return self.env['account.move.line']._format_aml_name(line_name, move_ref, move_name=move_name) - - def _get_report_line_move_line(self, options, aml_query_result, partner_line_id, init_bal_by_col_group, level_shift=0): - if aml_query_result['payment_id']: - caret_type = 'account.payment' - else: - caret_type = 'account.move.line' - - columns = [] - report = self.env['account.report'].browse(options['report_id']) - for column in options['columns']: - col_expr_label = column['expression_label'] - - if col_expr_label not in aml_query_result: - raise UserError(_("The column '%s' is not available for this report.", col_expr_label)) - - col_value = aml_query_result[col_expr_label] if column['column_group_key'] == aml_query_result['column_group_key'] else None - - if col_value is None: - columns.append(report._build_column_dict(None, None)) - else: - currency = False - - if col_expr_label == 'balance': - col_value += init_bal_by_col_group[column['column_group_key']] - - if col_expr_label == 'amount_currency': - currency = self.env['res.currency'].browse(aml_query_result['currency_id']) - - if currency == self.env.company.currency_id: - col_value = '' - - columns.append(report._build_column_dict(col_value, column, options=options, currency=currency)) - - return { - 'id': report._get_generic_line_id('account.move.line', aml_query_result['id'], parent_line_id=partner_line_id, markup=aml_query_result['partial_id']), - 'parent_id': partner_line_id, - 'name': self._format_aml_name(aml_query_result['name'], aml_query_result['ref'], aml_query_result['move_name']), - 'columns': columns, - 'caret_options': caret_type, - 'level': 3 + level_shift, - } - - def _get_report_line_total(self, options, totals_by_column_group): - column_values = [] - report = self.env['account.report'].browse(options['report_id']) - for column in options['columns']: - col_value = totals_by_column_group[column['column_group_key']].get(column['expression_label']) - column_values.append(report._build_column_dict(col_value, column, options=options)) - - return { - 'id': report._get_generic_line_id(None, None, markup='total'), - 'name': _('Total'), - 'level': 1, - 'columns': column_values, - } - - def open_journal_items(self, options, params): - params['view_ref'] = 'account.view_move_line_tree_grouped_partner' - report = self.env['account.report'].browse(options['report_id']) - action = report.open_journal_items(options=options, params=params) - action.get('context', {}).update({'search_default_group_by_account': 0}) - return action diff --git a/addons/at_accounting/models/account_payment.py b/addons/at_accounting/models/account_payment.py deleted file mode 100644 index 235cf20..0000000 --- a/addons/at_accounting/models/account_payment.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -import ast -from odoo import models, _ - - -class AccountPayment(models.Model): - _inherit = "account.payment" - - def action_open_manual_reconciliation_widget(self): - ''' Open the manual reconciliation widget for the current payment. - :return: A dictionary representing an action. - ''' - self.ensure_one() - action_values = self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_move_line_posted_unreconciled') - if self.partner_id: - context = ast.literal_eval(action_values['context']) - context.update({'search_default_partner_id': self.partner_id.id}) - if self.partner_type == 'customer': - context.update({'search_default_trade_receivable': 1}) - elif self.partner_type == 'supplier': - context.update({'search_default_trade_payable': 1}) - action_values['context'] = context - return action_values - - def button_open_statement_lines(self): - # OVERRIDE - """ Redirect the user to the statement line(s) reconciled to this payment. - :return: An action to open the view of the payment in the reconciliation widget. - """ - self.ensure_one() - - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - extra_domain=[('id', 'in', self.reconciled_statement_line_ids.ids)], - default_context={ - 'create': False, - 'default_st_line_id': self.reconciled_statement_line_ids.ids[-1], - }, - name=_("Matched Transactions") - ) diff --git a/addons/at_accounting/models/account_reconcile_model.py b/addons/at_accounting/models/account_reconcile_model.py deleted file mode 100644 index 91e8a89..0000000 --- a/addons/at_accounting/models/account_reconcile_model.py +++ /dev/null @@ -1,557 +0,0 @@ -from odoo import fields, models, Command, tools -from odoo.tools import SQL - -import re -from collections import defaultdict -from dateutil.relativedelta import relativedelta - - -class AccountReconcileModel(models.Model): - _inherit = 'account.reconcile.model' - - #################################################### - # RECONCILIATION PROCESS - #################################################### - - def _apply_lines_for_bank_widget(self, residual_amount_currency, partner, st_line): - """ Apply the reconciliation model lines to the statement line passed as parameter. - - :param residual_amount_currency: The open balance of the statement line in the bank reconciliation widget - expressed in the statement line currency. - :param partner: The partner set on the wizard. - :param st_line: The statement line processed by the bank reconciliation widget. - :return: A list of python dictionaries (one per reconcile model line) representing - the journal items to be created by the current reconcile model. - """ - self.ensure_one() - currency = st_line.foreign_currency_id or st_line.journal_id.currency_id or st_line.company_currency_id - vals_list = [] - for line in self.line_ids: - vals = line._apply_in_bank_widget(residual_amount_currency, partner, st_line) - amount_currency = vals['amount_currency'] - - if currency.is_zero(amount_currency): - continue - - vals_list.append(vals) - residual_amount_currency -= amount_currency - - return vals_list - - #################################################### - # RECONCILIATION CRITERIA - #################################################### - - def _apply_rules(self, st_line, partner): - available_models = self.filtered(lambda m: m.rule_type != 'writeoff_button').sorted() - - for rec_model in available_models: - - if not rec_model._is_applicable_for(st_line, partner): - continue - - if rec_model.rule_type == 'invoice_matching': - rules_map = rec_model._get_invoice_matching_rules_map() - for rule_index in sorted(rules_map.keys()): - for rule_method in rules_map[rule_index]: - candidate_vals = rule_method(st_line, partner) - if not candidate_vals: - continue - - if candidate_vals.get('amls'): - res = rec_model._get_invoice_matching_amls_result(st_line, partner, candidate_vals) - if res: - return { - **res, - 'model': rec_model, - } - else: - return { - **candidate_vals, - 'model': rec_model, - } - - elif rec_model.rule_type == 'writeoff_suggestion': - return { - 'model': rec_model, - 'status': 'write_off', - 'auto_reconcile': rec_model.auto_reconcile, - } - return {} - - def _is_applicable_for(self, st_line, partner): - """ Returns true iff this reconciliation model can be used to search for matches - for the provided statement line and partner. - """ - self.ensure_one() - - # Filter on journals, amount nature, amount and partners - # All the conditions defined in this block are non-match conditions. - if ((self.match_journal_ids and st_line.move_id.journal_id not in self.match_journal_ids) - or (self.match_nature == 'amount_received' and st_line.amount < 0) - or (self.match_nature == 'amount_paid' and st_line.amount > 0) - or (self.match_amount == 'lower' and abs(st_line.amount) >= self.match_amount_max) - or (self.match_amount == 'greater' and abs(st_line.amount) <= self.match_amount_min) - or (self.match_amount == 'between' and (abs(st_line.amount) > self.match_amount_max or abs(st_line.amount) < self.match_amount_min)) - or (self.match_partner and not partner) - or (self.match_partner and self.match_partner_ids and partner not in self.match_partner_ids) - or (self.match_partner and self.match_partner_category_ids and not (partner.category_id & self.match_partner_category_ids)) - ): - return False - - # Filter on label, note and transaction_type - for record, rule_field, record_field in [(st_line, 'label', 'payment_ref'), (st_line.move_id, 'note', 'narration'), (st_line, 'transaction_type', 'transaction_type')]: - rule_term = (self['match_' + rule_field + '_param'] or '').lower() - record_term = (record[record_field] or '').lower() - - # This defines non-match conditions - if ((self['match_' + rule_field] == 'contains' and rule_term not in record_term) - or (self['match_' + rule_field] == 'not_contains' and rule_term in record_term) - or (self['match_' + rule_field] == 'match_regex' and not re.match(rule_term, record_term)) - ): - return False - - return True - - def _get_invoice_matching_amls_domain(self, st_line, partner): - aml_domain = st_line._get_default_amls_matching_domain() - - if st_line.amount > 0.0: - aml_domain.append(('balance', '>', 0.0)) - else: - aml_domain.append(('balance', '<', 0.0)) - - currency = st_line.foreign_currency_id or st_line.currency_id - if self.match_same_currency: - aml_domain.append(('currency_id', '=', currency.id)) - - if partner: - aml_domain.append(('partner_id', '=', partner.id)) - - if self.past_months_limit: - date_limit = fields.Date.context_today(self) - relativedelta(months=self.past_months_limit) - aml_domain.append(('date', '>=', fields.Date.to_string(date_limit))) - - return aml_domain - - def _get_st_line_text_values_for_matching(self, st_line): - """ Collect the strings that could be used on the statement line to perform some matching. - :param st_line: The current statement line. - :return: A list of strings. - """ - self.ensure_one() - allowed_fields = [] - if self.match_text_location_label: - allowed_fields.append('payment_ref') - if self.match_text_location_note: - allowed_fields.append('narration') - if self.match_text_location_reference: - allowed_fields.append('ref') - return st_line._get_st_line_strings_for_matching(allowed_fields=allowed_fields) - - def _get_invoice_matching_st_line_tokens(self, st_line): - """ Parse the textual information from the statement line passed as parameter - in order to extract from it the meaningful information in order to perform the matching. - - :param st_line: A statement line. - :return: A tuple of list of tokens, each one being a string. - The first element is a list of tokens you may match on numerical information. - The second element is a list of tokens you may match exactly. - """ - st_line_text_values = self._get_st_line_text_values_for_matching(st_line) - significant_token_size = 4 - numerical_tokens = [] - exact_tokens = set() # preventing duplicates - text_tokens = [] - for text_value in st_line_text_values: - split_text = (text_value or '').split() - # Exact tokens - exact_tokens.add(text_value) - exact_tokens.update( - token for token in split_text - if len(token) >= significant_token_size - ) - # Text tokens - tokens = [ - ''.join(x for x in token if re.match(r'[0-9a-zA-Z\s]', x)) - for token in split_text - ] - - # Numerical tokens - for token in tokens: - # The token is too short to be significant. - if len(token) < significant_token_size: - continue - - text_tokens.append(token) - - formatted_token = ''.join(x for x in token if x.isdecimal()) - - # The token is too short after formatting to be significant. - if len(formatted_token) < significant_token_size: - continue - - numerical_tokens.append(formatted_token) - - return numerical_tokens, list(exact_tokens), text_tokens - - def _get_invoice_matching_amls_candidates(self, st_line, partner): - """ Returns the match candidates for the 'invoice_matching' rule, with respect to the provided parameters. - - :param st_line: A statement line. - :param partner: The partner associated to the statement line. - """ - def get_order_by_clause(prefix=SQL()): - direction = SQL(' DESC') if self.matching_order == 'new_first' else SQL(' ASC') - return SQL(", ").join( - SQL("%s%s%s", prefix, SQL(field), direction) - for field in ('date_maturity', 'date', 'id') - ) - - assert self.rule_type == 'invoice_matching' - self.env['account.move'].flush_model() - self.env['account.move.line'].flush_model() - - aml_domain = self._get_invoice_matching_amls_domain(st_line, partner) - query = self.env['account.move.line']._where_calc(aml_domain) - tables = query.from_clause - where_clause = query.where_clause or SQL("TRUE") - - aml_cte = SQL() - sub_queries: list[SQL] = [] - numerical_tokens, exact_tokens, _text_tokens = self._get_invoice_matching_st_line_tokens(st_line) - if numerical_tokens or exact_tokens: - aml_cte = SQL(''' - WITH aml_cte AS ( - SELECT - account_move_line.id as account_move_line_id, - account_move_line.date as account_move_line_date, - account_move_line.date_maturity as account_move_line_date_maturity, - account_move_line.name as account_move_line_name, - account_move_line__move_id.name as account_move_line__move_id_name, - account_move_line__move_id.ref as account_move_line__move_id_ref - FROM %s - JOIN account_move account_move_line__move_id ON account_move_line__move_id.id = account_move_line.move_id - WHERE %s - ) - ''', tables, where_clause) - if numerical_tokens: - for table_alias, field in ( - ('account_move_line', 'name'), - ('account_move_line__move_id', 'name'), - ('account_move_line__move_id', 'ref'), - ): - sub_queries.append(SQL(r''' - SELECT - account_move_line_id as id, - account_move_line_date as date, - account_move_line_date_maturity as date_maturity, - UNNEST( - REGEXP_SPLIT_TO_ARRAY( - SUBSTRING( - REGEXP_REPLACE(%(field)s, '[^0-9\s]', '', 'g'), - '\S(?:.*\S)*' - ), - '\s+' - ) - ) AS token - FROM aml_cte - WHERE %(field)s IS NOT NULL - ''', field=SQL("%s_%s", SQL(table_alias), SQL(field)))) - if exact_tokens: - for table_alias, field in ( - ('account_move_line', 'name'), - ('account_move_line__move_id', 'name'), - ('account_move_line__move_id', 'ref'), - ): - sub_queries.append(SQL(''' - SELECT - account_move_line_id as id, - account_move_line_date as date, - account_move_line_date_maturity as date_maturity, - %(field)s AS token - FROM aml_cte - WHERE %(field)s != '' - ''', field=SQL("%s_%s", SQL(table_alias), SQL(field)))) - if sub_queries: - order_by = get_order_by_clause(prefix=SQL('sub.')) - candidate_ids = [r[0] for r in self.env.execute_query(SQL( - ''' - %s - SELECT - sub.id, - COUNT(*) AS nb_match - FROM (%s) AS sub - WHERE sub.token IN %s - GROUP BY sub.date_maturity, sub.date, sub.id - HAVING COUNT(*) > 0 - ORDER BY nb_match DESC, %s - ''', - aml_cte, - SQL(" UNION ALL ").join(sub_queries), - tuple(numerical_tokens + exact_tokens), - order_by, - ))] - if candidate_ids: - return { - 'allow_auto_reconcile': True, - 'amls': self.env['account.move.line'].browse(candidate_ids), - } - elif self.match_text_location_label or self.match_text_location_note or self.match_text_location_reference: - # In the case any of the Label, Note or Reference matching rule has been toggled, and the query didn't return - # any candidates, the model should not try to mount another aml instead. - return - - if not partner: - st_line_currency = st_line.foreign_currency_id or st_line.journal_id.currency_id or st_line.company_currency_id - if st_line_currency == self.company_id.currency_id: - aml_amount_field = SQL('amount_residual') - else: - aml_amount_field = SQL('amount_residual_currency') - - order_by = get_order_by_clause(prefix=SQL('account_move_line.')) - rows = self.env.execute_query(SQL( - ''' - SELECT account_move_line.id - FROM %s - WHERE - %s - AND account_move_line.currency_id = %s - AND ROUND(account_move_line.%s, %s) = ROUND(%s, %s) - ORDER BY %s - ''', - tables, - where_clause, - st_line_currency.id, - aml_amount_field, - st_line_currency.decimal_places, - -st_line.amount_residual, - st_line_currency.decimal_places, - order_by, - )) - amls = self.env['account.move.line'].browse([row[0] for row in rows]) - else: - amls = self.env['account.move.line'].search(aml_domain, order=get_order_by_clause().code) - - if amls: - return { - 'allow_auto_reconcile': False, - 'amls': amls, - } - - def _get_invoice_matching_rules_map(self): - """ Get a mapping that could be overridden in others modules. - - :return: a mapping where: - * priority_order: Defines in which order the rules will be evaluated, the lowest comes first. - This is extremely important since the algorithm stops when a rule returns some candidates. - * rule: Method taking as parameters and returning the candidates journal items found. - """ - rules_map = defaultdict(list) - rules_map[10].append(self._get_invoice_matching_amls_candidates) - return rules_map - - def _get_partner_from_mapping(self, st_line): - """Find partner with mapping defined on model. - - For invoice matching rules, matches the statement line against each - regex defined in partner mapping, and returns the partner corresponding - to the first one matching. - - :param st_line (Model): - The statement line that needs a partner to be found - :return Model: - The partner found from the mapping. Can be empty an empty recordset - if there was nothing found from the mapping or if the function is - not applicable. - """ - self.ensure_one() - - if self.rule_type not in ('invoice_matching', 'writeoff_suggestion'): - return self.env['res.partner'] - - for partner_mapping in self.partner_mapping_line_ids: - match_payment_ref = True - if partner_mapping.payment_ref_regex: - match_payment_ref = re.match(partner_mapping.payment_ref_regex, st_line.payment_ref) if st_line.payment_ref else False - - match_narration = True - if partner_mapping.narration_regex: - match_narration = re.match( - partner_mapping.narration_regex, - tools.html2plaintext(st_line.narration or '').rstrip(), - flags=re.DOTALL, # Ignore '/n' set by online sync. - ) - - if match_payment_ref and match_narration: - return partner_mapping.partner_id - return self.env['res.partner'] - - def _get_invoice_matching_amls_result(self, st_line, partner, candidate_vals): - def _create_result_dict(amls_values_list, status): - if 'rejected' in status: - return - - result = {'amls': self.env['account.move.line']} - for aml_values in amls_values_list: - result['amls'] |= aml_values['aml'] - - if 'allow_write_off' in status and self.line_ids: - result['status'] = 'write_off' - - if 'allow_auto_reconcile' in status and candidate_vals['allow_auto_reconcile'] and self.auto_reconcile: - result['auto_reconcile'] = True - - return result - - st_line_currency = st_line.foreign_currency_id or st_line.currency_id - st_line_amount = st_line._prepare_move_line_default_vals()[1]['amount_currency'] - sign = 1 if st_line_amount > 0.0 else -1 - - amls = candidate_vals['amls'] - amls_values_list = [] - amls_with_epd_values_list = [] - same_currency_mode = amls.currency_id == st_line_currency - for aml in amls: - aml_values = { - 'aml': aml, - 'amount_residual': aml.amount_residual, - 'amount_residual_currency': aml.amount_residual_currency, - } - - amls_values_list.append(aml_values) - - # Manage the early payment discount. - if aml.move_id.invoice_payment_term_id: - last_discount_date = aml.move_id.invoice_payment_term_id._get_last_discount_date(aml.move_id.date) - else: - last_discount_date = False - if same_currency_mode \ - and aml.move_id.move_type in ('out_invoice', 'out_receipt', 'in_invoice', 'in_receipt') \ - and not aml.matched_debit_ids \ - and not aml.matched_credit_ids \ - and last_discount_date \ - and st_line.date <= last_discount_date: - - rate = abs(aml.amount_currency) / abs(aml.balance) if aml.balance else 1.0 - amls_with_epd_values_list.append({ - **aml_values, - 'amount_residual': st_line.company_currency_id.round(aml.discount_amount_currency / rate), - 'amount_residual_currency': aml.discount_amount_currency, - }) - else: - amls_with_epd_values_list.append(aml_values) - - def match_batch_amls(amls_values_list): - if not same_currency_mode: - return None, [] - - kepts_amls_values_list = [] - sum_amount_residual_currency = 0.0 - for aml_values in amls_values_list: - - if st_line_currency.compare_amounts(st_line_amount, -aml_values['amount_residual_currency']) == 0: - # Special case: the amounts are the same, submit the line directly. - return 'perfect', [aml_values] - - if st_line_currency.compare_amounts(sign * (st_line_amount + sum_amount_residual_currency), 0.0) > 0: - # Here, we still have room for other candidates ; so we add the current one to the list we keep. - # Then, we continue iterating, even if there is no room anymore, just in case one of the following candidates - # is an exact match, which would then be preferred on the current candidates. - kepts_amls_values_list.append(aml_values) - sum_amount_residual_currency += aml_values['amount_residual_currency'] - - if st_line_currency.is_zero(sign * (st_line_amount + sum_amount_residual_currency)): - return 'perfect', kepts_amls_values_list - elif kepts_amls_values_list: - return 'partial', kepts_amls_values_list - else: - return None, [] - - # Try to match a batch with the early payment feature. Only a perfect match is allowed. - match_type, kepts_amls_values_list = match_batch_amls(amls_with_epd_values_list) - if match_type != 'perfect': - kepts_amls_values_list = [] - - # Try to match the amls having the same currency as the statement line. - if not kepts_amls_values_list: - _match_type, kepts_amls_values_list = match_batch_amls(amls_values_list) - - # Try to match the whole candidates. - if not kepts_amls_values_list: - kepts_amls_values_list = amls_values_list - - # Try to match the amls having the same currency as the statement line. - if kepts_amls_values_list: - status = self._check_rule_propositions(st_line, kepts_amls_values_list) - result = _create_result_dict(kepts_amls_values_list, status) - if result: - return result - - def _check_rule_propositions(self, st_line, amls_values_list): - """ Check restrictions that can't be handled for each move.line separately. - Note: Only used by models having a type equals to 'invoice_matching'. - :param st_line: The statement line. - :param amls_values_list: The candidates account.move.line as a list of dict: - * aml: The record. - * amount_residual: The amount residual to consider. - * amount_residual_currency: The amount residual in foreign currency to consider. - :return: A string representing what to do with the candidates: - * rejected: Reject candidates. - * allow_write_off: Allow to generate the write-off from the reconcile model lines if specified. - * allow_auto_reconcile: Allow to automatically reconcile entries if 'auto_validate' is enabled. - """ - self.ensure_one() - - if not self.allow_payment_tolerance: - return {'allow_write_off', 'allow_auto_reconcile'} - - st_line_currency = st_line.foreign_currency_id or st_line.currency_id - st_line_amount_curr = st_line._prepare_move_line_default_vals()[1]['amount_currency'] - amls_amount_curr = sum( - st_line._prepare_counterpart_amounts_using_st_line_rate( - aml_values['aml'].currency_id, - aml_values['amount_residual'], - aml_values['amount_residual_currency'], - )['amount_currency'] - for aml_values in amls_values_list - ) - sign = 1 if st_line_amount_curr > 0.0 else -1 - amount_curr_after_rec = st_line_currency.round( - sign * (amls_amount_curr + st_line_amount_curr) - ) - - # The statement line will be fully reconciled. - if st_line_currency.is_zero(amount_curr_after_rec): - return {'allow_auto_reconcile'} - - # The payment amount is higher than the sum of invoices. - # In that case, don't check the tolerance and don't try to generate any write-off. - if amount_curr_after_rec > 0.0: - return {'allow_auto_reconcile'} - - # No tolerance, reject the candidates. - if self.payment_tolerance_param == 0: - return {'rejected'} - - # If the tolerance is expressed as a fixed amount, check the residual payment amount doesn't exceed the - # tolerance. - if self.payment_tolerance_type == 'fixed_amount' and st_line_currency.compare_amounts(-amount_curr_after_rec, self.payment_tolerance_param) <= 0: - return {'allow_write_off', 'allow_auto_reconcile'} - - # The tolerance is expressed as a percentage between 0 and 100.0. - reconciled_percentage_left = (abs(amount_curr_after_rec / amls_amount_curr)) * 100.0 - if self.payment_tolerance_type == 'percentage' and st_line_currency.compare_amounts(reconciled_percentage_left, self.payment_tolerance_param) <= 0: - return {'allow_write_off', 'allow_auto_reconcile'} - - return {'rejected'} - - def run_auto_reconciliation(self): - """ Tries to auto-reconcile as many statements as possible within time limit - arbitrary set to 3 minutes (the rest will be reconciled asynchronously with the regular cron). - """ - # 'limit_time_real_cron' defaults to -1. - # Manual fallback applied for non-POSIX systems where this key is disabled (set to None). - cron_limit_time = tools.config['limit_time_real_cron'] or -1 - limit_time = cron_limit_time if 0 < cron_limit_time < 180 else 180 - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(limit_time=limit_time) diff --git a/addons/at_accounting/models/account_reconcile_model_line.py b/addons/at_accounting/models/account_reconcile_model_line.py deleted file mode 100644 index 4be6936..0000000 --- a/addons/at_accounting/models/account_reconcile_model_line.py +++ /dev/null @@ -1,117 +0,0 @@ -from odoo import models, Command, _ -from odoo.exceptions import UserError - -import re - -from math import copysign - - -class AccountReconcileModelLine(models.Model): - _inherit = 'account.reconcile.model.line' - - def _prepare_aml_vals(self, partner): - """ Prepare a dictionary that will be used later to create a new journal item (account.move.line) for the - given reconcile model line. - - :param partner: The partner to be linked to the journal item. - :return: A python dictionary. - """ - self.ensure_one() - - taxes = self.tax_ids - if taxes and partner: - fiscal_position = self.env['account.fiscal.position']._get_fiscal_position(partner) - if fiscal_position: - taxes = fiscal_position.map_tax(taxes) - - values = { - 'name': self.label, - 'partner_id': partner.id, - 'analytic_distribution': self.analytic_distribution, - 'tax_ids': [Command.set(taxes.ids)], - 'reconcile_model_id': self.model_id.id, - } - if self.account_id: - values['account_id'] = self.account_id.id - return values - - def _apply_in_manual_widget(self, residual_amount_currency, partner, currency): - """ Prepare a dictionary that will be used later to create a new journal item (account.move.line) for the - given reconcile model line used by the manual reconciliation widget. - - Note: 'journal_id' is added to the returned dictionary even if it is a related readonly field. - It's a hack for the manual reconciliation widget. Indeed, a single journal entry will be created for each - journal. - - :param residual_amount_currency: The current balance expressed in the account's currency. - :param partner: The partner to be linked to the journal item. - :param currency: The currency set on the account in the manual reconciliation widget. - :return: A python dictionary. - """ - self.ensure_one() - - if self.amount_type == 'percentage': - amount_currency = currency.round(residual_amount_currency * (self.amount / 100.0)) - elif self.amount_type == 'fixed': - sign = 1 if residual_amount_currency > 0.0 else -1 - amount_currency = currency.round(self.amount * sign) - else: - raise UserError(_("This reconciliation model can't be used in the manual reconciliation widget because its " - "configuration is not adapted")) - - return { - **self._prepare_aml_vals(partner), - 'currency_id': currency.id, - 'amount_currency': amount_currency, - 'journal_id': self.journal_id.id, - } - - def _apply_in_bank_widget(self, residual_amount_currency, partner, st_line): - """ Prepare a dictionary that will be used later to create a new journal item (account.move.line) for the - given reconcile model line used by the bank reconciliation widget. - - :param residual_amount_currency: The current balance expressed in the statement line's currency. - :param partner: The partner to be linked to the journal item. - :param st_line: The statement line mounted inside the bank reconciliation widget. - :return: A python dictionary. - """ - self.ensure_one() - currency = st_line.foreign_currency_id or st_line.journal_id.currency_id or st_line.company_currency_id - - aml_values = {'currency_id': currency.id} - - if self.amount_type == 'percentage_st_line': - transaction_amount, transaction_currency, journal_amount, journal_currency, _company_amount, _company_currency \ - = st_line._get_accounting_amounts_and_currencies() - if self.model_id.rule_type == 'writeoff_button' and self.model_id.counterpart_type in ('sale', 'purchase'): - # The invoice should be created using the transaction currency. - aml_values['amount_currency'] = currency.round(-transaction_amount * self.amount / 100.0) - aml_values['percentage_st_line'] = self.amount / 100.0 - aml_values['currency_id'] = transaction_currency.id - else: - # The additional journal items follow the journal currency. - aml_values['amount_currency'] = currency.round(-journal_amount * self.amount / 100.0) - aml_values['currency_id'] = journal_currency.id - elif self.amount_type == 'regex': - match = re.search(self.amount_string, st_line.payment_ref) - if match: - sign = 1 if residual_amount_currency > 0.0 else -1 - decimal_separator = self.model_id.decimal_separator - try: - extracted_match_group = re.sub(r'[^\d' + decimal_separator + ']', '', match.group(1)) - extracted_balance = float(extracted_match_group.replace(decimal_separator, '.')) - aml_values['amount_currency'] = copysign(extracted_balance * sign, residual_amount_currency) - except ValueError: - aml_values['amount_currency'] = 0.0 - else: - aml_values['amount_currency'] = 0.0 - - if 'amount_currency' not in aml_values: - aml_values.update(self._apply_in_manual_widget(residual_amount_currency, partner, currency)) - else: - aml_values.update(self._prepare_aml_vals(partner)) - - if not aml_values.get('name', False): - aml_values['name'] = st_line.payment_ref - - return aml_values diff --git a/addons/at_accounting/models/account_report.py b/addons/at_accounting/models/account_report.py deleted file mode 100644 index 27e32f3..0000000 --- a/addons/at_accounting/models/account_report.py +++ /dev/null @@ -1,7256 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import ast -import base64 -import datetime -import io -import json -import logging -import re -from ast import literal_eval -from collections import defaultdict -from functools import cmp_to_key -from itertools import groupby - -import markupsafe -from dateutil.relativedelta import relativedelta -from PIL import ImageFont - -from odoo import models, fields, api, _, osv -from odoo.addons.web.controllers.utils import clean_action -from odoo.exceptions import RedirectWarning, UserError, ValidationError -from odoo.models import check_method_name -from odoo.tools import date_utils, get_lang, float_is_zero, float_repr, SQL, parse_version, Query -from odoo.tools.float_utils import float_round, float_compare -from odoo.tools.misc import file_path, format_date, formatLang, split_every, xlsxwriter -from odoo.tools.safe_eval import expr_eval, safe_eval - -_logger = logging.getLogger(__name__) - -ACCOUNT_CODES_ENGINE_SPLIT_REGEX = re.compile(r"(?=[+-])") - -ACCOUNT_CODES_ENGINE_TERM_REGEX = re.compile( - r"^(?P[+-]?)"\ - r"(?P([A-Za-z\d.]*|tag\([\w.]+\))((?=\\)|(?<=[^CD])))"\ - r"(\\\((?P([A-Za-z\d.]+,)*[A-Za-z\d.]*)\))?"\ - r"(?P[DC]?)$" -) - -ACCOUNT_CODES_ENGINE_TAG_ID_PREFIX_REGEX = re.compile(r"tag\(((?P\d+)|(?P\w+\.\w+))\)") - -# Performance optimisation: those engines always will receive None as their next_groupby, allowing more efficient batching. -NO_NEXT_GROUPBY_ENGINES = {'tax_tags', 'account_codes'} - -NUMBER_FIGURE_TYPES = ('float', 'integer', 'monetary', 'percentage') - -LINE_ID_HIERARCHY_DELIMITER = '|' - -CURRENCIES_USING_LAKH = {'AFN', 'BDT', 'INR', 'MMK', 'NPR', 'PKR', 'LKR'} - - -class AccountReportAnnotation(models.Model): - _name = 'account.report.annotation' - _description = 'Account Report Annotation' - - report_id = fields.Many2one('account.report', help="The id of the annotated report.") - line_id = fields.Char(index=True, help="The id of the annotated line.") - text = fields.Char(string="The annotation's content.") - date = fields.Date(help="Date considered as annotated by the annotation.") - fiscal_position_id = fields.Many2one('account.fiscal.position', help="The fiscal position used while annotating.") - - @api.model_create_multi - def create(self, values): - fiscal_positions_with_foreign_vat = self.env['account.fiscal.position'].search([('foreign_vat', '!=', False)], limit=1) - for annotation in values: - if 'line_id' in annotation: - annotation['line_id'] = self._remove_tax_grouping_from_line_id(annotation['line_id']) - if 'fiscal_position_id' in annotation: - if annotation['fiscal_position_id'] == 'domestic': - del annotation['fiscal_position_id'] - elif annotation['fiscal_position_id'] == 'all': - annotation['fiscal_position_id'] = fiscal_positions_with_foreign_vat.id - else: - annotation['fiscal_position_id'] = int(annotation['fiscal_position_id']) - - return super().create(values) - - def _remove_tax_grouping_from_line_id(self, line_id): - """ - Remove the tax grouping from the line_id. This is needed because the tax grouping is not relevant for the annotation. - Tax grouping are any group using 'account.group' in the line_id. - """ - return self.env['account.report']._build_line_id([ - (markup, model, res_id) - for markup, model, res_id in self.env['account.report']._parse_line_id(line_id, markup_as_string=True) - if model != 'account.group' - ]) - -class AccountReport(models.Model): - _inherit = 'account.report' - - horizontal_group_ids = fields.Many2many(string="Horizontal Groups", comodel_name='account.report.horizontal.group') - annotations_ids = fields.One2many(string="Annotations", comodel_name='account.report.annotation', inverse_name='report_id') - - # Those fields allow case-by-case fine-tuning of the engine, for custom reports. - custom_handler_model_id = fields.Many2one(string='Custom Handler Model', comodel_name='ir.model') - custom_handler_model_name = fields.Char(string='Custom Handler Model Name', related='custom_handler_model_id.model') - - # Account Coverage Report - is_account_coverage_report_available = fields.Boolean(compute='_compute_is_account_coverage_report_available') - - tax_closing_start_date = fields.Date( # the default value is set in _auto_init - string="Start Date", - company_dependent=True - ) - - # Fields used for send reports by cron - send_and_print_values = fields.Json(copy=False) - - def _auto_init(self): - super()._auto_init() - - def precommit(): - self.env['ir.default'].set( - 'account.report', - 'tax_closing_start_date', - fields.Date.context_today(self).replace(month=1, day=1), - ) - self.env.cr.precommit.add(precommit) - - @api.constrains('custom_handler_model_id') - def _validate_custom_handler_model(self): - for report in self: - if report.custom_handler_model_id: - custom_handler_model = self.env.registry['account.report.custom.handler'] - current_model = self.env[report.custom_handler_model_name] - if not isinstance(current_model, custom_handler_model): - raise ValidationError(_( - "Field 'Custom Handler Model' can only reference records inheriting from [%s].", - custom_handler_model._name - )) - - def unlink(self): - for report in self: - action, menuitem = report._get_existing_menuitem() - menuitem.unlink() - action.unlink() - return super().unlink() - - def write(self, vals): - if 'active' in vals: - for report in self: - dummy, menuitem = report._get_existing_menuitem() - menuitem.active = vals['active'] - return super().write(vals) - - #################################################### - # CRON - #################################################### - - @api.model - def _cron_account_report_send(self, job_count=10): - """ Handle Send & Print async processing. - :param job_count: maximum number of jobs to process if specified. - """ - to_process = self.env['account.report'].search( - [('send_and_print_values', '!=', False)], - ) - if not to_process: - return - - processed_count = 0 - need_retrigger = False - - for report in to_process: - if need_retrigger: - break - send_and_print_vals = report.send_and_print_values - report_partner_ids = send_and_print_vals.get('report_options', {}).get('partner_ids', []) - need_retrigger = processed_count + len(report_partner_ids) > job_count - for _id in report_partner_ids[:job_count - processed_count]: - options = { - **send_and_print_vals['report_options'], - 'partner_ids': [_id], - } - self.env['account.report.send']._process_send_and_print(report=report, options=options) - processed_count += 1 - report_partner_ids.remove(_id) - if report_partner_ids: - send_and_print_vals['report_options']['partner_ids'] = report_partner_ids - report.send_and_print_values = send_and_print_vals - else: - report.send_and_print_values = False - - if need_retrigger: - self.env.ref('at_accounting.ir_cron_account_report_send')._trigger() - - #################################################### - # MENU MANAGEMENT - #################################################### - - def _get_existing_menuitem(self): - self.ensure_one() - action = self.env['ir.actions.client']\ - .search([('name', '=', self.name), ('tag', '=', 'account_report')])\ - .filtered(lambda act: ast.literal_eval(act.context).get('report_id') == self.id) - menuitem = self.env['ir.ui.menu']\ - .with_context({'active_test': False, 'ir.ui.menu.full_list': True})\ - .search([('action', '=', f'ir.actions.client,{action.id}')]) - return action, menuitem - - def _create_menu_item_for_report(self): - """ Adds a default menu item for this report. This is called by an action on the report, for reports created manually by the user. - """ - self.ensure_one() - - action, menuitem = self._get_existing_menuitem() - - if menuitem: - raise UserError(_("This report already has a menuitem.")) - - if not action: - action = self.env['ir.actions.client'].create({ - 'name': self.name, - 'tag': 'account_report', - 'context': {'report_id': self.id}, - }) - - self.env['ir.ui.menu'].create({ - 'name': self.name, - 'parent_id': self.env['ir.model.data']._xmlid_to_res_id('account.menu_finance_reports'), - 'action': f'ir.actions.client,{action.id}', - }) - - return { - 'type': 'ir.actions.client', - 'tag': 'reload', - } - - #################################################### - # OPTIONS: journals - #################################################### - - def _get_filter_journals(self, options, additional_domain=None): - return self.env['account.journal'].with_context(active_test=False).search([ - *self.env['account.journal']._check_company_domain(self.get_report_company_ids(options)), - *(additional_domain or []), - ], order="company_id, name") - - def _get_filter_journal_groups(self, options): - return self.env['account.journal.group'].search([ - *self.env['account.journal.group']._check_company_domain(self.get_report_company_ids(options)), - ], order='sequence') - - def _init_options_journals(self, options, previous_options, additional_journals_domain=None): - # The additional additional_journals_domain optional parameter allows calling this with an additional restriction on journals, - # to regenerate the journal options accordingly. - def option_value(value, selected=False, group_journals=None): - result = { - 'id': value.id, - 'model': value._name, - 'name': value.display_name, - 'selected': selected, - } - - if value._name == 'account.journal.group': - result.update({ - 'title': value.display_name, - 'journals': group_journals.ids, - 'journal_types': list(set(group_journals.mapped('type'))), - }) - elif value._name == 'account.journal': - result.update({ - 'title': f"{value.name} - {value.code}", - 'type': value.type, - 'visible': True, - }) - - return result - - if not self.filter_journals: - return - - previous_journals = previous_options.get('journals', []) - previous_journal_group_action = previous_options.get('__journal_group_action', {}) - - all_journals = self._get_filter_journals(options, additional_domain=additional_journals_domain) - all_journal_groups = self._get_filter_journal_groups(options) - - options['journals'] = [] - options['selected_journal_groups'] = {} - - groups_journals_selected = set() - options_journal_groups = [] - - # First time opening the report, and make sure it's not specifically stated that we should not reset the filter - is_opening_report = previous_options.get('is_opening_report') # key from JS controller when report is being opened - # a key to prevent the reset of the journals filter even when is_opening_report is True - can_reset_journals_filter = not previous_options.get('not_reset_journals_filter') - - # 1. Handle journal group selection - for group in all_journal_groups: - group_journals = all_journals - group.excluded_journal_ids - selected = False - first_group_already_selected = bool(options['selected_journal_groups']) # only one group should be selected at most - - # select the first group by default when opening the report - if is_opening_report and not first_group_already_selected and can_reset_journals_filter: - selected = True - # Otherwise, select the previous selected group (if any) - elif group.id == previous_journal_group_action.get('id'): - selected = previous_journal_group_action.get('action') == 'add' - - group_option = option_value(group, selected=selected, group_journals=group_journals) - options_journal_groups.append(group_option) - - # Select all the group journals - if selected: - options['selected_journal_groups'] = group_option - groups_journals_selected |= set(group_journals.ids) - - # 2. Handle journals selection - previous_selected_journals_ids = { - journal['id'] - for journal in previous_journals - if journal.get('model') == 'account.journal' and journal.get('selected') - } - - company_journals_map = defaultdict(list) - journals_selected = set() - - for journal in all_journals: - selected = False - - if journal.id in groups_journals_selected: - selected = True - - elif not options['selected_journal_groups'] and previous_journal_group_action.get('action') != 'remove': - if journal.id in previous_selected_journals_ids: - selected = True - - if selected: - journals_selected.add(journal.id) - - company_journals_map[journal.company_id].append(option_value(journal, selected=journal.id in journals_selected)) - - # 3. Recompute selected groups in case the set of selected journals is equal to a group's accepted journals - for group in options_journal_groups: - if journals_selected == set(group['journals']): - group['selected'] = True - options['selected_journal_groups'] = group - - # 4. Unselect all journals if all are selected and no group is specifically selected - if journals_selected == set(all_journals.ids) and not options['selected_journal_groups']: - for company, journals in company_journals_map.items(): - for journal in journals: - journal['selected'] = False - - # 5. Build group options - if all_journal_groups: - options['journals'] = [{ - 'id': 'divider', - 'name': _("Multi-ledger"), - 'model': 'account.journal.group', - }] + options_journal_groups - - if not company_journals_map: - options['name_journal_group'] = _("No Journal") - return - - # 6. Build journals options - if len(company_journals_map) > 1 or all_journal_groups: - for company, journals in company_journals_map.items(): - - # if not is_opening_report, then gets the unfolded attribute of the company from the previous options - unfolded = False if is_opening_report else next( - (entry.get('unfolded') for entry in previous_journals - if entry['model'] == 'res.company' and entry['name'] == company.name), False) - - for journal in journals: - journal['visible'] = unfolded - - options['journals'].append({ - 'id': 'divider', - 'model': company._name, - 'name': company.display_name, - 'unfolded': unfolded, - }) - - options['journals'] += journals - - else: - options['journals'].extend(next(iter(company_journals_map.values()), [])) - - # 7 Compute the name to display on the widget - if options.get('selected_journal_groups'): - names_to_display = [options['selected_journal_groups']['name']] - elif len(all_journals) == len(journals_selected) or not journals_selected: - names_to_display = [_("All Journals")] - else: - names_to_display = [] - - for journal in options['journals']: - if journal.get('model') == 'account.journal' and journal['selected']: - names_to_display += [journal['name']] - - # 8. Abbreviate the name - max_nb_journals_displayed = 5 - nb_remaining = len(names_to_display) - max_nb_journals_displayed - displayed_names = ', '.join(names_to_display[:max_nb_journals_displayed]) - if nb_remaining == 1: - options['name_journal_group'] = _("%(names)s and one other", names=displayed_names) - elif nb_remaining > 1: - options['name_journal_group'] = _("%(names)s and %(remaining)s others", names=displayed_names, remaining=nb_remaining) - else: - options['name_journal_group'] = displayed_names - - @api.model - def _get_options_journals(self, options): - selected_journals = [ - journal for journal in options.get('journals', []) - if journal['model'] == 'account.journal' and journal['selected'] - ] - if not selected_journals: - # If no journal is specifically selected, we actually want to select them all. - # This is needed, because some reports will not use ALL available journals and filter by type. - # Without getting them from the options, we will use them all, which is wrong. - selected_journals = [ - journal for journal in options.get('journals', []) - if journal['model'] == 'account.journal' - ] - return selected_journals - - @api.model - def _get_options_journals_domain(self, options): - # Make sure to return an empty array when nothing selected to handle archived journals. - selected_journals = self._get_options_journals(options) - return selected_journals and [('journal_id', 'in', [j['id'] for j in selected_journals])] or [] - - # #################################################### - # OPTIONS: USER DEFINED FILTERS ON AML - #################################################### - def _init_options_aml_ir_filters(self, options, previous_options): - options['aml_ir_filters'] = [] - if not self.filter_aml_ir_filters: - return - - ir_filters = self.env['ir.filters'].search([('model_id', '=', 'account.move.line')]) - if not ir_filters: - return - - aml_ir_filters = [{'id': x.id, 'name': x.name, 'selected': False} for x in ir_filters] - previous_options_aml_ir_filters = previous_options.get('aml_ir_filters', []) - previous_options_filters_map = {filter_item['id']: filter_item for filter_item in previous_options_aml_ir_filters} - - for filter_item in aml_ir_filters: - if filter_item['id'] in previous_options_filters_map: - filter_item['selected'] = previous_options_filters_map[filter_item['id']]['selected'] - - options['aml_ir_filters'] = aml_ir_filters - - @api.model - def _get_options_aml_ir_filters(self, options): - selected_filters_ids = [ - filter_item['id'] - for filter_item in options.get('aml_ir_filters', []) - if filter_item['selected'] - ] - - if not selected_filters_ids: - return [] - - selected_ir_filters = self.env['ir.filters'].browse(selected_filters_ids) - return osv.expression.OR([filter_record._get_eval_domain() for filter_record in selected_ir_filters]) - - #################################################### - # OPTIONS: date + comparison - #################################################### - - @api.model - def _get_dates_period(self, date_from, date_to, mode, period_type=None): - '''Compute some information about the period: - * The name to display on the report. - * The period type (e.g. quarter) if not specified explicitly. - :param date_from: The starting date of the period. - :param date_to: The ending date of the period. - :param period_type: The type of the interval date_from -> date_to. - :return: A dictionary containing: - * date_from * date_to * string * period_type * mode * - ''' - def match(dt_from, dt_to): - return (dt_from, dt_to) == (date_from, date_to) - - def get_quarter_name(date_to, date_from): - date_to_quarter_string = format_date(self.env, fields.Date.to_string(date_to), date_format='MMM yyyy') - date_from_quarter_string = format_date(self.env, fields.Date.to_string(date_from), date_format='MMM') - return f"{date_from_quarter_string} - {date_to_quarter_string}" - - string = None - # If no date_from or not date_to, we are unable to determine a period - if not period_type or period_type == 'custom': - date = date_to or date_from - company_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date) - if match(company_fiscalyear_dates['date_from'], company_fiscalyear_dates['date_to']): - period_type = 'fiscalyear' - if company_fiscalyear_dates.get('record'): - string = company_fiscalyear_dates['record'].name - elif match(*date_utils.get_month(date)): - period_type = 'month' - elif match(*date_utils.get_quarter(date)): - period_type = 'quarter' - elif match(*date_utils.get_fiscal_year(date)): - period_type = 'year' - elif match(date_utils.get_month(date)[0], fields.Date.today()): - period_type = 'today' - else: - period_type = 'custom' - elif period_type == 'fiscalyear': - date = date_to or date_from - company_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date) - record = company_fiscalyear_dates.get('record') - string = record and record.name - elif period_type == 'tax_period': - day, month = self.env.company._get_tax_closing_start_date_attributes(self) - months_per_period = self.env.company._get_tax_periodicity_months_delay(self) - # We need to format ourselves the date and not switch the period type to the actual period because we do not want to write the actual period in the options but keep tax_period - if day == 1 and month == 1 and months_per_period in (1, 3, 12): - match months_per_period: - case 1: - string = format_date(self.env, fields.Date.to_string(date_to), date_format='MMM yyyy') - case 3: - string = get_quarter_name(date_to, date_from) - case 12: - string = date_to.strftime('%Y') - else: - dt_from_str = format_date(self.env, fields.Date.to_string(date_from)) - dt_to_str = format_date(self.env, fields.Date.to_string(date_to)) - string = '%s - %s' % (dt_from_str, dt_to_str) - - if not string: - fy_day = self.env.company.fiscalyear_last_day - fy_month = int(self.env.company.fiscalyear_last_month) - if mode == 'single': - string = _('As of %s', format_date(self.env, date_to)) - elif period_type == 'year' or ( - period_type == 'fiscalyear' and (date_from, date_to) == date_utils.get_fiscal_year(date_to)): - string = date_to.strftime('%Y') - elif period_type == 'fiscalyear' and (date_from, date_to) == date_utils.get_fiscal_year(date_to, day=fy_day, month=fy_month): - string = '%s - %s' % (date_to.year - 1, date_to.year) - elif period_type == 'month': - string = format_date(self.env, fields.Date.to_string(date_to), date_format='MMM yyyy') - elif period_type == 'quarter': - string = get_quarter_name(date_to, date_from) - else: - dt_from_str = format_date(self.env, fields.Date.to_string(date_from)) - dt_to_str = format_date(self.env, fields.Date.to_string(date_to)) - string = _('From %(date_from)s\nto %(date_to)s', date_from=dt_from_str, date_to=dt_to_str) - - return { - 'string': string, - 'period_type': period_type, - 'currency_table_period_key': f"{date_from if mode == 'range' else 'None'}_{date_to}", - 'mode': mode, - 'date_from': date_from and fields.Date.to_string(date_from) or False, - 'date_to': fields.Date.to_string(date_to), - } - - @api.model - def _get_shifted_dates_period(self, options, period_vals, periods, tax_period=False): - '''Shift the period. - :param period_vals: A dictionary generated by the _get_dates_period method. - :param periods: The number of periods we want to move either in the future or the past - :return: A dictionary containing: - * date_from * date_to * string * period_type * - ''' - period_type = period_vals['period_type'] - mode = period_vals['mode'] - date_from = fields.Date.from_string(period_vals['date_from']) - date_to = fields.Date.from_string(period_vals['date_to']) - if period_type == 'month': - date_to = date_from + relativedelta(months=periods) - elif period_type == 'quarter': - date_to = date_from + relativedelta(months=3 * periods) - elif period_type == 'year': - date_to = date_from + relativedelta(years=periods) - elif period_type in {'custom', 'today'}: - date_to = date_from + relativedelta(days=periods) - - if tax_period or 'tax_period' in period_type: - month_per_period = self.env.company._get_tax_periodicity_months_delay(self) - date_from, date_to = self.env.company._get_tax_closing_period_boundaries(date_from + relativedelta(months=month_per_period * periods), self) - return self._get_dates_period(date_from, date_to, mode, period_type='tax_period') - if period_type in ('fiscalyear', 'today'): - # Don't pass the period_type to _get_dates_period to be able to retrieve the account.fiscal.year record if - # necessary. - company_fiscalyear_dates = {} - # This loop is needed because a fiscal year can be a month, quarter, etc - for _ in range(abs(periods)): - date_to = (date_from if periods < 0 else date_to) + relativedelta(days=periods) - company_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_to) - if periods < 0: - date_from = company_fiscalyear_dates['date_from'] - else: - date_to = company_fiscalyear_dates['date_to'] - - return self._get_dates_period(company_fiscalyear_dates['date_from'], company_fiscalyear_dates['date_to'], mode) - if period_type in ('month', 'custom'): - return self._get_dates_period(*date_utils.get_month(date_to), mode, period_type='month') - if period_type == 'quarter': - return self._get_dates_period(*date_utils.get_quarter(date_to), mode, period_type='quarter') - if period_type == 'year': - return self._get_dates_period(*date_utils.get_fiscal_year(date_to), mode, period_type='year') - return None - - @api.model - def _get_dates_previous_year(self, options, period_vals): - '''Shift the period to the previous year. - :param options: The report options. - :param period_vals: A dictionary generated by the _get_dates_period method. - :return: A dictionary containing: - * date_from * date_to * string * period_type * - ''' - period_type = period_vals['period_type'] - mode = period_vals['mode'] - date_from = fields.Date.from_string(period_vals['date_from']) - date_from = date_from - relativedelta(years=1) - date_to = fields.Date.from_string(period_vals['date_to']) - date_to = date_to - relativedelta(years=1) - - if period_type == 'month': - date_from, date_to = date_utils.get_month(date_to) - - return self._get_dates_period(date_from, date_to, mode, period_type=period_type) - - def _init_options_date(self, options, previous_options): - """ Initialize the 'date' options key. - - :param options: The current report options to build. - :param previous_options: The previous options coming from another report. - """ - date = previous_options.get('date', {}) - period_date_to = date.get('date_to') - period_date_from = date.get('date_from') - mode = date.get('mode') - date_filter = date.get('filter', 'custom') - - default_filter = self.default_opening_date_filter - options_mode = 'range' if self.filter_date_range else 'single' - date_from = date_to = period_type = False - - if mode == 'single' and options_mode == 'range': - # 'single' date mode to 'range'. - if date_filter: - date_to = fields.Date.from_string(period_date_to or period_date_from) - date_from = self.env.company.compute_fiscalyear_dates(date_to)['date_from'] - options_filter = 'custom' - else: - options_filter = default_filter - elif mode == 'range' and options_mode == 'single': - # 'range' date mode to 'single'. - if date_filter == 'custom': - date_to = fields.Date.from_string(period_date_to or period_date_from) - date_from = date_utils.get_month(date_to)[0] - options_filter = 'custom' - elif date_filter: - options_filter = date_filter - else: - options_filter = default_filter - elif (mode is None or mode == options_mode) and date: - # Same date mode. - if date_filter == 'custom': - if options_mode == 'range': - date_from = fields.Date.from_string(period_date_from) - date_to = fields.Date.from_string(period_date_to) - else: - date_to = fields.Date.from_string(period_date_to or period_date_from) - date_from = date_utils.get_month(date_to)[0] - - options_filter = 'custom' - else: - options_filter = date_filter - else: - # Default. - options_filter = default_filter - - # Compute 'date_from' / 'date_to'. - if not date_from or not date_to: - if options_filter == 'today': - date_to = fields.Date.context_today(self) - date_from = self.env.company.compute_fiscalyear_dates(date_to)['date_from'] - period_type = 'today' - elif 'month' in options_filter: - date_from, date_to = date_utils.get_month(fields.Date.context_today(self)) - period_type = 'month' - elif 'quarter' in options_filter: - date_from, date_to = date_utils.get_quarter(fields.Date.context_today(self)) - period_type = 'quarter' - elif 'year' in options_filter: - company_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(fields.Date.context_today(self)) - date_from = company_fiscalyear_dates['date_from'] - date_to = company_fiscalyear_dates['date_to'] - elif 'tax_period' in options_filter: - if 'custom' in options_filter: - base_date = fields.Date.from_string(period_date_to) - else: - base_date = fields.Date.context_today(self) - - date_from, date_to = self.env.company._get_tax_closing_period_boundaries(base_date, self) - period_type = 'tax_period' - - options['date'] = self._get_dates_period( - date_from, - date_to, - options_mode, - period_type=period_type, - ) - - if any(option in options_filter for option in ['previous', 'next']): - new_period = date.get('period', -1 if 'previous' in options_filter else 1) - options['date'] = self._get_shifted_dates_period(options, options['date'], new_period, tax_period='tax_period' in options_filter) - # This line is useful for the export and tax closing so that the period is set in the options. - options['date']['period'] = new_period - - options['date']['filter'] = options_filter - - def _init_options_comparison(self, options, previous_options): - """ Initialize the 'comparison' options key. - - This filter must be loaded after the 'date' filter. - - :param options: The current report options to build. - :param previous_options: The previous options coming from another report. - """ - if not self.filter_period_comparison: - return - - previous_comparison = previous_options.get('comparison', {}) - previous_filter = previous_comparison.get('filter') - - period_order = previous_comparison.get('period_order') or 'descending' - if previous_filter == 'custom': - # Try to adapt the previous 'custom' filter. - date_from = previous_comparison.get('date_from') - date_to = previous_comparison.get('date_to') - number_period = 1 - options_filter = 'custom' - else: - # Use the 'date' options. - date_from = options['date']['date_from'] - date_to = options['date']['date_to'] - number_period = max(previous_comparison.get('number_period', 1) or 0, 0) - options_filter = number_period and previous_filter or 'no_comparison' - - options['comparison'] = { - 'filter': options_filter, - 'number_period': number_period, - 'date_from': date_from, - 'date_to': date_to, - 'periods': [], - 'period_order': period_order, - } - - date_from_obj = fields.Date.from_string(date_from) - date_to_obj = fields.Date.from_string(date_to) - - if options_filter == 'custom': - options['comparison']['periods'].append(self._get_dates_period( - date_from_obj, - date_to_obj, - options['date']['mode'], - )) - elif options_filter in ('previous_period', 'same_last_year'): - previous_period = options['date'] - for dummy in range(0, number_period): - if options_filter == 'previous_period': - period_vals = self._get_shifted_dates_period(options, previous_period, -1) - elif options_filter == 'same_last_year': - period_vals = self._get_dates_previous_year(options, previous_period) - else: - date_from_obj = fields.Date.from_string(date_from) - date_to_obj = fields.Date.from_string(date_to) - period_vals = self._get_dates_period(date_from_obj, date_to_obj, previous_period['mode']) - options['comparison']['periods'].append(period_vals) - previous_period = period_vals - - if len(options['comparison']['periods']) > 0: - options['comparison'].update(options['comparison']['periods'][0]) - - def _init_options_column_percent_comparison(self, options, previous_options): - if options['selected_horizontal_group_id'] is None: - if self.filter_growth_comparison and len(options['columns']) == 2 and len(options.get('comparison', {}).get('periods', [])) == 1: - options['column_percent_comparison'] = 'growth' - - if self.filter_budgets and any(budget['selected'] for budget in options.get('budgets', [])): - options['column_percent_comparison'] = 'budget' - - def _get_options_date_domain(self, options, date_scope): - date_from, date_to = self._get_date_bounds_info(options, date_scope) - - scope_domain = [('date', '<=', date_to)] - if date_from: - scope_domain += [('date', '>=', date_from)] - - return scope_domain - - def _get_date_bounds_info(self, options, date_scope): - # Default values (the ones from 'strict_range') - date_to = options['date']['date_to'] - date_from = options['date']['date_from'] if options['date']['mode'] == 'range' else None - - if date_scope == 'from_beginning': - date_from = None - - elif date_scope == 'to_beginning_of_period': - date_tmp = fields.Date.from_string(date_from or date_to) - relativedelta(days=1) - date_to = date_tmp.strftime('%Y-%m-%d') - date_from = None - - elif date_scope == 'from_fiscalyear': - date_tmp = fields.Date.from_string(date_to) - date_tmp = self.env.company.compute_fiscalyear_dates(date_tmp)['date_from'] - date_from = date_tmp.strftime('%Y-%m-%d') - - elif date_scope == 'to_beginning_of_fiscalyear': - date_tmp = fields.Date.from_string(date_to) - date_tmp = self.env.company.compute_fiscalyear_dates(date_tmp)['date_from'] - relativedelta(days=1) - date_to = date_tmp.strftime('%Y-%m-%d') - date_from = None - - elif date_scope == 'previous_tax_period': - eve_of_date_from = fields.Date.from_string(options['date']['date_from']) - relativedelta(days=1) - date_from, date_to = self.env.company._get_tax_closing_period_boundaries(eve_of_date_from, self) - - return date_from, date_to - - - #################################################### - # OPTIONS: analytic filter - #################################################### - - def _init_options_analytic(self, options, previous_options): - if not self.filter_analytic: - return - - - if self.env.user.has_group('analytic.group_analytic_accounting'): - previous_analytic_accounts = previous_options.get('analytic_accounts', []) - analytic_account_ids = [int(x) for x in previous_analytic_accounts] - selected_analytic_accounts = self.env['account.analytic.account'].with_context(active_test=False).search([('id', 'in', analytic_account_ids)]) - - options['display_analytic'] = True - options['analytic_accounts'] = selected_analytic_accounts.ids - options['selected_analytic_account_names'] = selected_analytic_accounts.mapped('name') - - #################################################### - # OPTIONS: partners - #################################################### - - def _init_options_partner(self, options, previous_options): - if not self.filter_partner: - return - - options['partner'] = True - previous_partner_ids = previous_options.get('partner_ids') or [] - options['partner_categories'] = previous_options.get('partner_categories') or [] - - selected_partner_ids = [int(partner) for partner in previous_partner_ids] - # search instead of browse so that record rules apply and filter out the ones the user does not have access to - selected_partners = selected_partner_ids and self.env['res.partner'].with_context(active_test=False).search([('id', 'in', selected_partner_ids)]) or self.env['res.partner'] - options['selected_partner_ids'] = selected_partners.mapped('name') - options['partner_ids'] = selected_partners.ids - - selected_partner_category_ids = [int(category) for category in options['partner_categories']] - selected_partner_categories = selected_partner_category_ids and self.env['res.partner.category'].browse(selected_partner_category_ids) or self.env['res.partner.category'] - options['selected_partner_categories'] = selected_partner_categories.mapped('name') - - @api.model - def _get_options_partner_domain(self, options): - domain = [] - if options.get('partner_ids'): - partner_ids = [int(partner) for partner in options['partner_ids']] - domain.append(('partner_id', 'in', partner_ids)) - if options.get('partner_categories'): - partner_category_ids = [int(category) for category in options['partner_categories']] - domain.append(('partner_id.category_id', 'in', partner_category_ids)) - return domain - - #################################################### - # OPTIONS: all_entries - #################################################### - - @api.model - def _get_options_all_entries_domain(self, options): - if not options.get('all_entries'): - return [('parent_state', '=', 'posted')] - else: - return [('parent_state', '!=', 'cancel')] - - #################################################### - # OPTIONS: not reconciled entries - #################################################### - def _init_options_reconciled(self, options, previous_options): - if self.filter_unreconciled: - options['unreconciled'] = previous_options.get('unreconciled', False) - else: - options['unreconciled'] = False - - @api.model - def _get_options_unreconciled_domain(self, options): - if options.get('unreconciled'): - return ['&', ('full_reconcile_id', '=', False), ('balance', '!=', '0')] - return [] - - #################################################### - # OPTIONS: account_type - #################################################### - - def _init_options_account_type(self, options, previous_options): - ''' - Initialize a filter based on the account_type of the line (trade/non trade, payable/receivable). - Selects a name to display according to the selections. - The group display name is selected according to the display name of the options selected. - ''' - if self.filter_account_type in ('disabled', False): - return - - account_type_list = [ - {'id': 'trade_receivable', 'name': _("Receivable"), 'selected': True}, - {'id': 'non_trade_receivable', 'name': _("Non Trade Receivable"), 'selected': False}, - {'id': 'trade_payable', 'name': _("Payable"), 'selected': True}, - {'id': 'non_trade_payable', 'name': _("Non Trade Payable"), 'selected': False}, - ] - - if self.filter_account_type == 'receivable': - options['account_type'] = account_type_list[:2] - elif self.filter_account_type == 'payable': - options['account_type'] = account_type_list[2:] - else: - options['account_type'] = account_type_list - - if previous_options.get('account_type'): - previously_selected_ids = {x['id'] for x in previous_options['account_type'] if x.get('selected')} - for opt in options['account_type']: - opt['selected'] = opt['id'] in previously_selected_ids - - - @api.model - def _get_options_account_type_domain(self, options): - all_domains = [] - selected_domains = [] - if not options.get('account_type') or len(options.get('account_type')) == 0: - return [] - for opt in options.get('account_type', []): - if opt['id'] == 'trade_receivable': - domain = [('account_id.non_trade', '=', False), ('account_id.account_type', '=', 'asset_receivable')] - elif opt['id'] == 'trade_payable': - domain = [('account_id.non_trade', '=', False), ('account_id.account_type', '=', 'liability_payable')] - elif opt['id'] == 'non_trade_receivable': - domain = [('account_id.non_trade', '=', True), ('account_id.account_type', '=', 'asset_receivable')] - elif opt['id'] == 'non_trade_payable': - domain = [('account_id.non_trade', '=', True), ('account_id.account_type', '=', 'liability_payable')] - if opt['selected']: - selected_domains.append(domain) - all_domains.append(domain) - return osv.expression.OR(selected_domains or all_domains) - - #################################################### - # OPTIONS: order column - #################################################### - - @api.model - def _init_options_order_column(self, options, previous_options): - # options['order_column'] is in the form {'expression_label': expression label of the column to order, 'direction': the direction order ('ASC' or 'DESC')} - options['order_column'] = None - - previous_value = previous_options and previous_options.get('order_column') - if previous_value: - for col in options['columns']: - if col['sortable'] and col['expression_label'] == previous_value['expression_label']: - options['order_column'] = previous_value - break - - #################################################### - # OPTIONS: hierarchy - #################################################### - - def _init_options_hierarchy(self, options, previous_options): - company_ids = self.get_report_company_ids(options) - if self.filter_hierarchy != 'never' and self.env['account.group'].search_count(self.env['account.group']._check_company_domain(company_ids), limit=1): - options['display_hierarchy_filter'] = True - if 'hierarchy' in previous_options: - options['hierarchy'] = previous_options['hierarchy'] - else: - options['hierarchy'] = self.filter_hierarchy == 'by_default' - else: - options['hierarchy'] = False - options['display_hierarchy_filter'] = False - - @api.model - def _create_hierarchy(self, lines, options): - """Compute the hierarchy based on account groups when the option is activated. - - The option is available only when there are account.group for the company. - It should be called when before returning the lines to the client/templater. - The lines are the result of _get_lines(). If there is a hierarchy, it is left - untouched, only the lines related to an account.account are put in a hierarchy - according to the account.group's and their prefixes. - """ - if not lines: - return lines - - def get_account_group_hierarchy(account): - # Create codes path in the hierarchy based on account. - groups = self.env['account.group'] - if account.group_id: - group = account.group_id - while group: - groups += group - group = group.parent_id - return list(groups.sorted(reverse=True)) - - def create_hierarchy_line(account_group, column_totals, level, parent_id): - line_id = self._get_generic_line_id('account.group', account_group.id if account_group else None, parent_line_id=parent_id) - unfolded = line_id in options.get('unfolded_lines') or options['unfold_all'] - name = account_group.display_name if account_group else _('(No Group)') - columns = [] - for column_total, column in zip(column_totals, options['columns']): - columns.append(self._build_column_dict(column_total, column, options=options)) - return { - 'id': line_id, - 'name': name, - 'title_hover': name, - 'unfoldable': True, - 'unfolded': unfolded, - 'level': level, - 'parent_id': parent_id, - 'columns': columns, - } - - def compute_group_totals(line, group=None): - return [ - hierarchy_total + (column.get('no_format') or 0.0) if isinstance(hierarchy_total, float) else hierarchy_total - for hierarchy_total, column - in zip(hierarchy[group]['totals'], line['columns']) - ] - - def render_lines(account_groups, current_level, parent_line_id, skip_no_group=True): - to_treat = [(current_level, parent_line_id, group) for group in account_groups.sorted()] - - if None in hierarchy and not skip_no_group: - to_treat.append((current_level, parent_line_id, None)) - - while to_treat: - level_to_apply, parent_id, group = to_treat.pop(0) - group_data = hierarchy[group] - hierarchy_line = create_hierarchy_line(group, group_data['totals'], level_to_apply, parent_id) - new_lines.append(hierarchy_line) - treated_child_groups = self.env['account.group'] - - for account_line in group_data['lines']: - for child_group in group_data['child_groups']: - if child_group not in treated_child_groups and child_group['code_prefix_end'] < account_line['name']: - render_lines(child_group, hierarchy_line['level'] + 1, hierarchy_line['id']) - treated_child_groups += child_group - - markup, model, account_id = self._parse_line_id(account_line['id'])[-1] - account_line_id = self._get_generic_line_id(model, account_id, markup=markup, parent_line_id=hierarchy_line['id']) - account_line.update({ - 'id': account_line_id, - 'parent_id': hierarchy_line['id'], - 'level': hierarchy_line['level'] + 1, - }) - new_lines.append(account_line) - - for child_line in account_line_children_map[account_id]: - markup, model, res_id = self._parse_line_id(child_line['id'])[-1] - child_line.update({ - 'id': self._get_generic_line_id(model, res_id, markup=markup, parent_line_id=account_line_id), - 'parent_id': account_line_id, - 'level': account_line['level'] + 1, - }) - new_lines.append(child_line) - - to_treat = [ - (level_to_apply + 1, hierarchy_line['id'], child_group) - for child_group - in group_data['child_groups'].sorted() - if child_group not in treated_child_groups - ] + to_treat - - def create_hierarchy_dict(): - return defaultdict(lambda: { - 'lines': [], - 'totals': [('' if column.get('figure_type') == 'string' else 0.0) for column in options['columns']], - 'child_groups': self.env['account.group'], - }) - - # Precompute the account groups of the accounts in the report - account_ids = [] - for line in lines: - markup, res_model, model_id = self._parse_line_id(line['id'])[-1] - if res_model == 'account.account': - account_ids.append(model_id) - self.env['account.account'].browse(account_ids).group_id - - new_lines, total_lines = [], [] - - # root_line_id is the id of the parent line of the lines we want to render - root_line_id = self._build_parent_line_id(self._parse_line_id(lines[0]['id'])) or None - last_account_line_id = account_id = None - current_level = 0 - account_line_children_map = defaultdict(list) - account_groups = self.env['account.group'] - root_account_groups = self.env['account.group'] - hierarchy = create_hierarchy_dict() - - for line in lines: - markup, res_model, model_id = self._parse_line_id(line['id'])[-1] - - # Account lines are used as the basis for the computation of the hierarchy. - if res_model == 'account.account': - last_account_line_id = line['id'] - current_level = line['level'] - account_id = model_id - account = self.env[res_model].browse(account_id) - account_groups = get_account_group_hierarchy(account) - - if not account_groups: - hierarchy[None]['lines'].append(line) - hierarchy[None]['totals'] = compute_group_totals(line) - else: - for i, group in enumerate(account_groups): - if i == 0: - hierarchy[group]['lines'].append(line) - if i == len(account_groups) - 1 and group not in root_account_groups: - root_account_groups += group - if group.parent_id and group not in hierarchy[group.parent_id]['child_groups']: - hierarchy[group.parent_id]['child_groups'] += group - - hierarchy[group]['totals'] = compute_group_totals(line, group=group) - - # This is not an account line, so we check to see if it is a descendant of the last account line. - # If so, it is added to the mapping of the lines that are related to this account. - elif last_account_line_id and line.get('parent_id', '').startswith(last_account_line_id): - account_line_children_map[account_id].append(line) - - # This is a total line that is not linked to an account. It is saved in order to be added at the end. - elif markup == 'total': - total_lines.append(line) - - # This line ends the scope of the current hierarchy and is (possibly) the root of a new hierarchy. - # We render the current hierarchy and set up to build a new hierarchy - else: - render_lines(root_account_groups, current_level, root_line_id, skip_no_group=False) - - new_lines.append(line) - - # Reset the hierarchy-related variables for a new hierarchy - root_line_id = line['id'] - last_account_line_id = account_id = None - current_level = 0 - account_line_children_map = defaultdict(list) - root_account_groups = self.env['account.group'] - account_groups = self.env['account.group'] - hierarchy = create_hierarchy_dict() - - render_lines(root_account_groups, current_level, root_line_id, skip_no_group=False) - - return new_lines + total_lines - - #################################################### - # OPTIONS: prefix groups threshold - #################################################### - - def _init_options_prefix_groups_threshold(self, options, previous_options): - previous_threshold = previous_options.get('prefix_groups_threshold') - options['prefix_groups_threshold'] = self.prefix_groups_threshold - - #################################################### - # OPTIONS: fiscal position (multi vat) - #################################################### - - def _init_options_fiscal_position(self, options, previous_options): - if self.filter_fiscal_position and self.country_id and len(options['companies']) == 1: - vat_fpos_domain = [ - *self.env['account.fiscal.position']._check_company_domain(next(comp_id for comp_id in self.get_report_company_ids(options))), - ('foreign_vat', '!=', False), - ] - - vat_fiscal_positions = self.env['account.fiscal.position'].search([ - *vat_fpos_domain, - ('country_id', '=', self.country_id.id), - ]) - - options['allow_domestic'] = self.env.company.account_fiscal_country_id == self.country_id - - accepted_prev_vals = {*vat_fiscal_positions.ids} - if options['allow_domestic']: - accepted_prev_vals.add('domestic') - if len(vat_fiscal_positions) > (0 if options['allow_domestic'] else 1) or not accepted_prev_vals: - accepted_prev_vals.add('all') - - if previous_options.get('fiscal_position') in accepted_prev_vals: - # Legit value from previous options; keep it - options['fiscal_position'] = previous_options['fiscal_position'] - elif len(vat_fiscal_positions) == 1 and not options['allow_domestic']: - # Only one foreign fiscal position: always select it, menu will be hidden - options['fiscal_position'] = vat_fiscal_positions.id - else: - # Multiple possible values; by default, show the values of the company's area (if allowed), or everything - options['fiscal_position'] = options['allow_domestic'] and 'domestic' or 'all' - else: - # No country, or we're displaying data from several companies: disable fiscal position filtering - vat_fiscal_positions = [] - options['allow_domestic'] = True - previous_fpos = previous_options.get('fiscal_position') - options['fiscal_position'] = previous_fpos if previous_fpos in ('all', 'domestic') else 'all' - - options['available_vat_fiscal_positions'] = [{ - 'id': fiscal_pos.id, - 'name': fiscal_pos.name, - 'company_id': fiscal_pos.company_id.id, - } for fiscal_pos in vat_fiscal_positions] - - def _get_options_fiscal_position_domain(self, options): - def get_foreign_vat_tax_tag_extra_domain(fiscal_position=None): - # We want to gather any line wearing a tag, whatever its fiscal position. - # Nevertheless, if a country is using the same report for several regions (e.g. India) we need to exclude - # the lines from the other regions to avoid reporting numbers that don't belong to the current region. - fp_ids_to_exclude = self.env['account.fiscal.position'].search([ - ('id', '!=', fiscal_position.id if fiscal_position else False), - ('foreign_vat', '!=', False), - ('country_id', '=', self.country_id.id), - ]).ids - - if fiscal_position and fiscal_position.country_id == self.env.company.account_fiscal_country_id: - # We are looking for a fiscal position inside our country which means we need to exclude - # the local fiscal position which is represented by `False`. - fp_ids_to_exclude.append(False) - - return [ - ('tax_tag_ids.country_id', '=', self.country_id.id), - ('move_id.fiscal_position_id', 'not in', fp_ids_to_exclude), - ] - - fiscal_position_opt = options.get('fiscal_position') - - if fiscal_position_opt == 'domestic': - domain = [ - '|', - ('move_id.fiscal_position_id', '=', False), - ('move_id.fiscal_position_id.foreign_vat', '=', False), - ] - tax_tag_domain = get_foreign_vat_tax_tag_extra_domain() - return osv.expression.OR([domain, tax_tag_domain]) - - if isinstance(fiscal_position_opt, int): - # It's a fiscal position id - domain = [('move_id.fiscal_position_id', '=', fiscal_position_opt)] - fiscal_position = self.env['account.fiscal.position'].browse(fiscal_position_opt) - tax_tag_domain = get_foreign_vat_tax_tag_extra_domain(fiscal_position) - return osv.expression.OR([domain, tax_tag_domain]) - - # 'all', or option isn't specified - return [] - - #################################################### - # OPTIONS: MULTI COMPANY - #################################################### - - def _init_options_companies(self, options, previous_options): - if self.filter_multi_company == 'selector': - companies = self.env.companies - elif self.filter_multi_company == 'tax_units': - companies = self._multi_company_tax_units_init_options(options, previous_options=previous_options) - else: - # Multi-company is disabled for this report ; only accept the sub-branches of the current company from the selector - companies = self.env.company._accessible_branches() - - options['companies'] = [{'name': c.name, 'id': c.id, 'currency_id': c.currency_id.id} for c in companies] - - def _multi_company_tax_units_init_options(self, options, previous_options): - """ Initializes the companies option for reports configured to compute it from tax units. - """ - tax_units_domain = [('company_ids', 'in', self.env.company.id)] - - if self.country_id: - tax_units_domain.append(('country_id', '=', self.country_id.id)) - - available_tax_units = self.env['account.tax.unit'].search(tax_units_domain) - - # Filter available units to only consider the ones whose companies are all accessible to the user - available_tax_units = available_tax_units.filtered( - lambda x: all(unit_company in self.env.user.company_ids for unit_company in x.sudo().company_ids) - # sudo() to avoid bypassing companies the current user does not have access to - ) - - options['available_tax_units'] = [{ - 'id': tax_unit.id, - 'name': tax_unit.name, - 'company_ids': tax_unit.company_ids.ids - } for tax_unit in available_tax_units] - - # Available tax_unit option values that are currently allowed by the company selector - # A js hack ensures the page is reloaded and the selected companies modified - # when clicking on a tax unit option in the UI, so we don't need to worry about that here. - companies_authorized_tax_unit_opt = { - *(available_tax_units.filtered(lambda x: set(self.env.companies) == set(x.company_ids)).ids), - 'company_only' - } - - if previous_options.get('tax_unit') in companies_authorized_tax_unit_opt: - options['tax_unit'] = previous_options['tax_unit'] - - else: - # No tax_unit gotten from previous options; initialize it - # A tax_unit will be set by default if only one tax unit is available for the report - # (which should always be true for non-generic reports, which have a country), and the companies of - # the unit are the only ones currently selected. - if companies_authorized_tax_unit_opt == {'company_only'}: - options['tax_unit'] = 'company_only' - elif len(available_tax_units) == 1 and available_tax_units[0].id in companies_authorized_tax_unit_opt: - options['tax_unit'] = available_tax_units[0].id - else: - options['tax_unit'] = 'company_only' - - # Finally initialize multi_company filter - if options['tax_unit'] == 'company_only': - companies = self.env.company._get_branches_with_same_vat(accessible_only=True) - else: - tax_unit = available_tax_units.filtered(lambda x: x.id == options['tax_unit']) - companies = tax_unit.company_ids - - return companies - - #################################################### - # OPTIONS: MULTI CURRENCY - #################################################### - def _init_options_multi_currency(self, options, previous_options): - options['multi_currency'] = ( - any([company.get('currency_id') != options['companies'][0].get('currency_id') for company in options['companies']]) - or any([column.figure_type != 'monetary' for column in self.column_ids]) - or any(expression.figure_type and expression.figure_type != 'monetary' for expression in self.line_ids.expression_ids) - ) - - #################################################### - # OPTIONS: CURRENCY TABLE - #################################################### - def _init_options_currency_table(self, options, previous_options): - companies = self.env['res.company'].browse(self.get_report_company_ids(options)) - table_type = 'monocurrency' if self.env['res.currency']._check_currency_table_monocurrency(companies) else self.currency_translation - - periods = {} - for col_group in options['column_groups'].values(): - if col_group['forced_options'].get('no_impact_on_currency_table'): - # This key is used to ignore the colum group in the creation of the periods list for - # the currency table. This way, its dates won't influence. It's useful for groups corresponding - # to an initial balance of some sorts, like on the Trial Balance. - continue - - col_group_date = col_group['forced_options'].get('date', options['date']) - - col_group_date_from = col_group_date['date_from'] if col_group_date['mode'] == 'range' else None - col_group_date_to = col_group_date['date_to'] - period_key = col_group_date['currency_table_period_key'] - - already_present_period = periods.get(period_key) - if already_present_period: - # This can happen for custom reports, needing to enforce the same rates on multiple column groups with - # different dates (e.g. Trial Balance). In that case, the date_from and date_to of the currency table period must respectively - # be the lowest and highest among those groups. - if col_group_date_from and already_present_period['from'] > col_group_date_from: - already_present_period['from'] = col_group_date_from - - if already_present_period['to'] < col_group_date_to: - already_present_period['to'] = col_group_date_to - else: - periods[period_key] = { - 'from': col_group_date_from, - 'to': col_group_date_to, - } - - options['currency_table'] = {'type': table_type, 'periods': periods} - - @api.model - def _currency_table_apply_rate(self, value: SQL) -> SQL: - """ Returns an SQL term to use in a SELECT statement converting the value passed as parameter into the current company's currency, using the - currency table (which must be joined in the query as well ; using _currency_table_aml_join for account.move.line, or _get_currency_table for - other more specific uses). - """ - return SQL("(%(value)s) * COALESCE(account_currency_table.rate, 1)", value=value) - - @api.model - def _currency_table_aml_join(self, options, aml_alias=SQL('account_move_line')) -> SQL: - """ Returns the JOIN condition to the currency table in a query needing to use it to convert aml balances from one currency to another. - """ - if options['currency_table']['type'] == 'cta': - return SQL( - """ - JOIN account_account aml_ct_account - ON aml_ct_account.id = %(aml_table)s.account_id - LEFT JOIN %(currency_table)s - ON %(aml_table)s.company_id = account_currency_table.company_id - AND ( - account_currency_table.rate_type = CASE - WHEN aml_ct_account.account_type LIKE %(equity_prefix)s THEN 'historical' - WHEN aml_ct_account.account_type LIKE ANY (ARRAY[%(income_prefix)s, %(expense_prefix)s, 'equity_unaffected']) THEN 'average' - ELSE 'closing' - END - ) - AND (account_currency_table.date_from IS NULL OR account_currency_table.date_from <= %(aml_table)s.date) - AND (account_currency_table.date_next IS NULL OR account_currency_table.date_next > %(aml_table)s.date) - AND (account_currency_table.period_key = %(period_key)s OR account_currency_table.period_key IS NULL) - """, - aml_table=aml_alias, - equity_prefix='equity%', - income_prefix='income%', - expense_prefix='expense%', - currency_table=self._get_currency_table(options), - period_key=options['date']['currency_table_period_key'], - ) - - return SQL( - """ - JOIN %(currency_table)s - ON %(aml_table)s.company_id = account_currency_table.company_id - AND (account_currency_table.period_key = %(period_key)s OR account_currency_table.period_key IS NULL) - """, - aml_table=aml_alias, - currency_table=self._get_currency_table(options), - period_key=options['date']['currency_table_period_key'], - ) - - @api.model - def _get_currency_table(self, options) -> SQL: - """ Returns the currency table table definition to be injected in the JOIN condition of an SQL query needing to use it. - """ - if options['currency_table']['type'] == 'monocurrency': - companies = self.env['res.company'].browse(self.get_report_company_ids(options)) - return self.env['res.currency']._get_monocurrency_currency_table_sql(companies, use_cta_rates=options['currency_table']['type'] == 'cta') - - return SQL('account_currency_table') - - def _init_currency_table(self, options): - """ Creates the currency table temporary table if necessary, using the provided options to compute its periods. - This function should always be called before any query invovlving the currency table is run. - """ - if options['currency_table']['type'] != 'monocurrency': - companies = self.env['res.company'].browse(self.get_report_company_ids(options)) - - self.env['res.currency']._create_currency_table( - companies, - [(period_key, period['from'], period['to']) for period_key, period in options['currency_table']['periods'].items()], - use_cta_rates=options['currency_table']['type'] == 'cta', - ) - - #################################################### - # OPTIONS: ROUNDING UNIT - #################################################### - def _init_options_rounding_unit(self, options, previous_options): - default = 'decimals' - options['rounding_unit'] = previous_options.get('rounding_unit', default) - options['rounding_unit_names'] = self._get_rounding_unit_names() - - def _get_rounding_unit_names(self): - currency_symbol = self.env.company.currency_id.symbol - currency_name = self.env.company.currency_id.name - - rounding_unit_names = [ - ('decimals', (f'.{currency_symbol}', '')), - ('units', (f'{currency_symbol}', '')), - ('thousands', (f'K{currency_symbol}', _('Amounts in Thousands'))), - ('millions', (f'M{currency_symbol}', _('Amounts in Millions'))), - ] - - if currency_name in CURRENCIES_USING_LAKH: - rounding_unit_names.insert(3, ('lakhs', (f'L{currency_symbol}', _('Amounts in Lakhs')))) - - return dict(rounding_unit_names) - - # #################################################### - # OPTIONS: ALL ENTRIES - #################################################### - def _init_options_all_entries(self, options, previous_options): - if self.filter_show_draft: - options['all_entries'] = previous_options.get('all_entries', False) - else: - options['all_entries'] = False - - #################################################### - # OPTIONS: UNFOLDED LINES - #################################################### - def _init_options_unfolded(self, options, previous_options): - options['unfold_all'] = self.filter_unfold_all and previous_options.get('unfold_all', False) - - previous_section_source_id = previous_options.get('sections_source_id') - if not previous_section_source_id or previous_section_source_id == options['sections_source_id']: - # Only keep the unfolded lines if they belong to the same report or a section of the same report - options['unfolded_lines'] = previous_options.get('unfolded_lines', []) - else: - options['unfolded_lines'] = [] - - #################################################### - # OPTIONS: HIDE LINE AT 0 - #################################################### - def _init_options_hide_0_lines(self, options, previous_options): - if self.filter_hide_0_lines != 'never': - previous_val = previous_options.get('hide_0_lines') - if previous_val is not None: - options['hide_0_lines'] = previous_val - else: - options['hide_0_lines'] = self.filter_hide_0_lines == 'by_default' - else: - options['hide_0_lines'] = False - - def _filter_out_0_lines(self, lines): - """ Returns a list containing all lines that are not zero or that are parent to non-zero lines. - Can be used to ensure printed report does not include 0 lines, when hide_0_lines is toggled. - """ - lines_to_hide = set() # contain line ids to remove from lines - has_visible_children = set() # contain parent line ids - # Traverse lines in reverse to keep track of visible parent lines required by children lines - for line in reversed(lines): - is_zero_line = all(col.get('figure_type') not in NUMBER_FIGURE_TYPES or col.get('is_zero', True) for col in line['columns']) - if is_zero_line and line['id'] not in has_visible_children: - lines_to_hide.add(line['id']) - if line.get('parent_id') and line['id'] not in lines_to_hide: - has_visible_children.add(line['parent_id']) - return list(filter(lambda x: x['id'] not in lines_to_hide, lines)) - - #################################################### - # OPTIONS: HORIZONTAL GROUP - #################################################### - def _init_options_horizontal_groups(self, options, previous_options): - options['available_horizontal_groups'] = [ - { - 'id': horizontal_group.id, - 'name': horizontal_group.name, - } - for horizontal_group in self.horizontal_group_ids - ] - previous_selected = previous_options.get('selected_horizontal_group_id') - options['selected_horizontal_group_id'] = previous_selected if previous_selected in self.horizontal_group_ids.ids else None - - #################################################### - # OPTIONS: SEARCH BAR - #################################################### - def _init_options_search_bar(self, options, previous_options): - if self.search_bar: - options['search_bar'] = True - if 'default_filter_accounts' not in self._context and 'filter_search_bar' in previous_options: - options['filter_search_bar'] = previous_options['filter_search_bar'] - - #################################################### - # OPTIONS: COLUMN HEADERS - #################################################### - - def _init_options_column_headers(self, options, previous_options): - # Prepare column headers, in case the order of the comparison is ascending we reverse the order of the columns - all_comparison_date_vals = ([options['date']] + options.get('comparison', {}).get('periods', [])) - if options.get('comparison') and options['comparison']['period_order'] == 'ascending': - all_comparison_date_vals = all_comparison_date_vals[::-1] - - column_headers = [ - [ - { - 'name': comparison_date_vals['string'], - 'forced_options': {'date': comparison_date_vals}, - } - for comparison_date_vals in all_comparison_date_vals - ], # First level always consists of date comparison. Horizontal groupby are done on following levels. - ] - - # Handle horizontal groups - selected_horizontal_group_id = options.get('selected_horizontal_group_id') - if selected_horizontal_group_id: - horizontal_group = self.env['account.report.horizontal.group'].browse(selected_horizontal_group_id) - - for field_name, records in horizontal_group._get_header_levels_data(): - header_level = [ - { - 'name': record.display_name, - 'horizontal_groupby_element': {field_name: record.id}, - } - for record in records - ] - column_headers.append(header_level) - else: - # Insert budget column headers if needed - selected_budgets = [budget for budget in options.get('budgets', []) if budget['selected']] - if selected_budgets: - budget_headers = [{ - 'name': '', - 'forced_options': { - 'budget_base': True, - } - }] - - for budget in selected_budgets: - # Add budget amount column - budget_headers.append({ - 'name': budget['name'], - 'forced_options': { - 'compute_budget': budget['id'], - }, - 'colspan': 1, - }) - if len(self.column_ids.filtered(lambda column: column.figure_type == 'monetary')) == 1: - # Add budget percentage column (only if one column in the report) - budget_headers.append({ - 'name': "%", - 'forced_options': { - 'budget_percentage': budget['id'], - }, - 'colspan': 1, - }) - - column_headers.append(budget_headers) - - options['column_headers'] = column_headers - - #################################################### - # OPTIONS: COLUMNS - #################################################### - def _init_options_columns(self, options, previous_options): - default_group_vals = {'horizontal_groupby_element': {}, 'forced_options': {}} - all_column_group_vals_in_order = self._generate_columns_group_vals_recursively(options['column_headers'], default_group_vals) - - columns, column_groups = self._build_columns_from_column_group_vals(options, all_column_group_vals_in_order) - - options['columns'] = columns - options['column_groups'] = column_groups - - # Debug column is only shown when there is a single column group, so that we can display all the subtotals of the line in a clear way - options['show_debug_column'] = options['export_mode'] != 'print' \ - and self.env.user.has_group('base.group_no_one') \ - and len(options['column_groups']) == 1 \ - and len(self.line_ids) > 0 # No debug column on fully dynamic reports by default (they can customize this) - - # Show an additional column summing all the horizontal groups if there is no comparison and only one level of horizontal group - options['show_horizontal_group_total'] = options.get('selected_horizontal_group_id') \ - and options.get('comparison', {}).get('filter') == 'no_comparison' \ - and len(self.column_ids) == 1 \ - and len(options['column_headers']) == 2 - - def _generate_columns_group_vals_recursively(self, next_levels_headers, previous_levels_group_vals): - if next_levels_headers: - rslt = [] - for header_element in next_levels_headers[0]: - current_level_group_vals = {} - for key in previous_levels_group_vals: - current_level_group_vals[key] = {**previous_levels_group_vals.get(key, {}), **header_element.get(key, {})} - - rslt += self._generate_columns_group_vals_recursively(next_levels_headers[1:], current_level_group_vals) - return rslt - else: - return [previous_levels_group_vals] - - def _build_columns_from_column_group_vals(self, options, all_column_group_vals_in_order): - def _generate_domain_from_horizontal_group_hash_key_tuple(group_hash_key): - domain = [] - for field_name, field_value in group_hash_key: - domain.append((field_name, '=', field_value)) - return domain - - columns = [] - column_groups = {} - for column_group_val in all_column_group_vals_in_order: - horizontal_group_key_tuple = self._get_dict_hashable_key_tuple(column_group_val['horizontal_groupby_element']) # Empty tuple if no grouping - column_group_key = str(self._get_dict_hashable_key_tuple(column_group_val)) # Unique identifier for the column group - - column_groups[column_group_key] = { - 'forced_options': column_group_val['forced_options'], - 'forced_domain': _generate_domain_from_horizontal_group_hash_key_tuple(horizontal_group_key_tuple), - } - - # for budget, only one column in needed, regardless of the number of columns in the report - if any(budget_key in column_group_val['forced_options'] for budget_key in ('compute_budget', 'budget_percentage')): - columns.append({ - 'name': "", - 'column_group_key': column_group_key, - 'expression_label': 'balance', - 'sortable': False, - 'figure_type': 'monetary', - 'blank_if_zero': False, - 'style': "text-align: center; white-space: nowrap;", - }) - - else: - for report_column in self.column_ids: - columns.append({ - 'name': report_column.name, - 'column_group_key': column_group_key, - 'expression_label': report_column.expression_label, - 'sortable': report_column.sortable, - 'figure_type': report_column.figure_type, - 'blank_if_zero': report_column.blank_if_zero, - 'style': "text-align: center; white-space: nowrap;", - }) - - return columns, column_groups - - def _get_dict_hashable_key_tuple(self, dict_to_convert): - rslt = [] - for key, value in sorted(dict_to_convert.items()): - if isinstance(value, dict): - value = self._get_dict_hashable_key_tuple(value) - rslt.append((key, value)) - return tuple(rslt) - - #################################################### - # OPTIONS: BUTTONS - #################################################### - - def action_open_report_form(self, options, params): - return { - 'type': 'ir.actions.act_window', - 'res_model': 'account.report', - 'view_mode': 'form', - 'views': [(False, 'form')], - 'res_id': self.id, - } - - def _init_options_buttons(self, options, previous_options): - options['buttons'] = [ - {'name': _('PDF'), 'sequence': 10, 'action': 'export_file', 'action_param': 'export_to_pdf', 'file_export_type': _('PDF'), 'branch_allowed': True, 'always_show': True}, - {'name': _('XLSX'), 'sequence': 20, 'action': 'export_file', 'action_param': 'export_to_xlsx', 'file_export_type': _('XLSX'), 'branch_allowed': True, 'always_show': True}, - ] - - def open_account_report_file_download_error_wizard(self, errors, content): - self.ensure_one() - - model = 'account.report.file.download.error.wizard' - vals = {'actionable_errors': errors} - - if content: - vals['file_name'] = content['file_name'] - vals['file_content'] = base64.b64encode(re.sub(r'\n\s*\n', '\n', content['file_content']).encode()) - - return { - 'type': 'ir.actions.act_window', - 'res_model': model, - 'res_id': self.env[model].create(vals).id, - 'target': 'new', - 'views': [(False, 'form')], - } - - def get_export_mime_type(self, file_type): - """ Returns the MIME type associated with a report export file type, - for attachment generation. - """ - type_mapping = { - 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'pdf': 'application/pdf', - 'xml': 'application/xml', - 'xaf': 'application/vnd.sun.xml.writer', - 'txt': 'text/plain', - 'csv': 'text/csv', - 'zip': 'application/zip', - } - return type_mapping.get(file_type, False) - - def _init_options_section_buttons(self, options, previous_options): - """ In case we're displaying a section, we want to replace its buttons by its source report's. This needs to be done last, after calling the - custom handler, to avoid its _custom_options_initializer function to generate additional buttons. - """ - if options['sections_source_id'] != self.id: - # We need to re-call a full get_options in case a custom options initializer adds new buttons depending on other options. - # This way, we're sure we always get all buttons that are needed. - sections_source = self.env['account.report'].browse(options['sections_source_id']) - options['buttons'] = sections_source.get_options(previous_options={**options, 'no_report_reroute': True})['buttons'] - - #################################################### - # OPTIONS: VARIANTS - #################################################### - def _init_options_variants(self, options, previous_options): - allowed_variant_ids = set() - - previous_section_source_id = previous_options.get('sections_source_id') - if previous_section_source_id: - previous_section_source = self.env['account.report'].browse(previous_section_source_id) - if self in previous_section_source.section_report_ids: - options['variants_source_id'] = (previous_section_source.root_report_id or previous_section_source).id - allowed_variant_ids.add(previous_section_source_id) - - if 'variants_source_id' not in options: - options['variants_source_id'] = (self.root_report_id or self).id - - available_variants = self.env['account.report'] - options['has_inactive_variants'] = False - allowed_country_variant_ids = {} - all_variants = self._get_variants(options['variants_source_id']) - for variant in all_variants.filtered(lambda x: x._is_available_for(options)): - if not self.root_report_id and variant != self and variant.active: # Non-route reports don't reroute the variant when computing their options - allowed_variant_ids.add(variant.id) - if variant.country_id: - allowed_country_variant_ids.setdefault(variant.country_id.id, []).append(variant.id) - - if variant.active: - available_variants += variant - else: - options['has_inactive_variants'] = True - - options['available_variants'] = [ - { - 'id': variant.id, - 'name': variant.display_name, - 'country_id': variant.country_id.id, # To ease selection of default variant to open, without needing browsing again - } - for variant in sorted(available_variants, key=lambda x: (x.country_id and 1 or 0, x.sequence, x.id)) - ] - - previous_opt_report_id = previous_options.get('selected_variant_id') - if previous_opt_report_id in allowed_variant_ids or previous_opt_report_id == self.id: - options['selected_variant_id'] = previous_opt_report_id - elif allowed_country_variant_ids: - country_id = self.env.company.account_fiscal_country_id.id - report_id = (allowed_country_variant_ids.get(country_id) or next(iter(allowed_country_variant_ids.values())))[0] - options['selected_variant_id'] = report_id - else: - options['selected_variant_id'] = self.id - - def _get_variants(self, report_id): - source_report = self.env['account.report'].browse(report_id) - if source_report.root_report_id: - # We need to get the root report in order to get all variants - source_report = source_report.root_report_id - return source_report + source_report.with_context(active_test=False).variant_report_ids - - #################################################### - # OPTIONS: SECTIONS - #################################################### - def _init_options_sections(self, options, previous_options): - if options.get('selected_variant_id'): - options['sections_source_id'] = options['selected_variant_id'] - else: - options['sections_source_id'] = self.id - - source_report = self.env['account.report'].browse(options['sections_source_id']) - - available_sections = source_report.section_report_ids if source_report.use_sections else self.env['account.report'] - options['sections'] = [{'name': section.name, 'id': section.id} for section in available_sections] - - if available_sections: - section_id = previous_options.get('selected_section_id') - if not section_id or section_id not in available_sections.ids: - section_id = available_sections[0].id - - options['selected_section_id'] = section_id - - options['has_inactive_sections'] = bool(self.env['account.report'].with_context(active_test=False).search_count([ - ('section_main_report_ids', 'in', options['sections_source_id']), - ('active', '=', False) - ])) - - #################################################### - # OPTIONS: REPORT_ID - #################################################### - def _init_options_report_id(self, options, previous_options): - if previous_options.get('no_report_reroute'): - # Used for exports - options['report_id'] = self.id - else: - options['report_id'] = options.get('selected_section_id') or options.get('selected_variant_id') or self.id - - #################################################### - # OPTIONS: EXPORT MODE - #################################################### - def _init_options_export_mode(self, options, previous_options): - options['export_mode'] = previous_options.get('export_mode') - - #################################################### - # OPTIONS: HORIZONTAL SPLIT - #################################################### - def _init_options_horizontal_split(self, options, previous_options): - if any(line.horizontal_split_side for line in self.line_ids): - options['horizontal_split'] = previous_options.get('horizontal_split', False) - - #################################################### - # OPTIONS: CUSTOM - #################################################### - def _init_options_custom(self, options, previous_options): - custom_handler_model = self._get_custom_handler_model() - if custom_handler_model: - self.env[custom_handler_model]._custom_options_initializer(self, options, previous_options) - - #################################################### - # OPTIONS: INTEGER ROUNDING - #################################################### - def _init_options_integer_rounding(self, options, previous_options): - if self.integer_rounding: - options['integer_rounding'] = self.integer_rounding - if options.get('export_mode') == 'file': - options['integer_rounding_enabled'] = True - else: - options['integer_rounding_enabled'] = previous_options.get('integer_rounding_enabled', True) - return options - - #################################################### - # OPTIONS: BUDGETS - #################################################### - def _init_options_budgets(self, options, previous_options): - if self.filter_budgets: - previous_selection = {budget_option['id'] for budget_option in previous_options.get('budgets', []) if budget_option.get('selected')} - - options['budgets'] = [ - { - 'id': budget.id, - 'name': budget.name, - 'selected': budget.id in previous_selection, - 'company_id': budget.company_id.id, - } - for budget in self.env['account.report.budget'].search([('company_id', '=', self.env.company.id)]) - ] - options['show_all_accounts'] = previous_options.get('show_all_accounts') or False - - #################################################### - # OPTIONS: LOADING CALL - #################################################### - def _init_options_loading_call(self, options, previous_options): - """ Used by the js to know if it needs to reload the options (to not overwrite new options from the js) """ - options['loading_call_number'] = previous_options.get('loading_call_number') or 0 - return options - - #################################################### - # OPTIONS: READONLY QUERY - #################################################### - def _init_options_readonly_query(self, options, previous_options): - options['readonly_query'] = ( - options['currency_table']['type'] == 'monocurrency' - and not any(budget_opt['selected'] for budget_opt in options.get('budgets', [])) - ) - - #################################################### - # OPTIONS: CORE - #################################################### - - @api.readonly - def get_options(self, previous_options): - self.ensure_one() - - initializers_in_sequence = self._get_options_initializers_in_sequence() - - options = {} - - if previous_options.get('_running_export_test'): - options['_running_export_test'] = True - - # We need report_id to be initialized. Compute the necessary options to check for reroute. - for reroute_initializer_index, initializer in enumerate(initializers_in_sequence): - initializer(options, previous_options=previous_options) - - # pylint: disable=W0143 - if initializer == self._init_options_report_id: - break - - # Stop the computation to check for reroute once we have computed the necessary information - if (not self.root_report_id or (self.use_sections and self.section_report_ids)) and options['report_id'] != self.id: - # Load the variant/section instead of the root report - variant_options = {**previous_options} - for reroute_opt_key in ('selected_variant_id', 'selected_section_id', 'variants_source_id', 'sections_source_id'): - opt_val = options.get(reroute_opt_key) - if opt_val: - variant_options[reroute_opt_key] = opt_val - - return self.env['account.report'].browse(options['report_id']).get_options(variant_options) - - # No reroute; keep on and compute the other options - for initializer_index in range(reroute_initializer_index + 1, len(initializers_in_sequence)): - initializer = initializers_in_sequence[initializer_index] - initializer(options, previous_options=previous_options) - - # Sort the buttons list by sequence, for rendering - options_companies = self.env['res.company'].browse(self.get_report_company_ids(options)) - if not options_companies._all_branches_selected(): - for button in filter(lambda x: not x.get('branch_allowed'), options['buttons']): - button['error_action'] = 'show_error_branch_allowed' - - options['buttons'] = sorted(options['buttons'], key=lambda x: x.get('sequence', 90)) - - return options - - def _get_options_initializers_in_sequence(self): - """ Gets all filters in the right order to initialize them, so that each filter is - guaranteed to be after all of its dependencies in the resulting list. - - :return: a list of initializer functions, each accepting two parameters: - - options (mandatory): The options dictionary to be modified by this initializer to include its related option's data - - - previous_options (optional, defaults to None): A dict with default options values, coming from a previous call to the report. - These values can be considered or ignored on a case-by-case basis by the initializer, - depending on functional needs. - """ - initializer_prefix = '_init_options_' - initializers = [ - getattr(self, attr) for attr in dir(self) - if attr.startswith(initializer_prefix) - ] - - # Order them in a dependency-compliant way - forced_sequence_map = self._get_options_initializers_forced_sequence_map() - initializers.sort(key=lambda x: forced_sequence_map.get(x, forced_sequence_map.get('default'))) - - return initializers - - def _get_options_initializers_forced_sequence_map(self): - """ By default, not specific order is ensured for the filters when calling _get_options_initializers_in_sequence. - This function allows giving them a sequence number. It can be overridden - to make filters depend on each other. - - :return: dict(str, int): str is the filter name, int is its sequence (lowest = first). - Multiple filters may share the same sequence, their relative order is then not guaranteed. - """ - return { - self._init_options_companies: 10, - self._init_options_variants: 15, - self._init_options_sections: 16, - self._init_options_report_id: 17, - self._init_options_fiscal_position: 20, - self._init_options_date: 30, - self._init_options_horizontal_groups: 40, - self._init_options_comparison: 50, - self._init_options_export_mode: 60, - self._init_options_integer_rounding: 70, - - 'default': 200, - - self._init_options_column_headers: 990, - self._init_options_columns: 1000, - self._init_options_column_percent_comparison: 1010, - self._init_options_order_column: 1020, - self._init_options_hierarchy: 1030, - self._init_options_prefix_groups_threshold: 1040, - self._init_options_custom: 1050, - self._init_options_currency_table: 1055, - self._init_options_section_buttons: 1060, - self._init_options_readonly_query: 1070, - } - - def _get_options_domain(self, options, date_scope): - self.ensure_one() - - available_scopes = dict(self.env['account.report.expression']._fields['date_scope'].selection) - if date_scope and date_scope not in available_scopes: # date_scope can be passed to None explicitly to ignore the dates - raise UserError(_("Unknown date scope: %s", date_scope)) - - domain = [ - ('display_type', 'not in', ('line_section', 'line_note')), - ('company_id', 'in', self.get_report_company_ids(options)), - ] - if not options.get('compute_budget'): - domain += self._get_options_journals_domain(options) - if date_scope: - domain += self._get_options_date_domain(options, date_scope) - domain += self._get_options_partner_domain(options) - domain += self._get_options_all_entries_domain(options) - domain += self._get_options_unreconciled_domain(options) - domain += self._get_options_fiscal_position_domain(options) - domain += self._get_options_account_type_domain(options) - domain += self._get_options_aml_ir_filters(options) - - if self.only_tax_exigible: - domain += self.env['account.move.line']._get_tax_exigible_domain() - - if options.get('forced_domain'): - # That option key is set when splitting options between column groups - domain += options['forced_domain'] - - return domain - - #################################################### - # QUERIES - #################################################### - - def _get_report_query(self, options, date_scope, domain=None) -> Query: - """ Get a Query object that references the records needed for this report. """ - domain = self._get_options_domain(options, date_scope) + (domain or []) - - self.env['account.move.line'].check_access('read') - - query = self.env['account.move.line']._where_calc(domain) - - if options.get('compute_budget'): - self._create_report_budget_temp_table(options) - query._tables['account_move_line'] = SQL.identifier('account_report_budget_temp_aml') - query.add_where(SQL( - "%s AND budget_id = %s", - query.where_clause, - options['compute_budget'], - )) - - # Wrap the query with 'company_id IN (...)' to avoid bypassing company access rights. - self.env['account.move.line']._apply_ir_rules(query) - - return query - - def _create_report_budget_temp_table(self, options): - self._cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='account_report_budget_temp_aml'") - if self._cr.fetchone(): - return - - stored_aml_fields, fields_to_insert = self.env['account.move.line']._prepare_aml_shadowing_for_report({ - 'id': SQL.identifier("id"), - 'balance': SQL.identifier('amount'), - 'company_id': self.env.company.id, - 'parent_state': 'posted', - 'date': SQL.identifier('date'), - 'account_id': SQL.identifier("account_id"), - 'debit': SQL("CASE WHEN (amount > 0) THEN amount else 0 END"), - 'credit': SQL("CASE WHEN (amount < 0) THEN -amount else 0 END"), - }) - - self._cr.execute(SQL( - """ - -- Create a temporary table, dropping not null constraints because we're not filling those columns - CREATE TEMPORARY TABLE IF NOT EXISTS account_report_budget_temp_aml () inherits (account_move_line) ON COMMIT DROP; - ALTER TABLE account_report_budget_temp_aml NO INHERIT account_move_line; - ALTER TABLE account_report_budget_temp_aml ALTER COLUMN move_id DROP NOT NULL; - ALTER TABLE account_report_budget_temp_aml ALTER COLUMN currency_id DROP NOT NULL; - ALTER TABLE account_report_budget_temp_aml ALTER COLUMN journal_id DROP NOT NULL; - ALTER TABLE account_report_budget_temp_aml ALTER COLUMN display_type DROP NOT NULL; - ALTER TABLE account_report_budget_temp_aml ADD budget_id INTEGER NOT NULL; - - INSERT INTO account_report_budget_temp_aml (%(stored_aml_fields)s, budget_id) - SELECT %(fields_to_insert)s, budget_id - FROM account_report_budget_item - WHERE budget_id IN %(available_budget_ids)s; - - -- Create a supporting index to avoid seq.scans - CREATE INDEX IF NOT EXISTS account_report_budget_temp_aml__composite_idx ON account_report_budget_temp_aml (account_id, journal_id, date, company_id); - -- Update statistics for correct planning - ANALYZE account_report_budget_temp_aml - """, - stored_aml_fields=stored_aml_fields, - fields_to_insert=fields_to_insert, - available_budget_ids=tuple(budget_option['id'] for budget_option in options['budgets']), - )) - - if options.get('show_all_accounts'): - stored_aml_fields, fields_to_insert = self.env['account.move.line']._prepare_aml_shadowing_for_report({ - # Using nextval will consume a sequence number, we decide to do it to avoid comparing apples and oranges - 'id': SQL("(SELECT nextval('account_report_budget_item_id_seq'))"), - 'balance': SQL("0"), - 'company_id': self.env.company.id, - 'parent_state': 'posted', - 'date': SQL("%s", options['date']['date_from']), - 'account_id': SQL.identifier("accounts", "id"), - 'debit': SQL("0"), - 'credit': SQL("0"), - }) - accounts_subquery = self.env['account.account']._where_calc([ - ('company_ids', 'in', self.get_report_company_ids(options)), - ('internal_group', 'in', ['income', 'expense']), - ]) - self._cr.execute(SQL( - """ - -- Insert dynamic combinations of account_id and budget_id into the temporary table - INSERT INTO account_report_budget_temp_aml (%(stored_aml_fields)s, budget_id) - SELECT %(fields_to_insert)s, budgets.id AS budget_id - FROM (%(accounts_subquery)s) AS accounts - CROSS JOIN ( - SELECT id - FROM account_report_budget - WHERE id IN %(available_budget_ids)s - ) AS budgets - """, - stored_aml_fields=stored_aml_fields, - fields_to_insert=fields_to_insert, - accounts_subquery=accounts_subquery.select(), - available_budget_ids=tuple(budget_option['id'] for budget_option in options['budgets']), - income='income%', - expense='expense%', - company_ids=tuple(), - )) - - #################################################### - # LINE IDS MANAGEMENT HELPERS - #################################################### - def _get_generic_line_id(self, model_name, value, markup=None, parent_line_id=None): - """ Generates a generic line id from the provided parameters. - - Such a generic id consists of a string repeating 1 to n times the following pattern: - markup-model-value, each occurence separated by a LINE_ID_HIERARCHY_DELIMITER character from the previous one. - - Each pattern corresponds to a level of hierarchy in the report, so that - the n-1 patterns starting the id of a line actually form the id of its generator line. - EX: a~b~c|d~e~f|g~h~i => This line is a subline generated by a~b~c|d~e~f where | is the LINE_ID_HIERARCHY_DELIMITER. - - Each pattern consists of the three following elements: - - markup: a (possibly empty) free string or json-formatted dict allowing finer identification of the line - (like the name of the field for account.accounting.reports) - - - model: the model this line has been generated for, or an empty string if there is none - - - value: the groupby value for this line (typically the id of a record - or the value of a field), or an empty string if there isn't any. - """ - self.ensure_one() - - if parent_line_id: - parent_id_list = self._parse_line_id(parent_line_id, markup_as_string=True) - else: - parent_id_list = [(None, 'account.report', self.id)] - - # In case the markup is a dict, it must be converted to a string, but in a way such that the keys are ordered alphabetically. - # This is useful, notably for annotations where the ids of the lines are stored, therefore requiring a consistent ordering - if isinstance(markup, dict): - markup = json.dumps(markup, sort_keys=True) - - new_line = self._build_line_id(parent_id_list + [(markup, model_name, value)]) - return new_line - - @api.model - def _get_model_info_from_id(self, line_id): - """ Parse the provided generic report line id. - - :param line_id: the report line id (i.e. markup~model~value|markup2~model2~value2 where | is the LINE_ID_HIERARCHY_DELIMITER) - :return: tuple(model, id) of the report line. Each of those values can be None if the id contains no information about them. - """ - last_id_tuple = self._parse_line_id(line_id)[-1] - return last_id_tuple[-2:] - - @api.model - def _build_line_id(self, current): - """ Build a generic line id string from its list representation, converting - the None values for model and value to empty strings. - :param current (list): list of tuple(markup, model, value) - """ - def convert_none(x): - return x if x is not None and x is not False else '' - return LINE_ID_HIERARCHY_DELIMITER.join(f'{convert_none(markup)}~{convert_none(model)}~{convert_none(value)}' for markup, model, value in current) - - @api.model - def _build_parent_line_id(self, current): - """Build the parent_line id based on the current position in the report. - - For instance, if current is [('markup1', 'account.account', 5), ('markup2', 'res.partner', 8)], it will return - markup1~account.account~5 - :param current (list): list of tuple(markup, model, value) - """ - return self._build_line_id(current[:-1]) - - @api.model - def _parse_markup(self, markup): - if not markup: - return markup - try: - result = json.loads(markup) - except json.JSONDecodeError: # the markup is not a JSON object - return markup - if isinstance(result, dict): - return result - - return markup - - @api.model - def _parse_line_id(self, line_id, markup_as_string=False): - """Parse the provided string line id and convert it to its list representation. - Empty strings for model and value will be converted to None. - - For instance if line_id is markup1~account.account~5|markup2~res.partner~8 (where | is the LINE_ID_HIERARCHY_DELIMITER), - it will return [('markup1', 'account.account', 5), ('markup2', 'res.partner', 8)] - :param line_id (str): the generic line id to parse - """ - return line_id and [ - # When there is a model, value is an id, so we cast it to and int. Else, we keep the original value (for groupby lines on - # non-relational fields, for example). - (self._parse_markup(markup) if not markup_as_string else markup, model or None, int(value) if model and value else (value or None)) - for markup, model, value in (key.rsplit('~', 2) for key in line_id.split(LINE_ID_HIERARCHY_DELIMITER)) - ] or [] - - @api.model - def _get_unfolded_lines(self, lines, parent_line_id): - """ Return a list of all children lines for specified parent_line_id. - NB: It will return the parent_line itself! - - For instance if parent_line_ids is '~account.report.line~84|{"groupby": "currency_id"}~res.currency~174' - (where | is the LINE_ID_HIERARCHY_DELIMITER), it will return every subline for this currency. - :param lines: list of report lines - :param parent_line_id: id of a specified line - :return: A list of all children lines for a specified parent_line_id - """ - return [ - line for line in lines - if line['id'].startswith(parent_line_id) - ] - - @api.model - def _get_res_id_from_line_id(self, line_id, target_model_name): - """ Parses the provided generic line id and returns the most local (i.e. the furthest on the right) record id it contains which - corresponds to the provided model name. If line_id does not contain anything related to target_model_name, None will be returned. - - For example, parsing ~account.move~1|~res.partner~2|~account.move~3 (where | is the LINE_ID_HIERARCHY_DELIMITER) - with target_model_name='account.move' will return 3. - """ - dict_result = self._get_res_ids_from_line_id(line_id, [target_model_name]) - return dict_result[target_model_name] if dict_result else None - - - @api.model - def _get_res_ids_from_line_id(self, line_id, target_model_names): - """ Parses the provided generic line id and returns the most local (i.e. the furthest on the right) record ids it contains which - correspond to the provided model names, in the form {model_name: res_id}. If a model is not present in line_id, its model will be absent - from the resulting dict. - - For example, parsing ~account.move~1|~res.partner~2|~account.move~3 with target_model_names=['account.move', 'res.partner'] will return - {'account.move': 3, 'res.partner': 2}. - """ - result = {} - models_to_find = set(target_model_names) - for dummy, model, value in reversed(self._parse_line_id(line_id)): - if model in models_to_find: - result[model] = value - models_to_find.remove(model) - - return result - - @api.model - def _get_markup(self, line_id): - """ Directly returns the markup associated with the provided line_id. - """ - return self._parse_line_id(line_id)[-1][0] if line_id else None - - def _build_subline_id(self, parent_line_id, subline_id_postfix): - """ Creates a new subline id by concatanating parent_line_id with the provided id postfix. - """ - return f"{parent_line_id}{LINE_ID_HIERARCHY_DELIMITER}{subline_id_postfix}" - - #################################################### - # CARET OPTIONS MANAGEMENT - #################################################### - - def _get_caret_options(self): - if self.custom_handler_model_id: - return self.env[self.custom_handler_model_name]._caret_options_initializer() - return self._caret_options_initializer_default() - - def _caret_options_initializer_default(self): - return { - 'account.account': [ - {'name': _("General Ledger"), 'action': 'caret_option_open_general_ledger'}, - ], - - 'account.move': [ - {'name': _("View Journal Entry"), 'action': 'caret_option_open_record_form'}, - ], - - 'account.move.line': [ - {'name': _("View Journal Entry"), 'action': 'caret_option_open_record_form', 'action_param': 'move_id'}, - ], - - 'account.payment': [ - {'name': _("View Payment"), 'action': 'caret_option_open_record_form', 'action_param': 'payment_id'}, - ], - - 'account.bank.statement': [ - {'name': _("View Bank Statement"), 'action': 'caret_option_open_statement_line_reco_widget'}, - ], - - 'res.partner': [ - {'name': _("View Partner"), 'action': 'caret_option_open_record_form'}, - ], - } - - def caret_option_open_record_form(self, options, params): - model, record_id = self._get_model_info_from_id(params['line_id']) - record = self.env[model].browse(record_id) - target_record = record[params['action_param']] if 'action_param' in params else record - - view_id = self._resolve_caret_option_view(target_record) - - action = { - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'views': [(view_id, 'form')], # view_id will be False in case the default view is needed - 'res_model': target_record._name, - 'res_id': target_record.id, - 'context': self._context, - } - - if view_id is not None: - action['view_id'] = view_id - - return action - - def _get_caret_option_view_map(self): - return { - 'account.payment': 'account.view_account_payment_form', - 'res.partner': 'base.view_partner_form', - 'account.move': 'account.view_move_form', - } - - def _resolve_caret_option_view(self, target): - '''Retrieve the target view of the caret option. - - :param target: The target record of the redirection. - :return: The id of the target view. - ''' - view_map = self._get_caret_option_view_map() - - view_xmlid = view_map.get(target._name) - if not view_xmlid: - return None - - return self.env['ir.model.data']._xmlid_lookup(view_xmlid)[1] - - def caret_option_open_general_ledger(self, options, params): - # When coming from a specific account, the unfold must only be retained - # on the specified account. Better performance and more ergonomic - # as it opens what client asked. And "Unfold All" is 1 clic away. - options["unfold_all"] = False - - records_to_unfold = [] - for _dummy, model, record_id in self._parse_line_id(params['line_id']): - if model in ('account.group', 'account.account'): - records_to_unfold.append((model, record_id)) - - if not records_to_unfold or records_to_unfold[-1][0] != 'account.account': - raise UserError(_("'Open General Ledger' caret option is only available form report lines targetting accounts.")) - - general_ledger = self.env.ref('at_accounting.general_ledger_report') - lines_to_unfold = [] - for model, record_id in records_to_unfold: - parent_line_id = lines_to_unfold[-1] if lines_to_unfold else None - # Re-create the hierarchy of account groups that should be unfolded in GL - generic_line_id = general_ledger._get_generic_line_id(model, record_id, parent_line_id=parent_line_id) - lines_to_unfold.append(generic_line_id) - - options['not_reset_journals_filter'] = True # prevents resetting the default journal group - gl_options = general_ledger.get_options(options) - gl_options['not_reset_journals_filter'] = True # prevents resetting the default journal group - gl_options['unfolded_lines'] = lines_to_unfold - - account_id = self.env['account.account'].browse(records_to_unfold[-1][1]) - action_vals = self.env['ir.actions.actions']._for_xml_id('at_accounting.action_account_report_general_ledger') - action_vals['params'] = { - 'options': gl_options, - 'ignore_session': True, - } - action_vals['context'] = dict(ast.literal_eval(action_vals['context']), default_filter_accounts=account_id.code) - - return action_vals - - def caret_option_open_statement_line_reco_widget(self, options, params): - model, record_id = self._get_model_info_from_id(params['line_id']) - record = self.env[model].browse(record_id) - if record._name == 'account.bank.statement.line': - return record.action_open_recon_st_line() - elif record._name == 'account.bank.statement': - return record.action_open_bank_reconcile_widget() - raise UserError(_("'View Bank Statement' caret option is only available for report lines targeting bank statements.")) - - #################################################### - # MISC - #################################################### - - def _get_custom_handler_model(self): - """ Check whether the current report has a custom handler and if it does, return its name. - Otherwise, try to fall back on the root report. - """ - return self.custom_handler_model_name or self.root_report_id.custom_handler_model_name or None - - def dispatch_report_action(self, options, action, action_param=None, on_sections_source=False): - """ Dispatches calls made by the client to either the report itself, or its custom handler if it exists. - The action should be a public method, by definition, but a check is made to make sure - it is not trying to call a private method. - """ - self.ensure_one() - - if on_sections_source: - report_to_call = self.env['account.report'].browse(options['sections_source_id']) - return report_to_call.dispatch_report_action(options, action, action_param=action_param, on_sections_source=False) - - if self.id not in (options['report_id'], options.get('sections_source_id')): - raise UserError(_("Trying to dispatch an action on a report not compatible with the provided options.")) - - check_method_name(action) - args = [options, action_param] if action_param is not None else [options] - custom_handler_model = self._get_custom_handler_model() - if custom_handler_model and hasattr(self.env[custom_handler_model], action): - return getattr(self.env[custom_handler_model], action)(*args) - return getattr(self, action)(*args) - - def _get_custom_report_function(self, function_name, prefix): - """ Returns a report function from its name, first checking it to ensure it's private (and raising if it isn't). - This helper is used by custom report fields containing function names. - The function will be called on the report's custom handler if it exists, or on the report itself otherwise. - """ - self.ensure_one() - function_name_prefix = f'_report_{prefix}_' - if not function_name.startswith(function_name_prefix): - raise UserError(_("Method '%(method_name)s' must start with the '%(prefix)s' prefix.", method_name=function_name, prefix=function_name_prefix)) - - if self.custom_handler_model_id: - handler = self.env[self.custom_handler_model_name] - if hasattr(handler, function_name): - return getattr(handler, function_name) - - if not hasattr(self, function_name): - raise UserError(_("Invalid method “%s”", function_name)) - # Call the check method without the private prefix to check for others security risks. - return getattr(self, function_name) - - def _get_lines(self, options, all_column_groups_expression_totals=None, warnings=None): - self.ensure_one() - - if options['report_id'] != self.id: - # Should never happen; just there to prevent BIG issues and directly spot them - raise UserError(_("Inconsistent report_id in options dictionary. Options says %(options_report)s; report is %(report)s.", options_report=options['report_id'], report=self.id)) - - # Necessary to ensure consistency of the data if some of them haven't been written in database yet - self.env.flush_all() - - if warnings is not None: - self._generate_common_warnings(options, warnings) - - # Merge static and dynamic lines in a common list - if all_column_groups_expression_totals is None: - self._init_currency_table(options) - all_column_groups_expression_totals = self._compute_expression_totals_for_each_column_group( - self.line_ids.expression_ids, - options, - warnings=warnings, - ) - - dynamic_lines = self._get_dynamic_lines(options, all_column_groups_expression_totals, warnings=warnings) - - lines = [] - line_cache = {} # {report_line: report line dict} - hide_if_zero_lines = self.env['account.report.line'] - - # There are two types of lines: - # - static lines: the ones generated from self.line_ids - # - dynamic lines: the ones generated from a call to the functions referred to by self.dynamic_lines_generator - # This loops combines both types of lines together within the lines list - for line in self.line_ids: # _order ensures the sequence of the lines - # Inject all the dynamic lines whose sequence is inferior to the next static line to add - while dynamic_lines and line.sequence > dynamic_lines[0][0]: - lines.append(dynamic_lines.pop(0)[1]) - parent_generic_id = line_cache[line.parent_id]['id'] if line.parent_id else None # The parent line has necessarily been treated in a previous iteration - line_dict = self._get_static_line_dict(options, line, all_column_groups_expression_totals, parent_id=parent_generic_id) - line_cache[line] = line_dict - - if line.hide_if_zero: - hide_if_zero_lines += line - - lines.append(line_dict) - - for dummy, left_dynamic_line in dynamic_lines: - lines.append(left_dynamic_line) - - # Manage growth comparison - if options.get('column_percent_comparison') == 'growth': - for line in lines: - first_value, second_value = line['columns'][0]['no_format'], line['columns'][1]['no_format'] - - green_on_positive = True - model, line_id = self._get_model_info_from_id(line['id']) - - if model == 'account.report.line' and line_id: - report_line = self.env['account.report.line'].browse(line_id) - compared_expression = report_line.expression_ids.filtered( - lambda expr: expr.label == line['columns'][0]['expression_label'] - ) - green_on_positive = compared_expression.green_on_positive - - line['column_percent_comparison_data'] = self._compute_column_percent_comparison_data( - options, first_value, second_value, green_on_positive=green_on_positive - ) - # Manage budget comparison - elif options.get('column_percent_comparison') == 'budget': - for line in lines: - self._set_budget_column_comparisons(options, line) - - # Manage hide_if_zero lines: - # - If they have column values: hide them if all those values are 0 (or empty) - # - If they don't: hide them if all their children's column values are 0 (or empty) - # Also, hide all the children of a hidden line. - hidden_lines_dict_ids = set() - for line in hide_if_zero_lines: - children_to_check = line - current = line - while current: - children_to_check |= current - current = current.children_ids - - all_children_zero = True - hide_candidates = set() - for child in children_to_check: - child_line_dict_id = line_cache[child]['id'] - - if child_line_dict_id in hidden_lines_dict_ids: - continue - elif all(col.get('is_zero', True) for col in line_cache[child]['columns']): - hide_candidates.add(child_line_dict_id) - else: - all_children_zero = False - break - - if all_children_zero: - hidden_lines_dict_ids |= hide_candidates - - lines[:] = filter(lambda x: x['id'] not in hidden_lines_dict_ids and x.get('parent_id') not in hidden_lines_dict_ids, lines) - - # Create the hierarchy of lines if necessary - if options.get('hierarchy'): - lines = self._create_hierarchy(lines, options) - - # Handle totals below sections for static lines - lines = self._add_totals_below_sections(lines, options) - - # Unfold lines (static or dynamic) if necessary and add totals below section to dynamic lines - lines = self._fully_unfold_lines_if_needed(lines, options) - - if self.custom_handler_model_id: - lines = self.env[self.custom_handler_model_name]._custom_line_postprocessor(self, options, lines) - - if warnings is not None: - custom_handler_name = self.custom_handler_model_name or self.root_report_id.custom_handler_model_name - if custom_handler_name: - self.env[custom_handler_name]._customize_warnings(self, options, all_column_groups_expression_totals, warnings) - - # Format values in columns of lines that will be displayed - self._format_column_values(options, lines) - - if options.get('export_mode') == 'print' and options.get('hide_0_lines'): - lines = self._filter_out_0_lines(lines) - - return lines - - @api.model - def format_column_values(self, options, lines): - self._format_column_values(options, lines, force_format=True) - - return lines - - def _format_column_values(self, options, line_dict_list, force_format=False): - for line_dict in line_dict_list: - for column_dict in line_dict['columns']: - if 'name' in column_dict and not force_format: - # Columns which have already received a name are assumed to be already formatted; nothing needs to be done for them. - # This gives additional flexibility to custom reports, if needed. - continue - - if not column_dict: - continue - elif column_dict.get('is_zero') and column_dict.get('blank_if_zero'): - rslt = '' - else: - rslt = self.format_value( - options, - column_dict.get('no_format'), - column_dict.get('figure_type'), - format_params=column_dict.get('format_params'), - ) - - column_dict['name'] = rslt - - # Handle the total in case of an horizontal group when there is no comparison and only one level of horizontal group - if options.get('show_horizontal_group_total'): - # In case the line has no formula - if all(column['no_format'] is None for column in line_dict['columns']): - continue - # In case total below section, some line don't have the value displayed - if self.env.company.totals_below_sections and not options.get('ignore_totals_below_sections') and line_dict['unfolded']: - continue - - figure_type_is_valid = all(column['figure_type'] in {'float', 'integer', 'monetary'} for column in line_dict['columns']) - total_value = sum(column["no_format"] for column in line_dict['columns']) if figure_type_is_valid else None - line_dict['horizontal_group_total_data'] = { - 'name': self.format_value( - options, - total_value, - line_dict['columns'][0]['figure_type'], - format_params=line_dict['columns'][0]['format_params'], - ), - 'no_format': total_value, - } - - def _generate_common_warnings(self, options, warnings): - # Display a warning if we're displaying only the data of the current company, but it's also part of a tax unit - if options.get('available_tax_units') and options['tax_unit'] == 'company_only': - warnings['at_accounting.common_warning_tax_unit'] = {} - - report_company_ids = self.get_report_company_ids(options) - # The _accessible_branches function will return the accessible branches from the ones that are already selected, - # and the report_company_ids function will return the current company and its branches (that are selected) with the same VAT - # or tax unit. Therefore, we will display the warning only when the selected companies do not have the same VAT - # and in the context of branches. - if self.filter_multi_company == 'tax_units' and any(accessible_branch.id not in report_company_ids for accessible_branch in self.env.company._accessible_branches()): - warnings['at_accounting.tax_report_warning_tax_id_selected_companies'] = {'alert_type': 'warning'} - - # Check whether there are unposted entries for the selected period or not (if the report allows it) - if options.get('date') and options.get('all_entries') is not None: - if self.env['account.move'].search_count( - [('state', '=', 'draft'), ('date', '<=', options['date']['date_to'])], - limit=1, - ): - warnings['at_accounting.common_warning_draft_in_period'] = {} - - def _fully_unfold_lines_if_needed(self, lines, options): - def line_need_expansion(line_dict): - return line_dict.get('unfolded') and line_dict.get('expand_function') - - custom_unfold_all_batch_data = None - - # If it's possible to batch unfold and we're unfolding all lines, compute the batch, so that individual expansions are more efficient - if options['unfold_all'] and self.custom_handler_model_id: - lines_to_expand_by_function = {} - for line_dict in lines: - if line_need_expansion(line_dict): - lines_to_expand_by_function.setdefault(line_dict['expand_function'], []).append(line_dict) - - custom_unfold_all_batch_data = self.env[self.custom_handler_model_name]._custom_unfold_all_batch_data_generator(self, options, lines_to_expand_by_function) - - i = 0 - while i < len(lines): - # We iterate in such a way that if the lines added by an expansion need expansion, they will get it as well - line_dict = lines[i] - if line_need_expansion(line_dict): - groupby = line_dict.get('groupby') - progress = line_dict.get('progress') - to_insert = self._expand_unfoldable_line( - line_dict['expand_function'], line_dict['id'], groupby, options, progress, 0, line_dict.get('horizontal_split_side'), - unfold_all_batch_data=custom_unfold_all_batch_data, - ) - lines = lines[:i+1] + to_insert + lines[i+1:] - i += 1 - - return lines - - def _generate_total_below_section_line(self, section_line_dict): - return { - **section_line_dict, - 'id': self._get_generic_line_id(None, None, parent_line_id=section_line_dict['id'], markup='total'), - 'level': section_line_dict['level'] if section_line_dict['level'] != 0 else 1, # Total line should not be level 0 - 'name': _("Total %s", section_line_dict['name']), - 'parent_id': section_line_dict['id'], - 'unfoldable': False, - 'unfolded': False, - 'caret_options': None, - 'action_id': None, - 'page_break': False, # If the section's line possesses a page break, we don't want the total to have it. - } - - def _get_static_line_dict(self, options, line, all_column_groups_expression_totals, parent_id=None): - line_id = self._get_generic_line_id('account.report.line', line.id, parent_line_id=parent_id) - columns = self._build_static_line_columns(line, options, all_column_groups_expression_totals) - has_children = (any(col['has_sublines'] for col in columns) or bool(line.children_ids)) - groupby = line._get_groupby(options) - - rslt = { - 'id': line_id, - 'name': line.name, - 'groupby': groupby, - 'unfoldable': line.foldable and has_children, - 'unfolded': bool((not line.foldable and (line.children_ids or groupby)) or line_id in options['unfolded_lines']) or (has_children and options['unfold_all']), - 'columns': columns, - 'level': line.hierarchy_level, - 'page_break': line.print_on_new_page, - 'action_id': line.action_id.id, - 'expand_function': groupby and '_report_expand_unfoldable_line_with_groupby' or None, - } - - if line.horizontal_split_side: - rslt['horizontal_split_side'] = line.horizontal_split_side - - if parent_id: - rslt['parent_id'] = parent_id - - if options['export_mode'] == 'file': - rslt['code'] = line.code - - if options['show_debug_column']: - first_group_key = list(options['column_groups'].keys())[0] - column_group_totals = all_column_groups_expression_totals[first_group_key] - # Only consider the first column group, as show_debug_column is only true if there is but one. - - engine_selection_labels = dict(self.env['account.report.expression']._fields['engine']._description_selection(self.env)) - expressions_detail = defaultdict(lambda: []) - col_expression_to_figure_type = { - column.get('expression_label'): column.get('figure_type') for column in options['columns'] - } - for expression in line.expression_ids.filtered(lambda x: not x.label.startswith('_default')): - engine_label = engine_selection_labels[expression.engine] - figure_type = expression.figure_type or col_expression_to_figure_type.get(expression.label) or 'none' - expressions_detail[engine_label].append(( - expression.label, - {'formula': expression.formula, 'subformula': expression.subformula, 'value': self.format_value(options, column_group_totals[expression]['value'], figure_type)} - )) - - # Sort results so that they can be rendered nicely in the UI - for details in expressions_detail.values(): - details.sort(key=lambda x: x[0]) - sorted_expressions_detail = sorted(expressions_detail.items(), key=lambda x: x[0]) - - if sorted_expressions_detail: - try: - rslt['debug_popup_data'] = json.dumps({'expressions_detail': sorted_expressions_detail}) - except TypeError: - raise UserError(_( - 'Invalid subformula in expression "%(expression)s" of line "%(line)s": %(subformula)s', - expression=expression.label, - line=expression.report_line_id.name, - subformula=expression.subformula, - )) - return rslt - - @api.model - def _build_static_line_columns(self, line, options, all_column_groups_expression_totals, groupby_model=None): - line_expressions_map = {expr.label: expr for expr in line.expression_ids} - columns = [] - for column_data in options['columns']: - col_group_key = column_data['column_group_key'] - current_group_expression_totals = all_column_groups_expression_totals[col_group_key] - target_line_res_dict = {expr.label: current_group_expression_totals[expr] for expr in line.expression_ids if not expr.label.startswith('_default')} - - column_expr_label = column_data['expression_label'] - column_res_dict = target_line_res_dict.get(column_expr_label, {}) - column_value = column_res_dict.get('value') - column_has_sublines = column_res_dict.get('has_sublines', False) - column_expression = line_expressions_map.get(column_expr_label, self.env['account.report.expression']) - figure_type = column_expression.figure_type or column_data['figure_type'] - - # Handle info popup - info_popup_data = {} - - # Check carryover - carryover_expr_label = '_carryover_%s' % column_expr_label - carryover_value = target_line_res_dict.get(carryover_expr_label, {}).get('value', 0) - if self.env.company.currency_id.compare_amounts(0, carryover_value) != 0: - info_popup_data['carryover'] = self._format_value(options, carryover_value, 'monetary') - - carryover_expression = line_expressions_map[carryover_expr_label] - if carryover_expression.carryover_target: - info_popup_data['carryover_target'] = carryover_expression._get_carryover_target_expression(options).report_line_name - # If it's not set, it means the carryover needs to target the same expression - - applied_carryover_value = target_line_res_dict.get('_applied_carryover_%s' % column_expr_label, {}).get('value', 0) - if self.env.company.currency_id.compare_amounts(0, applied_carryover_value) != 0: - info_popup_data['applied_carryover'] = self._format_value(options, applied_carryover_value, 'monetary') - info_popup_data['allow_carryover_audit'] = self.env.user.has_group('base.group_no_one') - info_popup_data['expression_id'] = line_expressions_map['_applied_carryover_%s' % column_expr_label]['id'] - info_popup_data['column_group_key'] = col_group_key - - # Handle manual edition popup - edit_popup_data = {} - formatter_params = {} - if column_expression.engine == 'external' and column_expression.subformula \ - and len(options['companies']) == 1 \ - and (not options['available_vat_fiscal_positions'] or options['fiscal_position'] != 'all'): - - # Compute rounding for manual values - rounding = None - if figure_type == 'integer': - rounding = 0 - else: - rounding_opt_match = re.search(r"\Wrounding\W*=\W*(?P\d+)", column_expression.subformula) - if rounding_opt_match: - rounding = int(rounding_opt_match.group('rounding')) - elif figure_type == 'monetary': - rounding = self.env.company.currency_id.decimal_places - - if 'editable' in column_expression.subformula: - edit_popup_data = { - 'column_group_key': col_group_key, - 'target_expression_id': column_expression.id, - 'rounding': rounding, - 'figure_type': figure_type, - 'column_value': column_value, - } - - formatter_params['digits'] = rounding - - # Handle editable financial budgets - editable_budget = groupby_model == 'account.account' and options['column_groups'][col_group_key]['forced_options'].get('compute_budget') - if editable_budget and self.env.user.has_group('account.group_account_manager'): - edit_popup_data = { - 'column_group_key': col_group_key, - 'target_expression_id': column_expression.id, - 'rounding': self.env.company.currency_id.decimal_places, - 'figure_type': 'monetary', - 'column_value': column_value, - } - - # Build result - if column_value is not None: #In case column value is zero, we still want to go through the condition - foreign_currency_id = target_line_res_dict.get(f'_currency_{column_expr_label}', {}).get('value') - if foreign_currency_id: - formatter_params['currency'] = self.env['res.currency'].browse(foreign_currency_id) - - column_data = self._build_column_dict( - column_value, - column_data, - options=options, - column_expression=column_expression if column_expression else None, - has_sublines=column_has_sublines, - report_line_id=line.id, - **formatter_params, - ) - - if info_popup_data: - column_data['info_popup_data'] = json.dumps(info_popup_data) - - if edit_popup_data: - column_data['edit_popup_data'] = json.dumps(edit_popup_data) - - columns.append(column_data) - - return columns - - def _build_column_dict( - self, col_value, col_data, - options=None, currency=False, digits=1, - column_expression=None, has_sublines=False, - report_line_id=None, - ): - # Empty column - if col_value is None and col_data is None: - return {} - - col_data = col_data or {} - column_expression = column_expression or self.env['account.report.expression'] - options = options or {} - - blank_if_zero = column_expression.blank_if_zero or col_data.get('blank_if_zero', False) - figure_type = column_expression.figure_type or col_data.get('figure_type', 'string') - - format_params = {} - if figure_type == 'monetary' and currency: - format_params['currency_id'] = currency.id - elif figure_type in ('float', 'percentage'): - format_params['digits'] = digits - - col_group_key = col_data.get('column_group_key') - - return { - 'auditable': col_value is not None - and column_expression.auditable - and not options['column_groups'][col_group_key]['forced_options'].get('compute_budget'), - 'blank_if_zero': blank_if_zero, - 'column_group_key': col_group_key, - 'currency': currency, - 'currency_symbol': (currency or self.env.company.currency_id).symbol if options.get('multi_currency') else None, - 'digits': digits, - 'expression_label': col_data.get('expression_label'), - 'figure_type': figure_type, - 'green_on_positive': column_expression.green_on_positive, - 'has_sublines': has_sublines, - 'is_zero': col_value is None or ( - isinstance(col_value, (int, float)) - and figure_type in NUMBER_FIGURE_TYPES - and self._is_value_zero(col_value, figure_type, format_params) - ), - 'no_format': col_value, - 'format_params': format_params, - 'report_line_id': report_line_id, - 'sortable': col_data.get('sortable', False), - 'comparison_mode': col_data.get('comparison_mode'), - } - - def _get_dynamic_lines(self, options, all_column_groups_expression_totals, warnings=None): - if self.custom_handler_model_id: - rslt = self.env[self.custom_handler_model_name]._dynamic_lines_generator(self, options, all_column_groups_expression_totals, warnings=warnings) - self._apply_integer_rounding_to_dynamic_lines(options, (line for _sequence, line in rslt)) - return rslt - return [] - - def _apply_integer_rounding_to_dynamic_lines(self, options, dynamic_lines): - if options.get('integer_rounding_enabled'): - for line in dynamic_lines: - for column_dict in line.get('columns', []): - if 'name' not in column_dict and column_dict.get('figure_type') == 'monetary' and column_dict.get('no_format'): - # If 'name' is already in it, no need to round the amount ; it is forced by the custom report already - column_dict['no_format'] = float_round( - column_dict['no_format'], - precision_digits=0, - rounding_method=options['integer_rounding'], - ) - - def _compute_expression_totals_for_each_column_group(self, expressions, options, - groupby_to_expand=None, forced_all_column_groups_expression_totals=None, col_groups_restrict=None, offset=0, limit=None, include_default_vals=False, warnings=None): - """ - Main computation function for static lines. - - :param expressions: The account.report.expression objects to evaluate. - - :param options: The options dict for this report, obtained from.get_options({}). - - :param groupby_to_expand: The full groupby string for the grouping we want to evaluate. If None, the aggregated value will be computed. - For example, when evaluating a group by partner_id, which further will be divided in sub-groups by account_id, - then id, the full groupby string will be: 'partner_id, account_id, id'. - - :param forced_all_column_groups_expression_totals: The expression totals already computed for this report, to which we will add the - new totals we compute for expressions (or update the existing ones if some - expressions are already in forced_all_column_groups_expression_totals). This is - a dict in the same format as returned by this function. - This parameter is for example used when adding manual values, where only - the expressions possibly depending on the new manual value - need to be updated, while we want to keep all the other values as-is. - - :param col_groups_restrict: List of column group keys of the groups to compute. Other column groups will be ignored, and will - not be added to the result of this function (they can still be provided beforehand through - forced_all_column_groups_expression_totals). If not provided, all colum groups will be computed. - - :param offset: The SQL offset to use when computing the result of these expressions. Used if self.load_more_limit is set, to handle - the load more feature. - - :param limit: The SQL limit to apply when computing these expressions' result. Used if self.load_more_limit is set, to handle - the load more feature. - - :return: dict(column_group_key, expressions_totals), where: - - column group key is string identifying each column group in a unique way ; as in options['column_groups'] - - expressions_totals is a dict in the format returned by _compute_expression_totals_for_single_column_group - """ - - def add_expressions_to_groups(expressions_to_add, grouped_formulas, force_date_scope=None): - """ Groups the expressions that should be computed together. - """ - for expression in expressions_to_add: - engine = expression.engine - - if engine not in grouped_formulas: - grouped_formulas[engine] = {} - - date_scope = force_date_scope or self._standardize_date_scope_for_date_range(expression.date_scope) - groupby_data = expression.report_line_id._parse_groupby(options, groupby_to_expand=groupby_to_expand) - - next_groupby = groupby_data['next_groupby'] if engine not in NO_NEXT_GROUPBY_ENGINES else None - grouping_key = (date_scope, groupby_data['current_groupby'], next_groupby) - - if grouping_key not in grouped_formulas[engine]: - grouped_formulas[engine][grouping_key] = {} - - formula = expression.formula - - if expression.engine == 'aggregation' and expression.formula == 'sum_children': - formula = ' + '.join( - f'_expression:{child_expr.id}' - for child_expr in expression.report_line_id.children_ids.expression_ids.filtered(lambda e: e.label == expression.label) - ) - - if formula not in grouped_formulas[engine][grouping_key]: - grouped_formulas[engine][grouping_key][formula] = expression - else: - grouped_formulas[engine][grouping_key][formula] |= expression - - if groupby_to_expand and any(not expression.report_line_id._get_groupby(options) for expression in expressions): - raise UserError(_("Trying to expand groupby results on lines without a groupby value.")) - - # Group formulas for batching (when possible) - grouped_formulas = {} - if expressions and not include_default_vals: - expressions = expressions.filtered(lambda x: not x.label.startswith('_default')) - for expression in expressions: - add_expressions_to_groups(expression, grouped_formulas) - - if expression.engine == 'aggregation' and expression.subformula == 'cross_report': - # Always expand aggregation expressions, in case their subexpressions are not in expressions parameter - # (this can happen in cross report, or when auditing an individual aggregation expression) - expanded_cross = expression._expand_aggregations() - forced_date_scope = self._standardize_date_scope_for_date_range(expression.date_scope) - add_expressions_to_groups(expanded_cross, grouped_formulas, force_date_scope=forced_date_scope) - - # Treat each formula batch for each column group - all_column_groups_expression_totals = {} - for group_key, group_options in self._split_options_per_column_group(options).items(): - if forced_all_column_groups_expression_totals: - forced_column_group_totals = forced_all_column_groups_expression_totals.get(group_key, None) - else: - forced_column_group_totals = None - - if not col_groups_restrict or group_key in col_groups_restrict: - current_group_expression_totals = self._compute_expression_totals_for_single_column_group( - group_options, - grouped_formulas, - forced_column_group_expression_totals=forced_column_group_totals, - offset=offset, - limit=limit, - warnings=warnings, - ) - else: - current_group_expression_totals = forced_column_group_totals - - all_column_groups_expression_totals[group_key] = current_group_expression_totals - - return all_column_groups_expression_totals - - def _standardize_date_scope_for_date_range(self, date_scope): - """ Depending on the fact the report accepts date ranges or not, different date scopes might mean the same thing. - This function is used so that, in those cases, only one of these date_scopes' values is used, to avoid useless creation - of multiple computation batches and improve the overall performance as much as possible. - """ - if not self.filter_date_range and date_scope == 'strict_range': - return 'from_beginning' - else: - return date_scope - - def _split_options_per_column_group(self, options): - """ Get a specific option dict per column group, each enforcing the comparison and horizontal grouping associated - with the column group. Each of these options dict will contain a new key 'owner_column_group', with the column group key of the - group it was generated for. - - :param options: The report options upon which the returned options be be based. - - :return: A dict(column_group_key, options_dict), where column_group_key is the string identifying each column group (the keys - of options['column_groups'], and options_dict the generated options for this group. - """ - options_per_group = {} - for group_key in options['column_groups']: - group_options = self._get_column_group_options(options, group_key) - options_per_group[group_key] = group_options - - return options_per_group - - def _get_column_group_options(self, options, group_key): - column_group = options['column_groups'][group_key] - return { - **options, - **column_group['forced_options'], - 'forced_domain': options.get('forced_domain', []) + column_group['forced_domain'] + column_group['forced_options'].get('forced_domain', []), - 'owner_column_group': group_key, - } - - def _compute_expression_totals_for_single_column_group(self, column_group_options, grouped_formulas, forced_column_group_expression_totals=None, offset=0, limit=None, warnings=None): - """ Evaluates expressions for a single column group. - - :param column_group_options: The options dict obtained from _split_options_per_column_group() for the column group to evaluate. - - :param grouped_formulas: A dict(engine, formula_dict), where: - - engine is a string identifying a report engine, in the same format as in account.report.expression's engine - field's technical labels. - - formula_dict is a dict in the same format as _compute_formula_batch's formulas_dict parameter, - containing only aggregation formulas. - - :param forced_column_group_expression_totals: The expression totals previously computed, in the same format as this function's result. - If provided, the result of this function will be an updated version of this parameter, - recomputing the expressions in grouped_fomulas. - - :param offset: The SQL offset to use when computing the result of these expressions. Used if self.load_more_limit is set, to handle - the load more feature. - - :param limit: The SQL limit to apply when computing these expressions' result. Used if self.load_more_limit is set, to handle - the load more feature. - - :return: A dict(expression, {'value': value, 'has_sublines': has_sublines}), where: - - expression is one of the account.report.expressions that got evaluated - - - value is the result of that evaluation. Two cases are possible: - - if we're evaluating a groupby: value will then be a in the form [(groupby_key, group_val)], where - - groupby_key is the key used in the SQL GROUP BY clause to generate this result - - group_val: The result computed by the engine for this group. Typically a float. - - - else: value will directly be the result computed for this expression - - - has_sublines: [optional key, will default to False if absent] - Whether or not this result corresponds to 1 or more subelements in the database (typically move lines). - This is used to know whether an unfoldable line has results to unfold in the UI. - """ - def inject_formula_results(formula_results, column_group_expression_totals, cross_report_expression_totals=None): - for (_key, expressions), result in formula_results.items(): - for expression in expressions: - subformula_error_format = _( - 'Invalid subformula in expression "%(expression)s" of line "%(line)s": %(subformula)s', - expression=expression.label, - line=expression.report_line_id.name, - subformula=expression.subformula, - ) - if expression.engine not in ('aggregation', 'external') and expression.subformula: - # aggregation subformulas behave differently (cross_report is markup ; if_below, if_above and force_between need evaluation) - # They are directly handled in aggregation engine - result_value_key = expression.subformula - else: - result_value_key = 'result' - - # The expression might be signed, so we can't just access the dict key, and directly evaluate it instead. - - if isinstance(result, list): - # Happens when expanding a groupby line, to compute its children. - # We then want to keep a list(grouping key, total) as the final result of each total - expression_value = [] - expression_has_sublines = False - for key, result_dict in result: - try: - expression_value.append((key, safe_eval(result_value_key, result_dict))) - except (ValueError, SyntaxError): - raise UserError(subformula_error_format) - expression_has_sublines = expression_has_sublines or result_dict.get('has_sublines') - else: - # For non-groupby lines, we directly set the total value for the line. - try: - expression_value = safe_eval(result_value_key, result) - except (ValueError, SyntaxError): - raise UserError(subformula_error_format) - expression_has_sublines = result.get('has_sublines') - - if column_group_options.get('integer_rounding_enabled'): - in_monetary_column = any( - col['expression_label'] == expression.label - for col in column_group_options['columns'] - if col['figure_type'] == 'monetary' - ) - - if (in_monetary_column and not expression.figure_type) or expression.figure_type == 'monetary': - expression_value = float_round(expression_value, precision_digits=0, rounding_method=column_group_options['integer_rounding']) - - expression_result = { - 'value': expression_value, - 'has_sublines': expression_has_sublines, - } - - if expression.report_line_id.report_id == self: - if expression in column_group_expression_totals: - # This can happen because of a cross report aggregation referencing an expression of its own report, - # but forcing a different date_scope onto it. This case is not supported for now ; splitting the aggregation can be - # used as a workaround. - raise UserError(_( - "Expression labelled '%(label)s' of line '%(line)s' is being overwritten when computing the current report. " - "Make sure the cross-report aggregations of this report only reference terms belonging to other reports.", - label=expression.label, line=expression.report_line_id.name - )) - column_group_expression_totals[expression] = expression_result - elif cross_report_expression_totals is not None: - # Entering this else means this expression needs to be evaluated because of a cross_report aggregation - cross_report_expression_totals[expression] = expression_result - - # Batch each engine that can be - column_group_expression_totals = dict(forced_column_group_expression_totals) if forced_column_group_expression_totals else {} - cross_report_expr_totals_by_scope = {} - batchable_engines = [ - selection_val[0] - for selection_val in self.env['account.report.expression']._fields['engine'].selection - if selection_val[0] != 'aggregation' - ] - for engine in batchable_engines: - for (date_scope, current_groupby, next_groupby), formulas_dict in grouped_formulas.get(engine, {}).items(): - formula_results = self._compute_formula_batch(column_group_options, engine, date_scope, formulas_dict, current_groupby, next_groupby, - offset=offset, limit=limit, warnings=warnings) - inject_formula_results( - formula_results, - column_group_expression_totals, - cross_report_expression_totals=cross_report_expr_totals_by_scope.setdefault(date_scope, {}) - ) - - # Now that everything else has been computed, resolve aggregation expressions - # (they can't be treated as the other engines, as if we batch them per date_scope, we'll not be able - # to compute expressions depending on other expressions with a different date scope). - aggregation_formulas_dict = {} - for (date_scope, _current_groupby, _next_groupby), formulas_dict in grouped_formulas.get('aggregation', {}).items(): - for formula, expressions in formulas_dict.items(): - for expression in expressions: - # group_by are ignored by this engine, so we merge every grouped entry into a common dict - forced_date_scope = date_scope if expression.subformula == 'cross_report' or expression.report_line_id.report_id != self else None - aggreation_formula_dict_key = (formula, forced_date_scope) - aggregation_formulas_dict.setdefault(aggreation_formula_dict_key, self.env['account.report.expression']) - aggregation_formulas_dict[aggreation_formula_dict_key] |= expression - - if aggregation_formulas_dict: - aggregation_formula_results = self._compute_totals_no_batch_aggregation(column_group_options, aggregation_formulas_dict, column_group_expression_totals, cross_report_expr_totals_by_scope) - inject_formula_results(aggregation_formula_results, column_group_expression_totals) - - return column_group_expression_totals - - def _compute_totals_no_batch_aggregation(self, column_group_options, formulas_dict, other_current_report_expr_totals, other_cross_report_expr_totals_by_scope): - """ Computes expression totals for 'aggregation' engine, after all other engines have been evaluated. - - :param column_group_options: The options for the column group being evaluated, as obtained from _split_options_per_column_group. - - :param formulas_dict: A dict {(formula, forced_date_scope): expressions}, containing only aggregation formulas. - forced_date_scope will only be set in case of cross_report expressions. Else, it will be None - - :param other_current_report_expr_totals: The expressions_totals obtained after computing all non-aggregation engines, for the expressions - belonging directly to self (so, not the ones referenced by a cross_report aggreation). - This is a dict in the same format as _compute_expression_totals_for_single_column_group's result - (the only difference being it does not contain any aggregation expression yet). - - :param other_cross_report_expr_totals: A dict(forced_date_scope, expression_totals), where expression_totals is in the same form as - _compute_expression_totals_for_single_column_group's result. This parameter contains the results - of the non-aggregation expressions used by cross_report expressions ; they all belong to different - reports than self. The forced_date_scope corresponds to the original date_scope set on the - cross_report expression referencing them. The same expressions can be referenced multiple times - under different date scopes. - - :return : A dict((formula, expressions), result), where result is in the form {'result': numeric_value} - """ - def _resolve_subformula_on_dict(result, line_codes_expression_map, subformula): - split_subformula = subformula.split('.') - if len(split_subformula) > 1: - line_code, expression_label = split_subformula - return result[line_codes_expression_map[line_code][expression_label]] - - if subformula.startswith('_expression:'): - expression_id = int(subformula.split(':')[1]) - return result[expression_id] - - # Wrong subformula; the KeyError is caught in the function below - raise KeyError() - - def _check_is_float(to_test): - try: - float(to_test) - return True - except ValueError: - return False - - def add_expression_to_map(expression, expression_res, figure_types_cache, current_report_eval_dict, current_report_codes_map, other_reports_eval_dict, other_reports_codes_map, cross_report=False): - """ - Process an expression and its result, updating various dictionaries with relevant information. - Parameters: - - expression (object): The expression object to process. - - expression_res (dict): The result of the expression. - - figure_types_cache (dict): {report : {label: figure_type}}. - - current_report_eval_dict (dict): {expression_id: value}. - - current_report_codes_map (dict): {line_code: {expression_label: expression_id}}. - - other_reports_eval_dict (dict): {forced_date_scope: {expression_id: value}}. - - other_reports_codes_map (dict): {forced_date_scope: {line_code: {expression_label: expression_id}}}. - - cross_report: A boolean to know if we are processsing cross_report expression. - """ - - expr_report = expression.report_line_id.report_id - report_default_figure_types = figure_types_cache.setdefault(expr_report, {}) - expression_label = report_default_figure_types.get(expression.label, '_not_in_cache') - if expression_label == '_not_in_cache': - report_default_figure_types[expression.label] = expr_report.column_ids.filtered( - lambda x: x.expression_label == expression.label).figure_type - - default_figure_type = figure_types_cache[expr_report][expression.label] - figure_type = expression.figure_type or default_figure_type - value = expression_res['value'] - if figure_type == 'monetary' and value: - value = self.env.company.currency_id.round(value) - - if cross_report: - other_reports_eval_dict.setdefault(forced_date_scope, {})[expression.id] = value - else: - current_report_eval_dict[expression.id] = value - - current_report_eval_dict = {} # {expression_id: value} - other_reports_eval_dict = {} # {forced_date_scope: {expression_id: value}} - current_report_codes_map = {} # {line_code: {expression_label: expression_id}} - other_reports_codes_map = {} # {forced_date_scope: {line_code: {expression_label: expression_id}}} - - figure_types_cache = {} # {report : {label: figure_type}} - for expression, expression_res in other_current_report_expr_totals.items(): - add_expression_to_map(expression, expression_res, figure_types_cache, current_report_eval_dict, current_report_codes_map, other_reports_eval_dict, other_reports_codes_map) - if expression.report_line_id.code: - current_report_codes_map.setdefault(expression.report_line_id.code, {})[expression.label] = expression.id - - for forced_date_scope, scope_expr_totals in other_cross_report_expr_totals_by_scope.items(): - for expression, expression_res in scope_expr_totals.items(): - add_expression_to_map(expression, expression_res, figure_types_cache, current_report_eval_dict, current_report_codes_map, other_reports_eval_dict, other_reports_codes_map, True) - if expression.report_line_id.code: - other_reports_codes_map.setdefault(forced_date_scope, {}).setdefault(expression.report_line_id.code, {})[expression.label] = expression.id - - # Complete current_report_eval_dict with the formulas of uncomputed aggregation lines - aggregations_terms_to_evaluate = set() # Those terms are part of the formulas to evaluate; we know they will get a value eventually - for (formula, forced_date_scope), expressions in formulas_dict.items(): - for expression in expressions: - aggregations_terms_to_evaluate.add(f"_expression:{expression.id}") # In case it needs to be called by sum_children - - if expression.report_line_id.code: - if expression.report_line_id.report_id == self: - current_report_codes_map.setdefault(expression.report_line_id.code, {})[expression.label] = expression.id - else: - other_reports_codes_map.setdefault(forced_date_scope, {}).setdefault(expression.report_line_id.code, {})[expression.label] = expression.id - - aggregations_terms_to_evaluate.add(f"{expression.report_line_id.code}.{expression.label}") - - if not expression.subformula: - # Expressions with bounds cannot be replaced by their formula in formulas calling them (otherwize, bounds would be ignored). - # Same goes for cross_report, otherwise the forced_date_scope will be ignored, leading to an impossibility to get evaluate the expression. - if expression.report_line_id.report_id == self: - eval_dict = current_report_eval_dict - else: - eval_dict = other_reports_eval_dict.setdefault(forced_date_scope, {}) - - eval_dict[expression.id] = formula - - rslt = {} - to_treat = [(formula, formula, forced_date_scope) for (formula, forced_date_scope) in formulas_dict.keys()] # Formed like [(expanded formula, original unexpanded formula)] - term_separator_regex = r'(?\w+)\(" - r"(?P\w+)[.](?P\w+),[ ]*" - r"(?P.*)\)$", - expression.subformula - ) - if not other_expr_criterium_match: - raise UserError(_("Wrong format for if_other_expr_above/if_other_expr_below formula: %s", expression.subformula)) - - criterium_code = other_expr_criterium_match['line_code'] - criterium_label = other_expr_criterium_match['expr_label'] - criterium_expression_id = full_codes_map.get(criterium_code, {}).get(criterium_label) - criterium_val = full_eval_dict.get(criterium_expression_id) - - if not criterium_expression_id: - raise UserError(_("This subformula references an unknown expression: %s", expression.subformula)) - - if not isinstance(criterium_val, (float, int)): - # The criterium expression has not be evaluated yet. Postpone the evaluation of this formula, and skip this expression - # for now. We still try to evaluate other expressions using this formula if any; this means those expressions will - # be processed a second time later, giving the same result. This is a rare corner case, and not so costly anyway. - to_treat.append((formula, unexpanded_formula, forced_date_scope)) - continue - - bound_subformula = other_expr_criterium_match['criterium'].replace('other_expr_', '') # e.g. 'if_other_expr_above' => 'if_above' - bound_params = other_expr_criterium_match['bound_params'] - bound_value = self._aggregation_apply_bounds(column_group_options, f"{bound_subformula}({bound_params})", criterium_val) - expression_result = formula_result * int(bool(bound_value)) - - else: - expression_result = self._aggregation_apply_bounds(column_group_options, expression.subformula, formula_result) - - if column_group_options.get('integer_rounding_enabled'): - expression_result = float_round(expression_result, precision_digits=0, rounding_method=column_group_options['integer_rounding']) - - # Store result - standardized_expression_scope = self._standardize_date_scope_for_date_range(expression.date_scope) - if (forced_date_scope == standardized_expression_scope or not forced_date_scope) and expression.report_line_id.report_id == self: - # This condition ensures we don't return necessary subcomputations in the final result - rslt[(unexpanded_formula, expression)] = {'result': expression_result} - - # Handle recursive aggregations (explicit or through the sum_children shortcut). - # We need to make the result of our computation available to other aggregations, as they are still waiting in to_treat to be evaluated. - if expression.report_line_id.report_id == self: - current_report_eval_dict[expression.id] = expression_result - else: - other_reports_eval_dict.setdefault(forced_date_scope, {})[expression.id] = expression_result - - return rslt - - def _aggregation_apply_bounds(self, column_group_options, subformula, unbound_value): - """ Applies the bounds of the provided aggregation expression to an unbounded value that got computed for it and returns the result. - Bounds can be defined as subformulas of aggregation expressions, with the following possible values: - - - if_above(CUR(bound_value)): - => Result will be 0 if it's <= the provided bound value; else it'll be unbound_value - - - if_below(CUR(bound_value)): - => Result will be 0 if it's >= the provided bound value; else it'll be unbound_value - - - if_between(CUR(bound_value1), CUR(bound_value2)): - => Result will be unbound_value if it's strictly between the provided bounds. Else, it will - be brought back to the closest bound. - - - round(decimal_places): - => Result will be round(unbound_value, decimal_places) - - (where CUR is a currency code, and bound_value* are float amounts in CUR currency) - """ - if not subformula: - return unbound_value - - # So an expression can't have bounds and be cross_reports, for simplicity. - # To do that, just split the expression in two parts. - if subformula and subformula.startswith('round'): - precision_string = re.match(r"round\((?P\d+)\)", subformula)['precision'] - return round(unbound_value, int(precision_string)) - - if subformula not in {'cross_report', 'ignore_zero_division'}: - company_currency = self.env.company.currency_id - date_to = column_group_options['date']['date_to'] - - match = re.match( - r"(?P\w*)" - r"\((?P[A-Z]{3})\((?P[-]?\d+(\.\d+)?)\)" - r"(,(?P[A-Z]{3})\((?P[-]?\d+(\.\d+)?)\))?\)$", - subformula.replace(' ', '') - ) - group_values = match.groupdict() - - # Convert the provided bounds into company currency - currency_code_1 = group_values.get('currency_1') - currency_code_2 = group_values.get('currency_2') - currency_codes = [ - currency_code - for currency_code in [currency_code_1, currency_code_2] - if currency_code and currency_code != company_currency.name - ] - - if currency_codes: - currencies = self.env['res.currency'].with_context(active_test=False).search([('name', 'in', currency_codes)]) - else: - currencies = self.env['res.currency'] - - amount_1 = float(group_values['amount_1'] or 0) - amount_2 = float(group_values['amount_2'] or 0) - for currency in currencies: - if currency != company_currency: - if currency.name == currency_code_1: - amount_1 = currency._convert(amount_1, company_currency, self.env.company, date_to) - if amount_2 and currency.name == currency_code_2: - amount_2 = currency._convert(amount_2, company_currency, self.env.company, date_to) - - # Evaluate result - criterium = group_values['criterium'] - if criterium == 'if_below': - if company_currency.compare_amounts(unbound_value, amount_1) >= 0: - return 0 - elif criterium == 'if_above': - if company_currency.compare_amounts(unbound_value, amount_1) <= 0: - return 0 - elif criterium == 'if_between': - if company_currency.compare_amounts(unbound_value, amount_1) < 0 or company_currency.compare_amounts(unbound_value, amount_2) > 0: - return 0 - else: - raise UserError(_("Unknown bound criterium: %s", criterium)) - - return unbound_value - - def _compute_formula_batch(self, column_group_options, formula_engine, date_scope, formulas_dict, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - """ Evaluates a batch of formulas. - - :param column_group_options: The options for the column group being evaluated, as obtained from _split_options_per_column_group. - - :param formula_engine: A string identifying a report engine. Must be one of account.report.expression's engine field's technical labels. - - :param date_scope: The date_scope under which to evaluate the fomulas. Must be one of account.report.expression's date_scope field's - technical labels. - - :param formulas_dict: A dict in the dict(formula, expressions), where: - - formula: a formula to be evaluated with the engine referred to by parent dict key - - expressions: a recordset of all the expressions to evaluate using formula (possibly with distinct subformulas) - - :param current_groupby: The groupby to evaluate, or None if there isn't any. In case of multi-level groupby, only contains the element - that needs to be computed (so, if unfolding a line doing 'partner_id,account_id,id'; current_groupby will only be - 'partner_id'). Subsequent groupby will be in next_groupby. - - :param next_groupby: Full groupby string of the groups that will have to be evaluated next for these expressions, or None if there isn't any. - For example, in the case depicted in the example of current_groupby, next_groupby will be 'account_id,id'. - - :param offset: The SQL offset to use when computing the result of these expressions. - - :param limit: The SQL limit to apply when computing these expressions' result. - - :return: The result might have two different formats depending on the situation: - - if we're computing a groupby: {(formula, expressions): [(grouping_key, {'result': value, 'has_sublines': boolean}), ...], ...} - - if we're not: {(formula, expressions): {'result': value, 'has_sublines': boolean}, ...} - 'result' key is the default; different engines might use one or multiple other keys instead, depending of the subformulas they allow - (e.g. 'sum', 'sum_if_pos', ...) - """ - engine_function_name = f'_compute_formula_batch_with_engine_{formula_engine}' - return getattr(self, engine_function_name)( - column_group_options, date_scope, formulas_dict, current_groupby, next_groupby, - offset=offset, limit=limit, warnings=warnings, - ) - - def _compute_formula_batch_with_engine_tax_tags(self, options, date_scope, formulas_dict, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - """ Report engine. - - The formulas made for this report simply consist of a tag label. When an expression using this engine is created, it also creates two - account.account.tag objects, namely -tag and +tag, where tag is the chosen formula. The balance of the expressions using this engine is - computed by gathering all the move lines using their tags, and applying the sign of their tag to their balance, together with a -1 factor - if the tax_tag_invert field of the move line is True. - - This engine does not support any subformula. - """ - self._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - all_expressions = self.env['account.report.expression'] - for expressions in formulas_dict.values(): - all_expressions |= expressions - tags = all_expressions._get_matching_tags() - - groupby_sql = SQL.identifier('account_move_line', current_groupby) if current_groupby else None - query = self._get_report_query(options, date_scope) - tail_query = self._get_engine_query_tail(offset, limit) - lang = get_lang(self.env, self.env.user.lang).code - acc_tag_name = self.with_context(lang='en_US').env['account.account.tag']._field_to_sql('acc_tag', 'name') - sql = SQL( - """ - SELECT - SUBSTRING(%(acc_tag_name)s, 2, LENGTH(%(acc_tag_name)s) - 1) AS formula, - SUM(%(balance_select)s - * CASE WHEN acc_tag.tax_negate THEN -1 ELSE 1 END - * CASE WHEN account_move_line.tax_tag_invert THEN -1 ELSE 1 END - ) AS balance, - COUNT(account_move_line.id) AS aml_count - %(select_groupby_sql)s - - FROM %(table_references)s - - JOIN account_account_tag_account_move_line_rel aml_tag - ON aml_tag.account_move_line_id = account_move_line.id - JOIN account_account_tag acc_tag - ON aml_tag.account_account_tag_id = acc_tag.id - AND acc_tag.id IN %(tag_ids)s - %(currency_table_join)s - - WHERE %(search_condition)s - - GROUP BY %(groupby_clause)s - - ORDER BY %(groupby_clause)s - - %(tail_query)s - """, - acc_tag_name=acc_tag_name, - select_groupby_sql=SQL(', %s AS grouping_key', groupby_sql) if groupby_sql else SQL(), - table_references=query.from_clause, - tag_ids=tuple(tags.ids), - balance_select=self._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=self._currency_table_aml_join(options), - search_condition=query.where_clause, - groupby_clause=SQL( - "SUBSTRING(%(acc_tag_name)s, 2, LENGTH(%(acc_tag_name)s) - 1)%(groupby_sql)s", - acc_tag_name=acc_tag_name, - groupby_sql=SQL(', %s', groupby_sql) if groupby_sql else SQL(), - ), - tail_query=tail_query, - ) - - self._cr.execute(sql) - - rslt = {formula_expr: [] if current_groupby else {'result': 0, 'has_sublines': False} for formula_expr in formulas_dict.items()} - for query_res in self._cr.dictfetchall(): - - formula = query_res['formula'] - rslt_dict = {'result': query_res['balance'], 'has_sublines': query_res['aml_count'] > 0} - if current_groupby: - rslt[(formula, formulas_dict[formula])].append((query_res['grouping_key'], rslt_dict)) - else: - rslt[(formula, formulas_dict[formula])] = rslt_dict - - return rslt - - def _compute_formula_batch_with_engine_domain(self, options, date_scope, formulas_dict, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - """ Report engine. - - Formulas made for this engine consist of a domain on account.move.line. Only those move lines will be used to compute the result. - - This engine supports a few subformulas, each returning a slighlty different result: - - sum: the result will be sum of the matched move lines' balances - - - sum_if_pos: the result will be the same as sum only if it's positive; else, it will be 0 - - - sum_if_neg: the result will be the same as sum only if it's negative; else, it will be 0 - - - count_rows: the result will be the number of sublines this expression has. If the parent report line has no groupby, - then it will be the number of matching amls. If there is a groupby, it will be the number of distinct grouping - keys at the first level of this groupby (so, if groupby is 'partner_id, account_id', the number of partners). - """ - def _format_result_depending_on_groupby(formula_rslt): - if not current_groupby: - if formula_rslt: - # There should be only one element in the list; we only return its totals (a dict) ; so that a list is only returned in case - # of a groupby being unfolded. - return formula_rslt[0][1] - else: - # No result at all - return { - 'sum': 0, - 'sum_if_pos': 0, - 'sum_if_neg': 0, - 'count_rows': 0, - 'has_sublines': False, - } - return formula_rslt - - self._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - groupby_sql = SQL.identifier('account_move_line', current_groupby) if current_groupby else None - - rslt = {} - - for formula, expressions in formulas_dict.items(): - try: - line_domain = literal_eval(formula) - except (ValueError, SyntaxError): - raise UserError(_( - 'Invalid domain formula in expression "%(expression)s" of line "%(line)s": %(formula)s', - expression=expressions.label, - line=expressions.report_line_id.name, - formula=formula, - )) - query = self._get_report_query(options, date_scope, domain=line_domain) - - tail_query = self._get_engine_query_tail(offset, limit) - query = SQL( - """ - SELECT - COALESCE(SUM(%(balance_select)s), 0.0) AS sum, - COUNT(DISTINCT account_move_line.%(select_count_field)s) AS count_rows - %(select_groupby_sql)s - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - %(group_by_groupby_sql)s - %(order_by_sql)s - %(tail_query)s - """, - select_count_field=SQL.identifier(next_groupby.split(',')[0] if next_groupby else 'id'), - select_groupby_sql=SQL(', %s AS grouping_key', groupby_sql) if groupby_sql else SQL(), - table_references=query.from_clause, - balance_select=self._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=self._currency_table_aml_join(options), - search_condition=query.where_clause, - group_by_groupby_sql=SQL('GROUP BY %s', groupby_sql) if groupby_sql else SQL(), - order_by_sql=SQL(' ORDER BY %s', groupby_sql) if groupby_sql else SQL(), - tail_query=tail_query, - ) - - # Fetch the results. - formula_rslt = [] - self._cr.execute(query) - all_query_res = self._cr.dictfetchall() - - total_sum = 0 - for query_res in all_query_res: - res_sum = query_res['sum'] - total_sum += res_sum - totals = { - 'sum': res_sum, - 'sum_if_pos': 0, - 'sum_if_neg': 0, - 'count_rows': query_res['count_rows'], - 'has_sublines': query_res['count_rows'] > 0, - } - formula_rslt.append((query_res.get('grouping_key', None), totals)) - - # Handle sum_if_pos, -sum_if_pos, sum_if_neg and -sum_if_neg - expressions_by_sign_policy = defaultdict(lambda: self.env['account.report.expression']) - for expression in expressions: - subformula_without_sign = expression.subformula.replace('-', '').strip() - if subformula_without_sign in ('sum_if_pos', 'sum_if_neg'): - expressions_by_sign_policy[subformula_without_sign] += expression - else: - expressions_by_sign_policy['no_sign_check'] += expression - - # Then we have to check the total of the line and only give results if its sign matches the desired policy. - # This is important for groupby managements, for which we can't just check the sign query_res by query_res - if expressions_by_sign_policy['sum_if_pos'] or expressions_by_sign_policy['sum_if_neg']: - sign_policy_with_value = 'sum_if_pos' if self.env.company.currency_id.compare_amounts(total_sum, 0.0) >= 0 else 'sum_if_neg' - # >= instead of > is intended; usability decision: 0 is considered positive - - formula_rslt_with_sign = [(grouping_key, {**totals, sign_policy_with_value: totals['sum']}) for grouping_key, totals in formula_rslt] - - for sign_policy in ('sum_if_pos', 'sum_if_neg'): - policy_expressions = expressions_by_sign_policy[sign_policy] - - if policy_expressions: - if sign_policy == sign_policy_with_value: - rslt[(formula, policy_expressions)] = _format_result_depending_on_groupby(formula_rslt_with_sign) - else: - rslt[(formula, policy_expressions)] = _format_result_depending_on_groupby([]) - - if expressions_by_sign_policy['no_sign_check']: - rslt[(formula, expressions_by_sign_policy['no_sign_check'])] = _format_result_depending_on_groupby(formula_rslt) - - return rslt - - def _compute_formula_batch_with_engine_account_codes(self, options, date_scope, formulas_dict, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - r""" Report engine. - - Formulas made for this engine target account prefixes. Each of the prefix used in the formula will be evaluated as the sum of the move - lines made on the accounts matching it. Those prefixes can be used together with arithmetic operations to perform them on the obtained - results. - Example: '123 - 456' will substract the balance of all account starting with 456 from the one of all accounts starting with 123. - - It is also possible to exclude some subprefixes, with \ operator. - Example: '123\(1234)' will match prefixes all accounts starting with '123', except the ones starting with '1234' - - To only match the balance of an account is it's positive (debit) or negative (credit), the letter D or C can be put just next to the prefix: - Example '123D': will give the total balance of accounts starting with '123' if it's positive, else it will be evaluated as 0. - - Multiple subprefixes can be excluded if needed. - Example: '123\(1234,1236) - - All these syntaxes can be mixed together. - Example: '123D\(1235) + 56 - 416C' - - Note: if C or D character needs to be part of the prefix, it is possible to differentiate them of debit and credit match characters - by using an empty prefix exclusion. - Example 1: '123D\' will take the total balance of accounts starting with '123D' - Example 2: '123D\C' will return the balance of accounts starting with '123D' if it's negative, 0 otherwise. - """ - self._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - # Gather the account code prefixes to compute the total from - prefix_details_by_formula = {} # in the form {formula: [(1, prefix1), (-1, prefix2)]} - prefixes_to_compute = set() - for formula in formulas_dict: - prefix_details_by_formula[formula] = [] - for token in ACCOUNT_CODES_ENGINE_SPLIT_REGEX.split(formula.replace(' ', '')): - if token: - token_match = ACCOUNT_CODES_ENGINE_TERM_REGEX.match(token) - - if not token_match: - raise UserError(_("Invalid token '%(token)s' in account_codes formula '%(formula)s'", token=token, formula=formula)) - - parsed_token = token_match.groupdict() - - if not parsed_token: - raise UserError(_("Could not parse account_code formula from token '%s'", token)) - - multiplicator = -1 if parsed_token['sign'] == '-' else 1 - excluded_prefixes_match = token_match['excluded_prefixes'] - excluded_prefixes = excluded_prefixes_match.split(',') if excluded_prefixes_match else [] - prefix = token_match['prefix'] - - # We group using both prefix and excluded_prefixes as keys, for the case where two expressions would - # include the same prefix, but exlcude different prefixes (example 104\(1041) and 104\(1042)) - prefix_key = (prefix, *excluded_prefixes) - prefix_details_by_formula[formula].append((multiplicator, prefix_key, token_match['balance_character'])) - prefixes_to_compute.add((prefix, tuple(excluded_prefixes))) - - # Create the subquery for the WITH linking our prefixes with account.account entries - all_prefixes_queries: list[SQL] = [] - prefilter = self.env['account.account']._check_company_domain(self.get_report_company_ids(options)) - for prefix, excluded_prefixes in prefixes_to_compute: - account_domain = [ - *prefilter, - ] - - tag_match = ACCOUNT_CODES_ENGINE_TAG_ID_PREFIX_REGEX.match(prefix) - - if tag_match: - if tag_match['ref']: - tag_id = self.env['ir.model.data']._xmlid_to_res_id(tag_match['ref']) - else: - tag_id = int(tag_match['id']) - - account_domain.append(('tag_ids', 'in', [tag_id])) - else: - account_domain.append(('code', '=like', f'{prefix}%')) - - excluded_prefixes_domains = [] - - for excluded_prefix in excluded_prefixes: - excluded_prefixes_domains.append([('code', '=like', f'{excluded_prefix}%')]) - - if excluded_prefixes_domains: - account_domain.append('!') - account_domain += osv.expression.OR(excluded_prefixes_domains) - - prefix_query = self.env['account.account']._where_calc(account_domain) - all_prefixes_queries.append(prefix_query.select( - SQL("%s AS prefix", [prefix, *excluded_prefixes]), - SQL("account_account.id AS account_id"), - )) - - # Build a map to associate each account with the prefixes it matches - accounts_prefix_map = defaultdict(list) - for prefix, account_id in self.env.execute_query(SQL(' UNION ALL ').join(all_prefixes_queries)): - accounts_prefix_map[account_id].append(tuple(prefix)) - - # Run main query - query = self._get_report_query(options, date_scope) - - current_groupby_aml_sql = SQL.identifier('account_move_line', current_groupby) if current_groupby else SQL() - tail_query = self._get_engine_query_tail(offset, limit) - if current_groupby_aml_sql and tail_query: - tail_query_additional_groupby_where_sql = SQL( - """ - AND %(current_groupby_aml_sql)s IN ( - SELECT DISTINCT %(current_groupby_aml_sql)s - FROM account_move_line - WHERE %(search_condition)s - ORDER BY %(current_groupby_aml_sql)s - %(tail_query)s - ) - """, - current_groupby_aml_sql=current_groupby_aml_sql, - search_condition=query.where_clause, - tail_query=tail_query, - ) - else: - tail_query_additional_groupby_where_sql = SQL() - - extra_groupby_sql = SQL(", %s", current_groupby_aml_sql) if current_groupby_aml_sql else SQL() - extra_select_sql = SQL(", %s AS grouping_key", current_groupby_aml_sql) if current_groupby_aml_sql else SQL() - - query = SQL( - """ - SELECT - account_move_line.account_id AS account_id, - SUM(%(balance_select)s) AS sum, - COUNT(account_move_line.id) AS aml_count - %(extra_select_sql)s - FROM %(table_references)s - %(currency_table_join)s - WHERE %(search_condition)s - %(tail_query_additional_groupby_where_sql)s - GROUP BY account_move_line.account_id%(extra_groupby_sql)s - %(order_by_sql)s - %(tail_query)s - """, - extra_select_sql=extra_select_sql, - table_references=query.from_clause, - balance_select=self._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=self._currency_table_aml_join(options), - search_condition=query.where_clause, - extra_groupby_sql=extra_groupby_sql, - tail_query_additional_groupby_where_sql=tail_query_additional_groupby_where_sql, - order_by_sql=SQL('ORDER BY %s', SQL.identifier('account_move_line', current_groupby)) if current_groupby else SQL(), - tail_query=tail_query if not tail_query_additional_groupby_where_sql else SQL(), - ) - self._cr.execute(query) - - # Parse result - rslt = {} - - res_by_prefix_account_id = {} - for query_res in self._cr.dictfetchall(): - # Done this way so that we can run similar code for groupby and non-groupby - grouping_key = query_res['grouping_key'] if current_groupby else None - account_id = query_res['account_id'] - for prefix_key in accounts_prefix_map[account_id]: - res_by_prefix_account_id.setdefault(prefix_key, {})\ - .setdefault(account_id, [])\ - .append((grouping_key, {'result': query_res['sum'], 'has_sublines': query_res['aml_count'] > 0})) - - for formula, prefix_details in prefix_details_by_formula.items(): - rslt_key = (formula, formulas_dict[formula]) - rslt_destination = rslt.setdefault(rslt_key, [] if current_groupby else {'result': 0, 'has_sublines': False}) - rslt_groups_by_grouping_keys = {} - for multiplicator, prefix_key, balance_character in prefix_details: - res_by_account_id = res_by_prefix_account_id.get(prefix_key, {}) - - for account_results in res_by_account_id.values(): - account_total_value = sum(group_val['result'] for (group_key, group_val) in account_results) - comparator = self.env.company.currency_id.compare_amounts(account_total_value, 0.0) - - # Manage balance_character. - if not balance_character or (balance_character == 'D' and comparator >= 0) or (balance_character == 'C' and comparator < 0): - - for group_key, group_val in account_results: - rslt_group = { - **group_val, - 'result': multiplicator * group_val['result'], - } - if not current_groupby: - rslt_destination['result'] += rslt_group['result'] - rslt_destination['has_sublines'] = rslt_destination['has_sublines'] or rslt_group['has_sublines'] - elif group_key in rslt_groups_by_grouping_keys: - # Will happen if the same grouping key is used on move lines with different accounts. - # This comes from the GROUPBY in the SQL query, which uses both grouping key and account. - # When this happens, we want to aggregate the results of each grouping key, to avoid duplicates in the end result. - already_treated_rslt_group = rslt_groups_by_grouping_keys[group_key] - already_treated_rslt_group['has_sublines'] = already_treated_rslt_group['has_sublines'] or rslt_group['has_sublines'] - already_treated_rslt_group['result'] += rslt_group['result'] - else: - rslt_groups_by_grouping_keys[group_key] = rslt_group - rslt_destination.append((group_key, rslt_group)) - - return rslt - - def _compute_formula_batch_with_engine_external(self, options, date_scope, formulas_dict, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - """ Report engine. - - This engine computes its result from the account.report.external.value objects that are linked to the expression. - - Two different formulas are possible: - - sum: if the result must be the sum of all the external values in the period. - - most_recent: it the result must be the value of the latest external value in the period, which can be a number or a text - - No subformula is allowed for this engine. - """ - self._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - if current_groupby or next_groupby or offset or limit: - raise UserError(_("'external' engine does not support groupby, limit nor offset.")) - - # Date clause - date_from, date_to = self._get_date_bounds_info(options, date_scope) - external_value_domain = [('date', '<=', date_to)] - if date_from: - external_value_domain.append(('date', '>=', date_from)) - - # Company clause - external_value_domain.append(('company_id', 'in', self.get_report_company_ids(options))) - - # Fiscal Position clause - fpos_option = options['fiscal_position'] - if fpos_option == 'domestic': - external_value_domain.append(('foreign_vat_fiscal_position_id', '=', False)) - elif fpos_option != 'all': - # Then it's a fiscal position id - external_value_domain.append(('foreign_vat_fiscal_position_id', '=', int(fpos_option))) - - # Do the computation - where_clause = self.env['account.report.external.value']._where_calc(external_value_domain).where_clause - - # We have to execute two separate queries, one for text values and one for numeric values - num_queries = [] - string_queries = [] - monetary_queries = [] - for formula, expressions in formulas_dict.items(): - query_end = SQL() - if formula == 'most_recent': - query_end = SQL( - """ - GROUP BY date - ORDER BY date DESC - LIMIT 1 - """, - ) - string_query = """ - SELECT %(expression_id)s, text_value - FROM account_report_external_value - WHERE %(where_clause)s AND target_report_expression_id = %(expression_id)s - """ - monetary_query = """ - SELECT - %(expression_id)s, - COALESCE(SUM(COALESCE(%(balance_select)s, 0)), 0) - FROM account_report_external_value - %(currency_table_join)s - WHERE %(where_clause)s AND target_report_expression_id = %(expression_id)s - %(query_end)s - """ - num_query = """ - SELECT %(expression_id)s, SUM(COALESCE(value, 0)) - FROM account_report_external_value - WHERE %(where_clause)s AND target_report_expression_id = %(expression_id)s - %(query_end)s - """ - - for expression in expressions: - if expression.figure_type == "string": - string_queries.append(SQL( - string_query, - expression_id=expression.id, - where_clause=where_clause, - )) - elif expression.figure_type == "monetary": - monetary_queries.append(SQL( - monetary_query, - expression_id=expression.id, - balance_select=self._currency_table_apply_rate(SQL("CAST(value AS numeric)")), - currency_table_join=SQL( - """ - JOIN %(currency_table)s - ON account_currency_table.company_id = account_report_external_value.company_id - AND account_currency_table.rate_type = 'current' - """, - currency_table=self._get_currency_table(options), - ), - where_clause=where_clause, - query_end=query_end, - )) - else: - num_queries.append(SQL( - num_query, - expression_id=expression.id, - where_clause=where_clause, - query_end=query_end, - )) - - # Convert to dict to have expression ids as keys - query_results_dict = {} - for query_list in (num_queries, string_queries, monetary_queries): - if query_list: - query_results = self.env.execute_query(SQL(' UNION ALL ').join(SQL("(%s)", query) for query in query_list)) - query_results_dict.update(dict(query_results)) - - # Build result dict - rslt = {} - for formula, expressions in formulas_dict.items(): - for expression in expressions: - expression_value = query_results_dict.get(expression.id) - # If expression_value is None, we have no previous value for this expression (set default at 0.0) - expression_value = expression_value or ('' if expression.figure_type == 'string' else 0.0) - rslt[(formula, expression)] = {'result': expression_value, 'has_sublines': False} - - return rslt - - def _compute_formula_batch_with_engine_custom(self, options, date_scope, formulas_dict, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - self._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) - - rslt = {} - for formula, expressions in formulas_dict.items(): - custom_engine_function = self._get_custom_report_function(formula, 'custom_engine') - rslt[(formula, expressions)] = custom_engine_function( - expressions, options, date_scope, current_groupby, next_groupby, offset=offset, limit=limit, warnings=warnings) - return rslt - - def _get_engine_query_tail(self, offset, limit) -> SQL: - """ Helper to generate the OFFSET, LIMIT and ORDER conditions of formula engines' queries. - """ - query_tail = SQL() - - if offset: - query_tail = SQL("%s OFFSET %s", query_tail, offset) - - if limit: - query_tail = SQL("%s LIMIT %s", query_tail, limit) - - return query_tail - - def _generate_carryover_external_values(self, options): - """ Generates the account.report.external.value objects corresponding to this report's carryover under the provided options. - - In case of multicompany setup, we need to split the carryover per company, for ease of audit, and so that the carryover isn't broken when - a company leaves a tax unit. - - We first generate the carryover for the wholy-aggregated report, so that we can see what final result we want. - Indeed due to force_between, if_above and if_below conditions, each carryover might be different from the sum of the individidual companies' - carryover values. To handle this case, we generate each company's carryover values separately, then do a carryover adjustment on the - main company (main for tax units, first one selected else) in order to bring their total to the result we computed for the whole unit. - """ - self.ensure_one() - - if len(options['column_groups']) > 1: - # The options must be forged in order to generate carryover values. Entering this conditions means this hasn't been done in the right way. - raise UserError(_("Carryover can only be generated for a single column group.")) - - # Get the expressions to evaluate from the report - carryover_expressions = self.line_ids.expression_ids.filtered(lambda x: x.label.startswith('_carryover_')) - expressions_to_evaluate = carryover_expressions._expand_aggregations() - - # Expression totals for all selected companies - expression_totals_per_col_group = self._compute_expression_totals_for_each_column_group(expressions_to_evaluate, options) - expression_totals = expression_totals_per_col_group[list(options['column_groups'].keys())[0]] - carryover_values = {expression: expression_totals[expression]['value'] for expression in carryover_expressions} - - if len(options['companies']) == 1: - company = self.env['res.company'].browse(self.get_report_company_ids(options)) - self._create_carryover_for_company(options, company, {expr: result for expr, result in carryover_values.items()}) - else: - multi_company_carryover_values_sum = defaultdict(lambda: 0) - - column_group_key = next(col_group_key for col_group_key in options['column_groups']) - for company_opt in options['companies']: - company = self.env['res.company'].browse(company_opt['id']) - company_options = {**options, 'companies': [{'id': company.id, 'name': company.name}]} - company_expressions_totals = self._compute_expression_totals_for_each_column_group(expressions_to_evaluate, company_options) - company_carryover_values = {expression: company_expressions_totals[column_group_key][expression]['value'] for expression in carryover_expressions} - self._create_carryover_for_company(options, company, company_carryover_values) - - for carryover_expr, carryover_val in company_carryover_values.items(): - multi_company_carryover_values_sum[carryover_expr] += carryover_val - - # Adjust multicompany amounts on main company - main_company = self._get_sender_company_for_export(options) - for expr in carryover_expressions: - difference = carryover_values[expr] - multi_company_carryover_values_sum[expr] - self._create_carryover_for_company(options, main_company, {expr: difference}, label=_("Carryover adjustment for tax unit")) - - @api.model - def _generate_default_external_values(self, date_from, date_to, is_tax_report=False): - """ Generates the account.report.external.value objects for the given dates. - If is_tax_report, the values are only created for tax reports, else for all other reports. - """ - options_dict = {} - default_expr_by_report = defaultdict(list) - tax_report = self.env.ref('account.generic_tax_report') - company = self.env.company - previous_options = { - 'date': { - 'date_from': date_from, - 'date_to': date_to, - } - } - - # Get all the default expressions from all reports - default_expressions = self.env['account.report.expression'].search([('label', '=like', '_default_%')]) - # Options depend on the report, also we need to filter out tax report/other reports depending on is_tax_report - # Hence we need to group the default expressions by report - for expr in default_expressions: - report = expr.report_line_id.report_id - if is_tax_report == (tax_report in (report + report.root_report_id + report.section_main_report_ids.root_report_id)): - if report not in options_dict: - options = report.with_context(allowed_company_ids=[company.id]).get_options(previous_options) - options_dict[report] = options - - if report._is_available_for(options_dict[report]): - default_expr_by_report[report].append(expr) - - external_values_create_vals = [] - for report, report_default_expressions in default_expr_by_report.items(): - options = options_dict[report] - fpos_options = {options['fiscal_position']} - - for available_fp in options['available_vat_fiscal_positions']: - fpos_options.add(available_fp['id']) - - # remove 'all' from fiscal positions if we have several of them - all will then include the sum of other fps - # but if there aren't any other fps, we need to keep 'all' - if len(fpos_options) > 1 and 'all' in fpos_options: - fpos_options.remove('all') - - # The default values should be created for every fiscal position available - for fiscal_pos in fpos_options: - fiscal_pos_id = int(fiscal_pos) if fiscal_pos not in {'domestic', 'all'} else None - fp_options = {**options, 'fiscal_position': fiscal_pos} - - expressions_to_compute = {} - for default_expression in report_default_expressions: - # The default expression needs to have the same label as the target external expression, e.g. '_default_balance' - target_label = default_expression.label[len('_default_'):] - target_external_expression = default_expression.report_line_id.expression_ids.filtered(lambda x: x.label == target_label) - # If the value has been created before/modified manually, we shouldn't create anything - # and we won't recompute expression totals for them - external_value = self.env['account.report.external.value'].search([ - ('company_id', '=', company.id), - ('date', '>=', date_from), - ('date', '<=', date_to), - ('foreign_vat_fiscal_position_id', '=', fiscal_pos_id), - ('target_report_expression_id', '=', target_external_expression.id), - ]) - - if not external_value: - expressions_to_compute[default_expression] = target_external_expression.id - - # Evaluate the expressions for the report to fetch the value of the default expression - # These have to be computed for each fiscal position - expression_totals_per_col_group = report.with_company(company)\ - ._compute_expression_totals_for_each_column_group(expressions_to_compute, fp_options, include_default_vals=True) - expression_totals = expression_totals_per_col_group[list(fp_options['column_groups'].keys())[0]] - - for expression, target_expression in expressions_to_compute.items(): - external_values_create_vals.append({ - 'name': _("Manual value"), - 'value': expression_totals[expression]['value'], - 'date': date_to, - 'target_report_expression_id': target_expression, - 'foreign_vat_fiscal_position_id': fiscal_pos_id, - 'company_id': company.id, - }) - - self.env['account.report.external.value'].create(external_values_create_vals) - - @api.model - def _get_sender_company_for_export(self, options): - """ Return the sender company when generating an export file from this report. - :return: self.env.company if not using a tax unit, else the main company of that unit - """ - if options.get('tax_unit', 'company_only') != 'company_only': - tax_unit = self.env['account.tax.unit'].browse(options['tax_unit']) - return tax_unit.main_company_id - - report_companies = self.env['res.company'].browse(self.get_report_company_ids(options)) - options_main_company = report_companies[0] - - if options.get('tax_unit') is not None and options_main_company._get_branches_with_same_vat() == report_companies: - # The line with the smallest number of parents in the VAT sub-hierarchy is assumed to be the root - return report_companies.sorted(lambda x: len(x.parent_ids))[0] - elif options_main_company._all_branches_selected(): - return options_main_company.root_id - - return options_main_company - - def _create_carryover_for_company(self, options, company, carryover_per_expression, label=None): - date_from = options['date']['date_from'] - date_to = options['date']['date_to'] - fiscal_position_opt = options['fiscal_position'] - - if carryover_per_expression and fiscal_position_opt == 'all': - # Not supported, as it wouldn't make sense, and would make the code way more complicated (because of if_below/if_above/force_between, - # just in the same way as it is explained below for multi company) - raise UserError(_("Cannot generate carryover values for all fiscal positions at once!")) - - external_values_create_vals = [] - for expression, carryover_value in carryover_per_expression.items(): - if not company.currency_id.is_zero(carryover_value): - target_expression = expression._get_carryover_target_expression(options) - external_values_create_vals.append({ - 'name': label or _("Carryover from %(date_from)s to %(date_to)s", date_from=format_date(self.env, date_from), date_to=format_date(self.env, date_to)), - 'value': carryover_value, - 'date': date_to, - 'target_report_expression_id': target_expression.id, - 'foreign_vat_fiscal_position_id': fiscal_position_opt if isinstance(fiscal_position_opt, int) else None, - 'carryover_origin_expression_label': expression.label, - 'carryover_origin_report_line_id': expression.report_line_id.id, - 'company_id': company.id, - }) - - self.env['account.report.external.value'].create(external_values_create_vals) - - def get_default_report_filename(self, options, extension): - """The default to be used for the file when downloading pdf,xlsx,...""" - self.ensure_one() - - sections_source_id = options['sections_source_id'] - if sections_source_id != self.id: - sections_source = self.env['account.report'].browse(sections_source_id) - else: - sections_source = self - - return f"{sections_source.name.lower().replace(' ', '_')}.{extension}" - - def execute_action(self, options, params=None): - action_id = int(params.get('actionId')) - action = self.env['ir.actions.actions'].sudo().browse([action_id]) - action_type = action.type - action = self.env[action.type].sudo().browse([action_id]) - action_read = clean_action(action.read()[0], env=action.env) - - if action_type == 'ir.actions.client': - # Check if we are opening another report. If so, generate options for it from the current options. - if action.tag == 'account_report': - target_report = self.env['account.report'].browse(ast.literal_eval(action_read['context'])['report_id']) - new_options = target_report.get_options(previous_options=options) - action_read.update({'params': {'options': new_options, 'ignore_session': True}}) - - if params.get('id'): - # Add the id of the calling object in the action's context - if isinstance(params['id'], int): - # id of the report line might directly be the id of the model we want. - model_id = params['id'] - else: - # It can also be a generic account.report id, as defined by _get_generic_line_id - model_id = self._get_model_info_from_id(params['id'])[1] - - context = action_read.get('context') and literal_eval(action_read['context']) or {} - context.setdefault('active_id', model_id) - action_read['context'] = context - - return action_read - - def action_audit_cell(self, options, params): - report_line = self.env['account.report.line'].browse(params['report_line_id']) - expression_label = params['expression_label'] - expression = report_line.expression_ids.filtered(lambda x: x.label == expression_label) - column_group_options = self._get_column_group_options(options, params['column_group_key']) - - # Audit of external values - if expression.engine == 'external': - date_from, date_to = self._get_date_bounds_info(column_group_options, expression.date_scope) - external_values_domain = [('target_report_expression_id', '=', expression.id), ('date', '<=', date_to)] - if date_from: - external_values_domain.append(('date', '>=', date_from)) - - if expression.formula == 'most_recent': - query = self.env['account.report.external.value']._where_calc(external_values_domain) - rows = self.env.execute_query(SQL(""" - SELECT ARRAY_AGG(id) - FROM %s - WHERE %s - GROUP BY date - ORDER BY date DESC - LIMIT 1 - """, query.from_clause, query.where_clause or SQL("TRUE"))) - if rows: - external_values_domain = [('id', 'in', rows[0][0])] - - return { - 'name': _("Manual values"), - 'type': 'ir.actions.act_window', - 'res_model': 'account.report.external.value', - 'view_mode': 'list', - 'views': [(False, 'list')], - 'domain': external_values_domain, - } - - # Audit of move lines - # If we're auditing a groupby line, we need to make sure to restrict the result of what we audit to the right group values - column = next((col for col in report_line.report_id.column_ids if col.expression_label == expression_label), self.env['account.report.column']) - if column.custom_audit_action_id: - action_dict = column.custom_audit_action_id._get_action_dict() - else: - action_dict = { - 'name': _("Journal Items"), - 'type': 'ir.actions.act_window', - 'res_model': 'account.move.line', - 'view_mode': 'list', - 'views': [(False, 'list')], - } - - action = clean_action(action_dict, env=self.env) - action['domain'] = self._get_audit_line_domain(column_group_options, expression, params) - return action - - def action_view_all_variants(self, options, params): - return { - 'name': _('All Report Variants'), - 'type': 'ir.actions.act_window', - 'res_model': 'account.report', - 'view_mode': 'list', - 'views': [(False, 'list'), (False, 'form')], - 'context': { - 'active_test': False, - }, - 'domain': [('id', 'in', self._get_variants(options['variants_source_id']).filtered( - lambda x: x._is_available_for(options) - ).mapped('id'))], - } - - def _get_audit_line_domain(self, column_group_options, expression, params): - groupby_domain = self._get_audit_line_groupby_domain(params['calling_line_dict_id']) - # Aggregate all domains per date scope, then create the final domain. - audit_or_domains_per_date_scope = {} - for expression_to_audit in expression._expand_aggregations(): - expression_domain = self._get_expression_audit_aml_domain(expression_to_audit, column_group_options) - - if expression_domain is None: - continue - - date_scope = expression.date_scope if expression.subformula == 'cross_report' else expression_to_audit.date_scope - audit_or_domains = audit_or_domains_per_date_scope.setdefault(date_scope, []) - audit_or_domains.append(osv.expression.AND([ - expression_domain, - groupby_domain, - ])) - - if audit_or_domains_per_date_scope: - domain = osv.expression.OR([ - osv.expression.AND([ - osv.expression.OR(audit_or_domains), - self._get_options_domain(column_group_options, date_scope), - groupby_domain, - ]) - for date_scope, audit_or_domains in audit_or_domains_per_date_scope.items() - ]) - else: - # Happens when no expression was provided (empty recordset), or if none of the expressions had a standard engine - domain = osv.expression.AND([ - self._get_options_domain(column_group_options, 'strict_range'), - groupby_domain, - ]) - - # Analytic Filter - if column_group_options.get("analytic_accounts"): - domain = osv.expression.AND([ - domain, - [("analytic_distribution", "in", column_group_options["analytic_accounts"])], - ]) - - return domain - - def _get_audit_line_groupby_domain(self, calling_line_dict_id): - parsed_line_dict_id = self._parse_line_id(calling_line_dict_id) - groupby_domain = [] - for markup, dummy, grouping_key in parsed_line_dict_id: - if isinstance(markup, dict) and 'groupby' in markup: - groupby_field_name = markup['groupby'] - custom_handler_model = self._get_custom_handler_model() - if custom_handler_model and (custom_groupby_data := self.env[custom_handler_model]._get_custom_groupby_map().get(groupby_field_name)): - groupby_domain += custom_groupby_data['domain_builder'](grouping_key) - else: - groupby_domain.append((groupby_field_name, '=', grouping_key)) - - return groupby_domain - - def _get_expression_audit_aml_domain(self, expression_to_audit, options): - """ Returns the domain used to audit a single provided expression. - - 'account_codes' engine's D and C formulas can't be handled by a domain: we make the choice to display - everything for them (so, audit shows all the lines that are considered by the formula). To avoid confusion from the user - when auditing such lines, a default group by account can be used in the list view. - """ - if expression_to_audit.engine == 'account_codes': - formula = expression_to_audit.formula.replace(' ', '') - - account_codes_domains = [] - for token in ACCOUNT_CODES_ENGINE_SPLIT_REGEX.split(formula.replace(' ', '')): - if token: - match_dict = ACCOUNT_CODES_ENGINE_TERM_REGEX.match(token).groupdict() - tag_match = ACCOUNT_CODES_ENGINE_TAG_ID_PREFIX_REGEX.match(match_dict['prefix']) - account_codes_domain = [] - - if tag_match: - if tag_match['ref']: - tag_id = self.env['ir.model.data']._xmlid_to_res_id(tag_match['ref']) - else: - tag_id = int(tag_match['id']) - - account_codes_domain.append(('account_id.tag_ids', 'in', [tag_id])) - else: - account_codes_domain.append(('account_id.code', '=like', f"{match_dict['prefix']}%")) - - excluded_prefix_str = match_dict['excluded_prefixes'] - if excluded_prefix_str: - for excluded_prefix in excluded_prefix_str.split(','): - # "'not like', prefix%" doesn't work - account_codes_domain += ['!', ('account_id.code', '=like', f"{excluded_prefix}%")] - - account_codes_domains.append(account_codes_domain) - - return osv.expression.OR(account_codes_domains) - - if expression_to_audit.engine == 'tax_tags': - tags = self.env['account.account.tag']._get_tax_tags(expression_to_audit.formula, expression_to_audit.report_line_id.report_id.country_id.id) - return [('tax_tag_ids', 'in', tags.ids)] - - if expression_to_audit.engine == 'domain': - return ast.literal_eval(expression_to_audit.formula) - - return None - - def open_journal_items(self, options, params): - ''' Open the journal items view with the proper filters and groups ''' - record_model, record_id = self._get_model_info_from_id(params.get('line_id')) - view_id = self.env.ref(params['view_ref']).id if params.get('view_ref') else None - - ctx = { - 'search_default_group_by_account': 1, - 'search_default_posted': 0 if options.get('all_entries') else 1, - 'date_from': options.get('date').get('date_from'), - 'date_to': options.get('date').get('date_to'), - 'search_default_journal_id': params.get('journal_id', False), - 'expand': 1, - } - - if options['date'].get('date_from'): - ctx['search_default_date_between'] = 1 - else: - ctx['search_default_date_before'] = 1 - - if options.get('selected_journal_groups'): - ctx.update({ - 'search_default_journal_group_id': [options['selected_journal_groups']['id']], - }) - - journal_type = params.get('journal_type') - if journal_type or options.get('selected_journal_groups') and options['selected_journal_groups']['journal_types']: - type_to_view_param = { - 'bank': { - 'filter': 'search_default_bank', - 'view_id': self.env.ref('account.view_move_line_tree_grouped_bank_cash').id - }, - 'cash': { - 'filter': 'search_default_cash', - 'view_id': self.env.ref('account.view_move_line_tree_grouped_bank_cash').id - }, - 'general': { - 'filter': 'search_default_misc_filter', - 'view_id': self.env.ref('account.view_move_line_tree_grouped_misc').id - }, - 'sale': { - 'filter': 'search_default_sales', - 'view_id': self.env.ref('account.view_move_line_tree_grouped_sales_purchases').id - }, - 'purchase': { - 'filter': 'search_default_purchases', - 'view_id': self.env.ref('account.view_move_line_tree_grouped_sales_purchases').id - }, - 'credit': { - 'filter': 'search_default_credit', - 'view_id': self.env.ref('account.view_move_line_tree').id - }, - } - if options.get('selected_journal_groups'): - ctx_to_update = {} - for journal_type in options['selected_journal_groups']['journal_types']: - ctx_to_update[type_to_view_param[journal_type]['filter']] = 1 - ctx.update(ctx_to_update) - else: - ctx.update({ - type_to_view_param[journal_type]['filter']: 1, - }) - view_id = type_to_view_param[journal_type]['view_id'] - - action_domain = [('display_type', 'not in', ('line_section', 'line_note'))] - - if record_id is None: - # Default filters don't support the 'no set' value. For this case, we use a domain on the action instead - model_fields_map = { - 'account.account': 'account_id', - 'res.partner': 'partner_id', - 'account.journal': 'journal_id', - } - model_field = model_fields_map.get(record_model) - if model_field: - action_domain += [(model_field, '=', False)] - else: - model_default_filters = { - 'account.account': 'search_default_account_id', - 'res.partner': 'search_default_partner_id', - 'account.journal': 'search_default_journal_id', - 'product.product': 'search_default_product_id', - 'product.category': 'search_default_product_category_id', - } - model_filter = model_default_filters.get(record_model) - if model_filter: - ctx.update({ - 'active_id': record_id, - model_filter: [record_id], - }) - - if options: - for account_type in options.get('account_type', []): - ctx.update({ - f"search_default_{account_type['id']}": account_type['selected'] and 1 or 0, - }) - - if options.get('journals') and 'search_default_journal_id' not in ctx: - selected_journals = [journal['id'] for journal in options['journals'] if journal.get('selected')] - if len(selected_journals) == 1: - ctx['search_default_journal_id'] = selected_journals - - if options.get('analytic_accounts'): - analytic_ids = [int(r) for r in options['analytic_accounts']] - ctx.update({ - 'search_default_analytic_accounts': 1, - 'analytic_ids': analytic_ids, - }) - - return { - 'name': self._get_action_name(params, record_model, record_id), - 'view_mode': 'list,pivot,graph,kanban', - 'res_model': 'account.move.line', - 'views': [(view_id, 'list')], - 'type': 'ir.actions.act_window', - 'domain': action_domain, - 'context': ctx, - } - - def open_unposted_moves(self, options, params=None): - ''' Open the list of draft journal entries that might impact the reporting''' - action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_journal_line") - action = clean_action(action, env=self.env) - action['domain'] = [('state', '=', 'draft'), ('date', '<=', options['date']['date_to'])] - #overwrite the context to avoid default filtering on 'misc' journals - action['context'] = {} - return action - - def _get_generated_deferral_entries_domain(self, options): - """Get the search domain for the generated deferral entries of the current period. - - :param options: the report's `options` dict containing `date_from`, `date_to` and `deferred_report_type` - :return: a search domain that can be used to get the deferral entries - """ - if options.get('deferred_report_type') == 'expense': - account_types = ('expense', 'expense_depreciation', 'expense_direct_cost') - else: - account_types = ('income', 'income_other') - date_to = fields.Date.from_string(options['date']['date_to']) - date_to_next_reversal = fields.Date.to_string(date_to + datetime.timedelta(days=1)) - return [ - ('company_id', '=', self.env.company.id), - # We exclude the reversal entries of the previous period that fall on the first day of this period - ('date', '>', options['date']['date_from']), - # We include the reversal entries of the current period that fall on the first day of the next period - ('date', '<=', date_to_next_reversal), - ('deferred_original_move_ids', '!=', False), - ('line_ids.account_id.account_type', 'in', account_types), - ('state', '!=', 'cancel'), - ] - - def open_deferral_entries(self, options, params): - domain = self._get_generated_deferral_entries_domain(options) - deferral_line_ids = self.env['account.move'].search(domain).line_ids.ids - return { - 'type': 'ir.actions.act_window', - 'name': _('Deferred Entries'), - 'res_model': 'account.move.line', - 'domain': [('id', 'in', deferral_line_ids)], - 'views': [(False, 'list'), (False, 'form')], - 'context': { - 'search_default_group_by_move': True, - 'expand': True, - } - } - - def action_modify_manual_value(self, line_id, options, column_group_key, new_value_str, target_expression_id, rounding, json_friendly_column_group_totals): - """ Edit a manual value from the report, updating or creating the corresponding account.report.external.value object. - - :param options: The option dict the report is evaluated with. - - :param column_group_key: The string identifying the column group into which the change as manual value needs to be done. - - :param new_value_str: The new value to be set, as a string. - - :param rounding: The number of decimal digits to round with. - - :param json_friendly_column_group_totals: The expression totals by column group already computed for this report, in the format returned - by _get_json_friendly_column_group_totals. These will be used to reevaluate the report, recomputing - only the expressions depending on the newly-modified manual value, and keeping all the results - from the previous computations for the other ones. - """ - self.ensure_one() - - target_column_group_options = self._get_column_group_options(options, column_group_key) - self._init_currency_table(target_column_group_options) - - if target_column_group_options.get('compute_budget'): - expressions_to_recompute = self.env['account.report.expression'].browse(target_expression_id) \ - + self.line_ids.expression_ids.filtered(lambda x: x.engine == 'aggregation') - self._action_modify_manual_budget_value(line_id, target_column_group_options, new_value_str, target_expression_id, rounding) - else: - expressions_to_recompute = self.line_ids.expression_ids.filtered(lambda x: x.engine in ('external', 'aggregation')) - self._action_modify_manual_external_value(target_column_group_options, new_value_str, target_expression_id, rounding) - - # We recompute values for each column group, not only the one we modified a value in; this is important in case some date_scope is used to - # retrieve the manual value from a previous period. - - all_column_groups_expression_totals = self._convert_json_friendly_column_group_totals( - json_friendly_column_group_totals, - expressions_to_exclude=expressions_to_recompute, - ) - - recomputed_expression_totals = self._compute_expression_totals_for_each_column_group( - expressions_to_recompute, options, forced_all_column_groups_expression_totals=all_column_groups_expression_totals) - - return { - 'lines': self._get_lines(options, all_column_groups_expression_totals=recomputed_expression_totals), - 'column_groups_totals': self._get_json_friendly_column_group_totals(recomputed_expression_totals), - } - - def _convert_json_friendly_column_group_totals(self, json_friendly_column_group_totals, expressions_to_exclude=None, col_groups_to_exclude=None): - """ json_friendly_column_group_totals contains ids instead of expressions (because it comes from js) ; this function is used - to convert them back to records. - """ - all_column_groups_expression_totals = {} - for column_group_key, expression_totals in json_friendly_column_group_totals.items(): - if col_groups_to_exclude and column_group_key in col_groups_to_exclude: - continue - - all_column_groups_expression_totals[column_group_key] = {} - for expr_id, expr_totals in expression_totals.items(): - expression = self.env['account.report.expression'].browse(int(expr_id)) # Should already be in cache, so acceptable - if not expressions_to_exclude or expression not in expressions_to_exclude: - all_column_groups_expression_totals[column_group_key][expression] = expr_totals - - return all_column_groups_expression_totals - - def _action_modify_manual_external_value(self, target_column_group_options, new_value_str, target_expression_id, rounding): - """ Edit a manual value from the report, updating or creating the corresponding account.report.external.value object. - - :param target_column_group_options: The options dict of the column group where the modification happened. - - :param column_group_key: The string identifying the column group into which the change as manual value needs to be done. - - :param new_value_str: The new value to be set, as a string. - - :param rounding: The number of decimal digits to round with. - - """ - if len(target_column_group_options['companies']) > 1: - raise UserError(_("Editing a manual report line is not allowed when multiple companies are selected.")) - - if target_column_group_options['fiscal_position'] == 'all' and target_column_group_options['available_vat_fiscal_positions']: - raise UserError(_("Editing a manual report line is not allowed in multivat setup when displaying data from all fiscal positions.")) - - # Create the manual value - target_expression = self.env['account.report.expression'].browse(target_expression_id) - date_from, date_to = self._get_date_bounds_info(target_column_group_options, target_expression.date_scope) - fiscal_position_id = target_column_group_options['fiscal_position'] if isinstance(target_column_group_options['fiscal_position'], int) else False - - external_values_domain = [ - ('target_report_expression_id', '=', target_expression.id), - ('company_id', '=', self.env.company.id), - ('foreign_vat_fiscal_position_id', '=', fiscal_position_id), - ] - - if target_expression.formula == 'most_recent': - value_to_adjust = 0 - existing_value_to_modify = self.env['account.report.external.value'].search([ - *external_values_domain, - ('date', '=', date_to), - ]) - - # There should be at most 1 - if len(existing_value_to_modify) > 1: - raise UserError(_("Inconsistent data: more than one external value at the same date for a 'most_recent' external line.")) - else: - existing_external_values = self.env['account.report.external.value'].search([ - *external_values_domain, - ('date', '>=', date_from), - ('date', '<=', date_to), - ], order='date ASC') - existing_value_to_modify = existing_external_values[-1] if existing_external_values and str(existing_external_values[-1].date) == date_to else None - value_to_adjust = sum(existing_external_values.filtered(lambda x: x != existing_value_to_modify).mapped('value')) - - if not new_value_str and target_expression.figure_type != 'string': - new_value_str = '0' - - try: - float(new_value_str) - is_number = True - except ValueError: - is_number = False - - if target_expression.figure_type == 'string': - value_to_set = new_value_str - else: - if not is_number: - raise UserError(_("%s is not a numeric value", new_value_str)) - if target_expression.figure_type == 'boolean': - rounding = 0 - value_to_set = float_round(float(new_value_str) - value_to_adjust, precision_digits=rounding) - - field_name = 'value' if target_expression.figure_type != 'string' else 'text_value' - - if existing_value_to_modify: - existing_value_to_modify[field_name] = value_to_set - existing_value_to_modify.flush_recordset() - else: - self.env['account.report.external.value'].create({ - 'name': _("Manual value"), - field_name: value_to_set, - 'date': date_to, - 'target_report_expression_id': target_expression.id, - 'company_id': self.env.company.id, - 'foreign_vat_fiscal_position_id': fiscal_position_id, - }) - - def _action_modify_manual_budget_value(self, line_id, target_column_group_options, new_value_str, target_expression_id, rounding): - target_expression = self.env['account.report.expression'].browse(target_expression_id) - - if not new_value_str and target_expression.figure_type != 'string': - new_value_str = '0' - - try: - value_to_set = float_round(float(new_value_str), precision_digits=rounding) - except ValueError: - raise UserError(_("%s is not a numeric value", new_value_str)) - - model, account_id = self._get_model_info_from_id(line_id) - if model != 'account.account': - raise UserError(_("Budget items can only be edited from account lines.")) - - # Depending on the expression's formula, the balance of the account could be multiplied by -1 - # within the report. We need to apply the same multiplier on the budget item we create. - if target_expression.engine == 'domain' and target_expression.subformula.startswith('-'): - value_to_set *= -1 - elif target_expression.engine == 'account_codes': - account = self.env['account.account'].browse(account_id) - - # Search for the sign to apply to this account - for token in ACCOUNT_CODES_ENGINE_SPLIT_REGEX.split(target_expression.formula.replace(' ', '')): - if not token: - continue - - token_match = ACCOUNT_CODES_ENGINE_TERM_REGEX.match(token) - multiplicator = -1 if token_match['sign'] == '-' else 1 - prefix = token_match['prefix'] - - tag_match = ACCOUNT_CODES_ENGINE_TAG_ID_PREFIX_REGEX.match(prefix) - if tag_match: - if tag_match['ref']: - tag = self.env.ref(tag_match['ref']) - else: - tag = self.env['account.account.tag'].browse(tag_match['id']) - - account_matches = tag in account.tag_ids - else: - account_matches = account.code.startswith(prefix) - - if account_matches: - value_to_set *= multiplicator - break - - self.env['account.report.budget'].browse(target_column_group_options['compute_budget'])._create_or_update_budget_items( - value_to_set, - account_id, - rounding, - target_column_group_options['date']['date_from'], - target_column_group_options['date']['date_to'], - ) - - def action_display_inactive_sections(self, options): - self.ensure_one() - - return { - 'type': 'ir.actions.act_window', - 'name': _("Enable Sections"), - 'view_mode': 'list,form', - 'res_model': 'account.report', - 'domain': [('section_main_report_ids', 'in', options['sections_source_id']), ('active', '=', False)], - 'views': [(False, 'list'), (False, 'form')], - 'context': { - 'list_view_ref': 'at_accounting.account_report_add_sections_tree', - 'active_test': False, - }, - } - - @api.model - def sort_lines(self, lines, options, result_as_index=False): - ''' Sort report lines based on the 'order_column' key inside the options. - The value of options['order_column'] is an integer, positive or negative, indicating on which column - to sort and also if it must be an ascending sort (positive value) or a descending sort (negative value). - Note that for this reason, its indexing is made starting at 1, not 0. - If this key is missing or falsy, lines is returned directly. - - This method has some limitations: - - The selected_column must have 'sortable' in its classes. - - All lines are sorted except: - - lines having the 'total' class - - static lines (lines with model 'account.report.line') - - This only works when each line has an unique id. - - All lines inside the selected_column must have a 'no_format' value. - - Example: - - parent_line_1 balance=11 - child_line_1 balance=1 - child_line_2 balance=3 - child_line_3 balance=2 - child_line_4 balance=7 - child_line_5 balance=4 - child_line_6 (total line) - parent_line_2 balance=10 - child_line_7 balance=5 - child_line_8 balance=6 - child_line_9 (total line) - - - The resulting lines will be: - - parent_line_2 balance=10 - child_line_7 balance=5 - child_line_8 balance=6 - child_line_9 (total line) - parent_line_1 balance=11 - child_line_1 balance=1 - child_line_3 balance=2 - child_line_2 balance=3 - child_line_5 balance=4 - child_line_4 balance=7 - child_line_6 (total line) - - :param lines: The report lines. - :param options: The report options. - :return: Lines sorted by the selected column. - ''' - def needs_to_be_at_bottom(line_elem): - return self._get_markup(line_elem.get('id')) in ('total', 'load_more') - - def compare_values(a_line, b_line): - if column_index is False: - return 0 - type_seq = { - type(None): 0, - bool: 1, - float: 2, - int: 2, - str: 3, - datetime.date: 4, - datetime.datetime: 5, - } - - a_line_dict = lines[a_line] if result_as_index else a_line - b_line_dict = lines[b_line] if result_as_index else b_line - a_total = needs_to_be_at_bottom(a_line_dict) - b_total = needs_to_be_at_bottom(b_line_dict) - a_model = self._get_model_info_from_id(a_line_dict['id'])[0] - b_model = self._get_model_info_from_id(b_line_dict['id'])[0] - - # static lines are not sorted - if a_model == b_model == 'account.report.line': - return 0 - - if a_total: - if b_total: # a_total & b_total - return 0 - else: # a_total & !b_total - return -1 if descending else 1 - if b_total: # => !a_total & b_total - return 1 if descending else -1 - - a_val = a_line_dict['columns'][column_index].get('no_format') - b_val = b_line_dict['columns'][column_index].get('no_format') - type_a, type_b = type_seq[type(a_val)], type_seq[type(b_val)] - - if type_a == type_b: - return 0 if a_val == b_val else 1 if a_val > b_val else -1 - else: - return type_a - type_b - - def merge_tree(tree_elem, ls): - nonlocal descending # The direction of the sort is needed to compare total lines - ls.append(tree_elem) - - elem = tree[lines[tree_elem]['id']] if result_as_index else tree[tree_elem['id']] - - for tree_subelem in sorted(elem, key=comp_key, reverse=descending): - merge_tree(tree_subelem, ls) - - descending = options['order_column']['direction'] == 'DESC' # To keep total lines at the end, used in compare_values & merge_tree scopes - - column_index = False - for index, col in enumerate(options['columns']): - if options['order_column']['expression_label'] == col['expression_label']: - column_index = index # To know from which column to sort, used in merge_tree scope - break - - comp_key = cmp_to_key(compare_values) - sorted_list = [] - tree = defaultdict(list) - non_total_parents = set() - - for index, line in enumerate(lines): - line_parent = line.get('parent_id') or None - - if result_as_index: - tree[line_parent].append(index) - else: - tree[line_parent].append(line) - - line_markup = self._get_markup(line['id']) - - if line_markup != 'total': - non_total_parents.add(line_parent) - - if None not in tree and len(non_total_parents) == 1: - # Happens when unfolding a groupby line, to sort its children. - sorting_root = next(iter(non_total_parents)) - else: - sorting_root = None - - for line in sorted(tree[sorting_root], key=comp_key, reverse=descending): - merge_tree(line, sorted_list) - - return sorted_list - - def _get_annotations_domain_date_from(self, options): - if options['date']['filter'] in {'today', 'custom'} and options['date']['mode'] == 'single': - options_company_ids = [company['id'] for company in options['companies']] - root_companies_ids = self.env['res.company'].browse(options_company_ids).root_id.ids - fiscal_year = self.env['account.fiscal.year'].search_fetch([ - ('company_id', 'in', root_companies_ids), - ('date_from', '<=', options['date']['date_to']), - ('date_to', '>=', options['date']['date_to']), - ], limit=1, field_names=['date_from']) - if fiscal_year: - return datetime.datetime.combine(fiscal_year.date_from, datetime.time.min) - - period_date_from, _ = date_utils.get_fiscal_year( - datetime.datetime.strptime(options['date']['date_to'], '%Y-%m-%d'), - day=self.env.company.fiscalyear_last_day, - month=int(self.env.company.fiscalyear_last_month) - ) - return period_date_from - - date_from = datetime.datetime.strptime(options['date']['date_from'], '%Y-%m-%d') - if options['date']['period_type'] == "fiscalyear": - period_date_from, _ = date_utils.get_fiscal_year(date_from) - elif options['date']['period_type'] in ["year", "quarter", "month", "week", "day", "hour"]: - period_date_from = date_utils.start_of(date_from, options['date']['period_type']) - else: - period_date_from = date_from - return period_date_from - - def _adjust_date_for_joined_comparison(self, options, period_date_from): - comparison_filter = options.get('comparison', {}).get('filter') - if comparison_filter == 'previous_period': - comparison_date_from = datetime.datetime.strptime(options['comparison'].get('periods', [{}])[-1].get('date_from'), '%Y-%m-%d') - return min(period_date_from, comparison_date_from) - return period_date_from - - def _adjust_domain_for_unjoined_comparison(self, options, dates_domain): - comparison_filter = options.get('comparison', {}).get('filter') - if comparison_filter and comparison_filter not in {'no_comparison', 'previous_period'}: - unlinked_comparison_periods_domains_list = [ - ['&', ('date', '>=', period['date_from']), ('date', '<=', period['date_to'])] - for period in options['comparison']['periods'] - ] - dates_domain = osv.expression.OR([dates_domain, *unlinked_comparison_periods_domains_list]) - - return dates_domain - - def _build_annotations_domain(self, options): - domain = [('report_id', '=', options['report_id'])] - if options.get('date'): - period_date_from = self._get_annotations_domain_date_from(options) - period_date_from = self._adjust_date_for_joined_comparison(options, period_date_from) - dates_domain = osv.expression.AND([ - [('date', '>=', period_date_from)], - [('date', '<=', options['date']['date_to'])], - ]) - dates_domain = self._adjust_domain_for_unjoined_comparison(options, dates_domain) - - domain = osv.expression.AND([ - domain, - osv.expression.OR([ - [('date', '=', False)], - dates_domain, - ]), - ]) - - fiscal_position_option = options.get('fiscal_position') - if isinstance(fiscal_position_option, int): - domain = osv.expression.AND([domain, [('fiscal_position_id', '=', fiscal_position_option)]]) - elif fiscal_position_option == 'domestic': - domain = osv.expression.AND([domain, [('fiscal_position_id', '=', False)]]) - return domain - - def get_annotations(self, options): - """ - This method handles which annotations have to be displayed on the report. - This decision is based on the different dates and mode of display of those dates in the report. - - param options: dict of options used to generate the report - return: dict of lists containing for each annotated line_id of the report the list of annotations linked to it - """ - self.ensure_one() - annotations_by_line = defaultdict(list) - annotations = self.env['account.report.annotation'].search_read(self._build_annotations_domain(options)) - for annotation in annotations: - line_id_without_tax_grouping = self.env['account.report.annotation']._remove_tax_grouping_from_line_id(annotation['line_id']) - annotation['create_date'] = annotation['create_date'].date() - annotations_by_line[line_id_without_tax_grouping].append(annotation) - return annotations_by_line - - def get_report_information(self, options): - """ - return a dictionary of information that will be consumed by the AccountReport component. - """ - self.ensure_one() - - warnings = {} - self._init_currency_table(options) - all_column_groups_expression_totals = self._compute_expression_totals_for_each_column_group(self.line_ids.expression_ids, options, warnings=warnings) - - # Convert all_column_groups_expression_totals to a json-friendly form (its keys are records) - json_friendly_column_group_totals = self._get_json_friendly_column_group_totals(all_column_groups_expression_totals) - - if self.custom_handler_model_name: - custom_display_config = self.env[self.custom_handler_model_name]._get_custom_display_config() - elif self.root_report_id and self.root_report_id.custom_handler_model_name: - custom_display_config = self.env[self.root_report_id.custom_handler_model_name]._get_custom_display_config() - else: - custom_display_config = {} - - return { - 'caret_options': self._get_caret_options(), - 'column_headers_render_data': self._get_column_headers_render_data(options), - 'column_groups_totals': json_friendly_column_group_totals, - 'context': self.env.context, - 'custom_display': custom_display_config, - 'filters': { - 'show_all': self.filter_unfold_all, - 'show_analytic': options.get('display_analytic', False), - 'show_analytic_groupby': options.get('display_analytic_groupby', False), - 'show_analytic_plan_groupby': options.get('display_analytic_plan_groupby', False), - 'show_draft': self.filter_show_draft, - 'show_hierarchy': options.get('display_hierarchy_filter', False), - 'show_period_comparison': self.filter_period_comparison, - 'show_totals': self.env.company.totals_below_sections and not options.get('ignore_totals_below_sections'), - 'show_unreconciled': self.filter_unreconciled, - 'show_hide_0_lines': self.filter_hide_0_lines, - }, - 'annotations': self.get_annotations(options), - 'groups': { - 'analytic_accounting': self.env.user.has_group('analytic.group_analytic_accounting'), - 'account_readonly': self.env.user.has_group('account.group_account_readonly'), - 'account_user': self.env.user.has_group('account.group_account_user'), - }, - 'lines': self._get_lines(options, all_column_groups_expression_totals=all_column_groups_expression_totals, warnings=warnings), - 'warnings': warnings, - 'report': { - 'company_name': self.env.company.name, - 'company_country_code': self.env.company.country_code, - 'company_currency_symbol': self.env.company.currency_id.symbol, - 'name': self.name, - 'root_report_id': self.root_report_id, - } - } - - @api.readonly - def get_report_information_readonly(self, options): - """ Readonly version of get_report_information, to be called from RPC when options['readonly_query'] is True, - to better spread the load on servers when possible. - """ - return self.get_report_information(options) - - def _get_json_friendly_column_group_totals(self, all_column_groups_expression_totals): - # Convert all_column_groups_expression_totals to a json-friendly form (its keys are records) - json_friendly_column_group_totals = {} - for column_group_key, expressions_totals in all_column_groups_expression_totals.items(): - json_friendly_column_group_totals[column_group_key] = {expression.id: totals for expression, totals in expressions_totals.items()} - return json_friendly_column_group_totals - - def _is_available_for(self, options): - """ Called on report variants to know whether they are available for the provided options or not, computed for their root report, - computing their availability_condition field. - - Note that only the options initialized by the init_options with a more prioritary sequence than _init_options_variants are guaranteed to - be in the provided options' dict (since this function is called by _init_options_variants, while resolving a call to get_options()). - """ - self.ensure_one() - - companies = self.env['res.company'].browse(self.get_report_company_ids(options)) - - if self.availability_condition == 'country': - countries = companies.account_fiscal_country_id - if self.filter_fiscal_position: - foreign_vat_fpos = self.env['account.fiscal.position'].search([ - ('foreign_vat', '!=', False), - ('company_id', 'in', companies.ids), - ]) - countries += foreign_vat_fpos.country_id - - return not self.country_id or self.country_id in countries - - elif self.availability_condition == 'coa': - # When restricting to 'coa', the report is only available is all the companies have the same CoA as the report - return self.chart_template in set(companies.mapped('chart_template')) - - return True - - def _get_column_headers_render_data(self, options): - column_headers_render_data = {} - - # We only want to consider the columns that are visible in the current report and don't rely on self.column_ids - # since custom reports could alter them (e.g. for multi-currency purposes) - columns = [col for col in options['columns'] if col['column_group_key'] == next(k for k in options['column_groups'])] - - # Compute the colspan of each header level, aka the number of single columns it contains at the base of the hierarchy - level_colspan_list = column_headers_render_data['level_colspan'] = [] - for i in range(len(options['column_headers'])): - colspan = max(len(columns), 1) - for column_header in options['column_headers'][i + 1:]: - # Separate non-budget and budget headers - budget_count = sum( - any(key in header.get('forced_options', {}) for key in ('compute_budget', 'budget_percentage')) - for header in column_header - ) - non_budget_count = len(column_header) - budget_count - - # budget headers (amount and percentage) can only contain a single column each, regardless of the amount of columns in the report. - # This implies that we first need to multiply for the 'regular' columns and then add the budget columns. - colspan *= non_budget_count - colspan += budget_count - - level_colspan_list.append(colspan) - - # Compute the number of times each header level will have to be repeated, and its colspan to properly handle horizontal groups/comparisons - column_headers_render_data['level_repetitions'] = [] - for i in range(len(options['column_headers'])): - colspan = 1 - for column_header in options['column_headers'][:i]: - colspan *= len(column_header) - column_headers_render_data['level_repetitions'].append(colspan) - - # Custom reports have the possibility to define custom subheaders that will be displayed between the generic header and the column names. - column_headers_render_data['custom_subheaders'] = options.get('custom_columns_subheaders', []) * len(options['column_groups']) - - return column_headers_render_data - - def _get_action_name(self, params, record_model=None, record_id=None): - if not (record_model or record_id): - record_model, record_id = self._get_model_info_from_id(params.get('line_id')) - return params.get('name') or self.env[record_model].browse(record_id).display_name or '' - - def _format_lines_for_display(self, lines, options): - """ - This method should be overridden in a report in order to apply specific formatting when printing - the report lines. - - Used for example by the carryover functionnality in the generic tax report. - :param lines: A list with the lines for this report. - :param options: The options for this report. - :return: The formatted list of lines - """ - return lines - - def get_expanded_lines(self, options, line_dict_id, groupby, expand_function_name, progress, offset, horizontal_split_side): - self.env.flush_all() - self._init_currency_table(options) - - lines = self._expand_unfoldable_line(expand_function_name, line_dict_id, groupby, options, progress, offset, horizontal_split_side) - lines = self._fully_unfold_lines_if_needed(lines, options) - - if self.custom_handler_model_id: - lines = self.env[self.custom_handler_model_name]._custom_line_postprocessor(self, options, lines) - - self._format_column_values(options, lines) - return lines - - @api.readonly - def get_expanded_lines_readonly(self, options, line_dict_id, groupby, expand_function_name, progress, offset, horizontal_split_side): - """ Readonly version of get_expanded_lines_readonly, to be called from RPC when options['readonly_query'] is True, - to better spread the load on servers when possible. - """ - return self.get_expanded_lines(options, line_dict_id, groupby, expand_function_name, progress, offset, horizontal_split_side) - - def _expand_unfoldable_line(self, expand_function_name, line_dict_id, groupby, options, progress, offset, horizontal_split_side, unfold_all_batch_data=None): - if not expand_function_name: - raise UserError(_("Trying to expand a line without an expansion function.")) - - if not progress: - progress = {column_group_key: 0 for column_group_key in options['column_groups']} - - expand_function = self._get_custom_report_function(expand_function_name, 'expand_unfoldable_line') - expansion_result = expand_function(line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=unfold_all_batch_data) - - rslt = expansion_result['lines'] - - if horizontal_split_side: - for line in rslt: - line['horizontal_split_side'] = horizontal_split_side - - # Apply integer rounding to the result if needed. - # The groupby expansion function is the only one guaranteed to call the expressions computation, - # so the values computed for it will already have been rounded if integer rounding is enabled. No need to round them again. - if expand_function_name != '_report_expand_unfoldable_line_with_groupby': - self._apply_integer_rounding_to_dynamic_lines(options, rslt) - - if expansion_result.get('has_more'): - # We only add load_more line for groupby - next_offset = offset + expansion_result['offset_increment'] - rslt.append(self._get_load_more_line(next_offset, line_dict_id, expand_function_name, groupby, expansion_result.get('progress', 0), options)) - - # In some specific cases, we may want to add lines that are always at the end. So they need to be added after the load more line. - if expansion_result.get('after_load_more_lines'): - rslt.extend(expansion_result['after_load_more_lines']) - - return self._add_totals_below_sections(rslt, options) - - def _add_totals_below_sections(self, lines, options): - """ Returns a new list, corresponding to lines with the required total lines added as sublines of the sections it contains. - """ - if not self.env.company.totals_below_sections or options.get('ignore_totals_below_sections'): - return lines - - # Gather the lines needing the totals - lines_needing_total_below = set() - for line_dict in lines: - line_markup = self._get_markup(line_dict['id']) - - if line_markup != 'total': - # If we are on the first level of an expandable line, we arelady generate its total - if line_dict.get('unfoldable') or (line_dict.get('unfolded') and line_dict.get('expand_function')): - lines_needing_total_below.add(line_dict['id']) - - # All lines that are parent of other lines need to receive a total - line_parent_id = line_dict.get('parent_id') - if line_parent_id: - lines_needing_total_below.add(line_parent_id) - - # Inject the totals - if lines_needing_total_below: - lines_with_totals_below = [] - totals_below_stack = [] - for line_dict in lines: - while totals_below_stack and not line_dict['id'].startswith(totals_below_stack[-1]['parent_id'] + LINE_ID_HIERARCHY_DELIMITER): - lines_with_totals_below.append(totals_below_stack.pop()) - - lines_with_totals_below.append(line_dict) - - if line_dict['id'] in lines_needing_total_below and any(col.get('no_format') is not None for col in line_dict['columns']): - totals_below_stack.append(self._generate_total_below_section_line(line_dict)) - - while totals_below_stack: - lines_with_totals_below.append(totals_below_stack.pop()) - - return lines_with_totals_below - - return lines - - @api.model - def _get_load_more_line(self, offset, parent_line_id, expand_function_name, groupby, progress, options): - """ Returns a 'Load more' line allowing to reach the subsequent elements of an unfolded line with an expand function if the maximum - limit of sublines is reached (we load them by batch, using the load_more_limit field's value). - - :param offset: The offset to be passed to the expand function to generate the next results, when clicking on this 'load more' line. - - :param parent_line_id: The generic id of the line this load more line is created for. - - :param expand_function_name: The name of the expand function this load_more is created for (so, the one of its parent). - - :param progress: A json-formatted dict(column_group_key, value) containing the progress value for each column group, as it was - returned by the expand function. This is for example used by reports such as the general ledger, whose lines display a c - cumulative sum of their balance and the one of all the previous lines under the same parent. In this case, progress - will be the total sum of all the previous lines before the load_more line, that the subsequent lines will need to use as - base for their own cumulative sum. - - :param options: The options dict corresponding to this report's state. - """ - return { - 'id': self._get_generic_line_id(None, None, parent_line_id=parent_line_id, markup='load_more'), - 'name': _("Load more..."), - 'parent_id': parent_line_id, - 'expand_function': expand_function_name, - 'columns': [{} for col in options['columns']], - 'unfoldable': False, - 'unfolded': False, - 'offset': offset, - 'groupby': groupby, # We keep the groupby value from the parent, so that it can be propagated through js - 'progress': progress, - } - - def _report_expand_unfoldable_line_with_groupby(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None): - # The line we're expanding might be an inner groupby; we first need to find the report line generating it - report_line_id = None - for dummy, model, model_id in reversed(self._parse_line_id(line_dict_id)): - if model == 'account.report.line': - report_line_id = model_id - break - - if report_line_id is None: - raise UserError(_("Trying to expand a group for a line which was not generated by a report line: %s", line_dict_id)) - - line = self.env['account.report.line'].browse(report_line_id) - - if ',' not in groupby and options['export_mode'] is None: - # if ',' not in groupby, then its a terminal groupby (like 'id' in 'partner_id, id'), so we can use the 'load more' feature if necessary - # When printing, we want to ignore the limit. - limit_to_load = self.load_more_limit or None - else: - # Else, we disable it - limit_to_load = None - offset = 0 - - rslt_lines = line._expand_groupby(line_dict_id, groupby, options, offset=offset, limit=limit_to_load, load_one_more=bool(limit_to_load), unfold_all_batch_data=unfold_all_batch_data) - lines_to_load = rslt_lines[:self.load_more_limit] if limit_to_load else rslt_lines - - if not limit_to_load and options['export_mode'] is None: - lines_to_load = self._regroup_lines_by_name_prefix(options, rslt_lines, '_report_expand_unfoldable_line_groupby_prefix_group', line.hierarchy_level, - groupby=groupby, parent_line_dict_id=line_dict_id) - - return { - 'lines': lines_to_load, - 'offset_increment': len(lines_to_load), - 'has_more': len(lines_to_load) < len(rslt_lines) if limit_to_load else False, - } - - def _regroup_lines_by_name_prefix(self, options, lines_to_group, expand_function_name, parent_level, matched_prefix='', groupby=None, parent_line_dict_id=None): - """ Postprocesses a list of report line dictionaries in order to regroup them by name prefix and reduce the overall number of lines - if their number is above a provided threshold (set in the report configuration). - - The lines regrouped under a common prefix will be removed from the returned list of lines; only the prefix line will stay, folded. - Its expand function must ensure the right sublines are reloaded when unfolding it. - - :param options: Option dict for this report. - :lines_to_group: The lines list to regroup by prefix if necessary. They must all have the same parent line (which might be no line at all). - :expand_function_name: Name of the expand function to be called on created prefix group lines, when unfolding them - :parent_level: Level of the parent line, which generated the lines in lines_to_group. It will be used to compute the level of the prefix group lines. - :matched_prefix': A string containing the parent prefix that's already matched. For example, when computing prefix 'ABC', matched_prefix will be 'AB'. - :groupby: groupby value of the parent line, which generated the lines in lines_to_group. - :parent_line_dict_id: id of the parent line, which generated the lines in lines_to_group. - - :return: lines_to_group, grouped by prefix if it was necessary. - """ - threshold = options['prefix_groups_threshold'] - - # When grouping by prefix, we ignore the totals - lines_to_group_without_totals = list(filter(lambda x: self._get_markup(x['id']) != 'total', lines_to_group)) - - if options['export_mode'] == 'print' or threshold <= 0 or len(lines_to_group_without_totals) < threshold: - # No grouping needs to be done - return lines_to_group - - char_index = len(matched_prefix) - prefix_groups = defaultdict(list) - rslt = [] - for line in lines_to_group_without_totals: - line_name = line['name'].strip() - - if len(line_name) - 1 < char_index: - rslt.append(line) - else: - prefix_groups[line_name[char_index].lower()].append(line) - - float_figure_types = {'monetary', 'integer', 'float'} - unfold_all = options['export_mode'] == 'print' or options.get('unfold_all') - for prefix_key, prefix_sublines in sorted(prefix_groups.items(), key=lambda x: x[0]): - # Compute the total of this prefix line, summming all of its content - prefix_expression_totals_by_group = {} - for column_index, column_data in enumerate(options['columns']): - if column_data['figure_type'] in float_figure_types: - # Then we want to sum this column's value in our children - for prefix_subline in prefix_sublines: - prefix_expr_label_result = prefix_expression_totals_by_group.setdefault(column_data['column_group_key'], {}) - prefix_expr_label_result.setdefault(column_data['expression_label'], 0) - prefix_expr_label_result[column_data['expression_label']] += (prefix_subline['columns'][column_index]['no_format'] or 0) - - column_values = [] - for column in options['columns']: - col_value = prefix_expression_totals_by_group.get(column['column_group_key'], {}).get(column['expression_label']) - - column_values.append(self._build_column_dict(col_value, column, options=options)) - - line_id = self._get_generic_line_id(None, None, parent_line_id=parent_line_dict_id, markup={'groupby_prefix_group': prefix_key}) - - sublines_nber = len(prefix_sublines) - prefix_to_display = prefix_key.upper() - - if re.match(r'\s', prefix_to_display[-1]): - # In case the last character of the prefix to_display is blank, replace it by "[ ]", to make the space more visible to the user. - prefix_to_display = f'{prefix_to_display[:-1]}[ ]' - - if sublines_nber == 1: - prefix_group_line_name = f"{matched_prefix}{prefix_to_display} " + _("(1 line)") - else: - prefix_group_line_name = f"{matched_prefix}{prefix_to_display} " + _("(%s lines)", sublines_nber) - - prefix_group_line = { - 'id': line_id, - 'name': prefix_group_line_name, - 'unfoldable': True, - 'unfolded': unfold_all or line_id in options['unfolded_lines'], - 'columns': column_values, - 'groupby': groupby, - 'level': parent_level + 1, - 'parent_id': parent_line_dict_id, - 'expand_function': expand_function_name, - 'hide_line_buttons': True, - } - rslt.append(prefix_group_line) - - return rslt - - def _report_expand_unfoldable_line_groupby_prefix_group(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None): - """ Expand function used by prefix_group lines generated for groupby lines. - """ - report_line_id = None - parent_groupby_count = 0 - for markup, model, model_id in reversed(self._parse_line_id(line_dict_id)): - if model == 'account.report.line': - report_line_id = model_id - break - elif isinstance(markup, dict) and 'groupby' in markup or 'groupby_prefix_group' in markup: - parent_groupby_count += 1 - - if report_line_id is None: - raise UserError(_("Trying to expand a group for a line which was not generated by a report line: %s", line_dict_id)) - - report_line = self.env['account.report.line'].browse(report_line_id) - - - matched_prefix = self._get_prefix_groups_matched_prefix_from_line_id(line_dict_id) - first_groupby = groupby.split(',')[0] - expand_options = { - **options, - 'forced_domain': options.get('forced_domain', []) + [(f"{f'{first_groupby}.' if first_groupby != 'id' else ''}name", '=ilike', f'{matched_prefix}%')] - } - expanded_groupby_lines = report_line._expand_groupby(line_dict_id, groupby, expand_options) - parent_level = report_line.hierarchy_level + parent_groupby_count * 2 - - lines = self._regroup_lines_by_name_prefix( - options, - expanded_groupby_lines, - '_report_expand_unfoldable_line_groupby_prefix_group', - parent_level, - groupby=groupby, - matched_prefix=matched_prefix, - parent_line_dict_id=line_dict_id, - ) - - return { - 'lines': lines, - 'offset_increment': len(lines), - 'has_more': False, - } - - @api.model - def _get_prefix_groups_matched_prefix_from_line_id(self, line_dict_id): - matched_prefix = '' - for markup, dummy1, dummy2 in self._parse_line_id(line_dict_id): - if markup and isinstance(markup, dict) and 'groupby_prefix_group' in markup: - prefix_piece = markup['groupby_prefix_group'] - matched_prefix += prefix_piece.upper() - else: - # Might happen if a groupby is grouped by prefix, then a subgroupby is grouped by another subprefix. - # In this case, we want to reset the prefix group to only consider the one used in the subgroupby. - matched_prefix = '' - - return matched_prefix - - @api.model - def format_value(self, options, value, figure_type, format_params=None): - if format_params is None: - format_params = {} - - if 'currency' in format_params: - format_params['currency'] = self.env['res.currency'].browse(format_params['currency'].id) - - return self._format_value(options=options, value=value, figure_type=figure_type, format_params=format_params) - - def _format_value(self, options, value, figure_type, format_params=None): - """ Formats a value for display in a report (not especially numerical). figure_type provides the type of formatting we want. - """ - if value is None: - return '' - - if figure_type == 'none': - return value - - if isinstance(value, str) or figure_type == 'string': - return str(value) - - if format_params is None: - format_params = {} - - formatLang_params = { - 'rounding_method': 'HALF-UP', - 'rounding_unit': options.get('rounding_unit'), - } - - if figure_type == 'monetary': - currency = self.env['res.currency'].browse(format_params['currency_id']) if 'currency_id' in format_params else self.env.company.currency_id - if options.get('multi_currency'): - formatLang_params['currency_obj'] = currency - else: - formatLang_params['digits'] = currency.decimal_places - - elif figure_type == 'integer': - formatLang_params['digits'] = 0 - - elif figure_type == 'boolean': - return _("Yes") if bool(value) else _("No") - - elif figure_type in ('date', 'datetime'): - return format_date(self.env, value) - - else: - formatLang_params['digits'] = format_params.get('digits', 1) - - if self._is_value_zero(value, figure_type, format_params): - # Make sure -0.0 becomes 0.0 - value = abs(value) - - if self._context.get('no_format'): - return value - - formatted_amount = formatLang(self.env, value, **formatLang_params) - - if figure_type == 'percentage': - return f"{formatted_amount}%" - - return formatted_amount - - @api.model - def _is_value_zero(self, amount, figure_type, format_params): - if amount is None: - return True - - if figure_type == 'monetary': - currency = self.env['res.currency'].browse(format_params['currency_id']) if 'currency_id' in format_params else self.env.company.currency_id - return currency.is_zero(amount) - elif figure_type in NUMBER_FIGURE_TYPES: - return float_is_zero(amount, precision_digits=format_params.get('digits', 0)) - else: - return False - - def format_date(self, options, dt_filter='date'): - date_from = fields.Date.from_string(options[dt_filter]['date_from']) - date_to = fields.Date.from_string(options[dt_filter]['date_to']) - return self._get_dates_period(date_from, date_to, options['date']['mode'])['string'] - - def export_file(self, options, file_generator): - self.ensure_one() - - export_options = {**options, 'export_mode': 'file'} - - return { - 'type': 'ir_actions_account_report_download', - 'data': { - 'options': json.dumps(export_options), - 'file_generator': file_generator, - } - } - - def _get_report_send_recipients(self, options): - custom_handler_model = self._get_custom_handler_model() - if custom_handler_model and hasattr(self.env[custom_handler_model], '_get_report_send_recipients'): - return self.env[custom_handler_model]._get_report_send_recipients(options) - return self.env['res.partner'] - - def export_to_pdf(self, options): - self.ensure_one() - - base_url = self.env['ir.config_parameter'].sudo().get_param('report.url') or self.env['ir.config_parameter'].sudo().get_param('web.base.url') - rcontext = { - 'mode': 'print', - 'base_url': base_url, - 'company': self.env.company, - } - - print_options = self.get_options(previous_options={**options, 'export_mode': 'print'}) - if print_options['sections']: - reports_to_print = self.env['account.report'].browse([section['id'] for section in print_options['sections']]) - else: - reports_to_print = self - - reports_options = [] - for report in reports_to_print: - reports_options.append(report.get_options(previous_options={**print_options, 'selected_section_id': report.id})) - - grouped_reports_by_format = groupby( - zip(reports_to_print, reports_options), - key=lambda report: len(report[1]['columns']) > 5 or report[1].get('horizontal_split') - ) - - footer = self.env['ir.actions.report']._render_template("at_accounting.internal_layout", values=rcontext) - footer = self.env['ir.actions.report']._render_template("web.minimal_layout", values=dict(rcontext, subst=True, body=markupsafe.Markup(footer.decode()))) - - action_report = self.env['ir.actions.report'] - files_stream = [] - for is_landscape, reports_with_options in grouped_reports_by_format: - bodies = [] - - for report, report_options in reports_with_options: - bodies.append(report._get_pdf_export_html( - report_options, - report._filter_out_folded_children(report._get_lines(report_options)), - additional_context={'base_url': base_url} - )) - - files_stream.append( - io.BytesIO(action_report._run_wkhtmltopdf( - bodies, - footer=footer.decode(), - landscape=is_landscape or self._context.get('force_landscape_printing'), - specific_paperformat_args={ - 'data-report-margin-top': 10, - 'data-report-header-spacing': 10, - 'data-report-margin-bottom': 15, - } - ) - )) - - if len(files_stream) > 1: - result_stream = action_report._merge_pdfs(files_stream) - result = result_stream.getvalue() - # Close the different stream - result_stream.close() - for file_stream in files_stream: - file_stream.close() - else: - result = files_stream[0].read() - - return { - 'file_name': self.get_default_report_filename(options, 'pdf'), - 'file_content': result, - 'file_type': 'pdf', - } - - def _get_pdf_export_html(self, options, lines, additional_context=None, template=None): - report_info = self.get_report_information(options) - - custom_print_templates = report_info['custom_display'].get('pdf_export', {}) - template = custom_print_templates.get('pdf_export_main', 'at_accounting.pdf_export_main') - - render_values = { - 'report': self, - 'report_title': self.name, - 'options': options, - 'table_start': markupsafe.Markup(''), - 'table_end': markupsafe.Markup(''' - -
-
- - '''), - 'column_headers_render_data': self._get_column_headers_render_data(options), - 'custom_templates': custom_print_templates, - } - if additional_context: - render_values.update(additional_context) - - if options.get('order_column'): - lines = self.sort_lines(lines, options) - - lines = self._format_lines_for_display(lines, options) - - render_values['lines'] = lines - - # Manage annotations. - render_values['annotations'] = self._build_annotations_list_for_pdf_export(options['date'], lines, report_info['annotations']) - - options['css_custom_class'] = report_info['custom_display'].get('css_custom_class', '') - - # Render. - return self.env['ir.qweb']._render(template, render_values) - - def _build_annotations_list_for_pdf_export(self, date_options, lines, annotations_per_line_id): - annotations_to_render = [] - number = 0 - for line in lines: - if line_annotations := annotations_per_line_id.get(line['id']): - line['annotations'] = [] - for annotation in line_annotations: - report_period_date_from = datetime.datetime.strptime(date_options['date_from'], '%Y-%m-%d').date() - report_period_date_to = datetime.datetime.strptime(date_options['date_to'], '%Y-%m-%d').date() - if not annotation['date'] or report_period_date_from <= annotation['date'] <= report_period_date_to: - number += 1 - line['annotations'].append(str(number)) - annotations_to_render.append({ - 'number': str(number), - 'text': annotation['text'], - 'date': format_date(self.env, annotation['date']) if annotation['date'] else None, - }) - return annotations_to_render - - def _filter_out_folded_children(self, lines): - """ Returns a list containing all the lines of the provided list that need to be displayed when printing, - hence removing the children whose parent is folded (especially useful to remove total lines). - """ - rslt = [] - folded_lines = set() - for line in lines: - if line.get('unfoldable') and not line.get('unfolded'): - folded_lines.add(line['id']) - - if 'parent_id' not in line or line['parent_id'] not in folded_lines: - rslt.append(line) - return rslt - - def export_to_xlsx(self, options, response=None): - self.ensure_one() - output = io.BytesIO() - workbook = xlsxwriter.Workbook(output, { - 'in_memory': True, - 'strings_to_formulas': False, - }) - - print_options = self.get_options(previous_options={**options, 'export_mode': 'print'}) - if print_options['sections']: - reports_to_print = self.env['account.report'].browse([section['id'] for section in print_options['sections']]) - else: - reports_to_print = self - - reports_options = [] - for report in reports_to_print: - report_options = report.get_options(previous_options={**print_options, 'selected_section_id': report.id}) - reports_options.append(report_options) - report._inject_report_into_xlsx_sheet(report_options, workbook, workbook.add_worksheet(report.name[:31])) - - self._add_options_xlsx_sheet(workbook, reports_options) - - workbook.close() - output.seek(0) - generated_file = output.read() - output.close() - - return { - 'file_name': self.get_default_report_filename(options, 'xlsx'), - 'file_content': generated_file, - 'file_type': 'xlsx', - } - - @api.model - def _set_xlsx_cell_sizes(self, sheet, fonts, col, row, value, style, has_colspan): - """ This small helper will resize the cells if needed, to allow to get a better output. """ - def get_string_width(font, string): - return font.getlength(string) / 5 - - # Get the correct font for the row style - font_type = ('Bol' if style.bold else 'Reg') + ('Ita' if style.italic else '') - report_font = fonts[font_type] - - # 8.43 is the default width of a column in Excel. - if parse_version(xlsxwriter.__version__) >= parse_version('3.0.6'): - # cols_sizes was removed in 3.0.6 and colinfo was replaced by col_info - # see https://github.com/jmcnamara/XlsxWriter/commit/860f4a2404549aca1eccf9bf8361df95dc574f44 - try: - col_width = sheet.col_info[col][0] - except KeyError: - col_width = 8.43 - else: - col_width = sheet.col_sizes.get(col, [8.43])[0] - - row_height = sheet.row_sizes.get(row, [8.43])[0] - - if value is None: - value = '' - else: - try: # noqa: SIM105 - # This is needed, otherwise we could compute width on very long number such as 12.0999999998 - # which wouldn't show well in the end result as the numbers are rounded. - value = float_repr(float(value), self.env.company.currency_id.decimal_places) - except ValueError: - pass - - # Start by computing the width of the cell if we are not using colspans. - if not has_colspan: - # Ensure to take indents into account when computing the width. - formatted_value = f"{' ' * style.indent}{value}" - width = get_string_width( - report_font, - max(formatted_value.split('\n'), key=lambda line: get_string_width(report_font, line)) - ) - # We set the width if it is bigger than the current one, with a limit at 75 (max to avoid taking excessive space). - if width > col_width: - sheet.set_column(col, col, min(width + 4, 75)) # We need to add a little extra padding to ensure our columns are not clipping the text - - def _inject_report_into_xlsx_sheet(self, options, workbook, sheet): - - # We start by gathering the bold, italic and regular fonts to use later. - fonts = {} - for font_type in ('Reg', 'Bol', 'RegIta', 'BolIta'): - try: - lato_path = f'web/static/fonts/lato/Lato-{font_type}-webfont.ttf' - fonts[font_type] = ImageFont.truetype(file_path(lato_path), 12) - except (OSError, FileNotFoundError): - # This won't give great result, but it will work. - fonts[font_type] = ImageFont.load_default() - - def write_cell(sheet, x, y, value, style, colspan=1, datetime=False): - self._set_xlsx_cell_sizes(sheet, fonts, x, y, value, style, colspan > 1) - if colspan == 1: - if datetime: - sheet.write_datetime(y, x, value, style) - else: - sheet.write(y, x, value, style) - else: - sheet.merge_range(y, x, y, x + colspan - 1, value, style) - - date_default_col1_style = workbook.add_format({'font_name': 'Lato', 'align': 'left', 'font_size': 12, 'font_color': '#666666', 'indent': 2, 'num_format': 'yyyy-mm-dd'}) - date_default_style = workbook.add_format({'font_name': 'Lato', 'align': 'left', 'font_size': 12, 'font_color': '#666666', 'num_format': 'yyyy-mm-dd'}) - default_col1_style = workbook.add_format({'font_name': 'Lato', 'font_size': 12, 'font_color': '#666666', 'indent': 2}) - default_col2_style = workbook.add_format({'font_name': 'Lato', 'font_size': 12, 'font_color': '#666666'}) - default_style = workbook.add_format({'font_name': 'Lato', 'font_size': 12, 'font_color': '#666666'}) - annotation_style = workbook.add_format({'font_name': 'Lato', 'font_size': 12, 'font_color': '#666666', 'text_wrap': True}) - title_style = workbook.add_format({'font_name': 'Lato', 'font_size': 12, 'bold': True, 'bottom': 2}) - level_0_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 13, 'bottom': 6, 'font_color': '#666666'}) - level_1_col1_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 13, 'bottom': 1, 'font_color': '#666666', 'indent': 1}) - level_1_col1_total_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 13, 'bottom': 1, 'font_color': '#666666'}) - level_1_col2_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 13, 'bottom': 1, 'font_color': '#666666'}) - level_1_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 13, 'bottom': 1, 'font_color': '#666666'}) - level_2_col1_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 12, 'font_color': '#666666', 'indent': 2}) - level_2_col1_total_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 12, 'font_color': '#666666', 'indent': 1}) - level_2_col2_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 12, 'font_color': '#666666', 'indent': 1}) - level_2_style = workbook.add_format({'font_name': 'Lato', 'bold': True, 'font_size': 12, 'font_color': '#666666'}) - col1_styles = {} - - print_mode_self = self.with_context(no_format=True) - lines = self._filter_out_folded_children(print_mode_self._get_lines(options)) - annotations = self.get_annotations(options) - - # For reports with lines generated for accounts, the account name and codes are shown in a single column. - # To help user post-process the report if they need, we should in such a case split the account name and code in two columns. - account_lines_split_names = {} - for line in lines: - line_model = self._get_model_info_from_id(line['id'])[0] - if line_model == 'account.account': - # Reuse the _split_code_name to split the name and code in two values. - account_lines_split_names[line['id']] = self.env['account.account']._split_code_name(line['name']) - - original_x_offset = 1 if len(account_lines_split_names) > 0 else 0 - - y_offset = 0 - # 1 and not 0 to leave space for the line name. original_x_offset allows making place for the code column if needed. - x_offset = original_x_offset + 1 - - # Add headers. - # For this, iterate in the same way as done in main_table_header template - column_headers_render_data = self._get_column_headers_render_data(options) - for header_level_index, header_level in enumerate(options['column_headers']): - for header_to_render in header_level * column_headers_render_data['level_repetitions'][header_level_index]: - colspan = header_to_render.get('colspan', column_headers_render_data['level_colspan'][header_level_index]) - write_cell(sheet, x_offset, y_offset, header_to_render.get('name', ''), title_style, colspan + (1 if options['show_horizontal_group_total'] and header_level_index == 0 else 0)) - x_offset += colspan - if options.get('column_percent_comparison') == 'growth': - write_cell(sheet, x_offset, y_offset, '%', title_style) - x_offset += 1 - - if options['show_horizontal_group_total'] and header_level_index != 0: - horizontal_group_name = next((group['name'] for group in options['available_horizontal_groups'] if group['id'] == options['selected_horizontal_group_id']), None) - write_cell(sheet, x_offset, y_offset, horizontal_group_name, title_style) - x_offset += 1 - if annotations: - annotations_x_offset = x_offset - write_cell(sheet, annotations_x_offset, y_offset, 'Annotations', title_style) - x_offset += 1 - y_offset += 1 - x_offset = original_x_offset + 1 - - for subheader in column_headers_render_data['custom_subheaders']: - colspan = subheader.get('colspan', 1) - write_cell(sheet, x_offset, y_offset, subheader.get('name', ''), title_style, colspan) - x_offset += colspan - y_offset += 1 - x_offset = original_x_offset + 1 - - if account_lines_split_names: - # If we have a separate account code column, add a title for it - write_cell(sheet, x_offset - 1, y_offset, _("Account Code"), title_style) - - for column in options['columns']: - colspan = column.get('colspan', 1) - write_cell(sheet, x_offset, y_offset, column.get('name', ''), title_style, colspan) - x_offset += colspan - - if options['show_horizontal_group_total']: - write_cell(sheet, x_offset, y_offset, options['columns'][0].get('name', ''), title_style, colspan) - - if options.get('column_percent_comparison') == 'growth': - write_cell(sheet, x_offset, y_offset, '', title_style, colspan) - - y_offset += 1 - - if options.get('order_column'): - lines = self.sort_lines(lines, options) - - # Add lines. - counter = 1 - for y in range(0, len(lines)): - level = lines[y].get('level') - is_total_line = 'total' in lines[y].get('class', '').split(' ') - if level == 0: - y_offset += 1 - style = level_0_style - col1_style = style - col2_style = style - elif level == 1: - style = level_1_style - col1_style = level_1_col1_total_style if is_total_line else level_1_col1_style - col2_style = level_1_col2_style - elif level == 2: - style = level_2_style - col1_style = level_2_col1_total_style if is_total_line else level_2_col1_style - col2_style = level_2_col2_style - elif level and level >= 3: - style = default_style - col2_style = style - level_col1_styles = col1_styles.get(level) - if not level_col1_styles: - level_col1_styles = col1_styles[level] = { - 'default': workbook.add_format( - {'font_name': 'Lato', 'font_size': 12, 'font_color': '#666666', 'indent': level} - ), - 'total': workbook.add_format( - { - 'font_name': 'Lato', - 'bold': True, - 'font_size': 12, - 'font_color': '#666666', - 'indent': level - 1, - } - ), - } - col1_style = level_col1_styles['total'] if is_total_line else level_col1_styles['default'] - else: - style = default_style - col1_style = default_col1_style - col2_style = default_col2_style - - # write the first column, with a specific style to manage the indentation - x_offset = original_x_offset + 1 - if lines[y]['id'] in account_lines_split_names: - code, name = account_lines_split_names[lines[y]['id']] - write_cell(sheet, 0, y + y_offset, name, col1_style) - write_cell(sheet, 1, y + y_offset, code, col2_style) - else: - cell_type, cell_value = self._get_cell_type_value(lines[y]) - if cell_type == 'date': - write_cell(sheet, 0, y + y_offset, cell_value, date_default_col1_style, datetime=True) - else: - write_cell(sheet, 0, y + y_offset, cell_value, col1_style) - - if lines[y].get('parent_id') and lines[y]['parent_id'] in account_lines_split_names: - write_cell(sheet, 1, y + y_offset, account_lines_split_names[lines[y]['parent_id']][0], col2_style) - elif account_lines_split_names: - write_cell(sheet, 1, y + y_offset, "", col2_style) - - #write all the remaining cells - columns = lines[y]['columns'] - if options.get('column_percent_comparison') and 'column_percent_comparison_data' in lines[y]: - columns += [lines[y].get('column_percent_comparison_data')] - - if options['show_horizontal_group_total']: - columns += [lines[y].get('horizontal_group_total_data', {'name': 0})] - - for x, column in enumerate(columns, start=x_offset): - cell_type, cell_value = self._get_cell_type_value(column) - if cell_type == 'date': - write_cell(sheet, x + lines[y].get('colspan', 1) - 1, y + y_offset, cell_value, date_default_style, datetime=True) - else: - write_cell(sheet, x + lines[y].get('colspan', 1) - 1, y + y_offset, cell_value, style) - - # Write annotations. - if annotations and (line_annotations := annotations.get(lines[y]['id'])): - line_annotation_text = [] - for line_annotation in line_annotations: - line_annotation_text.append(f"{counter} - {line_annotation['text']}") - counter += 1 - write_cell(sheet, annotations_x_offset, y + y_offset, "\n".join(line_annotation_text), annotation_style) - - def _add_options_xlsx_sheet(self, workbook, options_list): - """Adds a new sheet for xlsx report exports with a summary of all filters and options activated at the moment of the export.""" - filters_sheet = workbook.add_worksheet(_("Filters")) - # Set first and second column widths. - filters_sheet.set_column(0, 0, 20) - filters_sheet.set_column(1, 1, 50) - name_style = workbook.add_format({'font_name': 'Arial', 'bold': True, 'bottom': 2}) - y_offset = 0 - - if len(options_list) == 1: - self.env['account.report'].browse(options_list[0]['report_id'])._inject_report_options_into_xlsx_sheet(options_list[0], filters_sheet, y_offset) - return - - # Find uncommon keys - options_sets = list(map(set, options_list)) - common_keys = set.intersection(*options_sets) - all_keys = set.union(*options_sets) - uncommon_options_keys = all_keys - common_keys - # Try to find the common filter values between all reports to avoid duplication. - common_options_values = {} - for key in common_keys: - first_value = options_list[0][key] - if all(options[key] == first_value for options in options_list[1:]): - common_options_values[key] = first_value - else: - uncommon_options_keys.add(key) - - # Write common options to the sheet. - filters_sheet.write(y_offset, 0, _("All"), name_style) - y_offset += 1 - y_offset = self._inject_report_options_into_xlsx_sheet(common_options_values, filters_sheet, y_offset) - - for report_options in options_list: - report = self.env['account.report'].browse(report_options['report_id']) - - filters_sheet.write(y_offset, 0, report.name, name_style) - y_offset += 1 - new_offset = report._inject_report_options_into_xlsx_sheet(report_options, filters_sheet, y_offset, uncommon_options_keys) - - if y_offset == new_offset: - y_offset -= 1 - # Clear the report name's cell since it didn't add any data to the xlsx. - filters_sheet.write(y_offset, 0, " ") - else: - y_offset = new_offset - - def _inject_report_options_into_xlsx_sheet(self, options, sheet, y_offset, options_to_print=None): - """ - Injects the report options into the filters sheet. - - :param options: Dictionary containing report options. - :param sheet: XLSX sheet to inject options into. - :param y_offset: Offset for the vertical position in the sheet. - :param options_to_print: Optional list of names to print. If not provided, all printable options will be included. - """ - def write_filter_lines(filter_title, filter_lines, y_offset): - sheet.write(y_offset, 0, filter_title) - for line in filter_lines: - sheet.write(y_offset, 1, line) - y_offset += 1 - return y_offset - - def should_print_option(option_key): - """Check if the option should be printed based on options_to_print.""" - return not options_to_print or option_key in options_to_print - - # Company - if should_print_option('companies'): - companies = options['companies'] - title = _("Companies") if len(companies) > 1 else _("Company") - lines = [company['name'] for company in companies] - y_offset = write_filter_lines(title, lines, y_offset) - - # Journals - if should_print_option('journals') and (journals := options.get('journals')): - journal_titles = [journal.get('title') for journal in journals if journal.get('selected')] - if journal_titles: - y_offset = write_filter_lines(_("Journals"), journal_titles, y_offset) - - # Partners - if should_print_option('selected_partner_ids') and (partner_names := options.get('selected_partner_ids')): - y_offset = write_filter_lines(_("Partners"), partner_names, y_offset) - - # Partner categories - if should_print_option('selected_partner_categories') and (partner_categories := options.get('selected_partner_categories')): - y_offset = write_filter_lines(_("Partner Categories"), partner_categories, y_offset) - - # Horizontal groups - if should_print_option('selected_horizontal_group_id') and (group_id := options.get('selected_horizontal_group_id')): - for horizontal_group in options['available_horizontal_groups']: - if horizontal_group['id'] == group_id: - filter_name = horizontal_group['name'] - y_offset = write_filter_lines(_("Horizontal Group"), [filter_name], y_offset) - break - - # Currency - if should_print_option('company_currency') and options.get('company_currency'): - y_offset = write_filter_lines(_("Company Currency"), [options['company_currency']['currency_name']], y_offset) - - # Filters - if should_print_option('aml_ir_filters'): - if options.get('aml_ir_filters') and any(opt['selected'] for opt in options['aml_ir_filters']): - filter_names = [opt['name'] for opt in options['aml_ir_filters'] if opt['selected']] - y_offset = write_filter_lines(_("Filters"), filter_names, y_offset) - - # Extra options - # Array of tuples for the extra options: (name, option_key, condition) - extra_options = [ - (_("With Draft Entries"), 'all_entries', self.filter_show_draft), - (_("Unreconciled Entries"), 'unreconciled', self.filter_unreconciled), - (_("Including Analytic Simulations"), 'include_analytic_without_aml', True) - ] - filter_names = [ - name for name, option_key, condition in extra_options - if (not options_to_print or option_key in options_to_print) and condition and options.get(option_key) - ] - if filter_names: - y_offset = write_filter_lines(_("Options"), filter_names, y_offset) - - return y_offset - - def _get_cell_type_value(self, cell): - if 'date' not in cell.get('class', '') or not cell.get('name'): - # cell is not a date - return ('text', cell.get('name', '')) - if isinstance(cell['name'], (float, datetime.date, datetime.datetime)): - # the date is xlsx compatible - return ('date', cell['name']) - try: - # the date is parsable to a xlsx compatible date - lg = get_lang(self.env, self.env.user.lang) - return ('date', datetime.datetime.strptime(cell['name'], lg.date_format)) - except: - # the date is not parsable thus is returned as text - return ('text', cell['name']) - - def get_vat_for_export(self, options, raise_warning=True): - """ Returns the VAT number to use when exporting this report with the provided - options. If a single fiscal_position option is set, its VAT number will be - used; else the current company's will be, raising an error if its empty. - """ - self.ensure_one() - - if self.filter_multi_company == 'tax_units' and options['tax_unit'] != 'company_only': - tax_unit = self.env['account.tax.unit'].browse(options['tax_unit']) - return tax_unit.vat - - if options['fiscal_position'] in {'all', 'domestic'}: - company = self._get_sender_company_for_export(options) - if not company.vat and raise_warning: - action = self.env.ref('base.action_res_company_form') - raise RedirectWarning(_('No VAT number associated with your company. Please define one.'), action.id, _("Company Settings")) - return company.vat - - fiscal_position = self.env['account.fiscal.position'].browse(options['fiscal_position']) - return fiscal_position.foreign_vat - - @api.model - def get_report_company_ids(self, options): - """ Returns a list containing the ids of the companies to be used to - render this report, following the provided options. - """ - return [comp_data['id'] for comp_data in options['companies']] - - def _get_partner_and_general_ledger_initial_balance_line(self, options, parent_line_id, eval_dict, account_currency=None, level_shift=0): - """ Helper to generate dynamic 'initial balance' lines, used by general ledger and partner ledger. - """ - line_columns = [] - for column in options['columns']: - col_value = eval_dict[column['column_group_key']].get(column['expression_label']) - col_expr_label = column['expression_label'] - - if col_value is None or (col_expr_label == 'amount_currency' and not account_currency): - line_columns.append(self._build_column_dict(None, None)) - else: - line_columns.append(self._build_column_dict( - col_value, - column, - options=options, - currency=account_currency if col_expr_label == 'amount_currency' else None, - )) - - # Display unfold & initial balance even when debit/credit column is hidden and the balance == 0 - if not any(isinstance(column.get('no_format'), (int, float)) and column.get('expression_label') != 'balance' for column in line_columns): - return None - - return { - 'id': self._get_generic_line_id(None, None, parent_line_id=parent_line_id, markup='initial'), - 'name': _("Initial Balance"), - 'level': 3 + level_shift, - 'parent_id': parent_line_id, - 'columns': line_columns, - } - - def _compute_column_percent_comparison_data(self, options, value1, value2, green_on_positive=True): - ''' Helper to get the additional columns due to the growth comparison feature. When only one comparison is - requested, an additional column is there to show the percentage of growth based on the compared period. - :param options: The report options. - :param value1: The value in the current period. - :param value2: The value in the compared period. - :param green_on_positive: A flag customizing the value with a green color depending if the growth is positive. - :return: The new columns to add to line['columns']. - ''' - if value1 is None or value2 is None or float_is_zero(value2, precision_rounding=0.1): - return {'name': _('n/a'), 'mode': 'muted'} - - comparison_type = options['column_percent_comparison'] - if comparison_type == 'growth': - - values_diff = value1 - value2 - growth = round(values_diff / value2 * 100, 1) - - # In case the comparison is made on a negative figure, the color should be the other - # way around. For example: - # 2018 2017 % - # Product Sales 1000.00 -1000.00 -200.0% - # - # The percentage is negative, which is mathematically correct, but my sales increased - # => it should be green, not red! - if float_is_zero(growth, 1): - return {'name': '0.0%', 'mode': 'muted'} - else: - return { - 'name': f"{float_repr(growth, 1)}%", - 'mode': 'red' if ((values_diff > 0) ^ green_on_positive) else 'green', - } - - elif comparison_type == 'budget': - percentage_value = value1 / value2 * 100 - if float_is_zero(percentage_value, 1): - # To avoid negative 0 - return {'name': '0.0%', 'mode': 'green'} - - comparison_value = float_compare(value1, value2, 1) - return { - 'name': f"{float_repr(percentage_value, 1)}%", - 'mode': 'green' if (comparison_value >= 0 and green_on_positive) or (comparison_value == -1 and not green_on_positive) else 'red', - } - - def _set_budget_column_comparisons(self, options, line): - """ - Set the percentage values in the budget columns - """ - for col_index, col in enumerate(line['columns']): - col_group_data = options['column_groups'][col['column_group_key']] - if 'budget_percentage' in col_group_data.get('forced_options'): - budget_id = col_group_data['forced_options']['budget_percentage'] - date_key = col_group_data.get('forced_options', {}).get('date') - if not date_key: - continue - - budget_base_col = None - budget_amount_col = None - for line_col in line['columns']: - other_col_group_key = line_col['column_group_key'] - other_col_options = options['column_groups'][other_col_group_key] - if other_col_options.get('forced_options', {}).get('date') == date_key: - if other_col_options.get('forced_options', {}).get('budget_base') and line_col['figure_type'] == 'monetary': - budget_base_col = line_col - elif other_col_options.get('forced_options', {}).get('compute_budget') == budget_id: - budget_amount_col = line_col - - value = self._compute_column_percent_comparison_data( - options, - budget_base_col['no_format'], - budget_amount_col['no_format'], - green_on_positive=budget_base_col['green_on_positive'], - ) - comparison_column = self._build_column_dict( - value['name'], - { - **budget_amount_col, - 'figure_type': 'string', - 'comparison_mode': value['mode'], - } - ) - line['columns'][col_index] = comparison_column - - def _check_groupby_fields(self, groupby_fields_name: list[str] | str): - """ Checks that each string in the groupby_fields_name list is a valid groupby value for an accounting report (so: it must be a field from - account.move.line, or a custom value allowed by the _get_custom_groupby_map function of the custom handler). - """ - self.ensure_one() - if isinstance(groupby_fields_name, str | bool): - groupby_fields_name = groupby_fields_name.split(',') if groupby_fields_name else [] - for field_name in (fname.strip() for fname in groupby_fields_name): - groupby_field = self.env['account.move.line']._fields.get(field_name) - custom_handler_name = self._get_custom_handler_model() - - if groupby_field: - if not groupby_field.store: - raise UserError(_("Field %s of account.move.line is not stored, and hence cannot be used in a groupby expression", field_name)) - elif custom_handler_name: - if field_name not in self.env[custom_handler_name]._get_custom_groupby_map(): - raise UserError(_("Field %s does not exist on account.move.line, and is not supported by this report's custom handler.", field_name)) - else: - raise UserError(_("Field %s does not exist on account.move.line.", field_name)) - - # ============ Accounts Coverage Debugging Tool - START ================ - @api.depends('country_id', 'chart_template', 'root_report_id') - def _compute_is_account_coverage_report_available(self): - for report in self: - report.is_account_coverage_report_available = ( - ( - self.availability_condition == 'country' and self.env.company.account_fiscal_country_id == self.country_id - or - self.availability_condition == 'coa' and self.env.company.chart_template == self.chart_template - or - self.availability_condition == 'always' - ) - and - self.root_report_id in ( - self.env.ref('at_accounting.profit_and_loss', raise_if_not_found=False), - self.env.ref('at_accounting.balance_sheet', raise_if_not_found=False) - ) - ) - - def action_download_xlsx_accounts_coverage_report(self): - """ - Generate an XLSX file that can be used to debug the - report by issuing the following warnings if applicable: - - an account exists in the Chart of Accounts but is not mentioned in any line of the report (red) - - an account is reported in multiple lines of the report (orange) - - an account is reported in a line of the report but does not exist in the Chart of Accounts (yellow) - """ - self.ensure_one() - if not self.is_account_coverage_report_available: - raise UserError(_("The Accounts Coverage Report is not available for this report.")) - - output = io.BytesIO() - workbook = xlsxwriter.Workbook(output, {'in_memory': True}) - worksheet = workbook.add_worksheet(_('Accounts coverage')) - worksheet.set_column(0, 0, 20) - worksheet.set_column(1, 1, 75) - worksheet.set_column(2, 2, 80) - worksheet.freeze_panes(1, 0) - - headers = [_("Account Code / Tag"), _("Error message"), _("Report lines mentioning the account code"), '#FFFFFF'] - lines = [headers] + self._generate_accounts_coverage_report_xlsx_lines() - for i, line in enumerate(lines): - worksheet.write_row(i, 0, line[:-1], workbook.add_format({'bg_color': line[-1]})) - - workbook.close() - attachment_id = self.env['ir.attachment'].create({ - 'name': f"{self.display_name} - {_('Accounts Coverage Report')}", - 'datas': base64.encodebytes(output.getvalue()) - }) - return { - "type": "ir.actions.act_url", - "url": f"/web/content/{attachment_id.id}", - "target": "download", - } - - def _generate_accounts_coverage_report_xlsx_lines(self): - """ - Generate the lines of the XLSX file that can be used to debug the - report by issuing the following warnings if applicable: - - an account exists in the Chart of Accounts but is not mentioned in any line of the report (red) - - an account is reported in multiple lines of the report (orange) - - an account is reported in a line of the report but does not exist in the Chart of Accounts (yellow) - """ - def get_account_domain(prefix): - # Helper function to get the right domain to find the account - # This function verifies if we have to look for a tag or if we have - # to look for an account code. - if tag_matching := ACCOUNT_CODES_ENGINE_TAG_ID_PREFIX_REGEX.match(prefix): - if tag_matching['ref']: - account_tag_id = self.env['ir.model.data']._xmlid_to_res_id(tag_matching['ref']) - else: - account_tag_id = int(tag_matching['id']) - return 'tag_ids', 'in', (account_tag_id,) - else: - return 'code', '=like', f'{prefix}%' - - self.ensure_one() - - all_reported_accounts = self.env["account.account"] # All accounts mentioned in the report (including those reported without using the account code) - accounts_by_expressions = {} # {expression_id: account.account objects} - reported_account_codes = [] # [{'prefix': ..., 'balance': ..., 'exclude': ..., 'line': ...}, ...] - non_existing_codes = defaultdict(lambda: self.env["account.report.line"]) # {non_existing_account_code: {lines_with_that_code,}} - lines_per_non_linked_tag = defaultdict(lambda: self.env['account.report.line']) - lines_using_bad_operator_per_tag = defaultdict(lambda: self.env['account.report.line']) - candidate_duplicate_codes = defaultdict(lambda: self.env["account.report.line"]) # {candidate_duplicate_account_code: {lines_with_that_code,}} - duplicate_codes = defaultdict(lambda: self.env["account.report.line"]) # {verified duplicate_account_code: {lines_with_that_code,}} - duplicate_codes_same_line = defaultdict(lambda: self.env["account.report.line"]) # {duplicate_account_code: {line_with_that_code_multiple_times,}} - common_account_domain = [ - *self.env['account.account']._check_company_domain(self.env.company), - ('deprecated', '=', False), - ] - - # tag_ids already linked to an account - avoid several search_count to know if the tag is used or not - tag_ids_linked_to_account = set(self.env['account.account'].search([('tag_ids', '!=', False)]).tag_ids.ids) - - expressions = self.line_ids.expression_ids._expand_aggregations() - for i, expr in enumerate(expressions): - reported_accounts = self.env["account.account"] - if expr.engine == "domain": - domain = literal_eval(expr.formula.strip()) - accounts_domain = [] - for j, operand in enumerate(domain): - if isinstance(operand, tuple): - operand = list(operand) - # Skip tuples that will not be used in the new domain to retrieve the reported accounts - if not operand[0].startswith('account_id.'): - if domain[j - 1] in ("&", "|", "!"): # Remove the operator linked to the tuple if it exists - accounts_domain.pop() - continue - operand[0] = operand[0].replace('account_id.', '') - # Check that the code exists in the CoA - if operand[0] == 'code' and not self.env["account.account"].search_count([operand]): - non_existing_codes[operand[2]] |= expr.report_line_id - elif operand[0] == 'tag_ids': - tag_ids = operand[2] - if not isinstance(tag_ids, (list, tuple, set)): - tag_ids = [tag_ids] - - if operand[1] in ('=', 'in'): - tag_ids_to_browse = [tag_id for tag_id in tag_ids if tag_id not in tag_ids_linked_to_account] - for tag in self.env['account.account.tag'].browse(tag_ids_to_browse): - lines_per_non_linked_tag[f'{tag.name} ({tag.id})'] |= expr.report_line_id - else: - for tag in self.env['account.account.tag'].browse(tag_ids): - lines_using_bad_operator_per_tag[f'{tag.name} ({tag.id}) - Operator: {operand[1]}'] |= expr.report_line_id - - accounts_domain.append(operand) - reported_accounts += self.env['account.account'].search(accounts_domain) - elif expr.engine == "account_codes": - account_codes = [] - for token in ACCOUNT_CODES_ENGINE_SPLIT_REGEX.split(expr.formula.replace(' ', '')): - if not token: - continue - token_match = ACCOUNT_CODES_ENGINE_TERM_REGEX.match(token) - if not token_match: - continue - - parsed_token = token_match.groupdict() - account_codes.append({ - 'prefix': parsed_token['prefix'], - 'balance': parsed_token['balance_character'], - 'exclude': parsed_token['excluded_prefixes'].split(',') if parsed_token['excluded_prefixes'] else [], - 'line': expr.report_line_id, - }) - - for account_code in account_codes: - reported_account_codes.append(account_code) - exclude_domain_accounts = [get_account_domain(exclude_code) for exclude_code in account_code['exclude']] - reported_accounts += self.env["account.account"].search([ - *common_account_domain, - get_account_domain(account_code['prefix']), - *[excl_domain for excl_tuple in exclude_domain_accounts for excl_domain in ("!", excl_tuple)], - ]) - - # Check that the code exists in the CoA or that the tag is linked to an account - prefixes_to_check = [account_code['prefix']] + account_code['exclude'] - for prefix_to_check in prefixes_to_check: - account_domain = get_account_domain(prefix_to_check) - if not self.env["account.account"].search_count([ - *common_account_domain, - account_domain, - ]): - # Identify if we're working with account codes or account tags - if account_domain[0] == 'code': - non_existing_codes[prefix_to_check] |= account_code['line'] - elif account_domain[0] == 'tag_ids': - lines_per_non_linked_tag[prefix_to_check] |= account_code['line'] - - all_reported_accounts |= reported_accounts - accounts_by_expressions[expr.id] = reported_accounts - - # Check if an account is reported multiple times in the same line of the report - if len(reported_accounts) != len(set(reported_accounts)): - seen = set() - for reported_account in reported_accounts: - if reported_account not in seen: - seen.add(reported_account) - else: - duplicate_codes_same_line[reported_account.code] |= expr.report_line_id - - # Check if the account is reported in multiple lines of the report - for expr2 in expressions[:i + 1]: - reported_accounts2 = accounts_by_expressions[expr2.id] - for duplicate_account in (reported_accounts & reported_accounts2): - if len(expr.report_line_id | expr2.report_line_id) > 1 \ - and expr.date_scope == expr2.date_scope \ - and expr.subformula == expr2.subformula: - candidate_duplicate_codes[duplicate_account.code] |= expr.report_line_id | expr2.report_line_id - - # Check that the duplicates are not false positives because of the balance character - for candidate_duplicate_code, candidate_duplicate_lines in candidate_duplicate_codes.items(): - seen_balance_chars = [] - for reported_account_code in reported_account_codes: - if candidate_duplicate_code.startswith(reported_account_code['prefix']) and reported_account_code['balance']: - seen_balance_chars.append(reported_account_code['balance']) - if not seen_balance_chars or seen_balance_chars.count("C") > 1 or seen_balance_chars.count("D") > 1: - duplicate_codes[candidate_duplicate_code] |= candidate_duplicate_lines - - # Check that all codes in CoA are correctly reported - if self.root_report_id == self.env.ref('at_accounting.profit_and_loss'): - accounts_in_coa = self.env["account.account"].search([ - *common_account_domain, - ('account_type', 'in', ("income", "income_other", "expense", "expense_depreciation", "expense_direct_cost")), - ('account_type', '!=', "off_balance"), - ]) - else: # Balance Sheet - accounts_in_coa = self.env["account.account"].search([ - *common_account_domain, - ('account_type', 'not in', ("off_balance", "income", "income_other", "expense", "expense_depreciation", "expense_direct_cost")) - ]) - - # Compute codes that exist in the CoA but are not reported in the report - non_reported_codes = set((accounts_in_coa - all_reported_accounts).mapped('code')) - - # Create the lines that will be displayed in the xlsx - all_reported_codes = sorted(set(all_reported_accounts.mapped("code")) | non_reported_codes | non_existing_codes.keys()) - errors_trie = self._get_accounts_coverage_report_errors_trie(all_reported_codes, non_reported_codes, duplicate_codes, duplicate_codes_same_line, non_existing_codes) - errors_trie['children'].update(**self._get_account_tag_coverage_report_errors_trie(lines_per_non_linked_tag, lines_using_bad_operator_per_tag)) # Add tags that are not linked to an account - - errors_trie = self._regroup_accounts_coverage_report_errors_trie(errors_trie) - return self._get_accounts_coverage_report_coverage_lines("", errors_trie) - - def _get_accounts_coverage_report_errors_trie(self, all_reported_codes, non_reported_codes, duplicate_codes, duplicate_codes_same_line, non_existing_codes): - """ - Create the trie that will be used to regroup the same errors on the same subcodes. - This trie will be in the form of: - { - "children": { - "1": { - "children": { - "10": { ... }, - "11": { ... }, - }, - "lines": { - "Line1", - "Line2", - }, - "errors": { - "DUPLICATE" - } - }, - "lines": { - "", - }, - "errors": { - None # Avoid that all codes are merged into the root with the code "" in case all of the errors are the same - }, - } - """ - errors_trie = {"children": {}, "lines": {}, "errors": {None}} - for reported_code in all_reported_codes: - current_trie = errors_trie - lines = self.env["account.report.line"] - errors = set() - if reported_code in non_reported_codes: - errors.add("NON_REPORTED") - elif reported_code in duplicate_codes_same_line: - lines |= duplicate_codes_same_line[reported_code] - errors.add("DUPLICATE_SAME_LINE") - elif reported_code in duplicate_codes: - lines |= duplicate_codes[reported_code] - errors.add("DUPLICATE") - elif reported_code in non_existing_codes: - lines |= non_existing_codes[reported_code] - errors.add("NON_EXISTING") - else: - errors.add("NONE") - - for j in range(1, len(reported_code) + 1): - current_trie = current_trie["children"].setdefault(reported_code[:j], { - "children": {}, - "lines": lines, - "errors": errors - }) - return errors_trie - - @api.model - def _get_account_tag_coverage_report_errors_trie(self, lines_per_non_linked_tag, lines_per_bad_operator_tag): - """ As we don't want to make a hierarchy for tags, we use a specific - function to handle tags. - """ - errors = { - non_linked_tag: { - 'children': {}, - 'lines': line, - 'errors': {'NON_LINKED'}, - } - for non_linked_tag, line in lines_per_non_linked_tag.items() - } - errors.update({ - bad_operator_tag: { - 'children': {}, - 'lines': line, - 'errors': {'BAD_OPERATOR'}, - } - for bad_operator_tag, line in lines_per_bad_operator_tag.items() - }) - return errors - - def _regroup_accounts_coverage_report_errors_trie(self, trie): - """ - Regroup the codes that have the same error under the same common subcode/prefix. - This is done in-place on the given trie. - """ - if trie.get("children"): - children_errors = set() - children_lines = self.env["account.report.line"] - if trie.get("errors"): # Add own error - children_errors |= set(trie.get("errors")) - for child in trie["children"].values(): - regroup = self._regroup_accounts_coverage_report_errors_trie(child) - children_lines |= regroup["lines"] - children_errors |= set(regroup["errors"]) - if len(children_errors) == 1 and children_lines and children_lines == trie["lines"]: - trie["children"] = {} - trie["lines"] = children_lines - trie["errors"] = children_errors - return trie - - def _get_accounts_coverage_report_coverage_lines(self, subcode, trie, coverage_lines=None): - """ - Create the coverage lines from the grouped trie. Each line has - - the account code - - the error message - - the lines on which the account code is used - - the color of the error message for the xlsx - """ - # Dictionnary of the three possible errors, their message and the corresponding color for the xlsx file - ERRORS = { - "NON_REPORTED": { - "msg": _("This account exists in the Chart of Accounts but is not mentioned in any line of the report"), - "color": "#FF0000" - }, - "DUPLICATE": { - "msg": _("This account is reported in multiple lines of the report"), - "color": "#FF8916" - }, - "DUPLICATE_SAME_LINE": { - "msg": _("This account is reported multiple times on the same line of the report"), - "color": "#E6A91D" - }, - "NON_EXISTING": { - "msg": _("This account is reported in a line of the report but does not exist in the Chart of Accounts"), - "color": "#FFBF00" - }, - "NON_LINKED": { - "msg": _("This tag is reported in a line of the report but is not linked to any account of the Chart of Accounts"), - "color": "#FFBF00", - }, - "BAD_OPERATOR": { - "msg": _("The used operator is not supported for this expression."), - "color": "#FFBF00", - } - } - if coverage_lines is None: - coverage_lines = [] - if trie.get("children"): - for child in trie.get("children"): - self._get_accounts_coverage_report_coverage_lines(child, trie["children"][child], coverage_lines) - else: - error = list(trie["errors"])[0] if trie["errors"] else False - if error and error != "NONE": - coverage_lines.append([ - subcode, - ERRORS[error]["msg"], - " + ".join(trie["lines"].sorted().mapped("name")), - ERRORS[error]["color"] - ]) - return coverage_lines - - # ============ Accounts Coverage Debugging Tool - END ================ - - def _generate_file_data_with_error_check(self, options, content_generator, generator_params, errors): - """ Checks for critical errors (i.e. errors that would cause the rendering to fail) in the generator values. - If at least one error is critical, the 'account.report.file.download.error.wizard' wizard is opened - before rendering the file, so they can be fixed. - If there are only non-critical errors, the wizard is opened after the file has been generated, - allowing the user to download it anyway. - - :param dict options: The report options. - :param def content_generator: The function used to generate the exported content. - :param dict generator_params: The parameters passed to the 'content_generator' method (List). - :param list errors: A list of errors in the following format: - [ - { - 'message': The error message to be displayed in the wizard (String), - 'action_text': The text of the action button (String), - 'action': Contains the action values (Dictionary), - 'level': One of 'info', 'warning', 'danger'. (String). - Only the 'danger' level represents a blocking error. - }, - {...}, - ] - :returns: The data that will be used by the file generator. - :rtype: dict - """ - if errors is None: - errors = [] - self.ensure_one() - if any(error_value.get('level') == 'danger' for error_value in errors.values()): - raise AccountReportFileDownloadException(errors) - - content = content_generator(**generator_params) - - file_data = { - 'file_name': self.get_default_report_filename(options, generator_params['file_type']), - 'file_content': re.sub(r'\n\s*\n', '\n', content).encode(), - 'file_type': generator_params['file_type'], - } - - if errors: - raise AccountReportFileDownloadException(errors, file_data) - - return file_data - - def action_create_composite_report(self): - return { - 'type': 'ir.actions.act_window', - 'res_model': 'account.report', - 'views': [[False, 'form']], - 'context': { - 'default_section_report_ids': self.ids, - } - } - - def show_error_branch_allowed(self, *args, **kwargs): - raise UserError(_("Please select the main company and its branches in the company selector to proceed.")) - - -class AccountReportLine(models.Model): - _inherit = 'account.report.line' - - display_custom_groupby_warning = fields.Boolean(compute='_compute_display_custom_groupby_warning') - - @api.depends('groupby', 'user_groupby') - def _compute_display_custom_groupby_warning(self): - for line in self: - line.display_custom_groupby_warning = line.get_external_id() and line.user_groupby != line.groupby - - @api.constrains('groupby', 'user_groupby') - def _validate_groupby(self): - super()._validate_groupby() - for report_line in self: - report_line.report_id._check_groupby_fields(report_line.user_groupby) - report_line.report_id._check_groupby_fields(report_line.groupby) - - def _expand_groupby(self, line_dict_id, groupby, options, offset=0, limit=None, load_one_more=False, unfold_all_batch_data=None): - """ Expand function used to get the sublines of a groupby. - groupby param is a string consisting of one or more coma-separated field names. Only the first one - will be used for the expansion; if there are subsequent ones, the generated lines will themselves used them as - their groupby value, and point to this expand_function, hence generating a hierarchy of groupby). - """ - self.ensure_one() - - group_indent = 0 - line_id_list = self.report_id._parse_line_id(line_dict_id) - - # Parse groupby - groupby_data = self._parse_groupby(options, groupby_to_expand=groupby) - groupby_model = groupby_data['current_groupby_model'] - next_groupby = groupby_data['next_groupby'] - current_groupby = groupby_data['current_groupby'] - custom_groupby_map = groupby_data['custom_groupby_map'] - - # If this line is a sub-groupby of groupby line (for example, when grouping by partner, id; the id line is a subgroup of partner), - # we need to add the domain of the parent groupby criteria to the options - prefix_groups_count = 0 - sub_groupby_domain = [] - full_sub_groupby_key_elements = [] - for markup, model, value in line_id_list: - if isinstance(markup, dict) and 'groupby' in markup: - field_name = markup['groupby'] - if field_name in custom_groupby_map: - sub_groupby_domain += custom_groupby_map[field_name]['domain_builder'](value) - else: - sub_groupby_domain.append((field_name, '=', value)) - full_sub_groupby_key_elements.append(f"{field_name}:{value}") - elif isinstance(markup, dict) and 'groupby_prefix_group' in markup: - prefix_groups_count += 1 - - if model == 'account.group': - group_indent += 1 - - if sub_groupby_domain: - forced_domain = options.get('forced_domain', []) + sub_groupby_domain - options = {**options, 'forced_domain': forced_domain} - - # If the report transmitted custom_unfold_all_batch_data dictionary, use it - full_sub_groupby_key = f"[{self.id}]{','.join(full_sub_groupby_key_elements)}=>{current_groupby}" - - cached_result = (unfold_all_batch_data or {}).get(full_sub_groupby_key) - - if cached_result is not None: - all_column_groups_expression_totals = cached_result - else: - all_column_groups_expression_totals = self.report_id._compute_expression_totals_for_each_column_group( - self.expression_ids, - options, - groupby_to_expand=groupby, - offset=offset, - limit=limit + 1 if limit and load_one_more else limit, - ) - - # Put similar grouping keys from different totals/periods together, so that we don't display multiple - # lines for the same grouping key - - figure_types_defaulting_to_0 = {'monetary', 'percentage', 'integer', 'float'} - - default_value_per_expr_label = { - col_opt['expression_label']: 0 if col_opt['figure_type'] in figure_types_defaulting_to_0 else None - for col_opt in options['columns'] - } - - # Gather default value for each expression, in case it has no value for a given grouping key - default_value_per_expression = {} - for expression in self.expression_ids: - if expression.figure_type: - default_value = 0 if expression.figure_type in figure_types_defaulting_to_0 else None - else: - default_value = default_value_per_expr_label.get(expression.label) - - default_value_per_expression[expression] = {'value': default_value} - - # Build each group's result - aggregated_group_totals = defaultdict(lambda: defaultdict(default_value_per_expression.copy)) - for column_group_key, expression_totals in all_column_groups_expression_totals.items(): - for expression in self.expression_ids: - for grouping_key, result in expression_totals[expression]['value']: - aggregated_group_totals[grouping_key][column_group_key][expression] = {'value': result} - - # Generate groupby lines - group_lines_by_keys = {} - for grouping_key, group_totals in aggregated_group_totals.items(): - # For this, we emulate a dict formatted like the result of _compute_expression_totals_for_each_column_group, so that we can call - # _build_static_line_columns like on non-grouped lines - line_id = self.report_id._get_generic_line_id(groupby_model, grouping_key, parent_line_id=line_dict_id, markup={'groupby': current_groupby}) - group_line_dict = { - # 'name' key will be set later, so that we can browse all the records of this expansion at once (in case we're dealing with records) - 'id': line_id, - 'unfoldable': bool(next_groupby), - 'unfolded': (next_groupby and options['unfold_all']) or line_id in options['unfolded_lines'], - 'groupby': next_groupby, - 'columns': self.report_id._build_static_line_columns(self, options, group_totals, groupby_model=groupby_model), - 'level': self.hierarchy_level + 2 * (prefix_groups_count + len(sub_groupby_domain) + 1) + (group_indent - 1), - 'parent_id': line_dict_id, - 'expand_function': '_report_expand_unfoldable_line_with_groupby' if next_groupby else None, - 'caret_options': groupby_model if not next_groupby else None, - } - - if self.report_id.custom_handler_model_id: - self.env[self.report_id.custom_handler_model_name]._custom_groupby_line_completer(self.report_id, options, group_line_dict) - - # Growth comparison column. - if options.get('column_percent_comparison') == 'growth': - compared_expression = self.expression_ids.filtered(lambda expr: expr.label == group_line_dict['columns'][0]['expression_label']) - group_line_dict['column_percent_comparison_data'] = self.report_id._compute_column_percent_comparison_data( - options, group_line_dict['columns'][0]['no_format'], group_line_dict['columns'][1]['no_format'], green_on_positive=compared_expression.green_on_positive) - # Manage budget comparison - elif options.get('column_percent_comparison') == 'budget': - self.report_id._set_budget_column_comparisons(options, group_line_dict) - - group_lines_by_keys[grouping_key] = group_line_dict - - # Sort grouping keys in the right order and generate line names - keys_and_names_in_sequence = {} # Order of this dict will matter - - if groupby_model: - browsed_groupby_keys = self.env[groupby_model].browse(list(key for key in group_lines_by_keys if key is not None)) - - out_of_sorting_record = None - records_to_sort = browsed_groupby_keys - if browsed_groupby_keys and load_one_more and len(browsed_groupby_keys) >= limit: - out_of_sorting_record = browsed_groupby_keys[-1] - records_to_sort = records_to_sort[:-1] - - for record in records_to_sort.with_context(active_test=False).sorted(): - keys_and_names_in_sequence[record.id] = record.display_name - - if None in group_lines_by_keys: - keys_and_names_in_sequence[None] = _("Unknown") - - if out_of_sorting_record: - keys_and_names_in_sequence[out_of_sorting_record.id] = out_of_sorting_record.display_name - - else: - for non_relational_key in sorted(group_lines_by_keys.keys(), key=lambda k: (k is None, k)): - if custom_groupby_name_builder := custom_groupby_map.get(current_groupby, {}).get('label_builder'): - keys_and_names_in_sequence[non_relational_key] = custom_groupby_name_builder(non_relational_key) - else: - keys_and_names_in_sequence[non_relational_key] = str(non_relational_key) if non_relational_key is not None else _("Unknown") - - # Build result: add a name to the groupby lines and handle totals below section for multi-level groupby - group_lines = [] - for grouping_key, line_name in keys_and_names_in_sequence.items(): - group_line_dict = group_lines_by_keys[grouping_key] - group_line_dict['name'] = line_name - group_lines.append(group_line_dict) - - if options.get('hierarchy'): - group_lines = self.report_id._create_hierarchy(group_lines, options) - - return group_lines - - def _get_groupby_line_name(self, groupby_field_name, groupby_model, grouping_key): - if groupby_model is None: - return grouping_key - - if grouping_key is None: - return _("Unknown") - - return self.env[groupby_model].browse(grouping_key).display_name - - def _parse_groupby(self, options, groupby_to_expand=None): - """ Retrieves the information needed to handle the groupby feature on the current line. - - :param groupby_to_expand: A coma-separated string containing, in order, all the fields that are used in the groupby we're expanding. - None if we're not expanding anything. - - :return: A dictionary with 4 keys: - 'current_groupby': The name of the value to be used to retrieve the results of the current groupby we're - expanding, or None if nothing is being expanded. That value can be either a field of account.move.line, or - a custom groupby value defined in this report's custom handler's _get_custom_groupby_map function. - - 'next_groupby': The subsequent groupings to be applied after current_groupby, as a string of coma-separated values (again, - either field names from account.move.line or a custom groupby defined on the handler). - If no subsequent grouping exists, next_groupby will be None. - - 'current_groupby_model': The model name corresponding to current_groupby, or None if current_groupby is None. - - 'custom_groupby_map'; The groupby map, used to handle custom groupby values, as returned by the _get_custom_groupby_map function - of the custom handler (by default, it will be an empty dict) - - EXAMPLE: - When computing a line with groupby=partner_id,account_id,id , without expanding it: - - groupby_to_expand will be None - - current_groupby will be None - - next_groupby will be 'partner_id,account_id,id' - - current_groupby_model will be None - - When expanding the first group level of the line: - - groupby_to_expand will be: partner_id,account_id,id - - current_groupby will be 'partner_id' - - next_groupby will be 'account_id,id' - - current_groupby_model will be 'res.partner' - - When expanding further: - - groupby_to_expand will be: account_id,id ; corresponding to the next_groupby computed when expanding partner_id - - current_groupby will be 'account_id' - - next_groupby will be 'id' - - current_groupby_model will be 'account.account' - """ - self.ensure_one() - - if groupby_to_expand: - groupby_to_expand = groupby_to_expand.replace(' ', '') - split_groupby = groupby_to_expand.split(',') - current_groupby = split_groupby[0] - next_groupby = ','.join(split_groupby[1:]) if len(split_groupby) > 1 else None - else: - current_groupby = None - groupby = self._get_groupby(options) - next_groupby = groupby.replace(' ', '') if groupby else None - - custom_handler_name = self.report_id._get_custom_handler_model() - custom_groupby_map = self.env[custom_handler_name]._get_custom_groupby_map() if custom_handler_name else {} - if current_groupby in custom_groupby_map: - groupby_model = custom_groupby_map[current_groupby]['model'] - elif current_groupby == 'id': - groupby_model = 'account.move.line' - elif current_groupby: - groupby_model = self.env['account.move.line']._fields[current_groupby].comodel_name - else: - groupby_model = None - - return { - 'current_groupby': current_groupby, - 'next_groupby': next_groupby, - 'current_groupby_model': groupby_model, - 'custom_groupby_map': custom_groupby_map, - } - - def _get_groupby(self, options): - self.ensure_one() - if options['export_mode'] == 'file': - return self.groupby - return self.user_groupby - - def action_reset_custom_groupby(self): - self.ensure_one() - self.user_groupby = self.groupby - - -class AccountReportExpression(models.Model): - _inherit = 'account.report.expression' - - def action_view_carryover_lines(self, options, column_group_key=None): - if column_group_key: - options = self.report_line_id.report_id._get_column_group_options(options, column_group_key) - - date_from, date_to = self.report_line_id.report_id._get_date_bounds_info(options, self.date_scope) - - return { - 'type': 'ir.actions.act_window', - 'name': _('Carryover lines for: %s', self.report_line_name), - 'res_model': 'account.report.external.value', - 'views': [(False, 'list')], - 'domain': [ - ('target_report_expression_id', '=', self.id), - ('date', '>=', date_from), - ('date', '<=', date_to), - ], - } - - -class AccountReportHorizontalGroup(models.Model): - _name = "account.report.horizontal.group" - _description = "Horizontal group for reports" - - name = fields.Char(string="Name", required=True, translate=True) - rule_ids = fields.One2many(string="Rules", comodel_name='account.report.horizontal.group.rule', inverse_name='horizontal_group_id', required=True) - report_ids = fields.Many2many(string="Reports", comodel_name='account.report') - - _sql_constraints = [ - ('name_uniq', 'unique (name)', "A horizontal group with the same name already exists."), - ] - - def _get_header_levels_data(self): - return [ - (rule.field_name, rule._get_matching_records()) - for rule in self.rule_ids - ] - -class AccountReportHorizontalGroupRule(models.Model): - _name = "account.report.horizontal.group.rule" - _description = "Horizontal group rule for reports" - - def _field_name_selection_values(self): - return [ - (aml_field['name'], aml_field['string']) - for aml_field in self.env['account.move.line'].fields_get().values() - if aml_field['type'] in ('many2one', 'many2many') - ] - - horizontal_group_id = fields.Many2one(string="Horizontal Group", comodel_name='account.report.horizontal.group', required=True) - domain = fields.Char(string="Domain", required=True, default='[]') - field_name = fields.Selection(string="Field", selection='_field_name_selection_values', required=True) - res_model_name = fields.Char(string="Model", compute='_compute_res_model_name') - - @api.depends('field_name') - def _compute_res_model_name(self): - for record in self: - if record.field_name: - record.res_model_name = self.env['account.move.line']._fields[record.field_name].comodel_name - else: - record.res_model_name = None - - def _get_matching_records(self): - self.ensure_one() - model_name = self.env['account.move.line']._fields[self.field_name].comodel_name - domain = ast.literal_eval(self.domain) - return self.env[model_name].search(domain) - - -class AccountReportCustomHandler(models.AbstractModel): - _name = 'account.report.custom.handler' - _description = 'Account Report Custom Handler' - - # This abstract model allows case-by-case localized changes of behaviors of reports. - # This is used for custom reports, for cases that cannot be supported by the standard engines. - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - """ Generates lines dynamically for reports that require a custom processing which cannot be handled - by regular report engines. - :return: A list of tuples [(sequence, line_dict), ...], where: - - sequence is the sequence to apply when rendering the line (can be mixed with static lines), - - line_dict is a dict containing all the line values. - """ - return [] - - def _caret_options_initializer(self): - """ Returns the caret options dict to be used when rendering this report, - in the same format as the one used in _caret_options_initializer_default (defined on 'account.report'). - If the result is empty, the engine will use the default caret options. - """ - return self.env['account.report']._caret_options_initializer_default() - - def _custom_options_initializer(self, report, options, previous_options): - """ To be overridden to add report-specific _init_options... code to the report. """ - if report.root_report_id: - report.root_report_id._init_options_custom(options, previous_options) - - def _custom_line_postprocessor(self, report, options, lines): - """ Postprocesses the result of the report's _get_lines() before returning it. """ - return lines - - def _custom_groupby_line_completer(self, report, options, line_dict): - """ Postprocesses the dict generated by the group_by_line, to customize its content. """ - - def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function): - """ When using the 'unfold all' option, some reports might end up recomputing the same query for - each line to unfold, leading to very inefficient computation. This function allows batching this computation, - and returns a dictionary where all results are cached, for use in expansion functions. - """ - return None - - def _get_custom_display_config(self): - """ To be overridden in order to change the templates used by Javascript to render this report (keeping the same - OWL components), and/or replace some of the default OWL components by custom-made ones. - - This function returns a dict (possibly empty, if there is no custom display config): - - { - 'css_custom_class: 'class', - 'components': { - - }, - 'pdf_export': { - - }, - 'templates': { - - }, - }, - """ - return {} - - def _get_custom_groupby_map(self): - """ Allows the use of custom values in the groupby field of account.report.line, to use them in custom engines. Those custom - values can be anything, and need to be properly handled by the custom engine using them. This allows adding support for grouping on - something else than just the fields of account.move.line, which is the default. - - :return: A dict, in the form {groupby_name: {'model': model, 'domain_builder': domain_builder}}, where: - - groupby_name is the custom value to use in groupby instead of one of aml's field names - - model: is a model name (a string), representing the model the value returned for this custom groupby targets. - The model will be used to compute the display_name to show for each generated groupby line, in the UI. - This value can be passed to None ; in such case, the raw value returned by the engine will be shown. - - domain_builder is a function to be called when expanding a groupby line generated by this custom groupby, to compute the - domain to apply in order to restrict the computation to the content of this groupby line. - This function must accept a single parameter, corresponding to the groupby value to compute the domain for. - - label_builder is a function to be called to compute a label for the groupby value, that will be shown as the line name - in the UI. This ways, translatable labels and multi-values keys serialized to json can be fully supported. - """ - return {} - - def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings): - """ To be overridden to add report-specific warnings in the warnings dictionary. - When a root report defines something in this function, its variants without any custom handler will also call the root report's - _customize_warnings function. This can hence be used to share warnings between all variants. - - Should only be used when necessary, _dynamic_lines_generator is preferred. - """ - - def _enable_export_buttons_for_common_vat_groups_in_branches(self, options): - """ Helper function to be called in _custom_options_initializer to change the behavior of the report so that the export - buttons are all forced to 'branch_allowed' in case the currently selected company branches all share the same VAT number, and - no unselected sub-branch of the active company has the same VAT number. Companies without explicit VAT number (empty vat field) - will be considered as having the same VAT number as their closest parent with a non-empty VAT. - """ - report_accepted_company_ids = set(self.env['account.report'].get_report_company_ids(options)) - same_vat_branch_ids = set(self.env.company._get_branches_with_same_vat().ids) - if report_accepted_company_ids == same_vat_branch_ids: - for button in options['buttons']: - button['branch_allowed'] = True - - -class AccountReportFileDownloadException(Exception): - def __init__(self, errors, content=None): - super().__init__() - self.errors = errors - self.content = content diff --git a/addons/at_accounting/models/account_sales_report.py b/addons/at_accounting/models/account_sales_report.py deleted file mode 100644 index b6ca0bb..0000000 --- a/addons/at_accounting/models/account_sales_report.py +++ /dev/null @@ -1,400 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from collections import defaultdict - -from odoo import _, api, fields, models -from odoo.tools import SQL - - -class ECSalesReportCustomHandler(models.AbstractModel): - _name = 'account.ec.sales.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'EC Sales Report Custom Handler' - - def _get_custom_display_config(self): - return { - 'components': { - 'AccountReportFilters': 'at_accounting.SalesReportFilters', - }, - } - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - """ - Generate the dynamic lines for the report in a vertical style (one line per tax per partner). - """ - lines = [] - totals_by_column_group = { - column_group_key: { - 'balance': 0.0, - 'goods': 0.0, - 'triangular': 0.0, - 'services': 0.0, - 'vat_number': '', - 'country_code': '', - 'sales_type_code': '', - } - for column_group_key in options['column_groups'] - } - - operation_categories = options['sales_report_taxes'].get('operation_category', {}) - ec_tax_filter_selection = {v.get('id'): v.get('selected') for v in options.get('ec_tax_filter_selection', [])} - for partner, results in self._query_partners(report, options, warnings): - for tax_ec_category in ('goods', 'triangular', 'services'): - if not ec_tax_filter_selection[tax_ec_category]: - # Skip the line if the tax is not selected - continue - partner_values = defaultdict(dict) - country_specific_code = operation_categories.get(tax_ec_category) - has_found_a_line = False - for col_grp_key in options['column_groups']: - partner_sum = results.get(col_grp_key, {}) - partner_values[col_grp_key]['vat_number'] = partner_sum.get('vat_number', 'UNKNOWN') - partner_values[col_grp_key]['country_code'] = partner_sum.get('country_code', 'UNKNOWN') - partner_values[col_grp_key]['sales_type_code'] = [] - partner_values[col_grp_key]['balance'] = partner_sum.get(tax_ec_category, 0.0) - totals_by_column_group[col_grp_key]['balance'] += partner_sum.get(tax_ec_category, 0.0) - for i, operation_id in enumerate(partner_sum.get('tax_element_id', [])): - if operation_id in options['sales_report_taxes'][tax_ec_category]: - has_found_a_line = True - partner_values[col_grp_key]['sales_type_code'] += [ - country_specific_code or - (partner_sum.get('sales_type_code') and partner_sum.get('sales_type_code')[i]) - or None] - partner_values[col_grp_key]['sales_type_code'] = ', '.join(set(partner_values[col_grp_key]['sales_type_code'])) - if has_found_a_line: - lines.append((0, self._get_report_line_partner(report, options, partner, partner_values, markup=tax_ec_category))) - - # Report total line. - if lines: - lines.append((0, self._get_report_line_total(report, options, totals_by_column_group))) - - return lines - - def _caret_options_initializer(self): - """ - Add custom caret option for the report to link to the partner and allow cleaner overrides. - """ - return { - 'ec_sales': [ - {'name': _("View Partner"), 'action': 'caret_option_open_record_form'} - ], - } - - def _custom_options_initializer(self, report, options, previous_options): - """ - Add the invoice lines search domain that is specific to the country. - Typically, the taxes tag_ids relative to the country for the triangular, sale of goods or services - :param dict options: Report options - :param dict previous_options: Previous report options - """ - super()._custom_options_initializer(report, options, previous_options=previous_options) - self._init_core_custom_options(report, options, previous_options) - options.update({ - 'sales_report_taxes': { - 'goods': tuple(self.env['account.tax'].search([ - *self.env['account.tax']._check_company_domain(self.env.company), - ('amount', '=', 0.0), - ('amount_type', '=', 'percent'), - ('type_tax_use', '=', 'sale'), - ]).ids), - 'services': tuple(), - 'triangular': tuple(), - 'use_taxes_instead_of_tags': True, - # We can't use tags as we don't have a country tax report correctly set, 'use_taxes_instead_of_tags' - # should never be used outside this case - } - }) - country_ids = self.env['res.country'].search([ - ('code', 'in', tuple(self._get_ec_country_codes(options))) - ]).ids - other_country_ids = tuple(set(country_ids) - {self.env.company.account_fiscal_country_id.id}) - options.setdefault('forced_domain', []).extend([ - '|', - ('move_id.partner_shipping_id.country_id', 'in', other_country_ids), - '&', - ('move_id.partner_shipping_id', '=', False), - ('partner_id.country_id', 'in', other_country_ids), - ]) - - report._init_options_journals(options, previous_options=previous_options) - - self._enable_export_buttons_for_common_vat_groups_in_branches(options) - - def _init_core_custom_options(self, report, options, previous_options): - """ - Add the invoice lines search domain that is common to all countries. - :param dict options: Report options - :param dict previous_options: Previous report options - """ - default_tax_filter = [ - {'id': 'goods', 'name': _('Goods'), 'selected': True}, - {'id': 'triangular', 'name': _('Triangular'), 'selected': True}, - {'id': 'services', 'name': _('Services'), 'selected': True}, - ] - - ec_tax_filter_selection = previous_options.get('ec_tax_filter_selection', default_tax_filter) - # In case we have a EC sale list report with more ec_tax_filter_selection the previous options will have extra - # item we just keep the default ones, and we let variant extend the function to add the ones they need - if ec_tax_filter_selection != default_tax_filter: - filtered_ec_tax_filter_selection = [item for item in ec_tax_filter_selection if item['id'] in {item['id'] for item in default_tax_filter}] - options['ec_tax_filter_selection'] = filtered_ec_tax_filter_selection - else: - options['ec_tax_filter_selection'] = ec_tax_filter_selection - - def _get_report_line_partner(self, report, options, partner, partner_values, markup=''): - """ - Convert the partner values to a report line. - :param dict options: Report options - :param recordset partner: the corresponding res.partner record - :param dict partner_values: Dictionary of values for the report line - :return dict: Return a dict with the values for the report line. - """ - column_values = [] - for column in options['columns']: - value = partner_values[column['column_group_key']].get(column['expression_label']) - column_values.append(report._build_column_dict(value, column, options=options)) - - return { - 'id': report._get_generic_line_id('res.partner', partner.id, markup=markup), - 'name': partner is not None and (partner.name or '')[:128] or _('Unknown Partner'), - 'columns': column_values, - 'level': 2, - 'trust': partner.trust if partner else None, - 'caret_options': 'ec_sales', - } - - def _get_report_line_total(self, report, options, totals_by_column_group): - """ - Convert the total values to a report line. - :param dict options: Report options - :param dict totals_by_column_group: Dictionary of values for the total line - :return dict: Return a dict with the values for the report line. - """ - column_values = [] - for column in options['columns']: - col_value = totals_by_column_group[column['column_group_key']].get(column['expression_label']) - col_value = col_value if column['figure_type'] == 'monetary' else '' - - column_values.append(report._build_column_dict(col_value, column, options=options)) - - return { - 'id': report._get_generic_line_id(None, None, markup='total'), - 'name': _('Total'), - 'class': 'total', - 'level': 1, - 'columns': column_values, - } - - def _query_partners(self, report, options, warnings=None): - ''' Execute the queries, perform all the computation, then - returns a lists of tuple (partner, fetched_values) sorted by the table's model _order: - - partner is a res.parter record. - - fetched_values is a dictionary containing: - - sums by operation type: {'goods': float, - 'triangular': float, - 'services': float, - - - tax identifiers: 'tax_element_id': list[int], > the tag_id in almost every case - 'sales_type_code': list[str], - - - partner identifier elements: 'vat_number': str, - 'full_vat_number': str, - 'country_code': str} - - :param options: The report options. - :return: (accounts_values, taxes_results) - ''' - groupby_partners = {} - - def assign_sum(row): - """ - Assign corresponding values from the SQL querry row to the groupby_partners dictionary. - If the line balance isn't 0, find the tax tag_id and check in which column/report line the SQL line balance - should be displayed. - - The tricky part is to allow for the report to be displayed in vertical or horizontal format. - In vertical, you have up to 3 lines per partner (one for each operation type). - In horizontal, you have one line with 3 columns per partner (one for each operation type). - - Add then the more straightforward data (vat number, country code, ...) - :param dict row: - """ - if not company_currency.is_zero(row['balance']): - groupby_partners.setdefault(row['groupby'], defaultdict(lambda: defaultdict(float))) - - groupby_partners_keyed = groupby_partners[row['groupby']][row['column_group_key']] - if row['tax_element_id'] in options['sales_report_taxes']['goods']: - groupby_partners_keyed['goods'] += row['balance'] - elif row['tax_element_id'] in options['sales_report_taxes']['triangular']: - groupby_partners_keyed['triangular'] += row['balance'] - elif row['tax_element_id'] in options['sales_report_taxes']['services']: - groupby_partners_keyed['services'] += row['balance'] - - groupby_partners_keyed.setdefault('tax_element_id', []).append(row['tax_element_id']) - groupby_partners_keyed.setdefault('sales_type_code', []).append(row['sales_type_code']) - - vat = row['vat_number'] or '' - vat_country_code = vat[:2] if vat[:2].isalpha() else None - groupby_partners_keyed.setdefault('vat_number', vat if not vat_country_code else vat[2:]) - groupby_partners_keyed.setdefault('full_vat_number', vat) - groupby_partners_keyed.setdefault('country_code', vat_country_code or row.get('country_code')) - - if warnings is not None: - if row['country_code'] not in self._get_ec_country_codes(options): - warnings['at_accounting.sales_report_warning_non_ec_country'] = {'alert_type': 'warning'} - elif not row.get('vat_number'): - warnings['at_accounting.sales_report_warning_missing_vat'] = {'alert_type': 'warning'} - if row.get('same_country') and row['country_code']: - warnings['at_accounting.sales_report_warning_same_country'] = {'alert_type': 'warning'} - - company_currency = self.env.company.currency_id - - # Execute the queries and dispatch the results. - query = self._get_query_sums(report, options) - self._cr.execute(query) - - dictfetchall = self._cr.dictfetchall() - for res in dictfetchall: - assign_sum(res) - - if groupby_partners: - partners = self.env['res.partner'].with_context(active_test=False).browse(groupby_partners.keys()) - else: - partners = self.env['res.partner'] - - return [(partner, groupby_partners[partner.id]) for partner in partners.sorted()] - - def _get_query_sums(self, report, options) -> SQL: - ''' Construct a query retrieving all the aggregated sums to build the report. It includes: - - sums for all partners. - - sums for the initial balances. - :param options: The report options. - :return: query as SQL object - ''' - queries = [] - # Create the currency table. - allowed_ids = self._get_tag_ids_filtered(options) - - # In the case of the generic report, we don't have a country defined. So no reliable tax report whose - # tag_ids can be used. So we have a fallback to tax_ids. - - if options.get('sales_report_taxes', {}).get('use_taxes_instead_of_tags'): - tax_elem_table = SQL('account_tax') - tax_elem_table_id = SQL('account_tax_id') - aml_rel_table = SQL('account_move_line_account_tax_rel') - tax_elem_table_name = self.env['account.tax']._field_to_sql('account_tax', 'name') - else: - tax_elem_table = SQL('account_account_tag') - tax_elem_table_id = SQL('account_account_tag_id') - aml_rel_table = SQL('account_account_tag_account_move_line_rel') - tax_elem_table_name = self.env['account.account.tag']._field_to_sql('account_account_tag', 'name') - - for column_group_key, column_group_options in report._split_options_per_column_group(options).items(): - query = report._get_report_query(column_group_options, 'strict_range') - if allowed_ids: - query.add_where(SQL('%s.id IN %s', tax_elem_table, tuple(allowed_ids))) - queries.append(SQL( - """ - SELECT - %(column_group_key)s AS column_group_key, - account_move_line.partner_id AS groupby, - res_partner.vat AS vat_number, - res_country.code AS country_code, - -SUM(%(balance_select)s) AS balance, - %(tax_elem_table_name)s AS sales_type_code, - %(tax_elem_table)s.id AS tax_element_id, - (comp_partner.country_id = res_partner.country_id) AS same_country - FROM %(table_references)s - %(currency_table_join)s - JOIN %(aml_rel_table)s ON %(aml_rel_table)s.account_move_line_id = account_move_line.id - JOIN %(tax_elem_table)s ON %(aml_rel_table)s.%(tax_elem_table_id)s = %(tax_elem_table)s.id - JOIN res_partner ON account_move_line.partner_id = res_partner.id - JOIN res_country ON res_partner.country_id = res_country.id - JOIN res_company ON res_company.id = account_move_line.company_id - JOIN res_partner comp_partner ON comp_partner.id = res_company.partner_id - WHERE %(search_condition)s - GROUP BY %(tax_elem_table)s.id, %(tax_elem_table)s.name, account_move_line.partner_id, - res_partner.vat, res_country.code, comp_partner.country_id, res_partner.country_id - """, - column_group_key=column_group_key, - tax_elem_table_name=tax_elem_table_name, - tax_elem_table=tax_elem_table, - table_references=query.from_clause, - balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")), - currency_table_join=report._currency_table_aml_join(column_group_options), - aml_rel_table=aml_rel_table, - tax_elem_table_id=tax_elem_table_id, - search_condition=query.where_clause, - )) - return SQL(' UNION ALL ').join(queries) - - @api.model - def _get_tag_ids_filtered(self, options): - """ - Helper function to get all the tag_ids concerned by the report for the given options. - :param dict options: Report options - :return tuple: tag_ids untyped after filtering - """ - allowed_taxes = set() - for operation_type in options.get('ec_tax_filter_selection', []): - if operation_type.get('selected'): - allowed_taxes.update(options['sales_report_taxes'][operation_type.get('id')]) - return allowed_taxes - - @api.model - def _get_ec_country_codes(self, options): - """ - Return the list of country codes for the EC countries. - :param dict options: Report options - :return set: List of country codes for a given date (UK case) - """ - rslt = {'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', - 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'XI'} - - # GB left the EU on January 1st 2021. But before this date, it's still to be considered as a EC country - if fields.Date.from_string(options['date']['date_from']) < fields.Date.from_string('2021-01-01'): - rslt.add('GB') - # Monaco is treated as part of France for VAT purposes (but should not be displayed within FR context) - if self.env.company.account_fiscal_country_id.code != 'FR': - rslt.add('MC') - - return rslt - - def get_warning_act_window(self, options, params): - act_window = {'type': 'ir.actions.act_window', 'context': {}} - if params['type'] == 'no_vat': - aml_domains = [ - ('partner_id.vat', '=', None), - ('partner_id.country_id.code', 'in', tuple(self._get_ec_country_codes(options))), - ] - act_window.update({ - 'name': _("Entries with partners with no VAT"), - 'context': {'search_default_group_by_partner': 1, 'expand': 1} - }) - elif params['type'] == 'non_ec_country': - aml_domains = [('partner_id.country_id.code', 'not in', tuple(self._get_ec_country_codes(options)))] - act_window['name'] = _("EC tax on non EC countries") - else: - aml_domains = [('partner_id.country_id.code', '=', options.get('same_country_warning'))] - act_window['name'] = _("EC tax on same country") - use_taxes_instead_of_tags = options.get('sales_report_taxes', {}).get('use_taxes_instead_of_tags') - tax_or_tag_field = 'tax_ids.id' if use_taxes_instead_of_tags else 'tax_tag_ids.id' - amls = self.env['account.move.line'].search([ - *aml_domains, - *self.env['account.report']._get_options_date_domain(options, 'strict_range'), - (tax_or_tag_field, 'in', tuple(self._get_tag_ids_filtered(options))) - ]) - - if params['model'] == 'move': - act_window.update({ - 'views': [[self.env.ref('account.view_move_tree').id, 'list'], (False, 'form')], - 'res_model': 'account.move', - 'domain': [('id', 'in', amls.move_id.ids)], - }) - else: - act_window.update({ - 'views': [(False, 'list'), (False, 'form')], - 'res_model': 'res.partner', - 'domain': [('id', 'in', amls.move_id.partner_id.ids)], - }) - - return act_window diff --git a/addons/at_accounting/models/account_tax.py b/addons/at_accounting/models/account_tax.py deleted file mode 100644 index e30ad53..0000000 --- a/addons/at_accounting/models/account_tax.py +++ /dev/null @@ -1,206 +0,0 @@ -from odoo import api, models, fields, Command, _ -from odoo.exceptions import ValidationError - -class AccountTax(models.Model): - _inherit = "account.tax" - - def _prepare_base_line_for_taxes_computation(self, record, **kwargs): - # EXTENDS 'account' - results = super()._prepare_base_line_for_taxes_computation(record, **kwargs) - results['deferred_start_date'] = self._get_base_line_field_value_from_record(record, 'deferred_start_date', kwargs, False) - results['deferred_end_date'] = self._get_base_line_field_value_from_record(record, 'deferred_end_date', kwargs, False) - return results - - def _prepare_tax_line_for_taxes_computation(self, record, **kwargs): - # EXTENDS 'account' - results = super()._prepare_tax_line_for_taxes_computation(record, **kwargs) - results['deferred_start_date'] = self._get_base_line_field_value_from_record(record, 'deferred_start_date', kwargs, False) - results['deferred_end_date'] = self._get_base_line_field_value_from_record(record, 'deferred_end_date', kwargs, False) - return results - - def _prepare_base_line_grouping_key(self, base_line): - # EXTENDS 'account' - results = super()._prepare_base_line_grouping_key(base_line) - results['deferred_start_date'] = base_line['deferred_start_date'] - results['deferred_end_date'] = base_line['deferred_end_date'] - return results - - def _prepare_base_line_tax_repartition_grouping_key(self, base_line, base_line_grouping_key, tax_data, tax_rep_data): - # EXTENDS 'account' - results = super()._prepare_base_line_tax_repartition_grouping_key(base_line, base_line_grouping_key, tax_data, tax_rep_data) - record = base_line['record'] - if ( - isinstance(record, models.Model) - and record._name == 'account.move.line' - and record._has_deferred_compatible_account() - and base_line['deferred_start_date'] - and base_line['deferred_end_date'] - and not tax_rep_data['tax_rep'].use_in_tax_closing - ): - results['deferred_start_date'] = base_line['deferred_start_date'] - results['deferred_end_date'] = base_line['deferred_end_date'] - else: - results['deferred_start_date'] = False - results['deferred_end_date'] = False - return results - - def _prepare_tax_line_repartition_grouping_key(self, tax_line): - # EXTENDS 'account' - results = super()._prepare_tax_line_repartition_grouping_key(tax_line) - results['deferred_start_date'] = tax_line['deferred_start_date'] - results['deferred_end_date'] = tax_line['deferred_end_date'] - return results - -class AccountTaxUnit(models.Model): - _name = "account.tax.unit" - _description = "Tax Unit" - - name = fields.Char(string="Name", required=True) - country_id = fields.Many2one(string="Country", comodel_name='res.country', required=True, help="The country in which this tax unit is used to group your companies' tax reports declaration.") - vat = fields.Char(string="Tax ID", required=True, help="The identifier to be used when submitting a report for this unit.") - company_ids = fields.Many2many(string="Companies", comodel_name='res.company', required=True, help="Members of this unit") - main_company_id = fields.Many2one(string="Main Company", comodel_name='res.company', required=True, help="Main company of this unit; the one actually reporting and paying the taxes.") - fpos_synced = fields.Boolean(string="Fiscal Positions Synchronised", compute='_compute_fiscal_position_completion', help="Technical field indicating whether Fiscal Positions exist for all companies in the unit") - - def create(self, vals_list): - res = super().create(vals_list) - - horizontal_groups = self.env['account.report.horizontal.group'].create([ - { - 'name': tax_unit.name, - 'rule_ids': [ - Command.create({ - 'field_name': 'company_id', - 'domain': f"[('account_tax_unit_ids', 'in', {tax_unit.id})]", - }), - ], - } - for tax_unit in res - ]) - - generic_tax_report = self.env.ref('account.generic_tax_report') - generic_tax_report.horizontal_group_ids |= horizontal_groups - - generic_tax_report_account_tax = self.env.ref('account.generic_tax_report_account_tax') - generic_tax_report_account_tax.horizontal_group_ids |= horizontal_groups - - generic_tax_report_tax_account = self.env.ref('account.generic_tax_report_tax_account') - generic_tax_report_tax_account.horizontal_group_ids |= horizontal_groups - - generic_ec_sales_report = self.env.ref('at_accounting.generic_ec_sales_report') - generic_ec_sales_report.horizontal_group_ids |= horizontal_groups - - for tax_unit in res: - generic_tax_report.variant_report_ids.filtered(lambda variant: variant.country_id == tax_unit.country_id).write( - { - 'horizontal_group_ids': [Command.link(group.id) for group in horizontal_groups], - } - ) - - return res - - @api.depends('company_ids') - def _compute_fiscal_position_completion(self): - for unit in self: - synced = True - for company in unit.company_ids: - origin_company = company._origin if isinstance(company.id, models.NewId) else company - fp = unit._get_tax_unit_fiscal_positions(companies=origin_company) - all_partners_with_fp = self.env['res.company'].search([]).with_company(origin_company).partner_id\ - .filtered(lambda p: p.property_account_position_id == fp) if fp else self.env['res.partner'] - synced = all_partners_with_fp == (unit.company_ids - origin_company).partner_id - if not synced: - break - unit.fpos_synced = synced - - def _get_tax_unit_fiscal_positions(self, companies, create_or_refresh=False): - """ - Retrieves or creates fiscal positions for all companies specified. - Each Fiscal Position contains all the taxes of the company mapped to no tax - - @param {recordset} companies: companies for which to find/create fiscal positions - @param {boolean} create_or_refresh: a boolean indicating whether the fiscal positions should be created if not found - @return {recordset} all the fiscal positions found/created for the companies requested. - """ - fiscal_positions = self.env['account.fiscal.position'].with_context(allowed_company_ids=self.env.user.company_ids.ids) - for unit in self: - for company in companies: - fp_identifier = 'account.tax_unit_%s_fp_%s' % (unit.id, company.id) - existing_fp = self.env.ref(fp_identifier, raise_if_not_found=False) - if create_or_refresh: - taxes_to_map = self.env['account.tax'].with_context( - allowed_company_ids=self.env.user.company_ids.ids, - ).search(self.env['account.tax']._check_company_domain(company)) - data = { - 'xml_id': fp_identifier, - 'values': { - 'name': unit.name, - 'company_id': company.id, - 'tax_ids': [Command.clear()] + [Command.create({'tax_src_id': tax.id}) for tax in taxes_to_map] - } - } - existing_fp = fiscal_positions._load_records([data]) - if existing_fp: - fiscal_positions += existing_fp - return fiscal_positions - - def action_sync_unit_fiscal_positions(self): - self._get_tax_unit_fiscal_positions(companies=self.env['res.company'].search([])).unlink() - for unit in self: - for company in unit.company_ids: - fp = unit._get_tax_unit_fiscal_positions(companies=company, create_or_refresh=True) - (unit.company_ids - company).with_company(company).partner_id.property_account_position_id = fp - - def unlink(self): - # EXTENDS base - self._get_tax_unit_fiscal_positions(companies=self.env['res.company'].search([])).unlink() - return super().unlink() - - @api.constrains('country_id', 'company_ids') - def _validate_companies_country(self): - for record in self: - currencies = set() - for company in record.company_ids: - currencies.add(company.currency_id) - - if any(unit != record and unit.country_id == record.country_id for unit in company.account_tax_unit_ids): - raise ValidationError(_("Company %(company)s already belongs to a tax unit in %(country)s. A company can at most be part of one tax unit per country.", company=company.name, country=record.country_id.name)) - - if len(currencies) > 1: - raise ValidationError(_("A tax unit can only be created between companies sharing the same main currency.")) - - @api.constrains('company_ids', 'main_company_id') - def _validate_main_company(self): - for record in self: - if record.main_company_id not in record.company_ids: - raise ValidationError(_("The main company of a tax unit has to be part of it.")) - - @api.constrains('company_ids') - def _validate_companies(self): - for record in self: - if len(record.company_ids) < 2: - raise ValidationError(_("A tax unit must contain a minimum of two companies. You might want to delete the unit.")) - - @api.constrains('country_id', 'vat') - def _validate_vat(self): - for record in self: - if not record.vat: - continue - - checked_country_code = self.env['res.partner']._run_vat_test(record.vat, record.country_id) - - if checked_country_code and checked_country_code != record.country_id.code.lower(): - raise ValidationError(_("The country detected for this VAT number does not match the one set on this Tax Unit.")) - - if not checked_country_code: - tu_label = _("tax unit [%s]", record.name) - error_message = self.env['res.partner']._build_vat_error_message(record.country_id.code.lower(), record.vat, tu_label) - raise ValidationError(error_message) - - @api.onchange('company_ids') - def _onchange_company_ids(self): - if self.main_company_id not in self.company_ids and self.company_ids: - self.main_company_id = self.company_ids[0]._origin - elif not self.company_ids: - self.main_company_id = False - diff --git a/addons/at_accounting/models/account_trial_balance_report.py b/addons/at_accounting/models/account_trial_balance_report.py deleted file mode 100644 index bc4a47c..0000000 --- a/addons/at_accounting/models/account_trial_balance_report.py +++ /dev/null @@ -1,218 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models, _, fields -from odoo.tools import float_compare -from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT - - -TRIAL_BALANCE_END_COLUMN_GROUP_KEY = '_trial_balance_end_column_group' - - -class TrialBalanceCustomHandler(models.AbstractModel): - _name = 'account.trial.balance.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Trial Balance Custom Handler' - - def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): - def _update_column(line, column_key, new_value): - line['columns'][column_key]['no_format'] = new_value - line['columns'][column_key]['is_zero'] = self.env.company.currency_id.is_zero(new_value) - - def _update_balance_columns(line, debit_column_key, credit_column_key, balance_column_key=None): - debit_value = line['columns'][debit_column_key]['no_format'] if debit_column_key is not None else False - credit_value = line['columns'][credit_column_key]['no_format'] if credit_column_key is not None else False - - if debit_value and credit_value: - new_debit_value = 0.0 - new_credit_value = 0.0 - - if self.env.company.currency_id.compare_amounts(debit_value, credit_value) == 1: - new_debit_value = debit_value - credit_value - else: - new_credit_value = (debit_value - credit_value) * -1 - - _update_column(line, debit_column_key, new_debit_value) - _update_column(line, credit_column_key, new_credit_value) - - if balance_column_key is not None: - _update_column(line, balance_column_key, debit_value - credit_value) - - lines = [line[1] for line in self.env['account.general.ledger.report.handler']._dynamic_lines_generator(report, options, all_column_groups_expression_totals, warnings=warnings)] - - # We need to find the index of debit and credit columns for initial and end balance in case of extra custom columns - init_balance_debit_index = next((index for index, column in enumerate(options['columns']) if column.get('expression_label') == 'debit'), None) - init_balance_credit_index = next((index for index, column in enumerate(options['columns']) if column.get('expression_label') == 'credit'), None) - - end_balance_debit_index = next((index for index, column in enumerate(options['columns']) if column.get('expression_label') == 'debit' and column.get('column_group_key') == TRIAL_BALANCE_END_COLUMN_GROUP_KEY), None) - end_balance_credit_index = next((index for index, column in enumerate(options['columns']) if column.get('expression_label') == 'credit' and column.get('column_group_key') == TRIAL_BALANCE_END_COLUMN_GROUP_KEY), None) - end_balance_balance_index = next((index for index, column in enumerate(options['columns']) if column.get('expression_label') == 'balance' and column.get('column_group_key') == TRIAL_BALANCE_END_COLUMN_GROUP_KEY), None) - - currency = self.env.company.currency_id - for line in lines[:-1]: - # Initial balance - _update_balance_columns(line, init_balance_debit_index, init_balance_credit_index) - - # End balance: sum all the previous columns for both debit and credit - if end_balance_debit_index is not None: - end_balance_debit_sum = sum( - currency.round(column['no_format']) - for index, column in enumerate(line['columns']) - if column.get('expression_label') == 'debit' and index != end_balance_debit_index and column['no_format'] is not None - ) - _update_column(line, end_balance_debit_index, end_balance_debit_sum) - - if end_balance_credit_index is not None: - end_balance_credit_sum = sum( - currency.round(column['no_format']) - for index, column in enumerate(line['columns']) - if column.get('expression_label') == 'credit' and index != end_balance_credit_index and column['no_format'] is not None - ) - _update_column(line, end_balance_credit_index, end_balance_credit_sum) - - _update_balance_columns(line, end_balance_debit_index, end_balance_credit_index, end_balance_balance_index) - - line.pop('expand_function', None) - line.pop('groupby', None) - line.update({ - 'unfoldable': False, - 'unfolded': False, - }) - - res_model = report._get_model_info_from_id(line['id'])[0] - if res_model == 'account.account': - line['caret_options'] = 'trial_balance' - - # Total line - if lines: - total_line = lines[-1] - - for index in (init_balance_debit_index, init_balance_credit_index, end_balance_debit_index, end_balance_credit_index): - if index is not None: - total_line['columns'][index]['no_format'] = sum(currency.round(line['columns'][index]['no_format']) for line in lines[:-1] if report._get_model_info_from_id(line['id'])[0] == 'account.account') - - return [(0, line) for line in lines] - - def _caret_options_initializer(self): - return { - 'trial_balance': [ - {'name': _("General Ledger"), 'action': 'caret_option_open_general_ledger'}, - {'name': _("Journal Items"), 'action': 'open_journal_items'}, - ], - } - - def _get_column_group_creation_data(self, report, options, previous_options=None): - """ - Return tuple of tuples containing a reference to the column_group creation function and on which side ('left' | 'right') of the report the column_group goes - """ - return ( - (self._create_column_group_initial_balance, 'left'), - (self._create_column_group_end_balance, 'right'), - ) - - @api.model - def _create_and_append_column_group(self, report, options, header_name, forced_options, side_to_append, group_vals, exclude_initial_balance=False, append_col_groups=True): - header_element = [{'name': header_name, 'forced_options': forced_options}] - column_headers = [header_element, *options['column_headers'][1:]] - column_group_vals = report._generate_columns_group_vals_recursively(column_headers, group_vals) - - if exclude_initial_balance: - # This column group must not include initial balance; we use a special option key for that in general ledger - for column_group in column_group_vals: - column_group['forced_options']['general_ledger_strict_range'] = True - - columns, column_groups = report._build_columns_from_column_group_vals(forced_options, column_group_vals) - - side_to_append['column_headers'] += header_element - if append_col_groups: - side_to_append['column_groups'] |= column_groups - side_to_append['columns'] += columns - - def _custom_options_initializer(self, report, options, previous_options): - """ Modifies the provided options to add a column group for initial balance and end balance, as well as the appropriate columns. - """ - default_group_vals = {'horizontal_groupby_element': {}, 'forced_options': {}} - left_side = {'column_headers': [], 'column_groups': {}, 'columns': []} - right_side = {'column_headers': [], 'column_groups': {}, 'columns': []} - - # Columns between initial and end balance must not include initial balance; we use a special option key for that in general ledger - for column_group in options['column_groups'].values(): - column_group['forced_options']['general_ledger_strict_range'] = True - - if options.get('comparison') and not options['comparison'].get('periods'): - options['comparison']['period_order'] = 'ascending' - - # Create column groups - for function, side in self._get_column_group_creation_data(report, options, previous_options): - function(report, options, previous_options, default_group_vals, left_side if side == 'left' else right_side) - - # Update options - options['column_headers'][0] = left_side['column_headers'] + options['column_headers'][0] + right_side['column_headers'] - options['column_groups'].update(left_side['column_groups']) - options['column_groups'].update(right_side['column_groups']) - options['columns'] = left_side['columns'] + options['columns'] + right_side['columns'] - options['ignore_totals_below_sections'] = True # So that GL does not compute them - - # All the periods displayed between initial and end balance need to use the same rates, so we manually change the period key. - # account.report will then compute the currency table periods accordingly - middle_periods_period_key = '_trial_balance_middle_periods' - for col_group in options['column_groups'].values(): - col_group_date = col_group['forced_options'].get('date') - if col_group_date: - col_group_date['currency_table_period_key'] = middle_periods_period_key - - report._init_options_order_column(options, previous_options) - - def _custom_line_postprocessor(self, report, options, lines): - # If the hierarchy is enabled, ensure to add the o_account_coa_column_contrast class to the hierarchy lines - if options.get('hierarchy'): - for line in lines: - model, dummy = report._get_model_info_from_id(line['id']) - if model == 'account.group': - line_classes = line.get('class', '') - line['class'] = line_classes + ' o_account_coa_column_contrast_hierarchy' - - return lines - - def _create_column_group_initial_balance(self, report, options, previous_options, default_group_vals, side_to_append): - initial_balance_options = self.env['account.general.ledger.report.handler']._get_options_initial_balance(options) - initial_forced_options = { - 'date': initial_balance_options['date'], - 'include_current_year_in_unaff_earnings': initial_balance_options['include_current_year_in_unaff_earnings'], - 'no_impact_on_currency_table': True, - } - - self._create_and_append_column_group( - report, - options, - _("Initial Balance"), - initial_forced_options, - side_to_append, - default_group_vals, - ) - - def _create_column_group_end_balance(self, report, options, previous_options, default_group_vals, side_to_append): - end_date_to = options['date']['date_to'] - end_date_from = options['comparison']['periods'][-1]['date_from'] if options.get('comparison', {}).get('periods') else options['date']['date_from'] - end_forced_options = { - 'date': report._get_dates_period( - fields.Date.from_string(end_date_from), - fields.Date.from_string(end_date_to), - 'range', - ), - } - - self._create_and_append_column_group( - report, - options, - _("End Balance"), - end_forced_options, - side_to_append, - default_group_vals, - append_col_groups=False, - ) - - # We don't add end_column_groups on purpose: they shouldn't be computed, since we'll just sum the values of other groups in that one. - # So, we don't want to run any SQL for it. We also force a dedicated column_group_key on the end columns, to better identify them. - for column_data in side_to_append['columns'][-len(report.column_ids):]: - column_data['column_group_key'] = TRIAL_BALANCE_END_COLUMN_GROUP_KEY diff --git a/addons/at_accounting/models/balance_sheet.py b/addons/at_accounting/models/balance_sheet.py deleted file mode 100644 index 171a36d..0000000 --- a/addons/at_accounting/models/balance_sheet.py +++ /dev/null @@ -1,11 +0,0 @@ -from odoo import models - - -class BalanceSheetCustomHandler(models.AbstractModel): - _name = 'account.balance.sheet.report.handler' - _inherit = 'account.report.custom.handler' - _description = "Balance Sheet Custom Handler" - - def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings): - if options['currency_table']['type'] == 'cta': - warnings['at_accounting.common_possibly_unbalanced_because_cta'] = {} diff --git a/addons/at_accounting/models/bank_rec_widget.py b/addons/at_accounting/models/bank_rec_widget.py deleted file mode 100644 index 09acc53..0000000 --- a/addons/at_accounting/models/bank_rec_widget.py +++ /dev/null @@ -1,1767 +0,0 @@ -# -*- coding: utf-8 -*- -from collections import defaultdict -from contextlib import contextmanager -import json -import markupsafe - -from odoo import _, api, fields, models, Command -from odoo.addons.web.controllers.utils import clean_action -from odoo.exceptions import UserError, RedirectWarning -from odoo.tools.misc import formatLang - - -class BankRecWidget(models.Model): - _name = "bank.rec.widget" - _description = "Bank reconciliation widget for a single statement line" - - - _auto = False - _table_query = "0" - - # ==== Business fields ==== - st_line_id = fields.Many2one(comodel_name='account.bank.statement.line') - move_id = fields.Many2one( - related='st_line_id.move_id', - depends=['st_line_id'], - ) - st_line_checked = fields.Boolean( - related='st_line_id.move_id.checked', - depends=['st_line_id'], - ) - st_line_is_reconciled = fields.Boolean( - related='st_line_id.is_reconciled', - depends=['st_line_id'], - ) - st_line_journal_id = fields.Many2one( - related='st_line_id.journal_id', - depends=['st_line_id'], - ) - st_line_transaction_details = fields.Html( - compute='_compute_st_line_transaction_details', - ) - transaction_currency_id = fields.Many2one( - comodel_name='res.currency', - compute='_compute_transaction_currency_id', - ) - journal_currency_id = fields.Many2one( - comodel_name='res.currency', - compute='_compute_journal_currency_id', - ) - partner_id = fields.Many2one( - comodel_name='res.partner', - string="Partner", - compute='_compute_partner_id', - store=True, - readonly=False, - ) - line_ids = fields.One2many( - comodel_name='bank.rec.widget.line', - inverse_name='wizard_id', - compute='_compute_line_ids', - compute_sudo=False, - store=True, - readonly=False, - ) - available_reco_model_ids = fields.Many2many( - comodel_name='account.reconcile.model', - compute='_compute_available_reco_model_ids', - store=True, - readonly=False, - ) - selected_reco_model_id = fields.Many2one( - comodel_name='account.reconcile.model', - compute='_compute_selected_reco_model_id', - ) - partner_name = fields.Char( - related='st_line_id.partner_name', - ) - - company_id = fields.Many2one( - comodel_name='res.company', - related='st_line_id.company_id', - depends=['st_line_id'], - ) - - country_code = fields.Char(related='company_id.country_id.code', depends=['company_id']) - - company_currency_id = fields.Many2one( - string="Wizard Company Currency", - related='company_id.currency_id', - depends=['st_line_id'], - ) - matching_rules_allow_auto_reconcile = fields.Boolean() - - # ==== Display fields ==== - state = fields.Selection( - selection=[ - ('invalid', "Invalid"), - ('valid', "Valid"), - ('reconciled', "Reconciled"), - ], - compute='_compute_state', - store=True, - help="Invalid: The bank transaction can't be validate since the suspense account is still involved\n" - "Valid: The bank transaction can be validated.\n" - "Reconciled: The bank transaction has already been processed. Nothing left to do." - ) - is_multi_currency = fields.Boolean( - compute='_compute_is_multi_currency', - ) - - # ==== JS fields ==== - selected_aml_ids = fields.Many2many( - comodel_name='account.move.line', - compute='_compute_selected_aml_ids', - ) - todo_command = fields.Json( - store=False, - ) - return_todo_command = fields.Json( - store=False, - ) - form_index = fields.Char() - - # ------------------------------------------------------------------------- - # COMPUTE METHODS - # ------------------------------------------------------------------------- - - @api.depends('st_line_id') - def _compute_line_ids(self): - for wizard in self: - if wizard.st_line_id: - - # Liquidity line. - line_ids_commands = [ - Command.clear(), - Command.create(wizard._lines_prepare_liquidity_line()), - ] - - _liquidity_lines, _suspense_lines, other_lines = wizard.st_line_id._seek_for_lines() - for aml in other_lines: - exchange_diff_amls = (aml.matched_debit_ids + aml.matched_credit_ids) \ - .exchange_move_id.line_ids.filtered(lambda l: l.account_id != aml.account_id) - if wizard.state == 'reconciled' and exchange_diff_amls: - line_ids_commands.append( - Command.create(wizard._lines_prepare_aml_line( - aml, # Create the aml line with un-squashed amounts (aml - exchange diff) - balance=aml.balance - sum(exchange_diff_amls.mapped('balance')), - amount_currency=aml.amount_currency - sum(exchange_diff_amls.mapped('amount_currency')), - )) - ) - for exchange_diff_aml in exchange_diff_amls: - line_ids_commands.append( - Command.create(wizard._lines_prepare_aml_line(exchange_diff_aml)) - ) - else: - line_ids_commands.append(Command.create(wizard._lines_prepare_aml_line(aml))) - - wizard.line_ids = line_ids_commands - - wizard._lines_add_auto_balance_line() - - else: - - wizard.line_ids = [Command.clear()] - - @api.depends('st_line_id') - def _compute_available_reco_model_ids(self): - for wizard in self: - if wizard.st_line_id: - available_reco_models = self.env['account.reconcile.model'].search([ - ('rule_type', '=', 'writeoff_button'), - ('company_id', '=', wizard.st_line_id.company_id.id), - '|', - ('match_journal_ids', '=', False), - ('match_journal_ids', '=', wizard.st_line_id.journal_id.id), - ]) - available_reco_models = available_reco_models.filtered( - lambda x: x.counterpart_type == 'general' - or len(x.line_ids.journal_id) <= 1 - ) - wizard.available_reco_model_ids = [Command.set(available_reco_models.ids)] - else: - wizard.available_reco_model_ids = [Command.clear()] - - @api.depends('line_ids.reconcile_model_id') - def _compute_selected_reco_model_id(self): - for wizard in self: - selected_reconcile_models = wizard.line_ids.reconcile_model_id.filtered(lambda x: x.rule_type == 'writeoff_button') - if len(selected_reconcile_models) == 1: - wizard.selected_reco_model_id = selected_reconcile_models.id - else: - wizard.selected_reco_model_id = None - - @api.depends('st_line_id', 'line_ids.account_id') - def _compute_state(self): - for wizard in self: - if not wizard.st_line_id: - wizard.state = 'invalid' - elif wizard.st_line_id.is_reconciled: - wizard.state = 'reconciled' - else: - suspense_account = wizard.st_line_id.journal_id.suspense_account_id - if suspense_account in wizard.line_ids.account_id: - wizard.state = 'invalid' - else: - wizard.state = 'valid' - - @api.depends('st_line_id') - def _compute_journal_currency_id(self): - for wizard in self: - wizard.journal_currency_id = wizard.st_line_id.journal_id.currency_id \ - or wizard.st_line_id.journal_id.company_id.currency_id - - def _format_transaction_details(self): - """ Format the 'transaction_details' field of the statement line to be more readable for the end user. - - Example: - { - "debtor": { - "name": None, - "private_id": None, - }, - "debtor_account": { - "iban": "BE84103080286059", - "bank_transaction_code": None, - "credit_debit_indicator": "DBIT", - "status": "BOOK", - "value_date": "2022-12-29", - "transaction_date": None, - "balance_after_transaction": None, - }, - } - - Becomes: - debtor_account: - iban: BE84103080286059 - credit_debit_indicator: DBIT - status: BOOK - value_date: 2022-12-29 - - :return: An html representation of the transaction details. - """ - self.ensure_one() - details = self.st_line_id.transaction_details - if not details: - return - - if isinstance(details, str): - details = json.loads(details) - - def node_to_html(header, node): - if not node: - return "" - - if isinstance(node, dict): - li_elements = markupsafe.Markup("").join(node_to_html(f"{k}: ", v) for k, v in node.items()) - value = li_elements and markupsafe.Markup('
    %s
') % li_elements - elif isinstance(node, (tuple, list)): - li_elements = markupsafe.Markup("").join(node_to_html(f"{i}: ", v) for i, v in enumerate(node, start=1)) - value = li_elements and markupsafe.Markup('
    %s
') % li_elements - else: - value = node - - if not value: - return "" - - return markupsafe.Markup('
  • %(header)s%(value)s
  • ') % { - 'header': header, - 'value': value, - } - - main_html = node_to_html('', details) - return markupsafe.Markup("
      %s
    ") % main_html - - @api.depends('st_line_id') - def _compute_st_line_transaction_details(self): - for wizard in self: - wizard.st_line_transaction_details = wizard._format_transaction_details() - - @api.depends('st_line_id') - def _compute_transaction_currency_id(self): - for wizard in self: - wizard.transaction_currency_id = wizard.st_line_id.foreign_currency_id or wizard.journal_currency_id - - @api.depends('st_line_id') - def _compute_partner_id(self): - for wizard in self: - if wizard.st_line_id: - wizard.partner_id = wizard.st_line_id._retrieve_partner() - else: - wizard.partner_id = None - - @api.depends('company_id') - def _compute_is_multi_currency(self): - self.is_multi_currency = self.env.user.has_groups('base.group_multi_currency') - - @api.depends('company_id', 'line_ids.source_aml_id') - def _compute_selected_aml_ids(self): - for wizard in self: - wizard.selected_aml_ids = [Command.set(wizard.line_ids.source_aml_id.ids)] - - # ------------------------------------------------------------------------- - # ONCHANGE METHODS - # ------------------------------------------------------------------------- - - @api.onchange('todo_command') - def _onchange_todo_command(self): - self.ensure_one() - todo_command = self.todo_command - self.todo_command = None - self.return_todo_command = None - - # Ensure the lines are well loaded. - # Suppose the initial values of 'line_ids' are 2 lines, - # "self.line_ids = [Command.create(...)]" will produce a single new line in 'line_ids' but three lines in case - # the field is accessed before. - self._ensure_loaded_lines() - - method_name = todo_command['method_name'] - getattr(self, f'_js_action_{method_name}')(*todo_command.get('args', []), **todo_command.get('kwargs', {})) - - # ------------------------------------------------------------------------- - # LOW-LEVEL METHODS - # ------------------------------------------------------------------------- - - @api.model - def new(self, values=None, origin=None, ref=None): - widget = super().new(values=values, origin=origin, ref=ref) - - # Ensure the lines are well loaded. - # Suppose the initial values of 'line_ids' are 2 lines, - # "self.line_ids = [Command.create(...)]" will produce a single new line in 'line_ids' but three lines in case - # the field is accessed before. - widget.line_ids - - return widget - - # ------------------------------------------------------------------------- - # INIT - # ------------------------------------------------------------------------- - - @api.model - def fetch_initial_data(self): - # Fields. - fields = self.fields_get() - field_attributes = self.env['ir.ui.view']._get_view_field_attributes() - for field_name, field in self._fields.items(): - if field.type == 'one2many': - fields[field_name]['relatedFields'] = self[field_name]\ - .fields_get(attributes=field_attributes) - del fields[field_name]['relatedFields'][field.inverse_name] - for one2many_fieldname, one2many_field in self[field_name]._fields.items(): - if one2many_field.type == "many2many": - comodel = self.env[one2many_field.comodel_name] - fields[field_name]['relatedFields'][one2many_fieldname]['relatedFields'] = comodel \ - .fields_get(allfields=['id', 'display_name'], attributes=field_attributes) - elif field.name == 'available_reco_model_ids': - fields[field_name]['relatedFields'] = self[field_name]\ - .fields_get(allfields=['id', 'display_name'], attributes=field_attributes) - - fields['todo_command']['onChange'] = True - - # Initial values. - initial_values = {} - for field_name, field in self._fields.items(): - if field.type == 'one2many': - initial_values[field_name] = [] - else: - initial_values[field_name] = field.convert_to_read(self[field_name], self, {}) - - return { - 'initial_values': initial_values, - 'fields': fields, - } - - # ------------------------------------------------------------------------- - # LINES METHODS - # ------------------------------------------------------------------------- - - def _ensure_loaded_lines(self): - # Ensure the lines are well loaded. - # Suppose the initial values of 'line_ids' are 2 lines, - # "self.line_ids = [Command.create(...)]" will produce a single new line in 'line_ids' but three lines in case - # the field is accessed before. - self.line_ids - - def _lines_turn_auto_balance_into_manual_line(self, line): - # When editing an auto_balance line, it becomes a custom manual line. - if line.flag == 'auto_balance': - line.flag = 'manual' - - def _lines_get_line_in_edit_form(self): - self.ensure_one() - - if not self.form_index: - return - - return self.line_ids.filtered(lambda x: x.index == self.form_index) - - def _lines_prepare_aml_line(self, aml, **kwargs): - self.ensure_one() - return { - 'flag': 'aml', - 'source_aml_id': aml.id, - **kwargs, - } - - def _lines_prepare_liquidity_line(self): - """ Create a line corresponding to the journal item having the liquidity account on the statement line.""" - self.ensure_one() - st_line = self.st_line_id - - # In case of a different currencies on the journal and on the transaction, we need to retrieve the transaction - # amount on the suspense line because a journal item can only have one foreign currency. Indeed, in such - # configuration, the foreign currency amount expressed in journal's currency is set on the liquidity line but - # the transaction amount is on the suspense account line. - liquidity_line, _suspense_lines, _other_lines = st_line._seek_for_lines() - - return self._lines_prepare_aml_line(liquidity_line, flag='liquidity') - - def _lines_prepare_auto_balance_line(self): - """ Create the auto_balance line if necessary in order to have fully balanced lines.""" - self.ensure_one() - st_line = self.st_line_id - - # Compute the current open balance. - transaction_amount, transaction_currency, journal_amount, _journal_currency, company_amount, _company_currency \ - = self.st_line_id._get_accounting_amounts_and_currencies() - open_amount_currency = -transaction_amount - open_balance = -company_amount - for line in self.line_ids: - if line.flag in ('liquidity', 'auto_balance'): - continue - - open_balance -= line.balance - journal_transaction_rate = abs(transaction_amount / journal_amount) if journal_amount else 0.0 - company_transaction_rate = abs(transaction_amount / company_amount) if company_amount else 0.0 - if line.currency_id == self.transaction_currency_id: - open_amount_currency -= line.amount_currency - elif line.currency_id == self.journal_currency_id: - open_amount_currency -= transaction_currency.round(line.amount_currency * journal_transaction_rate) - else: - open_amount_currency -= transaction_currency.round(line.balance * company_transaction_rate) - - # Create a new auto-balance line. - account = None - partner = self.partner_id - if partner: - name = _("Open balance of %(amount)s", amount=formatLang(self.env, transaction_amount, currency_obj=transaction_currency)) - partner_is_customer = partner.customer_rank and not partner.supplier_rank - partner_is_supplier = partner.supplier_rank and not partner.customer_rank - if partner_is_customer: - account = partner.with_company(st_line.company_id).property_account_receivable_id - elif partner_is_supplier: - account = partner.with_company(st_line.company_id).property_account_payable_id - elif st_line.amount > 0: - account = partner.with_company(st_line.company_id).property_account_receivable_id - else: - account = partner.with_company(st_line.company_id).property_account_payable_id - - if not account: - name = st_line.payment_ref - account = st_line.journal_id.suspense_account_id - - return { - 'flag': 'auto_balance', - - 'account_id': account.id, - 'name': name, - 'amount_currency': open_amount_currency, - 'balance': open_balance, - } - - def _lines_add_auto_balance_line(self): - ''' Add the line auto balancing the debit/credit. ''' - - # Drop the existing line then re-create it to ensure this line is always the last one. - line_ids_commands = [] - for auto_balance_line in self.line_ids.filtered(lambda x: x.flag == 'auto_balance'): - line_ids_commands.append(Command.unlink(auto_balance_line.id)) - - # Re-create a new auto-balance line if needed. - auto_balance_line_vals = self._lines_prepare_auto_balance_line() - if not self.company_currency_id.is_zero(auto_balance_line_vals['balance']): - line_ids_commands.append(Command.create(auto_balance_line_vals)) - self.line_ids = line_ids_commands - - def _lines_prepare_new_aml_line(self, aml, **kwargs): - return self._lines_prepare_aml_line( - aml, - flag='new_aml', - currency_id=aml.currency_id.id, - amount_currency=-aml.amount_residual_currency, - balance=-aml.amount_residual, - source_amount_currency=-aml.amount_residual_currency, - source_balance=-aml.amount_residual, - **kwargs, - ) - - def _lines_check_partial_amount(self, line): - if line.flag != 'new_aml': - return None - - exchange_diff_line = self.line_ids\ - .filtered(lambda x: x.flag == 'exchange_diff' and x.source_aml_id == line.source_aml_id) - auto_balance_line_vals = self._lines_prepare_auto_balance_line() - - auto_balance = auto_balance_line_vals['balance'] - current_balance = line.balance + exchange_diff_line.balance - has_enough_comp_debit = self.company_currency_id.compare_amounts(auto_balance, 0) < 0 \ - and self.company_currency_id.compare_amounts(current_balance, 0) > 0 \ - and self.company_currency_id.compare_amounts(current_balance, -auto_balance) > 0 - has_enough_comp_credit = self.company_currency_id.compare_amounts(auto_balance, 0) > 0 \ - and self.company_currency_id.compare_amounts(current_balance, 0) < 0 \ - and self.company_currency_id.compare_amounts(-current_balance, auto_balance) > 0 - - auto_amount_currency = auto_balance_line_vals['amount_currency'] - current_amount_currency = line.amount_currency - has_enough_curr_debit = line.currency_id.compare_amounts(auto_amount_currency, 0) < 0 \ - and line.currency_id.compare_amounts(current_amount_currency, 0) > 0 \ - and line.currency_id.compare_amounts(current_amount_currency, -auto_amount_currency) > 0 - has_enough_curr_credit = line.currency_id.compare_amounts(auto_amount_currency, 0) > 0 \ - and line.currency_id.compare_amounts(current_amount_currency, 0) < 0 \ - and line.currency_id.compare_amounts(-current_amount_currency, auto_amount_currency) > 0 - - if line.currency_id == self.transaction_currency_id: - if has_enough_curr_debit or has_enough_curr_credit: - amount_currency_after_partial = current_amount_currency + auto_amount_currency - - # Get the bank transaction rate. - transaction_amount, _transaction_currency, _journal_amount, _journal_currency, company_amount, _company_currency \ - = self.st_line_id._get_accounting_amounts_and_currencies() - rate = abs(company_amount / transaction_amount) if transaction_amount else 0.0 - - # Compute the amounts to make a partial. - balance_after_partial = line.company_currency_id.round(amount_currency_after_partial * rate) - new_line_balance = line.company_currency_id.round(balance_after_partial * abs(line.balance) / abs(current_balance)) - exchange_diff_line_balance = balance_after_partial - new_line_balance - return { - 'exchange_diff_line': exchange_diff_line, - 'amount_currency': amount_currency_after_partial, - 'balance': new_line_balance, - 'exchange_balance': exchange_diff_line_balance, - } - elif has_enough_comp_debit or has_enough_comp_credit: - # Compute the new value for balance. - balance_after_partial = current_balance + auto_balance - - # Get the rate of the original journal item. - rate = abs(line.source_amount_currency) / abs(line.source_balance) - - # Compute the amounts to make a partial. - new_line_balance = line.company_currency_id.round(balance_after_partial * abs(line.balance) / abs(current_balance)) - exchange_diff_line_balance = balance_after_partial - new_line_balance - amount_currency_after_partial = line.currency_id.round(new_line_balance * rate) - return { - 'exchange_diff_line': exchange_diff_line, - 'amount_currency': amount_currency_after_partial, - 'balance': new_line_balance, - 'exchange_balance': exchange_diff_line_balance, - } - return None - - def _do_amounts_apply_for_early_payment(self, open_amount_currency, total_early_payment_discount): - return self.transaction_currency_id.compare_amounts(open_amount_currency, total_early_payment_discount) == 0 - - def _lines_check_apply_early_payment_discount(self): - """ Try to apply the early payment discount on the currently mounted journal items. - :return: True if applied, False otherwise. - """ - all_aml_lines = self.line_ids.filtered(lambda x: x.flag == 'new_aml') - - # Get the balance without the 'new_aml' lines. - auto_balance_line_vals = self._lines_prepare_auto_balance_line() - open_balance_wo_amls = auto_balance_line_vals['balance'] + sum(all_aml_lines.mapped('balance')) - open_amount_currency_wo_amls = auto_balance_line_vals['amount_currency'] + sum(all_aml_lines.mapped('amount_currency')) - - # Get the balance after adding the 'new_aml' lines but without considering the partial amounts. - open_balance = open_balance_wo_amls - sum(all_aml_lines.mapped('source_balance')) - open_amount_currency = open_amount_currency_wo_amls - sum(all_aml_lines.mapped('source_amount_currency')) - - is_same_currency = all_aml_lines.currency_id == self.transaction_currency_id - at_least_one_aml_for_early_payment = False - - early_pay_aml_values_list = [] - total_early_payment_discount = 0.0 - - for aml_line in all_aml_lines: - aml = aml_line.source_aml_id - - if aml.move_id._is_eligible_for_early_payment_discount(self.transaction_currency_id, self.st_line_id.date): - at_least_one_aml_for_early_payment = True - total_early_payment_discount += aml.amount_currency - aml.discount_amount_currency - - early_pay_aml_values_list.append({ - 'aml': aml, - 'amount_currency': aml_line.amount_currency, - 'balance': aml_line.balance, - }) - - line_ids_create_command_list = [] - is_early_payment_applied = False - - # Cleanup the existing early payment discount lines. - for line in self.line_ids.filtered(lambda x: x.flag == 'early_payment'): - line_ids_create_command_list.append(Command.unlink(line.id)) - - if is_same_currency \ - and at_least_one_aml_for_early_payment \ - and self._do_amounts_apply_for_early_payment(open_amount_currency, total_early_payment_discount): - # == Compute the early payment discount lines == - # Remove the partials on existing lines. - for aml_line in all_aml_lines: - aml_line.amount_currency = aml_line.source_amount_currency - aml_line.balance = aml_line.source_balance - - # Add the early payment lines. - early_payment_values = self.env['account.move']._get_invoice_counterpart_amls_for_early_payment_discount( - early_pay_aml_values_list, - open_balance, - ) - - for vals_list in early_payment_values.values(): - for vals in vals_list: - line_ids_create_command_list.append(Command.create({ - 'flag': 'early_payment', - 'account_id': vals['account_id'], - 'date': self.st_line_id.date, - 'name': vals['name'], - 'partner_id': vals['partner_id'], - 'currency_id': vals['currency_id'], - 'amount_currency': vals['amount_currency'], - 'balance': vals['balance'], - 'analytic_distribution': vals.get('analytic_distribution'), - 'tax_ids': vals.get('tax_ids', []), - 'tax_tag_ids': vals.get('tax_tag_ids', []), - 'tax_repartition_line_id': vals.get('tax_repartition_line_id'), - 'group_tax_id': vals.get('group_tax_id'), - })) - is_early_payment_applied = True - - if line_ids_create_command_list: - self.line_ids = line_ids_create_command_list - - return is_early_payment_applied - - def _lines_check_apply_partial_matching(self): - """ Try to apply a partial matching on the currently mounted journal items. - :return: True if applied, False otherwise. - """ - all_aml_lines = self.line_ids.filtered(lambda x: x.flag == 'new_aml') - if all_aml_lines: - last_line = all_aml_lines[-1] - - # Cleanup the existing partials if not on the last line. - line_ids_commands = [] - lines_impacted = self.env['bank.rec.widget.line'] - for aml_line in all_aml_lines: - is_partial = aml_line.display_stroked_amount_currency or aml_line.display_stroked_balance - if is_partial and not aml_line.manually_modified: - line_ids_commands.append(Command.update(aml_line.id, { - 'amount_currency': aml_line.source_amount_currency, - 'balance': aml_line.source_balance, - })) - lines_impacted |= aml_line - if line_ids_commands: - self.line_ids = line_ids_commands - self._lines_recompute_exchange_diff(lines_impacted) - - # Check for a partial reconciliation. - partial_amounts = self._lines_check_partial_amount(last_line) - - if partial_amounts: - # Make a partial: an auto-balance line is no longer necessary. - last_line.amount_currency = partial_amounts['amount_currency'] - last_line.balance = partial_amounts['balance'] - exchange_line = partial_amounts['exchange_diff_line'] - if exchange_line: - exchange_line.balance = partial_amounts['exchange_balance'] - if exchange_line.currency_id == self.company_currency_id: - exchange_line.amount_currency = exchange_line.balance - return True - - return False - - def _lines_load_new_amls(self, amls, reco_model=None): - """ Create counterpart lines for the journal items passed as parameter.""" - # Create a new line for each aml. - line_ids_commands = [] - kwargs = {'reconcile_model_id': reco_model.id} if reco_model else {} - for aml in amls: - aml_line_vals = self._lines_prepare_new_aml_line(aml, **kwargs) - line_ids_commands.append(Command.create(aml_line_vals)) - - if not line_ids_commands: - return - - self.line_ids = line_ids_commands - - def _prepare_base_line_for_taxes_computation(self, line): - """ Convert the current dictionary in order to use the generic taxes computation method defined on account.tax. - :return: A python dictionary. - """ - self.ensure_one() - tax_type = line.tax_ids[0].type_tax_use if line.tax_ids else None - is_refund = (tax_type == 'sale' and line.balance > 0.0) or (tax_type == 'purchase' and line.balance < 0.0) - - if line.force_price_included_taxes and line.tax_ids: - special_mode = 'total_included' - base_amount = line.tax_base_amount_currency - else: - special_mode = 'total_excluded' - base_amount = line.amount_currency - - return self.env['account.tax']._prepare_base_line_for_taxes_computation( - line, - price_unit=base_amount, - quantity=1.0, - is_refund=is_refund, - special_mode=special_mode, - ) - - def _prepare_tax_line_for_taxes_computation(self, line): - """ Convert the current dictionary in order to use the generic taxes computation method defined on account.tax. - :return: A python dictionary. - """ - self.ensure_one() - return self.env['account.tax']._prepare_tax_line_for_taxes_computation(line) - - def _lines_prepare_tax_line(self, tax_line_vals): - self.ensure_one() - - tax_rep = self.env['account.tax.repartition.line'].browse(tax_line_vals['tax_repartition_line_id']) - name = tax_rep.tax_id.name - if self.st_line_id.payment_ref: - name = f'{name} - {self.st_line_id.payment_ref}' - currency = self.env['res.currency'].browse(tax_line_vals['currency_id']) - amount_currency = tax_line_vals['amount_currency'] - balance = self.st_line_id._prepare_counterpart_amounts_using_st_line_rate(currency, None, amount_currency)['balance'] - - return { - 'flag': 'tax_line', - - 'account_id': tax_line_vals['account_id'], - 'date': self.st_line_id.date, - 'name': name, - 'partner_id': tax_line_vals['partner_id'], - 'currency_id': currency.id, - 'amount_currency': amount_currency, - 'balance': balance, - - 'analytic_distribution': tax_line_vals['analytic_distribution'], - 'tax_repartition_line_id': tax_rep.id, - 'tax_ids': tax_line_vals['tax_ids'], - 'tax_tag_ids': tax_line_vals['tax_tag_ids'], - 'group_tax_id': tax_line_vals['group_tax_id'], - } - - def _lines_recompute_taxes(self): - self.ensure_one() - AccountTax = self.env['account.tax'] - base_amls = self.line_ids.filtered(lambda x: x.flag == 'manual' and not x.tax_repartition_line_id) - base_lines = [self._prepare_base_line_for_taxes_computation(x) for x in base_amls] - tax_amls = self.line_ids.filtered(lambda x: x.flag == 'tax_line') - tax_lines = [self._prepare_tax_line_for_taxes_computation(x) for x in tax_amls] - AccountTax._add_tax_details_in_base_lines(base_lines, self.company_id) - AccountTax._round_base_lines_tax_details(base_lines, self.company_id) - AccountTax._add_accounting_data_in_base_lines_tax_details(base_lines, self.company_id, include_caba_tags=True) - tax_results = AccountTax._prepare_tax_lines(base_lines, self.company_id, tax_lines=tax_lines) - - line_ids_commands = [] - - # Update the base lines. - for base_line, to_update in tax_results['base_lines_to_update']: - line = base_line['record'] - amount_currency = to_update['amount_currency'] - balance = self.st_line_id\ - ._prepare_counterpart_amounts_using_st_line_rate(line.currency_id, line.source_balance, amount_currency)['balance'] - - line_ids_commands.append(Command.update(line.id, { - 'balance': balance, - 'amount_currency': amount_currency, - 'tax_tag_ids': to_update['tax_tag_ids'], - })) - - # Tax lines that are no longer needed. - for tax_line_vals in tax_results['tax_lines_to_delete']: - line_ids_commands.append(Command.unlink(tax_line_vals['record'].id)) - - # Newly created tax lines. - for tax_line_vals in tax_results['tax_lines_to_add']: - line_ids_commands.append(Command.create(self._lines_prepare_tax_line(tax_line_vals))) - - # Update of existing tax lines. - for tax_line_vals, grouping_key, to_update in tax_results['tax_lines_to_update']: - new_line_vals = self._lines_prepare_tax_line({**grouping_key, **to_update}) - line_ids_commands.append(Command.update(tax_line_vals['record'].id, { - 'amount_currency': new_line_vals['amount_currency'], - 'balance': new_line_vals['balance'], - })) - - self.line_ids = line_ids_commands - - def _get_key_mapping_aml_and_exchange_diff(self, line): - if line.source_aml_id: - return 'source_aml_id', line.source_aml_id.id - return None, None - - def _reorder_exchange_and_aml_lines(self): - # Reorder to put each exchange line right after the corresponding new_aml. - new_lines_ids = [] - exchange_lines = self.line_ids.filtered(lambda x: x.flag == 'exchange_diff') - source_2_exchange_mapping = defaultdict(lambda: self.env['bank.rec.widget.line']) - for line in exchange_lines: - source_2_exchange_mapping[self._get_key_mapping_aml_and_exchange_diff(line)] |= line - for line in self.line_ids: - if line in exchange_lines: - continue - - new_lines_ids.append(line.id) - line_key = self._get_key_mapping_aml_and_exchange_diff(line) - if line_key in source_2_exchange_mapping: - new_lines_ids += source_2_exchange_mapping[line_key].mapped('id') - self.line_ids = self.env['bank.rec.widget.line'].browse(new_lines_ids) - - def _remove_related_exchange_diff_lines(self, lines): - """ Delete the exchange_diff_lines related to the lines given in parameter. - If the parameter (lines) is not set, then all exchange_diff_lines will be removed - """ - exch_diff_command_unlink = [] - for line in lines: - if line.flag == 'exchange_diff': - continue - - line_source_key, line_source_id = self._get_key_mapping_aml_and_exchange_diff(line) - if not line_source_key: - continue - exch_diff_command_unlink += [ - Command.unlink(exch_diff.id) - for exch_diff in self.line_ids.filtered(lambda x: x[line_source_key] and x[line_source_key].id == line_source_id) - ] - - if exch_diff_command_unlink: - self.line_ids = exch_diff_command_unlink - - def _lines_get_account_balance_exchange_diff(self, currency, balance, amount_currency): - # Compute the balance of the line using the rate/currency coming from the bank transaction. - amounts_in_st_curr = self.st_line_id._prepare_counterpart_amounts_using_st_line_rate( - currency, - balance, - amount_currency, - ) - origin_balance = amounts_in_st_curr['balance'] - if currency == self.company_currency_id and self.transaction_currency_id != self.company_currency_id: - # The reconciliation will be expressed using the rate of the statement line. - origin_balance = balance - elif currency != self.company_currency_id and self.transaction_currency_id == self.company_currency_id: - # The reconciliation will be expressed using the foreign currency of the aml to cover the Mexican - # case. - origin_balance = currency\ - ._convert(amount_currency, self.transaction_currency_id, self.company_id, self.st_line_id.date) - - # Compute the exchange difference balance. - exchange_diff_balance = origin_balance - balance - if self.company_currency_id.is_zero(exchange_diff_balance): - return self.env['account.account'], 0.0 - - expense_exchange_account = self.company_id.expense_currency_exchange_account_id - income_exchange_account = self.company_id.income_currency_exchange_account_id - - if exchange_diff_balance > 0.0: - account = expense_exchange_account - else: - account = income_exchange_account - return account, exchange_diff_balance - - def _lines_get_exchange_diff_values(self, line): - if line.flag != 'new_aml': - return [] - account, exchange_diff_balance = self._lines_get_account_balance_exchange_diff(line.currency_id, line.balance, line.amount_currency) - if line.currency_id.is_zero(exchange_diff_balance): - return [] - return [{ - 'flag': 'exchange_diff', - 'source_aml_id': line.source_aml_id.id, - 'account_id': account.id, - 'date': line.date, - 'name': _("Exchange Difference: %s", line.name), - 'partner_id': line.partner_id.id, - 'currency_id': line.currency_id.id, - 'amount_currency': exchange_diff_balance if line.currency_id == self.company_currency_id else 0.0, - 'balance': exchange_diff_balance, - 'source_amount_currency': line.amount_currency, - 'source_balance': exchange_diff_balance, - }] - - def _lines_recompute_exchange_diff(self, lines): - """ Recompute the exchange_diffs for the given lines, creating some if necessary. - If lines are not given, the method will be applied on all new_amls - """ - self.ensure_one() - # If the method is called after deleting lines we should delete the related exchange diffs - deleted_lines = lines - self.line_ids - self._remove_related_exchange_diff_lines(deleted_lines) - lines = lines - deleted_lines - - exchange_diffs_aml = self.line_ids.filtered(lambda x: x.flag == 'exchange_diff').grouped('source_aml_id') - line_ids_commands = [] - reorder_needed = False - - for line in lines: - exchange_diff_values = self._lines_get_exchange_diff_values(line) - if line.source_aml_id and line.source_aml_id in exchange_diffs_aml: - line_ids_commands += [ - Command.update(exchange_diffs_aml[line.source_aml_id].id, exch_diff_val) - for exch_diff_val in exchange_diff_values - ] - else: - line_ids_commands += [ - Command.create(exch_diff_val) - for exch_diff_val in exchange_diff_values - ] - reorder_needed = True - - if line_ids_commands: - self.line_ids = line_ids_commands - if reorder_needed: - self._reorder_exchange_and_aml_lines() - - def _lines_prepare_reco_model_write_off_vals(self, reco_model, write_off_vals): - self.ensure_one() - - balance = self.st_line_id\ - ._prepare_counterpart_amounts_using_st_line_rate(self.transaction_currency_id, None, write_off_vals['amount_currency'])['balance'] - - return { - 'flag': 'manual', - - 'account_id': write_off_vals['account_id'], - 'date': self.st_line_id.date, - 'name': write_off_vals['name'], - 'partner_id': write_off_vals['partner_id'], - 'currency_id': write_off_vals['currency_id'], - 'amount_currency': write_off_vals['amount_currency'], - 'balance': balance, - 'tax_base_amount_currency': write_off_vals['amount_currency'], - 'force_price_included_taxes': True, - - 'reconcile_model_id': reco_model.id, - 'analytic_distribution': write_off_vals['analytic_distribution'], - 'tax_ids': write_off_vals['tax_ids'], - } - - # ------------------------------------------------------------------------- - # LINES UPDATE METHODS - # ------------------------------------------------------------------------- - - def _line_value_changed_account_id(self, line): - self.ensure_one() - self._lines_turn_auto_balance_into_manual_line(line) - - # Recompute taxes. - if line.flag not in ('tax_line', 'early_payment') and line.tax_ids: - self._lines_recompute_taxes() - self._lines_add_auto_balance_line() - - def _line_value_changed_date(self, line): - self.ensure_one() - if line.flag == 'liquidity' and line.date: - self.st_line_id.date = line.date - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_global_info': True, 'reset_record': True} - - def _line_value_changed_ref(self, line): - self.ensure_one() - if line.flag == 'liquidity': - self.st_line_id.move_id.ref = line.ref - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_record': True} - - def _line_value_changed_narration(self, line): - self.ensure_one() - if line.flag == 'liquidity': - self.st_line_id.move_id.narration = line.narration - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_record': True} - - def _line_value_changed_name(self, line): - self.ensure_one() - if line.flag == 'liquidity': - self.st_line_id.payment_ref = line.name - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_global_info': True, 'reset_record': True} - return - - self._lines_turn_auto_balance_into_manual_line(line) - - def _line_value_changed_amount_transaction_currency(self, line): - self.ensure_one() - if line.flag == 'liquidity': - if line.transaction_currency_id != self.journal_currency_id: - self.st_line_id.amount_currency = line.amount_transaction_currency - self.st_line_id.foreign_currency_id = line.transaction_currency_id - else: - self.st_line_id.amount_currency = 0.0 - self.st_line_id.foreign_currency_id = None - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_global_info': True, 'reset_record': True} - - def _line_value_changed_transaction_currency_id(self, line): - self._line_value_changed_amount_transaction_currency(line) - - def _line_value_changed_amount_currency(self, line): - self.ensure_one() - if line.flag == 'liquidity': - self.st_line_id.amount = line.amount_currency - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_global_info': True, 'reset_record': True} - return - - self._lines_turn_auto_balance_into_manual_line(line) - - sign = -1 if line.amount_currency < 0.0 else 1 - if line.flag == 'new_aml': - # The balance must keep the same sign as the original aml and must not exceed its original value. - line.amount_currency = sign * max(0.0, min(abs(line.amount_currency), abs(line.source_amount_currency))) - line.manually_modified = True - - # If the user remove completely the value, reset to the original balance. - if not line.amount_currency: - line.amount_currency = line.source_amount_currency - - elif not line.amount_currency: - line.amount_currency = 0.0 - - if line.currency_id == line.company_currency_id: - # Single currency: amount_currency must be equal to balance. - line.balance = line.amount_currency - elif line.flag == 'new_aml': - if line.currency_id.compare_amounts(abs(line.amount_currency), abs(line.source_amount_currency)) == 0.0: - # The value has been reset to its original value. Reset the balance as well to avoid rounding issues. - line.balance = line.source_balance - else: - # Apply the rate. - if line.source_balance: - rate = abs(line.source_amount_currency / line.source_balance) - line.balance = line.company_currency_id.round(line.amount_currency / rate) - else: - line.balance = 0.0 - elif line.flag in ('manual', 'early_payment', 'tax_line'): - if line.currency_id in (self.transaction_currency_id, self.journal_currency_id): - line.balance = self.st_line_id\ - ._prepare_counterpart_amounts_using_st_line_rate(line.currency_id, None, line.amount_currency)['balance'] - else: - line.balance = line.currency_id\ - ._convert(line.amount_currency, self.company_currency_id, self.company_id, self.st_line_id.date) - - if line.flag not in ('tax_line', 'early_payment'): - if line.tax_ids: - # Manual edition of amounts. Disable the price_included mode. - line.force_price_included_taxes = False - self._lines_recompute_taxes() - self._lines_recompute_exchange_diff(line) - - self._lines_add_auto_balance_line() - - def _line_value_changed_balance(self, line): - self.ensure_one() - if line.flag == 'liquidity': - self.st_line_id.amount = line.balance - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_global_info': True, 'reset_record': True} - return - - self._lines_turn_auto_balance_into_manual_line(line) - - sign = -1 if line.balance < 0.0 else 1 - if line.flag == 'new_aml': - # The balance must keep the same sign as the original aml and must not exceed its original value. - line.balance = sign * max(0.0, min(abs(line.balance), abs(line.source_balance))) - line.manually_modified = True - - # If the user remove completely the value, reset to the original balance. - if not line.balance: - line.balance = line.source_balance - - elif not line.balance: - line.balance = 0.0 - - # Single currency: amount_currency must be equal to balance. - if line.currency_id == line.company_currency_id: - line.amount_currency = line.balance - self._line_value_changed_amount_currency(line) - elif line.flag == 'exchange_diff': - self._lines_add_auto_balance_line() - else: - self._lines_recompute_exchange_diff(line) - self._lines_add_auto_balance_line() - - def _line_value_changed_currency_id(self, line): - self.ensure_one() - self._line_value_changed_amount_currency(line) - - def _line_value_changed_tax_ids(self, line): - self.ensure_one() - self._lines_turn_auto_balance_into_manual_line(line) - - if line.tax_ids: - # Adding taxes but no tax before. - if not line.tax_base_amount_currency: - line.tax_base_amount_currency = line.amount_currency - line.force_price_included_taxes = True - else: - if line.force_price_included_taxes: - # Removing taxes letting the field empty. - # If the user didn't touch the amount_currency/balance, restore the original amount. - line.amount_currency = line.tax_base_amount_currency - self._line_value_changed_amount_currency(line) - line.tax_base_amount_currency = False - - self._lines_recompute_taxes() - self._lines_add_auto_balance_line() - - def _line_value_changed_partner_id(self, line): - self.ensure_one() - if line.flag == 'liquidity': - self.st_line_id.partner_id = line.partner_id - self._action_reload_liquidity_line() - self.return_todo_command = {'reset_global_info': True, 'reset_record': True} - return - - self._lines_turn_auto_balance_into_manual_line(line) - - new_account = None - if line.partner_id: - partner_is_customer = line.partner_id.customer_rank and not line.partner_id.supplier_rank - partner_is_supplier = line.partner_id.supplier_rank and not line.partner_id.customer_rank - is_partner_receivable_amount_zero = line.partner_currency_id.is_zero(line.partner_receivable_amount) - is_partner_payable_amount_zero = line.partner_currency_id.is_zero(line.partner_payable_amount) - if partner_is_customer or not is_partner_receivable_amount_zero and is_partner_payable_amount_zero: - new_account = line.partner_receivable_account_id - elif partner_is_supplier or is_partner_receivable_amount_zero and not is_partner_payable_amount_zero: - new_account = line.partner_payable_account_id - elif self.st_line_id.amount < 0.0: - new_account = line.partner_payable_account_id or line.partner_receivable_account_id - else: - new_account = line.partner_receivable_account_id or line.partner_payable_account_id - - if new_account: - # Set the new receivable/payable account if any. - line.account_id = new_account - self._line_value_changed_account_id(line) - elif line.flag not in ('tax_line', 'early_payment') and line.tax_ids: - # Recompute taxes. - self._lines_recompute_taxes() - self._lines_add_auto_balance_line() - - def _line_value_changed_analytic_distribution(self, line): - self.ensure_one() - self._lines_turn_auto_balance_into_manual_line(line) - - # Recompute taxes. - if line.flag not in ('tax_line', 'early_payment') and any(x.analytic for x in line.tax_ids): - self._lines_recompute_taxes() - self._lines_add_auto_balance_line() - - # ------------------------------------------------------------------------- - # ACTIONS - # ------------------------------------------------------------------------- - - def _action_trigger_matching_rules(self): - self.ensure_one() - - if self.st_line_id.is_reconciled: - return - - reconcile_models = self.env['account.reconcile.model'].search([ - ('rule_type', '!=', 'writeoff_button'), - ('company_id', '=', self.company_id.id), - '|', - ('match_journal_ids', '=', False), - ('match_journal_ids', '=', self.st_line_id.journal_id.id), - ]) - matching = reconcile_models._apply_rules(self.st_line_id, self.partner_id) - - if matching.get('amls'): - reco_model = matching['model'] - # In case there is a write-off, keep the whole amount and let the write-off doing the auto-balancing. - allow_partial = matching.get('status') != 'write_off' - self._action_add_new_amls(matching['amls'], reco_model=reco_model, allow_partial=allow_partial) - if matching.get('status') == 'write_off': - reco_model = matching['model'] - self._action_select_reconcile_model(reco_model) - if matching.get('auto_reconcile'): - self.matching_rules_allow_auto_reconcile = True - return matching - - def _prepare_embedded_views_data(self): - self.ensure_one() - st_line = self.st_line_id - - context = { - 'search_view_ref': 'at_accounting.view_account_move_line_search_bank_rec_widget', - 'list_view_ref': 'at_accounting.view_account_move_line_list_bank_rec_widget', - } - - if self.partner_id: - context['search_default_partner_id'] = self.partner_id.id - - dynamic_filters = [] - - # == Dynamic Customer/Vendor filter == - journal = st_line.journal_id - - account_ids = set() - - inbound_accounts = journal._get_journal_inbound_outstanding_payment_accounts() - journal.default_account_id - outbound_accounts = journal._get_journal_outbound_outstanding_payment_accounts() - journal.default_account_id - - # Matching on debit account. - for account in inbound_accounts: - account_ids.add(account.id) - - # Matching on credit account. - for account in outbound_accounts: - account_ids.add(account.id) - - rec_pay_matching_filter = { - 'name': 'receivable_payable_matching', - 'description': _("Customer/Vendor"), - 'domain': [ - '|', - # Matching invoices. - '&', - ('account_id.account_type', 'in', ('asset_receivable', 'liability_payable')), - ('payment_id', '=', False), - # Matching Payments. - '&', - ('account_id', 'in', tuple(account_ids)), - ('payment_id', '!=', False), - ], - 'no_separator': True, - 'is_default': False, - } - - misc_matching_filter = { - 'name': 'misc_matching', - 'description': _("Misc"), - 'domain': ['!'] + rec_pay_matching_filter['domain'], - 'is_default': False, - } - - dynamic_filters.append(rec_pay_matching_filter) - dynamic_filters.append(misc_matching_filter) - - # Stringify the domain. - for dynamic_filter in dynamic_filters: - dynamic_filter['domain'] = str(dynamic_filter['domain']) - - return { - 'amls': { - 'domain': st_line._get_default_amls_matching_domain(), - 'dynamic_filters': dynamic_filters, - 'context': context, - }, - } - - def _action_mount_st_line(self, st_line): - self.ensure_one() - self.st_line_id = st_line - self.form_index = self.line_ids[0].index if self.state == 'reconciled' else None - self._action_trigger_matching_rules() - - def _js_action_mount_st_line(self, st_line_id): - self.ensure_one() - st_line = self.env['account.bank.statement.line'].browse(st_line_id) - self._action_mount_st_line(st_line) - self.return_todo_command = self._prepare_embedded_views_data() - - def _js_action_restore_st_line_data(self, initial_data): - self.ensure_one() - initial_values = initial_data['initial_values'] - - self.st_line_id = self.env['account.bank.statement.line'].browse(initial_values['st_line_id']) - return_todo_command = initial_values['return_todo_command'] - - # Skip restore and trigger matching rules if the liquidity line was modified - liquidity_line = self.line_ids.filtered(lambda l: l.flag == 'liquidity') - initial_liquidity_line_values = next((cmd[2] for cmd in initial_values['line_ids'] if cmd[2]['flag'] == 'liquidity'), {}) - initial_liquidity_line = self.env['bank.rec.widget.line'].new(initial_liquidity_line_values) - for field in initial_liquidity_line_values.keys() - ['index', 'suggestion_html']: - if initial_liquidity_line[field] != liquidity_line[field]: - self._js_action_mount_st_line(self.st_line_id.id) - return - - # If the user goes to reco model and create a new one, we want to make it appearing when coming back. - # That's why we pop 'available_reco_model_ids' as well. - for field_name in ('id', 'st_line_id', 'todo_command', 'return_todo_command', 'available_reco_model_ids'): - initial_values.pop(field_name, None) - - st_line_domain = self.st_line_id._get_default_amls_matching_domain() - initial_values['line_ids'] = self._process_restore_lines_ids(initial_values['line_ids']) - self.update(initial_values) - - if ( - return_todo_command - and return_todo_command.get('res_model') == 'account.move' - and (created_invoice := self.env['account.move'].browse(return_todo_command['res_id'])) - and created_invoice.state == 'posted' - ): - lines = created_invoice.line_ids.filtered_domain(st_line_domain) - self._action_add_new_amls(lines) - else: - self._lines_add_auto_balance_line() - - self.return_todo_command = self._prepare_embedded_views_data() - - def _process_restore_lines_ids(self, initial_commands): - st_line_domain = self.st_line_id._get_default_amls_matching_domain() - still_available_aml_ids = self.env['account.move.line'].browse( - orm_command[2]['source_aml_id'] - for orm_command in initial_commands - if orm_command[0] == Command.CREATE and orm_command[2].get('source_aml_id') - ).filtered_domain(st_line_domain).ids - still_available_aml_ids += [None] # still available if there was no source - line_ids_commands = [Command.clear()] - for orm_command in initial_commands: - match orm_command: - case (Command.CREATE, _, values) if values.get('source_aml_id' in still_available_aml_ids): - # Discard the virtual id coming from the client - line_ids_commands.append(Command.create(values)) - case _: - line_ids_commands.append(orm_command) - return line_ids_commands - - def _action_reload_liquidity_line(self): - self.ensure_one() - self = self.with_context(default_st_line_id=self.st_line_id.id) - - self.invalidate_model() - - # Ensure the lines are well loaded. - # Suppose the initial values of 'line_ids' are 2 lines, - # "self.line_ids = [Command.create(...)]" will produce a single new line in 'line_ids' but three lines in case - # the field is accessed before. - self.line_ids - - self._action_trigger_matching_rules() - - # Focus back the liquidity line. - self._js_action_mount_line_in_edit(self.line_ids.filtered(lambda x: x.flag == 'liquidity').index) - - def _validation_lines_vals(self, line_ids_create_command_list, aml_to_exchange_diff_vals, to_reconcile): - partners = (self.line_ids.filtered(lambda x: x.flag != 'liquidity')).partner_id - partner_to_set = partners if len(partners) == 1 else self.env['res.partner'] - source2exchange = self.line_ids.filtered(lambda l: l.flag == 'exchange_diff').grouped('source_aml_id') - for line in self.line_ids: - if line.flag == 'exchange_diff': - continue - - amount_currency = line.amount_currency - balance = line.balance - if line.flag == 'new_aml': - to_reconcile.append((len(line_ids_create_command_list) + 1, line.source_aml_id)) - exchange_diff = source2exchange.get(line.source_aml_id) - if exchange_diff: - aml_to_exchange_diff_vals[len(line_ids_create_command_list) + 1] = { - 'amount_residual': exchange_diff.balance, - 'amount_residual_currency': exchange_diff.amount_currency, - 'analytic_distribution': exchange_diff.analytic_distribution, - } - # Squash amounts of exchange diff into corresponding new_aml - amount_currency += exchange_diff.amount_currency - balance += exchange_diff.balance - line_ids_create_command_list.append(Command.create(line._get_aml_values( - sequence=len(line_ids_create_command_list) + 1, - partner_id=partner_to_set.id if line.flag in ('liquidity', 'auto_balance') else line.partner_id.id, - amount_currency=amount_currency, - balance=balance, - ))) - - def _action_validate(self): - self.ensure_one() - partners = (self.line_ids.filtered(lambda x: x.flag != 'liquidity')).partner_id - partner_to_set = partners if len(partners) == 1 else self.env['res.partner'] - - # Prepare the lines to be created. - to_reconcile = [] - line_ids_create_command_list = [] - aml_to_exchange_diff_vals = {} - - self._validation_lines_vals(line_ids_create_command_list, aml_to_exchange_diff_vals, to_reconcile) - - st_line = self.st_line_id - move = st_line.move_id - - # Update the move. - move_ctx = move.with_context( - force_delete=True, - skip_readonly_check=True, - ) - move_ctx.write({'partner_id': partner_to_set.id, 'line_ids': [Command.clear()] + line_ids_create_command_list}) - - AccountMoveLine = self.env['account.move.line'] - sequence2lines = move_ctx.line_ids.grouped('sequence') - lines = [ - (sequence2lines[index], counterpart_aml) - for index, counterpart_aml in to_reconcile - ] - all_line_ids = tuple({_id for line, counterpart in lines for _id in (line + counterpart).ids}) - # Handle exchange diffs - exchange_diff_moves = None - lines_with_exch_diff = AccountMoveLine - if aml_to_exchange_diff_vals: - exchange_diff_vals_list = [] - for line, counterpart in lines: - line = line.with_prefetch(all_line_ids) - counterpart = counterpart.with_prefetch(all_line_ids) - exchange_diff_amounts = aml_to_exchange_diff_vals.get(line.sequence, {}) - exchange_analytic_distribution = exchange_diff_amounts.pop('analytic_distribution', False) - if exchange_diff_amounts: - related_exchange_diff_amls = line if exchange_diff_amounts['amount_residual'] * line.amount_residual > 0 else counterpart - exchange_diff_vals_list.append(related_exchange_diff_amls._prepare_exchange_difference_move_vals( - [exchange_diff_amounts], - exchange_date=max(line.date, counterpart.date), - exchange_analytic_distribution=exchange_analytic_distribution, - )) - lines_with_exch_diff += line - exchange_diff_moves = AccountMoveLine._create_exchange_difference_moves(exchange_diff_vals_list) - - # Perform the reconciliation. - self.env['account.move.line'].with_context(no_exchange_difference=True)._reconcile_plan( - [(line + counterpart).with_prefetch(all_line_ids) for line, counterpart in lines]) - - # Assign exchange move to partials. - for index, line in enumerate(lines_with_exch_diff): - exchange_move = exchange_diff_moves[index] - for debit_credit in ('debit', 'credit'): - partials = line[f'matched_{debit_credit}_ids'] \ - .filtered(lambda partial: partial[f'{debit_credit}_move_id'].move_id != exchange_move) - partials.exchange_move_id = exchange_move - - # Fill missing partner. - st_line_ctx = st_line.with_context(skip_account_move_synchronization=True, skip_readonly_check=True) - st_line_ctx.partner_id = partner_to_set - - # Create missing partner bank if necessary. - if st_line.account_number and st_line.partner_id: - st_line_ctx.partner_bank_id = st_line._find_or_create_bank_account() or st_line.partner_bank_id - - # Refresh analytic lines. - move.line_ids.analytic_line_ids.unlink() - move.line_ids.with_context(validate_analytic=True)._create_analytic_lines() - - @contextmanager - def _action_validate_method(self): - self.ensure_one() - st_line = self.st_line_id - - yield - - # The current record has been invalidated. Reload it completely. - self.st_line_id = st_line - self._ensure_loaded_lines() - self.return_todo_command = {'done': True} - - def _js_action_validate(self): - with self._action_validate_method(): - self._action_validate() - - def _action_to_check(self): - self.st_line_id.move_id.checked = False - self.invalidate_recordset(fnames=['st_line_checked']) - self._action_validate() - - def _js_action_to_check(self): - self.ensure_one() - - if self.state == 'valid': - # The validation can be performed. - with self._action_validate_method(): - self._action_to_check() - else: - # No need any validation. - self.st_line_id.move_id.checked = False - self.invalidate_recordset(fnames=['st_line_checked']) - self.return_todo_command = {'done': True} - - def _js_action_reset(self): - self.ensure_one() - st_line = self.st_line_id - - # Hashed entries shouldn't be modified; we will provide clear errors as well as redirect the user if needed. - if st_line.inalterable_hash: - if not st_line.has_reconciled_entries: - raise UserError(_("You can't hit the reset button on a secured bank transaction.")) - else: - raise RedirectWarning( - message=_("This bank transaction is locked up tighter than a squirrel in a nut factory! You can't hit the reset button on it. So, do you want to \"unreconcile\" it instead?"), - action=st_line.move_id.open_reconcile_view(), - button_text=_('View Reconciled Entries'), - ) - - st_line.action_undo_reconciliation() - - # The current record has been invalidated. Reload it completely. - self.st_line_id = st_line - self._ensure_loaded_lines() - self._action_trigger_matching_rules() - self.return_todo_command = {'done': True} - - def _js_action_set_as_checked(self): - self.ensure_one() - self.st_line_id.move_id.checked = True - self.invalidate_recordset(fnames=['st_line_checked']) - self.return_todo_command = {'done': True} - - def _action_clear_manual_operations_form(self): - self.form_index = None - - def _action_remove_lines(self, lines): - self.ensure_one() - if not lines: - return - - is_taxes_recomputation_needed = bool(lines.tax_ids) - has_new_aml = any(line.flag == 'new_aml' for line in lines) - - # Update 'line_ids'. - self.line_ids = [ - Command.unlink(line.id) - for line in lines - ] - self._remove_related_exchange_diff_lines(lines) - - # Recompute taxes and auto balance the lines. - if is_taxes_recomputation_needed: - self._lines_recompute_taxes() - if has_new_aml and not self._lines_check_apply_early_payment_discount(): - self._lines_check_apply_partial_matching() - self._lines_add_auto_balance_line() - self._action_clear_manual_operations_form() - - def _js_action_remove_line(self, line_index): - self.ensure_one() - line = self.line_ids.filtered(lambda x: x.index == line_index) - self._action_remove_lines(line) - - def _action_select_reconcile_model(self, reco_model): - self.ensure_one() - - # Cleanup a previously selected model. - self.line_ids = [ - Command.unlink(x.id) - for x in self.line_ids - if x.flag not in ('new_aml', 'liquidity') and x.reconcile_model_id and x.reconcile_model_id != reco_model - ] - self._lines_recompute_taxes() - - if reco_model.to_check: - self.st_line_id.move_id.checked = False - self.invalidate_recordset(fnames=['st_line_checked']) - - # Compute the residual balance on which apply the newly selected model. - auto_balance_line_vals = self._lines_prepare_auto_balance_line() - residual_balance = auto_balance_line_vals['amount_currency'] - - write_off_vals_list = reco_model._apply_lines_for_bank_widget(residual_balance, self.partner_id, self.st_line_id) - - if reco_model.rule_type == 'writeoff_button' and reco_model.counterpart_type in ('sale', 'purchase'): - invoice = self._create_invoice_from_write_off_values(reco_model, write_off_vals_list) - - action = { - 'type': 'ir.actions.act_window', - 'res_model': 'account.move', - 'context': {'create': False}, - 'view_mode': 'form', - 'res_id': invoice.id, - } - self.return_todo_command = clean_action(action, self.env) - else: - # Apply the newly generated lines. - self.line_ids = [ - Command.create(self._lines_prepare_reco_model_write_off_vals(reco_model, x)) - for x in write_off_vals_list - ] - - self._lines_recompute_taxes() - self._lines_add_auto_balance_line() - - def _js_action_select_reconcile_model(self, reco_model_id): - self.ensure_one() - reco_model = self.env['account.reconcile.model'].browse(reco_model_id) - self._action_select_reconcile_model(reco_model) - - def _create_invoice_from_write_off_values(self, reco_model, write_off_vals_list): - # Create a new invoice/bill and redirect the user to it. - journal = reco_model.line_ids.journal_id[:1] - - invoice_line_ids = [] - total_amount_currency = 0.0 - percentage_st_line = 0.0 - for write_off_values in write_off_vals_list: - write_off_values = dict(write_off_values) - total_amount_currency -= ( - write_off_values['amount_currency'] - if 'percentage_st_line' not in write_off_values - else 0 - ) - percentage_st_line += write_off_values.pop('percentage_st_line', 0) - write_off_values.pop('currency_id', None) - write_off_values.pop('partner_id', None) - write_off_values.pop('reconcile_model_id', None) - invoice_line_ids.append(write_off_values) - - st_line_amount = self.st_line_id.amount_currency if self.st_line_id.foreign_currency_id else self.st_line_id.amount - total_amount_currency += self.transaction_currency_id.round(st_line_amount * percentage_st_line) - - # Type of move depends on debit or credit of bank statement line and reconciliation model chosen. - if reco_model.counterpart_type == 'sale': - move_type = 'out_invoice' if total_amount_currency > 0 else 'out_refund' - else: - move_type = 'in_invoice' if total_amount_currency < 0 else 'in_refund' - - price_unit_sign = 1 if total_amount_currency < 0.0 else -1 - invoice_line_ids_commands = [] - for line_values in invoice_line_ids: - price_total = price_unit_sign * line_values.pop('amount_currency') - taxes = self.env['account.tax'].browse(line_values['tax_ids'][0][2]) - line_values['price_unit'] = self._get_invoice_price_unit_from_price_total(price_total, taxes) - invoice_line_ids_commands.append(Command.create(line_values)) - - invoice_values = { - 'invoice_date': self.st_line_id.date, - 'move_type': move_type, - 'partner_id': self.st_line_id.partner_id.id, - 'currency_id': self.transaction_currency_id.id, - 'payment_reference': self.st_line_id.payment_ref, - 'invoice_line_ids': invoice_line_ids_commands, - } - if journal: - invoice_values['journal_id'] = journal.id - - invoice = self.env['account.move'].create(invoice_values) - if not invoice.currency_id.is_zero(invoice.amount_total - total_amount_currency): - invoice._check_total_amount(abs(total_amount_currency)) - return invoice - - def _get_invoice_price_unit_from_price_total(self, price_total, taxes): - """ Determine price unit based on the total amount and taxes applied. """ - self.ensure_one() - taxes_computation = taxes._get_tax_details( - price_total, - 1.0, - precision_rounding=self.transaction_currency_id.rounding, - rounding_method=self.company_id.tax_calculation_rounding_method, - special_mode='total_included', - ) - return taxes_computation['total_excluded'] + sum(x['tax_amount'] for x in taxes_computation['taxes_data'] if x['tax'].price_include) - - def _action_add_new_amls(self, amls, reco_model=None, allow_partial=True): - self.ensure_one() - existing_amls = set(self.line_ids.filtered(lambda x: x.flag in ('new_aml', 'aml', 'liquidity')).source_aml_id) - amls = amls.filtered(lambda x: x not in existing_amls) - if not amls: - return - - self._lines_load_new_amls(amls, reco_model=reco_model) - added_lines = self.line_ids.filtered(lambda x: x.flag == 'new_aml' and x.source_aml_id in amls) - self._lines_recompute_exchange_diff(added_lines) - if not self._lines_check_apply_early_payment_discount() and allow_partial: - self._lines_check_apply_partial_matching() - self._lines_add_auto_balance_line() - self._action_clear_manual_operations_form() - - def _js_action_add_new_aml(self, aml_id): - self.ensure_one() - aml = self.env['account.move.line'].browse(aml_id) - self._action_add_new_amls(aml) - - def _action_remove_new_amls(self, amls): - self.ensure_one() - to_remove = self.line_ids.filtered(lambda x: x.flag == 'new_aml' and x.source_aml_id in amls) - self._action_remove_lines(to_remove) - - def _js_action_remove_new_aml(self, aml_id): - self.ensure_one() - aml = self.env['account.move.line'].browse(aml_id) - self._action_remove_new_amls(aml) - - def _js_action_mount_line_in_edit(self, line_index): - self.ensure_one() - self.form_index = line_index - - def _js_action_line_changed(self, form_index, field_name): - self.ensure_one() - line = self.line_ids.filtered(lambda x: x.index == form_index) - - # Invalidate the cache of newly set value to force the recomputation of computed fields. - value = line[field_name] - line.invalidate_recordset(fnames=[field_name], flush=False) - line[field_name] = value - - getattr(self, f'_line_value_changed_{field_name}')(line) - - def _js_action_line_set_partner_receivable_account(self, form_index): - self.ensure_one() - line = self.line_ids.filtered(lambda x: x.index == form_index) - line.account_id = line.partner_receivable_account_id - self._line_value_changed_account_id(line) - - def _js_action_line_set_partner_payable_account(self, form_index): - self.ensure_one() - line = self.line_ids.filtered(lambda x: x.index == form_index) - line.account_id = line.partner_payable_account_id - self._line_value_changed_account_id(line) - - def _js_action_redirect_to_move(self, form_index): - self.ensure_one() - line = self.line_ids.filtered(lambda x: x.index == form_index) - move = line.source_aml_move_id - - action = { - 'type': 'ir.actions.act_window', - 'context': {'create': False}, - 'view_mode': 'form', - } - - if move.origin_payment_id: - action.update({ - 'res_model': 'account.payment', - 'res_id': move.origin_payment_id.id, - }) - else: - action.update({ - 'res_model': 'account.move', - 'res_id': move.id, - }) - self.return_todo_command = clean_action(action, self.env) - - def _js_action_apply_line_suggestion(self, form_index): - self.ensure_one() - line = self.line_ids.filtered(lambda x: x.index == form_index) - - # Since 'balance'/'amount_currency' are both dependencies of 'suggestion_balance'/'suggestion_amount_currency', - # keep the value in variable before assigning anything to avoid an inconsistency after applying - # 'suggestion_amount_currency' but before updating 'balance'. - suggestion_amount_currency = line.suggestion_amount_currency - suggestion_balance = line.suggestion_balance - - line.amount_currency = suggestion_amount_currency - line.balance = suggestion_balance - - if line.currency_id == line.company_currency_id: - self._line_value_changed_balance(line) - else: - self._line_value_changed_amount_currency(line) - - @api.model - def collect_global_info_data(self, journal_id): - journal = self.env['account.journal'].browse(journal_id) - balance = '' - if journal.exists() and any(company in journal.company_id._accessible_branches() for company in self.env.companies): - balance = formatLang(self.env, - journal.current_statement_balance, - currency_obj=journal.currency_id or journal.company_id.sudo().currency_id) - return {'balance_amount': balance} diff --git a/addons/at_accounting/models/bank_rec_widget_line.py b/addons/at_accounting/models/bank_rec_widget_line.py deleted file mode 100644 index a88cdad..0000000 --- a/addons/at_accounting/models/bank_rec_widget_line.py +++ /dev/null @@ -1,502 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import _, api, fields, models, Command -from odoo.osv import expression -from odoo.tools.misc import formatLang, frozendict - -import markupsafe -import uuid - - -class BankRecWidgetLine(models.Model): - _name = "bank.rec.widget.line" - _inherit = "analytic.mixin" - _description = "Line of the bank reconciliation widget" - - # This model is never saved inside the database. - # _auto=False' & _table_query = "0" prevent the ORM to create the corresponding postgresql table. - _auto = False - _table_query = "0" - - wizard_id = fields.Many2one(comodel_name='bank.rec.widget') - index = fields.Char(compute='_compute_index') - flag = fields.Selection( - selection=[ - ('liquidity', 'liquidity'), - ('new_aml', 'new_aml'), - ('aml', 'aml'), - ('exchange_diff', 'exchange_diff'), - ('tax_line', 'tax_line'), - ('manual', 'manual'), - ('early_payment', 'early_payment'), - ('auto_balance', 'auto_balance'), - ], - ) - - journal_default_account_id = fields.Many2one( - related='wizard_id.st_line_id.journal_id.default_account_id', - depends=['wizard_id'], - ) - account_id = fields.Many2one( - comodel_name='account.account', - compute='_compute_account_id', - store=True, - readonly=False, - check_company=True, - domain="""[ - ('deprecated', '=', False), - ('id', '!=', journal_default_account_id), - ('account_type', 'not in', ('asset_cash', 'off_balance')), - ]""", - ) - date = fields.Date( - compute='_compute_date', - store=True, - readonly=False, - ) - name = fields.Char( - compute='_compute_name', - store=True, - readonly=False, - ) - partner_id = fields.Many2one( - comodel_name='res.partner', - compute='_compute_partner_id', - store=True, - readonly=False, - ) - currency_id = fields.Many2one( - comodel_name='res.currency', - compute='_compute_currency_id', - store=True, - readonly=False, - ) - company_id = fields.Many2one(related='wizard_id.company_id') - country_code = fields.Char(related='company_id.country_id.code', depends=['company_id']) - company_currency_id = fields.Many2one(related='wizard_id.company_currency_id') - amount_currency = fields.Monetary( - currency_field='currency_id', - compute='_compute_amount_currency', - store=True, - readonly=False, - ) - balance = fields.Monetary( - currency_field='company_currency_id', - compute='_compute_balance', - store=True, - readonly=False, - ) - transaction_currency_id = fields.Many2one( - related='wizard_id.st_line_id.foreign_currency_id', - depends=['wizard_id'], - ) - amount_transaction_currency = fields.Monetary( - currency_field='transaction_currency_id', - related='wizard_id.st_line_id.amount_currency', - depends=['wizard_id'], - ) - debit = fields.Monetary( - currency_field='company_currency_id', - compute='_compute_from_balance', - ) - credit = fields.Monetary( - currency_field='company_currency_id', - compute='_compute_from_balance', - ) - force_price_included_taxes = fields.Boolean() - tax_base_amount_currency = fields.Monetary( - currency_field='currency_id', - ) - - source_aml_id = fields.Many2one(comodel_name='account.move.line') - source_aml_move_id = fields.Many2one( - comodel_name='account.move', - compute='_compute_source_aml_fields', - store=True, - readonly=False, - ) - source_aml_move_name = fields.Char( - compute='_compute_source_aml_fields', - store=True, - readonly=False, - ) - tax_repartition_line_id = fields.Many2one( - comodel_name='account.tax.repartition.line', - compute='_compute_tax_repartition_line_id', - store=True, - readonly=False, - ) - tax_ids = fields.Many2many( - comodel_name='account.tax', - compute='_compute_tax_ids', - store=True, - readonly=False, - check_company=True, - ) - tax_tag_ids = fields.Many2many( - comodel_name='account.account.tag', - compute='_compute_tax_tag_ids', - store=True, - readonly=False, - ) - group_tax_id = fields.Many2one( - comodel_name='account.tax', - compute='_compute_group_tax_id', - store=True, - readonly=False, - ) - reconcile_model_id = fields.Many2one(comodel_name='account.reconcile.model') - source_amount_currency = fields.Monetary(currency_field='currency_id') - source_balance = fields.Monetary(currency_field='company_currency_id') - source_debit = fields.Monetary( - currency_field='company_currency_id', - compute='_compute_from_source_balance', - ) - source_credit = fields.Monetary( - currency_field='company_currency_id', - compute='_compute_from_source_balance', - ) - - display_stroked_amount_currency = fields.Boolean(compute='_compute_display_stroked_amount_currency') - display_stroked_balance = fields.Boolean(compute='_compute_display_stroked_balance') - - partner_currency_id = fields.Many2one( - comodel_name='res.currency', - compute='_compute_partner_info', - ) - partner_receivable_account_id = fields.Many2one( - comodel_name='account.account', - compute='_compute_partner_info', - ) - partner_payable_account_id = fields.Many2one( - comodel_name='account.account', - compute='_compute_partner_info', - ) - partner_receivable_amount = fields.Monetary( - currency_field='partner_currency_id', - compute='_compute_partner_info', - ) - partner_payable_amount = fields.Monetary( - currency_field='partner_currency_id', - compute='_compute_partner_info', - ) - - bank_account = fields.Char( - compute='_compute_bank_account', - ) - suggestion_html = fields.Html( - compute='_compute_suggestion', - sanitize=False, - ) - suggestion_amount_currency = fields.Monetary( - currency_field='currency_id', - compute='_compute_suggestion', - ) - suggestion_balance = fields.Monetary( - currency_field='company_currency_id', - compute='_compute_suggestion', - ) - ref = fields.Char( - compute='_compute_ref_narration', - store=True, - readonly=False, - ) - narration = fields.Html( - compute='_compute_ref_narration', - store=True, - readonly=False, - ) - - manually_modified = fields.Boolean() - - def _compute_index(self): - for line in self: - line.index = uuid.uuid4() - - @api.depends('source_aml_id') - def _compute_account_id(self): - for line in self: - if line.flag in ('aml', 'new_aml', 'liquidity', 'exchange_diff'): - line.account_id = line.source_aml_id.account_id - else: - line.account_id = line.account_id - - @api.depends('source_aml_id') - def _compute_date(self): - for line in self: - if line.flag in ('aml', 'new_aml', 'exchange_diff'): - line.date = line.source_aml_id.date - elif line.flag in ('liquidity', 'auto_balance', 'manual', 'early_payment', 'tax_line'): - line.date = line.wizard_id.st_line_id.date - else: - line.date = line.date - - @api.depends('source_aml_id') - def _compute_name(self): - for line in self: - if line.flag in ('aml', 'new_aml', 'liquidity'): - # In the case the source_aml_id is from a credit note, the aml might not have a name set - line.name = line.source_aml_id.name or line.source_aml_move_name - else: - line.name = line.name - - @api.depends('source_aml_id') - def _compute_partner_id(self): - for line in self: - if line.flag in ('aml', 'new_aml'): - line.partner_id = line.source_aml_id.partner_id - elif line.flag in ('liquidity', 'auto_balance', 'manual', 'early_payment', 'tax_line'): - line.partner_id = line.wizard_id.partner_id - else: - line.partner_id = line.partner_id - - @api.depends('source_aml_id') - def _compute_currency_id(self): - for line in self: - if line.flag in ('aml', 'new_aml', 'liquidity', 'exchange_diff'): - line.currency_id = line.source_aml_id.currency_id - elif line.flag in ('auto_balance', 'manual', 'early_payment'): - line.currency_id = line.wizard_id.transaction_currency_id - else: - line.currency_id = line.currency_id - - @api.depends('source_aml_id') - def _compute_balance(self): - for line in self: - if line.flag in ('aml', 'liquidity'): - line.balance = line.source_aml_id.balance - else: - line.balance = line.balance - - @api.depends('source_aml_id') - def _compute_amount_currency(self): - for line in self: - if line.flag in ('aml', 'liquidity'): - line.amount_currency = line.source_aml_id.amount_currency - else: - line.amount_currency = line.amount_currency - - @api.depends('balance') - def _compute_from_balance(self): - for line in self: - line.debit = line.balance if line.balance > 0.0 else 0.0 - line.credit = -line.balance if line.balance < 0.0 else 0.0 - - @api.depends('source_balance') - def _compute_from_source_balance(self): - for line in self: - line.source_debit = line.source_balance if line.source_balance > 0.0 else 0.0 - line.source_credit = -line.source_balance if line.source_balance < 0.0 else 0.0 - - @api.depends('source_aml_id', 'account_id', 'partner_id') - def _compute_analytic_distribution(self): - cache = {} - for line in self: - if line.flag in ('liquidity', 'aml'): - line.analytic_distribution = line.source_aml_id.analytic_distribution - elif line.flag in ('tax_line', 'early_payment'): - line.analytic_distribution = line.analytic_distribution - else: - arguments = frozendict({ - "partner_id": line.partner_id.id, - "partner_category_id": line.partner_id.category_id.ids, - "account_prefix": line.account_id.code, - "company_id": line.company_id.id, - }) - if arguments not in cache: - cache[arguments] = self.env['account.analytic.distribution.model']._get_distribution(arguments) - line.analytic_distribution = cache[arguments] or line.analytic_distribution - - @api.depends('source_aml_id') - def _compute_tax_repartition_line_id(self): - for line in self: - if line.flag == 'aml': - line.tax_repartition_line_id = line.source_aml_id.tax_repartition_line_id - else: - line.tax_repartition_line_id = line.tax_repartition_line_id - - @api.depends('source_aml_id') - def _compute_tax_ids(self): - for line in self: - if line.flag == 'aml': - line.tax_ids = [Command.set(line.source_aml_id.tax_ids.ids)] - else: - line.tax_ids = line.tax_ids - - @api.depends('source_aml_id') - def _compute_tax_tag_ids(self): - for line in self: - if line.flag == 'aml': - line.tax_tag_ids = [Command.set(line.source_aml_id.tax_tag_ids.ids)] - else: - line.tax_tag_ids = line.tax_tag_ids - - @api.depends('source_aml_id') - def _compute_group_tax_id(self): - for line in self: - if line.flag == 'aml': - line.group_tax_id = line.source_aml_id.group_tax_id - else: - line.group_tax_id = line.group_tax_id - - @api.depends('currency_id', 'amount_currency', 'source_amount_currency') - def _compute_display_stroked_amount_currency(self): - for line in self: - line.display_stroked_amount_currency = \ - line.flag == 'new_aml' \ - and line.currency_id.compare_amounts(line.amount_currency, line.source_amount_currency) != 0 - - @api.depends('currency_id', 'balance', 'source_balance') - def _compute_display_stroked_balance(self): - for line in self: - line.display_stroked_balance = \ - line.flag in ('new_aml', 'exchange_diff') \ - and line.currency_id.compare_amounts(line.balance, line.source_balance) != 0 - - @api.depends('flag') - def _compute_source_aml_fields(self): - for line in self: - line.source_aml_move_id = None - line.source_aml_move_name = None - if line.flag in ('new_aml', 'liquidity'): - line.source_aml_move_id = line.source_aml_id.move_id - line.source_aml_move_name = line.source_aml_id.move_id.name - elif line.flag == 'aml': - partials = line.source_aml_id.matched_debit_ids + line.source_aml_id.matched_credit_ids - all_counterpart_lines = partials.debit_move_id + partials.credit_move_id - counterpart_lines = all_counterpart_lines - line.source_aml_id - partials.exchange_move_id.line_ids - if len(counterpart_lines) == 1: - line.source_aml_move_id = counterpart_lines.move_id - line.source_aml_move_name = counterpart_lines.move_id.name - - @api.depends('wizard_id.form_index', 'partner_id') - def _compute_partner_info(self): - for line in self: - line.partner_receivable_amount = 0.0 - line.partner_payable_amount = 0.0 - line.partner_currency_id = None - line.partner_receivable_account_id = None - line.partner_payable_account_id = None - - if not line.partner_id or line.index != line.wizard_id.form_index: - continue - - line.partner_currency_id = line.company_currency_id - partner = line.partner_id.with_company(line.wizard_id.company_id) - common_domain = [('parent_state', '=', 'posted'), ('partner_id', '=', partner.id)] - line.partner_receivable_account_id = partner.property_account_receivable_id - if line.partner_receivable_account_id: - results = self.env['account.move.line']._read_group( - domain=expression.AND([common_domain, [('account_id', '=', line.partner_receivable_account_id.id)]]), - aggregates=['amount_residual:sum'], - ) - line.partner_receivable_amount = results[0][0] - line.partner_payable_account_id = partner.property_account_payable_id - if line.partner_payable_account_id: - results = self.env['account.move.line']._read_group( - domain=expression.AND([common_domain, [('account_id', '=', line.partner_payable_account_id.id)]]), - aggregates=['amount_residual:sum'], - ) - line.partner_payable_amount = results[0][0] - - @api.depends('flag') - def _compute_bank_account(self): - for line in self: - bank_account = line.wizard_id.st_line_id.partner_bank_id.display_name or line.wizard_id.st_line_id.account_number - if line.flag == 'liquidity' and bank_account: - line.bank_account = bank_account - else: - line.bank_account = None - - @api.depends('wizard_id.form_index', 'amount_currency', 'balance') - def _compute_suggestion(self): - for line in self: - line.suggestion_html = None - line.suggestion_amount_currency = None - line.suggestion_balance = None - - if line.flag != 'new_aml' or line.index != line.wizard_id.form_index: - continue - - aml = line.source_aml_id - wizard = line.wizard_id - residual_amount_before_reco = abs(aml.amount_residual_currency) - residual_amount_after_reco = abs(aml.amount_residual_currency + line.amount_currency) - reconciled_amount = residual_amount_before_reco - residual_amount_after_reco - is_fully_reconciled = aml.currency_id.is_zero(residual_amount_after_reco) - is_invoice = aml.move_id.is_invoice(include_receipts=True) - - if is_fully_reconciled: - lines = [ - _("The invoice %(display_name_html)s with an open amount of %(open_amount)s will be entirely paid by the transaction.") - if is_invoice else - _("%(display_name_html)s with an open amount of %(open_amount)s will be fully reconciled by the transaction.") - ] - partial_amounts = wizard._lines_check_partial_amount(line) - if partial_amounts: - lines.append( - _("You might want to record a %(btn_start)spartial payment%(btn_end)s.") - if is_invoice else - _("You might want to make a %(btn_start)spartial reconciliation%(btn_end)s instead.") - ) - line.suggestion_amount_currency = partial_amounts['amount_currency'] - line.suggestion_balance = partial_amounts['balance'] - else: - if is_invoice: - lines = [ - _("The invoice %(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s."), - _("You might want to set the invoice as %(btn_start)sfully paid%(btn_end)s."), - ] - else: - lines = [ - _("%(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s."), - _("You might want to %(btn_start)sfully reconcile%(btn_end)s the document."), - ] - line.suggestion_amount_currency = line.source_amount_currency - line.suggestion_balance = line.source_balance - - display_name_html = markupsafe.Markup(""" - - """) % { - 'display_name': aml.move_id.display_name, - } - - extra_text = markupsafe.Markup('
    ').join(lines) % { - 'amount': formatLang(self.env, reconciled_amount, currency_obj=aml.currency_id), - 'open_amount': formatLang(self.env, residual_amount_before_reco, currency_obj=aml.currency_id), - 'display_name_html': display_name_html, - 'btn_start': markupsafe.Markup( - ''), - } - line.suggestion_html = markupsafe.Markup("""
    %s
    """) % extra_text - - @api.depends('flag') - def _compute_ref_narration(self): - for line in self: - if line.flag == 'liquidity': - line.ref = line.wizard_id.st_line_id.ref - line.narration = line.wizard_id.st_line_id.narration - else: - line.ref = line.narration = None - - def _get_aml_values(self, **kwargs): - self.ensure_one() - create_dict = { - 'name': self.name, - 'account_id': self.account_id.id, - 'currency_id': self.currency_id.id, - 'amount_currency': self.amount_currency, - 'balance': self.debit - self.credit, - 'reconcile_model_id': self.reconcile_model_id.id, - 'analytic_distribution': self.analytic_distribution, - 'tax_repartition_line_id': self.tax_repartition_line_id.id, - 'tax_ids': [Command.set(self.tax_ids.ids)], - 'tax_tag_ids': [Command.set(self.tax_tag_ids.ids)], - 'group_tax_id': self.group_tax_id.id, - **kwargs, - } - if self.flag == 'early_payment': - create_dict['display_type'] = 'epd' - return create_dict diff --git a/addons/at_accounting/models/bank_reconciliation_report.py b/addons/at_accounting/models/bank_reconciliation_report.py deleted file mode 100644 index 74ee03b..0000000 --- a/addons/at_accounting/models/bank_reconciliation_report.py +++ /dev/null @@ -1,589 +0,0 @@ -from datetime import date -import logging -from odoo import models, fields, _ -from odoo.exceptions import UserError -from odoo.tools import SQL - -_logger = logging.getLogger(__name__) - - -class BankReconciliationReportCustomHandler(models.AbstractModel): - _name = 'account.bank.reconciliation.report.handler' - _inherit = 'account.report.custom.handler' - _description = 'Bank Reconciliation Report Custom Handler' - - ###################### - # Options - ###################### - def _custom_options_initializer(self, report, options, previous_options): - super()._custom_options_initializer(report, options, previous_options=previous_options) - - # Options is needed otherwise some elements added in the post processor go on the total line - options['ignore_totals_below_sections'] = True - if 'active_id' in self._context and self._context.get('active_model') == 'account.journal': - options['bank_reconciliation_report_journal_id'] = self._context['active_id'] - elif 'bank_reconciliation_report_journal_id' in previous_options: - options['bank_reconciliation_report_journal_id'] = previous_options['bank_reconciliation_report_journal_id'] - else: - # This should never happen except in some test cases - options['bank_reconciliation_report_journal_id'] = self.env['account.journal'].search([('type', '=', 'bank')], limit=1).id - - # Remove multi-currency columns if needed - is_multi_currency = self.env.user.has_group('base.group_multi_currency') and self.env.user.has_group('base.group_no_one') - if not is_multi_currency: - options['columns'] = [ - column for column in options['columns'] - if column['expression_label'] not in ('amount_currency', 'currency') - ] - - ###################### - # Getter - ###################### - def _get_bank_journal_and_currencies(self, options): - journal = self.env['account.journal'].browse(options.get('bank_reconciliation_report_journal_id')) - company_currency = journal.company_id.currency_id - journal_currency = journal.currency_id or company_currency - return journal, journal_currency, company_currency - - ###################### - # Return function - ###################### - def _build_custom_engine_result(self, date=None, label=None, amount_currency=None, amount_currency_currency_id=None, currency=None, amount=0, amount_currency_id=None, has_sublines=False): - return { - 'date': date, - 'label': label, - 'amount_currency': amount_currency, - 'amount_currency_currency_id': amount_currency_currency_id, - 'currency': currency, - 'amount': amount, - 'amount_currency_id': amount_currency_id, - 'has_sublines': has_sublines, - } - - ###################### - # Engine - ###################### - def _report_custom_engine_forced_currency_amount(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - _journal, journal_currency, _company_currency = self._get_bank_journal_and_currencies(options) - return self._build_custom_engine_result(amount_currency_id=journal_currency.id) - - def _report_custom_engine_unreconciled_last_statement_receipts(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_common(options, 'receipts', current_groupby, True) - - def _report_custom_engine_unreconciled_last_statement_payments(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_common(options, 'payments', current_groupby, True) - - def _report_custom_engine_unreconciled_receipts(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_common(options, 'receipts', current_groupby, False) - - def _report_custom_engine_unreconciled_payments(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_common(options, 'payments', current_groupby, False) - - def _report_custom_engine_outstanding_receipts(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_outstanding_common(options, 'receipts', current_groupby) - - def _report_custom_engine_outstanding_payments(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_outstanding_common(options, 'payments', current_groupby) - - def _report_custom_engine_misc_operations(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - report = self.env['account.report'].browse(options['report_id']) - report._check_groupby_fields([current_groupby] if current_groupby else []) - - journal, journal_currency, _company_currency = self._get_bank_journal_and_currencies(options) - - bank_miscellaneous_domain = self._get_bank_miscellaneous_move_lines_domain(options, journal) - - misc_operations_amount = self.env["account.move.line"]._read_group( - domain=bank_miscellaneous_domain or [], - groupby=current_groupby or [], - aggregates=['balance:sum'] - )[-1][0] # Needed to get the balance from the tuples given by the read group - return self._build_custom_engine_result(amount=misc_operations_amount or 0, amount_currency_id=journal_currency.id) - - def _report_custom_engine_last_statement_balance_amount(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - if current_groupby: - raise UserError(_("Custom engine _report_custom_engine_last_statement_balance_amount does not support groupby")) - - journal, journal_currency, _company_currency = self._get_bank_journal_and_currencies(options) - last_statement = self._get_last_bank_statement(journal, options) - - return self._build_custom_engine_result(amount=last_statement.balance_end_real, amount_currency_id=journal_currency.id) - - def _report_custom_engine_transaction_without_statement_amount(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - return self._bank_reconciliation_report_custom_engine_common(options, 'all', current_groupby, False, unreconciled=False) - - def _bank_reconciliation_report_custom_engine_common(self, options, internal_type, current_groupby, from_last_statement, unreconciled=True): - """ - Retrieve entries for bank reconciliation based on specified parameters. - Parameters: - - options (dict): A dictionary containing options of the report. - - internal_type (str): The internal type used for classification (e.g., receipt, payment). For the receipt - we will query the entries with a positive amounts and for the payment - the negative amounts. - If the internal type is another thing that receipt or payment it will get all the - entries position or negative - - current_groupby (str): The current grouping criteria. - - last_statement (bool, optional): If True, query entries from the last bank statement. - Otherwise, query entries that are not part of the last bank - statement. - - unreconciled (bool, optional): If True, query the unreconciled entries only - - """ - journal, journal_currency, _company_currency = self._get_bank_journal_and_currencies(options) - if not journal: - return self._build_custom_engine_result() - - report = self.env['account.report'].browse(options['report_id']) - report._check_groupby_fields([current_groupby] if current_groupby else []) - - def build_result_dict(query_res_lines): - # The query should find exactly one account move line per bank statement line - if current_groupby == 'id': - res = query_res_lines[0] - foreign_currency = self.env['res.currency'].browse(res['foreign_currency_id']) - rate = 1 # journal_currency / foreign_currency - if foreign_currency: - rate = (res['amount'] / res['amount_currency']) if res['amount_currency'] else 0 - - return self._build_custom_engine_result( - date=res['date'] if res['date'] else None, - label=res['payment_ref'] or res['ref'] or '/', - amount_currency=-res['amount_residual'] if res['foreign_currency_id'] else None, - amount_currency_currency_id=foreign_currency.id if res['foreign_currency_id'] else None, - currency=foreign_currency.display_name if res['foreign_currency_id'] else None, - amount=-res['amount_residual'] * rate if res['amount_residual'] else None, - amount_currency_id=journal_currency.id, - ) - else: - amount = 0 - for res in query_res_lines: - rate = 1 # journal_currency / foreign_currency - if res['foreign_currency_id']: - rate = (res['amount'] / res['amount_currency']) if res['amount_currency'] else 0 - amount += -res.get('amount_residual', 0) * rate if unreconciled else res.get('amount', 0) - - return self._build_custom_engine_result( - amount=amount, - amount_currency_id=journal_currency.id, - has_sublines=bool(len(query_res_lines)), - ) - - query = report._get_report_query(options, 'strict_range', domain=[ - ('journal_id', '=', journal.id), - ('account_id', '=', journal.default_account_id.id), # There should be only 1 line per move with that account - ]) - - if from_last_statement: - last_statement_id = self._get_last_bank_statement(journal, options).id - if last_statement_id: - last_statement_id_condition = SQL("st_line.statement_id = %s", last_statement_id) - else: - # If there is no last statement, the last statement section must be empty and the other must have all - # transaction - return self._compute_result([], current_groupby, build_result_dict) - else: - last_statement_id_condition = SQL("st_line.statement_id IS NULL") - - if internal_type == 'receipts': - st_line_amount_condition = SQL("AND st_line.amount > 0") - elif internal_type == 'payments': - st_line_amount_condition = SQL("AND st_line.amount < 0") - else: - # For the Transaction without statement, the internal type is 'all' - st_line_amount_condition = SQL("") - - # Build query - query = SQL( - """ - SELECT %(select_from_groupby)s, - st_line.id, - move.name, - move.ref, - move.date, - st_line.payment_ref, - st_line.amount, - st_line.amount_residual, - st_line.amount_currency, - st_line.foreign_currency_id - FROM %(table_references)s - JOIN account_bank_statement_line st_line ON st_line.move_id = account_move_line.move_id - JOIN account_move move ON move.id = st_line.move_id - WHERE %(search_condition)s - %(is_unreconciled)s - %(st_line_amount_condition)s - AND %(last_statement_id_condition)s - GROUP BY %(group_by)s, - st_line.id, - move.id - """, - select_from_groupby=SQL("%s AS grouping_key", SQL.identifier('account_move_line', current_groupby)) if current_groupby else SQL('null'), - table_references=query.from_clause, - search_condition=query.where_clause, - is_receipt=SQL("st_line.amount > 0") if internal_type == "receipts" else SQL("st_line.amount < 0"), - is_unreconciled=SQL("AND NOT st_line.is_reconciled") if unreconciled else SQL(""), - st_line_amount_condition=st_line_amount_condition, - last_statement_id_condition=last_statement_id_condition, - group_by=SQL.identifier('account_move_line', current_groupby) if current_groupby else SQL('st_line.id'), # Same key in the groupby because we can't put a null key in a group by - ) - - self._cr.execute(query) - query_res_lines = self._cr.dictfetchall() - - return self._compute_result(query_res_lines, current_groupby, build_result_dict) - - def _bank_reconciliation_report_custom_engine_outstanding_common(self, options, internal_type, current_groupby): - """ - This engine retrieves the data of all recorded payments/receipts that have not been matched with a bank - statement yet - """ - journal, journal_currency, company_currency = self._get_bank_journal_and_currencies(options) - if not journal: - return self._build_custom_engine_result() - - report = self.env['account.report'].browse(options['report_id']) - report._check_groupby_fields([current_groupby] if current_groupby else []) - - def build_result_dict(query_res_lines): - if current_groupby == 'id': - res = query_res_lines[0] - convert = not (journal_currency and res['currency_id'] == journal_currency.id) - amount_currency = res['amount_residual_currency'] if res['is_account_reconcile'] else res['amount_currency'] - balance = res['amount_residual'] if res['is_account_reconcile'] else res['balance'] - foreign_currency = self.env['res.currency'].browse(res['currency_id']) - - return self._build_custom_engine_result( - date=res['date'] if res['date'] else None, - label=res['ref'] if res['ref'] else None, - amount_currency=amount_currency if convert else None, - amount_currency_currency_id=foreign_currency.id if convert else None, - currency=foreign_currency.display_name if convert else None, - amount=company_currency._convert(balance, journal_currency, journal.company_id, options['date']['date_to']) if convert else amount_currency, - amount_currency_id=journal_currency.id, - ) - else: - amount = 0 - for res in query_res_lines: - convert = not (journal_currency and res['currency_id'] == journal_currency.id) - if convert: - balance = res['amount_residual'] if res['is_account_reconcile'] else res['balance'] - amount += company_currency._convert(balance, journal_currency, journal.company_id, options['date']['date_to']) - else: - amount += res['amount_residual_currency'] if res['is_account_reconcile'] else res['amount_currency'] - - return self._build_custom_engine_result( - amount=amount, - amount_currency_id=journal_currency.id, - has_sublines=bool(len(query_res_lines)), - ) - - accounts = journal._get_journal_inbound_outstanding_payment_accounts() + journal._get_journal_outbound_outstanding_payment_accounts() - - query = report._get_report_query(options, 'from_beginning', domain=[ - ('journal_id', '=', journal.id), - ('account_id', 'in', accounts.ids), - ('full_reconcile_id', '=', False), - ('amount_residual_currency', '!=', 0.0) - ]) - - # Build query - query = SQL( - """ - SELECT %(select_from_groupby)s, - account_move_line.account_id, - account_move_line.payment_id, - account_move_line.move_id, - account_move_line.currency_id, - account_move_line.move_name AS name, - account_move_line.ref, - account_move_line.date, - account.reconcile AS is_account_reconcile, - SUM(account_move_line.amount_residual) AS amount_residual, - SUM(account_move_line.balance) AS balance, - SUM(account_move_line.amount_residual_currency) AS amount_residual_currency, - SUM(account_move_line.amount_currency) AS amount_currency - FROM %(table_references)s - JOIN account_account account ON account.id = account_move_line.account_id - WHERE %(search_condition)s - AND %(is_receipt)s - GROUP BY %(group_by)s, - account_move_line.account_id, - account_move_line.payment_id, - account_move_line.move_id, - account_move_line.currency_id, - account_move_line.move_name, - account_move_line.ref, - account_move_line.date, - account.reconcile - """, - select_from_groupby=SQL("%s AS grouping_key", SQL.identifier('account_move_line', current_groupby)) if current_groupby else SQL('null'), - table_references=query.from_clause, - search_condition=query.where_clause, - is_receipt=SQL("account_move_line.balance > 0") if internal_type == "receipts" else SQL("account_move_line.balance < 0"), - group_by=SQL.identifier('account_move_line', current_groupby) if current_groupby else SQL('account_move_line.account_id'), # Same key in the groupby because we can't put a null key in a group by - ) - self._cr.execute(query) - query_res_lines = self._cr.dictfetchall() - - return self._compute_result(query_res_lines, current_groupby, build_result_dict) - - def _compute_result(self, query_res_lines, current_groupby, build_result_dict): - if not current_groupby: - return build_result_dict(query_res_lines) - else: - rslt = [] - - all_res_per_grouping_key = {} - for query_res in query_res_lines: - grouping_key = query_res['grouping_key'] - all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res) - - for grouping_key, query_res_lines in all_res_per_grouping_key.items(): - rslt.append((grouping_key, build_result_dict(query_res_lines))) - - return rslt - - def _custom_line_postprocessor(self, report, options, lines): - lines = super()._custom_line_postprocessor(report, options, lines) - journal, _journal_currency, _company_currency = self._get_bank_journal_and_currencies(options) - if not journal: - return lines - - last_statement = self._get_last_bank_statement(journal, options) - - for line in lines: - line_id = report._get_res_id_from_line_id(line['id'], 'account.report.line') - code = self.env['account.report.line'].browse(line_id).code - - if code == "balance_bank": - line['name'] = _("Balance of '%s'", journal.default_account_id.display_name) - - if code == "last_statement_balance": - line['class'] = 'o_bold_tr' - if last_statement: - line['columns'][1].update({ - 'name': last_statement.display_name, - 'auditable': True, - }) - - if code == "transaction_without_statement": - line['class'] = 'o_bold_tr' - - if code == "misc_operations": - line['class'] = 'o_bold_tr' - - # Check if it's a leaf node - model, _model_id = report._get_model_info_from_id(line['id']) - if model == "account.move.line": - line_name = line['name'].split() - line['name'] = line_name[0] # This will give just the name without the ref or label - - return lines - - def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings): - journal, journal_currency, _company_currency = self._get_bank_journal_and_currencies(options) - inconsistent_statement = self._get_inconsistent_statements(options, journal).ids - bank_miscellaneous_domain = self._get_bank_miscellaneous_move_lines_domain(options, journal) - has_bank_miscellaneous_move_lines = bank_miscellaneous_domain and bool(self.env['account.move.line'].search_count(bank_miscellaneous_domain, limit=1)) - last_statement, balance_gl, balance_end, unexplained_difference, general_ledger_not_matching = self._compute_journal_balances(report, options, journal, journal_currency) - - if warnings is not None: - if last_statement and general_ledger_not_matching: - warnings['at_accounting.journal_balance'] = { - 'alert_type': 'warning', - 'general_ledger_amount': balance_gl, - 'last_bank_statement_amount': balance_end, - 'unexplained_difference': unexplained_difference, - } - if inconsistent_statement: - warnings['at_accounting.inconsistent_statement_warning'] = {'alert_type': 'warning', 'args': inconsistent_statement} - if has_bank_miscellaneous_move_lines: - warnings['at_accounting.has_bank_miscellaneous_move_lines'] = {'alert_type': 'warning', 'args': journal.default_account_id.display_name} - - def _compute_journal_balances(self, report, options, journal, journal_currency): - """ - This function compute all necessary information for the warning 'at_accounting.journal_balance' - :param report: The bank reconciliation report. - :param options: The report options. - :param journal: The journal used. - """ - # Get domain and balances - domain = report._get_options_domain(options, 'from_beginning') - balance_gl = journal._get_journal_bank_account_balance(domain=domain)[0] - last_statement, balance_end, difference, general_ledger_not_matching = self._compute_balances(options, journal, balance_gl, journal_currency) - - # Format values - balance_gl = report.format_value(options, balance_gl, format_params={'currency_id': journal_currency.id}, figure_type='monetary') - balance_end = report.format_value(options, balance_end, format_params={'currency_id': journal_currency.id}, figure_type='monetary') - difference = report.format_value(options, difference, format_params={'currency_id': journal_currency.id}, figure_type='monetary') - - return last_statement, balance_gl, balance_end, difference, general_ledger_not_matching - - def _compute_balances(self, options, journal, balance_gl, report_currency): - """ - This function will compute the balance of the last statement and the unexplained difference. - :param options: The report options. - :param journal: The journal used. - :param balance_gl: The balance of the general ledger. - :param report_currency: The currency of the report. - """ - report_date = fields.Date.from_string(options['date']['date_to']) - last_statement = self._get_last_bank_statement(journal, options) - balance_end = 0 - difference = 0 - general_ledger_not_matching = False - - if last_statement: - lines_before_date_to = last_statement.line_ids.filtered(lambda line: line.date <= report_date) - balance_end = last_statement.balance_start + sum(lines_before_date_to.mapped('amount')) - difference = balance_gl - balance_end - general_ledger_not_matching = not report_currency.is_zero(difference) - - return last_statement, balance_end, difference, general_ledger_not_matching - - def _get_last_bank_statement(self, journal, options): - """ - Retrieve the last bank statement created using this journal. - :param journal: The journal used. - :param domain: An additional domain to be applied on the account.bank.statement model. - :return: An account.bank.statement record or an empty recordset. - """ - report_date = fields.Date.from_string(options['date']['date_to']) - last_statement_domain = [('journal_id', '=', journal.id), ('statement_id', '!=', False), ('date', '<=', report_date)] - last_st_line = self.env['account.bank.statement.line'].search(last_statement_domain, order='date desc, id desc', limit=1) - return last_st_line.statement_id - - def _get_inconsistent_statements(self, options, journal): - """ - Retrieve the account.bank.statements records on the range of the options date having different starting - balance regarding its previous statement. - :param options: The report options. - :param journal: The account.journal from which this report has been opened. - :return: An account.bank.statements recordset. - """ - return self.env['account.bank.statement'].search([ - ('journal_id', '=', journal.id), - ('date', '<=', options['date']['date_to']), - ('is_valid', '=', False), - ]) - - def _get_bank_miscellaneous_move_lines_domain(self, options, journal): - """ - Get the domain to be used to retrieve the journal items affecting the bank accounts but not linked to - a statement line. (Limited in a year) - :param options: The report options. - :param journal: The account.journal from which this report has been opened. - :return: A domain to search on the account.move.line model. - - """ - if not journal.default_account_id: - return None - - report = self.env['account.report'].browse(options['report_id']) - domain = [ - ('account_id', '=', journal.default_account_id.id), - ('statement_line_id', '=', False), - *report._get_options_domain(options, 'from_beginning'), - ] - - fiscal_lock_date = journal.company_id._get_user_fiscal_lock_date(journal) - if fiscal_lock_date != date.min: - domain.append(('date', '>', fiscal_lock_date)) - - if journal.company_id.account_opening_move_id: - domain.append(('move_id', '!=', journal.company_id.account_opening_move_id.id)) - - return domain - - ################ - # Audit - ################ - def action_audit_cell(self, options, params): - report_line = self.env['account.report.line'].browse(params['report_line_id']) - if report_line.code == "balance_bank": - return self.action_redirect_to_general_ledger(options) - elif report_line.code == "misc_operations": - return self.open_bank_miscellaneous_move_lines(options) - elif report_line.code == "last_statement_balance": - return self.action_redirect_to_bank_statement_widget(options) - else: - return report_line.report_id.action_audit_cell(options, params) - - ################ - # ACTIONS - ################ - def action_redirect_to_general_ledger(self, options): - """ - Action to redirect to the general ledger - :param options: The report options. - :return: Actions to the report - """ - general_ledger_action = self.env['ir.actions.actions']._for_xml_id('at_accounting.action_account_report_general_ledger') - general_ledger_action['params'] = { - 'options': options, - 'ignore_session': True, - } - - return general_ledger_action - - def action_redirect_to_bank_statement_widget(self, options): - """ - Redirect the user to the requested bank statement, if empty displays all bank transactions of the journal. - :param options: The report options. - :param params: The action params containing at least 'statement_id', can be false. - :return: A dictionary representing an ir.actions.act_window. - """ - journal = self.env['account.journal'].browse(options.get('bank_reconciliation_report_journal_id')) - last_statement = self._get_last_bank_statement(journal, options) - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - default_context={'create': False, 'search_default_statement_id': last_statement.id}, - name=last_statement.display_name, - ) - - def open_bank_miscellaneous_move_lines(self, options): - """ - An action opening the account.move.line list view affecting the bank account balance but not linked to - a bank statement line. - :param options: The report options. - :param params: -Not used-. - :return: An action redirecting to the list view of journal items. - """ - journal = self.env['account.journal'].browse(options['bank_reconciliation_report_journal_id']) - - return { - 'name': _('Journal Items'), - 'type': 'ir.actions.act_window', - 'res_model': 'account.move.line', - 'view_type': 'list', - 'view_mode': 'list', - 'target': 'current', - 'views': [(self.env.ref('account.view_move_line_tree').id, 'list')], - 'domain': self.env['account.bank.reconciliation.report.handler']._get_bank_miscellaneous_move_lines_domain(options, journal), - } - - def bank_reconciliation_report_open_inconsistent_statements(self, options, params=None): - """ - An action opening the account.bank.statement view (form or list) depending the 'inconsistent_statement_ids' - key set on the options. - :param options: The report options. - :param params: -Not used-. - :return: An action redirecting to a view of statements. - """ - inconsistent_statement_ids = params['args'] - action = { - 'name': _("Inconsistent Statements"), - 'type': 'ir.actions.act_window', - 'res_model': 'account.bank.statement', - } - if len(inconsistent_statement_ids) == 1: - action.update({ - 'view_mode': 'form', - 'res_id': inconsistent_statement_ids[0], - 'views': [(False, 'form')], - }) - else: - action.update({ - 'view_mode': 'list', - 'domain': [('id', 'in', inconsistent_statement_ids)], - 'views': [(False, 'list')], - }) - return action diff --git a/addons/at_accounting/models/budget.py b/addons/at_accounting/models/budget.py deleted file mode 100644 index ef4afcb..0000000 --- a/addons/at_accounting/models/budget.py +++ /dev/null @@ -1,111 +0,0 @@ -from itertools import zip_longest - -from odoo import api, Command, fields, models, _ -from odoo.exceptions import ValidationError -from odoo.tools import date_utils, float_is_zero, float_round - - -class AccountReportBudget(models.Model): - _name = 'account.report.budget' - _description = "Accounting Report Budget" - _order = 'sequence, id' - - sequence = fields.Integer(string="Sequence") - name = fields.Char(string="Name", required=True) - item_ids = fields.One2many(string="Items", comodel_name='account.report.budget.item', inverse_name='budget_id') - company_id = fields.Many2one(string="Company", comodel_name='res.company', required=True, default=lambda x: x.env.company) - - @api.constrains('name') - def _contrains_name(self): - for budget in self: - if not budget.name: - raise ValidationError(_("Please enter a valid budget name.")) - - @api.model_create_multi - def create(self, create_values): - for values in create_values: - if name := values.get('name'): - values['name'] = name.strip() - return super().create(create_values) - - def _create_or_update_budget_items(self, value_to_set, account_id, rounding, date_from, date_to): - """ This method will create / update several budget items following the number - of months between date_from(include) and date_to(include). - - :param value_to_set: The value written by the user in the report cell. - :param account_id: The related account id. - :param rounding: The rounding for the decimal precision. - :param date_from: The start date for the budget item creation. - :param date_to: The end date for the budget item creation. - """ - self.ensure_one() - - date_from, date_to = fields.Date.to_date(date_from), fields.Date.to_date(date_to) - existing_budget_items = self.env['account.report.budget.item'].search_fetch([ - ('budget_id', '=', self.id), - ('account_id', '=', account_id), - ('date', '<=', date_to), - ('date', '>=', date_from), - ], ['id', 'amount']) - total_amount = sum(existing_budget_items.mapped('amount')) - - value_to_compute = value_to_set - total_amount - if float_is_zero(value_to_compute, precision_digits=rounding): - # In case the computed amount equals 0, we do an early return as - # it's not necessary to create new budget item - return - - start_month_dates = [ - date_utils.start_of(date, 'month') - for date in date_utils.date_range(date_from, date_to) - ] - - # Fill a list with the same amounts for each month - amounts = [float_round(value_to_compute / len(start_month_dates), precision_digits=rounding, rounding_method='DOWN')] * len(start_month_dates) - # Add the remainder in the last amount - amounts[-1] += float_round(value_to_compute - sum(amounts), precision_digits=rounding) - - budget_items_commands = [] - for existing_budget_item, start_month_date, amount in zip_longest(existing_budget_items, start_month_dates, amounts): - if existing_budget_item: - budget_items_commands.append(Command.update(existing_budget_item.id, { - 'amount': existing_budget_item.amount + amount, - })) - else: - budget_items_commands.append(Command.create({ - 'account_id': account_id, - 'amount': amount, - 'date': start_month_date, - })) - - if budget_items_commands: - self.item_ids = budget_items_commands - # Make sure that the model is flushed before continuing the code and fetching these new items - self.env['account.report.budget.item'].flush_model() - - def copy_data(self, default=None): - vals_list = super().copy_data(default=default) - return [dict(vals, name=self.env._("%s (copy)", budget.name)) for budget, vals in zip(self, vals_list)] - - def copy(self, default=None): - new_budgets = super().copy(default) - for old_budget, new_budget in zip(self, new_budgets): - for item in old_budget.item_ids: - item.copy({ - 'budget_id': new_budget.id, - 'account_id': item.account_id.id, - 'amount': item.amount, - 'date': item.date, - }) - - return new_budgets - - -class AccountReportBudgetItem(models.Model): - _name = 'account.report.budget.item' - _description = "Accounting Report Budget Item" - - budget_id = fields.Many2one(string="Budget", comodel_name='account.report.budget', required=True, ondelete='cascade') - account_id = fields.Many2one(string="Account", comodel_name='account.account', required=True) - amount = fields.Float(string="Amount", default=0) - date = fields.Date(required=True) diff --git a/addons/at_accounting/models/chart_template.py b/addons/at_accounting/models/chart_template.py deleted file mode 100644 index 0a9728b..0000000 --- a/addons/at_accounting/models/chart_template.py +++ /dev/null @@ -1,39 +0,0 @@ -# coding: utf-8 -from odoo import fields, models, _ -from odoo.exceptions import ValidationError - - -class AccountChartTemplate(models.AbstractModel): - _inherit = 'account.chart.template' - - def _post_load_data(self, template_code, company, template_data): - super()._post_load_data(template_code, company, template_data) - - company = company or self.env.company - default_misc_journal = self.env['account.journal'].search([ - *self.env['account.journal']._check_company_domain(company), - ('type', '=', 'general') - ], limit=1) - if not default_misc_journal: - raise ValidationError(_("No default miscellaneous journal could be found for the active company")) - - company.update({ - 'totals_below_sections': company.anglo_saxon_accounting, - 'account_tax_periodicity_journal_id': default_misc_journal, - 'account_tax_periodicity_reminder_day': 7, - }) - default_misc_journal.show_on_dashboard = True - - generic_tax_report = self.env.ref('account.generic_tax_report') - tax_report = self.env['account.report'].search([ - ('availability_condition', '=', 'country'), - ('country_id', '=', company.country_id.id), - ('root_report_id', '=', generic_tax_report.id), - ], limit=1) - if not tax_report: - tax_report = generic_tax_report - - _dummy, period_end = company._get_tax_closing_period_boundaries(fields.Date.today(), tax_report) - activity = company._get_tax_closing_reminder_activity(tax_report.id, period_end) - if not activity: - company._generate_tax_closing_reminder_activity(tax_report, period_end) diff --git a/addons/at_accounting/models/digest.py b/addons/at_accounting/models/digest.py deleted file mode 100644 index 01a0079..0000000 --- a/addons/at_accounting/models/digest.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models, _ -from odoo.exceptions import AccessError - - -class Digest(models.Model): - _inherit = 'digest.digest' - - kpi_account_bank_cash = fields.Boolean('Bank & Cash Moves') - kpi_account_bank_cash_value = fields.Monetary(compute='_compute_kpi_account_total_bank_cash_value') - - def _compute_kpi_account_total_bank_cash_value(self): - if not self.env.user.has_group('account.group_account_user'): - raise AccessError(_("Do not have access, skip this data for user's digest email")) - - start, end, companies = self._get_kpi_compute_parameters() - data = self.env['account.move']._read_group([ - ('date', '>=', start), - ('date', '<', end), - ('journal_id.type', 'in', ('cash', 'bank')), - ('company_id', 'in', companies.ids), - ], ['company_id'], ['amount_total:sum']) - data = dict(data) - - for record in self: - company = record.company_id or self.env.company - record.kpi_account_bank_cash_value = data.get(company) - - def _compute_kpis_actions(self, company, user): - res = super(Digest, self)._compute_kpis_actions(company, user) - res.update({'kpi_account_bank_cash': 'account.open_account_journal_dashboard_kanban&menu_id=%s' % (self.env.ref('account.menu_finance').id)}) - return res diff --git a/addons/at_accounting/models/executive_summary_report.py b/addons/at_accounting/models/executive_summary_report.py deleted file mode 100644 index f048a4f..0000000 --- a/addons/at_accounting/models/executive_summary_report.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models -from odoo.exceptions import UserError - -class ExecutiveSummaryReport(models.Model): - _inherit = 'account.report' - - def _report_custom_engine_executive_summary_ndays(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None): - if current_groupby or next_groupby: - raise UserError("NDays expressions of executive summary report don't support the 'group by' feature.") - - date_diff = fields.Date.from_string(options['date']['date_to']) - fields.Date.from_string(options['date']['date_from']) - return {'result': date_diff.days} diff --git a/addons/at_accounting/models/ir_actions.py b/addons/at_accounting/models/ir_actions.py deleted file mode 100644 index f581a6c..0000000 --- a/addons/at_accounting/models/ir_actions.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import models - -class IrActionsAccountReportDownload(models.AbstractModel): - - _name = 'ir_actions_account_report_download' - _description = 'Technical model for accounting report downloads' - - def _get_readable_fields(self): - - return self.env['ir.actions.actions']._get_readable_fields() | {'data'} diff --git a/addons/at_accounting/models/ir_ui_menu.py b/addons/at_accounting/models/ir_ui_menu.py deleted file mode 100644 index 50a7902..0000000 --- a/addons/at_accounting/models/ir_ui_menu.py +++ /dev/null @@ -1,21 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models - - -class IrUiMenu(models.Model): - _inherit = 'ir.ui.menu' - - def _visible_menu_ids(self, debug=False): - visible_ids = super()._visible_menu_ids(debug) - # These menus should only be visible to accountants (users with group_account_readonly) and the group specified on the menu - # We want to avoid moving these menus to the new `accountant` module - if not self.env.user.has_group('account.group_account_readonly'): - accounting_menus = [ - 'at_accounting.account_tag_menu', - 'at_accounting.menu_account_group', - 'at_accounting.menu_action_account_report_multicurrency_revaluation', - ] - hidden_menu_ids = {self.env.ref(r).sudo().id for r in accounting_menus if self.env.ref(r, raise_if_not_found=False)} - return visible_ids - hidden_menu_ids - return visible_ids diff --git a/addons/at_accounting/models/mail_activity.py b/addons/at_accounting/models/mail_activity.py deleted file mode 100644 index 930e358..0000000 --- a/addons/at_accounting/models/mail_activity.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models, _ - - -class AccountTaxReportActivity(models.Model): - _inherit = "mail.activity" - - account_tax_closing_params = fields.Json(string="Tax closing additional params") - - def action_open_tax_activity(self): - self.ensure_one() - if self.activity_type_id == self.env.ref('at_accounting.mail_activity_type_tax_report_to_pay'): - move = self.env['account.move'].browse(self.res_id) - return move._action_tax_to_pay_wizard() - - journal = self.env['account.journal'].browse(self.res_id) - options = {} - if self.account_tax_closing_params: - options = self.env['account.move']._get_tax_closing_report_options( - journal.company_id, - self.env['account.fiscal.position'].browse(self.account_tax_closing_params['fpos_id']) if self.account_tax_closing_params['fpos_id'] else False, - self.env['account.report'].browse(self.account_tax_closing_params['report_id']), - fields.Date.from_string(self.account_tax_closing_params['tax_closing_end_date']) - ) - action = self.env["ir.actions.actions"]._for_xml_id("at_accounting.action_account_report_gt") - action.update({'params': {'options': options, 'ignore_session': True}}) - return action diff --git a/addons/at_accounting/models/mail_activity_type.py b/addons/at_accounting/models/mail_activity_type.py deleted file mode 100644 index 8022b5e..0000000 --- a/addons/at_accounting/models/mail_activity_type.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models - - -class AccountTaxReportActivityType(models.Model): - _inherit = "mail.activity.type" - - category = fields.Selection(selection_add=[('tax_report', 'Tax report')]) diff --git a/addons/at_accounting/models/res_company.py b/addons/at_accounting/models/res_company.py deleted file mode 100644 index fac4fb4..0000000 --- a/addons/at_accounting/models/res_company.py +++ /dev/null @@ -1,664 +0,0 @@ -from odoo import api, fields, models, _ -from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT -from datetime import timedelta -from odoo.tools import date_utils -import datetime -from dateutil.relativedelta import relativedelta -import itertools -from odoo.exceptions import UserError -from odoo.tools.misc import format_date - - -class ResCompany(models.Model): - _inherit = 'res.company' - - invoicing_switch_threshold = fields.Date(string="Invoicing Switch Threshold", - help="Every payment and invoice before this date will receive the 'From Invoicing' status, hiding all the accounting entries related to it. Use this option after installing Accounting if you were using only Invoicing before, before importing all your actual accounting data in to Odoo.") - predict_bill_product = fields.Boolean(string="Predict Bill Product") - - sign_invoice = fields.Boolean(string='Display signing field on invoices') - signing_user = fields.Many2one(comodel_name='res.users') - - # Deferred expense management - deferred_expense_journal_id = fields.Many2one( - comodel_name='account.journal', - string="Deferred Expense Journal", - ) - deferred_expense_account_id = fields.Many2one( - comodel_name='account.account', - string="Deferred Expense Account", - ) - generate_deferred_expense_entries_method = fields.Selection( - string="Generate Deferred Expense Entries", - selection=[ - ('on_validation', 'On bill validation'), - ('manual', 'Manually & Grouped'), - ], - default='on_validation', - required=True, - ) - deferred_expense_amount_computation_method = fields.Selection( - string="Deferred Expense Based on", - selection=[ - ('day', 'Days'), - ('month', 'Months'), - ('full_months', 'Full Months'), - ], - default='month', - required=True, - ) - - # Deferred revenue management - deferred_revenue_journal_id = fields.Many2one( - comodel_name='account.journal', - string="Deferred Revenue Journal", - ) - deferred_revenue_account_id = fields.Many2one( - comodel_name='account.account', - string="Deferred Revenue Account", - ) - generate_deferred_revenue_entries_method = fields.Selection( - string="Generate Deferred Revenue Entries", - selection=[ - ('on_validation', 'On bill validation'), - ('manual', 'Manually & Grouped'), - ], - default='on_validation', - required=True, - ) - deferred_revenue_amount_computation_method = fields.Selection( - string="Deferred Revenue Based on", - selection=[ - ('day', 'Days'), - ('month', 'Months'), - ('full_months', 'Full Months'), - ], - default='month', - required=True, - ) - totals_below_sections = fields.Boolean( - string='Add totals below sections', - help='When ticked, totals and subtotals appear below the sections of the report.') - account_tax_periodicity = fields.Selection([ - ('year', 'annually'), - ('semester', 'semi-annually'), - ('4_months', 'every 4 months'), - ('trimester', 'quarterly'), - ('2_months', 'every 2 months'), - ('monthly', 'monthly')], string="Delay units", help="Periodicity", default='monthly', required=True) - account_tax_periodicity_reminder_day = fields.Integer(string='Start from', default=7, required=True) - account_tax_periodicity_journal_id = fields.Many2one('account.journal', string='Journal', - domain=[('type', '=', 'general')], check_company=True) - account_revaluation_journal_id = fields.Many2one('account.journal', domain=[('type', '=', 'general')], - check_company=True) - account_revaluation_expense_provision_account_id = fields.Many2one('account.account', - string='Expense Provision Account', - check_company=True) - account_revaluation_income_provision_account_id = fields.Many2one('account.account', - string='Income Provision Account', - check_company=True) - account_tax_unit_ids = fields.Many2many(string="Tax Units", comodel_name='account.tax.unit', - help="The tax units this company belongs to.") - account_representative_id = fields.Many2one('res.partner', string='Accounting Firm', - help="Specify an Accounting Firm that will act as a representative when exporting reports.") - account_display_representative_field = fields.Boolean(compute='_compute_account_display_representative_field') - - def write(self, vals): - old_threshold_vals = {} - for record in self: - old_threshold_vals[record] = record.invoicing_switch_threshold - - rslt = super(ResCompany, self).write(vals) - - for record in self: - if 'invoicing_switch_threshold' in vals and old_threshold_vals[record] != vals[ - 'invoicing_switch_threshold']: - self.env['account.move.line'].flush_model(['move_id', 'parent_state']) - self.env['account.move'].flush_model( - ['company_id', 'date', 'state', 'payment_state', 'payment_state_before_switch']) - if record.invoicing_switch_threshold: - # If a new date was set as threshold, we switch all the - # posted moves and payments before it to 'invoicing_legacy'. - # We also reset to posted all the moves and payments that - # were 'invoicing_legacy' and were posterior to the threshold - self.env.cr.execute(""" - update account_move_line aml - set parent_state = 'posted' - from account_move move - where aml.move_id = move.id - and move.payment_state = 'invoicing_legacy' - and move.date >= %(switch_threshold)s - and move.company_id = %(company_id)s; - - update account_move - set state = 'posted', - payment_state = payment_state_before_switch, - payment_state_before_switch = null - where payment_state = 'invoicing_legacy' - and date >= %(switch_threshold)s - and company_id = %(company_id)s; - - update account_move_line aml - set parent_state = 'cancel' - from account_move move - where aml.move_id = move.id - and move.state = 'posted' - and move.date < %(switch_threshold)s - and move.company_id = %(company_id)s; - - update account_move - set state = 'cancel', - payment_state_before_switch = payment_state, - payment_state = 'invoicing_legacy' - where state = 'posted' - and date < %(switch_threshold)s - and company_id = %(company_id)s; - """, {'company_id': record.id, 'switch_threshold': record.invoicing_switch_threshold}) - else: - # If the threshold date has been emptied, we re-post all the - # invoicing_legacy entries. - self.env.cr.execute(""" - update account_move_line aml - set parent_state = 'posted' - from account_move move - where aml.move_id = move.id - and move.payment_state = 'invoicing_legacy' - and move.company_id = %(company_id)s; - - update account_move - set state = 'posted', - payment_state = payment_state_before_switch, - payment_state_before_switch = null - where payment_state = 'invoicing_legacy' - and company_id = %(company_id)s; - """, {'company_id': record.id}) - - self.env['account.move.line'].invalidate_model(['parent_state']) - self.env['account.move'].invalidate_model(['state', 'payment_state', 'payment_state_before_switch']) - - return rslt - - def compute_fiscalyear_dates(self, current_date): - """Compute the start and end dates of the fiscal year where the given 'date' belongs to. - - :param current_date: A datetime.date/datetime.datetime object. - :return: A dictionary containing: - * date_from - * date_to - * [Optionally] record: The fiscal year record. - """ - self.ensure_one() - date_str = current_date.strftime(DEFAULT_SERVER_DATE_FORMAT) - - # Search a fiscal year record containing the date. - # If a record is found, then no need further computation, we get the dates range directly. - fiscalyear = self.env['account.fiscal.year'].search([ - ('company_id', '=', self.id), - ('date_from', '<=', date_str), - ('date_to', '>=', date_str), - ], limit=1) - if fiscalyear: - return { - 'date_from': fiscalyear.date_from, - 'date_to': fiscalyear.date_to, - 'record': fiscalyear, - } - - date_from, date_to = date_utils.get_fiscal_year( - current_date, day=self.fiscalyear_last_day, month=int(self.fiscalyear_last_month)) - - date_from_str = date_from.strftime(DEFAULT_SERVER_DATE_FORMAT) - date_to_str = date_to.strftime(DEFAULT_SERVER_DATE_FORMAT) - - # Search for fiscal year records reducing the delta between the date_from/date_to. - # This case could happen if there is a gap between two fiscal year records. - # E.g. two fiscal year records: 2017-01-01 -> 2017-02-01 and 2017-03-01 -> 2017-12-31. - # => The period 2017-02-02 - 2017-02-30 is not covered by a fiscal year record. - - fiscalyear_from = self.env['account.fiscal.year'].search([ - ('company_id', '=', self.id), - ('date_from', '<=', date_from_str), - ('date_to', '>=', date_from_str), - ], limit=1) - if fiscalyear_from: - date_from = fiscalyear_from.date_to + timedelta(days=1) - - fiscalyear_to = self.env['account.fiscal.year'].search([ - ('company_id', '=', self.id), - ('date_from', '<=', date_to_str), - ('date_to', '>=', date_to_str), - ], limit=1) - if fiscalyear_to: - date_to = fiscalyear_to.date_from - timedelta(days=1) - - return {'date_from': date_from, 'date_to': date_to} - - def _get_unreconciled_statement_lines_redirect_action(self, unreconciled_statement_lines): - # OVERRIDE account - return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget( - extra_domain=[('id', 'in', unreconciled_statement_lines.ids)], - name=_('Unreconciled statements lines'), - ) - - @api.depends('account_fiscal_country_id.code') - def _compute_account_display_representative_field(self): - country_set = self._get_countries_allowing_tax_representative() - for record in self: - record.account_display_representative_field = record.account_fiscal_country_id.code in country_set - - def _get_countries_allowing_tax_representative(self): - """ Returns a set containing the country codes of the countries for which - it is possible to use a representative to submit the tax report. - This function is a hook that needs to be overridden in localisation modules. - """ - return set() - - def _get_default_misc_journal(self): - """ Returns a default 'miscellanous' journal to use for - account_tax_periodicity_journal_id field. This is useful in case a - CoA was already installed on the company at the time the module - is installed, so that the field is set automatically when added.""" - return self.env['account.journal'].search([ - *self.env['account.journal']._check_company_domain(self), - ('type', '=', 'general'), - ], limit=1) - - def _get_tax_closing_journal(self): - journals = self.env['account.journal'] - for company in self: - journals |= company.account_tax_periodicity_journal_id or company._get_default_misc_journal() - - return journals - - @api.model_create_multi - def create(self, vals_list): - companies = super().create(vals_list) - companies._initiate_account_onboardings() - return companies - - def write(self, values): - tax_closing_update_dependencies = ('account_tax_periodicity', 'account_tax_periodicity_journal_id.id') - to_update = self.env['res.company'] - for company in self: - if company._get_tax_closing_journal(): - need_tax_closing_update = any( - update_dep in values and company.mapped(update_dep)[0] != values[update_dep] - for update_dep in tax_closing_update_dependencies - ) - - if need_tax_closing_update: - to_update += company - - res = super().write(values) - - # Early return - if not to_update: - return res - - to_reset_closing_moves = self.env['account.move'].sudo().search([ - ('company_id', 'in', to_update.ids), - ('tax_closing_report_id', '!=', False), - ('state', '=', 'draft'), - ]) - to_reset_closing_moves.button_cancel() - misc_journals = self.env['account.journal'].sudo().search([ - *self.env['account.journal']._check_company_domain(to_update), - ('type', '=', 'general'), - ]) - to_reset_closing_reminder_activities = self.env['mail.activity'].sudo().search([ - ('res_id', 'in', misc_journals.ids), - ('res_model_id', '=', self.env['ir.model']._get_id('account.journal')), - ('activity_type_id', '=', self.env.ref('at_accounting.tax_closing_activity_type').id), - ('active', '=', True), - ]) - to_reset_closing_reminder_activities.action_cancel() - generic_tax_report = self.env.ref('account.generic_tax_report') - - # Create a new reminder - # The user is unlikely to change the periodicity often and for multiple companies at once - # So it is fair enough to make this that way as we are obliged to get the tax report for each company - # And then loop over all the reports to get their period boudaries and look for activity - for company in to_update: - tax_reports = self.env['account.report'].search([ - ('availability_condition', '=', 'country'), - ('country_id', 'in', company.account_enabled_tax_country_ids.ids), - ('root_report_id', '=', generic_tax_report.id), - ]) - if not tax_reports.filtered(lambda x: x.country_id == company.account_fiscal_country_id): - tax_reports += generic_tax_report - - for tax_report in tax_reports: - period_start, period_end = company._get_tax_closing_period_boundaries(fields.Date.today(), tax_report) - activity = company._get_tax_closing_reminder_activity(tax_report.id, period_end) - if not activity and self.env['account.move'].search_count([ - ('date', '<=', period_end), - ('date', '>=', period_start), - ('tax_closing_report_id', '=', tax_report.id), - ('company_id', '=', company.id), - ('state', '=', 'posted') - ]) == 0: - company._generate_tax_closing_reminder_activity(tax_report, period_end) - - hidden_tax_journals = self._get_tax_closing_journal().sudo().filtered(lambda j: not j.show_on_dashboard) - if hidden_tax_journals: - hidden_tax_journals.show_on_dashboard = True - - return res - - def _get_and_update_tax_closing_moves(self, in_period_date, report, fiscal_positions=None, include_domestic=False): - """ Searches for tax closing moves. If some are missing for the provided parameters, - they are created in draft state. Also, existing moves get updated in case of configuration changes - (closing journal or periodicity, for example). Note the content of these moves stays untouched. - - :param in_period_date: A date within the tax closing period we want the closing for. - :param fiscal_positions: The fiscal positions we want to generate the closing for (as a recordset). - :param include_domestic: Whether or not the domestic closing (i.e. the one without any fiscal_position_id) must be included - - :return: The closing moves, as a recordset. - """ - self.ensure_one() - - if not fiscal_positions: - fiscal_positions = [] - - # Compute period dates depending on the date - period_start, period_end = self._get_tax_closing_period_boundaries(in_period_date, report) - periodicity = self._get_tax_periodicity(report) - tax_closing_journal = self._get_tax_closing_journal() - - all_closing_moves = self.env['account.move'] - for fpos in itertools.chain(fiscal_positions, [False] if include_domestic else []): - fpos_id = fpos.id if fpos else False - tax_closing_move = self.env['account.move'].search([ - ('state', '=', 'draft'), - ('company_id', '=', self.id), - ('tax_closing_report_id', '=', report.id), - ('date', '>=', period_start), - ('date', '<=', period_end), - ('fiscal_position_id', '=', fpos.id if fpos else None), - ]) - - # This should never happen, but can be caused by wrong manual operations - if len(tax_closing_move) > 1: - if fpos: - error = _("Multiple draft tax closing entries exist for fiscal position %(position)s after %(period_start)s. There should be at most one. \n %(closing_entries)s", - position=fpos.name, period_start=period_start, closing_entries=tax_closing_move.mapped('display_name')) - - else: - error = _("Multiple draft tax closing entries exist for your domestic region after %(period_start)s. There should be at most one. \n %(closing_entries)s", - period_start=period_start, closing_entries=tax_closing_move.mapped('display_name')) - - raise UserError(error) - - # Compute tax closing description - ref = _("%(report_label)s: %(period)s", report_label=self._get_tax_closing_report_display_name(report), period=self._get_tax_closing_move_description(periodicity, period_start, period_end, fpos, report)) - - # Values for update/creation of closing move - closing_vals = { - 'company_id': self.id,# Important to specify together with the journal, for branches - 'journal_id': tax_closing_journal.id, - 'date': period_end, - 'tax_closing_report_id': report.id, - 'fiscal_position_id': fpos_id, - 'ref': ref, - 'name': '/', # Explicitly set a void name so that we don't set the sequence for the journal and don't consume a sequence number - } - - if tax_closing_move: - tax_closing_move.write(closing_vals) - else: - # Create a new, empty, tax closing move - tax_closing_move = self.env['account.move'].create(closing_vals) - - # Create a reminder activity if it doesn't exist - activity = self._get_tax_closing_reminder_activity(report.id, period_end, fpos_id) - tax_closing_options = tax_closing_move._get_tax_closing_report_options(tax_closing_move.company_id, tax_closing_move.fiscal_position_id, tax_closing_move.tax_closing_report_id, tax_closing_move.date) - if not activity and report._get_sender_company_for_export(tax_closing_options) == tax_closing_move.company_id: - self._generate_tax_closing_reminder_activity(report, period_end, fpos) - - all_closing_moves += tax_closing_move - - return all_closing_moves - - def _get_tax_closing_report_display_name(self, report): - if report.get_external_id().get(report.id) in ('account.generic_tax_report', 'account.generic_tax_report_account_tax', 'account.generic_tax_report_tax_account'): - return _("Tax return") - - return report.display_name - - def _generate_tax_closing_reminder_activity(self, report, date_in_period=None, fiscal_position=None): - """ - Create a reminder on the current tax_closing_journal for a certain report with a fiscal_position or not if None. - The reminder will target the period from which the date sits in - """ - self.ensure_one() - if not date_in_period: - date_in_period = fields.Date.today() - # Search for an existing tax closing move - tax_closing_activity_type = self.env.ref('at_accounting.tax_closing_activity_type') - - # Tax period - period_start, period_end = self._get_tax_closing_period_boundaries(date_in_period, report) - periodicity = self._get_tax_periodicity(report) - activity_deadline = period_end + relativedelta(days=self.account_tax_periodicity_reminder_day) - - # Reminder title - summary = _( - "%(report_label)s: %(period)s", - report_label=self._get_tax_closing_report_display_name(report), - period=self._get_tax_closing_move_description(periodicity, period_start, period_end, fiscal_position, report) - ) - - activity_user = tax_closing_activity_type.default_user_id if tax_closing_activity_type else self.env['res.users'] - if activity_user and not (self in activity_user.company_ids and activity_user.has_group('account.group_account_manager')): - activity_user = self.env['res.users'] - - if not activity_user: - activity_user = self.env['res.users'].search( - [('company_ids', 'in', self.ids), ('groups_id', 'in', self.env.ref('account.group_account_manager').ids)], - limit=1, order="id ASC", - ) - - self.env['mail.activity'].with_context(mail_activity_quick_update=True).create({ - 'res_id': self._get_tax_closing_journal().id, - 'res_model_id': self.env['ir.model']._get_id('account.journal'), - 'activity_type_id': tax_closing_activity_type.id, - 'date_deadline': activity_deadline, - 'automated': True, - 'summary': summary, - 'user_id': activity_user.id or self.env.user.id, - 'account_tax_closing_params': { - 'report_id': report.id, - 'tax_closing_end_date': fields.Date.to_string(period_end), - 'fpos_id': fiscal_position.id if fiscal_position else False, - }, - }) - - def _get_tax_closing_reminder_activity(self, report_id, period_end, fpos_id=False): - self.ensure_one() - tax_closing_activity_type = self.env.ref('at_accounting.tax_closing_activity_type') - return self._get_tax_closing_journal().activity_ids.filtered( - lambda act: act.account_tax_closing_params and (act.activity_type_id == tax_closing_activity_type and act.account_tax_closing_params['report_id'] == report_id - and fields.Date.from_string(act.account_tax_closing_params['tax_closing_end_date']) == period_end - and act.account_tax_closing_params['fpos_id'] == fpos_id) - ) - - def _get_tax_closing_move_description(self, periodicity, period_start, period_end, fiscal_position, report): - """ Returns a string description of the provided period dates, with the - given tax periodicity. - """ - self.ensure_one() - - foreign_vat_fpos_count = self.env['account.fiscal.position'].search_count([ - ('company_id', '=', self.id), - ('foreign_vat', '!=', False) - ]) - if foreign_vat_fpos_count: - if fiscal_position: - country_code = fiscal_position.country_id.code - state_codes = fiscal_position.mapped('state_ids.code') if fiscal_position.state_ids else [] - else: - # On domestic country - country_code = self.account_fiscal_country_id.code - - # Only consider the state in case there are foreign VAT fpos on states in this country - vat_fpos_with_state_count = self.env['account.fiscal.position'].search_count([ - ('company_id', '=', self.id), - ('foreign_vat', '!=', False), - ('country_id', '=', self.account_fiscal_country_id.id), - ('state_ids', '!=', False), - ]) - state_codes = [self.state_id.code] if self.state_id and vat_fpos_with_state_count else [] - - if state_codes: - region_string = " (%s - %s)" % (country_code, ', '.join(state_codes)) - else: - region_string = " (%s)" % country_code - else: - # Don't add region information in case there is no foreign VAT fpos - region_string = '' - - # Shift back to normal dates if we are using a start date so periods aren't broken - start_day, start_month = self._get_tax_closing_start_date_attributes(report) - if start_day != 1 or start_month != 1: - return f"{format_date(self.env, period_start)} - {format_date(self.env, period_end)}{region_string}" - - if periodicity == 'year': - return f"{period_start.year}{region_string}" - elif periodicity == 'trimester': - return f"{format_date(self.env, period_start, date_format='qqq yyyy')}{region_string}" - elif periodicity == 'monthly': - return f"{format_date(self.env, period_start, date_format='LLLL yyyy')}{region_string}" - else: - return f"{format_date(self.env, period_start)} - {format_date(self.env, period_end)}{region_string}" - - def _get_tax_closing_period_boundaries(self, date, report): - """ Returns the boundaries of the tax period containing the provided date - for this company, as a tuple (start, end). - - This function needs to stay consitent with the one inside Javascript in the filters for the tax report - """ - self.ensure_one() - period_months = self._get_tax_periodicity_months_delay(report) - start_day, start_month = self._get_tax_closing_start_date_attributes(report) - aligned_date = date + relativedelta(days=-(start_day - 1)) # we offset the date back from start_day amount of day - 1 so we can compute months periods aligned to the start and end of months - year = aligned_date.year - month_offset = aligned_date.month - start_month - period_number = (month_offset // period_months) + 1 - - # If the date is before the start date and start month of this year, this mean we are in the previous period - # So the initial_date should be one year before and the period_number should be computed in reverse because month_offset is negative - if date < datetime.date(date.year, start_month, start_day): - year -= 1 - period_number = ((12 + month_offset) // period_months) + 1 - - month_delta = period_number * period_months - - # We need to work with offsets because it handle automatically the end of months (28, 29, 30, 31) - end_date = datetime.date(year, start_month, 1) + relativedelta(months=month_delta, days=start_day - 2) # -1 because the first days is aldready counted and -1 because the first day of the next period must not be in this range - start_date = datetime.date(year, start_month, 1) + relativedelta(months=month_delta - period_months, day=start_day) - - return start_date, end_date - - def _get_available_tax_unit(self, report): - """ - Must ensures that report has a country_id to search for a tax unit - - :return: A recordset of available tax units for this report country_id and this company - """ - self.ensure_one() - return self.env['account.tax.unit'].search([ - ('company_ids', 'in', self.id), - ('country_id', '=', report.country_id.id), - ], limit=1) - - def _get_tax_periodicity(self, report): - main_company = self - if report.filter_multi_company == 'tax_units' and report.country_id and (tax_unit := self._get_available_tax_unit(report)): - main_company = tax_unit.main_company_id - - return main_company.account_tax_periodicity - - def _get_tax_closing_start_date_attributes(self, report): - if not report.tax_closing_start_date: - start_year = fields.Date.start_of(fields.Date.today(), 'year') - return start_year.day, start_year.month - - main_company = self - if report.filter_multi_company == 'tax_units' and report.country_id and (tax_unit := self._get_available_tax_unit(report)): - main_company = tax_unit.main_company_id - - start_date = report.with_company(main_company).tax_closing_start_date - - return start_date.day, start_date.month - - def _get_tax_periodicity_months_delay(self, report): - """ Returns the number of months separating two tax returns with the provided periodicity - """ - self.ensure_one() - periodicities = { - 'year': 12, - 'semester': 6, - '4_months': 4, - 'trimester': 3, - '2_months': 2, - 'monthly': 1, - } - return periodicities[self._get_tax_periodicity(report)] - - def _get_branches_with_same_vat(self, accessible_only=False): - """ Returns all companies among self and its branch hierachy (considering children and parents) that share the same VAT number - as self. An empty VAT number is considered as being the same as the one of the closest parent with a VAT number. - - self is always returned as the first element of the resulting recordset (so that this can safely be used to restore the active company). - - Example: - - main company ; vat = 123 - - branch 1 - - branch 1_1 - - branch 2 ; vat = 456 - - branch 2_1 ; vat = 789 - - branch 2_2 - - In this example, the following VAT numbers will be considered for each company: - - main company: 123 - - branch 1: 123 - - branch 1_1: 123 - - branch 2: 456 - - branch 2_1: 789 - - branch 2_2: 456 - - :param accessible_only: whether the returned companies should exclude companies that are not in self.env.companies - """ - self.ensure_one() - - current = self.sudo() - same_vat_branch_ids = [current.id] # Current is always available - current_strict_parents = current.parent_ids - current - if accessible_only: - candidate_branches = current.root_id._accessible_branches() - else: - candidate_branches = self.env['res.company'].sudo().search([('id', 'child_of', current.root_id.ids)]) - - current_vat_check_set = {current.vat} if current.vat else set() - for branch in candidate_branches - current: - parents_vat_set = set(filter(None, (branch.parent_ids - current_strict_parents).mapped('vat'))) - if parents_vat_set == current_vat_check_set: - # If all the branches between the active company and branch (both included) share the same VAT number as the active company, - # we want to add the branch to the selection. - same_vat_branch_ids.append(branch.id) - - return self.browse(same_vat_branch_ids) - - gain_account_id = fields.Many2one( - 'account.account', - domain="[('deprecated', '=', False)]", - check_company=True, - help="Account used to write the journal item in case of gain while selling an asset", - ) - loss_account_id = fields.Many2one( - 'account.account', - domain="[('deprecated', '=', False)]", - check_company=True, - help="Account used to write the journal item in case of loss while selling an asset", - ) diff --git a/addons/at_accounting/models/res_config_settings.py b/addons/at_accounting/models/res_config_settings.py deleted file mode 100644 index 6cf1bd4..0000000 --- a/addons/at_accounting/models/res_config_settings.py +++ /dev/null @@ -1,262 +0,0 @@ -from datetime import date -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError -from calendar import monthrange -from dateutil.relativedelta import relativedelta -from odoo.tools.misc import format_date -from odoo.tools import date_utils - -ACCOUNT_DOMAIN = [('deprecated', '=', False), ('account_type', 'not in', - ('asset_receivable', 'liability_payable', 'asset_cash', - 'liability_credit_card', 'off_balance'))] - - -class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' - - fiscalyear_last_day = fields.Integer(related='company_id.fiscalyear_last_day', required=True, readonly=False) - fiscalyear_last_month = fields.Selection(related='company_id.fiscalyear_last_month', required=True, readonly=False) - use_anglo_saxon = fields.Boolean(string='Anglo-Saxon Accounting', related='company_id.anglo_saxon_accounting', readonly=False) - invoicing_switch_threshold = fields.Date(string="Invoicing Switch Threshold", related='company_id.invoicing_switch_threshold', readonly=False) - group_fiscal_year = fields.Boolean(string='Fiscal Years', implied_group='at_accounting.group_fiscal_year') - predict_bill_product = fields.Boolean(string="Predict Bill Product", related='company_id.predict_bill_product', readonly=False) - - sign_invoice = fields.Boolean(string='Authorized Signatory on invoice', related='company_id.sign_invoice', readonly=False) - signing_user = fields.Many2one( - comodel_name='res.users', - string="Signature used to sign all the invoice", - readonly=False, - related='company_id.signing_user', - help="Select a user here to override every signature on invoice by this user's signature" - ) - module_sign = fields.Boolean(string='Sign', compute='_compute_module_sign_status') - - # Deferred expense management - deferred_expense_journal_id = fields.Many2one( - comodel_name='account.journal', - help='Journal used for deferred entries', - readonly=False, - related='company_id.deferred_expense_journal_id', - ) - deferred_expense_account_id = fields.Many2one( - comodel_name='account.account', - help='Account used for deferred expenses', - readonly=False, - related='company_id.deferred_expense_account_id', - ) - generate_deferred_expense_entries_method = fields.Selection( - related='company_id.generate_deferred_expense_entries_method', - readonly=False, required=True, - help='Method used to generate deferred entries', - ) - deferred_expense_amount_computation_method = fields.Selection( - related='company_id.deferred_expense_amount_computation_method', - readonly=False, required=True, - help='Method used to compute the amount of deferred entries', - ) - - # Deferred revenue management - deferred_revenue_journal_id = fields.Many2one( - comodel_name='account.journal', - help='Journal used for deferred entries', - readonly=False, - related='company_id.deferred_revenue_journal_id', - ) - deferred_revenue_account_id = fields.Many2one( - comodel_name='account.account', - help='Account used for deferred revenues', - readonly=False, - related='company_id.deferred_revenue_account_id', - ) - generate_deferred_revenue_entries_method = fields.Selection( - related='company_id.generate_deferred_revenue_entries_method', - readonly=False, required=True, - help='Method used to generate deferred entries', - ) - deferred_revenue_amount_computation_method = fields.Selection( - related='company_id.deferred_revenue_amount_computation_method', - readonly=False, required=True, - help='Method used to compute the amount of deferred entries', - ) - totals_below_sections = fields.Boolean(related='company_id.totals_below_sections', - string='Add totals below sections', readonly=False, - help='When ticked, totals and subtotals appear below the sections of the report.') - account_tax_periodicity = fields.Selection(related='company_id.account_tax_periodicity', string='Periodicity', - readonly=False, required=True) - account_tax_periodicity_reminder_day = fields.Integer(related='company_id.account_tax_periodicity_reminder_day', - string='Reminder', readonly=False, required=True) - account_tax_periodicity_journal_id = fields.Many2one(related='company_id.account_tax_periodicity_journal_id', - string='Journal', readonly=False) - - account_reports_show_per_company_setting = fields.Boolean( - compute="_compute_account_reports_show_per_company_setting") - - @api.depends('sign_invoice') - def _compute_module_sign_status(self): - sign_installed = 'sign' in self.env['ir.module.module']._installed() - for settings in self: - settings.module_sign = sign_installed or settings.company_id.sign_invoice - - @api.constrains('fiscalyear_last_day', 'fiscalyear_last_month') - def _check_fiscalyear(self): - # We try if the date exists in 2020, which is a leap year. - # We do not define the constrain on res.company, since the recomputation of the related - # fields is done one field at a time. - for wiz in self: - try: - date(2020, int(wiz.fiscalyear_last_month), wiz.fiscalyear_last_day) - except ValueError: - raise ValidationError( - _('Incorrect fiscal year date: day is out of range for month. Month: %(month)s; Day: %(day)s', - month=wiz.fiscalyear_last_month, day=wiz.fiscalyear_last_day), - ) - - @api.model_create_multi - def create(self, vals_list): - # Amazing workaround: non-stored related fields on company are a BAD idea since the 2 fields - # must follow the constraint '_check_fiscalyear_last_day'. The thing is, in case of related - # fields, the inverse write is done one value at a time, and thus the constraint is verified - # one value at a time... so it is likely to fail. - for vals in vals_list: - fiscalyear_last_day = vals.pop('fiscalyear_last_day', False) or self.env.company.fiscalyear_last_day - fiscalyear_last_month = vals.pop('fiscalyear_last_month', False) or self.env.company.fiscalyear_last_month - vals = {} - if fiscalyear_last_day != self.env.company.fiscalyear_last_day: - vals['fiscalyear_last_day'] = fiscalyear_last_day - if fiscalyear_last_month != self.env.company.fiscalyear_last_month: - vals['fiscalyear_last_month'] = fiscalyear_last_month - if vals: - self.env.company.write(vals) - return super().create(vals_list) - - def open_tax_group_list(self): - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'name': 'Tax groups', - 'res_model': 'account.tax.group', - 'view_mode': 'list', - 'context': { - 'default_country_id': self.account_fiscal_country_id.id, - 'search_default_country_id': self.account_fiscal_country_id.id, - }, - } - - @api.depends('company_id') - def _compute_account_reports_show_per_company_setting(self): - custom_start_country_codes = self._get_country_codes_with_another_tax_closing_start_date() - countries = self.env['account.fiscal.position'].search([ - ('company_id', '=', self.env.company.id), - ('foreign_vat', '!=', False), - ]).mapped('country_id') + self.env.company.account_fiscal_country_id - for config_settings in self: - config_settings.account_reports_show_per_company_setting = bool(set(countries.mapped('code')) & custom_start_country_codes) - - def open_company_dependent_report_settings(self): - self.ensure_one() - generic_tax_report = self.env.ref('account.generic_tax_report') - available_reports = generic_tax_report._get_variants(generic_tax_report.id) - - return { - 'type': 'ir.actions.act_window', - 'name': _('Configure your start dates'), - 'res_model': 'account.report', - 'domain': [('id', 'in', available_reports.ids)], - 'views': [(self.env.ref('at_accounting.account_report_tree_configure_start_dates').id, 'list')] - } - - def _get_country_codes_with_another_tax_closing_start_date(self): - """ - To be overridden by specific countries that wants this - - Used to know which countries can have specific start dates settings on reports - - :returns set(str): A set of country codes from which the start date settings should be shown - """ - return set() - - - property_stock_journal = fields.Many2one( - 'account.journal', "Stock Journal", - check_company=True, - compute='_compute_property_stock_account', - inverse='_set_property_stock_journal') - property_account_income_categ_id = fields.Many2one( - 'account.account', "Income Account", - check_company=True, - domain=ACCOUNT_DOMAIN, - compute='_compute_property_stock_account', - inverse='_set_property_account_income_categ_id') - property_account_expense_categ_id = fields.Many2one( - 'account.account', "Expense Account", - check_company=True, - domain=ACCOUNT_DOMAIN, - compute='_compute_property_stock_account', - inverse='_set_property_account_expense_categ_id') - property_stock_valuation_account_id = fields.Many2one( - 'account.account', "Stock Valuation Account", - check_company=True, - domain="[('deprecated', '=', False)]", - compute='_compute_property_stock_account', - inverse='_set_property_stock_valuation_account_id') - property_stock_account_input_categ_id = fields.Many2one( - 'account.account', "Stock Input Account", - check_company=True, - domain="[('deprecated', '=', False)]", - compute='_compute_property_stock_account', - inverse='_set_property_stock_account_input_categ_id') - property_stock_account_output_categ_id = fields.Many2one( - 'account.account', "Stock Output Account", - check_company=True, - domain="[('deprecated', '=', False)]", - compute='_compute_property_stock_account', - inverse='_set_property_stock_account_output_categ_id') - - @api.depends('company_id') - def _compute_property_stock_account(self): - account_stock_properties_names = self._get_account_stock_properties_names() - ProductCategory = self.env['product.category'] - for record in self: - record = record.with_company(record.company_id) - for fname in account_stock_properties_names: - field = ProductCategory._fields[fname] - record[fname] = field.get_company_dependent_fallback(ProductCategory) - - def _set_property_stock_journal(self): - for record in self: - record._set_property('property_stock_journal') - - def _set_property_account_income_categ_id(self): - for record in self: - record._set_property('property_account_income_categ_id') - - def _set_property_account_expense_categ_id(self): - for record in self: - record._set_property('property_account_expense_categ_id') - - def _set_property_stock_valuation_account_id(self): - for record in self: - record._set_property('property_stock_valuation_account_id') - - def _set_property_stock_account_input_categ_id(self): - for record in self: - record._set_property('property_stock_account_input_categ_id') - - def _set_property_stock_account_output_categ_id(self): - for record in self: - record._set_property('property_stock_account_output_categ_id') - - def _set_property(self, field_name): - self.env['ir.default'].set('product.category', field_name, self[field_name].id, company_id=self.company_id.id) - - @api.model - def _get_account_stock_properties_names(self): - return [ - 'property_stock_journal', - 'property_account_income_categ_id', - 'property_account_expense_categ_id', - 'property_stock_valuation_account_id', - 'property_stock_account_input_categ_id', - 'property_stock_account_output_categ_id', - ] - diff --git a/addons/at_accounting/models/res_currency.py b/addons/at_accounting/models/res_currency.py deleted file mode 100644 index b0ed726..0000000 --- a/addons/at_accounting/models/res_currency.py +++ /dev/null @@ -1,27 +0,0 @@ -from odoo import models - - -class ResCurrency(models.Model): - _inherit = 'res.currency' - - def _get_currency_table_fiscal_year_bounds(self, main_company): - # EXTENDS account - default_bounds = super()._get_currency_table_fiscal_year_bounds(main_company) - manual_fiscal_years = self.env['account.fiscal.year'].search(self.env['account.fiscal.year']._check_company_domain(main_company), order='date_from ASC') - - manual_bounds = manual_fiscal_years.mapped(lambda x: (x.date_from, x.date_to)) - rslt = [] - for default_from, default_to in default_bounds: - while ( - manual_bounds - and ( - not default_to - or (default_from and default_from <= manual_bounds[0][0] and default_to >= manual_bounds[0][0]) - or default_to >= manual_bounds[0][1] - )): - rslt.append(manual_bounds.pop(0)) - - if not rslt or rslt[-1][1] < default_from: - rslt.append((default_from, default_to)) - - return rslt diff --git a/addons/at_accounting/models/res_partner.py b/addons/at_accounting/models/res_partner.py deleted file mode 100644 index d5fbe9d..0000000 --- a/addons/at_accounting/models/res_partner.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models, _ - - -class ResPartner(models.Model): - _name = 'res.partner' - _inherit = 'res.partner' - - account_represented_company_ids = fields.One2many('res.company', 'account_representative_id') - - def _get_followup_responsible(self): - return self.env.user - - def open_partner_ledger(self): - action = self.env["ir.actions.actions"]._for_xml_id("at_accounting.action_account_report_partner_ledger") - action['params'] = { - 'options': {'partner_ids': self.ids, 'unfold_all': len(self.ids) == 1}, - 'ignore_session': True, - } - return action - - def open_partner(self): - return { - 'type': 'ir.actions.act_window', - 'res_model': 'res.partner', - 'res_id': self.id, - 'views': [[False, 'form']], - 'view_mode': 'form', - 'target': 'current', - } - - @api.depends_context('show_more_partner_info') - def _compute_display_name(self): - if not self.env.context.get('show_more_partner_info'): - return super()._compute_display_name() - for partner in self: - res = "" - if partner.vat: - res += f" {partner.vat}," - if partner.country_id: - res += f" {partner.country_id.code}," - partner.display_name = f"{partner.name} - " + res - - def _get_partner_account_report_attachment(self, report, options=None): - self.ensure_one() - if self.lang: - # Print the followup in the customer's language - report = report.with_context(lang=self.lang) - - if not options: - options = report.get_options({ - 'partner_ids': self.ids, - 'unfold_all': True, - 'unreconciled': True, - 'hide_account': True, - 'all_entries': False, - }) - attachment_file = report.export_to_pdf(options) - return self.env['ir.attachment'].create([ - { - 'name': f"{self.name} - {attachment_file['file_name']}", - 'res_model': self._name, - 'res_id': self.id, - 'type': 'binary', - 'raw': attachment_file['file_content'], - 'mimetype': 'application/pdf', - }, - ]) diff --git a/addons/at_accounting/security/accounting_security.xml b/addons/at_accounting/security/accounting_security.xml deleted file mode 100644 index 5894cd7..0000000 --- a/addons/at_accounting/security/accounting_security.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - Accounting - Helps you handle your invoices and accounting actions. - - Invoicing: Invoices, payments and basic invoice reporting. - Invoicing & Banks: adds the accounting dashboard, bank management and follow-up reports. - Bookkeeper: access to all Accounting features, including reporting, asset management, analytic accounting, without configuration rights. - Administrator: full access including configuration rights and accounting data management. - Readonly: access to all the accounting data but in readonly mode, no actions allowed. - - - - - Read-only - - - - - Bookkeeper - - - - - - - - - diff --git a/addons/at_accounting/security/at_account_asset_security.xml b/addons/at_accounting/security/at_account_asset_security.xml deleted file mode 100644 index 2e736fa..0000000 --- a/addons/at_accounting/security/at_account_asset_security.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Account Asset multi-company - - - [('company_id', 'parent_of', company_ids)] - - - - Account Asset Group multi-company - - - [('company_id', 'parent_of', company_ids)] - - - diff --git a/addons/at_accounting/security/at_accounting_security.xml b/addons/at_accounting/security/at_accounting_security.xml deleted file mode 100644 index ad5cbd4..0000000 --- a/addons/at_accounting/security/at_accounting_security.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Helps you handle your invoices and accounting actions. - - Invoicing: Invoices, payments and basic invoice reporting. - Invoicing & Banks: adds the accounting dashboard, bank management and follow-up reports. - Administrator: full access including configuration rights. - - - - - Invoicing & Banks - - - - - - - - - Allow to define fiscal years of more or less than a year - - - diff --git a/addons/at_accounting/security/ir.model.access.csv b/addons/at_accounting/security/ir.model.access.csv deleted file mode 100644 index 39f73db..0000000 --- a/addons/at_accounting/security/ir.model.access.csv +++ /dev/null @@ -1,40 +0,0 @@ -"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"access_account_change_lock_date","access.account.change.lock.date","model_account_change_lock_date","account.group_account_manager",1,1,1,0 -"access_account_secure_entries_wizard","access.account.secure.entries.wizard","account.model_account_secure_entries_wizard","account.group_account_user",1,1,1,0 -"access_account_auto_reconcile_wizard","access.account.auto.reconcile.wizard","model_account_auto_reconcile_wizard","account.group_account_user",1,1,1,0 -"access_account_reconcile_wizard","access.account.reconcile.wizard","model_account_reconcile_wizard","account.group_account_user",1,1,1,0 - -access_account_fiscal_year_readonly,account.fiscal.year.user,model_account_fiscal_year,account.group_account_readonly,1,0,0,0 -access_account_fiscal_year_manager,account.fiscal.year.manager,model_account_fiscal_year,account.group_account_manager,1,1,1,1 - -access_bank_rec_widget,access.bank.rec.widget,model_bank_rec_widget,account.group_account_user,1,1,1,1 -access_bank_rec_widget_line,access.bank.rec.widget.line,model_bank_rec_widget_line,account.group_account_user,1,1,1,1 - - - -access_account_report_annotation_readonly,account.account_report_annotation_readonly,model_account_report_annotation,account.group_account_readonly,1,0,0,0 -access_account_report_annotation,account.account_report_annotation,model_account_report_annotation,account.group_account_user,1,1,1,1 -access_account_report_annotation_invoice,account.account_report_annotation,model_account_report_annotation,account.group_account_invoice,1,0,0,0 -access_account_reports_export_wizard,access.account_reports.export.wizard,model_account_reports_export_wizard,account.group_account_user,1,1,1,0 -access_account_reports_export_wizard_format,access.account_reports.export.wizard.format,model_account_reports_export_wizard_format,account.group_account_user,1,1,1,0 -access_account_report_file_download_error_wizard,account.report.file.download.error.wizard,model_account_report_file_download_error_wizard,account.group_account_user,1,1,1,0 -access_account_multicurrency_revaluation_wizard,access.account.multicurrency.revaluation.wizard,model_account_multicurrency_revaluation_wizard,account.group_account_user,1,1,1,0 -access_account_tax_unit_readonly,access_account_tax_unit_readonly,model_account_tax_unit,account.group_account_readonly,1,0,0,0 -access_account_tax_unit_manager,access_account_tax_unit_manager,model_account_tax_unit,account.group_account_manager,1,1,1,1 -access_account_report_horizontal_group_readonly,account.report.horizontal.group.readonly,model_account_report_horizontal_group,account.group_account_readonly,1,0,0,0 -access_account_report_horizontal_group_ac_user,account.report.horizontal.group.ac.user,model_account_report_horizontal_group,account.group_account_manager,1,1,1,1 -access_account_report_horizontal_group_rule_readonly,account.report.horizontal.group.rule.readonly,model_account_report_horizontal_group_rule,account.group_account_readonly,1,0,0,0 -access_account_report_horizontal_group_rule_ac_user,account.report.horizontal.group.rule.ac.user,model_account_report_horizontal_group_rule,account.group_account_manager,1,1,1,1 -access_account_report_budget_readonly,account.report.budget.readonly,model_account_report_budget,account.group_account_readonly,1,0,0,0 -access_account_report_budget_ac_user,account.report.budget.ac.user,model_account_report_budget,account.group_account_manager,1,1,1,1 -access_account_report_budget_item_readonly,account.report.budget.item.readonly,model_account_report_budget_item,account.group_account_readonly,1,0,0,0 -access_account_report_budget_item_ac_user,account.report.budget.item.ac.user,model_account_report_budget_item,account.group_account_manager,1,1,1,1 -access_account_report_send,access.account.report.send,model_account_report_send,account.group_account_invoice,1,1,1,1 - - -access_account_asset,account.asset,model_account_asset,account.group_account_readonly,1,0,0,0 -access_account_asset_manager,account.asset,model_account_asset,account.group_account_manager,1,1,1,1 -access_account_asset_invoicing_payment,account.asset,model_account_asset,account.group_account_invoice,1,0,1,0 -access_asset_modify,access.asset.modify,model_asset_modify,account.group_account_user,1,1,1,0 -access_account_asset_group,account.asset.group,model_account_asset_group,account.group_account_readonly,1,0,0,0 -access_account_asset_group_manager,account.asset.group,model_account_asset_group,account.group_account_manager,1,1,1,1 diff --git a/addons/at_accounting/static/.DS_Store b/addons/at_accounting/static/.DS_Store deleted file mode 100644 index aac05a5..0000000 Binary files a/addons/at_accounting/static/.DS_Store and /dev/null differ diff --git a/addons/at_accounting/test_csv_file/test_csv.csv b/addons/at_accounting/test_csv_file/test_csv.csv deleted file mode 100644 index 8e8bfea..0000000 --- a/addons/at_accounting/test_csv_file/test_csv.csv +++ /dev/null @@ -1,20 +0,0 @@ -02 01 15;;LAST STATEMENT;; $21,699.55 -02 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 MAILCHIMP MAILCHIMP.COMGA";($240.00); $21,459.55 -02 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 INDEED 203-564-2400 CT";($500.08); $20,959.46 -02 02 15;;"ACH CREDIT""AMERICAN EXPRESS-SETTLEMENT";$3,728.87 ; $24,688.34 -02 02 15;;"DEBIT CARD 6906""BAYSIDE MARKET/1 SAN FRANCISCO CA";($41.64); $24,646.70 -02 02 15;;"DEBIT CARD 6906""02/02 COMFORT INNS SAN FRANCISCOCA";($2,064.82); $22,581.88 -02 03 15;;"ACH CREDIT""CHECKFLUID INC -013015";$2,500.00 ; $25,081.88 -02 03 15;;"DEBIT CARD 6906""02/02 DISTRICT SF SAN FRANCISCOCA";($45.86); $25,036.02 -02 03 15;;"DEPOSIT-WIRED FUNDS""TVET OPERATING PLLC";$8,366.00 ; $33,402.02 -02 03 15;;"DEBIT CARD 6906""02/03 IBM USED PC 888S 188-874-6742 NY";($4,344.66); $29,057.36 -02 03 15;;"DEBIT CARD 6906""02/02 VIR ATL 9327 180-08628621 CT";($1,284.33); $27,773.03 -02 03 15;;"DEBIT CARD 6906""02/02 VIR ATL 9327 180-08628621 CT";($1,284.33); $26,488.70 -02 03 15;;"DEBIT CARD 6906""02/02 VIR ATL 9327 180-08628621 CT";($1,284.33); $25,204.37 -02 03 15;;"DEBIT CARD 6906""02/02 VIR ATL 9327 180-08628621 CT";($1,123.33); $24,081.04 -02 03 15;;"DEBIT CARD 6906""02/02 VIR ATL 9327 180-08628621 CT";($1,123.33); $22,957.71 -02 03 15;;"ACH DEBIT""AUTHNET GATEWAY -BILLING";($25.00); $22,932.71 -02 03 15;;"ACH DEBIT""WW 222 BROADWAY -ACH";($7,500.00); $15,432.71 -02 04 15;;"DEBIT CARD 6906""02/03 VIR ATL 9327 180-08628621 CT";($1,284.33); $14,148.38 -02 04 15;;"DEBIT CARD 6906""02/04 GROUPON INC 877-788-7858 IL";($204.23); $13,944.15 -02 05 15;;"ACH CREDIT""MERCHE-SOLUTIONS-MERCH DEP";$9,518.40 ; $23,462.55 diff --git a/addons/at_accounting/test_csv_file/test_csv_empty_date.csv b/addons/at_accounting/test_csv_file/test_csv_empty_date.csv deleted file mode 100644 index e1769a4..0000000 --- a/addons/at_accounting/test_csv_file/test_csv_empty_date.csv +++ /dev/null @@ -1,5 +0,0 @@ -02 01 15;;LAST STATEMENT;; $21,699.55 -02 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 MAILCHIMP MAILCHIMP.COMGA"; ($240.00); $21,459.55 -04 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 INDEED 203-564-2400 CT"; ($500.08); $20,959.46 -02 02 15;;"ACH CREDIT""AMERICAN EXPRESS-SETTLEMENT"; $3,728.87; $24,688.34 -;;"DEBIT CARD 6906""BAYSIDE MARKET/1 SAN FRANCISCO CA"; ($41.64); $24,646.70 diff --git a/addons/at_accounting/test_csv_file/test_csv_missing_values.csv b/addons/at_accounting/test_csv_file/test_csv_missing_values.csv deleted file mode 100644 index b930490..0000000 --- a/addons/at_accounting/test_csv_file/test_csv_missing_values.csv +++ /dev/null @@ -1,2 +0,0 @@ -TRANSFER;bank_ref_1;bank_statement_line_1;;1000 -TRANSFER;;bank_statement_line_2;;3500 diff --git a/addons/at_accounting/test_csv_file/test_csv_non_sorted.csv b/addons/at_accounting/test_csv_file/test_csv_non_sorted.csv deleted file mode 100644 index 98d6a30..0000000 --- a/addons/at_accounting/test_csv_file/test_csv_non_sorted.csv +++ /dev/null @@ -1,4 +0,0 @@ -02 01 15;;LAST STATEMENT;; $21,699.55 -02 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 MAILCHIMP MAILCHIMP.COMGA";($240.00); $21,459.55 -04 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 INDEED 203-564-2400 CT";($500.08); $20,959.46 -02 02 15;;"ACH CREDIT""AMERICAN EXPRESS-SETTLEMENT";$3,728.87 ; $24,688.34 diff --git a/addons/at_accounting/test_csv_file/test_csv_without_amount.csv b/addons/at_accounting/test_csv_file/test_csv_without_amount.csv deleted file mode 100644 index 3d1973e..0000000 --- a/addons/at_accounting/test_csv_file/test_csv_without_amount.csv +++ /dev/null @@ -1,3 +0,0 @@ -02 01 15;;LAST STATEMENT; -02 02 15;;"DEBIT CARD 6906 EFF 02-01""01/31 MAILCHIMP MAILCHIMP.COMGA"; - diff --git a/addons/at_accounting/tests/__init__.py b/addons/at_accounting/tests/__init__.py deleted file mode 100644 index 586c660..0000000 --- a/addons/at_accounting/tests/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from . import test_account_fiscal_year -from . import test_bank_rec_widget -from . import test_bank_rec_widget_tour -from . import test_prediction -from . import test_reconciliation_matching_rules -from . import test_account_auto_reconcile_wizard -from . import test_account_reconcile_wizard -from . import test_deferred_management -from . import test_ui -from . import test_signature -from . import test_change_lock_date_wizard -from . import test_tour -from . import common -from . import test_account_asset -from . import test_board_compute -from . import test_reevaluation_asset -from . import test_analytic_reports, test_financial_report, test_reconciliation_widget -from . import test_import_bank_statement \ No newline at end of file diff --git a/addons/at_accounting/tests/__pycache__/__init__.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 9203f60..0000000 Binary files a/addons/at_accounting/tests/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/common.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/common.cpython-312.pyc deleted file mode 100644 index 3316791..0000000 Binary files a/addons/at_accounting/tests/__pycache__/common.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_account_asset.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_account_asset.cpython-312.pyc deleted file mode 100644 index 0c2bf03..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_account_asset.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_account_auto_reconcile_wizard.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_account_auto_reconcile_wizard.cpython-312.pyc deleted file mode 100644 index 7cd277d..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_account_auto_reconcile_wizard.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_account_fiscal_year.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_account_fiscal_year.cpython-312.pyc deleted file mode 100644 index 78bc4b4..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_account_fiscal_year.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_account_reconcile_wizard.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_account_reconcile_wizard.cpython-312.pyc deleted file mode 100644 index ccb34ae..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_account_reconcile_wizard.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_analytic_reports.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_analytic_reports.cpython-312.pyc deleted file mode 100644 index 9074817..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_analytic_reports.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_bank_rec_widget.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_bank_rec_widget.cpython-312.pyc deleted file mode 100644 index 24358ac..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_bank_rec_widget.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_bank_rec_widget_common.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_bank_rec_widget_common.cpython-312.pyc deleted file mode 100644 index eb40f05..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_bank_rec_widget_common.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_bank_rec_widget_tour.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_bank_rec_widget_tour.cpython-312.pyc deleted file mode 100644 index ef01a76..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_bank_rec_widget_tour.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_board_compute.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_board_compute.cpython-312.pyc deleted file mode 100644 index 4974d80..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_board_compute.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_change_lock_date_wizard.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_change_lock_date_wizard.cpython-312.pyc deleted file mode 100644 index 34fb421..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_change_lock_date_wizard.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_deferred_management.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_deferred_management.cpython-312.pyc deleted file mode 100644 index 6bd9960..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_deferred_management.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_financial_report.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_financial_report.cpython-312.pyc deleted file mode 100644 index 75d05c1..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_financial_report.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_import_bank_statement.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_import_bank_statement.cpython-312.pyc deleted file mode 100644 index 7a95714..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_import_bank_statement.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_prediction.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_prediction.cpython-312.pyc deleted file mode 100644 index 07888ec..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_prediction.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_reconciliation_matching_rules.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_reconciliation_matching_rules.cpython-312.pyc deleted file mode 100644 index dee438d..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_reconciliation_matching_rules.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_reconciliation_widget.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_reconciliation_widget.cpython-312.pyc deleted file mode 100644 index 7a5bfd8..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_reconciliation_widget.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_reevaluation_asset.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_reevaluation_asset.cpython-312.pyc deleted file mode 100644 index 9888f4f..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_reevaluation_asset.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_signature.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_signature.cpython-312.pyc deleted file mode 100644 index 127bd03..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_signature.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_tour.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_tour.cpython-312.pyc deleted file mode 100644 index 3d42672..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_tour.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/__pycache__/test_ui.cpython-312.pyc b/addons/at_accounting/tests/__pycache__/test_ui.cpython-312.pyc deleted file mode 100644 index a0f0908..0000000 Binary files a/addons/at_accounting/tests/__pycache__/test_ui.cpython-312.pyc and /dev/null differ diff --git a/addons/at_accounting/tests/common.py b/addons/at_accounting/tests/common.py deleted file mode 100644 index a480f40..0000000 --- a/addons/at_accounting/tests/common.py +++ /dev/null @@ -1,429 +0,0 @@ -from odoo import fields -import copy -import io -import unittest -from collections import Counter -from datetime import datetime, date -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -try: - from openpyxl import load_workbook -except ImportError: - load_workbook = None -from odoo import Command, fields -from odoo.exceptions import UserError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT -from odoo.tools.misc import formatLang, file_open - -class TestAccountAssetCommon(AccountTestInvoicingCommon): - - @classmethod - def create_asset(cls, value, periodicity, periods, degressive_factor=None, import_depreciation=0, **kwargs): - if degressive_factor is not None: - kwargs["method_progress_factor"] = degressive_factor - return cls.env['account.asset'].create({ - 'name': 'nice asset', - 'account_asset_id': cls.company_data['default_account_assets'].id, - 'account_depreciation_id': cls.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': cls.company_data['default_account_expense'].id, - 'journal_id': cls.company_data['default_journal_misc'].id, - 'acquisition_date': "2020-02-01", - 'prorata_computation_type': 'none', - 'original_value': value, - 'salvage_value': 0, - 'method_number': periods, - 'method_period': '12' if periodicity == "yearly" else '1', - 'method': "linear", - 'already_depreciated_amount_import': import_depreciation, - **kwargs, - }) - - @classmethod - def _get_depreciation_move_values(cls, date, depreciation_value, remaining_value, depreciated_value, state): - return { - 'date': fields.Date.from_string(date), - 'depreciation_value': depreciation_value, - 'asset_remaining_value': remaining_value, - 'asset_depreciated_value': depreciated_value, - 'state': state, - } - -class TestAccountReportsCommon(AccountTestInvoicingCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.other_currency = cls.setup_other_currency('CAD') - cls.company_data_2 = cls.setup_other_company() - cls.company_data_2['company'].currency_id = cls.other_currency - cls.company_data_2['currency'] = cls.other_currency - - @classmethod - def _generate_options(cls, report, date_from, date_to, default_options=None): - ''' Create new options at a certain date. - :param report: The report. - :param date_from: A datetime object, str representation of a date or False. - :param date_to: A datetime object or str representation of a date. - :return: The newly created options. - ''' - if isinstance(date_from, datetime): - date_from_str = fields.Date.to_string(date_from) - else: - date_from_str = date_from - - if isinstance(date_to, datetime): - date_to_str = fields.Date.to_string(date_to) - else: - date_to_str = date_to - - if not default_options: - default_options = {} - - return report.get_options({ - 'selected_variant_id': report.id, - 'date': { - 'date_from': date_from_str, - 'date_to': date_to_str, - 'mode': 'range', - 'filter': 'custom', - }, - 'show_account': True, - 'show_currency': True, - **default_options, - }) - - def _update_comparison_filter(self, options, report, comparison_type, number_period, date_from=None, date_to=None): - ''' Modify the existing options to set a new filter_comparison. - :param options: The report options. - :param report: The report. - :param comparison_type: One of the following values: ('no_comparison', 'custom', 'previous_period', 'previous_year'). - :param number_period: The number of period to compare. - :param date_from: A datetime object for the 'custom' comparison_type. - :param date_to: A datetime object the 'custom' comparison_type. - :return: The newly created options. - ''' - previous_options = {**options, 'comparison': { - **options['comparison'], - 'date_from': date_from and date_from.strftime(DEFAULT_SERVER_DATE_FORMAT), - 'date_to': date_to and date_to.strftime(DEFAULT_SERVER_DATE_FORMAT), - 'filter': comparison_type, - 'number_period': number_period, - }} - return report.get_options(previous_options) - - def _update_multi_selector_filter(self, options, option_key, selected_ids): - ''' Modify a selector in the options to select . - :param options: The report options. - :param option_key: The key to the option. - :param selected_ids: The ids to be selected. - :return: The newly created options. - ''' - new_options = copy.deepcopy(options) - for c in new_options[option_key]: - c['selected'] = c['id'] in selected_ids - return new_options - - def assertColumnPercentComparisonValues(self, lines, expected_values): - filtered_lines = self._filter_folded_lines(lines) - - # Check number of lines. - self.assertEqual(len(filtered_lines), len(expected_values)) - - for value, expected_value in zip(filtered_lines, expected_values): - # Check number of columns. - key = 'column_percent_comparison_data' - self.assertEqual(len(value[key]) + 1, len(expected_value)) - # Check name, value and class. - self.assertEqual((value['name'], value[key]['name'], value[key]['mode']), expected_value) - - def assertHorizontalGroupTotal(self, lines, expected_values): - filtered_lines = self._filter_folded_lines(lines) - - # Check number of lines. - self.assertEqual(len(filtered_lines), len(expected_values)) - for line_dict_list, expected_values in zip(filtered_lines, expected_values): - column_values = [column['no_format'] for column in line_dict_list['columns']] - # Compare the Total column, Total column is there only under certain condition - if line_dict_list.get('horizontal_group_total_data'): - self.assertEqual(len(line_dict_list['columns']) + 1, len(expected_values[1:])) - # Compare the numbers column except the total - self.assertEqual(column_values, list(expected_values[1:-1])) - # Compare the total column - self.assertEqual(line_dict_list['horizontal_group_total_data']['no_format'], expected_values[-1]) - else: - # No total column - self.assertEqual(len(line_dict_list['columns']), len(expected_values[1:])) - self.assertEqual(column_values, list(expected_values[1:])) - - def assertHeadersValues(self, headers, expected_headers): - ''' Helper to compare the headers returned by the _get_table method - with some expected results. - An header is a row of columns. Then, headers is a list of list of dictionary. - :param headers: The headers to compare. - :param expected_headers: The expected headers. - :return: - ''' - # Check number of header lines. - self.assertEqual(len(headers), len(expected_headers)) - - for header, expected_header in zip(headers, expected_headers): - # Check number of columns. - self.assertEqual(len(header), len(expected_header)) - - for i, column in enumerate(header): - # Check name. - self.assertEqual(column['name'], self._convert_str_to_date(column['name'], expected_header[i])) - - def assertIdenticalLines(self, reports): - """Helper to compare report lines with the same `code` across multiple reports. - The helper checks the lines for similarity on: - - number of expressions - - expression label - - expression engine - - expression formula - - expression subformula - - expression date_scope - - :param reports: (recordset of account.report) The reports to check - """ - def expression_to_comparable_values(expr): - return ( - expr.label, - expr.engine, - expr.formula, - expr.subformula, - expr.date_scope - ) - - if not reports: - raise UserError('There are no reports to compare.') - visited_line_codes = set() - for line in reports.line_ids: - if not line.code or line.code in visited_line_codes: - continue - identical_lines = reports.line_ids.filtered(lambda l: l != line and l.code == line.code) - if not identical_lines: - continue - with self.subTest(line_code=line.code): - for tested_line in identical_lines: - self.assertCountEqual( - line.expression_ids.mapped(expression_to_comparable_values), - tested_line.expression_ids.mapped(expression_to_comparable_values), - ( - f'The line with code {line.code} from reports "{line.report_id.name}" and ' - f'"{tested_line.report_id.name}" has different expression values in both reports.' - ) - ) - visited_line_codes.add(line.code) - - def assertLinesValues(self, lines, columns, expected_values, options, currency_map=None, ignore_folded=True): - ''' Helper to compare the lines returned by the _get_lines method - with some expected results and ensuring the 'id' key of each line holds a unique value. - :param lines: See _get_lines. - :param columns: The columns index. - :param expected_values: A list of iterables. - :param options: The options from the current report. - :param currency_map: A map mapping each column_index to some extra options to test the lines: - - currency: The currency to be applied on the column. - - currency_code_index: The index of the column containing the currency code. - :param ignore_folded: Will not filter folded lines when True. - ''' - if currency_map is None: - currency_map = {} - - filtered_lines = self._filter_folded_lines(lines) if ignore_folded else lines - - # Compare the table length to see if any line is missing - self.assertEqual(len(filtered_lines), len(expected_values)) - - # Compare cell by cell the current value with the expected one. - to_compare_list = [] - for i, line in enumerate(filtered_lines): - compared_values = [[], []] - for j, index in enumerate(columns): - if index == 0: - current_value = line['name'] - else: - # Some lines may not have columns, like title lines. In such case, no values should be provided for these. - # Note that the function expect a tuple, so the line still need a comma after the name value. - if j > len(expected_values[i]) - 1: - break - current_value = line['columns'][index-1].get('name', '') - current_figure_type = line['columns'][index - 1].get('figure_type', '') - - expected_value = expected_values[i][j] - currency_data = currency_map.get(index, {}) - used_currency = None - if 'currency' in currency_data: - used_currency = currency_data['currency'] - elif 'currency_code_index' in currency_data: - currency_code = line['columns'][currency_data['currency_code_index'] - 1].get('name', '') - if currency_code: - used_currency = self.env['res.currency'].search([('name', '=', currency_code)], limit=1) - assert used_currency, "Currency having name=%s not found." % currency_code - if not used_currency: - used_currency = self.env.company.currency_id - - if type(expected_value) in (int, float) and type(current_value) == str: - if current_figure_type and current_figure_type != 'monetary': - expected_value = str(expected_value) - elif options.get('multi_currency'): - expected_value = formatLang(self.env, expected_value, currency_obj=used_currency) - else: - expected_value = formatLang(self.env, expected_value, digits=used_currency.decimal_places) - - compared_values[0].append(current_value) - compared_values[1].append(expected_value) - - to_compare_list.append(compared_values) - - errors = [] - for i, to_compare in enumerate(to_compare_list): - if to_compare[0] != to_compare[1]: - errors += [ - "\n==== Differences at index %s ====" % str(i), - "Current Values: %s" % str(to_compare[0]), - "Expected Values: %s" % str(to_compare[1]), - ] - - id_counts = Counter(line['id'] for line in lines) - duplicate_ids = {k: v for k, v in id_counts.items() if v > 1} - if duplicate_ids: - index_to_id = [ - f"index={index:<6} name={line.get('name', 'no line name?!')} \tline_id={line.get('id', 'no line id?!')}" - for index, line in enumerate(lines) - if line.get('id', 'no line id?!') in duplicate_ids - ] - errors += [ - "\n==== There are lines sharing the same id ====", - "\n".join(index_to_id) - ] - - if errors: - self.fail('\n'.join(errors)) - - def _filter_folded_lines(self, lines): - """ Children lines returned for folded lines (for example, totals below sections) should be ignored when comparing the results - in assertLinesValues (their parents are folded, so they are not shown anyway). This function returns a filtered version of lines - list, without the chilren of folded lines. - """ - filtered_lines = [] - folded_lines = set() - for line in lines: - if line.get('parent_id') in folded_lines: - folded_lines.add(line['id']) - else: - if line.get('unfoldable') and not line.get('unfolded'): - folded_lines.add(line['id']) - filtered_lines.append(line) - return filtered_lines - - def _convert_str_to_date(self, ref, val): - if isinstance(ref, date) and isinstance(val, str): - return datetime.strptime(val, '%Y-%m-%d').date() - return val - - @classmethod - def _create_tax_report_line(cls, name, report, tag_name=None, parent_line=None, sequence=None, code=None, formula=None): - """ Creates a tax report line - """ - create_vals = { - 'name': name, - 'code': code, - 'report_id': report.id, - 'sequence': sequence, - 'expression_ids': [], - } - if tag_name and formula: - raise UserError("Can't use this helper to create a line with both tags and formula") - if tag_name: - create_vals['expression_ids'].append(Command.create({ - "label": "balance", - "engine": "tax_tags", - "formula": tag_name, - })) - if parent_line: - create_vals['parent_id'] = parent_line.id - if formula: - create_vals['expression_ids'].append(Command.create({ - "label": "balance", - "engine": "aggregation", - "formula": formula, - })) - - return cls.env['account.report.line'].create(create_vals) - - @classmethod - def _get_tag_ids(cls, sign, expressions, company=False): - """ Helper function to define tag ids for taxes """ - return [(6, 0, cls.env['account.account.tag'].search([ - ('applicability', '=', 'taxes'), - ('country_id.code', '=', (company or cls.env.company).account_fiscal_country_id.code), - ('name', 'in', [f"{sign}{f}" for f in expressions.mapped('formula')]), - ]).ids)] - - @classmethod - def _get_basic_line_dict_id_from_report_line(cls, report_line): - """ Computes a full generic id for the provided report line (hence including the one of its parent as prefix), using no markup. - """ - report = report_line.report_id - if report_line.parent_id: - parent_line_id = cls._get_basic_line_dict_id_from_report_line(report_line.parent_id) - return report._get_generic_line_id(report_line._name, report_line.id, parent_line_id=parent_line_id) - - return report._get_generic_line_id(report_line._name, report_line.id) - - @classmethod - def _get_basic_line_dict_id_from_report_line_ref(cls, report_line_xmlid): - """ Same as _get_basic_line_dict_id_from_report_line, but from the line's xmlid, for convenience in the tests. - """ - return cls._get_basic_line_dict_id_from_report_line(cls.env.ref(report_line_xmlid)) - - @classmethod - def _get_audit_params_from_report_line(cls, options, report_line, report_line_dict, **kwargs): - return { - 'report_line_id': report_line.id, - 'calling_line_dict_id': report_line_dict['id'], - 'expression_label': 'balance', - 'column_group_key': next(iter(options['column_groups'])), - **kwargs, - } - - def _report_compare_with_test_file(self, report, xml_file=None, test_xml=None): - report_xml = self.get_xml_tree_from_string(report['file_content']) - if xml_file and not test_xml: - with file_open(f"{self.test_module}/tests/expected_xmls/{xml_file}", 'rb') as fp: - test_xml = fp.read() - test_xml_tree = self.get_xml_tree_from_string(test_xml) - self.assertXmlTreeEqual(report_xml, test_xml_tree) - - @classmethod - def _fill_tax_report_line_external_value(cls, target, amount, date): - cls.env['account.report.external.value'].create({ - 'company_id': cls.company_data['company'].id, - 'target_report_expression_id': cls.env.ref(target).id, - 'name': 'Manual value', - 'date': fields.Date.from_string(date), - 'value': amount, - }) - - def _test_xlsx_file(self, file_content, expected_values): - """ Takes in the binary content of a xlsx file and a dict of expected values. - It will then parse the file in order to compare the values with the expected ones. - The expected values dict format is: - 'row_number': ['cell_1_val', 'cell_2_val', ...] - - :param file_content: The binary content of the xlsx file - :param expected_values: The dict of expected values - """ - if load_workbook is None: - raise unittest.SkipTest("openpyxl not available") - - report_file = io.BytesIO(file_content) - xlsx = load_workbook(filename=report_file, data_only=True) - sheet = xlsx.worksheets[0] - sheet_values = list(sheet.values) - - for row, values in expected_values.items(): - row_values = [v if v is not None else '' for v in sheet_values[row]] - for row_value, expected_value in zip(row_values, values): - self.assertEqual(row_value, expected_value) \ No newline at end of file diff --git a/addons/at_accounting/tests/test_account_asset.py b/addons/at_accounting/tests/test_account_asset.py deleted file mode 100644 index 8d2d622..0000000 --- a/addons/at_accounting/tests/test_account_asset.py +++ /dev/null @@ -1,3037 +0,0 @@ -# -*- coding: utf-8 -*- - -import time - -from dateutil.relativedelta import relativedelta -from odoo import fields, Command -from odoo.exceptions import UserError, MissingError -from odoo.tests import Form, tagged, freeze_time -from odoo.addons.at_accounting.tests.common import TestAccountReportsCommon - - -@freeze_time('2021-07-01') -@tagged('post_install', '-at_install') -class TestAccountAsset(TestAccountReportsCommon): - - @classmethod - def setUpClass(cls): - super(TestAccountAsset, cls).setUpClass() - today = fields.Date.today() - cls.truck = cls.env['account.asset'].create({ - 'account_asset_id': cls.company_data['default_account_assets'].id, - 'account_depreciation_id': cls.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': cls.company_data['default_account_expense'].id, - 'journal_id': cls.company_data['default_journal_misc'].id, - 'name': 'truck', - 'acquisition_date': today + relativedelta(years=-6, months=-6), - 'original_value': 10000, - 'salvage_value': 2500, - 'method_number': 10, - 'method_period': '12', - 'method': 'linear', - }) - cls.truck.validate() - cls.env['account.move']._autopost_draft_entries() - - cls.account_asset_model_fixedassets = cls.env['account.asset'].create({ - 'account_depreciation_id': cls.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': cls.company_data['default_account_expense'].id, - 'account_asset_id': cls.company_data['default_account_assets'].id, - 'journal_id': cls.company_data['default_journal_purchase'].id, - 'name': 'Hardware - 3 Years', - 'method_number': 3, - 'method_period': '12', - 'state': 'model', - }) - - - cls.closing_invoice = cls.env['account.move'].create({ - 'move_type': 'out_invoice', - 'invoice_line_ids': [(0, 0, {'price_unit': 100})] - }) - - cls.env.company.loss_account_id = cls.company_data['default_account_expense'].copy() - cls.env.company.gain_account_id = cls.company_data['default_account_revenue'].copy() - cls.assert_counterpart_account_id = cls.company_data['default_account_expense'].copy().id - - cls.env.user.groups_id += cls.env.ref('analytic.group_analytic_accounting') - analytic_plan = cls.env['account.analytic.plan'].create({ - 'name': "Default Plan", - }) - cls.analytic_account = cls.env['account.analytic.account'].create({ - 'name': "Test Account", - 'plan_id': analytic_plan.id, - }) - - def update_form_values(self, asset_form): - for i in range(len(asset_form.depreciation_move_ids)): - with asset_form.depreciation_move_ids.edit(i) as line_edit: - line_edit.asset_remaining_value - - def test_account_asset_no_tax(self): - self.account_asset_model_fixedassets.account_depreciation_expense_id.tax_ids = self.tax_purchase_a - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 2000.0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - }) - CEO_car._onchange_model_id() - CEO_car.prorata_computation_type = 'constant_periods' - CEO_car.method_number = 5 - - # In order to test the process of Account Asset, I perform a action to confirm Account Asset. - CEO_car.validate() - - self.assertFalse(any(CEO_car.depreciation_move_ids.line_ids.mapped('tax_line_id'))) - - def test_00_account_asset(self): - """Test the lifecycle of an asset""" - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 2000.0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - }) - CEO_car._onchange_model_id() - CEO_car.prorata_computation_type = 'constant_periods' - CEO_car.method_number = 5 - - # In order to test the process of Account Asset, I perform a action to confirm Account Asset. - CEO_car.validate() - - # TOFIX: the method validate() makes the field account.asset.asset_type - # dirty, but this field has to be flushed in CEO_car's environment. - # This is because the field 'asset_type' is stored, computed and - # context-dependent, which explains why its value must be retrieved - # from the right environment. - CEO_car.flush_recordset() - - # I check Asset is now in Open state. - self.assertEqual(CEO_car.state, 'open', - 'Asset should be in Open state') - - # I compute depreciation lines for asset of CEOs Car. - self.assertEqual(CEO_car.method_number + 1, len(CEO_car.depreciation_move_ids), - 'Depreciation lines not created correctly') - - # Check that auto_post is set on the entries, in the future, and we cannot post them. - self.assertTrue(all(CEO_car.depreciation_move_ids.mapped(lambda m: m.auto_post != 'no'))) - with self.assertRaises(UserError): - CEO_car.depreciation_move_ids.action_post() - - # I Check that After creating all the moves of depreciation lines the state "Running". - CEO_car.depreciation_move_ids.write({'auto_post': 'no'}) - CEO_car.depreciation_move_ids.action_post() - self.assertEqual(CEO_car.state, 'open', - 'State of asset should be runing') - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 2000, - 'value_residual': 0, - 'salvage_value': 2000, - }]) - - self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: l.date), [{ - 'amount_total': 1000, - 'asset_remaining_value': 9000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 7000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 5000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 3000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 1000, - }, { - 'amount_total': 1000, - 'asset_remaining_value': 0, - }]) - - # Revert posted entries in order to be able to close - CEO_car.depreciation_move_ids._reverse_moves(cancel=True) - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 12000, - 'value_residual': 10000, - 'salvage_value': 2000, - }]) - reversed_moves_values = [{ - 'amount_total': 1000, - 'asset_remaining_value': 11000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 13000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 15000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 17000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 19000, - 'state': 'posted', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 20000, - 'state': 'posted', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 19000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 17000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 15000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 13000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 11000, - 'state': 'posted', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 10000, - 'state': 'posted', - }, { - 'amount_total': 10000, - 'asset_remaining_value': 0, - 'state': 'draft', - }] - - self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: l.date), reversed_moves_values) - self.assertRecordValues(CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft').line_ids, [{ - 'debit': 0, - 'credit': 10000, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 10000, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_expense_id.id, - }]) - - # Close - CEO_car.set_to_close(self.closing_invoice.invoice_line_ids, date=fields.Date.today() + relativedelta(days=-1)) - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 12000, - 'value_residual': 10000, - 'salvage_value': 2000, - }]) - self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ - 'amount_total': 12000, - 'asset_remaining_value': 0, - 'state': 'draft', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 1000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 3000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 5000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 7000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 9000, - 'state': 'posted', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 10000, - 'state': 'posted', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 9000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 7000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 5000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 3000, - 'state': 'posted', - }, { - 'amount_total': 2000, - 'asset_remaining_value': 1000, - 'state': 'posted', - }, { - 'amount_total': 1000, - 'asset_remaining_value': 0, - 'state': 'posted', - }]) - closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 12000, - 'account_id': CEO_car.account_asset_id.id, - }, { - 'debit': 0, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 100, - 'credit': 0, - 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, - }, { - 'debit': 11900, - 'credit': 0, - 'account_id': self.env.company.loss_account_id.id, - }]) - closing_move.action_post() - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 2000, - }]) - - def test_00_account_asset_new(self): - """Test the lifecycle of an asset""" - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 2000.0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - }) - CEO_car._onchange_model_id() - CEO_car.prorata_computation_type = 'constant_periods' - CEO_car.method_number = 5 - - # In order to test the process of Account Asset, I perform a action to confirm Account Asset. - CEO_car.validate() - - # I Check that After creating all the moves of depreciation lines the state of the asset is "Running". - CEO_car.depreciation_move_ids.write({'auto_post': 'no'}) - CEO_car.depreciation_move_ids.action_post() - self.assertEqual(CEO_car.state, 'open', - 'State of the asset should be running') - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 2000, - 'value_residual': 0, - 'salvage_value': 2000, - }]) - self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: l.date), [{ - 'amount_total': 1000, - 'asset_remaining_value': 9000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 7000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 5000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 3000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': 1000, - }, { - 'amount_total': 1000, - 'asset_remaining_value': 0, - }]) - - # Close - CEO_car.set_to_close(self.closing_invoice.invoice_line_ids, date=fields.Date.today() + relativedelta(days=30)) - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 12000, - 'value_residual': 10000, - 'salvage_value': 2000, - }]) - self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ - 'amount_total': 166.67, - 'asset_remaining_value': 9833.33, - 'state': 'draft', - }, { - 'amount_total': 12000, - 'asset_remaining_value': 0, - 'state': 'draft', - }]) - closing_move = max(CEO_car.depreciation_move_ids, key=lambda m: (m.date, m.id)) - self.assertRecordValues(closing_move, [{ - 'date': fields.Date.today() + relativedelta(days=30), - }]) - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 12000, - 'account_id': CEO_car.account_asset_id.id, - }, { - 'debit': 166.67, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 100, - 'credit': 0, - 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, - }, { - 'debit': 11733.33, - 'credit': 0, - 'account_id': self.env.company.loss_account_id.id, - }]) - CEO_car.depreciation_move_ids.auto_post = 'no' - CEO_car.depreciation_move_ids.action_post() - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 2000, - 'state': 'close', - }]) - - def test_01_account_asset(self): - """ Test if an an asset is created when an invoice is validated with an - item on an account for generating entries. - """ - account_asset_model = self.env['account.asset'].create({ - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Typical car - 3 Years', - 'method_number': 3, - 'method_period': '12', - 'prorata_computation_type': 'daily_computation', - 'state': 'model', - }) - - # The account needs a default model for the invoice to validate the revenue - self.company_data['default_account_assets'].create_asset = 'validate' - self.company_data['default_account_assets'].asset_model_ids = account_asset_model - - invoice = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, - 'invoice_date': '2020-12-31', - 'invoice_line_ids': [(0, 0, { - 'name': 'Very little red car', - 'account_id': self.company_data['default_account_assets'].id, - 'price_unit': 450, - 'quantity': 1, - })], - }) - invoice.action_post() - - asset = invoice.asset_ids - self.assertEqual(len(asset), 1, 'One and only one asset should have been created from invoice.') - - self.assertTrue(asset.state == 'open', - 'Asset should be in Open state') - first_invoice_line = invoice.invoice_line_ids[0] - self.assertEqual(asset.original_value, first_invoice_line.price_subtotal, - 'Asset value is not same as invoice line.') - - # I check data in move line and depreciation line. - first_depreciation_line = asset.depreciation_move_ids.sorted(lambda r: r.id)[0] - self.assertAlmostEqual(first_depreciation_line.asset_remaining_value, asset.original_value - first_depreciation_line.amount_total, - msg='Remaining value is incorrect.') - self.assertAlmostEqual(first_depreciation_line.asset_depreciated_value, first_depreciation_line.amount_total, - msg='Depreciated value is incorrect.') - - # I check next installment date. - last_depreciation_date = first_depreciation_line.date - installment_date = last_depreciation_date + relativedelta(months=+int(asset.method_period)) - self.assertEqual(asset.depreciation_move_ids.sorted(lambda r: r.id)[1].date, installment_date, - 'Installment date is incorrect.') - - def test_02_account_asset(self): - """Test the lifecycle of an asset""" - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 2000.0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - 'acquisition_date': '2010-01-31', - 'already_depreciated_amount_import': 10000.0, - }) - CEO_car._onchange_model_id() - - CEO_car.validate() - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 2000, - 'value_residual': 0, - 'salvage_value': 2000, - }]) - self.assertFalse(CEO_car.depreciation_move_ids) - CEO_car.set_to_close(self.closing_invoice.invoice_line_ids) - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 2000, - 'value_residual': 0, - 'salvage_value': 2000, - }]) - closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 12000, - 'account_id': CEO_car.account_asset_id.id, - }, { - 'debit': 10000, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 100, - 'credit': 0, - 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, - }, { - 'debit': 1900, - 'credit': 0, - 'account_id': CEO_car.company_id.loss_account_id.id, - }]) - closing_move.action_post() - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 2000, - }]) - - def test_03_account_asset(self): - """Test the salvage of an asset with gain""" - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - 'acquisition_date': '2010-01-31', - 'already_depreciated_amount_import': 12000.0, - }) - CEO_car._onchange_model_id() - - CEO_car.validate() - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 0, - }]) - self.assertFalse(CEO_car.depreciation_move_ids) - CEO_car.set_to_close(self.closing_invoice.invoice_line_ids) - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 0, - }]) - closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 12000, - 'account_id': CEO_car.account_asset_id.id, - }, { - 'debit': 12000, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 100, - 'credit': 0, - 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, - }, { - 'debit': 0, - 'credit': 100, - 'account_id': CEO_car.company_id.gain_account_id.id, - }]) - closing_move.action_post() - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 0, - }]) - - def test_04_account_asset(self): - """Test the salvage of an asset with gain""" - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 800.0, - 'model_id': self.account_asset_model_fixedassets.id, - 'acquisition_date': '2021-01-01', - 'already_depreciated_amount_import': 300.0, - }) - CEO_car._onchange_model_id() - CEO_car.method_number = 5 - - CEO_car.validate() - self.assertRecordValues(CEO_car, [{ - 'original_value': 800, - 'book_value': 500, - 'value_residual': 500, - 'salvage_value': 0, - }]) - self.assertEqual(len(CEO_car.depreciation_move_ids), 4) - CEO_car.set_to_close(self.closing_invoice.invoice_line_ids, date=fields.Date.today() + relativedelta(months=-6, days=-1)) - self.assertRecordValues(CEO_car, [{ - 'original_value': 800, - 'book_value': 500, - 'value_residual': 500, - 'salvage_value': 0, - }]) - closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 800, - 'account_id': CEO_car.account_asset_id.id, - }, { - 'debit': 300, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 100, - 'credit': 0, - 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, - }, { - 'debit': 400, - 'credit': 0, - 'account_id': CEO_car.company_id.loss_account_id.id, - }]) - closing_move.action_post() - self.assertRecordValues(CEO_car, [{ - 'original_value': 800, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 0, - }]) - - def test_05_account_asset(self): - """Test the salvage of an asset with gain""" - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 1000.0, - 'model_id': self.account_asset_model_fixedassets.id, - 'acquisition_date': '2020-01-01', - }) - CEO_car._onchange_model_id() - CEO_car.method_number = 5 - CEO_car.account_depreciation_id = CEO_car.account_asset_id - - CEO_car.validate() - self.assertRecordValues(CEO_car, [{ - 'original_value': 1000, - 'book_value': 800, - 'value_residual': 800, - 'salvage_value': 0, - }]) - self.assertEqual(len(CEO_car.depreciation_move_ids), 5) - CEO_car.set_to_close(self.env['account.move.line'], date=fields.Date.today() + relativedelta(days=-1)) - self.assertRecordValues(CEO_car, [{ - 'original_value': 1000, - 'book_value': 700, - 'value_residual': 700, - 'salvage_value': 0, - }]) - closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 1000, - 'account_id': CEO_car.account_asset_id.id, - }, { - 'debit': 300, - 'credit': 0, - 'account_id': CEO_car.account_depreciation_id.id, - }, { - 'debit': 700, - 'credit': 0, - 'account_id': CEO_car.company_id.loss_account_id.id, - }]) - closing_move.action_post() - self.assertRecordValues(CEO_car, [{ - 'original_value': 1000, - 'book_value': 0, - 'value_residual': 0, - 'salvage_value': 0, - }]) - - def test_06_account_asset(self): - """Test the correct computation of asset amounts""" - asset_account = self.env['account.account'].create({ - "name": "test_06_account_asset", - "code": "test.06.account.asset", - "account_type": 'asset_non_current', - "create_asset": "no", - "multiple_assets_per_line": True, - }) - - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 0, - 'state': 'draft', - 'method_period': '12', - 'method_number': 4, - 'name': "CEO's Car", - 'original_value': 1000.0, - 'acquisition_date': fields.Date.today() - relativedelta(years=3), - 'account_asset_id': asset_account.id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': asset_account.id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'prorata_computation_type': 'none', - }) - - CEO_car.validate() - posted_entries = len(CEO_car.depreciation_move_ids.filtered(lambda x: x.state == 'posted')) - self.assertEqual(posted_entries, 3) - - self.assertRecordValues(CEO_car, [{ - 'original_value': 1000, - 'book_value': 250, - 'value_residual': 250, - 'salvage_value': 0, - }]) - - def test_account_asset_cancel(self): - """Test the cancellation of an asset""" - today = fields.Date.today() - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 2000.0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - 'acquisition_date': today + relativedelta(years=-3, month=1, day=1), - }) - CEO_car._onchange_model_id() - CEO_car.method_number = 5 - CEO_car.validate() - - self.assertRecordValues(CEO_car, [{ - 'original_value': 12000, - 'book_value': 6000, - 'value_residual': 4000, - 'salvage_value': 2000, - }]) - CEO_car.set_to_cancelled() - - self.assertEqual(CEO_car.state, 'cancelled') - self.assertFalse(CEO_car.depreciation_move_ids) - - # Hashed journals should reverse entries instead of deleting - Hashed_car = CEO_car.copy() - Hashed_car.write({ - 'original_value': 12000.0, - 'method_number': 5, - 'name': "Hashed Car", - 'journal_id': CEO_car.journal_id.copy().id, - 'acquisition_date': today + relativedelta(years=-3, month=1, day=1), - }) - Hashed_car.journal_id.restrict_mode_hash_table = True - Hashed_car.validate() - self.assertTrue(False not in Hashed_car.depreciation_move_ids[:3].mapped('inalterable_hash')) - - for i in range(0, 4): - self.assertFalse(Hashed_car.depreciation_move_ids[i].reversal_move_ids) - - Hashed_car.set_to_cancelled() - - self.assertEqual(Hashed_car.state, 'cancelled') - for i in range(0, 2): - self.assertTrue(Hashed_car.depreciation_move_ids[i].reversal_move_ids.id > 0 or Hashed_car.depreciation_move_ids[i].reversed_entry_id.id > 0) - - # The depreciation schedule report should not contain cancelled assets - report = self.env.ref('at_accounting.assets_report') - options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - assets_in_report = [x['name'] for x in lines[:-1]] - - self.assertNotIn(CEO_car.name, assets_in_report) - self.assertNotIn(Hashed_car.name, assets_in_report) - - # When a lock date is applied, only the moves before the date are reversed, others are deleted - Locked_car = CEO_car.copy() - Locked_car.write({ - 'original_value': 12000.0, - 'method_number': 10, - 'name': "Locked Car", - 'acquisition_date': today + relativedelta(years=-3, month=1, day=1), - }) - Locked_car.validate() - Locked_car.company_id.fiscalyear_lock_date = today + relativedelta(years=-1) - - self.assertEqual(len(Locked_car.depreciation_move_ids), 10) - Locked_car.set_to_cancelled() - self.assertRecordValues(Locked_car, [{ - 'state': 'cancelled', - 'book_value': 12000.0, - 'value_residual': 10000, - 'salvage_value': 2000, - }]) - self.assertEqual(len(Locked_car.depreciation_move_ids), 4) - for depreciation in Locked_car.depreciation_move_ids: - self.assertTrue(depreciation.reversal_move_ids or depreciation.reversed_entry_id) - - - def test_asset_form(self): - """Test the form view of assets""" - asset_form = Form(self.env['account.asset']) - asset_form.name = "Test Asset" - asset_form.original_value = 10000 - asset_form.account_depreciation_id = self.company_data['default_account_assets'] - asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] - asset_form.journal_id = self.company_data['default_journal_misc'] - asset_form.prorata_computation_type = 'none' - asset = asset_form.save() - asset.validate() - - # Test that the depreciations are created upon validation of the asset according to the default values - self.assertEqual(len(asset.depreciation_move_ids), 5) - for move in asset.depreciation_move_ids: - self.assertEqual(move.amount_total, 2000) - - # Test that we cannot validate an asset with non zero remaining value of the last depreciation line - asset_form = Form(asset) - with self.assertRaises(UserError): - with self.cr.savepoint(): - with asset_form.depreciation_move_ids.edit(4) as line_edit: - line_edit.depreciation_value = 1000.0 - asset_form.save() - - # ... but we can with a zero remaining value on the last line. - asset_form = Form(asset) - with asset_form.depreciation_move_ids.edit(4) as line_edit: - line_edit.depreciation_value = 1000.0 - with asset_form.depreciation_move_ids.edit(3) as line_edit: - line_edit.depreciation_value = 3000.0 - self.update_form_values(asset_form) - asset_form.save() - - def test_asset_from_entry_line_form(self): - """Test that the asset is correcly created from a move line""" - - move_ids = self.env['account.move'].create([{ - 'ref': 'line1', - 'line_ids': [ - (0, 0, { - 'account_id': self.company_data['default_account_expense'].id, - 'debit': 300, - 'name': 'Furniture', - }), - (0, 0, { - 'account_id': self.company_data['default_account_assets'].id, - 'credit': 300, - }), - ] - }, { - 'ref': 'line2', - 'line_ids': [ - (0, 0, { - 'account_id': self.company_data['default_account_expense'].id, - 'debit': 600, - 'name': 'Furniture too', - }), - (0, 0, { - 'account_id': self.company_data['default_account_assets'].id, - 'credit': 600, - }), - ] - }, - ]) - move_ids.action_post() - move_line_ids = move_ids.mapped('line_ids').filtered(lambda x: x.debit) - - asset_form = Form(self.env['account.asset'].with_context(default_original_move_line_ids=move_line_ids.ids)) - asset_form.original_move_line_ids = move_line_ids - asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] - - asset = asset_form.save() - self.assertEqual(asset.value_residual, 900.0) - self.assertIn(asset.name, ['Furniture', 'Furniture too']) - self.assertEqual(asset.journal_id.type, 'general') - self.assertEqual(asset.account_asset_id, self.company_data['default_account_expense']) - self.assertEqual(asset.account_depreciation_id, self.company_data['default_account_expense']) - self.assertEqual(asset.account_depreciation_expense_id, self.company_data['default_account_expense']) - self.assertEqual(asset.acquisition_date, min(move_ids.mapped('date'))) - - def test_asset_from_bill_move_line_form(self): - """Test that the asset is correcly created from a move line""" - - move_ids = self.env['account.move'].create([{ - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'ref': 'line1', - 'date': '2020-06-01', - 'invoice_date': '2020-06-15', - 'invoice_line_ids': [ - Command.create({ - 'account_id': self.company_data['default_account_expense'].id, - 'price_unit': 300, - 'name': 'Furniture', - 'tax_ids': [], - }), - ] - }, { - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'ref': 'line2', - 'date': '2020-06-01', - 'invoice_date': '2020-06-14', - 'invoice_line_ids': [ - Command.create({ - 'account_id': self.company_data['default_account_expense'].id, - 'price_unit': 600, - 'name': 'Furniture too', - 'tax_ids': [], - }), - ] - }, - ]) - move_ids.action_post() - move_line_ids = move_ids.mapped('line_ids').filtered(lambda x: x.debit) - - asset_form = Form(self.env['account.asset'].with_context(default_original_move_line_ids=move_line_ids.ids)) - asset_form.original_move_line_ids = move_line_ids - asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] - - asset = asset_form.save() - self.assertEqual(asset.value_residual, 900.0) - self.assertRecordValues(asset, [{ - 'name': 'Furniture', - 'account_asset_id': self.company_data['default_account_expense'].id, - 'account_depreciation_id': self.company_data['default_account_expense'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'acquisition_date': min(move_ids.mapped('invoice_date')), - }]) - - def test_asset_from_bill_move_line_form_multicurrency(self): - """Test that the asset is correcly created from a move line using a foreign currency""" - - asset_account = self.company_data['default_account_assets'] - non_deductible_tax = self.env['account.tax'].create({ - 'name': 'Non-deductible Tax', - 'amount': 21, - 'amount_type': 'percent', - 'type_tax_use': 'purchase', - 'invoice_repartition_line_ids': [ - Command.create({'repartition_type': 'base'}), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': False - }), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': True - }), - ], - 'refund_repartition_line_ids': [ - Command.create({'repartition_type': 'base'}), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': False - }), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': True - }), - ], - }) - asset_account.tax_ids = non_deductible_tax - - asset_account.create_asset = 'no' - asset_account.multiple_assets_per_line = False - - vendor_bill = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'currency_id': self.other_currency.id, - 'invoice_date': '2020-01-01', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [ - Command.create({ - 'account_id': asset_account.id, - 'currency_id': self.other_currency.id, - 'name': 'Asus Laptop', - 'price_unit': 1000.0, - 'quantity': 1, - 'tax_ids': [Command.set(non_deductible_tax.ids)] - }), - Command.create({ - 'account_id': asset_account.id, - 'currency_id': self.other_currency.id, - 'name': 'Lenovo Laptop', - 'price_unit': 500.0, - 'quantity': 1, - 'tax_ids': [Command.set(non_deductible_tax.ids)] - }), - ], - }) - vendor_bill.action_post() - self.env.flush_all() - - move_line_ids = vendor_bill.mapped('line_ids').filtered(lambda x: 'Laptop' in x.name) - asset_form = Form(self.env['account.asset'].with_context( - default_original_move_line_ids=move_line_ids.ids, - asset_type='purchase' - )) - asset_form.original_move_line_ids = move_line_ids - asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] - - new_assets = asset_form.save() - self.assertEqual(len(new_assets), 1) - self.assertEqual(new_assets.original_value, 828.75) - self.assertEqual(new_assets.non_deductible_tax_value, 78.75) - - def test_asset_modify_value_00(self): - """Test the values of the asset and value increase 'assets' after a - modification of residual and/or salvage values. - Increase the residual value, increase the salvage value""" - self.assertEqual(self.truck.value_residual, 3000) - self.assertEqual(self.truck.salvage_value, 2500) - - self.env['asset.modify'].create({ - 'name': 'New beautiful sticker :D', - 'asset_id': self.truck.id, - 'value_residual': 4000, - 'salvage_value': 3000, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - "account_asset_counterpart_id": self.assert_counterpart_account_id, - "account_depreciation_id": self.company_data['default_account_assets'].id, - }).modify() - self.assertEqual(self.truck.value_residual, 3000) - self.assertEqual(self.truck.salvage_value, 2500) - self.assertEqual(self.truck.children_ids.value_residual, 1000) - self.assertEqual(self.truck.children_ids.salvage_value, 500) - self.assertEqual(self.truck.account_depreciation_id.id, self.company_data['default_account_assets'].id) - - def test_asset_modify_value_01(self): - "Decrease the residual value, decrease the salvage value" - self.env['asset.modify'].create({ - 'name': "Accident :'(", - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'asset_id': self.truck.id, - 'value_residual': 1000, - 'salvage_value': 2000, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.assertEqual(self.truck.value_residual, 1000) - self.assertEqual(self.truck.salvage_value, 2000) - self.assertEqual(self.truck.children_ids.value_residual, 0) - self.assertEqual(self.truck.children_ids.salvage_value, 0) - self.assertEqual(max(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted'), key=lambda m: (m.date, m.id)).amount_total, 2500) - - def test_asset_modify_value_02(self): - "Decrease the residual value, increase the salvage value; same book value" - self.env['asset.modify'].create({ - 'name': "Don't wanna depreciate all of it", - 'asset_id': self.truck.id, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'value_residual': 1000, - 'salvage_value': 4500, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.assertEqual(self.truck.value_residual, 1000) - self.assertEqual(self.truck.salvage_value, 4500) - self.assertEqual(self.truck.children_ids.value_residual, 0) - self.assertEqual(self.truck.children_ids.salvage_value, 0) - - def test_asset_modify_value_03(self): - "Decrease the residual value, increase the salvage value; increase of book value" - self.env['asset.modify'].create({ - 'name': "Some aliens did something to my truck", - 'asset_id': self.truck.id, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'value_residual': 1000, - 'salvage_value': 6000, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.assertEqual(self.truck.value_residual, 1000) - self.assertEqual(self.truck.salvage_value, 4500) - self.assertEqual(self.truck.children_ids.value_residual, 0) - self.assertEqual(self.truck.children_ids.salvage_value, 1500) - - def test_asset_modify_value_04(self): - "Increase the residual value, decrease the salvage value; increase of book value" - self.env['asset.modify'].create({ - 'name': 'GODZILA IS REAL!', - 'asset_id': self.truck.id, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'value_residual': 4000, - 'salvage_value': 2000, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.assertEqual(self.truck.value_residual, 3500) - self.assertEqual(self.truck.salvage_value, 2000) - self.assertEqual(self.truck.children_ids.value_residual, 500) - self.assertEqual(self.truck.children_ids.salvage_value, 0) - - def test_asset_modify_report(self): - """Test the asset value modification flows""" - # PY + - Final PY + - Final Bookvalue - # -6 0 10000 0 10000 0 750 0 750 9250 - # -5 10000 0 0 10000 750 750 0 1500 8500 - # -4 10000 0 0 10000 1500 750 0 2250 7750 - # -3 10000 0 0 10000 2250 750 0 3000 7000 - # -2 10000 0 0 10000 3000 750 0 3750 6250 - # -1 10000 0 0 10000 3750 750 0 4500 5500 - # 0 10000 0 0 10000 4500 750 0 5250 4750 <-- today - # 1 10000 0 0 10000 5250 750 0 6000 4000 - # 2 10000 0 0 10000 6000 750 0 6750 3250 - # 3 10000 0 0 10000 6750 750 0 7500 2500 - - today = fields.Date.today() - - report = self.env.ref('at_accounting.assets_report') - # TEST REPORT - # look at all period, with unposted entries - options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - self.assertListEqual([ 0.0, 10000.0, 0.0, 10000.0, 0.0, 7500.0, 0.0, 7500.0, 2500.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - # look at all period, without unposted entries - options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': False}}) - self.assertListEqual([ 0.0, 10000.0, 0.0, 10000.0, 0.0, 4500.0, 0.0, 4500.0, 5500.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - # look only at this period - options = self._generate_options(report, today + relativedelta(years=0, month=1, day=1), today + relativedelta(years=0, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - self.assertListEqual([10000.0, 0.0, 0.0, 10000.0, 4500.0, 750.0, 0.0, 5250.0, 4750.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - # test value increase - # PY + - Final PY + - Final Bookvalue - # -6 0 10000 0 10000 750 0 750 9250 - # -5 10000 0 0 10000 750 750 0 1500 8500 - # -4 10000 0 0 10000 1500 750 0 2250 7750 - # -3 10000 0 0 10000 2250 750 0 3000 7000 - # -2 10000 0 0 10000 3000 750 0 3750 6250 - # -1 10000 1500 0 10000 3750 950 0 4700 6800 - # 0 10000 0 0 11500 4700 950 0 5650 5850 <-- today - # 1 11500 0 0 11500 5650 950 0 6600 4900 - # 2 11500 0 0 11500 6600 950 0 7550 3950 - # 3 11500 0 0 11500 7550 950 0 8500 3000 - self.assertEqual(self.truck.value_residual, 3000) - self.assertEqual(self.truck.salvage_value, 2500) - self.env['asset.modify'].create({ - 'name': 'New beautiful sticker :D', - 'asset_id': self.truck.id, - 'date': fields.Date.today() + relativedelta(years=-1, months=-6, days=-1), - 'value_residual': 4750, - 'salvage_value': 3000, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - - self.assertEqual(self.truck.value_residual + sum(self.truck.children_ids.mapped('value_residual')), 3800) - self.assertEqual(self.truck.salvage_value + sum(self.truck.children_ids.mapped('salvage_value')), 3000) - - # look at all period, with unposted entries - options = self._generate_options(report, today + relativedelta(years=-6, months=-6), today + relativedelta(years=+4, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - self.assertListEqual([0.0, 11500.0, 0.0, 11500.0, 0.0, 8500.0, 0.0, 8500.0, 3000.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - self.assertEqual('10 y', lines[1]['columns'][3]['name'], 'Depreciation Rate = 10%') - - # look only at this period - options = self._generate_options(report, today + relativedelta(years=0, month=1, day=1), today + relativedelta(years=0, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - self.assertListEqual([11500.0, 0.0, 0.0, 11500.0, 4700.0, 950.0, 0.0, 5650.0, 5850.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - # test value decrease - self.env['asset.modify'].create({ - 'name': "Huge scratch on beautiful sticker :'( It is ruined", - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'asset_id': self.truck.children_ids.id, - 'value_residual': 0, - 'salvage_value': 500, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.env['asset.modify'].create({ - 'name': "Huge scratch on beautiful sticker :'( It went through...", - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'asset_id': self.truck.id, - 'value_residual': 1000, - 'salvage_value': 2500, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.assertEqual(self.truck.value_residual + sum(self.truck.children_ids.mapped('value_residual')), 1000) - self.assertEqual(self.truck.salvage_value + sum(self.truck.children_ids.mapped('salvage_value')), 3000) - - # look at all period, with unposted entries - options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - self.assertListEqual([0.0, 11500.0, 0.0, 11500.0, 0.0, 8500.0, 0.0, 8500.0, 3000.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - # look only at previous period - options = self._generate_options(report, today + relativedelta(years=-1, month=1, day=1), today + relativedelta(years=-1, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - self.assertListEqual([10000.0, 1500.0, 0.0, 11500.0, 3750.0, 3750.0, 0.0, 7500.0, 4000.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - def test_asset_pause_resume(self): - """Test that depreciation remains the same after a pause and resume at a later date""" - today = fields.Date.today() - self.assertEqual(len(self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'draft')), 4) - self.env['asset.modify'].create({ - 'date': fields.Date.today() + relativedelta(days=-1), - 'asset_id': self.truck.id, - }).pause() - self.assertEqual(len(self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'draft')), 0) - with freeze_time(today) as frozen_time: - frozen_time.move_to(today + relativedelta(years=1)) - self.env['asset.modify'].with_context(resume_after_pause=True).create({ - 'asset_id': self.truck.id, - }).modify() - self.assertEqual(len(self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'posted')), 7) - self.assertEqual( - self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'draft').mapped('amount_total'), - [375.0, 750.0, 750.0, 750.0]) - - def test_asset_modify_sell_profit(self): - """Test that a credit is realised in the gain account when selling an asset for a sum greater than book value""" - closing_invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'invoice_line_ids': [(0, 0, {'price_unit': self.truck.book_value + 100})] - }) - self.env['asset.modify'].create({ - 'asset_id': self.truck.id, - 'invoice_line_ids': closing_invoice.invoice_line_ids, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'modify_action': 'sell', - }).sell_dispose() - - closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'ref': 'truck: Sale', - 'debit': 0, - 'credit': 10000, - 'account_id': self.truck.account_asset_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 4500, - 'credit': 0, - 'account_id': self.truck.account_depreciation_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 5600, - 'credit': 0, - 'account_id': closing_invoice.invoice_line_ids.account_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 0, - 'credit': 100, - 'account_id': self.env.company.gain_account_id.id, - }]) - - def test_asset_modify_sell_loss(self): - """Test that a debit is realised in the loss account when selling an asset for a sum less than book value""" - closing_invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'invoice_line_ids': [(0, 0, {'price_unit': self.truck.book_value - 100})] - }) - self.env['asset.modify'].create({ - 'asset_id': self.truck.id, - 'invoice_line_ids': closing_invoice.invoice_line_ids, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'modify_action': 'sell', - }).sell_dispose() - closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - - self.assertRecordValues(closing_move.line_ids, [{ - 'ref': 'truck: Sale', - 'debit': 0, - 'credit': 10000, - 'account_id': self.truck.account_asset_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 4500, - 'credit': 0, - 'account_id': self.truck.account_depreciation_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 5400, - 'credit': 0, - 'account_id': closing_invoice.invoice_line_ids.account_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 100, - 'credit': 0, - 'account_id': self.env.company.loss_account_id.id, - }]) - - def test_asset_sale_same_account_as_invoice(self): - """Test the sale of an asset with an invoice that has the same account as the Depreciation Account""" - closing_invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'invoice_line_ids': [ - Command.create({ - 'account_id': self.truck.account_depreciation_id.id, - 'price_unit': self.truck.book_value - 100 - }) - ] - }) - self.env['asset.modify'].create({ - 'asset_id': self.truck.id, - 'invoice_line_ids': closing_invoice.invoice_line_ids, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'modify_action': 'sell', - }).sell_dispose() - closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'ref': 'truck: Sale', - 'debit': 0, - 'credit': 10000, - 'account_id': self.truck.account_asset_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 4500, - 'credit': 0, - 'account_id': self.truck.account_depreciation_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 5400, - 'credit': 0, - 'account_id': closing_invoice.invoice_line_ids.account_id.id, - }, { - 'ref': 'truck: Sale', - 'debit': 100, - 'credit': 0, - 'account_id': self.env.company.loss_account_id.id, - }]) - - self.assertEqual(closing_move.depreciation_value, 3000, "Should be the remaining amount before the sale") - - def test_asset_modify_dispose(self): - """Test the loss of the remaining book_value when an asset is disposed using the wizard""" - self.env['asset.modify'].create({ - 'asset_id': self.truck.id, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'modify_action': 'dispose', - }).sell_dispose() - closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - self.assertRecordValues(closing_move.line_ids, [{ - 'ref': 'truck: Disposal', - 'debit': 0, - 'credit': 10000, - 'account_id': self.truck.account_asset_id.id, - }, { - 'ref': 'truck: Disposal', - 'debit': 4500, - 'credit': 0, - 'account_id': self.truck.account_depreciation_id.id, - }, { - 'ref': 'truck: Disposal', - 'debit': 5500, - 'credit': 0, - 'account_id': self.env.company.loss_account_id.id, - }]) - - def test_asset_reverse_depreciation(self): - """Test the reversal of a depreciation move""" - - self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted').mapped('depreciation_value')), 4500) - self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'draft').mapped('depreciation_value')), 3000) - self.assertEqual(max(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted'), key=lambda m: m.date).asset_remaining_value, 3000) - - report = self.env.ref('at_accounting.assets_report') - today = fields.Date.today() - - move_to_reverse = self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted').sorted(lambda m: m.date)[-1] - reversed_move = move_to_reverse._reverse_moves() - - # Check that the depreciation has been reported on the next move - min_date_draft = min(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'draft' and m.date > reversed_move.date), key=lambda m: m.date) - self.assertEqual(move_to_reverse.asset_remaining_value - min_date_draft.depreciation_value - reversed_move.depreciation_value, min_date_draft.asset_remaining_value) - self.assertEqual(move_to_reverse.asset_depreciated_value + min_date_draft.depreciation_value + reversed_move.depreciation_value, min_date_draft.asset_depreciated_value) - - # The amount is still there, it only has been reversed. But it has been added on the next draft move to complete the depreciation table - self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted').mapped('depreciation_value')), 4500) - self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'draft').mapped('depreciation_value')), 3000) - - # Check that the table shows fully depreciated at the end - self.assertEqual(max(self.truck.depreciation_move_ids, key=lambda m: m.date).asset_remaining_value, 0) - self.assertEqual(max(self.truck.depreciation_move_ids, key=lambda m: m.date).asset_depreciated_value, 7500) - - reversed_move.action_post() - - options = self._generate_options(report, today + relativedelta(years=0, month=7, day=1), today + relativedelta(years=0, month=7, day=31)) - lines = report._get_lines({**options, 'unfold_all': False, 'all_entries': True}) - # We take the reversal entry into account - self.assertListEqual([10000.0, 0.0, 0.0, 10000.0, 4500.0, -750.0, 0.0, 3750.0, 6250.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - options = self._generate_options(report, today + relativedelta(years=0, month=1, day=1), today + relativedelta(years=0, month=12, day=31)) - lines = report._get_lines({**options, 'unfold_all': False, 'all_entries': True}) - # With the report on the next entry, we get a normal depreciation amount for the year - self.assertListEqual([10000.0, 0.0, 0.0, 10000.0, 4500.0, 750.0, 0.0, 5250.0, 4750.0], - [x['no_format'] for x in lines[0]['columns'][4:]]) - - def test_ref_asset_depreciation(self): - """Test that the reference used in depreciation moves is correct""" - - for ref in self.truck.depreciation_move_ids.mapped('ref'): - self.assertEqual(ref, 'truck: Depreciation') - - def test_credit_note_out_refund(self): - """ - Test the behaviour of the asset creation when a credit note is created. - The asset created from the credit note should be the same as the one created from the invoice - with a negative value. - """ - depreciation_account = self.company_data['default_account_assets'].copy() - revenue_model = self.env['account.asset'].create({ - 'account_depreciation_id': depreciation_account.id, - 'account_depreciation_expense_id': self.company_data['default_account_revenue'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Hardware - 5 Years', - 'method_number': 5, - 'method_period': '12', - 'state': 'model', - }) - - depreciation_account.write({'create_asset': 'draft', 'asset_model_ids': revenue_model}) - - invoice = self.env['account.move'].create({ - 'invoice_date': '2019-07-01', - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [(0, 0, { - 'name': 'Hardware', - 'account_id': depreciation_account.id, - 'price_unit': 5000, - 'quantity': 1, - 'tax_ids': False, - })], - }) - - invoice.action_post() - self.assertTrue(invoice.asset_ids) - - credit_note = invoice._reverse_moves([{'invoice_date': fields.Date.today()}]) - credit_note.action_post() - - invoice_asset = invoice.asset_ids - credit_note_asset = credit_note.asset_ids - - # check if invoice_asset still exists after validate the credit note - self.assertTrue(invoice_asset) - self.assertTrue(credit_note_asset) - - (invoice_asset + credit_note_asset).validate() - - self.assertRecordValues(credit_note_asset, [ - { - 'acquisition_date': invoice_asset.acquisition_date, - 'book_value': -invoice_asset.book_value, - 'value_residual': -invoice_asset.value_residual, - } - ]) - - for invoice_asset_move, credit_note_asset_move in zip(invoice_asset.depreciation_move_ids.sorted('date'), credit_note_asset.depreciation_move_ids.sorted('date')): - self.assertRecordValues(credit_note_asset_move, [ - { - 'date': invoice_asset_move.date, - 'state': invoice_asset_move.state, - 'depreciation_value': -invoice_asset_move.depreciation_value, - } - ]) - - def test_asset_multiple_assets_from_one_move_line_00(self): - """ Test the creation of a as many assets as the value of - the quantity property of a move line. """ - - account = self.env['account.account'].create({ - "name": "test account", - "code": "TEST", - "account_type": 'asset_non_current', - "create_asset": "draft", - "multiple_assets_per_line": True, - }) - move = self.env['account.move'].create({ - "partner_id": self.env['res.partner'].create({'name': 'Johny'}).id, - "ref": "line1", - "move_type": "in_invoice", - "invoice_date": "2020-12-31", - "invoice_line_ids": [ - (0, 0, { - "account_id": account.id, - "price_unit": 400.0, - "name": "stuff", - "quantity": 2, - "product_uom_id": self.env.ref('uom.product_uom_unit').id, - "tax_ids": [], - }), - ] - }) - move.action_post() - assets = move.asset_ids - assets = sorted(assets, key=lambda i: i['original_value'], reverse=True) - self.assertEqual(len(assets), 2, '3 assets should have been created') - self.assertEqual(assets[0].original_value, 400.0) - self.assertEqual(assets[1].original_value, 400.0) - - def test_asset_multiple_assets_from_one_move_line_01(self): - """ Test the creation of a as many assets as the value of - the quantity property of a move line. """ - - account = self.env['account.account'].create({ - "name": "test account", - "code": "TEST", - "account_type": 'asset_non_current', - "create_asset": "draft", - "multiple_assets_per_line": True, - }) - move = self.env['account.move'].create({ - "partner_id": self.env['res.partner'].create({'name': 'Johny'}).id, - "ref": "line1", - "move_type": "in_invoice", - "invoice_date": "2020-12-31", - "invoice_line_ids": [ - (0, 0, { - "account_id": account.id, - "name": "stuff", - "quantity": 3.0, - "price_unit": 1000.0, - "product_uom_id": self.env.ref('uom.product_uom_categ_unit').id, - }), - (0, 0, { - 'account_id': self.company_data['default_account_assets'].id, - "name": "stuff", - 'quantity': 1.0, - 'price_unit': -500.0, - }), - ] - }) - move.action_post() - self.assertEqual(sum(asset.original_value for asset in move.asset_ids), move.line_ids[0].debit) - - def test_asset_credit_note(self): - """Test the generated entries created from an in_refund invoice with asset""" - asset_model = self.env['account.asset'].create({ - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'account_asset_id': self.company_data['default_account_assets'].id, - 'journal_id': self.company_data['default_journal_purchase'].id, - 'name': 'Small car - 3 Years', - 'method_number': 3, - 'method_period': '12', - 'state': 'model', - }) - - self.company_data['default_account_assets'].create_asset = "validate" - self.company_data['default_account_assets'].asset_model_ids = asset_model - - invoice = self.env['account.move'].create({ - 'move_type': 'in_refund', - 'invoice_date': '2020-01-01', - 'date': '2020-01-01', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [(0, 0, { - 'name': 'Very little red car', - 'account_id': self.company_data['default_account_assets'].id, - 'price_unit': 450, - 'quantity': 1, - })], - }) - invoice.action_post() - depreciation_lines = self.env['account.move.line'].search([ - ('account_id', '=', asset_model.account_depreciation_id.id), - ('move_id.asset_id', '=', invoice.asset_ids.id), - ('debit', '=', 150), - ]) - self.assertEqual( - len(depreciation_lines), 3, - 'Three entries with a debit of 150 must be created on the Deferred Expense Account' - ) - - def test_asset_partial_credit_note(self): - """Test partial credit note on an in invoice that has generated draft assets. - - Test case: - - Create in invoice with the following lines: - - Product | Unit Price | Quantity | Multiple assets - --------------------------------------------------------- - Product B | 200 | 4 | TRUE - Product A | 100 | 7 | FALSE - Product A | 100 | 5 | TRUE - Product A | 150 | 6 | TRUE - Product A | 100 | 7 | FALSE - - - Add a credit note with the following lines: - - Product | Unit Price | Quantity - --------------------------------------- - Product A | 100 | 1 - Product A | 150 | 2 - Product A | 100 | 7 - """ - asset_model = self.env['account.asset'].create({ - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_sale'].id, - 'name': 'Maintenance Contract - 3 Years', - 'method_number': 3, - 'method_period': '12', - 'prorata_computation_type': 'none', - 'state': 'model', - }) - self.company_data['default_account_assets'].create_asset = 'draft' - self.company_data['default_account_assets'].asset_model_ids = asset_model - account_assets_multiple = self.company_data['default_account_assets'].copy() - account_assets_multiple.multiple_assets_per_line = True - - product_a = self.env['product.product'].create({ - 'name': 'Product A', - 'default_code': 'PA', - 'lst_price': 100.0, - 'standard_price': 100.0, - }) - product_b = self.env['product.product'].create({ - 'name': 'Product B', - 'default_code': 'PB', - 'lst_price': 200.0, - 'standard_price': 200.0, - }) - invoice = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'invoice_date': '2020-01-01', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [ - (0, 0, { - 'product_id': product_b.id, - 'name': 'Product B', - 'account_id': account_assets_multiple.id, - 'price_unit': 200.0, - 'quantity': 4, - }), - (0, 0, { - 'product_id': product_a.id, - 'name': 'Product A', - 'account_id': self.company_data['default_account_assets'].id, - 'price_unit': 100.0, - 'quantity': 7, - }), - (0, 0, { - 'product_id': product_a.id, - 'name': 'Product A', - 'account_id': account_assets_multiple.id, - 'price_unit': 100.0, - 'quantity': 5, - }), - (0, 0, { - 'product_id': product_a.id, - 'name': 'Product A', - 'account_id': account_assets_multiple.id, - 'price_unit': 150.0, - 'quantity': 6, - }), - (0, 0, { - 'product_id': product_a.id, - 'name': 'Product A', - 'account_id': self.company_data['default_account_assets'].id, - 'price_unit': 100.0, - 'quantity': 7, - }), - ], - }) - invoice.action_post() - product_a_100_lines = invoice.line_ids.filtered(lambda l: l.product_id == product_a and l.price_unit == 100.0) - product_a_150_lines = invoice.line_ids.filtered(lambda l: l.product_id == product_a and l.price_unit == 150.0) - product_b_lines = invoice.line_ids.filtered(lambda l: l.product_id == product_b) - self.assertEqual(len(invoice.line_ids.mapped(lambda l: l.asset_ids)), 17) - self.assertEqual(len(product_b_lines.asset_ids), 4) - self.assertEqual(len(product_a_100_lines.asset_ids), 7) - self.assertEqual(len(product_a_150_lines.asset_ids), 6) - credit_note = invoice._reverse_moves() - with Form(credit_note) as move_form: - move_form.invoice_date = move_form.date - move_form.invoice_line_ids.remove(0) - move_form.invoice_line_ids.remove(0) - with move_form.invoice_line_ids.edit(0) as line_form: - line_form.quantity = 1 - with move_form.invoice_line_ids.edit(1) as line_form: - line_form.quantity = 2 - credit_note.action_post() - self.assertEqual(len(invoice.line_ids.mapped(lambda l: l.asset_ids)), 17) - self.assertEqual(len(product_b_lines.asset_ids), 4) - self.assertEqual(len(product_a_100_lines.asset_ids), 7) - self.assertEqual(len(product_a_150_lines.asset_ids), 6) - - def test_asset_with_non_deductible_tax(self): - """Test that the assets' original_value and non_deductible_tax_value are correctly computed - from a move line with a non-deductible tax.""" - - asset_account = self.company_data['default_account_assets'] - non_deductible_tax = self.env['account.tax'].create({ - 'name': 'Non-deductible Tax', - 'amount': 21, - 'amount_type': 'percent', - 'type_tax_use': 'purchase', - 'invoice_repartition_line_ids': [ - Command.create({'repartition_type': 'base'}), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': False - }), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': True - }), - ], - 'refund_repartition_line_ids': [ - Command.create({'repartition_type': 'base'}), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': False - }), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': True - }), - ], - }) - asset_account.tax_ids = non_deductible_tax - - # 1. Automatic creation - asset_account.create_asset = 'draft' - asset_account.asset_model_ids = self.account_asset_model_fixedassets - asset_account.multiple_assets_per_line = True - - vendor_bill_auto = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'invoice_date': '2020-01-01', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [Command.create({ - 'account_id': asset_account.id, - 'name': 'Asus Laptop', - 'price_unit': 1000.0, - 'quantity': 2, - 'tax_ids': [Command.set(non_deductible_tax.ids)], - })], - }) - vendor_bill_auto.action_post() - - new_assets_auto = vendor_bill_auto.asset_ids - self.assertEqual(len(new_assets_auto), 2) - self.assertEqual(new_assets_auto.mapped('original_value'), [1105.0, 1105.0]) - self.assertEqual(new_assets_auto.mapped('non_deductible_tax_value'), [105.0, 105.0]) - - # 2. Manual creation - asset_account.create_asset = 'no' - asset_account.asset_model_ids = None - asset_account.multiple_assets_per_line = False - - vendor_bill_manu = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'invoice_date': '2020-01-01', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [ - Command.create({ - 'account_id': asset_account.id, - 'name': 'Asus Laptop', - 'price_unit': 1000.0, - 'quantity': 2, - 'tax_ids': [Command.set(non_deductible_tax.ids)] - }), - Command.create({ - 'account_id': asset_account.id, - 'name': 'Lenovo Laptop', - 'price_unit': 500.0, - 'quantity': 3, - 'tax_ids': [Command.set(non_deductible_tax.ids)] - }), - ], - }) - vendor_bill_manu.action_post() - - # TOFIX: somewhere above this the field account.asset.asset_type is made - # dirty, but this field has to be flushed in a specific environment. - # This is because the field 'asset_type' is stored, computed and - # context-dependent, which explains why its value must be retrieved - # from the right environment. - self.env.flush_all() - - move_line_ids = vendor_bill_manu.mapped('line_ids').filtered(lambda x: 'Laptop' in x.name) - asset_form = Form(self.env['account.asset'].with_context( - default_original_move_line_ids=move_line_ids.ids, - )) - asset_form.original_move_line_ids = move_line_ids - asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] - - new_assets_manu = asset_form.save() - self.assertEqual(len(new_assets_manu), 1) - self.assertEqual(new_assets_manu.original_value, 3867.5) - self.assertEqual(new_assets_manu.non_deductible_tax_value, 367.5) - - def test_asset_degressive_01(self): - """ Check the computation of an asset with degressive method, - start at middle of the year - """ - asset = self.env['account.asset'].create({ - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Degressive', - 'acquisition_date': '2021-07-01', - 'prorata_computation_type': 'constant_periods', - 'original_value': 10000, - 'method_number': 5, - 'method_period': '12', - 'method': 'degressive', - 'method_progress_factor': 0.5, - }) - - asset.validate() - - self.assertEqual(asset.method_number + 1, len(asset.depreciation_move_ids)) - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ - 'amount_total': 2500, - 'asset_remaining_value': 7500, - }, { - 'amount_total': 3750, - 'asset_remaining_value': 3750, - }, { - 'amount_total': 1875, - 'asset_remaining_value': 1875, - }, { - 'amount_total': 937.5, - 'asset_remaining_value': 937.5, - }, { - 'amount_total': 625.00, - 'asset_remaining_value': 312.50, - }, { - 'amount_total': 312.50, - 'asset_remaining_value': 0, - }]) - - def test_asset_degressive_02(self): - """ Check the computation of an asset with degressive method, - start at beginning of the year. - """ - asset = self.env['account.asset'].create({ - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Degressive', - 'acquisition_date': '2021-01-01', - 'original_value': 10000, - 'method_number': 5, - 'method_period': '12', - 'method': 'degressive', - 'method_progress_factor': 0.5, - }) - - asset.validate() - - self.assertEqual(asset.method_number, len(asset.depreciation_move_ids)) - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ - 'amount_total': 5000, - 'asset_remaining_value': 5000, - }, { - 'amount_total': 2500, - 'asset_remaining_value': 2500, - }, { - 'amount_total': 1250, - 'asset_remaining_value': 1250, - }, { - 'amount_total': 625, - 'asset_remaining_value': 625, - }, { - 'amount_total': 625, - 'asset_remaining_value': 0, - }]) - - def test_asset_negative_01(self): - """ Check the computation of an asset with negative value. """ - asset = self.env['account.asset'].create({ - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Degressive Linear', - 'acquisition_date': '2021-07-01', - 'original_value': -10000, - 'method_number': 5, - 'method_period': '12', - 'method': 'linear', - }) - asset.prorata_computation_type = 'constant_periods' - - asset.validate() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ - 'amount_total': 1000, - 'asset_remaining_value': -9000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': -7000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': -5000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': -3000, - }, { - 'amount_total': 2000, - 'asset_remaining_value': -1000, - }, { - 'amount_total': 1000, - 'asset_remaining_value': 0, - }]) - - def test_asset_daily_computation_01(self): - """ Check the computation of an asset with daily_computation. """ - asset = self.env['account.asset'].create({ - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Degressive Linear', - 'acquisition_date': '2021-07-01', - 'prorata_computation_type': 'daily_computation', - 'original_value': 10000, - 'method_number': 5, - 'method_period': '12', - 'method': 'linear', - }) - - asset.validate() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ - 'amount_total': 1007.67, - 'asset_remaining_value': 8992.33, - }, { - 'amount_total': 1998.90, - 'asset_remaining_value': 6993.43, - }, { - 'amount_total': 1998.91, - 'asset_remaining_value': 4994.52, - }, { - 'amount_total': 2004.38, - 'asset_remaining_value': 2990.14, - }, { - 'amount_total': 1998.90, - 'asset_remaining_value': 991.24, - }, { - 'amount_total': 991.24, - 'asset_remaining_value': 0, - }]) - - def test_decrement_book_value_with_negative_asset(self): - """ - Test the computation of book value and remaining value - when posting a depreciation move related with a negative asset - """ - depreciation_account = self.company_data['default_account_assets'].copy() - asset_model = self.env['account.asset'].create({ - 'name': 'test', - 'state': 'model', - 'active': True, - 'method': 'linear', - 'method_number': 5, - 'method_period': '1', - 'prorata_computation_type': 'constant_periods', - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': depreciation_account.id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_purchase'].id, - }) - - depreciation_account.can_create_asset = True - depreciation_account.create_asset = 'draft' - depreciation_account.asset_model_ids = asset_model - - refund = self.env['account.move'].create({ - 'move_type': 'in_refund', - 'partner_id': self.partner_a.id, - 'invoice_date': '2021-06-01', - 'invoice_line_ids': [Command.create({'name': 'refund', 'account_id': depreciation_account.id, 'price_unit': 500, 'tax_ids': False})], - }) - refund.action_post() - - self.assertTrue(refund.asset_ids) - - asset = refund.asset_ids - - self.assertEqual(asset.book_value, -refund.amount_total) - self.assertEqual(asset.value_residual, -refund.amount_total) - - asset.validate() - - self.assertEqual(len(asset.depreciation_move_ids.filtered(lambda m: m.state == 'posted')), 1) - self.assertEqual(asset.book_value, -400.0) - self.assertEqual(asset.value_residual, -400.0) - - def test_depreciation_schedule_report_with_negative_asset(self): - """ - Test the computation of depreciation schedule with negative asset - """ - asset = self.env['account.asset'].create({ - 'name': 'test', - 'original_value': -500, - 'method': 'linear', - 'method_number': 5, - 'method_period': '1', - 'acquisition_date': fields.Date.today() + relativedelta(months=-1), - 'prorata_computation_type': 'none', - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - }) - - asset.validate() - - report = self.env.ref('at_accounting.assets_report') - - options = self._generate_options(report, fields.Date.today() + relativedelta(months=-7, day=1), fields.Date.today() + relativedelta(months=-6, day=31)) - - expected_values_open_asset = [ - ("test", 0, 0, 500.0, -500.0, 0, 0, 100.0, -100.0, -400.0), - ] - - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_open_asset, options) - - expense_account_copy = self.company_data['default_account_expense'].copy() - - disposal_action_view = self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'modify_action': 'dispose', - 'loss_account_id': expense_account_copy.id, - 'date': fields.Date.today() - }).sell_dispose() - - self.env['account.move'].browse(disposal_action_view['res_id']).action_post() - - expected_values_closed_asset = [ - ("test", 0, 500.0, 500.0, 0, 0, 500.0, 500.0, 0, 0), - ] - options = self._generate_options(report, fields.Date.today() + relativedelta(months=-7, day=1), fields.Date.today()) - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_closed_asset, options) - - def test_depreciation_schedule_hierarchy(self): - # Remove previously existing assets. - assets = self.env['account.asset'].search([ - ('company_id', '=', self.env.company.id), - ('state', '!=', 'model'), - ]) - assets.state = 'draft' - assets.mapped('depreciation_move_ids').state = 'draft' - assets.unlink() - - # Create the account groups. - self.env['account.group'].create([ - {'name': 'Group 1', 'code_prefix_start': '1', 'code_prefix_end': '1'}, - {'name': 'Group 11', 'code_prefix_start': '11', 'code_prefix_end': '11'}, - {'name': 'Group 12', 'code_prefix_start': '12', 'code_prefix_end': '12'}, - ]) - - # Create the accounts. - account_a, account_a1, account_b, account_c, account_d, account_e = self.env['account.account'].create([ - {'code': '1100', 'name': 'Account A', 'account_type': 'asset_non_current'}, - {'code': '1110', 'name': 'Account A1', 'account_type': 'asset_non_current'}, - {'code': '1200', 'name': 'Account B', 'account_type': 'asset_non_current'}, - {'code': '1300', 'name': 'Account C', 'account_type': 'asset_non_current'}, - {'code': '1400', 'name': 'Account D', 'account_type': 'asset_non_current'}, - {'code': '9999', 'name': 'Account E', 'account_type': 'asset_non_current'}, - ]) - - # Create and validate the assets, and post the depreciation entries. - self.env['account.asset'].create([ - { - 'account_asset_id': account_id, - 'account_depreciation_id': account_id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': name, - 'acquisition_date': fields.Date.to_date('2020-07-01'), - 'original_value': original_value, - 'method': 'linear', - 'prorata_computation_type': 'none', - } - for account_id, name, original_value in [ - (account_a.id, 'ZenBook', 1250), - (account_a.id, 'ThinkBook', 1500), - (account_a1.id, 'XPS', 1750), - (account_b.id, 'MacBook', 2000), - (account_c.id, 'Aspire', 1600), - (account_d.id, 'Playstation', 550), - (account_e.id, 'Xbox', 500), - ] - ]).validate() - - # Configure the depreciation schedule report. - report = self.env.ref('at_accounting.assets_report') - options = self._generate_options(report, '2022-01-01', '2022-12-31') - options['hierarchy'] = True - self.env.company.totals_below_sections = True - - # Generate and compare actual VS expected values. - lines = [ - { - 'name': line['name'], - 'level': line['level'], - 'book_value': line['columns'][-1]['name'] - } - for line in (report._get_lines(options)) - ] - - expected_values = [ - # pylint: disable=C0326 - {'name': '1 Group 1', 'level': 1, 'book_value': '$\xa06,920.00'}, - {'name': '11 Group 11', 'level': 2, 'book_value': '$\xa03,600.00'}, - {'name': '1100 Account A', 'level': 3, 'book_value': '$\xa02,200.00'}, - {'name': 'ZenBook', 'level': 4, 'book_value': '$\xa01,000.00'}, - {'name': 'ThinkBook', 'level': 4, 'book_value': '$\xa01,200.00'}, - {'name': 'Total 1100 Account A', 'level': 3, 'book_value': '$\xa02,200.00'}, - {'name': '1110 Account A1', 'level': 3, 'book_value': '$\xa01,400.00'}, - {'name': 'XPS', 'level': 4, 'book_value': '$\xa01,400.00'}, - {'name': 'Total 1110 Account A1', 'level': 3, 'book_value': '$\xa01,400.00'}, - {'name': 'Total 11 Group 11', 'level': 2, 'book_value': '$\xa03,600.00'}, - {'name': '12 Group 12', 'level': 2, 'book_value': '$\xa01,600.00'}, - {'name': '1200 Account B', 'level': 3, 'book_value': '$\xa01,600.00'}, - {'name': 'MacBook', 'level': 4, 'book_value': '$\xa01,600.00'}, - {'name': 'Total 1200 Account B', 'level': 3, 'book_value': '$\xa01,600.00'}, - {'name': 'Total 12 Group 12', 'level': 2, 'book_value': '$\xa01,600.00'}, - {'name': '1300 Account C', 'level': 2, 'book_value': '$\xa01,280.00'}, - {'name': 'Aspire', 'level': 3, 'book_value': '$\xa01,280.00'}, - {'name': 'Total 1300 Account C', 'level': 2, 'book_value': '$\xa01,280.00'}, - {'name': '1400 Account D', 'level': 2, 'book_value': '$\xa0440.00'}, - {'name': 'Playstation', 'level': 3, 'book_value': '$\xa0440.00'}, - {'name': 'Total 1400 Account D', 'level': 2, 'book_value': '$\xa0440.00'}, - {'name': 'Total 1 Group 1', 'level': 1, 'book_value': '$\xa06,920.00'}, - {'name': '(No Group)', 'level': 1, 'book_value': '$\xa0400.00'}, - {'name': '9999 Account E', 'level': 2, 'book_value': '$\xa0400.00'}, - {'name': 'Xbox', 'level': 3, 'book_value': '$\xa0400.00'}, - {'name': 'Total 9999 Account E', 'level': 2, 'book_value': '$\xa0400.00'}, - {'name': 'Total (No Group)', 'level': 1, 'book_value': '$\xa0400.00'}, - {'name': 'Total', 'level': 1, 'book_value': '$\xa07,320.00'}, - ] - - self.assertEqual(len(lines), len(expected_values)) - self.assertEqual(lines, expected_values) - - def test_depreciation_schedule_disposal_move_unposted(self): - """ - Test the computation of values when disposing an asset, and the difference if the disposal move is posted - """ - asset = self.env['account.asset'].create({ - 'name': 'test asset', - 'method': 'linear', - 'original_value': 1000, - 'method_number': 5, - 'method_period': '12', - 'acquisition_date': fields.Date.today() + relativedelta(years=-2, month=1, day=1), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - }) - asset.validate() - - expense_account_copy = self.company_data['default_account_expense'].copy() - - disposal_action_view = self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'modify_action': 'dispose', - 'loss_account_id': expense_account_copy.id, - 'date': fields.Date.today() + relativedelta(days=-1) - }).sell_dispose() - - report = self.env.ref('at_accounting.assets_report') - options = self._generate_options(report, '2021-01-01', '2021-12-31') - - # The disposal move is in draft and should not be considered (depreciation and book value) - # Values are: name, assets_before, assets+, assets-, assets_after, depreciation_before, depreciation+, depreciation-, depreciation_after, book_value - expected_values_asset_disposal_unposted = [ - ("test asset", 1000.0, 0.0, 0, 1000.0, 400.0, 100.0, 0.0, 500.0, 500.0), - ] - - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_unposted, options) - - self.env['account.move'].browse(disposal_action_view.get('res_id')).action_post() - - expected_values_asset_disposal_posted = [ - ("test asset", 1000.0, 0.0, 1000.0, 0.0, 400.0, 100.0, 500.0, 0.0, 0.0), - ] - - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_posted, options) - - def test_depreciation_schedule_disposal_move_unposted_with_non_depreciable_value(self): - """ - Test the computation of values when disposing an asset with non-depreciable value, and the difference if the disposal move is posted - """ - asset = self.env['account.asset'].create({ - 'name': 'test asset', - 'method': 'linear', - 'original_value': 10000, - 'salvage_value': 8000, - 'method_number': 24, - 'method_period': '1', - 'acquisition_date': fields.Date.today() + relativedelta(months=-1, day=1), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - }) - asset.validate() - - report = self.env.ref('at_accounting.assets_report') - - options = self._generate_options(report, '2021-07-01', '2021-07-31') - - expected_values_asset_disposal_unposted = [ - ("test asset", 10000.0, 0.0, 0.0, 10000.0, 83.33, 0.0, 0.0, 83.33, 9916.67), - ] - - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_unposted, options) - - expense_account_copy = self.company_data['default_account_expense'].copy() - - disposal_action_view = self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'modify_action': 'dispose', - 'loss_account_id': expense_account_copy.id, - 'date': fields.Date.today() - }).sell_dispose() - - expected_values_asset_disposal_unposted = [ - ("test asset", 10000.0, 0.0, 0.0, 10000.0, 83.33, 2.69, 0.0, 86.02, 9913.98), - ] - - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_unposted, options) - - self.env['account.move'].browse(disposal_action_view['res_id']).action_post() - - expected_values_asset_disposal_posted = [ - ("test asset", 10000.0, 0.0, 10000.0, 0.0, 83.33, 2.69, 86.02, 0.0, 0.0), - ] - - self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_posted, options) - - def test_asset_analytic_on_lines(self): - CEO_car = self.env['account.asset'].create({ - 'salvage_value': 2000.0, - 'state': 'open', - 'method_period': '12', - 'method_number': 5, - 'name': "CEO's Car", - 'original_value': 12000.0, - 'model_id': self.account_asset_model_fixedassets.id, - 'acquisition_date': '2020-01-01', - }) - CEO_car._onchange_model_id() - CEO_car.method_number = 5 - CEO_car.analytic_distribution = {self.analytic_account.id: 100} - - # In order to test the process of Account Asset, I perform a action to confirm Account Asset. - CEO_car.validate() - - for move in CEO_car.depreciation_move_ids: - self.assertRecordValues(move.line_ids, [ - { - 'analytic_distribution': {str(self.analytic_account.id): 100}, - }, - { - 'analytic_distribution': {str(self.analytic_account.id): 100}, - }, - ]) - - CEO_car.analytic_distribution = {str(self.analytic_account.id): 200} - - # Only draft moves should have a changed analytic distribution - for move in CEO_car.depreciation_move_ids.filtered(lambda m: m.state == 'posted'): - self.assertRecordValues(move.line_ids, [ - { - 'analytic_distribution': {str(self.analytic_account.id): 100}, - }, - { - 'analytic_distribution': {str(self.analytic_account.id): 100}, - }, - ]) - - for move in CEO_car.depreciation_move_ids.filtered(lambda m: m.state == 'draft'): - self.assertRecordValues(move.line_ids, [ - { - 'analytic_distribution': {str(self.analytic_account.id): 200}, - }, - { - 'analytic_distribution': {str(self.analytic_account.id): 200}, - }, - ]) - - - def test_asset_analytic_filter(self): - """ - Test that the analytic filter works correctly. - """ - truck_b = self.truck.copy() - truck_b.acquisition_date = self.truck.acquisition_date - truck_b.validate() - self.truck.analytic_distribution = {self.analytic_account.id: 100} - self.env['account.move']._autopost_draft_entries() - - self.env.company.totals_below_sections = False - report = self.env.ref('at_accounting.assets_report') - - # No prefix group, no group by account - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none', 'unfold_all': False}) - - # without Analytic Filter - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('truck (copy)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Total', 20000, 0, 0, 20000, 9000, 0, 0, 9000, 11000,), - ], - options - ) - # with Analytic Filter - options['analytic_accounts'] = [self.analytic_account.id] - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Total', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ], - options - ) - - def test_asset_analytic_groupby(self): - """ - Test that the analytic groupby works correctly. - """ - truck_b = self.truck.copy() - truck_b.acquisition_date = self.truck.acquisition_date - truck_b.validate() - self.truck.analytic_distribution = {self.analytic_account.id: 100} - self.env['account.move']._autopost_draft_entries() - - self.env.company.totals_below_sections = False - report = self.env.ref('at_accounting.assets_report') - report.filter_analytic_groupby = True - - # No prefix group, no group by account - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none', 'unfold_all': False}) - - # without Analytic Groupby - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('truck (copy)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Total', 20000, 0, 0, 20000, 9000, 0, 0, 9000, 11000,), - ], - options - ) - # with Analytic Groupby - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={ - 'assets_grouping_field': 'none', - 'unfold_all': False, - 'analytic_accounts_groupby': [self.analytic_account.id], - }) - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Group | ANALYTIC | | ALL | - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23, 24, 25, 26], - [ - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500, 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), - ('truck (copy)', '', '', '', '', '', '', '', '', '', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), - ('Total', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500, 20000, 0, 0, 20000, 9000, 0, 0, 9000, 11000), - ], - options - ) - - def test_depreciation_schedule_report_first_depreciation(self): - """Test that the depreciation schedule report displays the correct first depreciation date.""" - # check that the truck's first depreciation date is correct: - # the truck has a yearly linear depreciation and it's prorate_date is 2015-01-01 - # therefore we expect it's first depreciation date to be the last day of 2015 - - today = fields.Date.today() - report = self.env.ref('at_accounting.assets_report') - options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) - lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) - - self.assertEqual(lines[1]['columns'][1]['name'], '12/31/2015') - - def test_asset_modify_sell_multicurrency(self): - """ Test that the closing invoice's currency is taken into account when selling an asset. """ - closing_invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'currency_id': self.other_currency.id, - 'invoice_line_ids': [Command.create({'price_unit': 5000})] - }) - self.env['asset.modify'].create({ - 'asset_id': self.truck.id, - 'invoice_line_ids': closing_invoice.invoice_line_ids, - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'modify_action': 'sell', - }).sell_dispose() - - closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - - self.assertRecordValues(closing_move.line_ids, [{ - 'debit': 0, - 'credit': 10000, - 'account_id': self.truck.account_asset_id.id, - }, { - 'debit': 4500, - 'credit': 0, - 'account_id': self.truck.account_depreciation_id.id, - }, { - 'debit': 2500, - 'credit': 0, - 'account_id': closing_invoice.invoice_line_ids.account_id.id, - }, { - 'debit': 3000, - 'credit': 0, - 'account_id': self.env.company.loss_account_id.id, - }]) - - def test_depreciation_schedule_prefix_groups(self): - asset_group = self.env['account.asset.group'].create({'name': 'Odoo Office'}) - for i in range(1, 3): - asset = self.env['account.asset'].create({ - 'method_period': '12', - 'method_number': 4, - 'name': f"Asset {i}", - 'original_value': i * 100.0, - 'acquisition_date': fields.Date.today() - relativedelta(years=3), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'asset_group_id': asset_group.id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'prorata_computation_type': 'none', - }) - asset.validate() - - self.env['account.move']._autopost_draft_entries() - - self.env.company.totals_below_sections = False - report = self.env.ref('at_accounting.assets_report') - - # No prefix group, no group by account - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none'}) - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), - ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), - ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), - ], - options, - ) - - # No prefix group, group by account - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'account_id'}) - options['unfold_all'] = True - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('151000 Fixed Asset', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), - ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), - ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), - ], - options, - ) - - report.prefix_groups_threshold = 3 - # Prefix group, no group by account - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none', 'unfold_all': True}) - options['unfold_all'] = True - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('A (2 lines)', 300, 0, 0, 300, 225, 0, 0, 225, 75,), - ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), - ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), - ('T (1 line)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), - ], - options, - ) - - # Prefix group, group by account - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'account_id', 'unfold_all': True}) - options['unfold_all'] = True - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('151000 Fixed Asset', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), - ('A (2 lines)', 300, 0, 0, 300, 225, 0, 0, 225, 75,), - ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), - ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), - ('T (1 line)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), - ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), - ], - options, - ) - - # No prefix group, group by asset group - options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'asset_group_id'}) - options['unfold_all'] = True - self.assertLinesValues( - # pylint: disable=C0326 - report._get_lines(options), - # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value - [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], - [ - ('(No Asset Group)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), - ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), - ('Odoo Office', 300, 0, 0, 300, 225, 0, 0, 225, 75), - ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25), - ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50), - ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575), - ], - options, - ) - - def test_archive_asset_model(self): - """ Test that we can archive an asset model. """ - self.account_asset_model_fixedassets.active = False - self.assertFalse(self.account_asset_model_fixedassets.active) - - def test_asset_increase_with_lock_year(self): - """ Test the dates at which the moves are posted even with increase, with lock date""" - self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2021-03-01') - - asset = self.env['account.asset'].create({ - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Car', - 'acquisition_date': fields.Date.today() + relativedelta(months=-6), - 'original_value': 12000, - 'method_number': 12, - 'method_period': '1', - 'method': 'linear', - }) - - asset.validate() - - self.assertRecordValues( - asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), - [ - {'date': fields.Date.to_date('2021-03-31')}, - {'date': fields.Date.to_date('2021-03-31')}, - {'date': fields.Date.to_date('2021-03-31')}, - {'date': fields.Date.to_date('2021-04-30')}, - {'date': fields.Date.to_date('2021-05-31')}, - {'date': fields.Date.to_date('2021-06-30')}, - {'date': fields.Date.to_date('2021-07-31')}, - {'date': fields.Date.to_date('2021-08-31')}, - {'date': fields.Date.to_date('2021-09-30')}, - {'date': fields.Date.to_date('2021-10-31')}, - {'date': fields.Date.to_date('2021-11-30')}, - {'date': fields.Date.to_date('2021-12-31')} - ] - ) - - self.assertEqual(asset.book_value, 6000) - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test increase with lock date', - 'value_residual': 8000.0, - 'date': fields.Date.today() + relativedelta(days=-1), - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - - self.assertEqual(asset.book_value, 8000) - - self.assertRecordValues( - asset.children_ids.depreciation_move_ids.sorted(lambda dep: (dep.date, dep.id)), - [ - {'date': fields.Date.to_date('2021-07-31'), 'depreciation_value': 333.33}, - {'date': fields.Date.to_date('2021-08-31'), 'depreciation_value': 333.34}, - {'date': fields.Date.to_date('2021-09-30'), 'depreciation_value': 333.33}, - {'date': fields.Date.to_date('2021-10-31'), 'depreciation_value': 333.33}, - {'date': fields.Date.to_date('2021-11-30'), 'depreciation_value': 333.34}, - {'date': fields.Date.to_date('2021-12-31'), 'depreciation_value': 333.33} - ] - ) - - def test_asset_decrease_with_lock_year(self): - """ Test the dates and values for the moves that are posted with decrease and lock date""" - self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2021-03-01') - - asset = self.env['account.asset'].create({ - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'name': 'Car', - 'acquisition_date': fields.Date.today() + relativedelta(months=-6), - 'original_value': 12000, - 'method_number': 12, - 'method_period': '1', - 'method': 'linear', - }) - - asset.validate() - - self.assertEqual(asset.book_value, 6000) - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test decrease with lock date', - 'value_residual': 4000.0, - 'date': fields.Date.today() + relativedelta(days=-1), - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - - self.assertEqual(asset.book_value, 4000) - - self.assertRecordValues( - asset.depreciation_move_ids.sorted(lambda dep: (dep.date, dep.id)), - [ - {'date': fields.Date.to_date('2021-03-31'), 'depreciation_value': 1000}, - {'date': fields.Date.to_date('2021-03-31'), 'depreciation_value': 1000}, - {'date': fields.Date.to_date('2021-03-31'), 'depreciation_value': 1000}, - {'date': fields.Date.to_date('2021-04-30'), 'depreciation_value': 1000}, - {'date': fields.Date.to_date('2021-05-31'), 'depreciation_value': 1000}, - {'date': fields.Date.to_date('2021-06-30'), 'depreciation_value': 1000}, - {'date': fields.Date.to_date('2021-06-30'), 'depreciation_value': 2000}, - {'date': fields.Date.to_date('2021-07-31'), 'depreciation_value': 666.67}, - {'date': fields.Date.to_date('2021-08-31'), 'depreciation_value': 666.66}, - {'date': fields.Date.to_date('2021-09-30'), 'depreciation_value': 666.67}, - {'date': fields.Date.to_date('2021-10-31'), 'depreciation_value': 666.67}, - {'date': fields.Date.to_date('2021-11-30'), 'depreciation_value': 666.66}, - {'date': fields.Date.to_date('2021-12-31'), 'depreciation_value': 666.67} - ] - ) - - def test_asset_onchange_model(self): - """ - Test the changes of account_asset_id when changing asset models - """ - account_asset = self.company_data['default_account_assets'].copy() - asset_model = self.env['account.asset'].create({ - 'name': 'test model', - 'state': 'model', - 'active': True, - 'method': 'linear', - 'method_number': 5, - 'method_period': '1', - 'prorata_computation_type': 'none', - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'account_asset_id': at_accounting.id, - 'journal_id': self.company_data['default_journal_misc'].id, - }) - - asset_model_with_account = self.env['account.asset'].create({ - 'name': 'test model with account', - 'state': 'model', - 'active': True, - 'method': 'linear', - 'method_number': 5, - 'method_period': '1', - 'prorata_computation_type': 'none', - 'account_depreciation_id': self.company_data['default_account_assets'].id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - }) - - asset_form = Form(self.env['account.asset']) - asset_form.name = "Test Asset" - asset_form.original_value = 10000 - asset_form.model_id = asset_model - - self.assertEqual(asset_form.account_asset_id, account_asset, "The account_asset_id should be the one from the model") - - asset_form.model_id = asset_model_with_account - self.assertEqual(asset_form.account_asset_id, self.company_data['default_account_assets'], "The account_asset_id should be computed from the depreciation account from the model") - - other_account_on_bill = self.company_data['default_account_assets'].copy() - other_account_on_bill.create_asset = 'draft' - other_account_on_bill.asset_model_ids = asset_model - invoice = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'invoice_date': '2020-12-31', - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [ - (0, 0, { - 'name': 'A beautiful small bomb', - 'account_id': other_account_on_bill.id, - 'price_unit': 200.0, - 'quantity': 1, - }), - ], - }) - invoice.action_post() - - self.assertEqual(invoice.asset_ids.account_asset_id, other_account_on_bill, - "The account should be the one from the bill, not the model") - - asset_form = Form(invoice.asset_ids) - asset_form.model_id = asset_model - - self.assertEqual(asset_form.account_asset_id, other_account_on_bill, "We keep the account from the bill") - - def test_asset_reevaluation_degressive_linear(self): - """ Tests the reevaluation of an asset in degressive_then_linear with a gross increase""" - asset = self.env['account.asset'].create({ - 'method_period': '12', - 'method_number': 5, - 'name': "Car with purple sticker", - 'original_value': 10000.0, - 'acquisition_date': fields.Date.today() - relativedelta(years=2), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'prorata_computation_type': 'none', - 'method': 'degressive_then_linear', - 'method_progress_factor': 0.4, - }) - asset.validate() - self.assertRecordValues(asset.depreciation_move_ids, [{ - 'depreciation_value': 4000, - 'asset_remaining_value': 6000, - 'state': 'posted', - }, { - 'depreciation_value': 2400, - 'asset_remaining_value': 3600, - 'state': 'posted', - }, { - 'depreciation_value': 2000, - 'asset_remaining_value': 1600, - 'state': 'draft', - }, { - 'depreciation_value': 1600, - 'asset_remaining_value': 0, - 'state': 'draft', - }]) - self.env['asset.modify'].create({ - 'name': "Inflation made it take 20%!", - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'asset_id': asset.id, - 'value_residual': 5600, - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [{ - # (2000 + 2000*6400/3600) / 5 - 'depreciation_value': 1111.11, - 'asset_remaining_value': 888.89, - 'state': 'draft', - }, { - 'depreciation_value': 888.89, - 'asset_remaining_value': 0, - 'state': 'draft', - }]) - - def test_asset_move_type(self): - """ Test the field asset_move_type set on account.move describing the - relation that a move can have towards an asset - """ - asset_account_id = self.company_data['default_account_assets'].id - - bill = self.env['account.move'].create([ - { - 'move_type': 'in_invoice', - 'invoice_date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'date': fields.Date.today() + relativedelta(months=-6, days=-1), - 'partner_id': self.partner_a.id, - 'invoice_line_ids': [Command.create({ - 'name': 'Truck', - 'account_id': asset_account_id, - 'quantity': 1.0, - 'price_unit': 1000.0, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - })], - }, - ]) - bill.action_post() - asset_line = bill.line_ids.filtered(lambda x: x.account_id.id == asset_account_id) - asset_form = Form(self.env['account.asset'].with_context(default_original_move_line_ids=asset_line.ids)) - asset_form.original_move_line_ids = asset_line - asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] - car = asset_form.save() - car.validate() - - # All depreciation move must be defined as depreciation - self.assertTrue(all(car.depreciation_move_ids.mapped(lambda m: m.asset_move_type == 'depreciation'))) - - # Negative revaluation - self.env['asset.modify'].create({ - 'name': 'Little scratch :(', - 'asset_id': car.id, - 'value_residual': car.book_value - 150, - 'date': fields.Date.today(), - }).modify() - - # Ensure that the added depreciation moves are one 'depreciation' and the other is 'negative_revaluation' - added_move_on_revaluation = car.depreciation_move_ids.filtered(lambda m: m.date == fields.Date.today()) - self.assertRecordValues(added_move_on_revaluation.sorted(lambda mv: mv.id), [ - {'asset_move_type': 'depreciation'}, - {'asset_move_type': 'negative_revaluation'} - ]) - - # Sell - closing_invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'invoice_line_ids': [Command.create( - {'price_unit': car.book_value + 100} # selling price: 849.46, net_gain_on_sale: 100.45 - )] - }) - - self.env['asset.modify'].create({ - 'asset_id': car.id, - 'modify_action': 'sell', - 'invoice_line_ids': closing_invoice.invoice_line_ids, - 'date': fields.Date.today(), - }).sell_dispose() - selling_move = car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') - selling_move.action_post() - - # Ensure that the added depreciation moves are one 'depreciation' and the other is 'sale' - added_move_on_sale = car.depreciation_move_ids.filtered(lambda m: m.date == fields.Date.today()) - added_move_on_revaluation - self.assertTrue(added_move_on_sale.asset_move_type == 'sale') - self.assertEqual(car.net_gain_on_sale, 100) - - # Create new asset to test positive revaluation and disposal - new_car = car.copy() - new_car.validate() - - # Positive revaluation - self.env['asset.modify'].create({ - 'name': 'New beautiful sticker :D', - 'asset_id': new_car.id, - 'value_residual': new_car.book_value + 50, - 'salvage_value': 0, - 'date': fields.Date.today(), - "account_asset_counterpart_id": self.assert_counterpart_account_id, - }).modify() - - self.assertEqual( - new_car.children_ids.original_move_line_ids.move_id.asset_move_type, - 'positive_revaluation', - "the original move of the child asset is set as 'positive_revaluation'" - ) - - disposal_action_view = self.env['asset.modify'].create({ - 'asset_id': new_car.id, - 'modify_action': 'dispose', - 'date': fields.Date.today() - }).sell_dispose() - - self.env['account.move'].browse(disposal_action_view['res_id']).action_post() - self.assertEqual(self.env['account.move'].browse(disposal_action_view['res_id']).asset_move_type, 'disposal') - - def test_asset_already_depreciated(self): - asset = self.env['account.asset'].create({ - 'method_period': '12', - 'method_number': 5, - 'name': "Car with purple sticker", - 'original_value': 10000.0, - 'acquisition_date': fields.Date.today() - relativedelta(years=1), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'prorata_computation_type': 'none', - 'already_depreciated_amount_import': 3000, - }) - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'date': fields.Date.today() - relativedelta(days=1), - 'name': 'Test reason', - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids, [{ - 'depreciation_value': 1000, - 'date': fields.Date.to_date('2021-12-31'), - }, { - 'depreciation_value': 2000, - 'date': fields.Date.to_date('2022-12-31'), - }, { - 'depreciation_value': 2000, - 'date': fields.Date.to_date('2023-12-31'), - }, { - 'depreciation_value': 2000, - 'date': fields.Date.to_date('2024-12-31'), - }, - ]) - - fully_depreciated_asset = self.env['account.asset'].create({ - 'method_period': '12', - 'method_number': 5, - 'name': "Car with purple sticker", - 'original_value': 10000.0, - 'acquisition_date': fields.Date.today() - relativedelta(years=2), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - 'prorata_computation_type': 'none', - 'salvage_value': 4000, - 'already_depreciated_amount_import': 6000, - }) - fully_depreciated_asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': fully_depreciated_asset.id, - 'date': fields.Date.today(), - 'modify_action': 'dispose', - }).sell_dispose() - self.assertEqual(len(fully_depreciated_asset.depreciation_move_ids), 1, "Only the disposal should be created") - - def test_asset_acquisition_date_from_bill(self): - """Test that the invoice date is used as acquisition date instead of date""" - self.company_data['default_account_assets'].create_asset = 'draft' - self.company_data['default_account_assets'].asset_model_ids = self.account_asset_model_fixedassets - - bill = self.env['account.move'].with_context(asset_type='purchase').create({ - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'date': '2020-06-15', - 'invoice_date': '2020-06-01', - 'invoice_line_ids': [Command.create({ - 'name': 'Insurance claim', - 'account_id': self.company_data['default_account_assets'].id, - 'price_unit': 450, - 'quantity': 1, - })], - }) - bill.action_post() - asset = bill.asset_ids - self.assertEqual(asset.acquisition_date, bill.invoice_date) - - def test_asset_write_multi_company(self): - assets = self.env['account.asset'].create([ - { - 'company_id': company_data['company'].id, - 'name': 'test asset', - } for company_data in [self.company_data, self.company_data_2] - ]) - self.assertEqual(assets[0].company_id, self.company_data['company']) - self.assertEqual(assets[1].company_id, self.company_data_2['company']) - assets.validate() - - def test_depreciation_moves_company_with_sub_company(self): - """The depreciation moves should have the company of the asset, even in multicompany setup""" - company = self.env.company - branch_x = self.env['res.company'].create({ - 'name': 'Branch X', - 'country_id': company.country_id.id, - 'parent_id': company.id, - }) - - asset_vals = { - 'method_period': '12', - 'method_number': 5, - 'name': "Car with purple sticker", - 'original_value': 10000.0, - 'acquisition_date': fields.Date.today() - relativedelta(years=1), - 'account_asset_id': self.company_data['default_account_assets'].id, - 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, - 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, - 'journal_id': self.company_data['default_journal_misc'].id, - } - - setup_list = [ - {'company_ids': (company + branch_x).ids, 'company_id': branch_x.id}, - {'company_ids': branch_x.ids, 'company_id': branch_x.id}, - {'company_ids': (company + branch_x).ids, 'company_id': company.id}, - {'company_ids': company.ids, 'company_id': company.id}, - ] - - expected_vals_list = [branch_x, branch_x, company, company] - - for setup, expected in zip(setup_list, expected_vals_list): - with self.subTest(setup=setup, expected_company=expected): - self.env.user.write({ - 'company_ids': [Command.set(setup['company_ids'])], - 'company_id': setup['company_id'], - }) - asset = self.env['account.asset'].create(asset_vals) - asset.compute_depreciation_board() - self.assertEqual(asset.depreciation_move_ids.mapped('company_id'), expected) diff --git a/addons/at_accounting/tests/test_account_auto_reconcile_wizard.py b/addons/at_accounting/tests/test_account_auto_reconcile_wizard.py deleted file mode 100644 index bca7fe7..0000000 --- a/addons/at_accounting/tests/test_account_auto_reconcile_wizard.py +++ /dev/null @@ -1,244 +0,0 @@ -from odoo import fields -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo.exceptions import UserError -from odoo.tests import tagged - - -@tagged('post_install', '-at_install') -class TestAccountAutoReconcileWizard(AccountTestInvoicingCommon): - """ Tests the account automatic reconciliation and its wizard. """ - - @classmethod - def setUpClass(cls): - super().setUpClass() - - cls.comp_curr = cls.company_data['currency'] - cls.foreign_curr = cls.setup_other_currency('EUR') - - cls.misc_journal = cls.company_data['default_journal_misc'] - cls.partners = cls.partner_a + cls.partner_b - cls.receivable_account = cls.company_data['default_account_receivable'] - cls.payable_account = cls.company_data['default_account_payable'] - cls.revenue_account = cls.company_data['default_account_revenue'] - cls.test_date = fields.Date.from_string('2016-01-01') - - def _create_many_lines(self): - self.line_1_group_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.line_2_group_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - self.line_3_group_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-03', partner=self.partner_a) - self.line_4_group_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-04', partner=self.partner_a) - self.line_5_group_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-05', partner=self.partner_a) - self.group_1 = self.line_1_group_1 + self.line_2_group_1 + self.line_3_group_1 + self.line_4_group_1 + self.line_5_group_1 - - self.line_1_group_2 = self.create_line_for_reconciliation(500.0, 500.0, self.comp_curr, '2016-01-01', partner=self.partner_b) - self.line_2_group_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.comp_curr, '2016-01-01', partner=self.partner_b) - self.line_3_group_2 = self.create_line_for_reconciliation(500.0, 500.0, self.comp_curr, '2017-01-02', partner=self.partner_b) - self.line_4_group_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.comp_curr, '2017-01-02', partner=self.partner_b) - self.group_2 = self.line_1_group_2 + self.line_2_group_2 + self.line_3_group_2 + self.line_4_group_2 - - self.line_1_group_3 = self.create_line_for_reconciliation(1500.0, 3000.0, self.foreign_curr, '2016-01-01', partner=self.partner_b) - self.line_2_group_3 = self.create_line_for_reconciliation(-1000.0, -3000.0, self.foreign_curr, '2017-01-01', partner=self.partner_b) - self.line_3_group_3 = self.create_line_for_reconciliation(3000.0, 3000.0, self.comp_curr, '2016-01-01', partner=self.partner_b) - self.line_4_group_3 = self.create_line_for_reconciliation(-3000.0, -3000.0, self.comp_curr, '2016-01-01', partner=self.partner_b) - self.group_3 = self.line_1_group_3 + self.line_2_group_3 + self.line_3_group_3 + self.line_4_group_3 - - self.line_1_group_4 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', account_1=self.payable_account, partner=self.partner_a) - self.line_2_group_4 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', account_1=self.payable_account, partner=self.partner_a) - self.group_4 = self.line_1_group_4 + self.line_2_group_4 - - def test_auto_reconcile_one_to_one(self): - self._create_many_lines() - should_be_reconciled = self.line_1_group_1 + self.line_2_group_1 + self.line_3_group_1 + self.line_4_group_1 \ - + self.line_1_group_2 + self.line_2_group_2 \ - + self.line_1_group_3 + self.line_2_group_3 + self.line_3_group_3 + self.line_4_group_3 - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - 'partner_ids': self.partners.ids, - 'search_mode': 'one_to_one', - }) - wizard.auto_reconcile() - - self.assertTrue(should_be_reconciled.full_reconcile_id) - self.assertEqual(self.line_1_group_1.full_reconcile_id, self.line_2_group_1.full_reconcile_id, - "Entries should be reconciled together since they are in the same group and have closer dates.") - self.assertEqual(self.line_3_group_1.full_reconcile_id, self.line_4_group_1.full_reconcile_id, - "Entries should be reconciled together since they are in the same group and have closer dates.") - self.assertEqual(self.line_1_group_2.full_reconcile_id, self.line_1_group_2.full_reconcile_id, - "Entries should be reconciled together since they are in the same group and have closer dates.") - self.assertEqual(self.line_1_group_3.full_reconcile_id, self.line_2_group_3.full_reconcile_id, - "Entries should be reconciled together since they are in the same group and have closer dates.") - self.assertEqual(self.line_3_group_3.full_reconcile_id, self.line_4_group_3.full_reconcile_id, - "Entries should be reconciled together since they are in the same group and have closer dates.") - self.assertNotEqual(self.line_2_group_3.full_reconcile_id, self.line_3_group_3.full_reconcile_id, - "Entries should NOT be reconciled together as they are of different currencies.") - self.assertFalse(self.line_5_group_1.reconciled, - "This entry shouldn't be reconciled since group 1 has an odd number of lines, they can't all be reconciled, and it's the most recent one.") - self.assertFalse((self.line_3_group_2 + self.line_4_group_2).full_reconcile_id, - "Entries shouldn't be reconciled since it's outside of accepted date range of the wizard.") - self.assertFalse((self.line_1_group_4 + self.line_2_group_4).full_reconcile_id, - "Entries shouldn't be reconciled since their account is out of the wizard's scope.") - - def test_auto_reconcile_zero_balance(self): - self._create_many_lines() - should_be_reconciled = self.line_1_group_2 + self.line_2_group_2 + self.group_3 - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - 'partner_ids': self.partners.ids, - 'search_mode': 'zero_balance', - }) - wizard.auto_reconcile() - - self.assertTrue(should_be_reconciled.full_reconcile_id) - self.assertFalse(self.group_1.full_reconcile_id, - "Entries shouldn't be reconciled since their total balance is not zero.") - self.assertEqual((self.line_1_group_2 + self.line_2_group_2).mapped('matching_number'), [self.line_1_group_2.matching_number] * 2, - "Entries should be reconciled together as their total balance is zero.") - self.assertEqual((self.line_1_group_3 + self.line_2_group_3).mapped('matching_number'), [self.line_1_group_3.matching_number] * 2, - "Entries should be reconciled together as their total balance is zero with the same currency.") - self.assertEqual((self.line_3_group_3 + self.line_4_group_3).mapped('matching_number'), [self.line_3_group_3.matching_number] * 2, - "Lines 3 and 4 are reconciled but not with two first lines since their currency is different.") - self.assertFalse(self.group_4.full_reconcile_id, - "Entries shouldn't be reonciled since their account is out of the wizard's scope.") - - def test_nothing_to_auto_reconcile(self): - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - 'partner_ids': self.partners.ids, - 'search_mode': 'zero_balance', - }) - with self.assertRaises(UserError): - wizard.auto_reconcile() - - def test_auto_reconcile_no_account_nor_partner_one_to_one(self): - self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - }) - reconciled_amls = wizard._auto_reconcile_one_to_one() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_no_account_nor_partner_zero_balance(self): - self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - }) - reconciled_amls = wizard._auto_reconcile_zero_balance() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_no_account_one_to_one(self): - self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'partner_ids': self.partners.ids, - }) - reconciled_amls = wizard._auto_reconcile_one_to_one() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_no_account_zero_balance(self): - self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'partner_ids': self.partners.ids, - }) - reconciled_amls = wizard._auto_reconcile_zero_balance() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_no_partner_one_to_one(self): - self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - }) - reconciled_amls = wizard._auto_reconcile_one_to_one() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_no_partner_zero_balance(self): - self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - }) - reconciled_amls = wizard._auto_reconcile_zero_balance() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_rounding_one_to_one(self): - """ Checks that two lines with different values, currency rounding aside, are reconciled in one-to-one mode. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - # Need to manually update the values to bypass ORM - self.env.cr.execute( - """ - UPDATE account_move_line SET amount_residual_currency = 1000.0000001 WHERE id = %(line_1_id)s; - UPDATE account_move_line SET amount_residual_currency = -999.999999 WHERE id = %(line_2_id)s; - """, - {'line_1_id': line_1.id, 'line_2_id': line_2.id} - ) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - }) - reconciled_amls = wizard._auto_reconcile_one_to_one() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_auto_reconcile_rounding_zero_balance(self): - """ Checks that two lines with different values, currency rounding aside, are reconciled in zero balance mode. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-01', partner=self.partner_a) - line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-02', partner=self.partner_a) - # Need to manually update the values to bypass ORM - self.env.cr.execute( - """ - UPDATE account_move_line SET amount_residual_currency = 1000.0000001 WHERE id = %(line_1_id)s; - UPDATE account_move_line SET amount_residual_currency = -999.999999 WHERE id = %(line_2_id)s; - """, - {'line_1_id': line_1.id, 'line_2_id': line_2.id} - ) - wizard = self.env['account.auto.reconcile.wizard'].new({ - 'from_date': '2016-01-01', - 'to_date': '2017-01-01', - 'account_ids': self.receivable_account.ids, - }) - reconciled_amls = wizard._auto_reconcile_zero_balance() - self.assertTrue(reconciled_amls.full_reconcile_id) - - def test_preset_wizard(self): - """ Tests that giving lines_ids to wizard presets correctly values. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-30', partner=self.partner_a) - line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.comp_curr, '2016-01-31', partner=self.partner_a) - wizard = self.env['account.auto.reconcile.wizard'].with_context(domain=[('id', 'in', (line_1 + line_2).ids)]).create({}) - self.assertRecordValues(wizard, [{ - 'account_ids': self.receivable_account.ids, - 'partner_ids': self.partner_a.ids, - 'from_date': fields.Date.from_string('2016-01-30'), - 'to_date': fields.Date.from_string('2016-01-31'), - 'search_mode': 'zero_balance', - }]) - - line_3 = self.create_line_for_reconciliation(1000.0, 1000.0, self.comp_curr, '2016-01-31', partner=self.partner_a) - line_4 = self.create_line_for_reconciliation(-500.0, -500.0, self.comp_curr, '2016-02-28', partner=None) - wizard = self.env['account.auto.reconcile.wizard'].with_context(domain=[('id', 'in', (line_3 + line_4).ids)]).create({}) - self.assertRecordValues(wizard, [{ - 'account_ids': self.receivable_account.ids, - 'partner_ids': [], - 'from_date': fields.Date.from_string('2016-01-31'), - 'to_date': fields.Date.from_string('2016-02-28'), - 'search_mode': 'one_to_one', - }]) diff --git a/addons/at_accounting/tests/test_account_fiscal_year.py b/addons/at_accounting/tests/test_account_fiscal_year.py deleted file mode 100644 index 1fad047..0000000 --- a/addons/at_accounting/tests/test_account_fiscal_year.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo.tests import tagged -from odoo import fields - - -@tagged('post_install', '-at_install') -class TestFiscalPosition(AccountTestInvoicingCommon): - - def check_compute_fiscal_year(self, company, date, expected_date_from, expected_date_to): - '''Compute the fiscal year at a certain date for the company passed as parameter. - Then, check if the result matches the 'expected_date_from'/'expected_date_to' dates. - - :param company: The company. - :param date: The date belonging to the fiscal year. - :param expected_date_from: The expected date_from after computation. - :param expected_date_to: The expected date_to after computation. - ''' - current_date = fields.Date.from_string(date) - res = company.compute_fiscalyear_dates(current_date) - self.assertEqual(res['date_from'], fields.Date.from_string(expected_date_from)) - self.assertEqual(res['date_to'], fields.Date.from_string(expected_date_to)) - - def test_default_fiscal_year(self): - '''Basic case with a fiscal year xxxx-01-01 - xxxx-12-31.''' - company = self.env.company - company.fiscalyear_last_day = 31 - company.fiscalyear_last_month = '12' - - self.check_compute_fiscal_year( - company, - '2017-12-31', - '2017-01-01', - '2017-12-31', - ) - - self.check_compute_fiscal_year( - company, - '2017-01-01', - '2017-01-01', - '2017-12-31', - ) - - def test_leap_fiscal_year_1(self): - '''Case with a leap year ending the 29 February.''' - company = self.env.company - company.fiscalyear_last_day = 29 - company.fiscalyear_last_month = '2' - - self.check_compute_fiscal_year( - company, - '2016-02-29', - '2015-03-01', - '2016-02-29', - ) - - self.check_compute_fiscal_year( - company, - '2015-03-01', - '2015-03-01', - '2016-02-29', - ) - - def test_leap_fiscal_year_2(self): - '''Case with a leap year ending the 28 February.''' - company = self.env.company - company.fiscalyear_last_day = 28 - company.fiscalyear_last_month = '2' - - self.check_compute_fiscal_year( - company, - '2016-02-29', - '2015-03-01', - '2016-02-29', - ) - - self.check_compute_fiscal_year( - company, - '2016-03-01', - '2016-03-01', - '2017-02-28', - ) - - def test_custom_fiscal_year(self): - '''Case with custom fiscal years.''' - company = self.env.company - company.fiscalyear_last_day = 31 - company.fiscalyear_last_month = '12' - - # Create custom fiscal year covering the 6 first months of 2017. - self.env['account.fiscal.year'].create({ - 'name': '6 month 2017', - 'date_from': '2017-01-01', - 'date_to': '2017-05-31', - 'company_id': company.id, - }) - - # Check before the custom fiscal year). - self.check_compute_fiscal_year( - company, - '2017-02-01', - '2017-01-01', - '2017-05-31', - ) - - # Check after the custom fiscal year. - self.check_compute_fiscal_year( - company, - '2017-11-01', - '2017-06-01', - '2017-12-31', - ) - - # Create custom fiscal year covering the 3 last months of 2017. - self.env['account.fiscal.year'].create({ - 'name': 'last 3 month 2017', - 'date_from': '2017-10-01', - 'date_to': '2017-12-31', - 'company_id': company.id, - }) - - # Check inside the custom fiscal years. - self.check_compute_fiscal_year( - company, - '2017-07-01', - '2017-06-01', - '2017-09-30', - ) diff --git a/addons/at_accounting/tests/test_account_reconcile_wizard.py b/addons/at_accounting/tests/test_account_reconcile_wizard.py deleted file mode 100644 index 97af078..0000000 --- a/addons/at_accounting/tests/test_account_reconcile_wizard.py +++ /dev/null @@ -1,765 +0,0 @@ -import re - -from odoo import Command, fields -from odoo.exceptions import UserError -from odoo.tests import tagged - -from odoo.addons.account.tests.common import AccountTestInvoicingCommon - - -@tagged('post_install', '-at_install') -class TestAccountReconcileWizard(AccountTestInvoicingCommon): - """ Tests the account reconciliation and its wizard. """ - - @classmethod - def setUpClass(cls): - super().setUpClass() - - cls.receivable_account = cls.company_data['default_account_receivable'] - cls.payable_account = cls.company_data['default_account_payable'] - cls.revenue_account = cls.company_data['default_account_revenue'] - cls.payable_account_2 = cls.env['account.account'].create({ - 'name': 'Payable Account 2', - 'account_type': 'liability_current', - 'code': 'PAY2.TEST', - 'reconcile': True - }) - cls.write_off_account = cls.env['account.account'].create({ - 'name': 'Write-Off Account', - 'account_type': 'liability_current', - 'code': 'WO.TEST', - 'reconcile': False - }) - - cls.misc_journal = cls.company_data['default_journal_misc'] - cls.test_date = fields.Date.from_string('2016-01-01') - cls.company_currency = cls.company_data['currency'] - cls.foreign_currency = cls.setup_other_currency('EUR') - cls.foreign_currency_2 = cls.setup_other_currency('XAF', rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)]) - - # ------------------------------------------------------------------------- - # HELPERS - # ------------------------------------------------------------------------- - def assertWizardReconcileValues(self, selected_lines, input_values, wo_expected_values, expected_transfer_values=None): - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=selected_lines.ids, - ).new(input_values) - if expected_transfer_values: - transfer_move = wizard.create_transfer() - # transfer move values - self.assertRecordValues(transfer_move.line_ids.sorted('balance'), expected_transfer_values) - # transfer warning message - self.assertTrue(wizard.transfer_warning_message) - regex_match = re.findall(r'([+-]*\d*,*\d+\.*\d+)', wizard.transfer_warning_message) - # match transferred amount - self.assertEqual( - float(regex_match[0].replace(',', '')), - transfer_move.amount_total_in_currency_signed or transfer_move.amount_total_signed - ) - transfer_from_account = transfer_move.line_ids.filtered(lambda aml: 'Transfer from' in aml.name).account_id - transfer_to_account = transfer_move.line_ids.account_id - transfer_from_account - transfer_from_amls = transfer_move.line_ids.filtered(lambda aml: aml.account_id == transfer_from_account) - transfer_amount = sum(aml.balance for aml in transfer_from_amls) - # match account codes - if transfer_amount > 0: - self.assertEqual(regex_match[1:], [transfer_from_account.code, transfer_to_account.code]) - else: - self.assertEqual(regex_match[1:], [transfer_to_account.code, transfer_from_account.code]) - write_off_move = wizard.create_write_off() - self.assertRecordValues(write_off_move.line_ids.sorted('balance'), wo_expected_values) - wizard.reconcile() - if wizard.allow_partials or ( - wizard.edit_mode - and wizard.reco_currency_id.compare_amounts(wizard.edit_mode_amount_currency, wizard.amount_currency) - ): - # partial reconcile - self.assertTrue(len(selected_lines.matched_debit_ids) > 0 or len(selected_lines.matched_credit_ids) > 0) - else: - # full reconcile - self.assertTrue(selected_lines.full_reconcile_id) - self.assertRecordValues( - selected_lines, - [{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(selected_lines), - ) - - # ------------------------------------------------------------------------- - # TESTS - # ------------------------------------------------------------------------- - def test_wizard_should_not_open(self): - """ Test that when we reconcile two lines that belong to the same account and have a 0 balance should - reconcile silently and not open the write-off wizard. - """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01') - (line_1 + line_2).action_reconcile() - self.assertRecordValues( - line_1 + line_2, - [{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * 2 - ) - - def test_wizard_should_open(self): - """ Test that when a write-off is required (because of transfer or non-zero balance) the wizard opens. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01') - line_3 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01') - line_4 = self.create_line_for_reconciliation(-900.0, -900.0, self.company_currency, '2016-01-01', account_1=self.payable_account) - for batch, sub_test_name in ( - (line_1 + line_2, 'Batch with non-zero balance in company currency'), - (line_1 + line_3, 'Batch with non-zero balance in foreign currency'), - (line_1 + line_4, 'Batch with different accounts'), - ): - with self.subTest(sub_test_name=sub_test_name): - returned_action = batch.action_reconcile() - self.assertEqual(returned_action.get('res_model'), 'account.reconcile.wizard') - - def test_reconcile_silently_same_account(self): - """ When balance is 0 we can silently reconcile items. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01') - lines = (line_1 + line_2) - lines.action_reconcile() - self.assertTrue(lines.full_reconcile_id) - self.assertRecordValues( - lines, - [{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(lines), - ) - - def test_reconcile_silently_transfer(self): - """ When balance is 0, and we need a transfer, we do the transfer+reconcile silently. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01', account_1=self.payable_account) - lines = (line_1 + line_2) - lines.action_reconcile() - self.assertTrue(lines.full_reconcile_id) - self.assertRecordValues( - lines, - [{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(lines), - ) - - def test_write_off_same_currency(self): - """ Reconciliation of two lines with no transfer/foreign currencies/taxes/reco models.""" - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - write_off_expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -500.0}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', 'balance': 500.0}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values) - - def test_write_off_one_foreign_currency(self): - """ Reconciliation of two lines with one of the two using foreign currency should reconcile in foreign currency.""" - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -500.0, 'amount_currency': -1500.0, 'currency_id': self.foreign_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 500.0, 'amount_currency': 1500.0, 'currency_id': self.foreign_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values) - - def test_write_off_mixed_foreign_currencies(self): - """ Write off with multiple currencies should reconcile in company currency.""" - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01') - line_3 = self.create_line_for_reconciliation(-400.0, -2400.0, self.foreign_currency_2, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2 + line_3, wizard_input_values, expected_values) - - def test_write_off_one_foreign_currency_change_rate(self): - """ Tests that write-off use the correct rate from/at wizard's date. """ - foreign_currency = self.setup_other_currency('CAD', rounding=0.001, rates=[('2016-01-01', 0.5), ('2017-01-01', 1 / 3)]) - new_date = fields.Date.from_string('2017-02-01') - line_1 = self.create_line_for_reconciliation(-2000.0, -2000.0, self.company_currency, '2017-01-01') # conversion in 2017 => -666.67🍫 - line_2 = self.create_line_for_reconciliation(2000.0, 1000.0, foreign_currency, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': new_date, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -1000.0, 'amount_currency': -333.333, 'currency_id': foreign_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 1000.0, 'amount_currency': 333.333, 'currency_id': foreign_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values) - - def test_write_off_mixed_foreign_currencies_change_rate(self): - """ Tests that write-off use the correct rate from/at wizard's date. """ - new_date = fields.Date.from_string('2017-02-01') - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01') - line_3 = self.create_line_for_reconciliation(-400.0, -2400.0, self.foreign_currency_2, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': new_date, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2 + line_3, wizard_input_values, expected_values) - - def test_write_off_both_same_foreign_currency_ensure_no_exchange_diff(self): - """ Test that if both AMLs have the same foreign currency and rate, the amount in company currency - is computed on the write-off in such a way that no exchange diff is created. - """ - foreign_currency = self.setup_other_currency('CAD', rounding=0.01, rates=[('2016-01-01', 1 / 0.225)]) - new_date = fields.Date.from_string('2017-02-01') - line_1 = self.create_line_for_reconciliation(21.38, 95.0, foreign_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(1.13, 5.0, foreign_currency, '2016-01-01') - line_3 = self.create_line_for_reconciliation(1.13, 5.0, foreign_currency, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': new_date, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -23.64, 'amount_currency': -105.0, 'currency_id': foreign_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 23.64, 'amount_currency': 105.0, 'currency_id': foreign_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2 + line_3, wizard_input_values, expected_values) - - def test_write_off_with_transfer_account_same_currency(self): - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(100.0, 100.0, self.company_currency, '2016-01-01', account_1=self.payable_account) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_transfer_values = [ - {'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id}, - {'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}', - 'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id}, - ] - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -1100.0, 'amount_currency': -1100.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 1100.0, 'amount_currency': 1100.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values) - - def test_write_off_with_transfer_account_one_foreign_currency(self): - line_1 = self.create_line_for_reconciliation(1100.0, 1100.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(100.0, 300.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_transfer_values = [ - {'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': -100.0, 'amount_currency': -300.0, 'currency_id': self.foreign_currency.id}, - {'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}', - 'balance': 100.0, 'amount_currency': 300.0, 'currency_id': self.foreign_currency.id}, - ] - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -1200.0, 'amount_currency': -3600.0, 'currency_id': self.foreign_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 1200.0, 'amount_currency': 3600.0, 'currency_id': self.foreign_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values) - - def test_write_off_with_complex_transfer(self): - partner_1 = self.env['res.partner'].create({'name': 'Test Partner 1'}) - partner_2 = self.env['res.partner'].create({'name': 'Test Partner 2'}) - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01', partner=partner_2) - line_2 = self.create_line_for_reconciliation(-100.0, -300.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account, partner=partner_1) - line_3 = self.create_line_for_reconciliation(-200.0, -200.0, self.company_currency, '2016-01-01', account_1=self.payable_account, partner=partner_2) - line_4 = self.create_line_for_reconciliation(-200.0, -600.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account, partner=partner_2) - line_5 = self.create_line_for_reconciliation(-200.0, -600.0, self.foreign_currency, '2016-01-01', account_1=self.payable_account, partner=partner_2) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_transfer_values = [ - {'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}', - 'balance': -400.0, 'amount_currency': -1200.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_2.id}, - {'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}', - 'balance': -200.0, 'amount_currency': -200.0, 'currency_id': self.company_currency.id, 'partner_id': partner_2.id}, - {'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}', - 'balance': -100.0, 'amount_currency': -300.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_1.id}, - {'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': 100.0, 'amount_currency': 300.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_1.id}, - {'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': 200.0, 'amount_currency': 200.0, 'currency_id': self.company_currency.id, 'partner_id': partner_2.id}, - {'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': 400.0, 'amount_currency': 1200.0, 'currency_id': self.foreign_currency.id, 'partner_id': partner_2.id}, - ] - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -300.0, 'amount_currency': -900.0, 'currency_id': self.foreign_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 300.0, 'amount_currency': 900.0, 'currency_id': self.foreign_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2 + line_3 + line_4 + line_5, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values) - - def test_write_off_with_tax(self): - """ Tests write-off with a tax set on the wizard. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01') - tax_recover_account_id = self.env['account.account'].create({ - 'name': 'Tax Account Test', - 'account_type': 'liability_current', - 'code': 'TAX.TEST', - 'reconcile': False - }) - base_tag = self.env['account.account.tag'].create({ - 'applicability': 'taxes', - 'name': 'base_tax_tag', - 'country_id': self.company_data['company'].country_id.id, - }) - tax_tag = self.env['account.account.tag'].create({ - 'applicability': 'taxes', - 'name': 'tax_tax_tag', - 'country_id': self.company_data['company'].country_id.id, - }) - tax_id = self.env['account.tax'].create({ - 'name': 'tax_test', - 'amount_type': 'percent', - 'amount': 25.0, - 'type_tax_use': 'sale', - 'company_id': self.company_data['company'].id, - 'invoice_repartition_line_ids': [ - Command.create({'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': [Command.set(base_tag.ids)]}), - Command.create({'factor_percent': 100, 'account_id': tax_recover_account_id.id, 'tag_ids': [Command.set(tax_tag.ids)]}), - ], - 'refund_repartition_line_ids': [ - Command.create({'factor_percent': 100, 'repartition_type': 'base', 'tag_ids': [Command.set(base_tag.ids)]}), - Command.create({'factor_percent': 100, 'account_id': tax_recover_account_id.id, 'tag_ids': [Command.set(tax_tag.ids)]}), - ], - }) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'tax_id': tax_id.id, - 'allow_partials': False, - 'date': self.test_date, - } - write_off_expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -500.0}, - {'account_id': tax_recover_account_id.id, 'name': f'{tax_id.name}', 'balance': 100.0}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', 'balance': 400.0}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values) - - def test_reconcile_partials_allowed(self): - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01') - lines = line_1 + line_2 - wizard_input_values = { - 'allow_partials': True, - } - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=lines.ids, - ).new(wizard_input_values) - wizard.reconcile() - self.assertTrue(len(lines.matched_debit_ids) > 0 or len(lines.matched_credit_ids) > 0) - - def test_raise_lock_date_violation(self): - """ If a write-off violates the lock date we display a banner and change the date afterwards. """ - company_id = self.company_data['company'] - company_id.fiscalyear_lock_date = fields.Date.from_string('2016-12-01') - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-06-01') - line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-06-01') - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=(line_1 + line_2).ids, - ).new({'date': self.test_date}) - self.assertTrue(bool(wizard.lock_date_violated_warning_message)) - - def test_raise_reconcile_too_many_accounts(self): - """ If you try to reconcile lines from more than 2 accounts, it should raise an error. """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01', account_1=self.payable_account) - line_3 = self.create_line_for_reconciliation(-500.0, -500.0, self.company_currency, '2016-01-01', account_1=self.payable_account_2) - with self.assertRaises(UserError): - (line_1 + line_2 + line_3).action_reconcile() - - def test_reconcile_no_receivable_no_payable_account(self): - """ If you try to reconcile lines in an account that is neither from payable nor receivable - it should reconcile in company currency. - """ - account = self.company_data['default_account_expense'] - account.reconcile = True - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01', account_1=account) - line_2 = self.create_line_for_reconciliation(-500.0, -1500.0, self.foreign_currency, '2016-01-01', account_1=account) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_values = [ - {'account_id': account.id, 'name': 'Write-Off Test Label', - 'balance': -500.0, 'amount_currency': -500.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 500.0, 'amount_currency': 500.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values) - - def test_reconcile_exchange_diff_foreign_currency(self): - """ When reconciling exchange_diff with amount_residual_currency = 0 we need to reconcile in company_currency. - """ - exchange_gain_account = self.company_data['company'].income_currency_exchange_account_id - exchange_gain_account.reconcile = True - line_1 = self.create_line_for_reconciliation(150.0, 0.0, self.foreign_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-100.0, 0.0, self.foreign_currency, '2016-01-01', account_1=exchange_gain_account) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - # Note the transfer will always be in the currency of the line transferred - expected_transfer_values = [ - {'account_id': self.receivable_account.id, 'name': f'Transfer from {exchange_gain_account.display_name}', - 'balance': -100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id}, - {'account_id': exchange_gain_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': 100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id}, - ] - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -50.0, 'amount_currency': -50.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 50.0, 'amount_currency': 50.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values) - - def test_write_off_on_same_account(self): - """ When creating a write-off in the same account than the one used by the lines to reconcile, - the lines and the write-off should be fully reconciled. - """ - line_1 = self.create_line_for_reconciliation(1000.0, 1000.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(2000.0, 2000.0, self.company_currency, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.receivable_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - write_off_expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -3000.0}, - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': 3000.0}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values) - - def test_reconcile_exchange_diff_foreign_currency_full(self): - """ When reconciling exchange_diff with amount_residual_currency = 0 we need to reconcile in company_currency. - """ - exchange_gain_account = self.company_data['company'].income_currency_exchange_account_id - exchange_gain_account.reconcile = True - line_1 = self.create_line_for_reconciliation(100.0, 0.0, self.foreign_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-100.0, 0.0, self.foreign_currency, '2016-01-01', account_1=exchange_gain_account) - lines = line_1 + line_2 - lines.action_reconcile() - self.assertTrue(lines.full_reconcile_id) - self.assertRecordValues( - lines, - [{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}] * len(lines), - ) - - def test_write_off_kpmg_case(self): - """ Test that write-off does a full reconcile with 2 foreign currencies using a custom exchange rate. """ - new_date = fields.Date.from_string('2017-02-01') - line_1 = self.create_line_for_reconciliation(1000.0, 1500.0, self.foreign_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(-900.0, -5400.0, self.foreign_currency_2, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': new_date, - } - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, [ - { - 'account_id': self.receivable_account.id, - 'balance': -100.0, - 'amount_currency': -150.0, - 'currency_id': self.foreign_currency.id, - }, - { - 'account_id': self.write_off_account.id, - 'balance': 100.0, - 'amount_currency': 150.0, - 'currency_id': self.foreign_currency.id, - }, - ]) - - def test_write_off_multi_curr_multi_residuals_force_partials(self): - """ Test that we raise an error when trying to reconcile lines with multiple residuals. - Here debit1 will be reconciled with credit1 first as they have the same currency. - Then residual of debit1 will try to reconcile with debit2 which is impossible - => 2 residuals both in foreign currency, we don't know in which currency we should make the write-off - => We should only allow partial reconciliation. """ - debit_1 = self.create_line_for_reconciliation(2000.0, 12000.0, self.foreign_currency_2, '2016-01-01') - credit_1 = self.create_line_for_reconciliation(-1000.0, -6000.0, self.foreign_currency_2, '2016-01-01') - debit_2 = self.create_line_for_reconciliation(2000.0, 3000.0, self.foreign_currency, '2016-01-01') - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=(debit_1 + debit_2 + credit_1).ids, - ).new() - self.assertRecordValues(wizard, [{'force_partials': True, 'allow_partials': True}]) - - def test_write_off_multi_curr_multi_residuals_exch_diff_force_partials(self): - debit_1 = self.create_line_for_reconciliation(2000.0, 0.0, self.foreign_currency_2, '2016-01-01') - credit_1 = self.create_line_for_reconciliation(-1000.0, 0.0, self.foreign_currency_2, '2016-01-01') - debit_2 = self.create_line_for_reconciliation(2000.0, 0.0, self.foreign_currency, '2016-01-01') - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=(debit_1 + debit_2 + credit_1).ids, - ).new() - self.assertRecordValues(wizard, [{'force_partials': True, 'allow_partials': True}]) - - def test_reconcile_with_partner_change(self): - partner_1 = self.env['res.partner'].create({'name': 'Test Partner 1'}) - partner_2 = self.env['res.partner'].create({'name': 'Test Partner 2'}) - line_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01', partner=partner_1) - line_2 = self.create_line_for_reconciliation(2000.0, 2000.0, self.company_currency, '2016-01-01') - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.receivable_account.id, - 'to_partner_id': partner_2.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - 'tax_id': self.tax_sale_a.id, - } - write_off_expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -1000.0, 'partner_id': partner_1.id}, - {'account_id': self.company_data['default_account_tax_sale'].id, 'name': '15%', 'balance': 130.43, 'partner_id': partner_2.id}, - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': 869.57, 'partner_id': partner_2.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values) - - def test_reconcile_with_partner_change_and_transfer(self): - partner_1 = self.env['res.partner'].create({'name': 'Test Partner 1'}) - partner_2 = self.env['res.partner'].create({'name': 'Test Partner 2'}) - line_1 = self.create_line_for_reconciliation(-1000.0, -1000.0, self.company_currency, '2016-01-01', account_1=self.payable_account) - line_2 = self.create_line_for_reconciliation(2000.0, 2000.0, self.company_currency, '2016-01-01', partner=partner_1) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.receivable_account.id, - 'to_partner_id': partner_2.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_transfer_values = [ - {'account_id': self.receivable_account.id, 'name': f'Transfer from {self.payable_account.display_name}', - 'balance': -1000.0, 'amount_currency': -1000.0, 'currency_id': self.company_currency.id}, - {'account_id': self.payable_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': 1000.0, 'amount_currency': 1000.0, 'currency_id': self.company_currency.id}, - ] - write_off_expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': -1000.0, 'partner_id': partner_1.id}, - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', 'balance': 1000.0, 'partner_id': partner_2.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, write_off_expected_values, expected_transfer_values) - - def test_reconcile_edit_mode_partial_foreign_curr(self): - line_1 = self.create_line_for_reconciliation(100.0, 300.0, self.foreign_currency, '2016-01-01') - wizard_input_values = { - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'date': self.test_date, - 'edit_mode_amount_currency': 30.0, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -10.0, 'amount_currency': -30.0, 'currency_id': self.foreign_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 10.0, 'amount_currency': 30.0, 'currency_id': self.foreign_currency.id}, - ] - self.assertWizardReconcileValues(line_1, wizard_input_values, expected_values) - - def test_reconcile_edit_mode_partial_company_curr(self): - line_1 = self.create_line_for_reconciliation(300.0, 300.0, self.company_currency, '2016-01-01') - wizard_input_values = { - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'date': self.test_date, - 'edit_mode_amount_currency': 100.0, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -100.0, 'amount_currency': -100.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 100.0, 'amount_currency': 100.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1, wizard_input_values, expected_values) - - def test_reconcile_edit_mode_partial_wrong_amount_raises(self): - line_1 = self.create_line_for_reconciliation(300.0, 300.0, self.company_currency, '2016-01-01') - wizard_input_values = { - 'account_id': self.write_off_account.id, - } - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=line_1.ids, - ).create(wizard_input_values) - with self.assertRaisesRegex(UserError, 'The amount of the write-off'): - wizard.edit_mode_amount_currency = -100.0 - - def test_reconcile_edit_mode_full_reconcile(self): - line_1 = self.create_line_for_reconciliation(300.0, 300.0, self.company_currency, '2016-01-01') - wizard_input_values = { - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'edit_mode_amount_currency': 300.0, - } - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -300.0, 'amount_currency': -300.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 300.0, 'amount_currency': 300.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1, wizard_input_values, expected_values) - - def test_reconcile_same_currency_same_side_not_recpay(self): - """ - Test the reconciliation with two lines on the same side (debit/credit), same currency and not on a receivable/payable account - """ - current_assets_account = self.company_data['default_account_assets'].copy({'name': 'Current Assets', 'account_type': 'asset_current', 'reconcile': True}) - line_1 = self.create_line_for_reconciliation(200, 200, self.company_currency, '2016-01-01', current_assets_account) - line_2 = self.create_line_for_reconciliation(200, 200, self.company_currency, '2016-01-01', current_assets_account) - - # Test the opening of the wizard without input values - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=(line_1 + line_2).ids, - ).new() - - self.assertRecordValues(wizard, [{'is_write_off_required': True, 'amount': 400, 'amount_currency': 400}]) - - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_values = [ - {'account_id': current_assets_account.id, 'name': 'Write-Off Test Label', - 'balance': -400.0, 'amount_currency': -400.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 400.0, 'amount_currency': 400.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values) - - def test_reconcile_foreign_currency_same_side_not_recpay(self): - """ - Test the reconciliation with two lines on the same side (debit/credit), one foreign currency and not on a receivable/payable account - """ - current_assets_account = self.company_data['default_account_assets'].copy({'name': 'Current Assets', 'account_type': 'asset_current', 'reconcile': True}) - line_1 = self.create_line_for_reconciliation(200, 300, self.foreign_currency, '2016-01-01', current_assets_account) - line_2 = self.create_line_for_reconciliation(200, 200, self.company_currency, '2016-01-01', current_assets_account) - - # Test the opening of the wizard without input values - wizard = self.env['account.reconcile.wizard'].with_context( - active_model='account.move.line', - active_ids=(line_1 + line_2).ids, - ).new() - - self.assertRecordValues(wizard, [{'is_write_off_required': True, 'amount': 400, 'amount_currency': 400}]) - - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - expected_values = [ - {'account_id': current_assets_account.id, 'name': 'Write-Off Test Label', - 'balance': -400.0, 'amount_currency': -400.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 400.0, 'amount_currency': 400.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values) - - def test_reconcile_same_side_exch_diff(self): - """ - Test the reconciliation with two lines on the same side (debit/credit), one exchange diff in foreign currency, - one regular aml in company currency - """ - exchange_gain_account = self.company_data['company'].income_currency_exchange_account_id - exchange_gain_account.reconcile = True - line_1 = self.create_line_for_reconciliation(150.0, 150.0, self.company_currency, '2016-01-01') - line_2 = self.create_line_for_reconciliation(100.0, 0.0, self.foreign_currency, '2016-01-01', account_1=exchange_gain_account) - wizard_input_values = { - 'journal_id': self.misc_journal.id, - 'account_id': self.write_off_account.id, - 'label': 'Write-Off Test Label', - 'allow_partials': False, - 'date': self.test_date, - } - # Note the transfer will always be in the currency of the line transferred - expected_transfer_values = [ - {'account_id': exchange_gain_account.id, 'name': f'Transfer to {self.receivable_account.display_name}', - 'balance': -100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id}, - {'account_id': self.receivable_account.id, 'name': f'Transfer from {exchange_gain_account.display_name}', - 'balance': 100.0, 'amount_currency': 0.0, 'currency_id': self.foreign_currency.id}, - ] - expected_values = [ - {'account_id': self.receivable_account.id, 'name': 'Write-Off Test Label', - 'balance': -250.0, 'amount_currency': -250.0, 'currency_id': self.company_currency.id}, - {'account_id': self.write_off_account.id, 'name': 'Write-Off Test Label', - 'balance': 250.0, 'amount_currency': 250.0, 'currency_id': self.company_currency.id}, - ] - self.assertWizardReconcileValues(line_1 + line_2, wizard_input_values, expected_values, expected_transfer_values=expected_transfer_values) diff --git a/addons/at_accounting/tests/test_analytic_reports.py b/addons/at_accounting/tests/test_analytic_reports.py deleted file mode 100644 index b623a0a..0000000 --- a/addons/at_accounting/tests/test_analytic_reports.py +++ /dev/null @@ -1,708 +0,0 @@ -from odoo import Command -from odoo.tests import tagged - -from .common import TestAccountReportsCommon - - -@tagged('post_install', '-at_install') -class TestAnalyticReport(TestAccountReportsCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - - cls.env.user.groups_id += cls.env.ref( - 'analytic.group_analytic_accounting') - cls.report = cls.env.ref('at_accounting.profit_and_loss') - cls.report.write({'filter_analytic': True}) - - cls.analytic_plan_parent = cls.env['account.analytic.plan'].create({ - 'name': 'Plan Parent', - }) - cls.analytic_plan_child = cls.env['account.analytic.plan'].create({ - 'name': 'Plan Child', - 'parent_id': cls.analytic_plan_parent.id, - }) - - cls.analytic_account_parent = cls.env['account.analytic.account'].create({ - 'name': 'Account 1', - 'plan_id': cls.analytic_plan_parent.id - }) - cls.analytic_account_parent_2 = cls.env['account.analytic.account'].create({ - 'name': 'Account 2', - 'plan_id': cls.analytic_plan_parent.id - }) - cls.analytic_account_child = cls.env['account.analytic.account'].create({ - 'name': 'Account 3', - 'plan_id': cls.analytic_plan_child.id - }) - cls.analytic_account_parent_3 = cls.env['account.analytic.account'].create({ - 'name': 'Account 4', - 'plan_id': cls.analytic_plan_parent.id - }) - - def test_report_group_by_analytic_plan(self): - - out_invoice = self.env['account.move'].create([{ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2019-05-01', - 'invoice_date': '2019-05-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 200.0, - 'analytic_distribution': { - self.analytic_account_parent.id: 100, - }, - }), - Command.create({ - 'product_id': self.product_b.id, - 'price_unit': 200.0, - 'analytic_distribution': { - self.analytic_account_child.id: 100, - }, - }), - ] - }]) - out_invoice.action_post() - - options = self._generate_options( - self.report, - '2019-01-01', - '2019-12-31', - default_options={ - 'analytic_plans_groupby': [self.analytic_plan_parent.id, self.analytic_plan_child.id], - } - ) - - lines = self.report._get_lines(options) - - self.assertLinesValues( - # pylint: disable=bad-whitespace - lines, - [ 0, 1, 2], - [ - ['Revenue', 400.00, 200.00], - ['Less Costs of Revenue', 0.00, 0.00], - ['Gross Profit', 400.00, 200.00], - ['Less Operating Expenses', 0.00, 0.00], - ['Operating Income (or Loss)', 400.00, 200.00], - ['Plus Other Income', 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00], - ['Net Profit', 400.00, 200.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - def test_report_analytic_filter(self): - - out_invoice = self.env['account.move'].create([{ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2023-02-01', - 'invoice_date': '2023-02-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 1000.0, - 'analytic_distribution': { - self.analytic_account_parent.id: 100, - }, - }) - ] - }]) - out_invoice.action_post() - - options = self._generate_options( - self.report, - '2023-01-01', - '2023-12-31', - default_options={ - 'analytic_accounts': [self.analytic_account_parent.id], - } - ) - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1], - [ - ['Revenue', 1000.00], - ['Less Costs of Revenue', 0.00], - ['Gross Profit', 1000.00], - ['Less Operating Expenses', 0.00], - ['Operating Income (or Loss)', 1000.00], - ['Plus Other Income', 0.00], - ['Less Other Expenses', 0.00], - ['Net Profit', 1000.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - # Set the unused analytic account in filter, as no move is - # using this account, the column should be empty - options['analytic_accounts'] = [self.analytic_account_child.id] - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1], - [ - ['Revenue', 0.00], - ['Less Costs of Revenue', 0.00], - ['Gross Profit', 0.00], - ['Less Operating Expenses', 0.00], - ['Operating Income (or Loss)', 0.00], - ['Plus Other Income', 0.00], - ['Less Other Expenses', 0.00], - ['Net Profit', 0.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - def test_report_audit_analytic_filter(self): - out_invoice = self.env['account.move'].create([{ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2023-02-01', - 'invoice_date': '2023-02-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 1000.0, - 'analytic_distribution': { - self.analytic_account_parent.id: 100, - }, - }), - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 500.0, - 'analytic_distribution': { - self.analytic_account_child.id: 100, - }, - }), - ], - }]) - out_invoice.action_post() - - options = self._generate_options( - self.report, - '2023-01-01', - '2023-12-31', - default_options={ - 'analytic_accounts': [self.analytic_account_parent.id], - } - ) - - lines = self.report._get_lines(options) - - report_line = self.report.line_ids[0] - report_line_dict = next(x for x in lines if x['name'] == report_line.name) - - action_dict = self.report.action_audit_cell( - options, - self._get_audit_params_from_report_line(options, report_line, report_line_dict), - ) - - audited_lines = self.env['account.move.line'].search(action_dict['domain']) - self.assertEqual(audited_lines, out_invoice.invoice_line_ids[0], "Only the line with the parent account should be shown") - - def test_report_analytic_groupby_and_filter(self): - """ - Test that the analytic filter is applied on the groupby columns - """ - - out_invoice = self.env['account.move'].create([{ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2023-02-01', - 'invoice_date': '2023-02-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 1000.0, - 'analytic_distribution': { - self.analytic_account_parent.id: 40, - self.analytic_account_child.id: 60, - }, - }) - ] - }]) - out_invoice.action_post() - - # Test with only groupby - options = self._generate_options( - self.report, - '2023-01-01', - '2023-12-31', - default_options={ - 'analytic_accounts_groupby': [self.analytic_account_parent.id, self.analytic_account_child.id], - } - ) - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1, 2, 3], - [ - ['Revenue', 400.00, 600.00, 1000.00], - ['Less Costs of Revenue', 0.00, 0.00, 0.00], - ['Gross Profit', 400.00, 600.00, 1000.00], - ['Less Operating Expenses', 0.00, 0.00, 0.00], - ['Operating Income (or Loss)', 400.00, 600.00, 1000.00], - ['Plus Other Income', 0.00, 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00, 0.00], - ['Net Profit', 400.00, 600.00, 1000.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - # Adding analytic filter for the two analytic accounts used on the invoice line - # The two groupby columns should still be filled - options['analytic_accounts'] = [self.analytic_account_parent.id, self.analytic_account_child.id] - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1, 2, 3], - [ - ['Revenue', 400.00, 600.00, 1000.00], - ['Less Costs of Revenue', 0.00, 0.00, 0.00], - ['Gross Profit', 400.00, 600.00, 1000.00], - ['Less Operating Expenses', 0.00, 0.00, 0.00], - ['Operating Income (or Loss)', 400.00, 600.00, 1000.00], - ['Plus Other Income', 0.00, 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00, 0.00], - ['Net Profit', 400.00, 600.00, 1000.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - # Keep only first analytic account on filter, the groupby column - # for this account should still be filled, unlike the other - options['analytic_accounts'] = [self.analytic_account_parent.id] - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1, 2, 3], - [ - ['Revenue', 400.00, 0.00, 1000.00], - ['Less Costs of Revenue', 0.00, 0.00, 0.00], - ['Gross Profit', 400.00, 0.00, 1000.00], - ['Less Operating Expenses', 0.00, 0.00, 0.00], - ['Operating Income (or Loss)', 400.00, 0.00, 1000.00], - ['Plus Other Income', 0.00, 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00, 0.00], - ['Net Profit', 400.00, 0.00, 1000.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - # Keep only first analytic account on filter, the groupby column - # for this account should still be filled, unlike the other - options['analytic_accounts'] = [self.analytic_account_child.id] - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1, 2, 3], - [ - ['Revenue', 0.00, 600.00, 1000.00], - ['Less Costs of Revenue', 0.00, 0.00, 0.00], - ['Gross Profit', 0.00, 600.00, 1000.00], - ['Less Operating Expenses', 0.00, 0.00, 0.00], - ['Operating Income (or Loss)', 0.00, 600.00, 1000.00], - ['Plus Other Income', 0.00, 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00, 0.00], - ['Net Profit', 0.00, 600.00, 1000.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - # Set an unused analytic account in filter, all the columns - # should be empty, as no move is using this account - options['analytic_accounts'] = [self.analytic_account_parent_2.id] - - self.assertLinesValues( - # pylint: disable=C0326 - # pylint: disable=bad-whitespace - self.report._get_lines(options), - [ 0, 1, 2, 3], - [ - ['Revenue', 0.00, 0.00, 0.00], - ['Less Costs of Revenue', 0.00, 0.00, 0.00], - ['Gross Profit', 0.00, 0.00, 0.00], - ['Less Operating Expenses', 0.00, 0.00, 0.00], - ['Operating Income (or Loss)', 0.00, 0.00, 0.00], - ['Plus Other Income', 0.00, 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00, 0.00], - ['Net Profit', 0.00, 0.00, 0.00], - ], - options, - currency_map={ - 1: {'currency': self.env.company.currency_id}, - 2: {'currency': self.env.company.currency_id}, - }, - ) - - def test_audit_cell_analytic_groupby_and_filter(self): - """ - Test that the analytic filters are applied on the auditing of the cells - """ - def _get_action_dict(options, column_index): - lines = self.report._get_lines(options) - report_line = self.report.line_ids[0] - report_line_dict = next(x for x in lines if x['name'] == report_line.name) - audit_param = self._get_audit_params_from_report_line(options, report_line, report_line_dict, column_group_key=list(options['column_groups'])[column_index]) - return self.report.action_audit_cell(options, audit_param) - - other_plan = self.env['account.analytic.plan'].create({'name': "Other Plan"}) - other_account = self.env['account.analytic.account'].create({'name': "Other Account", 'plan_id': other_plan.id, 'active': True}) - - out_invoices = self.env['account.move'].create([ - { - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2023-02-01', - 'invoice_date': '2023-02-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 1000.0, - 'analytic_distribution': { - self.analytic_account_parent.id: 40, - self.analytic_account_child.id: 60, - } - }), - ] - }, - { - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2023-02-01', - 'invoice_date': '2023-02-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 2000.0, - 'analytic_distribution': { - f'{self.analytic_account_parent.id},{other_account.id}': 100, - }, - }), - ] - } - ]) - out_invoices.action_post() - out_invoices = out_invoices.with_context(analytic_plan_id=self.analytic_plan_parent.id) - analytic_lines_parent = out_invoices.invoice_line_ids.analytic_line_ids.filtered(lambda line: line.auto_account_id == self.analytic_account_parent) - analytic_lines_other = out_invoices.with_context(analytic_plan_id=other_plan.id).invoice_line_ids.analytic_line_ids.filtered(lambda line: line.auto_account_id == other_account) - - # Test with only groupby - options = self._generate_options( - self.report, - '2023-01-01', - '2023-12-31', - default_options={ - 'analytic_accounts_groupby': [self.analytic_account_parent.id, other_account.id], - } - ) - action_dict = _get_action_dict(options, 0) # First Column => Parent - self.assertEqual( - self.env['account.analytic.line'].search(action_dict['domain']), - analytic_lines_parent, - "Only the Analytic Line related to the Parent should be shown", - ) - action_dict = _get_action_dict(options, 1) # Second Column => Other - self.assertEqual( - self.env['account.analytic.line'].search(action_dict['domain']), - analytic_lines_other, - "Only the Analytic Line related to the Parent should be shown", - ) - - action_dict = _get_action_dict(options, 2) # Third Column => AMLs - self.assertEqual( - out_invoices.line_ids.filtered_domain(action_dict['domain']), - out_invoices.invoice_line_ids, - "Both amls should be shown", - ) - - # Adding analytic filter for the two analytic accounts used on the invoice line - options['analytic_accounts'] = [self.analytic_account_parent.id, other_account.id] - action_dict = _get_action_dict(options, 0) # First Column => Parent - self.assertEqual( - self.env['account.analytic.line'].search(action_dict['domain']), - analytic_lines_parent, - "Still only the Analytic Line related to the Parent should be shown", - ) - action_dict = _get_action_dict(options, 1) # Second Column => Other - self.assertEqual( - self.env['account.analytic.line'].search(action_dict['domain']), - analytic_lines_other, - "Still only the Analytic Line related to the Parent should be shown", - ) - - action_dict = _get_action_dict(options, 2) # Third Column => AMLs - self.assertEqual( - out_invoices.line_ids.search(action_dict['domain']), - out_invoices.invoice_line_ids, - "Both amls should be shown", - ) - - def test_general_ledger_analytic_filter(self): - analytic_plan = self.env["account.analytic.plan"].create({ - "name": "Default Plan", - }) - analytic_account = self.env["account.analytic.account"].create({ - "name": "Test Account", - "plan_id": analytic_plan.id, - }) - - invoice = self.init_invoice( - "out_invoice", - amounts=[100, 200], - invoice_date="2023-01-01", - ) - invoice.action_post() - invoice.invoice_line_ids[0].analytic_distribution = {analytic_account.id: 100} - - general_ledger_report = self.env.ref("at_accounting.general_ledger_report") - options = self._generate_options( - general_ledger_report, - "2023-01-01", - "2023-01-01", - default_options={ - 'analytic_accounts': [analytic_account.id], - 'unfold_all': True, - } - ) - - self.assertLinesValues( - general_ledger_report._get_lines(options), - # Name Debit Credit Balance - [ 0, 5, 6, 7], - [ - ['400000 Product Sales', 0.00, 100.00, -100.00], - ['INV/2023/00001', 0.00, 100.00, -100.00], - ['Total 400000 Product Sales', 0.00, 100.00, -100.00], - ['Total', 0.00, 100.00, -100.00], - ], - options, - ) - - def test_analytic_groupby_with_horizontal_groupby(self): - - out_invoice_1 = self.env['account.move'].create([{ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2024-07-01', - 'invoice_date': '2024-07-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_b.id, - 'price_unit': 500.0, - 'analytic_distribution': { - self.analytic_account_parent_2.id: 80, - self.analytic_account_parent_3.id: -10, - }, - }), - ] - }]) - out_invoice_1.action_post() - - out_invoice_2 = self.env['account.move'].create([{ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2024-07-01', - 'invoice_date': '2024-07-01', - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 100.0, - 'analytic_distribution': { - self.analytic_account_parent.id: 100, - }, - }), - ] - }]) - out_invoice_2.action_post() - - horizontal_group = self.env['account.report.horizontal.group'].create({ - 'name': 'Horizontal Group Journal Entries', - 'report_ids': [self.report.id], - 'rule_ids': [ - Command.create({ - 'field_name': 'move_id', # this field is specific to account.move.line and not in account.analytic.line - 'domain': f"[('id', 'in', {(out_invoice_1 + out_invoice_2).ids})]", - }), - ], - }) - - options = self._generate_options( - self.report, - '2024-01-01', - '2024-12-31', - default_options={ - 'analytic_accounts_groupby': [self.analytic_account_parent.id, self.analytic_account_parent_2.id, self.analytic_account_parent_3.id], - 'selected_horizontal_group_id': horizontal_group.id, - } - ) - - self.assertLinesValues( - self.report._get_lines(options), - # Horizontal groupby [ Move 2 ] [ Move 1 ] - # Analytic groupby A1 A2 A3 Balance A1 A2 A3 Balance - [ 0, 1, 2, 3, 4, 5, 6, 7, 8], - [ - ['Revenue', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], - ['Less Costs of Revenue', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - ['Gross Profit', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], - ['Less Operating Expenses', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - ['Operating Income (or Loss)', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], - ['Plus Other Income', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - ['Less Other Expenses', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - ['Net Profit', 100.00, 0.00, 0.00, 100.00, 0.00, 400.00, -50.00, 500.00], - ], - options, - ) - - def test_analytic_groupby_with_analytic_simulations(self): - """ - Create an analytic simulation (analytic line without a move line) - and check that it is taken into account in the report - """ - - self.env['account.analytic.line'].create({ - 'name': 'Simulation', - 'date': '2019-05-01', - 'amount': 100.0, - 'unit_amount': 1.0, - 'company_id': self.env.company.id, - self.analytic_plan_parent._column_name(): self.analytic_account_parent.id, - 'general_account_id': self.company_data['default_account_revenue'].id, - }) - - options = self._generate_options( - self.report, - '2019-01-01', - '2019-12-31', - default_options={ - 'analytic_plans_groupby': [self.analytic_plan_parent.id, self.analytic_plan_child.id], - 'include_analytic_without_aml': True, - } - ) - - self.assertLinesValues( - self.report._get_lines(options), - [ 0, 1, 2], - [ - ('Revenue', 100.00, 0.00), - ('Less Costs of Revenue', 0.00, 0.00), - ('Gross Profit', 100.00, 0.00), - ('Less Operating Expenses', 0.00, 0.00), - ('Operating Income (or Loss)', 100.00, 0.00), - ('Plus Other Income', 0.00, 0.00), - ('Less Other Expenses', 0.00, 0.00), - ('Net Profit', 100.00, 0.00), - ], - options, - ) - - def test_analytic_groupby_plans_without_analytic_accounts(self): - """ - Ensure that grouping on several analytic plans without any analytic accounts works as expected - """ - analytic_plans_without_accounts = self.env['account.analytic.plan'].create([ - {'name': 'Plan 1'}, - {'name': 'Plan 2'}, - ]) - - options = self._generate_options( - self.report, '2019-01-01', '2019-12-31', - default_options={'analytic_plans_groupby': analytic_plans_without_accounts.ids} - ) - - self.assertEqual( - len(options['column_groups']), 3, - "the number of column groups should be 3, despite the 2 analytic plans having the exact same analytic accounts list" - ) - - self.assertLinesValues( - self.report._get_lines(options), - # Plan 1 Plan 2 Total - [ 0, 1, 2, 3], - [ - ('Revenue', 0.00, 0.00, 0.00), - ('Less Costs of Revenue', 0.00, 0.00, 0.00), - ('Gross Profit', 0.00, 0.00, 0.00), - ('Less Operating Expenses', 0.00, 0.00, 0.00), - ('Operating Income (or Loss)', 0.00, 0.00, 0.00), - ('Plus Other Income', 0.00, 0.00, 0.00), - ('Less Other Expenses', 0.00, 0.00, 0.00), - ('Net Profit', 0.00, 0.00, 0.00), - ], - options, - ) - - def test_profit_and_loss_multicompany_access_rights(self): - branch = self.env['res.company'].create([{ - 'name': "My Test Branch", - 'parent_id': self.env.company.id, - }]) - other_currency = self.setup_other_currency('EUR', rounding=0.001) - test_journal = self.env['account.journal'].create({ - 'name': 'Test Journal', - 'code': 'TEST', - 'type': 'sale', - 'company_id': self.env.company.id, - 'currency_id': other_currency.id, - }) - test_user = self.env['res.users'].create({ - 'login': 'test', - 'name': 'The King', - 'email': 'noop@example.com', - 'groups_id': [Command.link(self.env.ref('account.group_account_manager').id)], - 'company_ids': [Command.link(self.env.company.id), Command.link(branch.id)], - }) - self.env.invalidate_all() - - options = self._generate_options( - self.report.with_user(test_user).with_company(branch), '2019-01-01', '2019-12-31', - ) - lines = self.report._get_lines(options) - self.assertTrue(lines) - self.assertEqual(test_journal.display_name, "Test Journal (EUR)") diff --git a/addons/at_accounting/tests/test_bank_rec_widget.py b/addons/at_accounting/tests/test_bank_rec_widget.py deleted file mode 100644 index eda1da8..0000000 --- a/addons/at_accounting/tests/test_bank_rec_widget.py +++ /dev/null @@ -1,3263 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo.addons.at_accounting.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon -from odoo.tests import tagged -from odoo.tools import html2plaintext -from odoo import fields, Command - -from freezegun import freeze_time -from unittest.mock import patch -import re - - -@tagged('post_install', '-at_install') -class TestBankRecWidget(TestBankRecWidgetCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.company_data_2 = cls.setup_other_company() - - cls.early_payment_term = cls.env['account.payment.term'].create({ - 'name': "early_payment_term", - 'company_id': cls.company_data['company'].id, - 'discount_percentage': 10, - 'discount_days': 10, - 'early_discount': True, - 'line_ids': [ - Command.create({ - 'value': 'percent', - 'value_amount': 100, - 'nb_days': 20, - }), - ], - }) - - cls.account_revenue1 = cls.company_data['default_account_revenue'] - cls.account_revenue2 = cls.copy_account(cls.account_revenue1) - - cls.reco_model_bill = cls.env['account.reconcile.model'].create({ - 'name': "test create bill", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [ - Command.create({'amount_string': '50'}), - Command.create({'amount_string': '50'}), - ], - }) - - def assert_form_extra_text_value(self, wizard, regex): - line = wizard.line_ids.filtered(lambda x: x.index == wizard.form_index) - value = line.suggestion_html - if regex: - cleaned_value = html2plaintext(value).replace('\n', '') - if not re.match(regex, cleaned_value): - self.fail(f"The following 'form_extra_text':\n\n'{cleaned_value}'\n\n...doesn't match the provided regex:\n\n'{regex}'") - else: - self.assertFalse(value) - - def test_retrieve_partner_from_account_number(self): - st_line = self._create_st_line(1000.0, partner_id=None, account_number="014 474 8555") - bank_account = self.env['res.partner.bank'].create({ - 'acc_number': '0144748555', - 'partner_id': self.partner_a.id, - }) - self.assertEqual(st_line._retrieve_partner(), bank_account.partner_id) - - # Can't retrieve the partner since the bank account is used by multiple partners. - self.env['res.partner.bank'].create({ - 'acc_number': '0144748555', - 'partner_id': self.partner_b.id, - }) - self.assertEqual(st_line._retrieve_partner(), self.env['res.partner']) - - # Archive partner_a and see if partner_b is then chosen - self.partner_a.active = False - self.assertEqual(st_line._retrieve_partner(), self.partner_b) - - def test_retrieve_partner_from_account_number_in_other_company(self): - st_line = self._create_st_line(1000.0, partner_id=None, account_number="014 474 8555") - self.env['res.partner.bank'].create({ - 'acc_number': '0144748555', - 'partner_id': self.partner_a.id, - }) - - # Bank account is owned by another company. - new_company = self.env['res.company'].create({'name': "test_retrieve_partner_from_account_number_in_other_company"}) - self.partner_a.company_id = new_company - self.assertEqual(st_line._retrieve_partner(), self.env['res.partner']) - - def test_retrieve_partner_from_partner_name(self): - """ Ensure the partner having a name fitting exactly the 'partner_name' is retrieved first. - This test create two partners that will be ordered in the lexicographic order when performing - a search. So: - row1: "Turlututu tsoin tsoin" - row2: "turlututu" - - Since "turlututu" matches exactly (case insensitive) the partner_name of the statement line, - it should be suggested first. - - However if we have two partners called turlututu, we should not suggest any or we risk selecting - the wrong one. - """ - _partner_a, partner_b = self.env['res.partner'].create([ - {'name': "Turlututu tsoin tsoin"}, - {'name': "turlututu"}, - ]) - - st_line = self._create_st_line(1000.0, partner_id=None, partner_name="Turlututu") - self.assertEqual(st_line._retrieve_partner(), partner_b) - - self.env['res.partner'].create({'name': "turlututu"}) - self.assertFalse(st_line._retrieve_partner()) - - def test_retrieve_partner_suggested_account_from_rank(self): - """ Ensure a retrieved partner is proposing his receivable/payable according his customer/supplier rank. """ - partner = self.env['res.partner'].create({'name': "turlututu"}) - rec_account_id = partner.property_account_receivable_id.id - pay_account_id = partner.property_account_payable_id.id - - st_line = self._create_st_line(1000.0, partner_id=None, partner_name="turlututu") - liq_account_id = st_line.journal_id.default_account_id.id - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': liq_account_id, 'balance': 1000.0}, - {'flag': 'auto_balance', 'account_id': rec_account_id, 'balance': -1000.0}, - ]) - - partner._increase_rank('supplier_rank', 1) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': liq_account_id, 'balance': 1000.0}, - {'flag': 'auto_balance', 'account_id': pay_account_id, 'balance': -1000.0}, - ]) - - def test_res_partner_bank_find_create_when_archived(self): - """ Test we don't get the "The combination Account Number/Partner must be unique." error with archived - bank account. - """ - partner = self.env['res.partner'].create({ - 'name': "Zitycard", - 'bank_ids': [Command.create({ - 'acc_number': "123456789", - 'active': False, - })], - }) - - st_line = self._create_st_line( - 100.0, - partner_name="Zeumat Zitycard", - account_number="123456789", - ) - inv_line = self._create_invoice_line( - 'out_invoice', - partner_id=partner.id, - invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': []}], - ) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - wizard._action_validate() - - # Should not trigger the error. - self.env['res.partner.bank'].flush_model() - - def test_res_partner_bank_find_create_multi_company(self): - """ Test we don't get the "The combination Account Number/Partner must be unique." error when the bank account - already exists on another company. - """ - partner = self.env['res.partner'].create({ - 'name': "Zitycard", - 'bank_ids': [Command.create({'acc_number': "123456789"})], - }) - partner.bank_ids.company_id = self.company_data_2['company'] - self.env.user.company_ids = self.env.company - - st_line = self._create_st_line( - 100.0, - partner_name="Zeumat Zitycard", - account_number="123456789", - ) - inv_line = self._create_invoice_line( - 'out_invoice', - partner_id=partner.id, - invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': []}], - ) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - wizard._action_validate() - - # Should not trigger the error. - self.env['res.partner.bank'].flush_model() - - def test_validation_base_case(self): - st_line = self._create_st_line( - 1000.0, - date='2017-01-01', - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') - wizard._js_action_mount_line_in_edit(line.index) - line.account_id = self.account_revenue1 - wizard._line_value_changed_account_id(line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, - {'flag': 'manual', 'amount_currency': -1000.0, 'currency_id': self.company_data['currency'].id, 'balance': -1000.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # The amount is the same, no message under the 'amount' field. - self.assert_form_extra_text_value(wizard, False) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0, 'reconciled': False}, - {'account_id': self.account_revenue1.id, 'amount_currency': -1000.0, 'currency_id': self.company_data['currency'].id, 'balance': -1000.0, 'reconciled': False}, - ]) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, - {'flag': 'aml', 'account_id': self.account_revenue1.id, 'amount_currency': -1000.0, 'currency_id': self.company_data['currency'].id, 'balance': -1000.0}, - ]) - - def test_validation_exchange_difference(self): - # 240.0 curr2 == 120.0 comp_curr - st_line = self._create_st_line( - 120.0, - date='2017-01-01', - foreign_currency_id=self.other_currency.id, - amount_currency=240.0, - ) - # 240.0 curr2 == 80.0 comp_curr - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 240.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 120.0, 'currency_id': self.company_data['currency'].id, 'balance': 120.0}, - {'flag': 'new_aml', 'amount_currency': -240.0, 'currency_id': self.other_currency.id, 'balance': -80.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -40.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - wizard._action_validate() - - # Check the statement line. - self.assertRecordValues(st_line.line_ids.sorted(), [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 120.0, 'currency_id': self.company_data['currency'].id, 'balance': 120.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -240.0, 'currency_id': self.other_currency.id, 'balance': -120.0, 'reconciled': True}, - ]) - - # Check the partials. - partials = st_line.line_ids.matched_debit_ids - exchange_move = partials.exchange_move_id - _liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() - self.assertRecordValues(partials.sorted(), [ - # pylint: disable=C0326 - { - 'amount': 40.0, - 'debit_amount_currency': 0.0, - 'credit_amount_currency': 0.0, - 'debit_move_id': exchange_move.line_ids.sorted()[0].id, - 'credit_move_id': other_line.id, - 'exchange_move_id': False, - }, - { - 'amount': 80.0, - 'debit_amount_currency': 240.0, - 'credit_amount_currency': 240.0, - 'debit_move_id': inv_line.id, - 'credit_move_id': other_line.id, - 'exchange_move_id': exchange_move.id, - }, - ]) - - # Check the exchange diff journal entry. - self.assertRecordValues(exchange_move.line_ids.sorted(), [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 40.0, 'reconciled': True}, - {'account_id': self.env.company.income_currency_exchange_account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -40.0, 'reconciled': False}, - ]) - - def test_validation_new_aml_same_foreign_currency(self): - income_exchange_account = self.env.company.income_currency_exchange_account_id - - # 6000.0 curr2 == 1200.0 comp_curr (bank rate 5:1 instead of the odoo rate 4:1) - st_line = self._create_st_line( - 1200.0, - date='2017-01-01', - foreign_currency_id=self.other_currency_2.id, - amount_currency=6000.0, - ) - # 6000.0 curr2 == 1000.0 comp_curr (rate 6:1) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 6000.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1000.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # The amount is the same, no message under the 'amount' field. - self.assert_form_extra_text_value(wizard, False) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 200.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - - # Reset the wizard. - wizard._js_action_reset() - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'auto_balance', 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0}, - ]) - - # Create the same invoice with a higher amount to check the partial flow. - # 9000.0 curr2 == 1500.0 comp_curr (rate 6:1) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 9000.0}], - ) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'name': "turlututu"}, - {'flag': 'new_aml', 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1000.0, 'name': "INV/2016/00002"}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0, 'name': "Exchange Difference: INV/2016/00002"}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 9,000.000.+ reduced by 6,000.000.+ set the invoice as fully paid .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -9000.0, - 'suggestion_balance': -1500.0, - }]) - - # Switch to a full reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'name': "turlututu"}, - {'flag': 'new_aml', 'amount_currency': -9000.0, 'currency_id': self.other_currency_2.id, 'balance': -1500.0, 'name': "INV/2016/00002"}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -300.0, 'name': "Exchange Difference: INV/2016/00002"}, - {'flag': 'auto_balance', 'amount_currency': 3000.0, 'currency_id': self.other_currency_2.id, 'balance': 600.0, 'name': "Open balance of 6,000.000 $"}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 9,000.000.+ paid .+ record a partial payment .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -6000.0, - 'suggestion_balance': -1000.0, - }]) - - # Switch back to a partial reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Reconcile - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{ - 'payment_state': 'partial', - 'amount_residual': 3000.0, - }]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 200.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - - def test_validation_expense_exchange_difference(self): - expense_exchange_account = self.env.company.expense_currency_exchange_account_id - - # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) - st_line = self._create_st_line( - 1200.0, - date='2016-01-01', - ) - # 1800.0 comp_curr = 3600.0 foreign_curr in 2017 (rate 1:2) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 3600.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': expense_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - # Checks that the wizard still display the 3 initial lines - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0}, - {'flag': 'aml', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0}, - ]) - - def test_validation_income_exchange_difference(self): - income_exchange_account = self.env.company.income_currency_exchange_account_id - - # 1800.0 comp_curr = 3600.0 foreign_curr in 2017 (rate 1:2) - st_line = self._create_st_line( - 1800.0, - date='2017-01-01', - ) - # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 3600.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - # Checks that the wizard still display the 3 initial lines - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, - {'flag': 'aml', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0}, - ]) - - def test_validation_income_exchange_difference_with_rounding(self): - # 1000.0 comp_curr = 3000.0 foreign_curr in 2016 (rate 1:3) - # However divided in 3 invoices + rounding we have 333.33333 ≃ 333.33 comp_curr = 1000.0 foreign_curr - # this implies that the full amount has been used in foreign_curr but there is 0.01 in comp_curr - st_line = self._create_st_line( - 1000.0, - date='2016-01-01', - ) - - # 1500 comp_curr = 3000.0 foreign_curr in 2017 (rate 1:2) - inv_line_1 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - - inv_line_2 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - - inv_line_3 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line_1 + inv_line_2 + inv_line_3) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, - {'flag': 'auto_balance', 'amount_currency': -0.01, 'currency_id': self.company_data['currency'].id, 'balance': -0.01}, - ]) - - # Remove 0.01 cent in the balance of first exchange line - first_exchange_line = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff')[:1] - wizard._js_action_mount_line_in_edit(first_exchange_line.index) - first_exchange_line.balance = 166.66 - wizard._line_value_changed_balance(first_exchange_line) - - # Every line balance so no 'auto_balance' is generated - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.66}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, - ]) - - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - wizard._action_validate() - - # Check that the first line with exchange has -0.01 compared to others - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0, 'reconciled': False}, - {'account_id': inv_line_1.account_id.id, 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -333.34, 'reconciled': True}, - {'account_id': inv_line_2.account_id.id, 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -333.33, 'reconciled': True}, - {'account_id': inv_line_3.account_id.id, 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -333.33, 'reconciled': True}, - ]) - - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line_1.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line_2.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line_3.move_id, [{'payment_state': 'paid'}]) - - def test_validation_exchange_diff_multiple(self): - income_exchange_account = self.env.company.income_currency_exchange_account_id - foreign_currency = self.setup_other_currency('AED', rates=[('2016-01-01', 6.0), ('2017-01-01', 5.0)]) - - # 6000.0 curr2 == 1200.0 comp_curr (bank rate 5:1 instead of the odoo rate 6:1) - st_line = self._create_st_line( - 1200.0, - date='2016-01-01', - foreign_currency_id=foreign_currency.id, - amount_currency=6000.0, - ) - # 1000.0 foreign_curr == 166.67 comp_curr (rate 6:1) - inv_line_1 = self._create_invoice_line( - 'out_invoice', - currency_id=foreign_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - # 2000.00 foreign_curr == 400.0 comp_curr (rate 5:1) - inv_line_2 = self._create_invoice_line( - 'out_invoice', - currency_id=foreign_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 2000.0}], - ) - # 3000.0 foreign_curr == 500.0 comp_curr (rate 6:1) - inv_line_3 = self._create_invoice_line( - 'out_invoice', - currency_id=foreign_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 3000.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line_1 + inv_line_2 + inv_line_3) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': foreign_currency.id, 'balance': -166.67}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -33.33}, - {'flag': 'new_aml', 'amount_currency': -2000.0, 'currency_id': foreign_currency.id, 'balance': -400.0}, - {'flag': 'new_aml', 'amount_currency': -3000.0, 'currency_id': foreign_currency.id, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -100.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # The amount is the same, no message under the 'amount' field. - self.assert_form_extra_text_value(wizard, False) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line_1.account_id.id, 'amount_currency': -1000.0, 'currency_id': foreign_currency.id, 'balance': -200.0, 'reconciled': True}, - {'account_id': inv_line_2.account_id.id, 'amount_currency': -2000.0, 'currency_id': foreign_currency.id, 'balance': -400.0, 'reconciled': True}, - {'account_id': inv_line_3.account_id.id, 'amount_currency': -3000.0, 'currency_id': foreign_currency.id, 'balance': -600.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line_1.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line_2.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line_3.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues((inv_line_1 + inv_line_2 + inv_line_3).matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line_1.account_id.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': 33.33, 'reconciled': True}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -33.33, 'reconciled': False}, - {'account_id': inv_line_3.account_id.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': 100.0, 'reconciled': True}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -100.0, 'reconciled': False}, - ]) - - def test_validation_foreign_curr_st_line_comp_curr_payment_partial_exchange_difference(self): - comp_curr = self.env.company.currency_id - foreign_curr = self.other_currency - - st_line = self._create_st_line( - 650.0, - date='2017-01-01', - foreign_currency_id=foreign_curr.id, - amount_currency=800, - ) - - payment = self.env['account.payment'].create({ - 'partner_id': self.partner_a.id, - 'payment_type': 'inbound', - 'partner_type': 'customer', - 'date': '2017-01-01', - 'amount': 725.0, - }) - payment.action_post() - pay_line, _counterpart_lines, _writeoff_lines = payment._seek_for_lines() - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(pay_line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 650.0, 'currency_id': comp_curr.id, 'balance': 650.0}, - {'flag': 'new_aml', 'amount_currency': -650.0, 'currency_id': comp_curr.id, 'balance': -650.0}, - ]) - - # Switch to a full reconciliation. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - wizard._js_action_apply_line_suggestion(line.index) - - # 725 * 800 / 650 = 892.308 - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 650.0, 'currency_id': comp_curr.id, 'balance': 650.0}, - {'flag': 'new_aml', 'amount_currency': -725.0, 'currency_id': comp_curr.id, 'balance': -725.0}, - {'flag': 'auto_balance', 'amount_currency': 92.308, 'currency_id': foreign_curr.id, 'balance': 75.0}, - ]) - - # Switch to a partial reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 650.0, 'currency_id': comp_curr.id, 'balance': 650.0}, - {'flag': 'new_aml', 'amount_currency': -650.0, 'currency_id': comp_curr.id, 'balance': -650.0}, - ]) - - wizard._action_validate() - self.assertRecordValues(pay_line, [{'amount_residual': 75.0}]) - - def test_validation_remove_exchange_difference(self): - """ Test the case when the foreign currency is missing on the statement line. - In that case, the user can remove the exchange difference in order to fully reconcile both items without additional - write-off/exchange difference. - """ - # 1200.0 comp_curr = 2400.0 foreign_curr in 2017 (rate 1:2) - st_line = self._create_st_line( - 1200.0, - date='2017-01-01', - ) - # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 3600.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -2400.0, 'currency_id': self.other_currency.id, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -400.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Remove the partial. - line_index = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml').index - wizard._js_action_mount_line_in_edit(line_index) - wizard._js_action_apply_line_suggestion(line_index) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0}, - {'flag': 'auto_balance', 'amount_currency': 600.0, 'currency_id': self.company_data['currency'].id, 'balance': 600.0}, - ]) - - exchange_diff_index = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff').index - wizard._js_action_remove_line(exchange_diff_index) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, - ]) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - - def test_validation_new_aml_one_foreign_currency_on_st_line(self): - income_exchange_account = self.env.company.income_currency_exchange_account_id - - # 4800.0 curr2 == 1200.0 comp_curr (rate 4:1) - st_line = self._create_st_line( - 1200.0, - date='2017-01-01', - ) - # 4800.0 curr2 in 2016 (rate 6:1) - inv_line = self._create_invoice_line( - 'out_invoice', - invoice_date='2016-01-01', - currency_id=self.other_currency_2.id, - invoice_line_ids=[{'price_unit': 4800.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # The amount is the same, no message under the 'amount' field. - self.assert_form_extra_text_value(wizard, False) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - - # Checks that the wizard still display the 3 initial lines - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, - {'flag': 'aml', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, # represents the exchange diff - ]) - - # Reset the wizard. - wizard._js_action_reset() - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'auto_balance', 'amount_currency': -1200.0, 'currency_id': self.company_data['currency'].id, 'balance': -1200.0}, - ]) - - # Create the same invoice with a higher amount to check the partial flow. - # 4800.0 curr2 in 2016 (rate 6:1) - inv_line = self._create_invoice_line( - 'out_invoice', - invoice_date='2016-01-01', - currency_id=self.other_currency_2.id, - invoice_line_ids=[{'price_unit': 9600.0}], - ) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 9,600.000.+ reduced by 4,800.000.+ set the invoice as fully paid .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -9600.0, - 'suggestion_balance': -1600.0, - }]) - - # Switch to a full reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -9600.0, 'currency_id': self.other_currency_2.id, 'balance': -1600.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, - {'flag': 'auto_balance', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 9,600.000.+ paid .+ record a partial payment .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -4800.0, - 'suggestion_balance': -800.0, - }]) - - # Switch back to a partial reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Reconcile - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{ - 'payment_state': 'partial', - 'amount_residual': 4800.0, - }]) - self.assertRecordValues(inv_line, [{ - 'amount_residual_currency': 4800.0, - 'amount_residual': 800.0, - 'reconciled': False, - }]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - - def test_validation_new_aml_one_foreign_currency_on_inv_line(self): - income_exchange_account = self.env.company.income_currency_exchange_account_id - - # 1200.0 comp_curr is equals to 4800.0 curr2 in 2017 (rate 4:1) - st_line = self._create_st_line( - 1200.0, - date='2017-01-01', - ) - # 4800.0 curr2 == 800.0 comp_curr (rate 6:1) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 4800.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # The amount is the same, no message under the 'amount' field. - self.assert_form_extra_text_value(wizard, False) - - # Remove the line to see if the exchange difference is well removed. - wizard._action_remove_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'auto_balance', 'amount_currency': -1200.0, 'currency_id': self.company_data['currency'].id, 'balance': -1200.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'invalid'}]) - - # Mount the line again and validate. - wizard._action_add_new_amls(inv_line) - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - - # Reset the wizard. - wizard._js_action_reset() - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'auto_balance', 'amount_currency': -1200.0, 'currency_id': self.company_data['currency'].id, 'balance': -1200.0}, - ]) - - # Create the same invoice with a higher amount to check the partial flow. - # 7200.0 curr2 == 1200.0 comp_curr (rate 6:1) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 7200.0}], - ) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 7,200.000.+ reduced by 4,800.000.+ set the invoice as fully paid .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -7200.0, - 'suggestion_balance': -1200.0, - }]) - - # Switch to a full reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency': -7200.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -600.0}, - {'flag': 'auto_balance', 'amount_currency': 600.0, 'currency_id': self.company_data['currency'].id, 'balance': 600.0}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 7,200.000.+ paid .+ record a partial payment .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -4800.0, - 'suggestion_balance': -800.0, - }]) - - # Switch back to a partial reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Reconcile - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{ - 'payment_state': 'partial', - 'amount_residual': 2400.0, - }]) - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - # pylint: disable=C0326 - {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, - {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, - ]) - - def test_validation_new_aml_multi_currencies(self): - # 6300.0 curr2 == 1800.0 comp_curr (bank rate 3.5:1 instead of the odoo rate 4:1) - st_line = self._create_st_line( - 1800.0, - date='2017-01-01', - foreign_currency_id=self.other_currency_2.id, - amount_currency=6300.0, - ) - # 21600.0 curr3 == 1800.0 comp_curr (rate 12:1) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_3.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 21600.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'new_aml', 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # The amount is the same, no message under the 'amount' field. - self.assert_form_extra_text_value(wizard, False) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) - - # Reset the wizard. - wizard._js_action_reset() - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'auto_balance', 'amount_currency': -6300.0, 'currency_id': self.other_currency_2.id, 'balance': -1800.0}, - ]) - - # Create the same invoice with a higher amount to check the partial flow. - # 32400.0 curr3 == 2700.0 comp_curr (rate 12:1) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_3.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 32400.0}], - ) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'new_aml', 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 32,400.000.+ reduced by 21,600.000.+ set the invoice as fully paid .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -32400.0, - 'suggestion_balance': -2700.0, - }]) - - # Switch to a full reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'new_aml', 'amount_currency': -32400.0, 'currency_id': self.other_currency_3.id, 'balance': -2700.0}, - {'flag': 'auto_balance', 'amount_currency': 3150.0, 'currency_id': self.other_currency_2.id, 'balance': 900.0}, - ]) - - # Check the message under the 'amount' field. - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - self.assert_form_extra_text_value( - wizard, - r".+open amount of 32,400.000.+ paid .+ record a partial payment .", - ) - self.assertRecordValues(line, [{ - 'suggestion_amount_currency': -21600.0, - 'suggestion_balance': -1800.0, - }]) - - # Switch back to a partial reconciliation. - wizard._js_action_apply_line_suggestion(line.index) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Reconcile - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0, 'reconciled': False}, - {'account_id': inv_line.account_id.id, 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0, 'reconciled': True}, - ]) - self.assertRecordValues(st_line, [{'is_reconciled': True}]) - self.assertRecordValues(inv_line.move_id, [{ - 'payment_state': 'partial', - 'amount_residual': 10800.0, - }]) - - def test_validation_new_aml_multi_currencies_exchange_diff_custom_rates(self): - self.company_data['default_journal_bank'].currency_id = self.other_currency - - self.env['res.currency.rate'].create([ - { - 'name': '2017-02-01', - 'rate': 1.0683, - 'currency_id': self.other_currency.id, - 'company_id': self.env.company.id, - }, - { - 'name': '2017-03-01', - 'rate': 1.0812, - 'currency_id': self.other_currency.id, - 'company_id': self.env.company.id, - }, - ]) - - # 960.14 curr1 = 888.03 comp_curr - st_line = self._create_st_line( - -960.14, - date='2017-03-01', - ) - # 112.7 curr1 == 105.49 comp_curr - inv_line1 = self._create_invoice_line( - 'in_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-02-01', - invoice_line_ids=[{'price_unit': 112.7}], - ) - # 847.44 curr1 == 793.26 comp_curr - inv_line2 = self._create_invoice_line( - 'in_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-02-01', - invoice_line_ids=[{'price_unit': 847.44}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line1) - wizard._action_add_new_amls(inv_line2) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': -960.14, 'balance': -888.03}, - {'flag': 'new_aml', 'amount_currency': 112.7, 'balance': 105.49}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1.25}, - {'flag': 'new_aml', 'amount_currency': 847.44, 'balance': 793.26}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -9.47}, - ]) - wizard._action_remove_new_amls(inv_line1 + inv_line2) - wizard._action_add_new_amls(inv_line2) - wizard._action_add_new_amls(inv_line1) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': -960.14, 'balance': -888.03}, - {'flag': 'new_aml', 'amount_currency': 847.44, 'balance': 793.26}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -9.47}, - {'flag': 'new_aml', 'amount_currency': 112.7, 'balance': 105.49}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1.25}, - ]) - - def test_validation_with_partner(self): - partner = self.partner_a.copy() - - st_line = self._create_st_line(1000.0, partner_id=self.partner_a.id) - - # The wizard can be validated directly thanks to the receivable account set on the partner. - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Validate and check the statement line. - wizard._action_validate() - self.assertRecordValues(st_line, [{'partner_id': self.partner_a.id}]) - liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() - account = self.partner_a.property_account_receivable_id - self.assertRecordValues(liquidity_line + other_line, [ - # pylint: disable=C0326 - {'account_id': liquidity_line.account_id.id, 'balance': 1000.0}, - {'account_id': account.id, 'balance': -1000.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - # Match an invoice with a different partner. - wizard._js_action_reset() - inv_line = self._create_invoice_line( - 'out_invoice', - partner_id=partner.id, - invoice_line_ids=[{'price_unit': 1000.0}], - ) - wizard._action_add_new_amls(inv_line) - wizard._action_validate() - liquidity_line, suspense_line, other_line = st_line._seek_for_lines() - self.assertRecordValues(st_line, [{'partner_id': partner.id}]) - self.assertRecordValues(st_line.move_id, [{'partner_id': partner.id}]) - self.assertRecordValues(liquidity_line + other_line, [ - # pylint: disable=C0326 - {'account_id': liquidity_line.account_id.id, 'partner_id': partner.id, 'balance': 1000.0}, - {'account_id': inv_line.account_id.id, 'partner_id': partner.id, 'balance': -1000.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - # Reset the wizard and match invoices with different partners. - wizard._js_action_reset() - partner1 = self.partner_a.copy() - inv_line1 = self._create_invoice_line( - 'out_invoice', - partner_id=partner1.id, - invoice_line_ids=[{'price_unit': 300.0}], - ) - partner2 = self.partner_a.copy() - inv_line2 = self._create_invoice_line( - 'out_invoice', - partner_id=partner2.id, - invoice_line_ids=[{'price_unit': 300.0}], - ) - wizard._action_add_new_amls(inv_line1 + inv_line2) - wizard._action_validate() - liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() - self.assertRecordValues(st_line, [{'partner_id': False}]) - self.assertRecordValues(st_line.move_id, [{'partner_id': False}]) - self.assertRecordValues(liquidity_line + other_line, [ - # pylint: disable=C0326 - {'account_id': liquidity_line.account_id.id, 'partner_id': False, 'balance': 1000.0}, - {'account_id': inv_line1.account_id.id, 'partner_id': partner1.id, 'balance': -300.0}, - {'account_id': inv_line2.account_id.id, 'partner_id': partner2.id, 'balance': -300.0}, - {'account_id': account.id, 'partner_id': False, 'balance': -400.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - # Clear the accounts set on the partner and reset the widget. - # The wizard should be invalid since we are not able to set an open balance. - partner.property_account_receivable_id = None - wizard._js_action_reset() - liquidity_line, suspense_line, other_line = st_line._seek_for_lines() - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': liquidity_line.account_id.id}, - {'flag': 'auto_balance', 'account_id': suspense_line.account_id.id}, - ]) - self.assertRecordValues(wizard, [{'state': 'invalid'}]) - - def test_partner_receivable_payable_account(self): - self.partner_a.write({'customer_rank': 1, 'supplier_rank': 0}) # always receivable - self.partner_b.write({'customer_rank': 0, 'supplier_rank': 1}) # always payable - partner_c = self.partner_b.copy({'customer_rank': 3, 'supplier_rank': 2}) # no preference - - positive_st_line = self._create_st_line(1000) - journal_account = positive_st_line.journal_id.default_account_id - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=positive_st_line.id).new({}) - suspense_line = wizard.line_ids.filtered(lambda l: l.flag != "liquidity") - wizard._js_action_mount_line_in_edit(suspense_line.index) - - suspense_line.partner_id = self.partner_a - wizard._line_value_changed_partner_id(suspense_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'partner_id': False, 'account_id': journal_account.id}, - {'partner_id': self.partner_a.id, 'account_id': self.partner_a.property_account_receivable_id.id}, - ]) - - suspense_line.partner_id = self.partner_b - wizard._line_value_changed_partner_id(suspense_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'partner_id': False, 'account_id': journal_account.id}, - {'partner_id': self.partner_b.id, 'account_id': self.partner_b.property_account_payable_id.id}, - ]) - - suspense_line.partner_id = partner_c - wizard._line_value_changed_partner_id(suspense_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'partner_id': False, 'account_id': journal_account.id}, - {'partner_id': partner_c.id, 'account_id': partner_c.property_account_receivable_id.id}, - ]) - - negative_st_line = self._create_st_line(-1000) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=negative_st_line.id).new({}) - suspense_line = wizard.line_ids.filtered(lambda l: l.flag != "liquidity") - wizard._js_action_mount_line_in_edit(suspense_line.index) - - suspense_line.partner_id = self.partner_a - wizard._line_value_changed_partner_id(suspense_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'partner_id': False, 'account_id': journal_account.id}, - {'partner_id': self.partner_a.id, 'account_id': self.partner_a.property_account_receivable_id.id}, - ]) - - suspense_line.partner_id = self.partner_b - wizard._line_value_changed_partner_id(suspense_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'partner_id': False, 'account_id': journal_account.id}, - {'partner_id': self.partner_b.id, 'account_id': self.partner_b.property_account_payable_id.id}, - ]) - - suspense_line.partner_id = partner_c - wizard._line_value_changed_partner_id(suspense_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'partner_id': False, 'account_id': journal_account.id}, - {'partner_id': partner_c.id, 'account_id': partner_c.property_account_payable_id.id}, - ]) - - def test_validation_using_custom_account(self): - st_line = self._create_st_line(1000.0) - - # By default, the wizard can't be validated directly due to the suspense account. - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard, [{'state': 'invalid'}]) - - # Mount the auto-balance line in edit mode. - line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') - wizard._js_action_mount_line_in_edit(line.index) - liquidity_line, suspense_line, _other_lines = st_line._seek_for_lines() - self.assertRecordValues(line, [{ - 'account_id': suspense_line.account_id.id, - 'balance': -1000.0, - }]) - - # Switch to a custom account. - account = self.env['account.account'].create({ - 'name': "test_validation_using_custom_account", - 'code': "424242", - 'account_type': "asset_current", - }) - line.account_id = account - wizard._line_value_changed_account_id(line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': liquidity_line.account_id.id, 'balance': 1000.0}, - {'flag': 'manual', 'account_id': account.id, 'balance': -1000.0}, - ]) - - # The wizard can be validated. - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Validate and check the statement line. - wizard._action_validate() - liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() - self.assertRecordValues(liquidity_line + other_line, [ - # pylint: disable=C0326 - {'account_id': liquidity_line.account_id.id, 'balance': 1000.0}, - {'account_id': account.id, 'balance': -1000.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - def test_validation_with_taxes(self): - st_line = self._create_st_line(1000.0) - - tax_tags = self.env['account.account.tag'].create({ - 'name': f'tax_tag_{i}', - 'applicability': 'taxes', - 'country_id': self.env.company.account_fiscal_country_id.id, - } for i in range(4)) - - tax_21 = self.env['account.tax'].create({ - 'name': "tax_21", - 'amount': 21, - 'invoice_repartition_line_ids': [ - Command.create({ - 'factor_percent': 100, - 'repartition_type': 'base', - 'tag_ids': [Command.set(tax_tags[0].ids)], - }), - Command.create({ - 'factor_percent': 100, - 'repartition_type': 'tax', - 'tag_ids': [Command.set(tax_tags[1].ids)], - }), - ], - 'refund_repartition_line_ids': [ - Command.create({ - 'factor_percent': 100, - 'repartition_type': 'base', - 'tag_ids': [Command.set(tax_tags[2].ids)], - }), - Command.create({ - 'factor_percent': 100, - 'repartition_type': 'tax', - 'tag_ids': [Command.set(tax_tags[3].ids)], - }), - ], - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') - wizard._js_action_mount_line_in_edit(line.index) - line.tax_ids = [Command.link(tax_21.id)] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, - {'flag': 'manual', 'balance': -826.45, 'tax_tag_ids': tax_tags[0].ids}, - {'flag': 'tax_line', 'balance': -173.55, 'tax_tag_ids': tax_tags[1].ids}, - ]) - - # Remove the tax directly. - line.tax_ids = [Command.clear()] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, - {'flag': 'manual', 'balance': -1000.0, 'tax_tag_ids': []}, - ]) - - # Edit the base line. The tax tags should be the refund ones. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.tax_ids = [Command.link(tax_21.id)] - wizard._line_value_changed_tax_ids(line) - line.balance = 500.0 - wizard._line_value_changed_balance(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, - {'flag': 'manual', 'balance': 500.0, 'tax_tag_ids': tax_tags[2].ids}, - {'flag': 'tax_line', 'balance': 105.0, 'tax_tag_ids': tax_tags[3].ids}, - {'flag': 'auto_balance', 'balance': -1605.0, 'tax_tag_ids': []}, - ]) - - # Edit the base line. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.balance = -500.0 - wizard._line_value_changed_balance(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, - {'flag': 'manual', 'balance': -500.0, 'tax_tag_ids': tax_tags[0].ids}, - {'flag': 'tax_line', 'balance': -105.0, 'tax_tag_ids': tax_tags[1].ids}, - {'flag': 'auto_balance', 'balance': -395.0, 'tax_tag_ids': []}, - ]) - - # Edit the tax line. - line = wizard.line_ids.filtered(lambda x: x.flag == 'tax_line') - wizard._js_action_mount_line_in_edit(line.index) - line.balance = -100.0 - wizard._line_value_changed_balance(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, - {'flag': 'manual', 'balance': -500.0, 'tax_tag_ids': tax_tags[0].ids}, - {'flag': 'tax_line', 'balance': -100.0, 'tax_tag_ids': tax_tags[1].ids}, - {'flag': 'auto_balance', 'balance': -400.0, 'tax_tag_ids': []}, - ]) - - # Add a new tax. - tax_10 = self.env['account.tax'].create({ - 'name': "tax_10", - 'amount': 10, - }) - - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.tax_ids = [Command.link(tax_10.id)] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -500.0}, - {'flag': 'tax_line', 'balance': -105.0}, - {'flag': 'tax_line', 'balance': -50.0}, - {'flag': 'auto_balance', 'balance': -345.0}, - ]) - - # Remove the taxes. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.tax_ids = [Command.clear()] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -500.0}, - {'flag': 'auto_balance', 'balance': -500.0}, - ]) - - # Reset the amount. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.balance = -1000.0 - wizard._line_value_changed_balance(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -1000.0}, - ]) - - # Add taxes. We should be back into the "price included taxes" mode. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.tax_ids = [Command.link(tax_21.id)] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -826.45}, - {'flag': 'tax_line', 'balance': -173.55}, - ]) - - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.tax_ids = [Command.link(tax_10.id)] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -763.35}, - {'flag': 'tax_line', 'balance': -160.31}, - {'flag': 'tax_line', 'balance': -76.34}, - ]) - - # Changing the account should recompute the taxes but preserve the "price included taxes" mode. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.account_id = self.account_revenue1 - wizard._line_value_changed_account_id(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -763.35}, - {'flag': 'tax_line', 'balance': -160.31}, - {'flag': 'tax_line', 'balance': -76.34}, - ]) - - # The wizard can be validated. - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - # Validate and check the statement line. - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'balance': 1000.0}, - {'balance': -763.35}, - {'balance': -160.31}, - {'balance': -76.34}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - def test_validation_caba_tax_account(self): - """ Cash basis taxes usually put their tax lines on a transition account, and the cash basis entries then move those amounts - to the regular tax accounts. When using a cash basis tax in the bank reconciliation widget, their won't be any cash basis - entry and the lines will directly be exigible, so we want to use the final tax account directly. - """ - tax_account = self.company_data['default_account_tax_sale'] - - caba_tax = self.env['account.tax'].create({ - 'name': "CABA", - 'amount_type': 'percent', - 'amount': 20.0, - 'tax_exigibility': 'on_payment', - 'cash_basis_transition_account_id': self.safe_copy(tax_account).id, - 'invoice_repartition_line_ids': [ - (0, 0, { - 'repartition_type': 'base', - }), - (0, 0, { - 'repartition_type': 'tax', - 'account_id': tax_account.id, - }), - ], - 'refund_repartition_line_ids': [ - (0, 0, { - 'repartition_type': 'base', - }), - (0, 0, { - 'repartition_type': 'tax', - 'account_id': tax_account.id, - }), - ], - }) - - st_line = self._create_st_line(120.0) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') - wizard._js_action_mount_line_in_edit(line.index) - line.account_id = self.account_revenue1 - line.tax_ids = [Command.link(caba_tax.id)] - wizard._line_value_changed_tax_ids(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 120.0, 'account_id': st_line.journal_id.default_account_id.id}, - {'flag': 'manual', 'balance': -100.0, 'account_id': self.account_revenue1.id}, - {'flag': 'tax_line', 'balance': -20.0, 'account_id': tax_account.id}, - ]) - - self.assertRecordValues(wizard, [{'state': 'valid'}]) - - wizard._action_validate() - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'balance': 120.0, 'tax_ids': [], 'tax_line_id': False, 'account_id': st_line.journal_id.default_account_id.id}, - {'balance': -100.0, 'tax_ids': caba_tax.ids, 'tax_line_id': False, 'account_id': self.account_revenue1.id}, - {'balance': -20.0, 'tax_ids': [], 'tax_line_id': caba_tax.id, 'account_id': tax_account.id}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - def test_validation_changed_default_account(self): - st_line = self._create_st_line(100.0, partner_id=self.partner_a.id) - original_journal_account_id = st_line.journal_id.default_account_id - # Change the default account of the journal (exceptional case) - st_line.journal_id.default_account_id = self.company_data['default_journal_cash'].default_account_id - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard, [{'state': 'valid'}]) - # Validate and check the statement line. - wizard._action_validate() - liquidity_line, _suspense_line, _other_line = st_line._seek_for_lines() - self.assertRecordValues(liquidity_line, [ - {'account_id': original_journal_account_id.id, 'balance': 100.0}, - ]) - self.assertRecordValues(wizard, [{'state': 'reconciled'}]) - - def test_apply_taxes_with_reco_model(self): - st_line = self._create_st_line(1000.0) - - tax_21 = self.env['account.tax'].create({ - 'name': "tax_21", - 'amount': 21, - }) - - reco_model = self.env['account.reconcile.model'].create({ - 'name': "test_apply_taxes_with_reco_model", - 'rule_type': 'writeoff_button', - 'line_ids': [Command.create({ - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(tax_21.ids)], - })], - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_select_reconcile_model(reco_model) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1000.0}, - {'flag': 'manual', 'balance': -826.45}, - {'flag': 'tax_line', 'balance': -173.55}, - ]) - - def test_percentage_st_line_with_reco_model(self): - journal_curr = self.other_currency - foreign_curr = self.other_currency_2 - self.company_data['default_journal_bank'].currency_id = journal_curr - - # Setup triple currency. - st_line = self._create_st_line( - 1000.0, - date='2018-01-01', - foreign_currency_id=foreign_curr.id, - amount_currency=4000.0, - ) - - reco_model = self.env['account.reconcile.model'].create({ - 'name': "test_percentage_st_line_with_reco_model", - 'rule_type': 'writeoff_button', - 'line_ids': [ - Command.create({ - 'amount_type': 'percentage_st_line', - 'amount_string': str(percentage), - 'label': str(i), - 'account_id': self.account_revenue1.id, - }) - for i, percentage in enumerate((74.0, 24.0, 12.0, -10.0)) - ], - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_select_reconcile_model(reco_model) - - self.assertRecordValues(wizard.line_ids, [ - {'flag': 'liquidity', 'currency_id': journal_curr.id, 'amount_currency': 1000.0, 'balance': 500.0}, - {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': -740.0, 'balance': -370.0}, - {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': -240.0, 'balance': -120.0}, - {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': -120.0, 'balance': -60.0}, - {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': 100.0, 'balance': 50.0}, - ]) - - def test_manual_edits_not_replaced(self): - """ 2 partial payments should keep the edited balance """ - st_line = self._create_st_line( - 1200.0, - date='2017-02-01', - ) - inv_line_1 = self._create_invoice_line( - 'out_invoice', - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 3000.0}], - ) - inv_line_2 = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 4000.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line_1) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1200.0}, - {'flag': 'new_aml', 'balance':-1200.0}, - ]) - - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - line.balance = -600.0 - wizard._line_value_changed_balance(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1200.0}, - {'flag': 'new_aml', 'balance': -600.0}, - {'flag': 'auto_balance', 'balance': -600.0}, - ]) - - wizard._action_add_new_amls(inv_line_2) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 1200.0}, - {'flag': 'new_aml', 'balance': -600.0}, - {'flag': 'new_aml', 'balance': -600.0}, - ]) - - def test_manual_edits_not_replaced_multicurrency(self): - """ 2 partial payments should keep the edited amount_currency """ - st_line = self._create_st_line( - 1200.0, - date='2018-01-01', - foreign_currency_id=self.other_currency_2.id, - amount_currency=6000.0, # rate 5:1 - ) - - inv_line_1 = self._create_invoice_line( - 'out_invoice', - invoice_date='2016-01-01', - currency_id=self.other_currency_2.id, - invoice_line_ids=[{'price_unit': 6000.0}], # 1000 company curr (rate 6:1) - ) - inv_line_2 = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - currency_id=self.other_currency_2.id, - invoice_line_ids=[{'price_unit': 4000.0}], # 1000 company curr (rate 4:1) - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line_1) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency':-6000.0, 'balance':-1000.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -200.0}, - ]) - - line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') - wizard._js_action_mount_line_in_edit(line.index) - line.amount_currency = -3000.0 - wizard._line_value_changed_amount_currency(line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency':-3000.0, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -100.0}, - {'flag': 'auto_balance', 'amount_currency':-3000.0, 'balance': -600.0}, - ]) - - wizard._action_add_new_amls(inv_line_2) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 1200.0}, - {'flag': 'new_aml', 'amount_currency':-3000.0, 'balance': -500.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -100.0}, - {'flag': 'new_aml', 'amount_currency':-3000.0, 'balance': -750.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 150.0}, - ]) - - def test_creating_manual_line_multi_currencies(self): - # 6300.0 curr2 == 1800.0 comp_curr (bank rate 3.5:1 instead of the odoo rate 4:1) - st_line = self._create_st_line( - 1800.0, - date='2017-01-01', - foreign_currency_id=self.other_currency_2.id, - amount_currency=6300.0, - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'auto_balance', 'amount_currency': -6300.0, 'currency_id': self.other_currency_2.id, 'balance': -1800.0}, - ]) - - # Custom balance. - line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') - wizard._js_action_mount_line_in_edit(line.index) - line.balance = -1500.0 - wizard._line_value_changed_balance(line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'manual', 'amount_currency': -6300.0, 'currency_id': self.other_currency_2.id, 'balance': -1500.0}, - {'flag': 'auto_balance', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -300.0}, - ]) - - # Custom amount_currency. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.amount_currency = -4200.0 - wizard._line_value_changed_amount_currency(line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'manual', 'amount_currency': -4200.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0}, - {'flag': 'auto_balance', 'amount_currency': -2100.0, 'currency_id': self.other_currency_2.id, 'balance': -600.0}, - ]) - - # Custom currency_id. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.currency_id = self.other_currency - wizard._line_value_changed_currency_id(line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'manual', 'amount_currency': -4200.0, 'currency_id': self.other_currency.id, 'balance': -2100.0}, - {'flag': 'auto_balance', 'amount_currency': 1050.0, 'currency_id': self.other_currency_2.id, 'balance': 300.0}, - ]) - - # Custom balance. - line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') - wizard._js_action_mount_line_in_edit(line.index) - line.balance = -1800.0 - wizard._line_value_changed_balance(line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, - {'flag': 'manual', 'amount_currency': -4200.0, 'currency_id': self.other_currency.id, 'balance': -1800.0}, - ]) - - def test_auto_reconcile_cron(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - cron = self.env.ref('at_accounting.auto_reconcile_bank_statement_line') - self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink() - - st_line = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) - - self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - - rule = self.env['account.reconcile.model'].create({ - 'name': "test_auto_reconcile_cron", - 'rule_type': 'writeoff_suggestion', - 'auto_reconcile': False, - 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], - }) - - # The CRON is not doing anything since the model is not auto reconcile. - with freeze_time('2017-01-01'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines() - self.assertRecordValues(st_line, [{'is_reconciled': False, 'cron_last_check': False}]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) - - rule.auto_reconcile = True - - # The CRON don't consider old statement lines. - with freeze_time('2017-06-01'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines() - self.assertRecordValues(st_line, [{'is_reconciled': False, 'cron_last_check': False}]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) - - # The CRON will auto-reconcile the line. - with freeze_time('2017-01-02'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines() - self.assertRecordValues(st_line, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2017-01-02 00:00:00')}]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) - - st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2018-01-01') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 2) - self._create_invoice_line( - 'out_invoice', - invoice_date='2018-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - st_line2 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2018-01-01') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 3) - self._create_invoice_line( - 'out_invoice', - invoice_date='2018-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - - # Simulate the cron already tried to process 'st_line1' before. - with freeze_time('2017-12-31'): - st_line1.cron_last_check = fields.Datetime.now() - - # The statement line with no 'cron_last_check' must be processed before others. - with freeze_time('2018-01-02'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) - - self.assertRecordValues(st_line1 + st_line2, [ - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2017-12-31 00:00:00')}, - {'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2018-01-02 00:00:00')}, - ]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 4) - - with freeze_time('2018-01-03'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) - - self.assertRecordValues(st_line1, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2018-01-03 00:00:00')}]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 4) - - st_line3 = self._create_st_line(1234.0, date='2018-01-01') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 5) - self._create_invoice_line( - 'out_invoice', - invoice_date='2018-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - st_line4 = self._create_st_line(1234.0, date='2018-01-01') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 6) - self._create_invoice_line( - 'out_invoice', - invoice_date='2018-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - - # Make sure the CRON is no longer applicable. - rule.match_partner = True - rule.match_partner_ids = [Command.set(self.partner_a.ids)] - with freeze_time('2018-01-01'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) - - self.assertRecordValues(st_line3 + st_line4, [ - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-01 00:00:00')}, - {'is_reconciled': False, 'cron_last_check': False}, - ]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 7) - - # Make sure the statement lines are reconciled by the cron in the right order. - self.assertRecordValues(st_line3 + st_line4, [ - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-01 00:00:00')}, - {'is_reconciled': False, 'cron_last_check': False}, - ]) - - # st_line4 is processed because cron_last_check is null. - with freeze_time('2018-01-02'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) - - self.assertRecordValues(st_line3 + st_line4, [ - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-01 00:00:00')}, - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-02 00:00:00')}, - ]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 7) - - # st_line3 is processed because it has the oldest cron_last_check. - with freeze_time('2018-01-03'): - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) - - self.assertRecordValues(st_line3 + st_line4, [ - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-03 00:00:00')}, - {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-02 00:00:00')}, - ]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 7) - - def test_duplicate_amls_constraint(self): - st_line = self._create_st_line(1000.0) - inv_line = self._create_invoice_line( - 'out_invoice', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertTrue(len(wizard.line_ids), 2) - - wizard._action_add_new_amls(inv_line) - self.assertTrue(len(wizard.line_ids), 2) - - @freeze_time('2017-01-01') - def test_reconcile_model_with_payment_tolerance(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - - invoice_line = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - st_line = self._create_st_line(998.0, partner_id=self.partner_a.id, date='2017-01-01', payment_ref=invoice_line.move_id.name) - - rule = self.env['account.reconcile.model'].create({ - 'name': "test_reconcile_model_with_payment_tolerance", - 'rule_type': 'invoice_matching', - 'allow_payment_tolerance': True, - 'payment_tolerance_type': 'percentage', - 'payment_tolerance_param': 2.0, - 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_trigger_matching_rules() - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 998.0, 'reconcile_model_id': False}, - {'flag': 'new_aml', 'balance': -1000.0, 'reconcile_model_id': rule.id}, - {'flag': 'manual', 'balance': 2.0, 'reconcile_model_id': rule.id}, - ]) - - @freeze_time('2017-01-01') - def test_auto_reconcile_model_with_archived_partner(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - self.env['res.partner.bank'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - - # Needed as partner_a does not have a company and we need a company in order to find matching partner from account_number - self.partner_a.company_id = self.company_data['company'].id - self.env['res.partner.bank'].create({ - 'partner_id': self.partner_a.id, - 'acc_number': '12345', - }) - invoice_line = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - st_line = self._create_st_line(1000.0, account_number='12345', date='2017-01-01', payment_ref=invoice_line.move_id.name) - - self.env['account.reconcile.model'].create({ - 'name': "test_reconcile_model_with_archived_partner", - 'rule_type': 'invoice_matching', - 'auto_reconcile': True, - 'match_partner': True, - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_trigger_matching_rules() - self.assertEqual(wizard.partner_id, self.partner_a) - self.assertTrue(wizard.matching_rules_allow_auto_reconcile) - - # archive partner and trigger again, partner should still be set (match based on account_number), because it's the only match - self.partner_a.active = False - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_trigger_matching_rules() - self.assertEqual(wizard.partner_id, self.partner_a) # Partner should still be set - self.assertTrue(wizard.matching_rules_allow_auto_reconcile) # no special process because the partner is unactive - - def test_early_payment_included_multi_currency(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - self.early_payment_term.early_pay_discount_computation = 'included' - income_exchange_account = self.env.company.income_currency_exchange_account_id - expense_exchange_account = self.env.company.expense_currency_exchange_account_id - - inv_line1_with_epd = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - partner_id=self.partner_a.id, - invoice_payment_term_id=self.early_payment_term.id, - invoice_date='2016-12-01', - invoice_line_ids=[ - { - 'price_unit': 4800.0, - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - { - 'price_unit': 9600.0, - 'account_id': self.account_revenue2.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - ], - ) - inv_line1_with_epd_rec_lines = inv_line1_with_epd.move_id.line_ids\ - .filtered(lambda x: x.account_type == 'asset_receivable')\ - .sorted(lambda x: x.discount_date or x.date_maturity) - self.assertRecordValues( - inv_line1_with_epd_rec_lines, - [ - { - 'amount_currency': 16560.0, - 'balance': 2760.0, - 'discount_amount_currency': 14904.0, - 'discount_balance': 2484.0, - 'discount_date': fields.Date.from_string('2016-12-11'), - 'date_maturity': fields.Date.from_string('2016-12-21'), - }, - ], - ) - - inv_line2_with_epd = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - partner_id=self.partner_a.id, - invoice_payment_term_id=self.early_payment_term.id, - invoice_date='2017-01-20', - invoice_line_ids=[ - { - 'price_unit': 480.0, - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - { - 'price_unit': 960.0, - 'account_id': self.account_revenue2.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - ], - ) - inv_line2_with_epd_rec_lines = inv_line2_with_epd.move_id.line_ids\ - .filtered(lambda x: x.account_type == 'asset_receivable')\ - .sorted(lambda x: x.discount_date or x.date_maturity) - self.assertRecordValues( - inv_line2_with_epd_rec_lines, - [ - { - 'amount_currency': 1656.0, - 'balance': 414.0, - 'discount_amount_currency': 1490.4, - 'discount_balance': 372.6, - 'discount_date': fields.Date.from_string('2017-01-30'), - 'date_maturity': fields.Date.from_string('2017-02-09'), - }, - ], - ) - - # inv1: 16560.0 (no epd) - # inv2: 1490.4 (epd) - st_line = self._create_st_line( - 4512.0, # instead of 4512.6 (rate 1:4) - date='2017-01-04', - foreign_currency_id=self.other_currency_2.id, - amount_currency=18050.4, - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - - # Add all lines from the first invoice plus the first one from the second one. - wizard._action_add_new_amls(inv_line1_with_epd_rec_lines + inv_line2_with_epd_rec_lines) - liquidity_acc = st_line.journal_id.default_account_id - receivable_acc = self.company_data['default_account_receivable'] - early_pay_acc = self.env.company.account_journal_early_pay_discount_loss_account_id - tax_acc = self.company_data['default_tax_sale'].invoice_repartition_line_ids.account_id - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 4512.0, 'balance': 4512.0, 'account_id': liquidity_acc.id}, - {'flag': 'new_aml', 'amount_currency': -16560.0, 'balance': -2760.0, 'account_id': receivable_acc.id}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1379.45, 'account_id': income_exchange_account.id}, - {'flag': 'new_aml', 'amount_currency': -1656.0, 'balance': -414.0, 'account_id': receivable_acc.id}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 0.06, 'account_id': expense_exchange_account.id}, - {'flag': 'early_payment', 'amount_currency': 144.0, 'balance': 36.0, 'account_id': early_pay_acc.id}, - {'flag': 'early_payment', 'amount_currency': 21.6, 'balance': 5.4, 'account_id': tax_acc.id}, - {'flag': 'early_payment', 'amount_currency': 0.0, 'balance': -0.01, 'account_id': income_exchange_account.id}, - ]) - - def test_early_payment_excluded_multi_currency(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - self.early_payment_term.early_pay_discount_computation = 'excluded' - income_exchange_account = self.env.company.income_currency_exchange_account_id - expense_exchange_account = self.env.company.expense_currency_exchange_account_id - - inv_line1_with_epd = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - partner_id=self.partner_a.id, - invoice_payment_term_id=self.early_payment_term.id, - invoice_date='2016-12-01', - invoice_line_ids=[ - { - 'price_unit': 4800.0, - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - { - 'price_unit': 9600.0, - 'account_id': self.account_revenue2.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - ], - ) - inv_line1_with_epd_rec_lines = inv_line1_with_epd.move_id.line_ids\ - .filtered(lambda x: x.account_type == 'asset_receivable')\ - .sorted(lambda x: x.discount_date or x.date_maturity) - self.assertRecordValues( - inv_line1_with_epd_rec_lines, - [ - { - 'amount_currency': 16560.0, - 'balance': 2760.0, - 'discount_amount_currency': 15120.0, - 'discount_balance': 2520.0, - 'discount_date': fields.Date.from_string('2016-12-11'), - 'date_maturity': fields.Date.from_string('2016-12-21'), - }, - ], - ) - - inv_line2_with_epd = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - partner_id=self.partner_a.id, - invoice_payment_term_id=self.early_payment_term.id, - invoice_date='2017-01-20', - invoice_line_ids=[ - { - 'price_unit': 480.0, - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - { - 'price_unit': 960.0, - 'account_id': self.account_revenue2.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - ], - ) - inv_line2_with_epd_rec_lines = inv_line2_with_epd.move_id.line_ids\ - .filtered(lambda x: x.account_type == 'asset_receivable')\ - .sorted(lambda x: x.discount_date or x.date_maturity) - self.assertRecordValues( - inv_line2_with_epd_rec_lines, - [ - { - 'amount_currency': 1656.0, - 'balance': 414.0, - 'discount_amount_currency': 1512.0, - 'discount_balance': 378.0, - 'discount_date': fields.Date.from_string('2017-01-30'), - 'date_maturity': fields.Date.from_string('2017-02-09'), - }, - ], - ) - - # inv1: 16560.0 (no epd) - # inv2: 1512.0 (epd) - st_line = self._create_st_line( - 4515.0, # instead of 4518.0 (rate 1:4) - date='2017-01-04', - foreign_currency_id=self.other_currency_2.id, - amount_currency=18072.0, - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - - # Add all lines from the first invoice plus the first one from the second one. - wizard._action_add_new_amls(inv_line1_with_epd_rec_lines + inv_line2_with_epd_rec_lines[:2]) - liquidity_acc = st_line.journal_id.default_account_id - receivable_acc = self.company_data['default_account_receivable'] - early_pay_acc = self.env.company.account_journal_early_pay_discount_loss_account_id - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 4515.0, 'balance': 4515.0, 'account_id': liquidity_acc.id}, - {'flag': 'new_aml', 'amount_currency': -16560.0, 'balance': -2760.0, 'account_id': receivable_acc.id}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1377.25, 'account_id': income_exchange_account.id}, - {'flag': 'new_aml', 'amount_currency': -1656.0, 'balance': -414.0, 'account_id': receivable_acc.id}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 0.27, 'account_id': expense_exchange_account.id}, - {'flag': 'early_payment', 'amount_currency': 144.0, 'balance': 36.0, 'account_id': early_pay_acc.id}, - {'flag': 'early_payment', 'amount_currency': 0.0, 'balance': -0.02, 'account_id': income_exchange_account.id}, - ]) - - def test_early_payment_mixed_multi_currency(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - self.early_payment_term.early_pay_discount_computation = 'mixed' - income_exchange_account = self.env.company.income_currency_exchange_account_id - expense_exchange_account = self.env.company.expense_currency_exchange_account_id - - inv_line1_with_epd = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - partner_id=self.partner_a.id, - invoice_payment_term_id=self.early_payment_term.id, - invoice_date='2016-12-01', - invoice_line_ids=[ - { - 'price_unit': 4800.0, - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - { - 'price_unit': 9600.0, - 'account_id': self.account_revenue2.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - ], - ) - inv_line1_with_epd_rec_lines = inv_line1_with_epd.move_id.line_ids\ - .filtered(lambda x: x.account_type == 'asset_receivable')\ - .sorted(lambda x: x.discount_date or x.date_maturity) - self.assertRecordValues( - inv_line1_with_epd_rec_lines, - [ - { - 'amount_currency': 16344.0, - 'balance': 2724.0, - 'discount_amount_currency': 14904.0, - 'discount_balance': 2484.0, - 'discount_date': fields.Date.from_string('2016-12-11'), - 'date_maturity': fields.Date.from_string('2016-12-21'), - }, - ], - ) - - inv_line2_with_epd = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - partner_id=self.partner_a.id, - invoice_payment_term_id=self.early_payment_term.id, - invoice_date='2017-01-20', - invoice_line_ids=[ - { - 'price_unit': 480.0, - 'account_id': self.account_revenue1.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - { - 'price_unit': 960.0, - 'account_id': self.account_revenue2.id, - 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], - }, - ], - ) - inv_line2_with_epd_rec_lines = inv_line2_with_epd.move_id.line_ids\ - .filtered(lambda x: x.account_type == 'asset_receivable')\ - .sorted(lambda x: x.discount_date or x.date_maturity) - self.assertRecordValues( - inv_line2_with_epd_rec_lines, - [ - { - 'amount_currency': 1634.4, - 'balance': 408.6, - 'discount_amount_currency': 1490.4, - 'discount_balance': 372.6, - 'discount_date': fields.Date.from_string('2017-01-30'), - 'date_maturity': fields.Date.from_string('2017-02-09'), - }, - ], - ) - - # inv1: 16344.0 (no epd) - # inv2: 1490.4 (epd) - st_line = self._create_st_line( - 4458.0, # instead of 4458.6 (rate 1:4) - date='2017-01-04', - foreign_currency_id=self.other_currency_2.id, - amount_currency=17834.4, - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - - # Add all lines from the first invoice plus the first one from the second one. - wizard._action_add_new_amls(inv_line1_with_epd_rec_lines + inv_line2_with_epd_rec_lines[:2]) - liquidity_acc = st_line.journal_id.default_account_id - receivable_acc = self.company_data['default_account_receivable'] - early_pay_acc = self.env.company.account_journal_early_pay_discount_loss_account_id - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 4458.0, 'balance': 4458.0, 'account_id': liquidity_acc.id}, - {'flag': 'new_aml', 'amount_currency': -16344.0, 'balance': -2724.0, 'account_id': receivable_acc.id}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1361.45, 'account_id': income_exchange_account.id}, - {'flag': 'new_aml', 'amount_currency': -1634.4, 'balance': -408.6, 'account_id': receivable_acc.id}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 0.05, 'account_id': expense_exchange_account.id}, - {'flag': 'early_payment', 'amount_currency': 144.0, 'balance': 36.0, 'account_id': early_pay_acc.id}, - ]) - - def test_early_payment_included_intracomm_bill(self): - tax_tags = self.env['account.account.tag'].create({ - 'name': f'tax_tag_{i}', - 'applicability': 'taxes', - 'country_id': self.env.company.account_fiscal_country_id.id, - } for i in range(6)) - - intracomm_tax = self.env['account.tax'].create({ - 'name': 'tax20', - 'amount_type': 'percent', - 'amount': 20, - 'type_tax_use': 'purchase', - 'invoice_repartition_line_ids': [ - # pylint: disable=bad-whitespace - Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[0].ids)]}), - Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[1].ids)]}), - Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[2].ids)]}), - ], - 'refund_repartition_line_ids': [ - # pylint: disable=bad-whitespace - Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[3].ids)]}), - Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[4].ids)]}), - Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[5].ids)]}), - ], - }) - - early_payment_term = self.env['account.payment.term'].create({ - 'name': "early_payment_term", - 'company_id': self.company_data['company'].id, - 'early_pay_discount_computation': 'included', - 'early_discount': True, - 'discount_percentage': 2, - 'discount_days': 7, - 'line_ids': [ - Command.create({ - 'value': 'percent', - 'value_amount': 100.0, - 'nb_days': 30, - }), - ], - }) - - bill = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'invoice_payment_term_id': early_payment_term.id, - 'invoice_date': '2019-01-01', - 'date': '2019-01-01', - 'invoice_line_ids': [ - Command.create({ - 'name': 'line', - 'price_unit': 1000.0, - 'tax_ids': [Command.set(intracomm_tax.ids)], - }), - ], - }) - bill.action_post() - - st_line = self._create_st_line( - -980.0, - date='2017-01-01', - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(bill.line_ids.filtered(lambda x: x.account_type == 'liability_payable')) - wizard._action_validate() - - self.assertRecordValues(st_line.line_ids.sorted('balance'), [ - # pylint: disable=bad-whitespace - {'amount_currency': -980.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False}, - {'amount_currency': -20.0, 'tax_ids': intracomm_tax.ids, 'tax_tag_ids': tax_tags[3].ids, 'tax_tag_invert': True}, - {'amount_currency': -4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[4].ids, 'tax_tag_invert': True}, - {'amount_currency': 4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[5].ids, 'tax_tag_invert': True}, - {'amount_currency': 1000.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False}, - ]) - - def test_multi_currencies_with_custom_rate(self): - self.company_data['default_journal_bank'].currency_id = self.other_currency - st_line = self._create_st_line(1200.0) # rate 1:2 - self.assertRecordValues(st_line.move_id.line_ids, [ - # pylint: disable=C0326 - {'amount_currency': 1200.0, 'balance': 600.0}, - {'amount_currency': -1200.0, 'balance': -600.0}, - ]) - - # invoice with other_currency and rate 1:2 - invoice_line1 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 300.0}], # = 150 USD - ) - - # Remove all rates. - self.other_currency.rate_ids.unlink() - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 600.0}, - {'flag': 'auto_balance', 'amount_currency': -1200.0, 'balance': -600.0}, - ]) - - # invoice with other_currency_2 and rate 1:6 - invoice_line2 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 600.0}], # = 100 USD - ) - # invoice with other_currency_2 and rate 1:4 - invoice_line3 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency_2.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 400.0}], # = 100 USD - ) - - # Remove all rates. - self.other_currency_2.rate_ids.unlink() - - # Ensure no conversion rate has been made. - wizard._action_add_new_amls(invoice_line1 + invoice_line2 + invoice_line3) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 600.0}, - {'flag': 'new_aml', 'amount_currency': -300.0, 'balance': -150.0}, - {'flag': 'new_aml', 'amount_currency': -600.0, 'balance': -100.0}, - {'flag': 'new_aml', 'amount_currency': -400.0, 'balance': -100.0}, - {'flag': 'auto_balance', 'amount_currency': -500.0, 'balance': -250.0}, - ]) - - def test_partial_reconciliation_suggestion_with_mixed_invoice_and_refund(self): - """ Test the partial reconciliation suggestion is well recomputed when adding another - line. For example, when adding 2 invoices having an higher amount then a refund. In that - case, the partial on the second invoice should be removed since the difference is filled - by the newly added refund. - """ - st_line = self._create_st_line( - 1800.0, - date='2017-01-01', - foreign_currency_id=self.other_currency.id, - amount_currency=3600.0, - ) - - inv1 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 2400.0}], - ) - inv2 = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 2400.0}], - ) - refund = self._create_invoice_line( - 'out_refund', - currency_id=self.other_currency.id, - invoice_date='2016-01-01', - invoice_line_ids=[{'price_unit': 1200.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv1 + inv2) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'balance': 1800.0}, - {'flag': 'new_aml', 'amount_currency': -2400.0, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -400.0}, - {'flag': 'new_aml', 'amount_currency': -1200.0, 'balance': -400.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -200.0}, - ]) - wizard._action_add_new_amls(refund) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1800.0, 'balance': 1800.0}, - {'flag': 'new_aml', 'amount_currency': -2400.0, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -400.0}, - {'flag': 'new_aml', 'amount_currency': -2400.0, 'balance': -800.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -400.0}, - {'flag': 'new_aml', 'amount_currency': 1200.0, 'balance': 400.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 200.0}, - ]) - - def test_auto_reconcile_cron_with_time_limit(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - cron = self.env.ref('at_accounting.auto_reconcile_bank_statement_line') - self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink() - - st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) - st_line2 = self._create_st_line(5678.0, partner_id=self.partner_a.id, date='2017-01-02') - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 2) - - self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 5678.0}], - ) - self.env['account.reconcile.model'].create({ - 'name': "test_auto_reconcile_cron_with_time_limit", - 'rule_type': 'writeoff_suggestion', - 'auto_reconcile': True, - 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], - }) - - with freeze_time('2017-01-01 00:00:00') as frozen_time: - def datetime_now_override(): - frozen_time.tick() - return frozen_time() - with patch('odoo.fields.Datetime.now', side_effect=datetime_now_override): - # we simulate that the time limit is reached after first loop - self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(limit_time=1) - # after first loop, only one statement should be reconciled - self.assertRecordValues(st_line1, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2017-01-01 00:00:01')}]) - # the other one should be in queue for regular cron tigger - self.assertRecordValues(st_line2, [{'is_reconciled': False, 'cron_last_check': False}]) - self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 3) - - def test_auto_reconcile_cron_with_provided_statements_lines(self): - self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() - - st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01') - st_line2 = self._create_st_line(5678.0, partner_id=self.partner_a.id, date='2017-01-02') - self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 1234.0}], - ) - self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 5678.0}], - ) - self.env['account.reconcile.model'].create({ - 'name': "test_auto_reconcile_cron_with_time_limit", - 'rule_type': 'writeoff_suggestion', - 'auto_reconcile': True, - 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], - }) - with freeze_time('2017-01-01 00:00:00'): - # we call auto reconcile on st_lines1 **only** - st_line1._cron_try_auto_reconcile_statement_lines() - self.assertRecordValues(st_line1, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2017-01-01 00:00:00')}]) - self.assertRecordValues(st_line2, [{'is_reconciled': False, 'cron_last_check': False}]) - - @freeze_time('2019-01-01') - def test_button_apply_reco_model(self): - inv_line = self._create_invoice_line( - 'in_invoice', - invoice_date='2019-01-01', - invoice_line_ids=[{'price_unit': 980.0}], - ) - st_line = self._create_st_line(-1000.0, partner_id=self.partner_a.id, date=inv_line.date, payment_ref=inv_line.move_name) - - reco_model = self.env['account.reconcile.model'].create({ - 'name': "test_apply_taxes_with_reco_model", - 'rule_type': 'writeoff_button', - 'line_ids': [Command.create({ - 'account_id': self.account_revenue1.copy().id, - 'label': 'Bank Fees' - })], - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_trigger_matching_rules() - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': -1000.0}, - {'flag': 'new_aml', 'account_id': inv_line.account_id.id, 'balance': 980.0}, - {'flag': 'auto_balance', 'account_id': self.company_data['default_account_payable'].id, 'balance': 20.0}, - ]) - - wizard._action_select_reconcile_model(reco_model) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': -1000.0}, - {'flag': 'new_aml', 'account_id': inv_line.account_id.id, 'balance': 980.0}, - {'flag': 'manual', 'account_id': reco_model.line_ids[0].account_id.id, 'balance': 20.0}, - ]) - - def test_exchange_diff_on_partial_aml_multi_currency(self): - self.company_data['default_journal_bank'].currency_id = self.other_currency - st_line = self._create_st_line(-36000.0) # rate 1:2 - inv_line = self._create_invoice_line( - 'in_invoice', - invoice_date='2016-01-01', # rate 1:3 - currency_id=self.other_currency.id, - invoice_line_ids=[{'price_unit': 38000.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': -36000.0, 'currency_id': self.other_currency.id, 'balance': -18000.0}, - {'flag': 'new_aml', 'amount_currency': 36000.0, 'currency_id': self.other_currency.id, 'balance': 12000.0}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 6000.0}, - ]) - - def test_exchange_diff_on_partial_aml_multi_currency_close_amount(self): - self.other_currency.rate_ids.rate = 0.9839 - self.company_data['default_journal_bank'].currency_id = self.other_currency - - st_line = self._create_st_line(-37436.50) - self.assertRecordValues(st_line.line_ids, [ - # pylint: disable=C0326 - {'amount_currency': -37436.50, 'balance': -38049.09}, - {'amount_currency': 37436.50, 'balance': 38049.09}, - ]) - - inv_line = self._create_invoice_line( - 'in_invoice', - invoice_date=st_line.date, - currency_id=self.other_currency.id, - invoice_line_ids=[{'price_unit': 37436.52}], - ) - self.assertRecordValues(inv_line, [{ - 'amount_currency': -37436.52, - 'balance': -38049.11, - }]) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': -37436.50, 'currency_id': self.other_currency.id, 'balance': -38049.09}, - {'flag': 'new_aml', 'amount_currency': 37436.50, 'currency_id': self.other_currency.id, 'balance': 38049.09}, - ]) - - def test_matching_zero_amount_misc_entry(self): - """ Check for division by zero with foreign currencies and some 0 making a broken rate. """ - self.company_data['default_journal_bank'].currency_id = self.other_currency - st_line = self._create_st_line(0.0, amount_currency=10.0, foreign_currency_id=self.company_data['currency'].id) - - entry = self.env['account.move'].create({ - 'date': '2019-01-01', - 'line_ids': [ - Command.create({ - 'account_id': self.company_data['default_account_receivable'].id, - 'currency_id': self.other_currency.id, - 'debit': 1.0, - 'credit': 0.0, - }), - Command.create({ - 'account_id': self.company_data['default_account_revenue'].id, - 'currency_id': self.other_currency.id, - 'debit': 0.0, - 'credit': 1.0, - }), - ] - }) - entry.action_post() - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - aml = entry.line_ids.filtered('debit') - wizard._action_add_new_amls(aml) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'balance': 10.0}, - {'flag': 'new_aml', 'balance': -1.0}, - {'flag': 'exchange_diff', 'balance': 1.0}, - {'flag': 'auto_balance', 'balance': -10.0}, - ]) - - def test_amls_order_with_matching_amount(self): - """ AML's with a matching amount_residual should be displayed first when the order is not specified. """ - - foreign_st_line = self._create_st_line( - 500.0, - date='2016-01-01', - foreign_currency_id=self.other_currency.id, - amount_currency=1500.0, - ) - st_line = self._create_st_line( - 66.66, - date='2016-01-01', - ) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=foreign_st_line.id).new({}) - - aml1_id = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-30', - invoice_line_ids=[{'price_unit': 1000.0}], - ).id - aml2_id = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-29', - currency_id=self.other_currency.id, - invoice_line_ids=[{'price_unit': 1500.0}], # = 100 USD - ).id - aml3_id = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-28', - invoice_line_ids=[{'price_unit': 500.0}], - ).id - aml4_id = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-27', - invoice_line_ids=[{'price_unit': 55.55}], # = 55.550000000000004 - ).id - aml5_id = self._create_invoice_line( - 'out_invoice', - invoice_date='2017-01-26', - invoice_line_ids=[{'price_unit': 66.66}], - ).id - - # Check the lines without the context key. - wizard._js_action_mount_st_line(foreign_st_line.id) - domain = wizard.return_todo_command['amls']['domain'] - amls_list = self.env['account.move.line'].search_fetch(domain=domain, field_names=['id']) - self.assertEqual( - [x['id'] for x in amls_list], - [aml1_id, aml2_id, aml3_id, aml4_id, aml5_id], - ) - - # Check the lines with the context key. - suspense_line = wizard.line_ids.filtered(lambda l: l.flag == 'auto_balance') - amls_list = self.env['account.move.line']\ - .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ - .search_fetch(domain=domain, field_names=['id']) - self.assertEqual( - [x['id'] for x in amls_list], - [aml2_id, aml1_id, aml3_id, aml4_id, aml5_id], - ) - - # Check the order with limits and offsets - amls_list = self.env['account.move.line']\ - .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ - .search_fetch(domain=domain, field_names=['id'], limit=2) - self.assertEqual( - [x['id'] for x in amls_list], - [aml2_id, aml1_id], - ) - amls_list = self.env['account.move.line']\ - .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ - .search_fetch(domain=domain, field_names=['id'], offset=2, limit=3) - self.assertEqual( - [x['id'] for x in amls_list], - [aml3_id, aml4_id, aml5_id], - ) - - # Check rounding and new suspense line - wizard._js_action_mount_st_line(st_line.id) - suspense_line = wizard.line_ids.filtered(lambda l: l.flag == 'auto_balance') - amls_list = self.env['account.move.line']\ - .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ - .search_fetch(domain=domain, field_names=['id']) - self.assertEqual( - [x['id'] for x in amls_list], - [aml5_id, aml1_id, aml2_id, aml3_id, aml4_id], - ) - wizard._js_action_mount_line_in_edit(suspense_line.index) - suspense_line.balance = -11.11 - wizard._line_value_changed_balance(suspense_line) - suspense_line = wizard.line_ids.filtered(lambda l: l.flag == 'auto_balance') - self.assertEqual(suspense_line.balance, -55.55) - self.env.cr.execute(f""" - UPDATE account_move_line SET amount_residual_currency = 55.550000001 WHERE id = {aml4_id}; - """) - amls_list = self.env['account.move.line']\ - .with_context(preferred_aml_value=55.550003, preferred_aml_currency_id=suspense_line.currency_id.id)\ - .search_fetch(domain=domain, field_names=['id']) - self.assertEqual( - [x['id'] for x in amls_list], - [aml4_id, aml1_id, aml2_id, aml3_id, aml5_id], - ) - - # Check that context keys are not propagated - action = amls_list[0].action_open_business_doc() - self.assertFalse(action['context'].get('preferred_aml_value')) - - @freeze_time('2023-12-25') - def test_analtyic_distribution_model_exchange_diff_line(self): - """Test that the analytic distribution model is present on the exchange diff line.""" - expense_exchange_account = self.env.company.expense_currency_exchange_account_id - analytic_plan = self.env['account.analytic.plan'].create({ - 'name': 'Plan 1', - 'default_applicability': 'unavailable', - }) - analytic_account_1 = self.env['account.analytic.account'].create({'name': 'Account 1', 'plan_id': analytic_plan.id}) - analytic_account_2 = self.env['account.analytic.account'].create({'name': 'Account 1', 'plan_id': analytic_plan.id}) - distribution_model = self.env['account.analytic.distribution.model'].create({ - 'account_prefix': expense_exchange_account.code, - 'partner_id': self.partner_a.id, - 'analytic_distribution': {analytic_account_1.id: 100}, - }) - - # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) - st_line = self._create_st_line( - 1200.0, - date='2016-01-01', - ) - # 1800.0 comp_curr = 3600.0 foreign_curr in 2017 (rate 1:2) - inv_line = self._create_invoice_line( - 'out_invoice', - currency_id=self.other_currency.id, - invoice_date='2017-01-01', - invoice_line_ids=[{'price_unit': 3600.0}], - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'analytic_distribution': False}, - {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0, 'analytic_distribution': False}, - {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0, 'analytic_distribution': distribution_model.analytic_distribution}, - ]) - - # Test that the analytic distribution is kept on the creation of the exchange diff move - new_distribution = {**distribution_model.analytic_distribution, str(analytic_account_2.id): 100} - - line = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff') - line.analytic_distribution = new_distribution - wizard._action_validate() - - self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ - {'analytic_distribution': False}, - {'analytic_distribution': new_distribution}, - ]) - - def test_access_child_bank_with_user_set_on_child(self): - """ - Demo user with a Child Company as default company/allowed companies - should be able to access the Bank set on this same Child Company - """ - child_company = self.env['res.company'].create({ - 'name': 'Childest Company', - 'parent_id': self.env.company.id, - }) - child_bank_journal = self.env['account.journal'].create({ - 'name': 'Child Bank', - 'type': 'bank', - 'company_id': child_company.id, - }) - self.user.write({ - 'company_ids': [Command.set(child_company.ids)], - 'company_id': child_company.id, - 'groups_id': [ - Command.set(self.env.ref('account.group_account_user').ids), - ] - }) - res = self.env['bank.rec.widget'].with_user(self.user).collect_global_info_data(child_bank_journal.id) - self.assertTrue(res, "Journal should be accessible") - - def test_collect_global_info_data_other_company_bank_journal_with_user_on_main_company(self): - """ The aim of this test is checking that a user who having - access to 2 companies will have values even when he's - calling collect_global_info_data function if - it's current company it's not the one on the journal - but is still available. - To do that, we add 2 companies to the user, and try to - call collect_global_info_data on the journal of the second - company, even if the main company it's the first one. - """ - self.user.write({ - 'company_ids': [Command.set((self.company_data['company'] + self.company_data_2['company']).ids)], - 'company_id': self.company_data['company'].id, - }) - - result = self.env['bank.rec.widget'].with_user(self.user).collect_global_info_data(self.company_data_2['default_journal_bank'].id) - self.assertTrue(result['balance_amount'], "Balance amount shouldn't be False value") - - def test_collect_global_info_data_non_existing_bank_journal(self): - """ The aim of this test is checking that we receive an empty - string when we call collect_global_info_data function - with a non-existing journal. This use case could happen - when we try to open the bank rec widget on a journal that - is not actually existing. As this function is callable by - rpc, this usecase could happen. - """ - result = self.env['bank.rec.widget'].with_user(self.user).collect_global_info_data(99999999) - self.assertEqual(result['balance_amount'], "", "If no value, the function should return an empty string") - - def test_res_partner_bank_find_create_multi_account(self): - """ Make sure that we can save multiple bank accounts for a partner. """ - partner = self.env['res.partner'].create({'name': "Zitycard"}) - - for acc_number in ("123456789", "123456780"): - st_line = self._create_st_line(100.0, account_number=acc_number) - inv_line = self._create_invoice_line( - 'out_invoice', - partner_id=partner.id, - invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': []}], - ) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_add_new_amls(inv_line) - wizard._action_validate() - - bank_accounts = self.env['res.partner.bank'].sudo().with_context(active_test=False).search([ - ('partner_id', '=', partner.id), - ]) - self.assertEqual(len(bank_accounts), 2, "Second bank account was not registered!") - - #################################################### - # RECO MODEL MOVE CREATION - #################################################### - - def create_test_reco_invoice(self, st_line, reco_model): - """ Helper method to create a move given a statement line and reconciliation model """ - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - wizard._action_select_reconcile_model(reco_model) - move = self.env['account.move'].browse(wizard.return_todo_command['res_id']) - return move - - def assert_reco_invoice_values(self, move, st_line, expected_move_type, expected_amount_total=None): - """ Helper method to assert that values in a move match the information in the given statement line """ - if expected_amount_total is None: - expected_amount_total = abs(st_line.amount) - self.assertRecordValues(move, [{ - 'amount_total': expected_amount_total, - 'move_type': expected_move_type, - 'partner_id': st_line.partner_id.id, - 'invoice_date': st_line.date, - }]) - - def test_invoice_creation_from_reco_model(self): - """ Test the created invoice from a sale/purchase reconciliation model. """ - reco_model_invoice = self.env['account.reconcile.model'].create({ - 'name': "test reconcile create invoice", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'sale', - 'line_ids': [ - Command.create({'amount_string': '50'}), - Command.create({'amount_string': '50'}), - ], - }) - - for st_line_amount, move_type, reco_model in ( - (1000.0, 'out_invoice', reco_model_invoice), - (-1000.0, 'out_refund', reco_model_invoice), - (1000.0, 'in_refund', self.reco_model_bill), - (-1000.0, 'in_invoice', self.reco_model_bill), - ): - st_line = self._create_st_line(st_line_amount, partner_id=self.partner_a.id) - with self.subTest(): - move = self.create_test_reco_invoice(st_line, reco_model) - self.assert_reco_invoice_values(move, st_line, move_type) - - def test_invoice_reco_model_round_odd(self): - """ Test if correct move is created when rounding is needed for multiple reco model lines""" - # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. - st_line = self._create_st_line(amount=-111.11, partner_id=self.partner_a.id) - move = self.create_test_reco_invoice(st_line, self.reco_model_bill) - self.assert_reco_invoice_values(move, st_line, 'in_invoice') - - def test_invoice_reco_model_round_single_line(self): - """ Test if correct move is created with single-line reco model when rounding is needed """ - # A st_line of $150 with 15% tax (default) requires rounding, as without it the invoice would total $149.99 - reco_model_single_line = self.env['account.reconcile.model'].create({ - 'name': "single line reco model", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [Command.create({})], - }) - st_line = self._create_st_line(amount=150, partner_id=self.partner_a.id) - move = self.create_test_reco_invoice(st_line, reco_model_single_line) - self.assert_reco_invoice_values(move, st_line, 'in_refund') - - def test_invoice_reco_model_round_large_percentage(self): - """ Test if total move amount is correctly rounded when reco model lines go above 100% of st_line amount """ - # Reco model with two 100% st_line amount lines - reco_model_two_lines = self.env['account.reconcile.model'].create({ - 'name': "two lines reco model", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [Command.create({}), Command.create({})], - }) - # Reco model with one 200% st_line amount line - reco_model_single_line = self.env['account.reconcile.model'].create({ - 'name': "single line reco model", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [Command.create({'amount_string': '200'})], - }) - st_line = self._create_st_line(-150, partner_id=self.partner_a.id) - for reco_model in (reco_model_two_lines, reco_model_single_line): - with self.subTest(): - move = self.create_test_reco_invoice(st_line, reco_model) - # Total move amount should equal 2 * st_line amount = 300 in both cases - self.assert_reco_invoice_values(move, st_line, 'in_invoice', 300.0) - - def test_invoice_reco_model_round_combined(self): - """ Test if total move amount is correctly rounded when reco model lines are a combination of - percentage_st_line and fixed amount types """ - # Reco model with 100% amount + fixed amount, total move amount should equal st_line amount + fixed amount - reco_model_percentage_fixed = self.env['account.reconcile.model'].create({ - 'name': "combined percentage + fixed reco model", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'sale', - 'line_ids': [ - Command.create({}), - Command.create({ - 'amount_type': 'fixed', - 'amount_string': '50', - }), - ], - }) - st_line = self._create_st_line(amount=100, partner_id=self.partner_a.id) - move = self.create_test_reco_invoice(st_line, reco_model_percentage_fixed) - self.assert_reco_invoice_values(move, st_line, 'out_invoice', 150.0) - - def test_invoice_reco_model_multiple_taxes(self): - """ - Test if correct move is created through reco model writeoff button when the - reconciliation model has more than one tax per line. - Note: this only works if taxes are all price_include or all not price_include - """ - tax_21 = self.env['account.tax'].create({ - 'name': "tax_21", - 'amount': 21, - }) - reco_model_bill_mult_taxes = self.env['account.reconcile.model'].create({ - 'name': "test reconcile bill multiple taxes", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [ - Command.create({ - 'tax_ids': [Command.set((tax_21 + self.company_data['default_tax_purchase']).ids)], - }), - ], - }) - st_line = self._create_st_line(amount=-100, partner_id=self.partner_a.id) - move = self.create_test_reco_invoice(st_line, reco_model_bill_mult_taxes) - self.assert_reco_invoice_values(move, st_line, 'in_invoice') - - def test_invoice_creation_reco_model_percentage(self): - """ Test if correct move is created when rounding is needed """ - # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. - st_line = self._create_st_line(amount=150, partner_id=self.partner_a.id) - reco_model_bill_balance = self.env['account.reconcile.model'].create({ - 'name': "test balance", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [ - Command.create({ - 'amount_type': 'percentage', - 'amount_string': '50', - }), - Command.create({ - 'amount_type': 'percentage', - 'amount_string': '50', - }), - ], - }) - move = self.create_test_reco_invoice(st_line, reco_model_bill_balance) - self.assert_reco_invoice_values(move, st_line, 'in_refund', 112.5) - - def test_invoice_creation_reco_model_fixed(self): - """ Test if correct move is created when rounding is needed """ - # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. - st_line = self._create_st_line(amount=-150, partner_id=self.partner_a.id) - reco_model_invoice_fixed = self.env['account.reconcile.model'].create({ - 'name': "test fixed", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'sale', - 'line_ids': [ - Command.create({ - 'amount_type': 'fixed', - 'amount_string': '100', - }), - ], - }) - move = self.create_test_reco_invoice(st_line, reco_model_invoice_fixed) - self.assert_reco_invoice_values(move, st_line, 'out_refund', 100.0) - - def test_invoice_creation_reco_model_regex(self): - """ Test if correct move is created when rounding is needed """ - # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. - st_line = self._create_st_line(amount=150, partner_id=self.partner_a.id, payment_ref="150 invoice", statement_name='150 invoice') - reco_model_invoice_regex = self.env['account.reconcile.model'].create({ - 'name': "test label/regex", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'sale', - 'line_ids': [ - Command.create({ - 'amount_type': 'regex', - 'amount_string': r'([\d,.]+)', - }), - ], - }) - move = self.create_test_reco_invoice(st_line, reco_model_invoice_regex) - self.assert_reco_invoice_values(move, st_line, 'out_invoice', 150.0) - - def test_unreconciled_with_other_lines(self): - """Test that other lines are shown in the widget if they exist.""" - st_line = self._create_st_line( - 1000.0, - date='2017-01-01', - ) - - # Edit the associated move to partially reconcile some of the suspense amount; i.e. we add another line - liquidity_line, suspense_line, other_line = st_line._seek_for_lines() - other_account = st_line.journal_id.company_id.default_cash_difference_income_account_id - self.assertFalse(other_line) - move = st_line.move_id - move.button_draft() - move.write({'line_ids': [ - Command.create({'account_id': other_account.id, 'credit': 100.0}), - Command.update(suspense_line.id, {'credit': 900.0}), - ]}) - move.action_post() - liquidity_line, suspense_line, other_line = st_line._seek_for_lines() - self.assertTrue(other_line) - - # Check that the wizard displays the new line - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - self.assertRecordValues(wizard.line_ids, [ - # pylint: disable=C0326 - {'flag': 'liquidity', 'account_id': liquidity_line.account_id.id, 'amount_currency': 1000.0}, - {'flag': 'aml', 'account_id': other_line.account_id.id, 'amount_currency': -100.0}, - {'flag': 'auto_balance', 'account_id': suspense_line.account_id.id, 'amount_currency': -900.0}, - ]) diff --git a/addons/at_accounting/tests/test_bank_rec_widget_common.py b/addons/at_accounting/tests/test_bank_rec_widget_common.py deleted file mode 100644 index b961a5c..0000000 --- a/addons/at_accounting/tests/test_bank_rec_widget_common.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import Command -from odoo.addons.account.tests.common import AccountTestInvoicingCommon - - -class TestBankRecWidgetCommon(AccountTestInvoicingCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.other_currency = cls.setup_other_currency('EUR') - cls.other_currency_2 = cls.setup_other_currency('CAD', rounding=0.001, rates=[('2016-01-01', 6.0), ('2017-01-01', 4.0)]) - cls.other_currency_3 = cls.setup_other_currency('XAF', rounding=0.001, rates=[('2016-01-01', 12.0), ('2017-01-01', 8.0)]) - - @classmethod - def _create_invoice_line(cls, move_type, **kwargs): - ''' Create an invoice on the fly.''' - kwargs.setdefault('partner_id', cls.partner_a.id) - kwargs.setdefault('invoice_date', '2017-01-01') - kwargs.setdefault('invoice_line_ids', []) - for one2many_values in kwargs['invoice_line_ids']: - one2many_values.setdefault('name', 'xxxx') - one2many_values.setdefault('quantity', 1) - one2many_values.setdefault('tax_ids', []) - - invoice = cls.env['account.move'].create({ - 'move_type': move_type, - **kwargs, - 'invoice_line_ids': [Command.create(x) for x in kwargs['invoice_line_ids']], - }) - invoice.action_post() - return invoice.line_ids\ - .filtered(lambda l: l.account_id.account_type in ('asset_receivable', 'liability_payable')) - - @classmethod - def _create_st_line(cls, amount, date='2019-01-01', payment_ref='turlututu', **kwargs): - st_line = cls.env['account.bank.statement.line'].create({ - 'amount': amount, - 'date': date, - 'payment_ref': payment_ref, - 'journal_id': kwargs.get('journal_id', cls.company_data['default_journal_bank'].id), - **kwargs, - }) - # The automatic reconcile cron checks the create_date when considering st_lines to run on. - # create_date is a protected field so this is the only way to set it correctly - cls.env.cr.execute("UPDATE account_bank_statement_line SET create_date = %s WHERE id=%s", - (st_line.date, st_line.id)) - return st_line - - @classmethod - def _create_reconcile_model(cls, **kwargs): - return cls.env['account.reconcile.model'].create({ - 'name': "test", - 'rule_type': 'invoice_matching', - 'allow_payment_tolerance': True, - 'payment_tolerance_type': 'percentage', - 'payment_tolerance_param': 0.0, - **kwargs, - 'line_ids': [ - Command.create({ - 'account_id': cls.company_data['default_account_revenue'].id, - 'amount_type': 'percentage', - 'label': f"test {i}", - **line_vals, - }) - for i, line_vals in enumerate(kwargs.get('line_ids', [])) - ], - }) diff --git a/addons/at_accounting/tests/test_bank_rec_widget_tour.py b/addons/at_accounting/tests/test_bank_rec_widget_tour.py deleted file mode 100644 index fb262f2..0000000 --- a/addons/at_accounting/tests/test_bank_rec_widget_tour.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo.addons.at_accounting.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon -from odoo.tests import tagged, HttpCase -from odoo import Command - - -@tagged('post_install', '-at_install') -class TestBankRecWidget(TestBankRecWidgetCommon, HttpCase): - - @classmethod - def setUpClass(cls): - super().setUpClass() - - cls.st_line1 = cls._create_st_line(1000.0, payment_ref="line1", sequence=1) - cls.st_line2 = cls._create_st_line(1000.0, payment_ref="line2", sequence=2) - cls._create_st_line(1000.0, payment_ref="line3", sequence=3) - cls._create_st_line(1000.0, payment_ref="line_credit", sequence=4, journal_id=cls.company_data['default_journal_credit'].id) - - # INV/2019/00001: - cls._create_invoice_line( - 'out_invoice', - partner_id=cls.partner_a.id, - invoice_date='2019-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - - # INV/2019/00002: - cls._create_invoice_line( - 'out_invoice', - partner_id=cls.partner_a.id, - invoice_date='2019-01-01', - invoice_line_ids=[{'price_unit': 1000.0}], - ) - - cls.env['account.reconcile.model']\ - .search([('company_id', '=', cls.company_data['company'].id)])\ - .write({'past_months_limit': None}) - - cls.reco_model_invoice = cls.env['account.reconcile.model'].create({ - 'name': "test reconcile create invoice", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'sale', - 'line_ids': [ - Command.create({'amount_string': '50'}), - Command.create({'amount_string': '50'}), - ], - }) - - def test_tour_bank_rec_widget(self): - self.start_tour('/odoo', 'account_accountant_bank_rec_widget', login=self.env.user.login) - - self.assertRecordValues(self.st_line1.line_ids, [ - # pylint: disable=C0326 - {'account_id': self.st_line1.journal_id.default_account_id.id, 'balance': 1000.0, 'reconciled': False}, - {'account_id': self.company_data['default_account_receivable'].id, 'balance': -1000.0, 'reconciled': True}, - ]) - - tax_account = self.company_data['default_tax_sale'].invoice_repartition_line_ids.account_id - self.assertRecordValues(self.st_line2.line_ids, [ - # pylint: disable=C0326 - {'account_id': self.st_line2.journal_id.default_account_id.id, 'balance': 1000.0, 'tax_ids': []}, - {'account_id': self.company_data['default_account_payable'].id, 'balance': -869.57, 'tax_ids': self.company_data['default_tax_sale'].ids}, - {'account_id': tax_account.id, 'balance': -130.43, 'tax_ids': []}, - ]) - - def test_tour_bank_rec_widget_ui(self): - bank2 = self.env['account.journal'].create({ - 'name': 'Bank2', - 'type': 'bank', - 'code': 'BNK2', - }) - self._create_st_line(222.22, payment_ref="line4", sequence=4, journal_id=bank2.id) - # INV/2019/00003: - self._create_invoice_line( - 'out_invoice', - partner_id=self.partner_a.id, - invoice_date='2019-01-01', - invoice_line_ids=[{'price_unit': 2000.0}], - ) - self.st_line2.payment_ref = self.st_line2.payment_ref + ' - ' + 'INV/2019/00001' - self.start_tour('/odoo?debug=assets', 'account_accountant_bank_rec_widget_ui', timeout=120, login=self.env.user.login) - - def test_tour_bank_rec_widget_rainbowman_reset(self): - self.start_tour('/odoo?debug=assets', 'account_accountant_bank_rec_widget_rainbowman_reset', login=self.env.user.login) - - def test_tour_bank_rec_widget_statements(self): - self.start_tour('/odoo?debug=assets', 'account_accountant_bank_rec_widget_statements', login=self.env.user.login) - - def test_tour_invoice_creation_from_reco_model(self): - """ Test if move is created and added as a new_aml line in bank reconciliation widget """ - st_line = self._create_st_line(amount=1000, partner_id=self.partner_a.id) - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - # The tour creates a move through reco model button, posts it, returns to widget and validates the move - self.start_tour( - '/odoo', - 'account_accountant_bank_rec_widget_reconciliation_button', - login=self.env.user.login, - ) - # Mount the validated statement line to confirm that information matches. - wizard._js_action_mount_st_line(st_line.id) - self.assertRecordValues(wizard.line_ids, [ - {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': 1000}, - {'flag': 'aml', 'account_id': self.company_data['default_account_receivable'].id, 'balance': -1000}, - ]) - # Check that the aml comes from a move, and not from the auto-balance line - self.assertTrue(wizard.line_ids[1].source_aml_move_id) - - def test_tour_invoice_creation_reco_model_currency(self): - """ Test move creation through reconcile button when a foreign currency is used for the statement line """ - st_line = self._create_st_line( - 1800.0, - date='2019-02-01', - foreign_currency_id=self.other_currency.id, # rate 2:1 - amount_currency=3600.0, - partner_id=self.partner_a.id, - ) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - - self.start_tour( - '/odoo', - 'account_accountant_bank_rec_widget_reconciliation_button', - login=self.env.user.login, - ) - # Mount the validated statement line to confirm that information matches. - wizard._js_action_mount_st_line(st_line.id) - - # Move is created in the foreign currency, but in bank widget the balance appears in main currency. - # If aml was created from the reco model button, display name matches payment_ref. - self.assertRecordValues(wizard.line_ids, [ - {'flag': 'liquidity', 'balance': 1800, 'amount_currency': 1800}, - {'flag': 'aml', 'balance': -1800, 'amount_currency': -3600}, - ]) - # Confirm that the aml comes from a move, and not from the auto-balance line - self.assertTrue(wizard.line_ids[1].source_aml_move_id) - - def test_tour_invoice_creation_combined_reco_model(self): - """ Test creation of a move from a reconciliation model with different amount types """ - self.reco_model_invoice.name = "old test" # rename previous reco model to be able to reuse the existing tour - self.env['account.reconcile.model'].create({ - 'name': "test reconcile combined", - 'rule_type': 'writeoff_button', - 'counterpart_type': 'purchase', - 'line_ids': [ - Command.create({ - 'amount_type': 'percentage_st_line', - 'amount_string': '50', - }), - Command.create({ - 'amount_type': 'percentage', - 'amount_string': '50', - 'tax_ids': self.tax_purchase_b.ids, - }), - Command.create({ - 'amount_type': 'fixed', - 'amount_string': '100', - 'account_id': self.env.company.expense_currency_exchange_account_id.id, - 'tax_ids': [Command.clear()] # remove default tax added - }), - # Regex line will not be added to move, as the label of st line does not include digits - Command.create({ - 'amount_type': 'regex', - 'amount_string': r'BRT: ([\d,.]+)', - }), - ], - }) - - st_line = self._create_st_line(amount=-1000, partner_id=self.partner_a.id, payment_ref="combined test") - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) - # The tour creates a move through reco model button, posts it, returns to widget and validates the move - self.start_tour( - '/odoo', - 'account_accountant_bank_rec_widget_reconciliation_button', - login=self.env.user.login, - ) - # Mount the validated statement line to confirm that widget line matches created move and balance line is added. - wizard._js_action_mount_st_line(st_line.id) - self.assertRecordValues(wizard.line_ids, [ - {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': -1000}, - {'flag': 'aml', 'account_id': self.company_data['default_account_payable'].id, 'balance': 850}, - {'flag': 'aml', 'account_id': self.company_data['default_account_payable'].id, 'balance': 150}, - ]) - # Check that the aml comes from an existing move - move = wizard.line_ids[1].source_aml_move_id - self.assertTrue(move) - - # The total price of these lines should match the percentage or fixed amount of reco model lines - self.assertRecordValues(move.line_ids, [ - # 50% of statement line (of 1000.0) - {'price_total': 500, 'debit': 434.78, 'credit': 0, 'name': 'combined test', 'account_id': self.company_data['default_account_expense'].id}, - # 50% of balance (of residual value = 500.0) - {'price_total': 250, 'debit': 217.39, 'credit': 0, 'name': 'combined test', 'account_id': self.company_data['default_account_expense'].id}, - # fixed amount of 100.0, no tax in reco model line - {'price_total': 100, 'debit': 100, 'credit': 0, 'name': 'combined test', 'account_id': self.env.company.expense_currency_exchange_account_id.id}, - # Tax for line 1 (65.22 + 434.78 = 500) - {'price_total': 0, 'debit': 65.22, 'credit': 0, 'name': '15%', 'account_id': self.company_data['default_account_tax_purchase'].id}, - # Tax for line 1 (32.61 + 217.39 = 250) - {'price_total': 0, 'debit': 32.61, 'credit': 0, 'name': '15% (Copy)', 'account_id': self.company_data['default_account_tax_purchase'].id}, - {'price_total': 0, 'debit': 0, 'credit': 850, 'name': 'combined test', 'account_id': self.company_data['default_account_payable'].id}, - ]) diff --git a/addons/at_accounting/tests/test_board_compute.py b/addons/at_accounting/tests/test_board_compute.py deleted file mode 100644 index 88cb79c..0000000 --- a/addons/at_accounting/tests/test_board_compute.py +++ /dev/null @@ -1,1321 +0,0 @@ -from odoo.tests.common import tagged, freeze_time -from odoo.addons.at_accounting.tests.common import TestAccountAssetCommon -from odoo import fields - - -@freeze_time('2022-07-01') -@tagged('post_install', '-at_install') -class TestAccountAssetComputation(TestAccountAssetCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.car = cls.create_asset(value=60000, periodicity="yearly", periods=5, method="linear", salvage_value=0) - - def test_linear_5_years_no_prorata_asset(self): - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 36000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_linear_5_years_no_prorata_with_imported_amount_asset(self): - self.car.write({'already_depreciated_amount_import': 1000}) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 36000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=11000, remaining_value=48000, depreciated_value=11000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=23000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=35000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=47000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=59000, state='draft'), - ]) - - def test_linear_5_years_no_prorata_with_salvage_value_asset(self): - self.car.write({'salvage_value': 1000}) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 36400) - self.assertEqual(self.car.value_residual, 35400) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=11800, remaining_value=47200, depreciated_value=11800, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=11800, remaining_value=35400, depreciated_value=23600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=11800, remaining_value=23600, depreciated_value=35400, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=11800, remaining_value=11800, depreciated_value=47200, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=11800, remaining_value=0, depreciated_value=59000, state='draft'), - ]) - - def test_linear_5_years_constant_periods_asset(self): - self.car.write({ - 'prorata_computation_type': 'constant_periods', - 'prorata_date': '2020-07-01', - }) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 42000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=6000, remaining_value=54000, depreciated_value=6000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=42000, depreciated_value=18000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=30000, depreciated_value=30000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=18000, depreciated_value=42000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=6000, depreciated_value=54000, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=6000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_linear_5_years_daily_computation_asset(self): - self.car.write({ - 'prorata_computation_type': 'daily_computation', - 'prorata_date': '2020-07-01', - }) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 41960.57) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=6046, remaining_value=53954, depreciated_value=6046, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=11993.43, remaining_value=41960.57, depreciated_value=18039.43, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=11993.43, remaining_value=29967.14, depreciated_value=30032.86, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=11993.43, remaining_value=17973.71, depreciated_value=42026.29, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12026.28, remaining_value=5947.43, depreciated_value=54052.57, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=5947.43, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_degressive_5_years_no_prorata_asset(self): - self.car.write({ - 'method': 'degressive', - 'method_progress_factor': 0.3, - }) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 29400) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=18000, remaining_value=42000, depreciated_value=18000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12600, remaining_value=29400, depreciated_value=30600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=9800, remaining_value=19600, depreciated_value=40400, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=9800, remaining_value=9800, depreciated_value=50200, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=9800, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_degressive_5_years_no_prorata_with_imported_amount_asset(self): - self.car.write({ - 'method': 'degressive', - 'method_progress_factor': 0.3, - 'already_depreciated_amount_import': 1000, - }) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 29400) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=17000, remaining_value=42000, depreciated_value=17000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12600, remaining_value=29400, depreciated_value=29600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=9800, remaining_value=19600, depreciated_value=39400, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=9800, remaining_value=9800, depreciated_value=49200, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=9800, remaining_value=0, depreciated_value=59000, state='draft'), - ]) - - def test_degressive_5_years_no_prorata_with_salvage_value_asset(self): - self.car.write({ - 'method': 'degressive', - 'method_progress_factor': 0.3, - 'salvage_value': 1000, - }) - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 29910) - self.assertEqual(self.car.value_residual, 28910) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=17700, remaining_value=41300, depreciated_value=17700, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12390, remaining_value=28910, depreciated_value=30090, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=9636.67, remaining_value=19273.33, depreciated_value=39726.67, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=9636.67, remaining_value=9636.66, depreciated_value=49363.34, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=9636.66, remaining_value=0, depreciated_value=59000, state='draft'), - ]) - - def test_degressive_then_linear_5_years_no_prorata_asset(self): - asset = self.create_asset(value=60000, periodicity="yearly", periods=5, method="degressive_then_linear", degressive_factor=0.3) - asset.validate() - self.assertEqual(asset.state, 'open') - self.assertEqual(asset.book_value, 29400) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=18000, remaining_value=42000, depreciated_value=18000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12600, remaining_value=29400, depreciated_value=30600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=17400, depreciated_value=42600, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=5400, depreciated_value=54600, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=5400, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_degressive_then_linear_5_years_no_prorata_negative_asset(self): - asset = self.create_asset(value=-60000, periodicity="yearly", periods=5, method="degressive_then_linear", degressive_factor=0.3) - asset.validate() - self.assertEqual(asset.state, 'open') - self.assertEqual(asset.book_value, -29400) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=-18000, remaining_value=-42000, depreciated_value=-18000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=-12600, remaining_value=-29400, depreciated_value=-30600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=-12000, remaining_value=-17400, depreciated_value=-42600, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=-12000, remaining_value=-5400, depreciated_value=-54600, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=-5400, remaining_value=0, depreciated_value=-60000, state='draft'), - ]) - - def test_degressive_than_linear_5_years_no_prorata_with_imported_amount_asset(self): - asset = self.create_asset(value=60000, periodicity="yearly", periods=5, method="degressive_then_linear", degressive_factor=0.3, import_depreciation=1000) - asset.validate() - self.assertEqual(asset.state, 'open') - self.assertEqual(asset.book_value, 29400) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=18000-1000, remaining_value=42000, depreciated_value=17000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12600, remaining_value=29400, depreciated_value=29600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=17400, depreciated_value=41600, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=5400, depreciated_value=53600, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=5400, remaining_value=0, depreciated_value=59000, state='draft'), - ]) - - def test_degressive_than_linear_5_years_no_prorata_with_imported_amount_negative_asset(self): - asset = self.create_asset(value=-60000, periodicity="yearly", periods=5, method="degressive_then_linear", degressive_factor=0.3, import_depreciation=-1000) - asset.validate() - self.assertEqual(asset.state, 'open') - self.assertEqual(asset.book_value, -29400) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=-18000+1000, remaining_value=-42000, depreciated_value=-17000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=-12600, remaining_value=-29400, depreciated_value=-29600, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=-12000, remaining_value=-17400, depreciated_value=-41600, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=-12000, remaining_value=-5400, depreciated_value=-53600, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=-5400, remaining_value=0, depreciated_value=-59000, state='draft'), - ]) - - def test_degressive_than_linear_5_years_no_prorata_with_salvage_value_asset(self): - asset = self.create_asset(value=60000, periodicity="yearly", periods=5, salvage_value=1000, method="degressive_then_linear", degressive_factor=0.3) - asset.validate() - self.assertEqual(asset.state, 'open') - self.assertEqual(asset.value_residual, 28910) - self.assertEqual(asset.book_value, 28910 + 1000) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=17700, remaining_value=41300, depreciated_value=17700, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12390, remaining_value=28910, depreciated_value=30090, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=11800, remaining_value=17110, depreciated_value=41890, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=11800, remaining_value=5310, depreciated_value=53690, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=5310, remaining_value=0, depreciated_value=59000, state='draft'), - ]) - - def test_degressive_then_linear_36_month_constant_period_asset(self): - """ - The depreciation amount is computed that way: Compute a degressive amount for each year and split it by month linearly. - The depreciation value could vary by one currency unit to absorb small differences that are created over time. - """ - asset = self.create_asset(value=10000, periodicity="monthly", periods=36, method="degressive_then_linear", degressive_factor=0.4) - asset.validate() - self.assertEqual(asset.state, 'open') - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=333.33, remaining_value=9666.67, depreciated_value=333.33, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=333.34, remaining_value=9333.33, depreciated_value=666.67, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=333.33, remaining_value=9000.00, depreciated_value=1000.00, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=333.33, remaining_value=8666.67, depreciated_value=1333.33, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=333.34, remaining_value=8333.33, depreciated_value=1666.67, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=333.33, remaining_value=8000.00, depreciated_value=2000.00, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=333.33, remaining_value=7666.67, depreciated_value=2333.33, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=333.34, remaining_value=7333.33, depreciated_value=2666.67, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=333.33, remaining_value=7000.00, depreciated_value=3000.00, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=333.33, remaining_value=6666.67, depreciated_value=3333.33, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=333.34, remaining_value=6333.33, depreciated_value=3666.67, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=333.33, remaining_value=6000.00, depreciated_value=4000.00, state='posted'), - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=277.78, remaining_value=5722.22, depreciated_value=4277.78, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=277.78, remaining_value=5444.44, depreciated_value=4555.56, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=277.78, remaining_value=5166.66, depreciated_value=4833.34, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=277.77, remaining_value=4888.89, depreciated_value=5111.11, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=277.78, remaining_value=4611.11, depreciated_value=5388.89, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=277.78, remaining_value=4333.33, depreciated_value=5666.67, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=277.78, remaining_value=4055.55, depreciated_value=5944.45, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=277.78, remaining_value=3777.77, depreciated_value=6222.23, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=277.77, remaining_value=3500.00, depreciated_value=6500.00, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=277.78, remaining_value=3222.22, depreciated_value=6777.78, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=277.78, remaining_value=2944.44, depreciated_value=7055.56, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=277.78, remaining_value=2666.66, depreciated_value=7333.34, state='posted'), - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=277.77, remaining_value=2388.89, depreciated_value=7611.11, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=277.78, remaining_value=2111.11, depreciated_value=7888.89, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=277.78, remaining_value=1833.33, depreciated_value=8166.67, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=277.78, remaining_value=1555.55, depreciated_value=8444.45, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=277.78, remaining_value=1277.77, depreciated_value=8722.23, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=277.77, remaining_value=1000.00, depreciated_value=9000.00, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=277.78, remaining_value=722.22, depreciated_value=9277.78, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=277.78, remaining_value=444.44, depreciated_value=9555.56, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=277.78, remaining_value=166.66, depreciated_value=9833.34, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=166.66, remaining_value=0.00, depreciated_value=10000.00, state='draft'), - ]) - - - @freeze_time('2022-06-15') - def test_asset_degressive_then_linear_prorata_start_middle_of_year(self): - """ Check the computation of an asset with degressive-linear method, - start at middle of the year - """ - asset = self.create_asset( - value=10000, - periodicity="yearly", - periods=5, - method="degressive_then_linear", - degressive_factor=0.3, - acquisition_date="2021-07-01", - prorata_computation_type="constant_periods", - ) - asset.validate() - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1500.00, remaining_value=8500.00, depreciated_value=1500.0000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=2550.00, remaining_value=5950.00, depreciated_value=4050.000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=2000.00, remaining_value=3950.00, depreciated_value=6050.000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=2000.00, remaining_value=1950.00, depreciated_value=8050.000, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=1950.00, remaining_value=0.00, depreciated_value=10000.000, state='draft'), - ]) - - def test_asset_degressive_then_linear_prorata_start_middle_of_year_monthly(self): - """ Check the computation of an asset with degressive-linear method, - start at middle of the year, monthly depreciations - """ - asset = self.create_asset( - value=10000, - periodicity="monthly", - periods=36, - method="degressive_then_linear", - degressive_factor=0.6, - acquisition_date="2021-07-01", - prorata_computation_type="constant_periods", - ) - asset.validate() - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=500.00, remaining_value=9500.00, depreciated_value=500.00, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=500.00, remaining_value=9000.00, depreciated_value=1000.00, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=500.00, remaining_value=8500.00, depreciated_value=1500.00, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=500.00, remaining_value=8000.00, depreciated_value=2000.00, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=500.00, remaining_value=7500.00, depreciated_value=2500.00, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=500.00, remaining_value=7000.00, depreciated_value=3000.00, state='posted'), - - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=350.00, remaining_value=6650.00, depreciated_value=3350.00, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=350.00, remaining_value=6300.00, depreciated_value=3700.00, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=350.00, remaining_value=5950.00, depreciated_value=4050.00, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=350.00, remaining_value=5600.00, depreciated_value=4400.00, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=350.00, remaining_value=5250.00, depreciated_value=4750.00, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=350.00, remaining_value=4900.00, depreciated_value=5100.00, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=350.00, remaining_value=4550.00, depreciated_value=5450.00, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=350.00, remaining_value=4200.00, depreciated_value=5800.00, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=350.00, remaining_value=3850.00, depreciated_value=6150.00, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=350.00, remaining_value=3500.00, depreciated_value=6500.00, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=350.00, remaining_value=3150.00, depreciated_value=6850.00, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=350.00, remaining_value=2800.00, depreciated_value=7200.00, state='draft'), - - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=277.78, remaining_value=2522.22, depreciated_value=7477.78, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=277.78, remaining_value=2244.44, depreciated_value=7755.56, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=277.77, remaining_value=1966.67, depreciated_value=8033.33, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=277.78, remaining_value=1688.89, depreciated_value=8311.11, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=277.78, remaining_value=1411.11, depreciated_value=8588.89, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=277.78, remaining_value=1133.33, depreciated_value=8866.67, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=277.77, remaining_value=855.56, depreciated_value=9144.44, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=277.78, remaining_value=577.78, depreciated_value=9422.22, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=277.78, remaining_value=300.00, depreciated_value=9700.00, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=277.78, remaining_value=22.22, depreciated_value=9977.78, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=22.22, remaining_value=0.00, depreciated_value=10000.00, state='draft'), - ]) - - def test_linear_60_months_no_prorata_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 30000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=1000, remaining_value=59000, depreciated_value=1000, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=1000, remaining_value=58000, depreciated_value=2000, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=1000, remaining_value=57000, depreciated_value=3000, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=1000, remaining_value=56000, depreciated_value=4000, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=1000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=1000, remaining_value=54000, depreciated_value=6000, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1000, remaining_value=53000, depreciated_value=7000, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1000, remaining_value=52000, depreciated_value=8000, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=1000, remaining_value=51000, depreciated_value=9000, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=1000, remaining_value=49000, depreciated_value=11000, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1000, remaining_value=48000, depreciated_value=12000, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1000, remaining_value=47000, depreciated_value=13000, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=1000, remaining_value=46000, depreciated_value=14000, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=1000, remaining_value=44000, depreciated_value=16000, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1000, remaining_value=43000, depreciated_value=17000, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=1000, remaining_value=42000, depreciated_value=18000, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1000, remaining_value=41000, depreciated_value=19000, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1000, remaining_value=40000, depreciated_value=20000, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=1000, remaining_value=39000, depreciated_value=21000, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1000, remaining_value=38000, depreciated_value=22000, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=1000, remaining_value=37000, depreciated_value=23000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1000, remaining_value=36000, depreciated_value=24000, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=1000, remaining_value=35000, depreciated_value=25000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=1000, remaining_value=34000, depreciated_value=26000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=1000, remaining_value=33000, depreciated_value=27000, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=1000, remaining_value=32000, depreciated_value=28000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=31000, depreciated_value=29000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=30000, depreciated_value=30000, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=29000, depreciated_value=31000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=28000, depreciated_value=32000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=27000, depreciated_value=33000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=26000, depreciated_value=34000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=25000, depreciated_value=35000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=24000, depreciated_value=36000, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=1000, remaining_value=23000, depreciated_value=37000, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=1000, remaining_value=22000, depreciated_value=38000, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=1000, remaining_value=21000, depreciated_value=39000, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=1000, remaining_value=20000, depreciated_value=40000, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=1000, remaining_value=19000, depreciated_value=41000, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=1000, remaining_value=18000, depreciated_value=42000, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=1000, remaining_value=17000, depreciated_value=43000, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=1000, remaining_value=16000, depreciated_value=44000, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=1000, remaining_value=15000, depreciated_value=45000, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=1000, remaining_value=14000, depreciated_value=46000, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=1000, remaining_value=13000, depreciated_value=47000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=1000, remaining_value=12000, depreciated_value=48000, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=1000, remaining_value=11000, depreciated_value=49000, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=1000, remaining_value=10000, depreciated_value=50000, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=1000, remaining_value=9000, depreciated_value=51000, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=1000, remaining_value=8000, depreciated_value=52000, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=1000, remaining_value=7000, depreciated_value=53000, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=1000, remaining_value=6000, depreciated_value=54000, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=1000, remaining_value=5000, depreciated_value=55000, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=1000, remaining_value=4000, depreciated_value=56000, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=1000, remaining_value=3000, depreciated_value=57000, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=1000, remaining_value=2000, depreciated_value=58000, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=1000, remaining_value=1000, depreciated_value=59000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=1000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_linear_60_months_no_prorata_with_imported_amount_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'already_depreciated_amount_import': 1500, - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 30000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=500.0, remaining_value=58000.0, depreciated_value=500.0, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=1000.0, remaining_value=57000.0, depreciated_value=1500.0, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=1000.0, remaining_value=56000.0, depreciated_value=2500.0, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=1000.0, remaining_value=55000.0, depreciated_value=3500.0, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=1000.0, remaining_value=54000.0, depreciated_value=4500.0, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1000.0, remaining_value=53000.0, depreciated_value=5500.0, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1000.0, remaining_value=52000.0, depreciated_value=6500.0, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=1000.0, remaining_value=51000.0, depreciated_value=7500.0, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1000.0, remaining_value=50000.0, depreciated_value=8500.0, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=1000.0, remaining_value=49000.0, depreciated_value=9500.0, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1000.0, remaining_value=48000.0, depreciated_value=10500.0, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1000.0, remaining_value=47000.0, depreciated_value=11500.0, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=1000.0, remaining_value=46000.0, depreciated_value=12500.0, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1000.0, remaining_value=45000.0, depreciated_value=13500.0, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=1000.0, remaining_value=44000.0, depreciated_value=14500.0, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1000.0, remaining_value=43000.0, depreciated_value=15500.0, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=1000.0, remaining_value=42000.0, depreciated_value=16500.0, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1000.0, remaining_value=41000.0, depreciated_value=17500.0, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1000.0, remaining_value=40000.0, depreciated_value=18500.0, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=1000.0, remaining_value=39000.0, depreciated_value=19500.0, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1000.0, remaining_value=38000.0, depreciated_value=20500.0, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=1000.0, remaining_value=37000.0, depreciated_value=21500.0, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1000.0, remaining_value=36000.0, depreciated_value=22500.0, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=1000.0, remaining_value=35000.0, depreciated_value=23500.0, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=1000.0, remaining_value=34000.0, depreciated_value=24500.0, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=1000.0, remaining_value=33000.0, depreciated_value=25500.0, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=1000.0, remaining_value=32000.0, depreciated_value=26500.0, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000.0, remaining_value=31000.0, depreciated_value=27500.0, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000.0, remaining_value=30000.0, depreciated_value=28500.0, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000.0, remaining_value=29000.0, depreciated_value=29500.0, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000.0, remaining_value=28000.0, depreciated_value=30500.0, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000.0, remaining_value=27000.0, depreciated_value=31500.0, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000.0, remaining_value=26000.0, depreciated_value=32500.0, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000.0, remaining_value=25000.0, depreciated_value=33500.0, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000.0, remaining_value=24000.0, depreciated_value=34500.0, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=1000.0, remaining_value=23000.0, depreciated_value=35500.0, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=1000.0, remaining_value=22000.0, depreciated_value=36500.0, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=1000.0, remaining_value=21000.0, depreciated_value=37500.0, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=1000.0, remaining_value=20000.0, depreciated_value=38500.0, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=1000.0, remaining_value=19000.0, depreciated_value=39500.0, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=1000.0, remaining_value=18000.0, depreciated_value=40500.0, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=1000.0, remaining_value=17000.0, depreciated_value=41500.0, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=1000.0, remaining_value=16000.0, depreciated_value=42500.0, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=1000.0, remaining_value=15000.0, depreciated_value=43500.0, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=1000.0, remaining_value=14000.0, depreciated_value=44500.0, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=1000.0, remaining_value=13000.0, depreciated_value=45500.0, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=1000.0, remaining_value=12000.0, depreciated_value=46500.0, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=1000.0, remaining_value=11000.0, depreciated_value=47500.0, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=1000.0, remaining_value=10000.0, depreciated_value=48500.0, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=1000.0, remaining_value=9000.0, depreciated_value=49500.0, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=1000.0, remaining_value=8000.0, depreciated_value=50500.0, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=1000.0, remaining_value=7000.0, depreciated_value=51500.0, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=1000.0, remaining_value=6000.0, depreciated_value=52500.0, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=1000.0, remaining_value=5000.0, depreciated_value=53500.0, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=1000.0, remaining_value=4000.0, depreciated_value=54500.0, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=1000.0, remaining_value=3000.0, depreciated_value=55500.0, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=1000.0, remaining_value=2000.0, depreciated_value=56500.0, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=1000.0, remaining_value=1000.0, depreciated_value=57500.0, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=1000.0, remaining_value=0.0, depreciated_value=58500.0, state='draft'), - ]) - - def test_linear_60_months_no_prorata_with_salvage_value_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'method_progress_factor': 0.3, - 'salvage_value': 2000, - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 31000) - self.assertEqual(self.car.value_residual, 29000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=966.67, remaining_value=57033.33, depreciated_value=966.67, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=966.66, remaining_value=56066.67, depreciated_value=1933.33, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=966.67, remaining_value=55100.0, depreciated_value=2900.0, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=966.67, remaining_value=54133.33, depreciated_value=3866.67, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=966.66, remaining_value=53166.67, depreciated_value=4833.33, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=966.67, remaining_value=52200.0, depreciated_value=5800.0, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=966.67, remaining_value=51233.33, depreciated_value=6766.67, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=966.66, remaining_value=50266.67, depreciated_value=7733.33, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=966.67, remaining_value=49300.0, depreciated_value=8700.0, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=966.67, remaining_value=48333.33, depreciated_value=9666.67, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=966.66, remaining_value=47366.67, depreciated_value=10633.33, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=966.67, remaining_value=46400.0, depreciated_value=11600.0, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=966.67, remaining_value=45433.33, depreciated_value=12566.67, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=966.66, remaining_value=44466.67, depreciated_value=13533.33, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=966.67, remaining_value=43500.0, depreciated_value=14500.0, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=966.67, remaining_value=42533.33, depreciated_value=15466.67, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=966.66, remaining_value=41566.67, depreciated_value=16433.33, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=966.67, remaining_value=40600.0, depreciated_value=17400.0, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=966.67, remaining_value=39633.33, depreciated_value=18366.67, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=966.66, remaining_value=38666.67, depreciated_value=19333.33, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=966.67, remaining_value=37700.0, depreciated_value=20300.0, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=966.67, remaining_value=36733.33, depreciated_value=21266.67, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=966.66, remaining_value=35766.67, depreciated_value=22233.33, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=966.67, remaining_value=34800.0, depreciated_value=23200.0, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=966.67, remaining_value=33833.33, depreciated_value=24166.67, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=966.66, remaining_value=32866.67, depreciated_value=25133.33, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=966.67, remaining_value=31900.0, depreciated_value=26100.0, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=966.67, remaining_value=30933.33, depreciated_value=27066.67, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=966.66, remaining_value=29966.67, depreciated_value=28033.33, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=966.67, remaining_value=29000.0, depreciated_value=29000.0, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=966.67, remaining_value=28033.33, depreciated_value=29966.67, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=966.66, remaining_value=27066.67, depreciated_value=30933.33, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=966.67, remaining_value=26100.0, depreciated_value=31900.0, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=966.67, remaining_value=25133.33, depreciated_value=32866.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=966.66, remaining_value=24166.67, depreciated_value=33833.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=966.67, remaining_value=23200.0, depreciated_value=34800.0, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=966.67, remaining_value=22233.33, depreciated_value=35766.67, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=966.66, remaining_value=21266.67, depreciated_value=36733.33, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=966.67, remaining_value=20300.0, depreciated_value=37700.0, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=966.67, remaining_value=19333.33, depreciated_value=38666.67, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=966.66, remaining_value=18366.67, depreciated_value=39633.33, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=966.67, remaining_value=17400.0, depreciated_value=40600.0, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=966.67, remaining_value=16433.33, depreciated_value=41566.67, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=966.66, remaining_value=15466.67, depreciated_value=42533.33, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=966.67, remaining_value=14500.0, depreciated_value=43500.0, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=966.67, remaining_value=13533.33, depreciated_value=44466.67, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=966.66, remaining_value=12566.67, depreciated_value=45433.33, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=966.67, remaining_value=11600.0, depreciated_value=46400.0, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=966.67, remaining_value=10633.33, depreciated_value=47366.67, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=966.66, remaining_value=9666.67, depreciated_value=48333.33, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=966.67, remaining_value=8700.0, depreciated_value=49300.0, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=966.67, remaining_value=7733.33, depreciated_value=50266.67, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=966.66, remaining_value=6766.67, depreciated_value=51233.33, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=966.67, remaining_value=5800.0, depreciated_value=52200.0, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=966.67, remaining_value=4833.33, depreciated_value=53166.67, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=966.66, remaining_value=3866.67, depreciated_value=54133.33, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=966.67, remaining_value=2900.0, depreciated_value=55100.0, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=966.67, remaining_value=1933.33, depreciated_value=56066.67, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=966.66, remaining_value=966.67, depreciated_value=57033.33, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=966.67, remaining_value=0.0, depreciated_value=58000.0, state='draft'), - ]) - - def test_linear_60_months_constant_periods_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'prorata_computation_type': 'constant_periods', - 'prorata_date': '2020-07-01', - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 36000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1000, remaining_value=59000, depreciated_value=1000, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1000, remaining_value=58000, depreciated_value=2000, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=1000, remaining_value=57000, depreciated_value=3000, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1000, remaining_value=56000, depreciated_value=4000, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=1000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1000, remaining_value=54000, depreciated_value=6000, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1000, remaining_value=53000, depreciated_value=7000, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=1000, remaining_value=52000, depreciated_value=8000, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1000, remaining_value=51000, depreciated_value=9000, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=1000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1000, remaining_value=49000, depreciated_value=11000, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=1000, remaining_value=48000, depreciated_value=12000, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1000, remaining_value=47000, depreciated_value=13000, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1000, remaining_value=46000, depreciated_value=14000, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=1000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1000, remaining_value=44000, depreciated_value=16000, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=1000, remaining_value=43000, depreciated_value=17000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1000, remaining_value=42000, depreciated_value=18000, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=1000, remaining_value=41000, depreciated_value=19000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=1000, remaining_value=40000, depreciated_value=20000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=1000, remaining_value=39000, depreciated_value=21000, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=1000, remaining_value=38000, depreciated_value=22000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=37000, depreciated_value=23000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=36000, depreciated_value=24000, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=35000, depreciated_value=25000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=34000, depreciated_value=26000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=33000, depreciated_value=27000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=32000, depreciated_value=28000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=31000, depreciated_value=29000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=30000, depreciated_value=30000, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=1000, remaining_value=29000, depreciated_value=31000, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=1000, remaining_value=28000, depreciated_value=32000, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=1000, remaining_value=27000, depreciated_value=33000, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=1000, remaining_value=26000, depreciated_value=34000, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=1000, remaining_value=25000, depreciated_value=35000, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=1000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=1000, remaining_value=23000, depreciated_value=37000, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=1000, remaining_value=22000, depreciated_value=38000, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=1000, remaining_value=21000, depreciated_value=39000, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=1000, remaining_value=20000, depreciated_value=40000, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=1000, remaining_value=19000, depreciated_value=41000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=1000, remaining_value=18000, depreciated_value=42000, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=1000, remaining_value=17000, depreciated_value=43000, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=1000, remaining_value=16000, depreciated_value=44000, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=1000, remaining_value=15000, depreciated_value=45000, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=1000, remaining_value=14000, depreciated_value=46000, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=1000, remaining_value=13000, depreciated_value=47000, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=1000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=1000, remaining_value=11000, depreciated_value=49000, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=1000, remaining_value=10000, depreciated_value=50000, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=1000, remaining_value=9000, depreciated_value=51000, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=1000, remaining_value=8000, depreciated_value=52000, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=1000, remaining_value=7000, depreciated_value=53000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=1000, remaining_value=6000, depreciated_value=54000, state='draft'), - # 2025 - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=1000, remaining_value=5000, depreciated_value=55000, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=1000, remaining_value=4000, depreciated_value=56000, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=1000, remaining_value=3000, depreciated_value=57000, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=1000, remaining_value=2000, depreciated_value=58000, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=1000, remaining_value=1000, depreciated_value=59000, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=1000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_linear_60_months_daily_computation_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'prorata_computation_type': 'daily_computation', - 'prorata_date': '2020-07-01', - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 36013.14) - - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1018.62, remaining_value=58981.38, depreciated_value=1018.62, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1018.62, remaining_value=57962.76, depreciated_value=2037.24, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=985.76, remaining_value=56977.0, depreciated_value=3023.0, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1018.62, remaining_value=55958.38, depreciated_value=4041.62, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=985.76, remaining_value=54972.62, depreciated_value=5027.38, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1018.62, remaining_value=53954.0, depreciated_value=6046.0, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1018.62, remaining_value=52935.38, depreciated_value=7064.62, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=920.05, remaining_value=52015.33, depreciated_value=7984.67, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1018.62, remaining_value=50996.71, depreciated_value=9003.29, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=985.76, remaining_value=50010.95, depreciated_value=9989.05, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1018.62, remaining_value=48992.33, depreciated_value=11007.67, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=985.76, remaining_value=48006.57, depreciated_value=11993.43, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1018.62, remaining_value=46987.95, depreciated_value=13012.05, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1018.62, remaining_value=45969.33, depreciated_value=14030.67, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=985.76, remaining_value=44983.57, depreciated_value=15016.43, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1018.62, remaining_value=43964.95, depreciated_value=16035.05, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=985.76, remaining_value=42979.19, depreciated_value=17020.81, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1018.62, remaining_value=41960.57, depreciated_value=18039.43, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=1018.62, remaining_value=40941.95, depreciated_value=19058.05, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=920.04, remaining_value=40021.91, depreciated_value=19978.09, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=1018.62, remaining_value=39003.29, depreciated_value=20996.71, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=985.77, remaining_value=38017.52, depreciated_value=21982.48, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1018.62, remaining_value=36998.9, depreciated_value=23001.10, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=985.76, remaining_value=36013.14, depreciated_value=23986.86, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1018.62, remaining_value=34994.52, depreciated_value=25005.48, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1018.62, remaining_value=33975.9, depreciated_value=26024.10, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=985.76, remaining_value=32990.14, depreciated_value=27009.86, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1018.62, remaining_value=31971.52, depreciated_value=28028.48, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=985.76, remaining_value=30985.76, depreciated_value=29014.24, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1018.62, remaining_value=29967.14, depreciated_value=30032.86, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=1018.62, remaining_value=28948.52, depreciated_value=31051.48, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=920.04, remaining_value=28028.48, depreciated_value=31971.52, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=1018.62, remaining_value=27009.86, depreciated_value=32990.14, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=985.76, remaining_value=26024.10, depreciated_value=33975.9, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=1018.62, remaining_value=25005.48, depreciated_value=34994.52, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=985.76, remaining_value=24019.72, depreciated_value=35980.28, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=1018.62, remaining_value=23001.10, depreciated_value=36998.9, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=1018.62, remaining_value=21982.48, depreciated_value=38017.52, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=985.77, remaining_value=20996.71, depreciated_value=39003.29, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=1018.62, remaining_value=19978.09, depreciated_value=40021.91, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=985.76, remaining_value=18992.33, depreciated_value=41007.67, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=1018.62, remaining_value=17973.71, depreciated_value=42026.29, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=1018.62, remaining_value=16955.09, depreciated_value=43044.91, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=952.9, remaining_value=16002.19, depreciated_value=43997.81, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=1018.62, remaining_value=14983.57, depreciated_value=45016.43, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=985.76, remaining_value=13997.81, depreciated_value=46002.19, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=1018.62, remaining_value=12979.19, depreciated_value=47020.81, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=985.76, remaining_value=11993.43, depreciated_value=48006.57, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=1018.62, remaining_value=10974.81, depreciated_value=49025.19, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=1018.62, remaining_value=9956.19, depreciated_value=50043.81, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=985.76, remaining_value=8970.43, depreciated_value=51029.57, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=1018.62, remaining_value=7951.81, depreciated_value=52048.19, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=985.76, remaining_value=6966.05, depreciated_value=53033.95, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=1018.62, remaining_value=5947.43, depreciated_value=54052.57, state='draft'), - # 2025 - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=1018.62, remaining_value=4928.81, depreciated_value=55071.19, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=920.05, remaining_value=4008.76, depreciated_value=55991.24, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=1018.62, remaining_value=2990.14, depreciated_value=57009.86, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=985.76, remaining_value=2004.38, depreciated_value=57995.62, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=1018.62, remaining_value=985.76, depreciated_value=59014.24, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=985.76, remaining_value=0.0, depreciated_value=60000.0, state='draft'), - ]) - - def test_degressive_60_months_no_prorata_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'method': 'degressive', - 'method_progress_factor': 0.3, - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 24500) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=1500.0, remaining_value=58500.0, depreciated_value=1500.0, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=1500.0, remaining_value=57000.0, depreciated_value=3000.0, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=1500.0, remaining_value=55500.0, depreciated_value=4500.0, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=1500.0, remaining_value=54000.0, depreciated_value=6000.0, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=1500.0, remaining_value=52500.0, depreciated_value=7500.0, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=1500.0, remaining_value=51000.0, depreciated_value=9000.0, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1500.0, remaining_value=49500.0, depreciated_value=10500.0, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1500.0, remaining_value=48000.0, depreciated_value=12000.0, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=1500.0, remaining_value=46500.0, depreciated_value=13500.0, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1500.0, remaining_value=45000.0, depreciated_value=15000.0, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=1500.0, remaining_value=43500.0, depreciated_value=16500.0, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1500.0, remaining_value=42000.0, depreciated_value=18000.0, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1050.0, remaining_value=40950.0, depreciated_value=19050.0, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=1050.0, remaining_value=39900.0, depreciated_value=20100.0, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1050.0, remaining_value=38850.0, depreciated_value=21150.0, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=1050.0, remaining_value=37800.0, depreciated_value=22200.0, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1050.0, remaining_value=36750.0, depreciated_value=23250.0, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=1050.0, remaining_value=35700.0, depreciated_value=24300.0, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1050.0, remaining_value=34650.0, depreciated_value=25350.0, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1050.0, remaining_value=33600.0, depreciated_value=26400.0, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=1050.0, remaining_value=32550.0, depreciated_value=27450.0, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1050.0, remaining_value=31500.0, depreciated_value=28500.0, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=1050.0, remaining_value=30450.0, depreciated_value=29550.0, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1050.0, remaining_value=29400.0, depreciated_value=30600.0, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=816.67, remaining_value=28583.33, depreciated_value=31416.67, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=816.66, remaining_value=27766.67, depreciated_value=32233.33, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=816.67, remaining_value=26950.0, depreciated_value=33050.0, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=816.67, remaining_value=26133.33, depreciated_value=33866.67, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=816.66, remaining_value=25316.67, depreciated_value=34683.33, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=816.67, remaining_value=24500.0, depreciated_value=35500.0, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=816.67, remaining_value=23683.33, depreciated_value=36316.67, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=816.66, remaining_value=22866.67, depreciated_value=37133.33, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=816.67, remaining_value=22050.0, depreciated_value=37950.0, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=816.67, remaining_value=21233.33, depreciated_value=38766.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=816.66, remaining_value=20416.67, depreciated_value=39583.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=816.67, remaining_value=19600.0, depreciated_value=40400.0, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=816.67, remaining_value=18783.33, depreciated_value=41216.67, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=816.66, remaining_value=17966.67, depreciated_value=42033.33, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=816.67, remaining_value=17150.0, depreciated_value=42850.0, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=816.67, remaining_value=16333.33, depreciated_value=43666.67, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=816.66, remaining_value=15516.67, depreciated_value=44483.33, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=816.67, remaining_value=14700.0, depreciated_value=45300.0, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=816.67, remaining_value=13883.33, depreciated_value=46116.67, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=816.66, remaining_value=13066.67, depreciated_value=46933.33, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=816.67, remaining_value=12250.0, depreciated_value=47750.0, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=816.67, remaining_value=11433.33, depreciated_value=48566.67, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=816.66, remaining_value=10616.67, depreciated_value=49383.33, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=816.67, remaining_value=9800.0, depreciated_value=50200.0, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=816.67, remaining_value=8983.33, depreciated_value=51016.67, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=816.66, remaining_value=8166.67, depreciated_value=51833.33, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=816.67, remaining_value=7350.0, depreciated_value=52650.0, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=816.67, remaining_value=6533.33, depreciated_value=53466.67, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=816.66, remaining_value=5716.67, depreciated_value=54283.33, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=816.67, remaining_value=4900.0, depreciated_value=55100.0, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=816.67, remaining_value=4083.33, depreciated_value=55916.67, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=816.66, remaining_value=3266.67, depreciated_value=56733.33, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=816.67, remaining_value=2450.0, depreciated_value=57550.0, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=816.67, remaining_value=1633.33, depreciated_value=58366.67, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=816.66, remaining_value=816.67, depreciated_value=59183.33, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=816.67, remaining_value=0.0, depreciated_value=60000.0, state='draft'), - ]) - - def test_degressive_60_months_from_middle_year(self): - asset = self.create_asset( - value=100000, - periodicity='monthly', - periods=60, - method='degressive', - method_progress_factor=0.35, - acquisition_date='2022-07-01', - prorata_computation_type='constant_periods' - ) - asset.compute_depreciation_board() - self.assertEqual(asset.state, 'draft') - self.assertEqual(asset.book_value, 100000) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=2916.67, remaining_value=97083.33, depreciated_value=2916.67, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=2916.66, remaining_value=94166.67, depreciated_value=5833.33, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=2916.67, remaining_value=91250.00, depreciated_value=8750.00, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=2916.67, remaining_value=88333.33, depreciated_value=11666.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=2916.66, remaining_value=85416.67, depreciated_value=14583.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=2916.67, remaining_value=82500.00, depreciated_value=17500.00, state='draft'), - - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=2406.25, remaining_value=80093.75, depreciated_value=19906.25, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=2406.25, remaining_value=77687.50, depreciated_value=22312.50, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=2406.25, remaining_value=75281.25, depreciated_value=24718.75, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=2406.25, remaining_value=72875.00, depreciated_value=27125.00, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=2406.25, remaining_value=70468.75, depreciated_value=29531.25, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=2406.25, remaining_value=68062.50, depreciated_value=31937.50, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=2406.25, remaining_value=65656.25, depreciated_value=34343.75, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=2406.25, remaining_value=63250.00, depreciated_value=36750.00, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=2406.25, remaining_value=60843.75, depreciated_value=39156.25, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=2406.25, remaining_value=58437.50, depreciated_value=41562.50, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=2406.25, remaining_value=56031.25, depreciated_value=43968.75, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=2406.25, remaining_value=53625.00, depreciated_value=46375.00, state='draft'), - - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=1564.06, remaining_value=52060.94, depreciated_value=47939.06, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=1564.07, remaining_value=50496.87, depreciated_value=49503.13, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=1564.06, remaining_value=48932.81, depreciated_value=51067.19, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=1564.06, remaining_value=47368.75, depreciated_value=52631.25, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=1564.06, remaining_value=45804.69, depreciated_value=54195.31, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=1564.07, remaining_value=44240.62, depreciated_value=55759.38, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=1564.06, remaining_value=42676.56, depreciated_value=57323.44, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=1564.06, remaining_value=41112.50, depreciated_value=58887.50, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=1564.06, remaining_value=39548.44, depreciated_value=60451.56, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=1564.07, remaining_value=37984.37, depreciated_value=62015.63, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=1564.06, remaining_value=36420.31, depreciated_value=63579.69, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=1564.06, remaining_value=34856.25, depreciated_value=65143.75, state='draft'), - - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=1161.88, remaining_value=33694.37, depreciated_value=66305.63, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=1161.87, remaining_value=32532.50, depreciated_value=67467.50, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=1161.88, remaining_value=31370.62, depreciated_value=68629.38, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=1161.87, remaining_value=30208.75, depreciated_value=69791.25, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=1161.88, remaining_value=29046.87, depreciated_value=70953.13, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=1161.87, remaining_value=27885.00, depreciated_value=72115.00, state='draft'), - self._get_depreciation_move_values(date='2025-07-31', depreciation_value=1161.88, remaining_value=26723.12, depreciated_value=73276.88, state='draft'), - self._get_depreciation_move_values(date='2025-08-31', depreciation_value=1161.87, remaining_value=25561.25, depreciated_value=74438.75, state='draft'), - self._get_depreciation_move_values(date='2025-09-30', depreciation_value=1161.88, remaining_value=24399.37, depreciated_value=75600.63, state='draft'), - self._get_depreciation_move_values(date='2025-10-31', depreciation_value=1161.87, remaining_value=23237.50, depreciated_value=76762.50, state='draft'), - self._get_depreciation_move_values(date='2025-11-30', depreciation_value=1161.88, remaining_value=22075.62, depreciated_value=77924.38, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=1161.87, remaining_value=20913.75, depreciated_value=79086.25, state='draft'), - - self._get_depreciation_move_values(date='2026-01-31', depreciation_value=1161.88, remaining_value=19751.87, depreciated_value=80248.13, state='draft'), - self._get_depreciation_move_values(date='2026-02-28', depreciation_value=1161.87, remaining_value=18590.00, depreciated_value=81410.00, state='draft'), - self._get_depreciation_move_values(date='2026-03-31', depreciation_value=1161.88, remaining_value=17428.12, depreciated_value=82571.88, state='draft'), - self._get_depreciation_move_values(date='2026-04-30', depreciation_value=1161.87, remaining_value=16266.25, depreciated_value=83733.75, state='draft'), - self._get_depreciation_move_values(date='2026-05-31', depreciation_value=1161.88, remaining_value=15104.37, depreciated_value=84895.63, state='draft'), - self._get_depreciation_move_values(date='2026-06-30', depreciation_value=1161.87, remaining_value=13942.50, depreciated_value=86057.50, state='draft'), - self._get_depreciation_move_values(date='2026-07-31', depreciation_value=1161.88, remaining_value=12780.62, depreciated_value=87219.38, state='draft'), - self._get_depreciation_move_values(date='2026-08-31', depreciation_value=1161.87, remaining_value=11618.75, depreciated_value=88381.25, state='draft'), - self._get_depreciation_move_values(date='2026-09-30', depreciation_value=1161.88, remaining_value=10456.87, depreciated_value=89543.13, state='draft'), - self._get_depreciation_move_values(date='2026-10-31', depreciation_value=1161.87, remaining_value=9295.00, depreciated_value=90705.00, state='draft'), - self._get_depreciation_move_values(date='2026-11-30', depreciation_value=1161.88, remaining_value=8133.12, depreciated_value=91866.88, state='draft'), - self._get_depreciation_move_values(date='2026-12-31', depreciation_value=1161.87, remaining_value=6971.25, depreciated_value=93028.75, state='draft'), - - self._get_depreciation_move_values(date='2027-01-31', depreciation_value=1161.88, remaining_value=5809.37, depreciated_value=94190.63, state='draft'), - self._get_depreciation_move_values(date='2027-02-28', depreciation_value=1161.87, remaining_value=4647.50, depreciated_value=95352.50, state='draft'), - self._get_depreciation_move_values(date='2027-03-31', depreciation_value=1161.88, remaining_value=3485.62, depreciated_value=96514.38, state='draft'), - self._get_depreciation_move_values(date='2027-04-30', depreciation_value=1161.87, remaining_value=2323.75, depreciated_value=97676.25, state='draft'), - self._get_depreciation_move_values(date='2027-05-31', depreciation_value=1161.88, remaining_value=1161.87, depreciated_value=98838.13, state='draft'), - self._get_depreciation_move_values(date='2027-06-30', depreciation_value=1161.87, remaining_value=0.00, depreciated_value=100000.00, state='draft'), - ]) - - def test_degressive_60_months_from_middle_sync_with_fiscalyear(self): - company = self.env.company - company.fiscalyear_last_day = 30 - company.fiscalyear_last_month = '6' - asset = self.create_asset( - value=100000, - periodicity='monthly', - periods=60, - method='degressive', - method_progress_factor=0.35, - acquisition_date='2022-07-01', - prorata_computation_type='constant_periods' - ) - asset.compute_depreciation_board() - self.assertEqual(asset.state, 'draft') - self.assertEqual(asset.book_value, 100000) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=2916.67, remaining_value=97083.33, depreciated_value=2916.67, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=2916.66, remaining_value=94166.67, depreciated_value=5833.33, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=2916.67, remaining_value=91250.00, depreciated_value=8750.00, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=2916.67, remaining_value=88333.33, depreciated_value=11666.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=2916.66, remaining_value=85416.67, depreciated_value=14583.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=2916.67, remaining_value=82500.00, depreciated_value=17500.00, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=2916.67, remaining_value=79583.33, depreciated_value=20416.67, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=2916.66, remaining_value=76666.67, depreciated_value=23333.33, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=2916.67, remaining_value=73750.00, depreciated_value=26250.00, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=2916.67, remaining_value=70833.33, depreciated_value=29166.67, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=2916.66, remaining_value=67916.67, depreciated_value=32083.33, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=2916.67, remaining_value=65000.00, depreciated_value=35000.00, state='draft'), - - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=1895.83, remaining_value=63104.17, depreciated_value=36895.83, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=1895.84, remaining_value=61208.33, depreciated_value=38791.67, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=1895.83, remaining_value=59312.50, depreciated_value=40687.50, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=1895.83, remaining_value=57416.67, depreciated_value=42583.33, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=1895.84, remaining_value=55520.83, depreciated_value=44479.17, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=1895.83, remaining_value=53625.00, depreciated_value=46375.00, state='draft'), - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=1895.83, remaining_value=51729.17, depreciated_value=48270.83, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=1895.84, remaining_value=49833.33, depreciated_value=50166.67, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=1895.83, remaining_value=47937.50, depreciated_value=52062.50, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=1895.83, remaining_value=46041.67, depreciated_value=53958.33, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=1895.84, remaining_value=44145.83, depreciated_value=55854.17, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=1895.83, remaining_value=42250.00, depreciated_value=57750.00, state='draft'), - - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=1232.29, remaining_value=41017.71, depreciated_value=58982.29, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=1232.29, remaining_value=39785.42, depreciated_value=60214.58, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=1232.29, remaining_value=38553.13, depreciated_value=61446.87, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=1232.30, remaining_value=37320.83, depreciated_value=62679.17, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=1232.29, remaining_value=36088.54, depreciated_value=63911.46, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=1232.29, remaining_value=34856.25, depreciated_value=65143.75, state='draft'), - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=1232.29, remaining_value=33623.96, depreciated_value=66376.04, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=1232.29, remaining_value=32391.67, depreciated_value=67608.33, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=1232.29, remaining_value=31159.38, depreciated_value=68840.62, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=1232.30, remaining_value=29927.08, depreciated_value=70072.92, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=1232.29, remaining_value=28694.79, depreciated_value=71305.21, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=1232.29, remaining_value=27462.50, depreciated_value=72537.50, state='draft'), - - self._get_depreciation_move_values(date='2025-07-31', depreciation_value=1144.27, remaining_value=26318.23, depreciated_value=73681.77, state='draft'), - self._get_depreciation_move_values(date='2025-08-31', depreciation_value=1144.27, remaining_value=25173.96, depreciated_value=74826.04, state='draft'), - self._get_depreciation_move_values(date='2025-09-30', depreciation_value=1144.27, remaining_value=24029.69, depreciated_value=75970.31, state='draft'), - self._get_depreciation_move_values(date='2025-10-31', depreciation_value=1144.27, remaining_value=22885.42, depreciated_value=77114.58, state='draft'), - self._get_depreciation_move_values(date='2025-11-30', depreciation_value=1144.27, remaining_value=21741.15, depreciated_value=78258.85, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=1144.27, remaining_value=20596.88, depreciated_value=79403.12, state='draft'), - self._get_depreciation_move_values(date='2026-01-31', depreciation_value=1144.28, remaining_value=19452.60, depreciated_value=80547.40, state='draft'), - self._get_depreciation_move_values(date='2026-02-28', depreciation_value=1144.27, remaining_value=18308.33, depreciated_value=81691.67, state='draft'), - self._get_depreciation_move_values(date='2026-03-31', depreciation_value=1144.27, remaining_value=17164.06, depreciated_value=82835.94, state='draft'), - self._get_depreciation_move_values(date='2026-04-30', depreciation_value=1144.27, remaining_value=16019.79, depreciated_value=83980.21, state='draft'), - self._get_depreciation_move_values(date='2026-05-31', depreciation_value=1144.27, remaining_value=14875.52, depreciated_value=85124.48, state='draft'), - self._get_depreciation_move_values(date='2026-06-30', depreciation_value=1144.27, remaining_value=13731.25, depreciated_value=86268.75, state='draft'), - - self._get_depreciation_move_values(date='2026-07-31', depreciation_value=1144.27, remaining_value=12586.98, depreciated_value=87413.02, state='draft'), - self._get_depreciation_move_values(date='2026-08-31', depreciation_value=1144.27, remaining_value=11442.71, depreciated_value=88557.29, state='draft'), - self._get_depreciation_move_values(date='2026-09-30', depreciation_value=1144.27, remaining_value=10298.44, depreciated_value=89701.56, state='draft'), - self._get_depreciation_move_values(date='2026-10-31', depreciation_value=1144.27, remaining_value=9154.17, depreciated_value=90845.83, state='draft'), - self._get_depreciation_move_values(date='2026-11-30', depreciation_value=1144.27, remaining_value=8009.90, depreciated_value=91990.10, state='draft'), - self._get_depreciation_move_values(date='2026-12-31', depreciation_value=1144.27, remaining_value=6865.63, depreciated_value=93134.37, state='draft'), - self._get_depreciation_move_values(date='2027-01-31', depreciation_value=1144.28, remaining_value=5721.35, depreciated_value=94278.65, state='draft'), - self._get_depreciation_move_values(date='2027-02-28', depreciation_value=1144.27, remaining_value=4577.08, depreciated_value=95422.92, state='draft'), - self._get_depreciation_move_values(date='2027-03-31', depreciation_value=1144.27, remaining_value=3432.81, depreciated_value=96567.19, state='draft'), - self._get_depreciation_move_values(date='2027-04-30', depreciation_value=1144.27, remaining_value=2288.54, depreciated_value=97711.46, state='draft'), - self._get_depreciation_move_values(date='2027-05-31', depreciation_value=1144.27, remaining_value=1144.27, depreciated_value=98855.73, state='draft'), - self._get_depreciation_move_values(date='2027-06-30', depreciation_value=1144.27, remaining_value=0.00, depreciated_value=100000.00, state='draft'), - ]) - - def test_degressive_60_months_no_prorata_with_imported_amount_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'method': 'degressive', - 'method_progress_factor': 0.3, - 'already_depreciated_amount_import': 2000, - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 24500) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=1000.0, remaining_value=57000.0, depreciated_value=1000.0, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=1500.0, remaining_value=55500.0, depreciated_value=2500.0, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=1500.0, remaining_value=54000.0, depreciated_value=4000.0, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=1500.0, remaining_value=52500.0, depreciated_value=5500.0, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=1500.0, remaining_value=51000.0, depreciated_value=7000.0, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1500.0, remaining_value=49500.0, depreciated_value=8500.0, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1500.0, remaining_value=48000.0, depreciated_value=10000.0, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=1500.0, remaining_value=46500.0, depreciated_value=11500.0, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1500.0, remaining_value=45000.0, depreciated_value=13000.0, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=1500.0, remaining_value=43500.0, depreciated_value=14500.0, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1500.0, remaining_value=42000.0, depreciated_value=16000.0, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1050.0, remaining_value=40950.0, depreciated_value=17050.0, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=1050.0, remaining_value=39900.0, depreciated_value=18100.0, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1050.0, remaining_value=38850.0, depreciated_value=19150.0, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=1050.0, remaining_value=37800.0, depreciated_value=20200.0, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1050.0, remaining_value=36750.0, depreciated_value=21250.0, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=1050.0, remaining_value=35700.0, depreciated_value=22300.0, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1050.0, remaining_value=34650.0, depreciated_value=23350.0, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1050.0, remaining_value=33600.0, depreciated_value=24400.0, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=1050.0, remaining_value=32550.0, depreciated_value=25450.0, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1050.0, remaining_value=31500.0, depreciated_value=26500.0, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=1050.0, remaining_value=30450.0, depreciated_value=27550.0, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1050.0, remaining_value=29400.0, depreciated_value=28600.0, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=816.67, remaining_value=28583.33, depreciated_value=29416.67, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=816.66, remaining_value=27766.67, depreciated_value=30233.33, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=816.67, remaining_value=26950.0, depreciated_value=31050.0, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=816.67, remaining_value=26133.33, depreciated_value=31866.67, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=816.66, remaining_value=25316.67, depreciated_value=32683.33, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=816.67, remaining_value=24500.0, depreciated_value=33500.0, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=816.67, remaining_value=23683.33, depreciated_value=34316.67, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=816.66, remaining_value=22866.67, depreciated_value=35133.33, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=816.67, remaining_value=22050.0, depreciated_value=35950.0, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=816.67, remaining_value=21233.33, depreciated_value=36766.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=816.66, remaining_value=20416.67, depreciated_value=37583.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=816.67, remaining_value=19600.0, depreciated_value=38400.0, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=816.67, remaining_value=18783.33, depreciated_value=39216.67, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=816.66, remaining_value=17966.67, depreciated_value=40033.33, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=816.67, remaining_value=17150.0, depreciated_value=40850.0, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=816.67, remaining_value=16333.33, depreciated_value=41666.67, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=816.66, remaining_value=15516.67, depreciated_value=42483.33, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=816.67, remaining_value=14700.0, depreciated_value=43300.0, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=816.67, remaining_value=13883.33, depreciated_value=44116.67, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=816.66, remaining_value=13066.67, depreciated_value=44933.33, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=816.67, remaining_value=12250.0, depreciated_value=45750.0, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=816.67, remaining_value=11433.33, depreciated_value=46566.67, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=816.66, remaining_value=10616.67, depreciated_value=47383.33, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=816.67, remaining_value=9800.0, depreciated_value=48200.0, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=816.67, remaining_value=8983.33, depreciated_value=49016.67, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=816.66, remaining_value=8166.67, depreciated_value=49833.33, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=816.67, remaining_value=7350.0, depreciated_value=50650.0, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=816.67, remaining_value=6533.33, depreciated_value=51466.67, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=816.66, remaining_value=5716.67, depreciated_value=52283.33, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=816.67, remaining_value=4900.0, depreciated_value=53100.0, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=816.67, remaining_value=4083.33, depreciated_value=53916.67, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=816.66, remaining_value=3266.67, depreciated_value=54733.33, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=816.67, remaining_value=2450.0, depreciated_value=55550.0, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=816.67, remaining_value=1633.33, depreciated_value=56366.67, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=816.66, remaining_value=816.67, depreciated_value=57183.33, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=816.67, remaining_value=0.0, depreciated_value=58000.0, state='draft'), - ]) - - def test_degressive_60_months_no_prorata_with_salvage_value_asset(self): - self.car.write({ - 'method_number': 60, - 'method_period': '1', - 'method': 'degressive', - 'method_progress_factor': 0.3, - 'salvage_value': 2000, - }) - self.car.validate() - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 25683.33) - self.assertEqual(self.car.value_residual, 23683.33) - self.assertRecordValues(self.car.depreciation_move_ids, [ - # 2020 - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=1450.0, remaining_value=56550.0, depreciated_value=1450.0, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=1450.0, remaining_value=55100.0, depreciated_value=2900.0, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=1450.0, remaining_value=53650.0, depreciated_value=4350.0, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=1450.0, remaining_value=52200.0, depreciated_value=5800.0, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=1450.0, remaining_value=50750.0, depreciated_value=7250.0, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=1450.0, remaining_value=49300.0, depreciated_value=8700.0, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=1450.0, remaining_value=47850.0, depreciated_value=10150.0, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=1450.0, remaining_value=46400.0, depreciated_value=11600.0, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=1450.0, remaining_value=44950.0, depreciated_value=13050.0, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=1450.0, remaining_value=43500.0, depreciated_value=14500.0, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=1450.0, remaining_value=42050.0, depreciated_value=15950.0, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=1450.0, remaining_value=40600.0, depreciated_value=17400.0, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=1015.0, remaining_value=39585.0, depreciated_value=18415.0, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=1015.0, remaining_value=38570.0, depreciated_value=19430.0, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=1015.0, remaining_value=37555.0, depreciated_value=20445.0, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=1015.0, remaining_value=36540.0, depreciated_value=21460.0, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=1015.0, remaining_value=35525.0, depreciated_value=22475.0, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=1015.0, remaining_value=34510.0, depreciated_value=23490.0, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=1015.0, remaining_value=33495.0, depreciated_value=24505.0, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=1015.0, remaining_value=32480.0, depreciated_value=25520.0, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=1015.0, remaining_value=31465.0, depreciated_value=26535.0, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=1015.0, remaining_value=30450.0, depreciated_value=27550.0, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=1015.0, remaining_value=29435.0, depreciated_value=28565.0, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=1015.0, remaining_value=28420.0, depreciated_value=29580.0, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=789.44, remaining_value=27630.56, depreciated_value=30369.44, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=789.45, remaining_value=26841.11, depreciated_value=31158.89, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=789.44, remaining_value=26051.67, depreciated_value=31948.33, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=789.45, remaining_value=25262.22, depreciated_value=32737.78, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=789.44, remaining_value=24472.78, depreciated_value=33527.22, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=789.45, remaining_value=23683.33, depreciated_value=34316.67, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=789.44, remaining_value=22893.89, depreciated_value=35106.11, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=789.45, remaining_value=22104.44, depreciated_value=35895.56, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=789.44, remaining_value=21315.0, depreciated_value=36685.0, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=789.44, remaining_value=20525.56, depreciated_value=37474.44, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=789.45, remaining_value=19736.11, depreciated_value=38263.89, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=789.44, remaining_value=18946.67, depreciated_value=39053.33, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=789.44, remaining_value=18157.23, depreciated_value=39842.77, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=789.45, remaining_value=17367.78, depreciated_value=40632.22, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=789.44, remaining_value=16578.34, depreciated_value=41421.66, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=789.45, remaining_value=15788.89, depreciated_value=42211.11, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=789.44, remaining_value=14999.45, depreciated_value=43000.55, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=789.45, remaining_value=14210.0, depreciated_value=43790.0, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=789.44, remaining_value=13420.56, depreciated_value=44579.44, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=789.45, remaining_value=12631.11, depreciated_value=45368.89, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=789.44, remaining_value=11841.67, depreciated_value=46158.33, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=789.45, remaining_value=11052.22, depreciated_value=46947.78, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=789.44, remaining_value=10262.78, depreciated_value=47737.22, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=789.45, remaining_value=9473.33, depreciated_value=48526.67, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=789.44, remaining_value=8683.89, depreciated_value=49316.11, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=789.45, remaining_value=7894.44, depreciated_value=50105.56, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=789.44, remaining_value=7105.0, depreciated_value=50895.0, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=789.45, remaining_value=6315.55, depreciated_value=51684.45, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=789.44, remaining_value=5526.11, depreciated_value=52473.89, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=789.45, remaining_value=4736.66, depreciated_value=53263.34, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=789.44, remaining_value=3947.22, depreciated_value=54052.78, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=789.44, remaining_value=3157.78, depreciated_value=54842.22, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=789.45, remaining_value=2368.33, depreciated_value=55631.67, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=789.44, remaining_value=1578.89, depreciated_value=56421.11, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=789.45, remaining_value=789.44, depreciated_value=57210.56, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=789.44, remaining_value=0.0, depreciated_value=58000.0, state='draft'), - ]) - - def test_degressive_5_years_from_beggining_of_year(self): - asset = self.create_asset( - value=100000, - periodicity='yearly', - periods=5, - method='degressive', - method_progress_factor=0.35, - acquisition_date='2022-01-01', - prorata_computation_type='constant_periods' - ) - asset.compute_depreciation_board() - self.assertEqual(asset.state, 'draft') - self.assertEqual(asset.book_value, 100000) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=35000.00, remaining_value=65000.00, depreciated_value=35000.00, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=22750.00, remaining_value=42250.00, depreciated_value=57750.00, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=14787.50, remaining_value=27462.50, depreciated_value=72537.50, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=13731.25, remaining_value=13731.25, depreciated_value=86268.75, state='draft'), - self._get_depreciation_move_values(date='2026-12-31', depreciation_value=13731.25, remaining_value=0.00, depreciated_value=100000.00, state='draft'), - ]) - - def test_degressive_5_years_from_middle_of_year(self): - asset = self.create_asset( - value=100000, - periodicity='yearly', - periods=5, - method='degressive', - method_progress_factor=0.35, - acquisition_date='2022-07-01', - prorata_computation_type='constant_periods' - ) - asset.compute_depreciation_board() - self.assertEqual(asset.state, 'draft') - self.assertEqual(asset.book_value, 100000) - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=17500.00, remaining_value=82500.00, depreciated_value=17500.00, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=28875.00, remaining_value=53625.00, depreciated_value=46375.00, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=18768.75, remaining_value=34856.25, depreciated_value=65143.75, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=13942.50, remaining_value=20913.75, depreciated_value=79086.25, state='draft'), - self._get_depreciation_move_values(date='2026-12-31', depreciation_value=13942.50, remaining_value=6971.25, depreciated_value=93028.75, state='draft'), - self._get_depreciation_move_values(date='2027-12-31', depreciation_value=6971.25, remaining_value=0.00, depreciated_value=100000.00, state='draft'), - ]) - - def test_compute_board_in_mass(self): - book = self.create_asset(value=35, periodicity="monthly", periods=2, method="linear", salvage_value=0) - shelf = self.create_asset(value=250, periodicity="monthly", periods=8, method="linear", salvage_value=0) - screw = self.create_asset(value=1, periodicity="monthly", periods=1, method="linear", salvage_value=0) - - (book + screw).validate() - (book + shelf + screw).compute_depreciation_board() - - self.assertRecordValues(book.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=17.5, remaining_value=17.5, depreciated_value=17.5, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=17.5, remaining_value=0, depreciated_value=35, state='posted'), - ]) - - self.assertRecordValues(shelf.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=31.25, remaining_value=218.75, depreciated_value=31.25, state='draft'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=31.25, remaining_value=187.5, depreciated_value=62.5, state='draft'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=31.25, remaining_value=156.25, depreciated_value=93.75, state='draft'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=31.25, remaining_value=125, depreciated_value=125, state='draft'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=31.25, remaining_value=93.75, depreciated_value=156.25, state='draft'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=31.25, remaining_value=62.5, depreciated_value=187.5, state='draft'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=31.25, remaining_value=31.25, depreciated_value=218.75, state='draft'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=31.25, remaining_value=0, depreciated_value=250, state='draft'), - ]) - - self.assertRecordValues(screw.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=1, remaining_value=0, depreciated_value=1, state='posted'), - ]) - def test_copy_prorata_date(self): - """ Verifies that prorata date and acquisition date are copied when duplicate an asset - For this test, the prorata computation type is set to None. - The idea is of this test is to verify that we do copy prorata date. - """ - old_car_asset = self.create_asset( - value=60000, - periodicity='yearly', - periods=5, - method='linear', - salvage_value=0, - - ) - old_car_asset.validate() - - self.assertEqual(old_car_asset.state, 'open') - self.assertEqual(old_car_asset.book_value, 36000) - self.assertEqual(old_car_asset.acquisition_date, fields.Date.from_string('2020-02-01')) - self.assertEqual(old_car_asset.prorata_date, fields.Date.from_string('2020-01-01')) - self.assertRecordValues(old_car_asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - new_car_asset = old_car_asset.copy() - new_car_asset.original_value = 60000 - new_car_asset.validate() - - self.assertEqual(new_car_asset.state, 'open') - self.assertEqual(new_car_asset.book_value, 36000) - self.assertEqual(new_car_asset.acquisition_date, fields.Date.from_string('2020-02-01')) - self.assertEqual(new_car_asset.prorata_date, fields.Date.from_string('2020-01-01')) - self.assertRecordValues(new_car_asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_change_computation_method_before_lock_date(self): - """Test that we can change the computation method when there are draft moves before the lock date. - """ - self.car.company_id.fiscalyear_lock_date = '2022-06-30' - self.car.compute_depreciation_board() - - self.assertEqual(self.car.state, 'draft') - self.assertEqual(self.car.book_value, 60000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='draft'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - # Change the computation type - self.car.prorata_computation_type = 'constant_periods' - self.car.prorata_date = '2021-01-01' - self.car.compute_depreciation_board() - - self.assertEqual(self.car.state, 'draft') - self.assertEqual(self.car.book_value, 60000) - self.assertRecordValues(self.car.depreciation_move_ids.sorted(lambda m: (m.date, m.id)), [ - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2025-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_post_moves_after_lock_date(self): - """Test that we can change the computation method when there are draft moves before the lock date. - """ - self.car.company_id.fiscalyear_lock_date = '2021-06-30' - self.car.compute_depreciation_board() - - self.assertEqual(self.car.state, 'draft') - self.assertEqual(self.car.book_value, 60000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='draft'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - self.car.validate() - - self.assertEqual(self.car.state, 'open') - self.assertEqual(self.car.book_value, 36000) - self.assertRecordValues(self.car.depreciation_move_ids, [ - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=12000, remaining_value=48000, depreciated_value=12000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=12000, remaining_value=36000, depreciated_value=24000, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=12000, remaining_value=24000, depreciated_value=36000, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=12000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=12000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_assets_one_complete_period(self): - """Test the depreciation move value in case of having just one complete period (year or month) asset.""" - datas = [ - ('monthly', '2022-01-31', 'linear'), - ('monthly', '2022-01-31', 'degressive'), - ('monthly', '2022-01-31', 'degressive_then_linear'), - ('yearly', '2022-12-31', 'linear'), - ('yearly', '2022-12-31', 'degressive'), - ('yearly', '2022-12-31', 'degressive_then_linear'), - ] - for periodicity, end_depreciation_date, method in datas: - with self.subTest(period=periodicity, method=method, end_depreciation_date=end_depreciation_date): - asset = self.create_asset( - value=1000, - periodicity=periodicity, - periods=1, - method=method, - acquisition_date='2022-01-01', - prorata_date='2022-01-01', - prorata_computation_type='constant_periods', - account_depreciation_id=self.company_data['default_account_assets'].id, - ) - - asset.compute_depreciation_board() - self.assertEqual(asset.state, 'draft') - self.assertRecordValues(asset.depreciation_move_ids, [ - self._get_depreciation_move_values(date=end_depreciation_date, depreciation_value=1000.0, remaining_value=0.0, depreciated_value=1000.0, state='draft'), - ]) - asset.validate() - self.assertEqual(asset.state, 'open') diff --git a/addons/at_accounting/tests/test_change_lock_date_wizard.py b/addons/at_accounting/tests/test_change_lock_date_wizard.py deleted file mode 100644 index 6626f21..0000000 --- a/addons/at_accounting/tests/test_change_lock_date_wizard.py +++ /dev/null @@ -1,201 +0,0 @@ -from datetime import timedelta - -from odoo import fields -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo.addons.at_accounting.wizard.account_change_lock_date import SOFT_LOCK_DATE_FIELDS -from odoo.exceptions import UserError -from odoo.tests import tagged -from odoo.tools import frozendict - - -@tagged('post_install', '-at_install') -class TestChangeLockDateWizard(AccountTestInvoicingCommon): - - def test_exception_generation(self): - """ - Test the exception generation from the wizard. - Note that exceptions for 'everyone' and 'forever' are not tested here. - They do not create an exception (no 'account.lock_exception' object), but just change the lock date. - (See `test_everyone_forever_exception`.) - """ - self.env['account.lock_exception'].search([]).sudo().unlink() - - for lock_date_field in SOFT_LOCK_DATE_FIELDS: - with self.subTest(lock_date_field=lock_date_field), self.cr.savepoint() as sp: - # We can set the lock date if there is none. - self.env['account.change.lock.date'].create({lock_date_field: '2010-01-01'}).change_lock_date() - self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2010-01-01')) - - # We can increase the lock date if there is one. - self.env['account.change.lock.date'].create({lock_date_field: '2011-01-01'}).change_lock_date() - self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2011-01-01')) - - # We cannot remove the lock date; but we can create an exception - wizard = self.env['account.change.lock.date'].create({ - lock_date_field: False, - 'exception_applies_to': 'everyone', - 'exception_duration': '1h', - 'exception_reason': ':TestChangeLockDateWizard.test_exception_generation; remove', - }) - wizard.change_lock_date() - self.assertEqual(self.env['account.lock_exception'].search_count([]), 1) - exception = self.env['account.lock_exception'].search([]) - self.assertEqual(len(exception), 1) - self.assertRecordValues(exception, [{ - lock_date_field: False, - 'company_id': self.env.company.id, - 'user_id': False, - 'create_uid': self.env.user.id, - 'end_datetime': self.env.cr.now() + timedelta(hours=1), - 'reason': ':TestChangeLockDateWizard.test_exception_generation; remove', - }]) - exception.sudo().unlink() - - # Ensure we have not created any exceptions yet - self.assertEqual(self.env['account.lock_exception'].search_count([]), 0) - - # We cannot decrease the lock date; but we can create an exception - self.env['account.change.lock.date'].create({lock_date_field: '2009-01-01'}).change_lock_date() - self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2011-01-01')) - exception = self.env['account.lock_exception'].search([]) - self.assertEqual(len(exception), 1) - # Check lock date and default values on exception - self.assertRecordValues(exception, [{ - lock_date_field: fields.Date.from_string('2009-01-01'), - 'company_id': self.env.company.id, - 'user_id': self.env.user.id, - 'create_uid': self.env.user.id, - 'end_datetime': self.env.cr.now() + timedelta(minutes=5), - 'reason': False, - }]) - - sp.close() # Rollback to ensure all subtests start in the same situation - - def test_exception_generation_multiple(self): - """ - Test the exception generation from the wizard. - Here we test the case that we create multiple exceptions at once. - This should create an exception object for every changed lock date. - """ - self.env['account.lock_exception'].search([]).sudo().unlink() - - wizard = self.env['account.change.lock.date'].create({ - 'fiscalyear_lock_date': '2010-01-01', - 'tax_lock_date': '2010-01-01', - 'sale_lock_date': '2010-01-01', - 'purchase_lock_date': '2010-01-01', - }) - wizard.change_lock_date() - - self.assertRecordValues(self.env.company, [{ - 'fiscalyear_lock_date': fields.Date.from_string('2010-01-01'), - 'tax_lock_date': fields.Date.from_string('2010-01-01'), - 'sale_lock_date': fields.Date.from_string('2010-01-01'), - 'purchase_lock_date': fields.Date.from_string('2010-01-01'), - }]) - - wizard = self.env['account.change.lock.date'].create({ - 'fiscalyear_lock_date': '2009-01-01', - 'tax_lock_date': '2009-01-01', - 'sale_lock_date': '2009-01-01', - 'purchase_lock_date': '2009-01-01', - 'exception_applies_to': 'everyone', - 'exception_duration': '1h', - 'exception_reason': ':TestChangeLockDateWizard.test_exception_generation; remove', - }) - wizard.change_lock_date() - - exceptions = self.env['account.lock_exception'].search([]) - self.assertEqual(len(exceptions), 4) - expected_exceptions = { - frozendict({ - 'lock_date_field': 'fiscalyear_lock_date', - 'lock_date': fields.Date.from_string('2009-01-01'), - }), - frozendict({ - 'lock_date_field': 'tax_lock_date', - 'lock_date': fields.Date.from_string('2009-01-01'), - }), - frozendict({ - 'lock_date_field': 'sale_lock_date', - 'lock_date': fields.Date.from_string('2009-01-01'), - }), - frozendict({ - 'lock_date_field': 'purchase_lock_date', - 'lock_date': fields.Date.from_string('2009-01-01'), - }), - } - created_exceptions = { - frozendict({ - 'lock_date_field': exception.lock_date_field, - 'lock_date': exception.lock_date, - }) - for exception in exceptions - } - self.assertSetEqual(created_exceptions, expected_exceptions) - - def test_hard_lock_date(self): - self.env['account.lock_exception'].search([]).sudo().unlink() - - # We can set the hard lock date if there is none. - self.env['account.change.lock.date'].create({'hard_lock_date': '2010-01-01'}).change_lock_date() - self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2010-01-01')) - - # We can increase the hard lock date if there is one. - self.env['account.change.lock.date'].create({'hard_lock_date': '2011-01-01'}).change_lock_date() - self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2011-01-01')) - - # We cannot decrease the hard lock date; not even with an exception. - wizard = self.env['account.change.lock.date'].create({ - 'hard_lock_date': '2009-01-01', - 'exception_applies_to': 'everyone', - 'exception_duration': '1h', - 'exception_reason': ':TestChangeLockDateWizard.test_hard_lock_date', - }) - with self.assertRaises(UserError), self.env.cr.savepoint(): - wizard.change_lock_date() - self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2011-01-01')) - - # We cannot remove the hard lock date; not even with an exception. - wizard = self.env['account.change.lock.date'].create({ - 'hard_lock_date': False, - 'exception_applies_to': 'everyone', - 'exception_duration': '1h', - 'exception_reason': ':TestChangeLockDateWizard.test_hard_lock_date', - }) - with self.assertRaises(UserError), self.env.cr.savepoint(): - wizard.change_lock_date() - self.assertEqual(self.env.company.hard_lock_date, fields.Date.from_string('2011-01-01')) - - self.assertEqual(self.env['account.lock_exception'].search_count([]), 0) - - def test_everyone_forever_exception(self): - self.env['account.lock_exception'].search([]).sudo().unlink() - - for lock_date_field in SOFT_LOCK_DATE_FIELDS: - with self.subTest(lock_date_field=lock_date_field), self.cr.savepoint() as sp: - self.env['account.change.lock.date'].create({lock_date_field: '2010-01-01'}).change_lock_date() - self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2010-01-01')) - - # We can decrease the lock date with a 'forever' / 'everyone' exception. - self.env['account.change.lock.date'].create({ - lock_date_field: '2009-01-01', - 'exception_applies_to': 'everyone', - 'exception_duration': 'forever', - 'exception_reason': ':TestChangeLockDateWizard.test_everyone_forever_exception; remove', - }).change_lock_date() - self.assertEqual(self.env.company[lock_date_field], fields.Date.from_string('2009-01-01')) - - # We can remove the lock date with a 'forever' / 'everyone' exception. - self.env['account.change.lock.date'].create({ - lock_date_field: False, - 'exception_applies_to': 'everyone', - 'exception_duration': 'forever', - 'exception_reason': ':TestChangeLockDateWizard.test_everyone_forever_exception; remove', - }).change_lock_date() - self.assertEqual(self.env.company[lock_date_field], False) - - # Ensure we have not created any exceptions - self.assertEqual(self.env['account.lock_exception'].search_count([]), 0) - - sp.close() # Rollback to ensure all subtests start in the same situation diff --git a/addons/at_accounting/tests/test_deferred_management.py b/addons/at_accounting/tests/test_deferred_management.py deleted file mode 100644 index df0595f..0000000 --- a/addons/at_accounting/tests/test_deferred_management.py +++ /dev/null @@ -1,626 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=C0326 -import datetime - -from odoo import Command, fields -from odoo.tests import tagged -from odoo.addons.account.tests.common import AccountTestInvoicingCommon - -from freezegun import freeze_time - - -@tagged('post_install', '-at_install') -class TestDeferredManagement(AccountTestInvoicingCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.expense_accounts = [cls.env['account.account'].create({ - 'name': f'Expense {i}', - 'code': f'EXP{i}', - 'account_type': 'expense', - }) for i in range(3)] - cls.revenue_accounts = [cls.env['account.account'].create({ - 'name': f'Revenue {i}', - 'code': f'REV{i}', - 'account_type': 'income', - }) for i in range(3)] - - cls.company.deferred_expense_journal_id = cls.company_data['default_journal_misc'].id - cls.company.deferred_revenue_journal_id = cls.company_data['default_journal_misc'].id - cls.company.deferred_expense_account_id = cls.company_data['default_account_deferred_expense'].id - cls.company.deferred_revenue_account_id = cls.company_data['default_account_deferred_revenue'].id - - cls.expense_lines = [ - [cls.expense_accounts[0], 1000, '2023-01-01', '2023-04-30'], # 4 full months (=250/month) - [cls.expense_accounts[0], 1050, '2023-01-16', '2023-04-30'], # 3 full months + 15 days (=300/month) - [cls.expense_accounts[1], 1225, '2023-01-01', '2023-04-15'], # 3 full months + 15 days (=350/month) - [cls.expense_accounts[2], 1680, '2023-01-21', '2023-04-14'], # 2 full months + 10 days + 14 days (=600/month) - [cls.expense_accounts[2], 225, '2023-04-01', '2023-04-15'], # 15 days (=450/month) - ] - cls.revenue_lines = [ - [cls.revenue_accounts[0], 1000, '2023-01-01', '2023-04-30'], # 4 full months (=250/month) - [cls.revenue_accounts[0], 1050, '2023-01-16', '2023-04-30'], # 3 full months + 15 days (=300/month) - [cls.revenue_accounts[1], 1225, '2023-01-01', '2023-04-15'], # 3 full months + 15 days (=350/month) - [cls.revenue_accounts[2], 1680, '2023-01-21', '2023-04-14'], # 2 full months + 10 days + 14 days (=600/month) - [cls.revenue_accounts[2], 225, '2023-04-01', '2023-04-15'], # 15 days (=450/month) - ] - - def create_invoice(self, move_type, invoice_lines, date=None, post=True): - journal = self.company_data['default_journal_purchase'] if move_type == 'in_invoice' else self.company_data['default_journal_sale'] - move = self.env['account.move'].create({ - 'move_type': move_type, - 'partner_id': self.partner_a.id, - 'date': date or '2023-01-01', - 'invoice_date': date or '2023-01-01', - 'journal_id': journal.id, - 'invoice_line_ids': [ - Command.create({ - 'product_id': self.product_a.id, - 'quantity': 1, - 'account_id': account.id, - 'price_unit': price_unit, - 'deferred_start_date': start_date, - 'deferred_end_date': end_date, - }) for account, price_unit, start_date, end_date in invoice_lines - ] - }) - if post: - move.action_post() - return move - - def test_deferred_management_get_diff_dates(self): - def assert_get_diff_dates(start, end, expected): - diff = self.env['account.move']._get_deferred_diff_dates(fields.Date.to_date(start), fields.Date.to_date(end)) - self.assertAlmostEqual(diff, expected, 3) - - assert_get_diff_dates('2023-01-01', '2023-01-01', 0) - assert_get_diff_dates('2023-01-01', '2023-01-02', 1/30) - assert_get_diff_dates('2023-01-01', '2023-01-20', 19/30) - assert_get_diff_dates('2023-01-01', '2023-01-31', 29/30) - assert_get_diff_dates('2023-01-01', '2023-01-30', 29/30) - assert_get_diff_dates('2023-01-01', '2023-02-01', 1) - assert_get_diff_dates('2023-01-01', '2023-02-28', 1 + 29/30) - assert_get_diff_dates('2023-02-01', '2023-02-28', 29/30) - assert_get_diff_dates('2023-02-10', '2023-02-28', 20/30) - assert_get_diff_dates('2023-01-01', '2023-02-15', 1 + 14/30) - assert_get_diff_dates('2023-01-01', '2023-03-31', 2 + 29/30) - assert_get_diff_dates('2023-01-01', '2023-04-01', 3) - assert_get_diff_dates('2023-01-01', '2023-04-30', 3 + 29/30) - assert_get_diff_dates('2023-01-10', '2023-04-30', 3 + 20/30) - assert_get_diff_dates('2023-01-10', '2023-04-09', 2 + 29/30) - assert_get_diff_dates('2023-01-10', '2023-04-10', 3) - assert_get_diff_dates('2023-01-10', '2023-04-11', 3 + 1/30) - assert_get_diff_dates('2023-02-20', '2023-04-10', 1 + 20/30) - assert_get_diff_dates('2023-01-31', '2023-04-30', 3) - assert_get_diff_dates('2023-02-28', '2023-04-10', 1 + 10/30) - assert_get_diff_dates('2023-03-01', '2023-04-10', 1 + 9/30) - assert_get_diff_dates('2023-04-10', '2023-03-01', 1 + 9/30) - assert_get_diff_dates('2023-01-01', '2023-12-31', 11 + 29/30) - assert_get_diff_dates('2023-01-01', '2024-01-01', 12) - assert_get_diff_dates('2023-01-01', '2024-07-01', 18) - assert_get_diff_dates('2023-01-01', '2024-07-10', 18 + 9/30) - - def test_get_ends_of_month(self): - def assertEndsOfMonths(start_date, end_date, expected): - self.assertEqual( - self.env['account.move.line']._get_deferred_ends_of_month( - fields.Date.to_date(start_date), - fields.Date.to_date(end_date) - ), - [fields.Date.to_date(date) for date in expected] - ) - - assertEndsOfMonths('2023-01-01', '2023-01-01', ['2023-01-31']) - assertEndsOfMonths('2023-01-01', '2023-01-02', ['2023-01-31']) - assertEndsOfMonths('2023-01-01', '2023-01-20', ['2023-01-31']) - assertEndsOfMonths('2023-01-01', '2023-01-30', ['2023-01-31']) - assertEndsOfMonths('2023-01-01', '2023-01-31', ['2023-01-31']) - assertEndsOfMonths('2023-01-01', '2023-02-01', ['2023-01-31', '2023-02-28']) - assertEndsOfMonths('2023-01-01', '2023-02-28', ['2023-01-31', '2023-02-28']) - assertEndsOfMonths('2023-02-01', '2023-02-28', ['2023-02-28']) - assertEndsOfMonths('2023-02-10', '2023-02-28', ['2023-02-28']) - assertEndsOfMonths('2023-01-01', '2023-02-15', ['2023-01-31', '2023-02-28']) - assertEndsOfMonths('2023-01-01', '2023-03-31', ['2023-01-31', '2023-02-28', '2023-03-31']) - assertEndsOfMonths('2023-01-01', '2023-04-01', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30']) - assertEndsOfMonths('2023-01-01', '2023-04-30', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30']) - assertEndsOfMonths('2023-01-10', '2023-04-30', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30']) - assertEndsOfMonths('2023-01-10', '2023-04-09', ['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30']) - - def test_deferred_abnormal_dates(self): - """ - Test that we correctly detect abnormal dates. - In the deferred computations, we always assume that both the start and end date are inclusive - E.g: 1st January -> 31st December is *exactly* 1 year = 12 months - However, the user may instead put 1st January -> 1st January of next year which is then - 12 months + 1/30 month = 12.03 months which may result in odd amounts when deferrals are created. - This is what we call abnormal dates. - Other cases were the number of months is not round should not be handled and are not considered abnormal. - """ - move = self.create_invoice('in_invoice', [ - [self.expense_accounts[0], 0, '2023-01-01', '2023-12-30'], - [self.expense_accounts[0], 1, '2023-01-01', '2023-12-31'], - [self.expense_accounts[0], 2, '2023-01-01', '2024-01-01'], - [self.expense_accounts[0], 3, '2023-01-01', '2024-01-02'], - [self.expense_accounts[0], 4, '2023-01-01', '2024-01-31'], - [self.expense_accounts[0], 5, '2023-01-01', '2024-02-01'], - [self.expense_accounts[0], 6, '2023-01-02', '2024-02-01'], - [self.expense_accounts[0], 7, '2023-01-02', '2024-02-02'], - [self.expense_accounts[0], 8, '2023-01-31', '2024-01-30'], - [self.expense_accounts[0], 9, '2023-01-31', '2024-02-28'], # 29 days in Feb 2024 - # Following one is abnormal because we have a full months in February (= 30 accounting days) + 1 day in January - [self.expense_accounts[0], 10, '2023-01-31', '2024-02-29'], - [self.expense_accounts[0], 11, '2023-02-01', '2024-02-29'], - ], post=True) - lines = move.invoice_line_ids.sorted('price_unit') - self.assertFalse(lines[0].has_abnormal_deferred_dates) - self.assertFalse(lines[1].has_abnormal_deferred_dates) - self.assertTrue(lines[2].has_abnormal_deferred_dates) - self.assertFalse(lines[3].has_abnormal_deferred_dates) - self.assertFalse(lines[4].has_abnormal_deferred_dates) - self.assertTrue(lines[5].has_abnormal_deferred_dates) - self.assertFalse(lines[6].has_abnormal_deferred_dates) - self.assertTrue(lines[7].has_abnormal_deferred_dates) - self.assertFalse(lines[8].has_abnormal_deferred_dates) - self.assertFalse(lines[9].has_abnormal_deferred_dates) - self.assertTrue(lines[10].has_abnormal_deferred_dates) - self.assertFalse(lines[11].has_abnormal_deferred_dates) - - def test_deferred_expense_generate_entries_method(self): - # The deferred entries are NOT generated when the invoice is validated if the method is set to 'manual'. - self.company.generate_deferred_expense_entries_method = 'manual' - move2 = self.create_invoice('in_invoice', [self.expense_lines[0]], post=True) - self.assertEqual(len(move2.deferred_move_ids), 0) - - # Test that the deferred entries are generated when the invoice is validated. - self.company.generate_deferred_expense_entries_method = 'on_validation' - move = self.create_invoice('in_invoice', [self.expense_lines[0]], post=True) - self.assertEqual(len(move.deferred_move_ids), 5) # 1 for the invoice deferred + 4 for the deferred entries - # See test_deferred_expense_credit_note for the values - - def test_deferred_expense_reset_to_draft(self): - """ - Test that the deferred entries are deleted/reverted when the invoice is reset to draft. - """ - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 1680, '2023-01-21', '2023-04-14')], date='2023-03-15') - self.assertEqual(len(move.deferred_move_ids), 5) - move.button_draft() - self.assertFalse(move.deferred_move_ids) - - # With a lock date, we should reverse the moves that cannot be deleted - move.action_post() # Post the move to create the deferred entries with 'on_validation' method - self.assertEqual(len(move.deferred_move_ids), 5) - move.company_id.fiscalyear_lock_date = fields.Date.to_date('2023-02-15') - move.button_draft() - # January deferred entry is in lock period, so it is reversed, not deleted, thus we have one deferred entry and its revert - self.assertEqual(len(move.deferred_move_ids), 2) - self.assertEqual(move.deferred_move_ids[0].date, fields.Date.to_date('2023-02-28')) - self.assertEqual(move.deferred_move_ids[1].date, fields.Date.to_date('2023-01-31')) - - # If we repost the move, it should be allowed - move.action_post() - self.assertEqual(len(move.deferred_move_ids), 2 + 5) - - def assert_invoice_lines(self, move, expected_values, source_account, deferred_account): - deferred_moves = move.deferred_move_ids.sorted('date') - for deferred_move, expected_value in zip(deferred_moves, expected_values): - expected_date, expense_line_debit, expense_line_credit, deferred_line_debit, deferred_line_credit = expected_value - self.assertRecordValues(deferred_move, [{ - 'state': 'posted', - 'move_type': 'entry', - 'partner_id': self.partner_a.id, - 'date': fields.Date.to_date(expected_date), - }]) - expense_line = deferred_move.line_ids.filtered(lambda line: line.account_id == source_account) - self.assertRecordValues(expense_line, [ - {'debit': expense_line_debit, 'credit': expense_line_credit, 'partner_id': self.partner_a.id}, - ]) - deferred_line = deferred_move.line_ids.filtered(lambda line: line.account_id == deferred_account) - self.assertEqual(deferred_line.debit, deferred_line_debit) - self.assertEqual(deferred_line.credit, deferred_line_credit) - - def test_default_tax_on_account_not_on_deferred_entries(self): - """ - Test that the default taxes on an account are not calculated on deferral entries, since this would impact the - tax report. - """ - revenue_account_with_taxes = self.env['account.account'].create({ - 'name': 'Revenue with Taxes', - 'code': 'REVWTAXES', - 'account_type': 'income', - 'tax_ids': [Command.set(self.tax_sale_a.ids)] - }) - - move = self.create_invoice( - 'out_invoice', - [[revenue_account_with_taxes, 1000, '2023-01-01', '2023-04-30']], - date='2022-12-10' - ) - - expected_line_values = [ - # Date [Line expense] [Line deferred] - ('2022-12-10', 1000, 0, 0, 1000), - ('2023-01-31', 0, 250, 250, 0), - ('2023-02-28', 0, 250, 250, 0), - ('2023-03-31', 0, 250, 250, 0), - ] - - self.assert_invoice_lines( - move, - expected_line_values, - revenue_account_with_taxes, - self.company_data['default_account_deferred_revenue'] - ) - - for deferred_move in move.deferred_move_ids: - # There are no extra lines besides the two lines we checked before - self.assertEqual(len(deferred_move.line_ids), 2) - - - def test_deferred_values(self): - """ - Test that the debit/credit values are correctly computed, even after a credit note is issued. - """ - - expected_line_values1 = [ - # Date [Line expense] [Line deferred] - ('2022-12-10', 0, 1000, 1000, 0), - ('2023-01-31', 250, 0, 0, 250), - ('2023-02-28', 250, 0, 0, 250), - ('2023-03-31', 250, 0, 0, 250), - ] - expected_line_values2 = [ - # Date [Line expense] [Line deferred] - ('2022-12-10', 1000, 0, 0, 1000), - ('2023-01-31', 0, 250, 250, 0), - ('2023-02-28', 0, 250, 250, 0), - ('2023-03-31', 0, 250, 250, 0), - ] - - # Vendor bill and credit note - move = self.create_invoice('in_invoice', [self.expense_lines[0]], post=True, date='2022-12-10') - self.assert_invoice_lines(move, expected_line_values1, self.expense_accounts[0], self.company_data['default_account_deferred_expense']) - reverse_move = move._reverse_moves() - self.assert_invoice_lines(reverse_move, expected_line_values2, self.expense_accounts[0], self.company_data['default_account_deferred_expense']) - - # Customer invoice and credit note - move2 = self.create_invoice('out_invoice', [self.revenue_lines[0]], post=True, date='2022-12-10') - self.assert_invoice_lines(move2, expected_line_values2, self.revenue_accounts[0], self.company_data['default_account_deferred_revenue']) - reverse_move2 = move2._reverse_moves() - self.assert_invoice_lines(reverse_move2, expected_line_values1, self.revenue_accounts[0], self.company_data['default_account_deferred_revenue']) - - def test_deferred_values_rounding(self): - """ - Test that the debit/credit values are correctly computed when values are rounded - """ - - # Vendor Bill - expense_line = [self.expense_accounts[0], 500, '2020-08-07', '2020-12-07'] - expected_line_values = [ - # Date [Line expense] [Line deferred] - ('2020-08-07', 0, 500, 500, 0), - ('2020-08-31', 99.17, 0, 0, 99.17), - ('2020-09-30', 123.97, 0, 0, 123.97), - ('2020-10-31', 123.97, 0, 0, 123.97), - ('2020-11-30', 123.97, 0, 0, 123.97), - ('2020-12-07', 28.92, 0, 0, 28.92), - ] - self.assertEqual(self.company.currency_id.round(sum(x[1] for x in expected_line_values)), 500) - move = self.create_invoice('in_invoice', [expense_line], date='2020-08-07') - self.assert_invoice_lines(move, expected_line_values, self.expense_accounts[0], self.company_data['default_account_deferred_expense']) - - # Customer invoice - revenue_line = [self.revenue_accounts[0], 500, '2020-08-07', '2020-12-07'] - expected_line_values = [ - # Date [Line expense] [Line deferred] - ('2020-08-07', 500, 0, 0, 500), - ('2020-08-31', 0, 99.17, 99.17, 0), - ('2020-09-30', 0, 123.97, 123.97, 0), - ('2020-10-31', 0, 123.97, 123.97, 0), - ('2020-11-30', 0, 123.97, 123.97, 0), - ('2020-12-07', 0, 28.92, 28.92, 0), - ] - self.assertEqual(self.company.currency_id.round(sum(x[2] for x in expected_line_values)), 500) - move = self.create_invoice('out_invoice', [revenue_line], post=True, date='2020-08-07') - self.assert_invoice_lines(move, expected_line_values, self.revenue_accounts[0], self.company_data['default_account_deferred_revenue']) - - def test_deferred_expense_avoid_useless_deferred_entries(self): - """ - If we have an invoice with a start date in the beginning of the month, and an end date in the end of the month, - we should not create the deferred entries because the original invoice will be totally deferred - on the last day of the month, but the full amount will be accounted for on the same day too, thus - cancelling each other. Therefore we should not create the deferred entries. - """ - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 1680, '2023-01-01', '2023-01-31')], date='2023-01-01') - self.assertEqual(len(move.deferred_move_ids), 0) - - def test_deferred_expense_single_period_entries(self): - """ - If we have an invoice covering only one period, we should only avoid creating deferral entries when the - accounting date is the same as the period for the deferral. Otherwise we should still generate a deferral entry. - """ - self.company.deferred_expense_amount_computation_method = 'month' - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 1680, '2023-02-01', '2023-02-28')]) - self.assertRecordValues(move.deferred_move_ids, [ - {'date': fields.Date.to_date('2023-01-01')}, - {'date': fields.Date.to_date('2023-02-28')}, - ]) - - def test_taxes_deferred_after_date_added(self): - """ - Test that applicable taxes get deferred also when the dates of the base line are filled in after a first save. - """ - - expected_line_values = [ - # Date [Line expense] [Line deferred] - ('2022-12-10', 0, 1000, 1000, 0), - ('2022-12-10', 0, 100, 100, 0), - ('2023-01-31', 250, 0, 0, 250), - ('2023-01-31', 25, 0, 0, 25), - ('2023-02-28', 250, 0, 0, 250), - ('2023-02-28', 25, 0, 0, 25), - ('2023-03-31', 250, 0, 0, 250), - ('2023-03-31', 25, 0, 0, 25), - ] - - partially_deductible_tax = self.env['account.tax'].create({ - 'name': 'Partially deductible Tax', - 'amount': 20, - 'amount_type': 'percent', - 'type_tax_use': 'purchase', - 'invoice_repartition_line_ids': [ - Command.create({'repartition_type': 'base'}), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': False - }), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'account_id': self.company_data['default_account_tax_purchase'].id, - 'use_in_tax_closing': True - }), - ], - 'refund_repartition_line_ids': [ - Command.create({'repartition_type': 'base'}), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'use_in_tax_closing': False - }), - Command.create({ - 'factor_percent': 50, - 'repartition_type': 'tax', - 'account_id': self.company_data['default_account_tax_purchase'].id, - 'use_in_tax_closing': True - }), - ], - }) - - move = self.env['account.move'].create({ - 'move_type': 'in_invoice', - 'partner_id': self.partner_a.id, - 'date': '2022-12-10', - 'invoice_date': '2022-12-10', - 'journal_id': self.company_data['default_journal_purchase'].id, - 'invoice_line_ids': [ - Command.create({ - 'quantity': 1, - 'account_id': self.expense_lines[0][0].id, - 'price_unit': self.expense_lines[0][1], - 'tax_ids': [Command.set(partially_deductible_tax.ids)], - }) - ] - }) - - move.invoice_line_ids.write({ - 'deferred_start_date': self.expense_lines[0][2], - 'deferred_end_date': self.expense_lines[0][3], - }) - - move.action_post() - - self.assert_invoice_lines(move, expected_line_values, self.expense_accounts[0], self.company_data['default_account_deferred_expense']) - - def test_deferred_tax_key(self): - """ - Test that the deferred tax key is correctly computed. - and is the same between _compute_tax_key and _compute_all_tax - """ - lines = [ - [self.expense_accounts[0], 1000, '2023-01-01', '2023-04-30'], - [self.expense_accounts[0], 1000, False, False], - ] - move = self.create_invoice('in_invoice', lines, post=True) - original_amount_total = move.amount_total - self.assertEqual(len(move.line_ids.filtered(lambda l: l.display_type == 'tax')), 1) - move.button_draft() - move.action_post() - # The number of tax lines shouldn't change, nor the total amount - self.assertEqual(len(move.line_ids.filtered(lambda l: l.display_type == 'tax')), 1) - self.assertEqual(move.amount_total, original_amount_total) - - def test_compute_empty_start_date(self): - """ - Test that the deferred start date is computed when empty and posting the move. - """ - lines = [[self.expense_accounts[0], 1000, False, '2023-04-30']] - move = self.create_invoice('in_invoice', lines, post=False) - - # We don't have a deferred date in the beginning - self.assertFalse(move.line_ids[0].deferred_start_date) - - move.action_post() - # Deferred start date is set after post - self.assertEqual(move.line_ids[0].deferred_start_date, datetime.date(2023, 1, 1)) - - move.button_draft() - move.line_ids[0].deferred_start_date = False - move.invoice_date = '2023-02-01' - # Start date is set when changing invoice date - self.assertEqual(move.line_ids[0].deferred_start_date, datetime.date(2023, 2, 1)) - - move.line_ids[0].deferred_start_date = False - move.line_ids[0].deferred_end_date = '2023-05-31' - # Start date is set when changing deferred end date - self.assertEqual(move.line_ids[0].deferred_start_date, datetime.date(2023, 2, 1)) - - def test_deferred_on_accounting_date(self): - """ - When we are in `on_validation` mode, the deferral of the total amount should happen on the - accounting date of the move. - """ - move = self.create_invoice( - 'in_invoice', - [(self.expense_accounts[0], 1680, '2023-01-01', '2023-02-28')], - date='2023-01-10', - post=False - ) - move.date = '2023-01-15' - move.action_post() - self.assertRecordValues(move.deferred_move_ids, [ - {'date': fields.Date.to_date('2023-01-15')}, - {'date': fields.Date.to_date('2023-01-31')}, - {'date': fields.Date.to_date('2023-02-28')}, - ]) - - def test_deferred_entries_not_created_on_future_invoice(self): - """Test that we don't create deferred entries on a future posted invoice""" - tomorrow = fields.Date.to_date(fields.Date.today()) + datetime.timedelta(days=1) - move = self.create_invoice( - 'out_invoice', - [(self.expense_accounts[0], 1680, tomorrow, tomorrow + datetime.timedelta(days=100))], - date=tomorrow, - post=False - ) - move.auto_post = "at_date" - move._post() - self.assertFalse(move.deferred_move_ids) - - with freeze_time(tomorrow): - self.env.ref('account.ir_cron_auto_post_draft_entry').method_direct_trigger() - self.assertEqual(move.state, 'posted') - self.assertTrue(move.deferred_move_ids) - - def test_deferred_entries_created_on_auto_post_invoice(self): - """Test that deferred entries are created on an invoice with auto_post set to 'at_date'""" - yesterday = fields.Date.to_date(fields.Date.today()) - datetime.timedelta(days=1) - move = self.create_invoice( - 'out_invoice', - [(self.expense_accounts[0], 1680, yesterday, yesterday + datetime.timedelta(days=45))], - date=yesterday, - post=False - ) - move.auto_post = "at_date" - move._post() - self.assertEqual(move.state, 'posted') - self.assertTrue(move.deferred_move_ids) - - def test_deferred_compute_method_full_months(self): - """ - Test that the deferred amount is correctly computed when the new full_months method computation is used - """ - self.company.deferred_expense_amount_computation_method = 'full_months' - - dates = (('2024-06-05', '2025-06-04'), ('2024-06-30', '2025-06-29')) - for (date_from, date_to) in dates: - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, date_from, date_to)], date='2024-06-05') - self.assertRecordValues(move.deferred_move_ids.sorted('date'), [ - {'date': fields.Date.to_date('2024-06-05'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-06-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-07-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-08-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-09-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-10-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-11-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-12-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-01-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-02-28'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-03-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-04-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-05-31'), 'amount_total': 1000}, - # 0 for June 2025, so no move created - ]) - - # Start of month <=> Equal per month method - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-07-01', '2025-06-30')], date='2024-07-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-07-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-07-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-08-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-09-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-10-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-11-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2024-12-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-01-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-02-28'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-03-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-04-30'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-05-31'), 'amount_total': 1000}, - {'date': fields.Date.to_date('2025-06-30'), 'amount_total': 1000}, - ]) - - # Nothing to defer, everything is in the same month - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-01', '2024-01-16')], date='2024-01-01') - self.assertFalse(move.deferred_move_ids) - - # Round period of 2 months -> Divide by 2 - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-01', '2024-02-29')], date='2024-01-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-01-31'), 'amount_total': 6000}, - {'date': fields.Date.to_date('2024-02-29'), 'amount_total': 6000}, - ]) - - # Round period of 2 months -> Divide by 2 - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-15', '2024-03-14')], date='2024-01-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-01-31'), 'amount_total': 6000}, - {'date': fields.Date.to_date('2024-02-29'), 'amount_total': 6000}, - ]) - - # Period of exactly one month: full amount should be in Jan. So we revert 1st Jan, and account for 31st Jan <=> don't generate anything - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-15', '2024-02-14')], date='2024-01-01') - self.assertFalse(move.deferred_move_ids) - - # Not-round period of 1.5 month with only one end of month in January (same explanation as above) - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-01', '2024-02-15')], date='2024-01-01') - self.assertFalse(move.deferred_move_ids) - - # Not-round period of 1.5+ month with only one end of month in January (same explanation as above) - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-05', '2024-02-15')], date='2024-01-01') - self.assertFalse(move.deferred_move_ids) - - # Period of exactly one month: full amount should be in Feb. So we revert 1st Jan, and account for all on 29th Feb. - # Deferrals are in different months for this case, so we should the deferrals should be generated. - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-02-15', '2024-03-14')], date='2024-01-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-02-29'), 'amount_total': 12000}, - ]) - - # Not-round period of 1.5+ month: full amount should be in Feb. So we revert 1st Jan, and account for all on 29th Feb. - # Deferrals are in different months for this case, so we should the deferrals should be generated. - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-02-05', '2024-03-15')], date='2024-01-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-02-29'), 'amount_total': 12000}, - ]) - - # Not-round period of 1.5 month with 2 ends of months, so divide balance by 2 - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-16', '2024-02-29')], date='2024-01-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-01-31'), 'amount_total': 6000}, - {'date': fields.Date.to_date('2024-02-29'), 'amount_total': 6000}, - ]) - - # Not-round period of 2.5 month, with 3 ends of months, so divide balance by 3 - move = self.create_invoice('in_invoice', [(self.expense_accounts[0], 12000, '2024-01-16', '2024-03-31')], date='2024-01-01') - self.assertRecordValues(move.deferred_move_ids.sorted(lambda m: (m.date, m.amount_total)), [ - {'date': fields.Date.to_date('2024-01-01'), 'amount_total': 12000}, - {'date': fields.Date.to_date('2024-01-31'), 'amount_total': 4000}, - {'date': fields.Date.to_date('2024-02-29'), 'amount_total': 4000}, - {'date': fields.Date.to_date('2024-03-31'), 'amount_total': 4000}, - ]) diff --git a/addons/at_accounting/tests/test_financial_report.py b/addons/at_accounting/tests/test_financial_report.py deleted file mode 100644 index f1e8acb..0000000 --- a/addons/at_accounting/tests/test_financial_report.py +++ /dev/null @@ -1,919 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=C0326 - -from .common import TestAccountReportsCommon - -from odoo import fields, Command -from odoo.tests import tagged - -from freezegun import freeze_time - - -@tagged('post_install', '-at_install') -class TestFinancialReport(TestAccountReportsCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - - # ==== Partners ==== - cls.partner_c = cls._create_partner(name='partner_c') - - # ==== Accounts ==== - - # Cleanup existing "Current year earnings" accounts since we can only have one by company. - cls.env['account.account'].search([ - ('company_ids', 'in', (cls.company_data['company'] + cls.company_data_2['company']).ids), - ('account_type', '=', 'equity_unaffected'), - ]).unlink() - - account_type_data = [ - ('asset_receivable', {'reconcile': True}), - ('liability_payable', {'reconcile': True}), - ('asset_cash', {}), - ('asset_current', {}), - ('asset_prepayments', {}), - ('asset_fixed', {}), - ('asset_non_current', {}), - ('equity', {}), - ('equity_unaffected', {}), - ('income', {}), - ] - - accounts = cls.env['account.account'].create([{ - **data[1], - 'name': 'account%s' % i, - 'code': 'code%s' % i, - 'account_type': data[0], - } for i, data in enumerate(account_type_data)]) - - accounts_2 = cls.env['account.account'].create([{ - **data[1], - 'name': 'account%s' % (i + 100), - 'code': 'code%s' % (i + 100), - 'account_type': data[0], - 'company_ids': [Command.link(cls.company_data_2['company'].id)] - } for i, data in enumerate(account_type_data)]) - - for account in accounts_2: - account.code = account.with_company(cls.company_data_2['company']).code - - # ==== Custom filters ==== - - cls.horizontal_group = cls.env['account.report.horizontal.group'].create({ - 'name': 'Horizontal Group', - 'rule_ids': [ - Command.create({ - 'field_name': 'partner_id', - 'domain': f"[('id', 'in', {(cls.partner_a + cls.partner_b).ids})]", - }), - Command.create({ - 'field_name': 'account_id', - 'domain': f"[('id', 'in', {accounts[:2].ids})]", - }), - ], - }) - - # ==== Journal entries ==== - - cls.move_2019 = cls.env['account.move'].create({ - 'move_type': 'entry', - 'date': fields.Date.from_string('2019-01-01'), - 'line_ids': [ - (0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_b.id}), - (0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_c.id}), - (0, 0, {'debit': 25.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 200.0, 'credit': 0.0, 'account_id': accounts[1].id, 'partner_id': cls.partner_b.id}), - (0, 0, {'debit': 0.0, 'credit': 300.0, 'account_id': accounts[2].id, 'partner_id': cls.partner_c.id}), - (0, 0, {'debit': 400.0, 'credit': 0.0, 'account_id': accounts[3].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 0.0, 'credit': 1100.0, 'account_id': accounts[4].id, 'partner_id': cls.partner_b.id}), - (0, 0, {'debit': 700.0, 'credit': 0.0, 'account_id': accounts[6].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 0.0, 'credit': 800.0, 'account_id': accounts[7].id, 'partner_id': cls.partner_b.id}), - (0, 0, {'debit': 800.0, 'credit': 0.0, 'account_id': accounts[8].id, 'partner_id': cls.partner_c.id}), - ], - }) - cls.move_2019.action_post() - - cls.move_2018 = cls.env['account.move'].create({ - 'move_type': 'entry', - 'date': fields.Date.from_string('2018-01-01'), - 'line_ids': [ - (0, 0, {'debit': 1000.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 0.0, 'credit': 1000.0, 'account_id': accounts[2].id, 'partner_id': cls.partner_b.id}), - (0, 0, {'debit': 250.0, 'credit': 0.0, 'account_id': accounts[0].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 0.0, 'credit': 250.0, 'account_id': accounts[9].id, 'partner_id': cls.partner_a.id}), - ], - }) - cls.move_2018.action_post() - - cls.move_2017 = cls.env['account.move'].with_company(cls.company_data_2['company']).create({ - 'move_type': 'entry', - 'date': fields.Date.from_string('2017-01-01'), - 'line_ids': [ - (0, 0, {'debit': 2000.0, 'credit': 0.0, 'account_id': accounts_2[0].id, 'partner_id': cls.partner_a.id}), - (0, 0, {'debit': 0.0, 'credit': 4000.0, 'account_id': accounts_2[2].id, 'partner_id': cls.partner_b.id}), - (0, 0, {'debit': 0.0, 'credit': 5000.0, 'account_id': accounts_2[4].id, 'partner_id': cls.partner_c.id}), - (0, 0, {'debit': 7000.0, 'credit': 0.0, 'account_id': accounts_2[6].id, 'partner_id': cls.partner_a.id}), - ], - }) - cls.move_2017.action_post() - - cls.report = cls.env.ref('at_accounting.balance_sheet') - - cls.report_no_parent_id = cls.env["account.report"].create({ - 'name': "Test report", - - 'column_ids': [ - Command.create({ - 'name': 'Balance', - 'expression_label': 'balance', - 'sequence': 1 - }) - ], - - 'line_ids': [ - Command.create({ - 'name': "Invisible Partner A line", - 'code': "INVA", - 'sequence': 1, - 'hierarchy_level': 0, - 'groupby': "account_id", - 'foldable': True, - 'expression_ids': [Command.clear(), Command.create({ - 'label': 'balance', - 'engine': 'domain', - 'formula': [("partner_id", "=", cls.partner_a.id)], - 'subformula': 'sum', - })], - }), - Command.create({ - 'name': "Invisible Partner B line", - 'code': "INVB", - 'sequence': 2, - 'hierarchy_level': 0, - 'groupby': "account_id", - 'foldable': True, - 'expression_ids': [Command.clear(), Command.create({ - 'label': 'balance', - 'engine': 'domain', - 'formula': [("partner_id", "=", cls.partner_b.id)], - 'subformula': 'sum', - })], - }), - Command.create({ - 'name': "Total of Invisible lines", - 'code': "INVT", - 'sequence': 3, - 'hierarchy_level': 0, - 'expression_ids': [Command.clear(), Command.create({ - 'label': 'balance', - 'engine': 'aggregation', - 'formula': 'INVA.balance + INVB.balance', - })], - }), - ], - }) - - def _build_generic_id_from_financial_line(self, financial_rep_ln_xmlid): - report_line = self.env.ref(financial_rep_ln_xmlid) - return '-account.financial.html.report.line-%s' % report_line.id - - def _get_line_id_from_generic_id(self, generic_id): - return int(generic_id.split('-')[-1]) - - def test_financial_report_strict_range_on_report_lines_with_no_parent_id(self): - """ Tests that lines with no parent can be correctly filtered by date range """ - self.report_no_parent_id.filter_multi_company = 'disabled' - options = self._generate_options(self.report_no_parent_id, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31')) - - lines = self.report_no_parent_id._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - [ - ('Invisible Partner A line', 1150.0), - ('Invisible Partner B line', -1675.0), - ('Total of Invisible lines', -525.0), - - ], - options, - ) - - def test_financial_report_strict_empty_range_on_report_lines_with_no_parent_id(self): - """ Tests that lines with no parent can be correctly filtered by date range with no invoices""" - self.report_no_parent_id.filter_multi_company = 'disabled' - options = self._generate_options(self.report_no_parent_id, fields.Date.from_string('2019-03-01'), fields.Date.from_string('2019-03-31')) - - lines = self.report_no_parent_id._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - [ - ('Invisible Partner A line', 0.0), - ('Invisible Partner B line', 0.0), - ('Total of Invisible lines', 0.0), - ], - options, - ) - - @freeze_time("2016-06-06") - def test_balance_sheet_today_current_year_earnings(self): - invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'date': '2016-02-02', - 'invoice_line_ids': [Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 110, - 'tax_ids': [], - })] - }) - invoice.action_post() - - self.report.filter_multi_company = 'disabled' - options = self._generate_options(self.report, fields.Date.from_string('2016-06-01'), fields.Date.from_string('2016-06-06')) - options['date']['filter'] = 'today' - - lines = self.report._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - [ - ('ASSETS', 110.0), - ('Current Assets', 110.0), - ('Bank and Cash Accounts', 0.0), - ('Receivables', 110.0), - ('Current Assets', 0.0), - ('Prepayments', 0.0), - ('Total Current Assets', 110.0), - ('Plus Fixed Assets', 0.0), - ('Plus Non-current Assets', 0.0), - ('Total ASSETS', 110.0), - - ('LIABILITIES', 0.0), - ('Current Liabilities', 0.0), - ('Current Liabilities', 0.0), - ('Payables', 0.0), - ('Total Current Liabilities', 0.0), - ('Plus Non-current Liabilities', 0.0), - ('Total LIABILITIES', 0.0), - - ('EQUITY', 110.0), - ('Unallocated Earnings', 110.0), - ('Current Year Unallocated Earnings', 110.0), - ('Previous Years Unallocated Earnings', 0.0), - ('Total Unallocated Earnings', 110.0), - ('Retained Earnings', 0.0), - ('Current Year Retained Earnings', 0.0), - ('Previous Years Retained Earnings', 0.0), - ('Total Retained Earnings', 0.0), - ('Total EQUITY', 110.0), - - ('LIABILITIES + EQUITY', 110.0), - ], - options, - ) - - @freeze_time("2016-05-05") - def test_balance_sheet_last_month_vs_custom_current_year_earnings(self): - """ - Checks the balance sheet calls the right period of the P&L when using last_month date filter, or an equivalent custom filter - (this used to fail due to options regeneration made by the P&L's get_options())" - """ - to_invoice = [('15', '11'), ('15', '12'), ('16', '01'), ('16', '02'), ('16', '03'), ('16', '04')] - for year, month in to_invoice: - invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'invoice_date': f'20{year}-{month}-01', - 'invoice_line_ids': [Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 1000, - 'tax_ids': [], - })] - }) - invoice.action_post() - expected_result =[ - ('ASSETS', 6000.0), - ('Current Assets', 6000.0), - ('Bank and Cash Accounts', 0.0), - ('Receivables', 6000.0), - ('Current Assets', 0.0), - ('Prepayments', 0.0), - ('Total Current Assets', 6000.0), - ('Plus Fixed Assets', 0.0), - ('Plus Non-current Assets', 0.0), - ('Total ASSETS', 6000.0), - - ('LIABILITIES', 0.0), - ('Current Liabilities', 0.0), - ('Current Liabilities', 0.0), - ('Payables', 0.0), - ('Total Current Liabilities', 0.0), - ('Plus Non-current Liabilities', 0.0), - ('Total LIABILITIES', 0.0), - - ('EQUITY', 6000.0), - ('Unallocated Earnings', 6000.0), - ('Current Year Unallocated Earnings', 4000.0), - ('Previous Years Unallocated Earnings', 2000.0), - ('Total Unallocated Earnings', 6000.0), - ('Retained Earnings', 0.0), - ('Current Year Retained Earnings', 0.0), - ('Previous Years Retained Earnings', 0.0), - ('Total Retained Earnings', 0.0), - ('Total EQUITY', 6000.0), - ('LIABILITIES + EQUITY', 6000.0), - - ] - self.report.filter_multi_company = 'disabled' - options = self._generate_options(self.report, fields.Date.from_string('2016-05-05'), fields.Date.from_string('2016-05-05')) - - # End of Last Month - options['date']['filter'] = 'last_month' - lines = self.report._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - expected_result, - options, - ) - # Custom - options['date']['filter'] = 'custom' - lines = self.report._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - expected_result, - options, - ) - - def test_financial_report_single_company(self): - line_id = self._get_basic_line_dict_id_from_report_line_ref('at_accounting.account_financial_report_bank_view0') - self.report.filter_multi_company = 'disabled' - options = self._generate_options(self.report, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31')) - options['unfolded_lines'] = [line_id] - - lines = self.report._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - [ - ('ASSETS', 50.0), - ('Current Assets', -650.0), - ('Bank and Cash Accounts', -1300.0), - ('code2 account2', -1300.0), - ('Total Bank and Cash Accounts', -1300.0), - ('Receivables', 1350.0), - ('Current Assets', 400.0), - ('Prepayments', -1100.0), - ('Total Current Assets', -650.0), - ('Plus Fixed Assets', 0.0), - ('Plus Non-current Assets', 700.0), - ('Total ASSETS', 50.0), - - ('LIABILITIES', -200.0), - ('Current Liabilities', -200.0), - ('Current Liabilities', 0.0), - ('Payables', -200.0), - ('Total Current Liabilities', -200.0), - ('Plus Non-current Liabilities', 0.0), - ('Total LIABILITIES', -200.0), - - ('EQUITY', 250.0), - ('Unallocated Earnings', -550.0), - ('Current Year Unallocated Earnings', -800.0), - ('Previous Years Unallocated Earnings', 250.0), - ('Total Unallocated Earnings', -550.0), - ('Retained Earnings', 800.0), - ('Current Year Retained Earnings', 800.0), - ('Previous Years Retained Earnings', 0.0), - ('Total Retained Earnings', 800.0), - ('Total EQUITY', 250.0), - - ('LIABILITIES + EQUITY', 50.0), - ], - options, - ) - - unfolded_lines = self.report._get_unfolded_lines(lines, line_id) - self.assertLinesValues( - unfolded_lines, - # Name Balance - [ 0, 1], - [ - ('Bank and Cash Accounts', -1300.0), - ('code2 account2', -1300.0), - ('Total Bank and Cash Accounts', -1300.0), - ], - options, - ) - - def test_financial_report_multi_company_currency(self): - line_id = self._get_basic_line_dict_id_from_report_line_ref('at_accounting.account_financial_report_bank_view0') - options = self._generate_options(self.report, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31')) - options['unfolded_lines'] = [line_id] - - lines = self.report._get_lines(options) - self.assertLinesValues( - lines, - # Name Balance - [ 0, 1], - [ - ('ASSETS', 50.0), - ('Current Assets', -4150.0), - ('Bank and Cash Accounts', -3300.0), - ('code102 account102', -2000.0), - ('code2 account2', -1300.0), - ('Total Bank and Cash Accounts', -3300.0), - ('Receivables', 2350.0), - ('Current Assets', 400.0), - ('Prepayments', -3600.0), - ('Total Current Assets', -4150.0), - ('Plus Fixed Assets', 0.0), - ('Plus Non-current Assets', 4200.0), - ('Total ASSETS', 50.0), - - ('LIABILITIES', -200.0), - ('Current Liabilities', -200.0), - ('Current Liabilities', 0.0), - ('Payables', -200.0), - ('Total Current Liabilities', -200.0), - ('Plus Non-current Liabilities', 0.0), - ('Total LIABILITIES', -200.0), - - ('EQUITY', 250.0), - ('Unallocated Earnings', -550.0), - ('Current Year Unallocated Earnings', -800.0), - ('Previous Years Unallocated Earnings', 250.0), - ('Total Unallocated Earnings', -550.0), - ('Retained Earnings', 800.0), - ('Current Year Retained Earnings', 800.0), - ('Previous Years Retained Earnings', 0.0), - ('Total Retained Earnings', 800.0), - ('Total EQUITY', 250.0), - - ('LIABILITIES + EQUITY', 50.0), - ], - options, - ) - - unfolded_lines = self.report._get_unfolded_lines(lines, line_id) - self.assertLinesValues( - unfolded_lines, - # Name Balance - [ 0, 1], - [ - ('Bank and Cash Accounts', -3300.0), - ('code102 account102', -2000.0), - ('code2 account2', -1300.0), - ('Total Bank and Cash Accounts', -3300.0), - ], - options, - ) - - def test_financial_report_comparison(self): - line_id = self._get_basic_line_dict_id_from_report_line_ref('at_accounting.account_financial_report_bank_view0') - options = self._generate_options(self.report, fields.Date.from_string('2019-01-01'), fields.Date.from_string('2019-12-31')) - options = self._update_comparison_filter(options, self.report, 'custom', 1, date_to=fields.Date.from_string('2018-12-31')) - options['unfolded_lines'] = [line_id] - - lines = self.report._get_lines(options) - - self.assertColumnPercentComparisonValues( - lines, - [ - ('ASSETS', '-80.0%', 'red'), - ('Current Assets', '27.7%', 'red'), - ('Bank and Cash Accounts', '10.0%', 'red'), - ('code102 account102', '0.0%', 'muted'), - ('code2 account2', '30.0%', 'red'), - ('Total Bank and Cash Accounts', '10.0%', 'red'), - ('Receivables', '4.4%', 'green'), - ('Current Assets', 'n/a', 'muted'), - ('Prepayments', '44.0%', 'red'), - ('Total Current Assets', '27.7%', 'red'), - ('Plus Fixed Assets', 'n/a', 'muted'), - ('Plus Non-current Assets', '20.0%', 'green'), - ('Total ASSETS', '-80.0%', 'red'), - - ('LIABILITIES', 'n/a', 'muted'), - ('Current Liabilities', 'n/a', 'muted'), - ('Current Liabilities', 'n/a', 'muted'), - ('Payables', 'n/a', 'muted'), - ('Total Current Liabilities', 'n/a', 'muted'), - ('Plus Non-current Liabilities', 'n/a', 'muted'), - ('Total LIABILITIES', 'n/a', 'muted'), - - ('EQUITY', '0.0%', 'muted'), - ('Unallocated Earnings', '-320.0%', 'red'), - ('Current Year Unallocated Earnings', '-420.0%', 'red'), - ('Previous Years Unallocated Earnings', 'n/a', 'muted'), - ('Total Unallocated Earnings', '-320.0%', 'red'), - ('Retained Earnings', 'n/a', 'muted'), - ('Current Year Retained Earnings', 'n/a', 'muted'), - ('Previous Years Retained Earnings', 'n/a', 'muted'), - ('Total Retained Earnings', 'n/a', 'muted'), - ('Total EQUITY', '0.0%', 'muted'), - - - ('LIABILITIES + EQUITY', '-80.0%', 'green'), - ] - ) - - def test_financial_report_horizontal_group(self): - line_id = self._get_basic_line_dict_id_from_report_line_ref('at_accounting.account_financial_report_receivable0') - self.report.horizontal_group_ids |= self.horizontal_group - - options = self._generate_options( - self.report, - fields.Date.from_string('2019-01-01'), - fields.Date.from_string('2019-12-31'), - default_options={ - 'unfolded_lines': [line_id], - 'selected_horizontal_group_id': self.horizontal_group.id, - } - ) - options = self._update_comparison_filter(options, self.report, 'custom', 1, date_to=fields.Date.from_string('2018-12-31')) - - lines = self.report._get_lines(options) - self.assertHeadersValues( - options['column_headers'], - [ - ['As of 12/31/2019', 'As of 12/31/2018'], - ['partner_a', 'partner_b'], - ['code0 account0', 'code1 account1'], - ] - ) - self.assertLinesValues( - lines, - [ 0, 1, 2, 3, 4, 5, 6, 7, 8], - [ - ('ASSETS', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - ('Current Assets', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - ('Bank and Cash Accounts', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Receivables', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - ('code0 account0', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - ('Total Receivables', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - ('Current Assets', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Prepayments', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Total Current Assets', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - ('Plus Fixed Assets', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Plus Non-current Assets', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Total ASSETS', 1300.0, 0.0, 25.0, 0.0, 1250.0, 0.0, 0.0, 0.0), - - ('LIABILITIES', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0), - ('Current Liabilities', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0), - ('Current Liabilities', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Payables', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0), - ('Total Current Liabilities', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0), - ('Plus Non-current Liabilities', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Total LIABILITIES', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0), - - ('EQUITY', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Current Year Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Previous Years Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Total Unallocated Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Current Year Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Previous Years Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Total Retained Earnings', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - ('Total EQUITY', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - - ('LIABILITIES + EQUITY', 0.0, 0.0, 0.0, -200.0, 0.0, 0.0, 0.0, 0.0), - ], - options, - ) - - def test_financial_report_horizontal_group_total(self): - """ - In case we don't have comparison, just one column and one level of groupby a new column is added which is the total - of the horizontal group - """ - horizontal_group = self.env['account.report.horizontal.group'].create({ - 'name': 'Horizontal Group total', - 'rule_ids': [ - Command.create({ - 'field_name': 'partner_id', - 'domain': f"[('id', 'in', {(self.partner_a + self.partner_b).ids})]", - }), - ], - }) - self.report.horizontal_group_ids |= horizontal_group - options = self._generate_options(self.report, '2019-01-01', '2019-12-31', default_options={'selected_horizontal_group_id': horizontal_group.id}) - self.assertHeadersValues( - options['column_headers'], - [ - ['As of 12/31/2019'], - ['partner_a', 'partner_b'], - ] - ) - - self.assertTrue(options['show_horizontal_group_total']) - # Since we don't calculate the value when totals below section is activated, we disable it - self.env.company.totals_below_sections = False - self.assertHorizontalGroupTotal( - self.report._get_lines(options), - [ - ('ASSETS', 6900.0, -4075.0, 2825.0), - ('Current Assets', 2700.0, -4075.0, -1375.0), - ('Bank and Cash Accounts', 0.0, -3000.0, -3000.0), - ('Receivables', 2300.0, 25.0, 2325.0), - ('Current Assets', 400.0, 0.0, 400.0), - ('Prepayments', 0.0, -1100.0, -1100.0), - ('Plus Fixed Assets', 0.0, 0.0, 0.0), - ('Plus Non-current Assets', 4200.0, 0.0, 4200.0), - ('LIABILITIES', 0.0, -200.0, -200.0), - ('Current Liabilities', 0.0, -200.0, -200.0), - ('Current Liabilities', 0.0, 0.0, 0.0), - ('Payables', 0.0, -200.0, -200.0), - ('Plus Non-current Liabilities', 0.0, 0.0, 0.0), - ('EQUITY', 250.0, 800.0, 1050.0), - ('Unallocated Earnings', 250.0, 0.0, 250.0), - ('Current Year Unallocated Earnings', 0.0, 0.0, 0.0), - ('Previous Years Unallocated Earnings', 250.0, 0.0, 250.0), - ('Retained Earnings', 0.0, 800.0, 800.0), - ('Current Year Retained Earnings', 0.0, 800.0, 800.0), - ('Previous Years Retained Earnings', 0.0, 0.0, 0.0), - ('LIABILITIES + EQUITY', 250.0, 600.0, 850.0), - ], - ) - - options = self._generate_options(self.report, '2019-01-01', '2019-12-31', default_options={'selected_horizontal_group_id': horizontal_group.id}) - options = self._update_comparison_filter(options, self.report, 'custom', 1, date_to=fields.Date.from_string('2018-12-31')) - self.assertHeadersValues( - options['column_headers'], - [ - ['As of 12/31/2019', 'As of 12/31/2018'], - ['partner_a', 'partner_b'], - ] - ) - - self.assertFalse(options['show_horizontal_group_total']) - - self.assertHorizontalGroupTotal( - self.report._get_lines(options), - [ - ('ASSETS', 6900.0, -4075.0, 5750.0, -3000.0), - ('Current Assets', 2700.0, -4075.0, 2250.0, -3000.0), - ('Bank and Cash Accounts', 0.0, -3000.0, 0.0, -3000.0), - ('Receivables', 2300.0, 25.0, 2250.0, 0.0), - ('Current Assets', 400.0, 0.0, 0.0, 0.0), - ('Prepayments', 0.0, -1100.0, 0.0, 0.0), - ('Plus Fixed Assets', 0.0, 0.0, 0.0, 0.0), - ('Plus Non-current Assets', 4200.0, 0.0, 3500.0, 0.0), - ('LIABILITIES', 0.0, -200.0, 0.0, 0.0), - ('Current Liabilities', 0.0, -200.0, 0.0, 0.0), - ('Current Liabilities', 0.0, 0.0, 0.0, 0.0), - ('Payables', 0.0, -200.0, 0.0, 0.0), - ('Plus Non-current Liabilities', 0.0, 0.0, 0.0, 0.0), - ('EQUITY', 250.0, 800.0, 250.0, 0.0), - ('Unallocated Earnings', 250.0, 0.0, 250.0, 0.0), - ('Current Year Unallocated Earnings', 0.0, 0.0, 250.0, 0.0), - ('Previous Years Unallocated Earnings', 250.0, 0.0, 0.0, 0.0), - ('Retained Earnings', 0.0, 800.0, 0.0, 0.0), - ('Current Year Retained Earnings', 0.0, 800.0, 0.0, 0.0), - ('Previous Years Retained Earnings', 0.0, 0.0, 0.0, 0.0), - ('LIABILITIES + EQUITY', 250.0, 600.0, 250.0, 0.0), - ], - ) - - def test_hide_if_zero_with_no_formulas(self): - """ - Check if a report line stays displayed when hide_if_zero is True and no formulas - is set on the line but has some child which have balance != 0 - We check also if the line is hidden when all its children have balance == 0 - """ - account1, account2 = self.env['account.account'].create([{ - 'name': "test_financial_report_1", - 'code': "42241", - 'account_type': "asset_fixed", - }, { - 'name': "test_financial_report_2", - 'code': "42242", - 'account_type': "asset_fixed", - }]) - - moves = self.env['account.move'].create([ - { - 'move_type': 'entry', - 'date': '2019-04-01', - 'line_ids': [ - (0, 0, {'debit': 3.0, 'credit': 0.0, 'account_id': account1.id}), - (0, 0, {'debit': 0.0, 'credit': 3.0, 'account_id': self.company_data['default_account_revenue'].id}), - ], - }, - { - 'move_type': 'entry', - 'date': '2019-05-01', - 'line_ids': [ - (0, 0, {'debit': 0.0, 'credit': 1.0, 'account_id': account2.id}), - (0, 0, {'debit': 1.0, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}), - ], - }, - { - 'move_type': 'entry', - 'date': '2019-04-01', - 'line_ids': [ - (0, 0, {'debit': 0.0, 'credit': 3.0, 'account_id': account2.id}), - (0, 0, {'debit': 3.0, 'credit': 0.0, 'account_id': self.company_data['default_account_revenue'].id}), - ], - }, - ]) - moves.action_post() - moves.line_ids.flush_recordset() - - report = self.env["account.report"].create({ - 'name': "test_financial_report_sum", - 'column_ids': [ - Command.create({ - 'name': "Balance", - 'expression_label': 'balance', - 'sequence': 1, - }), - ], - 'line_ids': [ - Command.create({ - 'name': "Title", - 'code': 'TT', - 'hide_if_zero': True, - 'sequence': 0, - 'children_ids': [ - Command.create({ - 'name': "report_line_1", - 'code': 'TEST_L1', - 'sequence': 1, - 'expression_ids': [ - Command.create({ - 'label': 'balance', - 'engine': 'domain', - 'formula': f"[('account_id', '=', {account1.id})]", - 'subformula': 'sum', - 'date_scope': 'from_beginning', - }), - ], - }), - Command.create({ - 'name': "report_line_2", - 'code': 'TEST_L2', - 'sequence': 2, - 'expression_ids': [ - Command.create({ - 'label': 'balance', - 'engine': 'domain', - 'formula': f"[('account_id', '=', {account2.id})]", - 'subformula': 'sum', - 'date_scope': 'from_beginning', - }), - ], - }), - ] - }), - ], - }) - - # TODO without this, the create() puts newIds in the sublines, and flushing doesn't help. Seems to be an ORM bug. - self.env.invalidate_all() - - options = self._generate_options(report, fields.Date.from_string('2019-05-01'), fields.Date.from_string('2019-05-01')) - options = self._update_comparison_filter(options, report, 'previous_period', 2) - - self.assertLinesValues( - report._get_lines(options), - [ 0, 1, 2, 3], - [ - ("Title", '', '', ''), - ("report_line_1", 3.0, 3.0, 0.0), - ("report_line_2", -4.0, -3.0, 0.0), - ], - options, - ) - - move = self.env['account.move'].create({ - 'move_type': 'entry', - 'date': '2019-05-01', - 'line_ids': [ - (0, 0, {'debit': 0.0, 'credit': 3.0, 'account_id': account1.id}), - (0, 0, {'debit': 4.0, 'credit': 0.0, 'account_id': account2.id}), - (0, 0, {'debit': 0.0, 'credit': 1.0, 'account_id': self.company_data['default_account_revenue'].id}), - ], - }) - - move.action_post() - move.line_ids.flush_recordset() - - # With the comparison still on, the lines shouldn't be hidden - self.assertLinesValues( - report._get_lines(options), - [ 0, 1, 2, 3], - [ - ("Title", '', '', ''), - ("report_line_1", 0.0, 3.0, 0.0), - ("report_line_2", 0.0, -3.0, 0.0), - ], - options, - ) - - # Removing the comparison should hide the lines, as they will be 0 in every considered period (the current one) - options = self._update_comparison_filter(options, report, 'previous_period', 0) - self.assertLinesValues(report._get_lines(options), [0, 1, 2, 3], [], options) - - def test_option_hierarchy(self): - """ Check that the report lines are correct when the option "Hierarchy and subtotals is ticked""" - self.env['account.group'].create({ - 'name': 'Sales', - 'code_prefix_start': '40', - 'code_prefix_end': '49', - }) - - move = self.env['account.move'].create({ - 'date': '2020-02-02', - 'line_ids': [ - Command.create({ - 'account_id': self.company_data['default_account_revenue'].id, - 'name': 'name', - }) - ], - }) - move.action_post() - move.line_ids.flush_recordset() - profit_and_loss_report = self.env.ref('at_accounting.profit_and_loss') - line_id = self._get_basic_line_dict_id_from_report_line_ref('at_accounting.account_financial_report_revenue0') - options = self._generate_options(profit_and_loss_report, '2020-02-01', '2020-02-28') - options['unfolded_lines'] = [line_id] - options['hierarchy'] = True - self.env.company.totals_below_sections = False - lines = profit_and_loss_report._get_lines(options) - - unfolded_lines = profit_and_loss_report._get_unfolded_lines(lines, line_id) - unfolded_lines = [{'name': line['name'], 'level': line['level']} for line in unfolded_lines] - - self.assertEqual( - unfolded_lines, - [ - {'level': 1, 'name': 'Revenue'}, - {'level': 2, 'name': '40-49 Sales'}, - {'level': 3, 'name': '400000 Product Sales'}, - ] - ) - - def test_option_hierarchy_with_no_group_lines(self): - """ Check that the report lines of 'No Group' have correct ids with the option 'Hierarchy and subtotals' """ - self.env['account.group'].create({ - 'name': 'Sales', - 'code_prefix_start': '45', - 'code_prefix_end': '49', - }) - - move = self.env['account.move'].create({ - 'date': '2020-02-02', - 'line_ids': [ - Command.create({ - 'account_id': self.company_data['default_account_revenue'].id, - 'name': 'name', - }) - ], - }) - move.action_post() - move.line_ids.flush_recordset() - profit_and_loss_report = self.env.ref('at_accounting.profit_and_loss') - line_id = self._get_basic_line_dict_id_from_report_line_ref('at_accounting.account_financial_report_revenue0') - options = self._generate_options(profit_and_loss_report, '2020-02-01', '2020-02-28') - options['unfolded_lines'] = [line_id] - options['hierarchy'] = True - self.env.company.totals_below_sections = False - lines = profit_and_loss_report._get_lines(options) - lines_array = [{'name': line['name'], 'level': line['level']} for line in lines] - - self.assertEqual( - lines_array, - [ - {'name': 'Revenue', 'level': 1}, - {'name': '(No Group)', 'level': 2}, - {'name': '400000 Product Sales', 'level': 3}, - {'name': 'Less Costs of Revenue', 'level': 1}, - {'name': 'Gross Profit', 'level': 0}, - {'name': 'Less Operating Expenses', 'level': 1}, - {'name': 'Operating Income (or Loss)', 'level': 0}, - {'name': 'Plus Other Income', 'level': 1}, - {'name': 'Less Other Expenses', 'level': 1}, - {'name': 'Net Profit', 'level': 0}, - ] - ) - - self.assertEqual(lines[1]['id'], lines[0]['id'] + '|' + '~account.group~') - - def test_parse_line_id(self): - line_id_1 = self.env['account.report']._parse_line_id('markup1~account.account~5|markup2~res.partner~8|markup3~~') - line_id_2 = self.env['account.report']._parse_line_id('~account.report~14|{"groupby_prefix_group": "~"}~account.report~21') - - self.assertEqual(line_id_1, [('markup1', 'account.account', 5), ('markup2', 'res.partner', 8), ('markup3', None, None)]) - self.assertEqual(line_id_2, [('', 'account.report', 14), ({"groupby_prefix_group": "~"}, 'account.report', 21)]) diff --git a/addons/at_accounting/tests/test_import_bank_statement.py b/addons/at_accounting/tests/test_import_bank_statement.py deleted file mode 100644 index ec22a50..0000000 --- a/addons/at_accounting/tests/test_import_bank_statement.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo import fields -from odoo.exceptions import UserError, ValidationError -from odoo.tests import tagged -from odoo.tools import file_open - - -@tagged('post_install', '-at_install') -class TestAccountBankStatementImportCSV(AccountTestInvoicingCommon): - - def _import_file(self, csv_file_path, csv_fields=False): - # Create a bank account and journal corresponding to the CSV file (same currency and account number) - bank_journal = self.env['account.journal'].create({ - 'name': 'Bank 123456', - 'code': 'BNK67', - 'type': 'bank', - 'bank_acc_number': '123456', - 'currency_id': self.env.ref("base.USD").id, - }) - - # Use an import wizard to process the file - with file_open(csv_file_path, 'rb') as csv_file: - action = bank_journal.create_document_from_attachment(self.env['ir.attachment'].create({ - 'mimetype': 'text/csv', - 'name': 'test_csv.csv', - 'raw': csv_file.read(), - }).ids) - import_wizard = self.env['base_import.import'].browse( - action['params']['context']['wizard_id'] - ).with_context(action['params']['context']) - - import_wizard_options = { - 'date_format': '%m %d %y', - 'keep_matches': False, - 'encoding': 'utf-8', - 'fields': [], - 'quoting': '"', - 'bank_stmt_import': True, - 'headers': True, - 'separator': ';', - 'float_thousand_separator': ',', - 'float_decimal_separator': '.', - 'advanced': False, - } - import_wizard_fields = csv_fields or ['date', False, 'payment_ref', 'amount', 'balance'] - import_wizard.execute_import(import_wizard_fields, [], import_wizard_options, dryrun=False) - - def test_csv_file_import(self): - self._import_file('at_accounting/test_csv_file/test_csv.csv') - - # Check the imported bank statement - imported_statement = self.env['account.bank.statement'].search([('company_id', '=', self.env.company.id)]) - self.assertRecordValues(imported_statement, [{ - 'reference': 'test_csv.csv', - 'balance_start': 21699.55, - 'balance_end_real': 23462.55, - }]) - self.assertRecordValues(imported_statement.line_ids.sorted(lambda line: (line.date, line.payment_ref)), [ - {'date': fields.Date.from_string('2015-02-02'), 'amount': 3728.87, 'payment_ref': 'ACH CREDIT"AMERICAN EXPRESS-SETTLEMENT'}, - {'date': fields.Date.from_string('2015-02-02'), 'amount': -500.08, 'payment_ref': 'DEBIT CARD 6906 EFF 02-01"01/31 INDEED 203-564-2400 CT'}, - {'date': fields.Date.from_string('2015-02-02'), 'amount': -240.00, 'payment_ref': 'DEBIT CARD 6906 EFF 02-01"01/31 MAILCHIMP MAILCHIMP.COMGA'}, - {'date': fields.Date.from_string('2015-02-02'), 'amount': -2064.82, 'payment_ref': 'DEBIT CARD 6906"02/02 COMFORT INNS SAN FRANCISCOCA'}, - {'date': fields.Date.from_string('2015-02-02'), 'amount': -41.64, 'payment_ref': 'DEBIT CARD 6906"BAYSIDE MARKET/1 SAN FRANCISCO CA'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': 2500.00, 'payment_ref': 'ACH CREDIT"CHECKFLUID INC -013015'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -25.00, 'payment_ref': 'ACH DEBIT"AUTHNET GATEWAY -BILLING'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -7500.00, 'payment_ref': 'ACH DEBIT"WW 222 BROADWAY -ACH'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -45.86, 'payment_ref': 'DEBIT CARD 6906"02/02 DISTRICT SF SAN FRANCISCOCA'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -1123.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -1123.33, 'payment_ref': 'DEBIT CARD 6906"02/02 VIR ATL 9327 180-08628621 CT'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': -4344.66, 'payment_ref': 'DEBIT CARD 6906"02/03 IBM USED PC 888S 188-874-6742 NY'}, - {'date': fields.Date.from_string('2015-02-03'), 'amount': 8366.00, 'payment_ref': 'DEPOSIT-WIRED FUNDS"TVET OPERATING PLLC'}, - {'date': fields.Date.from_string('2015-02-04'), 'amount': -1284.33, 'payment_ref': 'DEBIT CARD 6906"02/03 VIR ATL 9327 180-08628621 CT'}, - {'date': fields.Date.from_string('2015-02-04'), 'amount': -204.23, 'payment_ref': 'DEBIT CARD 6906"02/04 GROUPON INC 877-788-7858 IL'}, - {'date': fields.Date.from_string('2015-02-05'), 'amount': 9518.40, 'payment_ref': 'ACH CREDIT"MERCHE-SOLUTIONS-MERCH DEP'}, - ]) - - def test_csv_file_import_with_missing_values(self): - self._import_file('at_accounting/test_csv_file/test_csv_missing_values.csv', ['transaction_type', 'ref', 'payment_ref', 'debit', 'credit']) - - imported_statement = self.env['account.bank.statement'].search([('company_id', '=', self.env.company.id)]) - - self.assertEqual(len(imported_statement.line_ids), 2) - - self.assertRecordValues(imported_statement.line_ids.sorted(lambda line: line.amount), [ - {'transaction_type': 'TRANSFER', 'ref': 'bank_ref_1', 'payment_ref': 'bank_statement_line_1', 'sequence': 0, 'amount': 1000.0}, - {'transaction_type': 'TRANSFER', 'ref': False, 'payment_ref': 'bank_statement_line_2', 'sequence': 1, 'amount': 3500.0}, - ]) - - def test_csv_file_import_non_ordered(self): - with self.assertRaises(UserError): - self._import_file('at_accounting/test_csv_file/test_csv_non_sorted.csv') - - def test_csv_file_empty_date(self): - with self.assertRaises(UserError): - self._import_file('at_accounting/test_csv_file/test_csv_empty_date.csv') - - def test_csv_file_import_without_amount(self): - csv_fields = ['date', False, 'payment_ref', 'balance'] - with self.assertRaisesRegex(ValidationError, "Make sure that an Amount or Debit and Credit is in the file."): - self._import_file('at_accounting/test_csv_file/test_csv_without_amount.csv', csv_fields) diff --git a/addons/at_accounting/tests/test_prediction.py b/addons/at_accounting/tests/test_prediction.py deleted file mode 100644 index 84cdf5b..0000000 --- a/addons/at_accounting/tests/test_prediction.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- encoding: utf-8 -*- - -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo import fields, Command -from odoo.tests import Form, tagged - - -@tagged('post_install', '-at_install') -class TestBillsPrediction(AccountTestInvoicingCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - - cls.company.predict_bill_product = True - - cls.test_partners = cls.env['res.partner'].create([{'name': 'test partner %s' % i} for i in range(7)]) - - accounts_data = [{ - 'code': 'test%s' % i, - 'name': name, - 'account_type': 'expense', - } for i, name in enumerate(( - "Test Maintenance and Repair", - "Test Purchase of services, studies and preparatory work", - "Test Various Contributions", - "Test Rental Charges", - "Test Purchase of commodity", - ))] - - cls.test_accounts = cls.env['account.account'].create(accounts_data) - - cls.frozen_today = fields.Date.today() - - def _create_bill(self, vendor, line_name, expected_account, account_to_set=None, post=True): - ''' Create a new vendor bill to test the prediction. - :param vendor: The vendor to set on the invoice. - :param line_name: The name of the invoice line that will be used to predict. - :param expected_account: The expected predicted account. - :param account_to_set: The optional account to set as a correction of the predicted account. - :return: The newly created vendor bill. - ''' - invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) - invoice_form.partner_id = vendor - invoice_form.invoice_date = self.frozen_today - with invoice_form.invoice_line_ids.new() as invoice_line_form: - # Set the default account to avoid "account_id is a required field" in case of bad configuration. - invoice_line_form.account_id = self.company_data['default_journal_purchase'].default_account_id - - invoice_line_form.quantity = 1.0 - invoice_line_form.price_unit = 42.0 - invoice_line_form.name = line_name - invoice = invoice_form.save() - invoice_line = invoice.invoice_line_ids - - self.assertEqual( - invoice_line.account_id, - expected_account, - "Account '%s' should have been predicted instead of '%s'" % ( - expected_account.display_name, - invoice_line.account_id.display_name, - ), - ) - - if account_to_set: - invoice_line.account_id = account_to_set - - if post: - invoice.action_post() - return invoice - - def test_account_prediction_flow(self): - default_account = self.company_data['default_journal_purchase'].default_account_id - self._create_bill(self.test_partners[0], "Maintenance and repair", self.test_accounts[0]) - self._create_bill(self.test_partners[5], "Subsidies obtained", default_account, account_to_set=self.test_accounts[1]) - self._create_bill(self.test_partners[6], "Prepare subsidies file", default_account, account_to_set=self.test_accounts[1]) - self._create_bill(self.test_partners[6], "Prepare subsidies file", self.test_accounts[1]) - self._create_bill(self.test_partners[1], "Contributions January", self.test_accounts[2]) - self._create_bill(self.test_partners[2], "Coca-cola", default_account, account_to_set=self.test_accounts[4]) - self._create_bill(self.test_partners[1], "Contribution February", self.test_accounts[2]) - self._create_bill(self.test_partners[3], "Electricity Bruxelles", default_account, account_to_set=self.test_accounts[3]) - self._create_bill(self.test_partners[3], "Electricity Grand-Rosière", self.test_accounts[3]) - self._create_bill(self.test_partners[2], "Purchase of coca-cola", self.test_accounts[4]) - self._create_bill(self.test_partners[4], "Crate of coca-cola", default_account, account_to_set=self.test_accounts[4]) - self._create_bill(self.test_partners[4], "Crate of coca-cola", self.test_accounts[4]) - self._create_bill(self.test_partners[1], "March", self.test_accounts[2]) - - def test_account_prediction_from_label_expected_behavior(self): - """Prevent the prediction from being annoying.""" - default_account = self.company_data['default_journal_purchase'].default_account_id - payable_account = self.company_data['default_account_payable'].copy() - payable_account.write({'name': f'Account payable - {self.test_accounts[0].name}'}) - - # There is no prior result, we take the default account, but we don't post - self._create_bill(self.test_partners[0], self.test_partners[0].name, default_account, post=False) - - # There is no prior result, we take the default account - self._create_bill(self.test_partners[0], "Drinks", default_account, account_to_set=self.test_accounts[0]) - - # There is only one prior account for the partner, we take that one - self._create_bill(self.test_partners[0], "Desert", self.test_accounts[0], account_to_set=self.test_accounts[1]) - - # We find something close enough, take that one - self._create_bill(self.test_partners[0], "Drinks too", self.test_accounts[0]) - - # There is no clear preference for any account (both previous accounts have the same rank) - # don't make any prediction and let the default behavior fill the account - invoice = self._create_bill(self.test_partners[0], "Main course", default_account) - invoice.button_draft() - - with Form(invoice) as move_form: - with move_form.invoice_line_ids.edit(0) as line_form: - # There isn't any account clearly better than the manually set one, we keep the current one - line_form.account_id = self.test_accounts[2] - line_form.name = "Apple" - self.assertEqual(line_form.account_id, self.test_accounts[2]) - - # There is an account that looks clearly better, use it - line_form.name = "Second desert" - self.assertEqual(line_form.account_id, self.test_accounts[1]) - - def test_account_prediction_with_product(self): - product = self.env['product.product'].create({ - 'name': 'product_a', - 'lst_price': 1000.0, - 'standard_price': 800.0, - 'property_account_income_id': self.company_data['default_account_revenue'].id, - 'property_account_expense_id': self.company_data['default_account_expense'].id, - }) - - invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) - invoice_form.partner_id = self.test_partners[0] - invoice_form.invoice_date = self.frozen_today - with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.product_id = product - invoice_line_form.name = "Maintenance and repair" - invoice = invoice_form.save() - - self.assertRecordValues(invoice.invoice_line_ids, [{ - 'name': "Maintenance and repair", - 'product_id': product.id, - 'account_id': self.company_data['default_account_expense'].id, - }]) - - def test_product_prediction_price_subtotal_computation(self): - invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) - invoice_form.partner_id = self.test_partners[0] - invoice_form.invoice_date = self.frozen_today - with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.product_id = self.product_a - invoice = invoice_form.save() - invoice.action_post() - - self.product_a.supplier_taxes_id = [Command.set(self.tax_purchase_b.ids)] - - invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) - invoice_form.partner_id = self.test_partners[0] - invoice_form.invoice_date = self.frozen_today - with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.name = 'product_a' - invoice = invoice_form.save() - - self.assertRecordValues(invoice.invoice_line_ids, [{ - 'quantity': 1.0, - 'price_unit': 800.0, - 'price_subtotal': 800.0, - 'balance': 800.0, - 'tax_ids': self.tax_purchase_b.ids, - }]) - - # In case a unit price is already set we do not update the unit price - invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) - invoice_form.partner_id = self.test_partners[0] - invoice_form.invoice_date = self.frozen_today - with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.price_unit = 42.0 - invoice_line_form.name = 'product_a' - invoice = invoice_form.save() - - self.assertRecordValues(invoice.invoice_line_ids, [{ - 'quantity': 1.0, - 'price_unit': 42.0, - 'price_subtotal': 42.0, - 'balance': 42.0, - 'tax_ids': self.tax_purchase_b.ids, - }]) - - # In case a tax is already set we do not update the taxes - invoice_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) - invoice_form.partner_id = self.test_partners[0] - invoice_form.invoice_date = self.frozen_today - with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.tax_ids = self.tax_purchase_a - invoice_line_form.name = 'product_a' - invoice = invoice_form.save() - - self.assertRecordValues(invoice.invoice_line_ids, [{ - 'quantity': 1.0, - 'price_unit': 800.0, - 'price_subtotal': 800.0, - 'balance': 800.0, - 'tax_ids': self.tax_purchase_a.ids, - }]) diff --git a/addons/at_accounting/tests/test_reconciliation_matching_rules.py b/addons/at_accounting/tests/test_reconciliation_matching_rules.py deleted file mode 100644 index be17bf3..0000000 --- a/addons/at_accounting/tests/test_reconciliation_matching_rules.py +++ /dev/null @@ -1,1306 +0,0 @@ -# -*- coding: utf-8 -*- -from freezegun import freeze_time -from contextlib import contextmanager - -from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo.tests import Form, tagged -from odoo import Command - - -@tagged('post_install', '-at_install') -class TestReconciliationMatchingRules(AccountTestInvoicingCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - - ################# - # Company setup # - ################# - cls.other_currency = cls.setup_other_currency('EUR') - cls.other_currency_2 = cls.setup_other_currency('CAD', rates=[('2016-01-01', 10.0), ('2017-01-01', 20.0)]) - - cls.account_pay = cls.company_data['default_account_payable'] - cls.current_assets_account = cls.env['account.account'].search([ - ('account_type', '=', 'asset_current'), - ('company_ids', '=', cls.company.id)], limit=1) - - cls.bank_journal = cls.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', cls.company.id)], limit=1) - cls.cash_journal = cls.env['account.journal'].search([('type', '=', 'cash'), ('company_id', '=', cls.company.id)], limit=1) - - cls.tax21 = cls.env['account.tax'].create({ - 'name': '21%', - 'type_tax_use': 'purchase', - 'amount': 21, - }) - - cls.tax12 = cls.env['account.tax'].create({ - 'name': '12%', - 'type_tax_use': 'purchase', - 'amount': 12, - }) - - cls.partner_1 = cls.env['res.partner'].create({'name': 'partner_1', 'company_id': cls.company.id}) - cls.partner_2 = cls.env['res.partner'].create({'name': 'partner_2', 'company_id': cls.company.id}) - cls.partner_3 = cls.env['res.partner'].create({'name': 'partner_3', 'company_id': cls.company.id}) - - ############### - # Rules setup # - ############### - cls.rule_1 = cls.env['account.reconcile.model'].create({ - 'name': 'Invoices Matching Rule', - 'sequence': '1', - 'rule_type': 'invoice_matching', - 'auto_reconcile': False, - 'match_nature': 'both', - 'match_same_currency': True, - 'allow_payment_tolerance': True, - 'payment_tolerance_type': 'percentage', - 'payment_tolerance_param': 0.0, - 'match_partner': True, - 'match_partner_ids': [(6, 0, (cls.partner_1 + cls.partner_2 + cls.partner_3).ids)], - 'company_id': cls.company.id, - 'line_ids': [(0, 0, {'account_id': cls.current_assets_account.id})], - }) - cls.rule_2 = cls.env['account.reconcile.model'].create({ - 'name': 'write-off model', - 'rule_type': 'writeoff_suggestion', - 'match_partner': True, - 'match_partner_ids': [], - 'line_ids': [(0, 0, {'account_id': cls.current_assets_account.id})], - }) - - ################## - # Invoices setup # - ################## - cls.invoice_line_1 = cls._create_invoice_line(100, cls.partner_1, 'out_invoice') - cls.invoice_line_2 = cls._create_invoice_line(200, cls.partner_1, 'out_invoice') - cls.invoice_line_3 = cls._create_invoice_line(300, cls.partner_1, 'in_refund', name="RBILL/2019/09/0013") - cls.invoice_line_4 = cls._create_invoice_line(1000, cls.partner_2, 'in_invoice') - cls.invoice_line_5 = cls._create_invoice_line(600, cls.partner_3, 'out_invoice') - cls.invoice_line_6 = cls._create_invoice_line(600, cls.partner_3, 'out_invoice', ref="RF12 3456") - cls.invoice_line_7 = cls._create_invoice_line(200, cls.partner_3, 'out_invoice') - - #################### - # Statements setup # - #################### - # TODO : account_number, partner_name, transaction_type, narration - invoice_number = cls.invoice_line_1.move_id.name - cls.bank_line_1, cls.bank_line_2,\ - cls.bank_line_3, cls.bank_line_4,\ - cls.bank_line_5, cls.cash_line_1 = cls.env['account.bank.statement.line'].create([ - { - 'journal_id': cls.bank_journal.id, - 'date': '2020-01-01', - 'payment_ref': 'invoice %s-%s' % tuple(invoice_number.split('/')[1:]), - 'partner_id': cls.partner_1.id, - 'amount': 100, - 'sequence': 1, - }, - { - 'journal_id': cls.bank_journal.id, - 'date': '2020-01-01', - 'payment_ref': 'xxxxx', - 'partner_id': cls.partner_1.id, - 'amount': 600, - 'sequence': 2, - }, - { - 'journal_id': cls.bank_journal.id, - 'date': '2020-01-01', - 'payment_ref': 'nawak', - 'narration': 'Communication: RF12 3456', - 'partner_id': cls.partner_3.id, - 'amount': 600, - 'sequence': 1, - }, - { - 'journal_id': cls.bank_journal.id, - 'date': '2020-01-01', - 'payment_ref': 'RF12 3456', - 'partner_id': cls.partner_3.id, - 'amount': 600, - 'sequence': 2, - }, - { - 'journal_id': cls.bank_journal.id, - 'date': '2020-01-01', - 'payment_ref': 'baaaaah', - 'ref': 'RF12 3456', - 'partner_id': cls.partner_3.id, - 'amount': 600, - 'sequence': 2, - }, - { - 'journal_id': cls.cash_journal.id, - 'date': '2020-01-01', - 'payment_ref': 'yyyyy', - 'partner_id': cls.partner_2.id, - 'amount': -1000, - 'sequence': 1, - }, - ]) - - @classmethod - def _create_invoice_line(cls, amount, partner, move_type, currency=None, ref=None, name=None, inv_date='2019-09-01'): - ''' Create an invoice on the fly.''' - invoice_form = Form(cls.env['account.move'].with_context(default_move_type=move_type, default_invoice_date=inv_date, default_date=inv_date)) - invoice_form.partner_id = partner - if currency: - invoice_form.currency_id = currency - if ref: - invoice_form.ref = ref - if name: - invoice_form.name = name - with invoice_form.invoice_line_ids.new() as invoice_line_form: - invoice_line_form.name = 'xxxx' - invoice_line_form.quantity = 1 - invoice_line_form.price_unit = amount - invoice_line_form.tax_ids.clear() - invoice = invoice_form.save() - invoice.action_post() - lines = invoice.line_ids - return lines.filtered(lambda l: l.account_id.account_type in ('asset_receivable', 'liability_payable')) - - @classmethod - def _create_st_line(cls, amount=1000.0, date='2019-01-01', payment_ref='turlututu', **kwargs): - st_line = cls.env['account.bank.statement.line'].create({ - 'journal_id': kwargs.get('journal_id', cls.bank_journal.id), - 'amount': amount, - 'date': date, - 'payment_ref': payment_ref, - 'partner_id': cls.partner_a.id, - **kwargs, - }) - return st_line - - @classmethod - def _create_reconcile_model(cls, **kwargs): - return cls.env['account.reconcile.model'].create({ - 'name': "test", - 'rule_type': 'invoice_matching', - 'allow_payment_tolerance': True, - 'payment_tolerance_type': 'percentage', - 'payment_tolerance_param': 0.0, - **kwargs, - 'line_ids': [ - Command.create({ - 'account_id': cls.company_data['default_account_revenue'].id, - 'amount_type': 'percentage', - 'label': f"test {i}", - **line_vals, - }) - for i, line_vals in enumerate(kwargs.get('line_ids', [])) - ], - 'partner_mapping_line_ids': [ - Command.create(line_vals) - for i, line_vals in enumerate(kwargs.get('partner_mapping_line_ids', [])) - ], - }) - - @freeze_time('2020-01-01') - def _check_statement_matching(self, rules, expected_values_list): - for statement_line, expected_values in expected_values_list.items(): - res = rules._apply_rules(statement_line, statement_line._retrieve_partner()) - self.assertDictEqual(res, expected_values) - - def test_matching_fields(self): - # Check without restriction. - self.rule_1.match_text_location_label = False - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, - self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - - @freeze_time('2020-01-01') - def test_matching_fields_match_text_location(self): - st_line = self._create_st_line(payment_ref="1111", ref="2222 3333", narration="4444 5555 6666") - - inv1 = self._create_invoice_line(1000, self.partner_a, 'out_invoice', ref="bernard 1111 gagnant") - inv2 = self._create_invoice_line(1000, self.partner_a, 'out_invoice', ref="2222 turlututu 3333") - inv3 = self._create_invoice_line(1000, self.partner_a, 'out_invoice', ref="4444 tsoin 5555 tsoin 6666") - - rule = self._create_reconcile_model( - allow_payment_tolerance=False, - match_text_location_label=True, - match_text_location_reference=False, - match_text_location_note=False, - ) - self.assertDictEqual( - rule._apply_rules(st_line, st_line._retrieve_partner()), - {'amls': inv1, 'model': rule}, - ) - - rule.match_text_location_reference = True - self.assertDictEqual( - rule._apply_rules(st_line, st_line._retrieve_partner()), - {'amls': inv2, 'model': rule}, - ) - - rule.match_text_location_note = True - self.assertDictEqual( - rule._apply_rules(st_line, st_line._retrieve_partner()), - {'amls': inv3, 'model': rule}, - ) - - def test_matching_fields_match_text_location_no_partner(self): - self.bank_line_2.unlink() # One line is enough for this test - self.bank_line_1.partner_id = None - - self.partner_1.name = "Bernard Gagnant" - - self.rule_1.write({ - 'match_partner': False, - 'match_partner_ids': [(5, 0, 0)], - 'line_ids': [(5, 0, 0)], - }) - - st_line_initial_vals = {'ref': None, 'payment_ref': 'nothing', 'narration': None} - recmod_initial_vals = {'match_text_location_label': False, 'match_text_location_note': False, 'match_text_location_reference': False} - - rec_mod_options_to_fields = { - 'match_text_location_label': 'payment_ref', - 'match_text_location_note': 'narration', - 'match_text_location_reference': 'ref', - } - - for rec_mod_field, st_line_field in rec_mod_options_to_fields.items(): - self.rule_1.write({**recmod_initial_vals, rec_mod_field: True}) - # Fully reinitialize the statement line - self.bank_line_1.write(st_line_initial_vals) - - # Test matching with the invoice ref - self.bank_line_1.write({st_line_field: self.invoice_line_1.move_id.payment_reference}) - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, - }) - - def test_matching_fields_match_journal_ids(self): - self.rule_1.match_text_location_label = False - self.rule_1.match_journal_ids |= self.cash_line_1.journal_id - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - - def test_matching_fields_match_nature(self): - self.rule_1.match_text_location_label = False - self.rule_1.match_nature = 'amount_received' - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, - self.bank_line_2: { - 'amls': self.invoice_line_2 + self.invoice_line_3 + self.invoice_line_1, - 'model': self.rule_1, - }, - self.cash_line_1: {}, - }) - self.rule_1.match_nature = 'amount_paid' - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - - def test_matching_fields_match_amount(self): - self.rule_1.match_text_location_label = False - self.rule_1.match_amount = 'lower' - self.rule_1.match_amount_max = 150 - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, - self.bank_line_2: {}, - self.cash_line_1: {}, - }) - self.rule_1.match_amount = 'greater' - self.rule_1.match_amount_min = 200 - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - self.rule_1.match_amount = 'between' - self.rule_1.match_amount_min = 200 - self.rule_1.match_amount_max = 800 - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1}, - self.cash_line_1: {}, - }) - - def test_matching_fields_match_label(self): - self.rule_1.match_text_location_label = False - self.rule_1.match_label = 'contains' - self.rule_1.match_label_param = 'yyyyy' - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - self.rule_1.match_label = 'not_contains' - self.rule_1.match_label_param = 'xxxxx' - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, - self.bank_line_2: {}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - self.rule_1.match_label = 'match_regex' - self.rule_1.match_label_param = 'xxxxx|yyyyy' - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - - @freeze_time('2019-01-01') - def test_zero_payment_tolerance(self): - rule = self._create_reconcile_model(line_ids=[{}]) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - - invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01') - - # Exact matching. - st_line = self._create_st_line(amount=bsl_sign * 1000.0, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule}}, - ) - - # No matching because there is no tolerance. - st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {}}, - ) - - # The payment amount is higher than the invoice one. - st_line = self._create_st_line(amount=bsl_sign * 1010.0, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule}}, - ) - - @freeze_time('2019-01-01') - def test_zero_payment_tolerance_auto_reconcile(self): - rule = self._create_reconcile_model( - auto_reconcile=True, - match_text_location_label = False, - line_ids=[{}], - ) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - - invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01') - - # No matching because there is no tolerance. - st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456') - self._check_statement_matching( - rule, - {st_line: {}}, - ) - - # The payment amount is higher than the invoice one. - st_line = self._create_st_line(amount=bsl_sign * 1010.0, payment_ref='123456') - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule}}, - ) - - @freeze_time('2019-01-01') - def test_not_enough_payment_tolerance(self): - rule = self._create_reconcile_model( - payment_tolerance_param=0.5, - line_ids=[{}], - ) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - with self.subTest(inv_type=inv_type, bsl_sign=bsl_sign): - - invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01') - - # No matching because there is no enough tolerance. - st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {}}, - ) - - # The payment amount is higher than the invoice one. - # However, since the invoice amount is lower than the payment amount, - # the tolerance is not checked and the invoice line is matched. - st_line = self._create_st_line(amount=bsl_sign * 1010.0, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule}}, - ) - - @freeze_time('2019-01-01') - def test_enough_payment_tolerance(self): - rule = self._create_reconcile_model( - payment_tolerance_param=2.0, - line_ids=[{}], - ) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - - invl = self._create_invoice_line(1210.0, self.partner_a, inv_type, inv_date='2019-01-01') - - # Enough tolerance to match the invoice line. - st_line = self._create_st_line(amount=bsl_sign * 1185.80, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule, 'status': 'write_off'}}, - ) - - # The payment amount is higher than the invoice one. - # However, since the invoice amount is lower than the payment amount, - # the tolerance is not checked and the invoice line is matched. - st_line = self._create_st_line(amount=bsl_sign * 1234.20, payment_ref=invl.name) - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule}}, - ) - - @freeze_time('2019-01-01') - def test_enough_payment_tolerance_auto_reconcile_not_full(self): - rule = self._create_reconcile_model( - payment_tolerance_param=1.0, - auto_reconcile=True, - match_text_location_label = False, - line_ids=[{'amount_type': 'percentage_st_line', 'amount_string': '200.0'}], - ) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - - invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01') - - # Enough tolerance to match the invoice line. - st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456') - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule, 'status': 'write_off'}}, - ) - - @freeze_time('2019-01-01') - def test_allow_payment_tolerance_lower_amount(self): - rule = self._create_reconcile_model(line_ids=[{'amount_type': 'percentage_st_line'}]) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - - invl = self._create_invoice_line(990.0, self.partner_a, inv_type, inv_date='2019-01-01') - st_line = self._create_st_line(amount=bsl_sign * 1000, payment_ref=invl.name) - - # Partial reconciliation. - self._check_statement_matching( - rule, - {st_line: {'amls': invl, 'model': rule}}, - ) - - @freeze_time('2019-01-01') - def test_enough_payment_tolerance_auto_reconcile(self): - rule = self._create_reconcile_model( - payment_tolerance_param=1.0, - auto_reconcile=True, - match_text_location_label = False, - line_ids=[{}], - ) - - for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)): - - invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01') - - # Enough tolerance to match the invoice line. - st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456') - self._check_statement_matching( - rule, - {st_line: { - 'amls': invl, - 'model': rule, - 'status': 'write_off', - }}, - ) - - @freeze_time('2019-01-01') - def test_percentage_st_line_auto_reconcile(self): - rule = self._create_reconcile_model( - payment_tolerance_param=1.0, - rule_type='writeoff_suggestion', - auto_reconcile=True, - line_ids=[ - {'amount_type': 'percentage_st_line', 'amount_string': '100.0', 'label': 'A'}, - {'amount_type': 'percentage_st_line', 'amount_string': '-100.0', 'label': 'B'}, - {'amount_type': 'percentage_st_line', 'amount_string': '100.0', 'label': 'C'}, - ], - ) - - for bsl_sign in (1, -1): - st_line = self._create_st_line(amount=bsl_sign * 1000.0) - self._check_statement_matching( - rule, - {st_line: { - 'model': rule, - 'status': 'write_off', - 'auto_reconcile': True, - }}, - ) - - def test_matching_fields_match_partner_category_ids(self): - self.rule_1.match_text_location_label = False - test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'}) - test_category2 = self.env['res.partner.category'].create({'name': 'Consulting Services2'}) - - self.partner_2.category_id = test_category + test_category2 - self.rule_1.match_partner_category_ids |= test_category - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: {}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - self.rule_1.match_partner_category_ids = False - - def test_mixin_rules(self): - ''' Test usage of rules together.''' - self.rule_1.match_text_location_label = False - # rule_1 is used before rule_2. - self.rule_1.sequence = 1 - self.rule_2.sequence = 2 - - self._check_statement_matching(self.rule_1 + self.rule_2, { - self.bank_line_1: { - 'amls': self.invoice_line_1, - 'model': self.rule_1, - }, - self.bank_line_2: { - 'amls': self.invoice_line_2 + self.invoice_line_3 + self.invoice_line_1, - 'model': self.rule_1, - }, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - - # rule_2 is used before rule_1. - self.rule_1.sequence = 2 - self.rule_2.sequence = 1 - - self._check_statement_matching(self.rule_1 + self.rule_2, { - self.bank_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'}, - self.bank_line_2: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'}, - self.cash_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'}, - }) - - # rule_2 is used before rule_1 but only on partner_1. - self.rule_2.match_partner_ids |= self.partner_1 - - self._check_statement_matching(self.rule_1 + self.rule_2, { - self.bank_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'}, - self.bank_line_2: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'}, - self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1}, - }) - - def test_auto_reconcile(self): - ''' Test auto reconciliation.''' - self.bank_line_1.amount += 5 - - self.rule_1.sequence = 2 - self.rule_1.auto_reconcile = True - self.rule_1.payment_tolerance_param = 10.0 - self.rule_1.match_text_location_label = False - self.rule_2.sequence = 1 - self.rule_2.match_partner_ids |= self.partner_2 - self.rule_2.auto_reconcile = True - - self._check_statement_matching(self.rule_1 + self.rule_2, { - self.bank_line_1: { - 'amls': self.invoice_line_1, - 'model': self.rule_1, - 'auto_reconcile': True, - }, - self.bank_line_2: { - 'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, - 'model': self.rule_1, - }, - self.cash_line_1: { - 'model': self.rule_2, - 'status': 'write_off', - 'auto_reconcile': True, - }, - }) - - def test_auto_reconcile_ref_with_spaces(self): - space_in_ref_invoice_line = self._create_invoice_line(600, self.partner_3, 'out_invoice', ref="This ref has spaces") - space_in_ref_bank_line = self._create_st_line( - amount=600.0, - date='2020-01-01', - payment_ref="This ref has spaces", - partner_id= self.partner_3.id, - ) - self.rule_1.auto_reconcile = True - - self._check_statement_matching(self.rule_1, { - space_in_ref_bank_line: { - 'model': self.rule_1, - 'auto_reconcile': True, - 'amls': space_in_ref_invoice_line - } - }) - - def test_larger_invoice_auto_reconcile(self): - ''' Test auto reconciliation with an invoice with larger amount than the - statement line's, for rules without write-offs.''' - self.bank_line_1.amount = 40 - self.invoice_line_1.move_id.payment_reference = self.bank_line_1.payment_ref - - self.rule_1.sequence = 2 - self.rule_1.allow_payment_tolerance = False - self.rule_1.auto_reconcile = True - self.rule_1.line_ids = [(5, 0, 0)] - self.rule_1.match_text_location_label = False - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: { - 'amls': self.invoice_line_1, - 'model': self.rule_1, - 'auto_reconcile': True, - }, - self.bank_line_2: { - 'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, - 'model': self.rule_1, - }, - }) - - def test_auto_reconcile_with_tax(self): - ''' Test auto reconciliation with a tax amount included in the bank statement line''' - self.rule_1.write({ - 'auto_reconcile': True, - 'rule_type': 'writeoff_suggestion', - 'line_ids': [(1, self.rule_1.line_ids.id, { - 'amount': 50, - 'force_tax_included': True, - 'tax_ids': [(6, 0, self.tax21.ids)], - }), (0, 0, { - 'amount': 100, - 'force_tax_included': False, - 'tax_ids': [(6, 0, self.tax12.ids)], - 'account_id': self.current_assets_account.id, - })] - }) - - self.bank_line_1.amount = -121 - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True}, - self.bank_line_2: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True}, - }) - - def test_auto_reconcile_with_tax_fpos(self): - """ Test the fiscal positions are applied by reconcile models when using taxes. - """ - self.rule_1.write({ - 'auto_reconcile': True, - 'rule_type': 'writeoff_suggestion', - 'line_ids': [(1, self.rule_1.line_ids.id, { - 'amount': 100, - 'force_tax_included': True, - 'tax_ids': [(6, 0, self.tax21.ids)], - })] - }) - - self.partner_1.country_id = self.env.ref('base.lu') - belgium = self.env.ref('base.be') - self.partner_2.country_id = belgium - - self.bank_line_2.partner_id = self.partner_2 - - self.bank_line_1.amount = -121 - self.bank_line_2.amount = -112 - - self.env['account.fiscal.position'].create({ - 'name': "Test", - 'country_id': belgium.id, - 'auto_apply': True, - 'tax_ids': [ - Command.create({ - 'tax_src_id': self.tax21.id, - 'tax_dest_id': self.tax12.id, - }), - ] - }) - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True}, - self.bank_line_2: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True}, - }) - - def test_reverted_move_matching(self): - partner = self.partner_1 - AccountMove = self.env['account.move'] - move = AccountMove.create({ - 'journal_id': self.bank_journal.id, - 'line_ids': [ - (0, 0, { - 'account_id': self.account_pay.id, - 'partner_id': partner.id, - 'name': 'One of these days', - 'debit': 10, - }), - (0, 0, { - 'account_id': self.inbound_payment_method_line.payment_account_id.id, - 'partner_id': partner.id, - 'name': 'I\'m gonna cut you into little pieces', - 'credit': 10, - }) - ], - }) - - payment_bnk_line = move.line_ids.filtered(lambda l: l.account_id == self.inbound_payment_method_line.payment_account_id) - - move.action_post() - move_reversed = move._reverse_moves() - self.assertTrue(move_reversed.exists()) - - self.rule_1.match_text_location_label = False - self.bank_line_1.write({ - 'payment_ref': '8', - 'partner_id': partner.id, - 'amount': -10, - }) - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': payment_bnk_line, 'model': self.rule_1}, - self.bank_line_2: { - 'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, - 'model': self.rule_1, - }, - }) - - def test_match_different_currencies(self): - partner = self.env['res.partner'].create({'name': 'Bernard Gagnant'}) - self.rule_1.write({'match_partner_ids': [(6, 0, partner.ids)], 'match_same_currency': False}) - - currency_inv = self.env.ref('base.EUR') - currency_inv.active = True - currency_statement = self.env.ref('base.JPY') - - currency_statement.active = True - - invoice_line = self._create_invoice_line(100, partner, 'out_invoice', currency=currency_inv) - - self.bank_line_1.write({'partner_id': partner.id, 'foreign_currency_id': currency_statement.id, 'amount_currency': 100, 'payment_ref': invoice_line.name}) - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': invoice_line, 'model': self.rule_1}, - self.bank_line_2: {}, - }) - - def test_invoice_matching_rule_no_partner(self): - """ Tests that a statement line without any partner can be matched to the - right invoice if they have the same payment reference. - """ - self.invoice_line_1.move_id.write({'payment_reference': 'Tournicoti66'}) - self.rule_1.allow_payment_tolerance = False - - self.bank_line_1.write({ - 'payment_ref': 'Tournicoti66', - 'partner_id': None, - 'amount': 95, - }) - - self.rule_1.write({ - 'line_ids': [(5, 0, 0)], - 'match_partner': False, - 'match_label': 'contains', - 'match_label_param': 'Tournicoti', # So that we only match what we want to test - }) - - # TODO: 'invoice_line_1' has no reason to match 'bank_line_1' here... to check - # self._check_statement_matching(self.rule_1, { - # self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1}, - # self.bank_line_2: {'amls': []}, - # }, self.bank_st) - - def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self): - self.invoice_line_1.move_id.ref = "doudlidou3555" - - self.bank_line_1.write({ - 'payment_ref': 'doudlidou3555', - 'partner_id': None, - 'amount': 95, - }) - - self.rule_1.write({ - 'match_partner': False, - 'match_label': 'contains', - 'match_label_param': 'doudlidou', # So that we only match what we want to test - 'payment_tolerance_param': 10.0, - 'auto_reconcile': True, - }) - - # Check bank reconciliation - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: { - 'amls': self.invoice_line_1, - 'model': self.rule_1, - 'status': 'write_off', - 'auto_reconcile': True, - }, - self.bank_line_2: {}, - }) - - def test_partner_mapping_rule(self): - st_line = self._create_st_line(partner_id=None, payment_ref=None) - - rule = self._create_reconcile_model( - partner_mapping_line_ids=[{ - 'partner_id': self.partner_1.id, - 'payment_ref_regex': 'toto.*', - }], - ) - - # No match because the reference is not matching the regex. - self.assertEqual(st_line._retrieve_partner(), self.env['res.partner']) - - st_line.payment_ref = "toto42" - - # Matching using the regex on payment_ref. - self.assertEqual(st_line._retrieve_partner(), self.partner_1) - - rule.partner_mapping_line_ids.narration_regex = ".*coincoin" - - # No match because the narration is not matching the regex. - self.assertEqual(st_line._retrieve_partner(), self.env['res.partner']) - - st_line.narration = "42coincoin" - - # Matching is back thanks to "coincoin". - self.assertEqual(st_line._retrieve_partner(), self.partner_1) - - # More complex matching to match something from bank sync data. - # Note: the indentation is done with multiple \n to mimic the bank sync behavior. Keep them for this test! - rule.partner_mapping_line_ids.narration_regex = ".*coincoin.*" - st_line.narration = """ - { - "informations": "coincoin turlututu tsoin tsoin", - } - """ - - # Same check with json data into the narration field. - self.assertEqual(st_line._retrieve_partner(), self.partner_1) - - def test_match_multi_currencies(self): - ''' Ensure the matching of candidates is made using the right statement line currency. - - In this test, the value of the statement line is 100 USD = 300 GOL = 900 DAR and we want to match two journal - items of: - - 100 USD = 200 GOL (= 600 DAR from the statement line point of view) - - 14 USD = 280 DAR - - Both journal items should be suggested to the user because they represents 98% of the statement line amount - (DAR). - ''' - partner = self.env['res.partner'].create({'name': 'Bernard Perdant'}) - - journal = self.env['account.journal'].create({ - 'name': 'test_match_multi_currencies', - 'code': 'xxxx', - 'type': 'bank', - 'currency_id': self.other_currency.id, - }) - - matching_rule = self.env['account.reconcile.model'].create({ - 'name': 'test_match_multi_currencies', - 'rule_type': 'invoice_matching', - 'match_partner': True, - 'match_partner_ids': [(6, 0, partner.ids)], - 'allow_payment_tolerance': True, - 'payment_tolerance_type': 'percentage', - 'payment_tolerance_param': 5.0, - 'match_same_currency': False, - 'company_id': self.company_data['company'].id, - 'past_months_limit': False, - 'match_text_location_label': False, - }) - - statement_line = self.env['account.bank.statement.line'].create({ - 'journal_id': journal.id, - 'date': '2016-01-01', - 'payment_ref': 'line', - 'partner_id': partner.id, - 'foreign_currency_id': self.other_currency_2.id, - 'amount': 300.0, # Rate is 3 GOL = 1 USD in 2016. - 'amount_currency': 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1. - }) - - move = self.env['account.move'].create({ - 'move_type': 'entry', - 'date': '2017-01-01', - 'journal_id': self.company_data['default_journal_misc'].id, - 'line_ids': [ - # Rate is 2 GOL = 1 USD in 2017. - # The statement line will consider this line equivalent to 600 DAR. - (0, 0, { - 'account_id': self.company_data['default_account_receivable'].id, - 'partner_id': partner.id, - 'currency_id': self.other_currency.id, - 'debit': 100.0, - 'credit': 0.0, - 'amount_currency': 200.0, - }), - # Rate is 20 GOL = 1 USD in 2017. - (0, 0, { - 'account_id': self.company_data['default_account_receivable'].id, - 'partner_id': partner.id, - 'currency_id': self.other_currency_2.id, - 'debit': 14.0, - 'credit': 0.0, - 'amount_currency': 280.0, - }), - # Line to balance the journal entry: - (0, 0, { - 'account_id': self.company_data['default_account_revenue'].id, - 'debit': 0.0, - 'credit': 114.0, - }), - ], - }) - move.action_post() - - move_line_1 = move.line_ids.filtered(lambda line: line.debit == 100.0) - move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0) - - self._check_statement_matching(matching_rule, { - statement_line: {'amls': move_line_1 + move_line_2, 'model': matching_rule} - }) - - @freeze_time('2020-01-01') - def test_matching_with_write_off_foreign_currency(self): - journal_foreign_curr = self.company_data['default_journal_bank'].copy() - journal_foreign_curr.currency_id = self.other_currency - - reco_model = self._create_reconcile_model( - auto_reconcile=True, - rule_type='writeoff_suggestion', - line_ids=[{ - 'amount_type': 'percentage', - 'amount': 100.0, - 'account_id': self.company_data['default_account_revenue'].id, - }], - ) - - st_line = self._create_st_line(amount=100.0, payment_ref='123456', journal_id=journal_foreign_curr.id) - self._check_statement_matching(reco_model, { - st_line: { - 'model': reco_model, - 'status': 'write_off', - 'auto_reconcile': True, - }, - }) - - def test_payment_similar_communications(self): - def create_payment_line(amount, memo, partner): - payment = self.env['account.payment'].create({ - 'amount': amount, - 'payment_type': 'inbound', - 'partner_type': 'customer', - 'partner_id': partner.id, - 'memo': memo, - 'destination_account_id': self.company_data['default_account_receivable'].id, - }) - payment.action_post() - - return payment.move_id.line_ids.filtered(lambda x: x.account_id.account_type not in {'asset_receivable', 'liability_payable'}) - - payment_partner = self.env['res.partner'].create({ - 'name': "Bernard Gagnant", - }) - - self.rule_1.match_partner_ids = [(6, 0, payment_partner.ids)] - - pmt_line_1 = create_payment_line(500, 'a1b2c3', payment_partner) - pmt_line_2 = create_payment_line(500, 'a1b2c3', payment_partner) - create_payment_line(500, 'd1e2f3', payment_partner) - - self.bank_line_1.write({ - 'amount': 1000, - 'payment_ref': 'a1b2c3', - 'partner_id': payment_partner.id, - }) - self.bank_line_2.unlink() - self.rule_1.allow_payment_tolerance = False - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {'amls': pmt_line_1 + pmt_line_2, 'model': self.rule_1, 'status': 'write_off'}, - }) - - def test_no_amount_check_keep_first(self): - """ In case the reconciliation model doesn't check the total amount of the candidates, - we still don't want to suggest more than are necessary to match the statement. - For example, if a statement line amounts to 250 and is to be matched with three invoices - of 100, 200 and 300 (retrieved in this order), only 100 and 200 should be proposed. - """ - self.rule_1.allow_payment_tolerance = False - self.rule_1.match_text_location_label = False - self.bank_line_2.amount = 250 - self.bank_line_1.partner_id = None - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: { - 'amls': self.invoice_line_1 + self.invoice_line_2, - 'model': self.rule_1, - 'status': 'write_off', - }, - }) - - def test_no_amount_check_exact_match(self): - """ If a reconciliation model finds enough candidates for a full reconciliation, - it should still check the following candidates, in case one of them exactly - matches the amount of the statement line. If such a candidate exist, all the - other ones are disregarded. - """ - self.rule_1.allow_payment_tolerance = False - self.rule_1.match_text_location_label = False - self.bank_line_2.amount = 300 - self.bank_line_1.partner_id = None - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: {}, - self.bank_line_2: { - 'amls': self.invoice_line_3, - 'model': self.rule_1, - 'status': 'write_off', - }, - }) - - @freeze_time('2019-01-01') - def test_invoice_matching_using_match_text_location(self): - @contextmanager - def rollback(): - savepoint = self.cr.savepoint() - yield - savepoint.rollback() - - rule = self._create_reconcile_model( - match_partner=False, - allow_payment_tolerance=False, - match_text_location_label=False, - match_text_location_reference=False, - match_text_location_note=False, - ) - st_line = self._create_st_line(amount=1000, partner_id=False) - invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'invoice_date': '2019-01-01', - 'invoice_line_ids': [Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 100, - })], - }) - invoice.action_post() - term_line = invoice.line_ids.filtered(lambda x: x.display_type == 'payment_term') - - # No match at all. - self.assertDictEqual( - rule._apply_rules(st_line, None), - {}, - ) - - with rollback(): - term_line.name = "1234" - st_line.payment_ref = "1234" - - # Matching if no checkbox checked. - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_line, 'model': rule}, - ) - - # No matching if other checkbox is checked. - rule.match_text_location_note = True - self.assertDictEqual( - rule._apply_rules(st_line, None), - {}, - ) - - with rollback(): - # Test Matching on exact_token. - term_line.name = "PAY-123" - st_line.payment_ref = "PAY-123" - - # Matching if no checkbox checked. - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_line, 'model': rule}, - ) - - with self.subTest(rule_field='match_text_location_label', st_line_field='payment_ref'): - with rollback(): - term_line.name = '' - st_line.payment_ref = '/?' - - # No exact matching when the term line name is an empty string - self.assertDictEqual( - rule._apply_rules(st_line, None), - {}, - ) - - for rule_field, st_line_field in ( - ('match_text_location_label', 'payment_ref'), - ('match_text_location_reference', 'ref'), - ('match_text_location_note', 'narration'), - ): - with self.subTest(rule_field=rule_field, st_line_field=st_line_field): - - with rollback(): - rule[rule_field] = True - st_line[st_line_field] = "123456" - term_line.name = "123456" - - # Matching if the corresponding flag is enabled. - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_line, 'model': rule}, - ) - - # It works also if the statement line contains the word. - st_line[st_line_field] = "payment for 123456 urgent!" - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_line, 'model': rule}, - ) - - # Not if the invoice has nothing in common even if numerical. - term_line.name = "78910" - self.assertDictEqual( - rule._apply_rules(st_line, None), - {}, - ) - - # Exact matching on a single word. - st_line[st_line_field] = "TURLUTUTU21" - term_line.name = "TURLUTUTU21" - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_line, 'model': rule}, - ) - - # No matching if not enough numerical values. - st_line[st_line_field] = "12" - term_line.name = "selling 3 apples, 2 tomatoes and 12kg of potatoes" - self.assertDictEqual( - rule._apply_rules(st_line, None), - {}, - ) - - invoice2 = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.partner_a.id, - 'invoice_date': '2019-01-01', - 'invoice_line_ids': [Command.create({ - 'product_id': self.product_a.id, - 'price_unit': 100, - })], - }) - invoice2.action_post() - term_lines = (invoice + invoice2).line_ids.filtered(lambda x: x.display_type == 'payment_term') - - # Matching multiple invoices. - rule.match_text_location_label = True - st_line.payment_ref = "paying invoices 1234 & 5678" - term_lines[0].name = "INV/1234" - term_lines[1].name = "INV/5678" - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_lines, 'model': rule}, - ) - - # Matching multiple invoices sharing the same reference. - term_lines[1].name = "INV/1234" - self.assertDictEqual( - rule._apply_rules(st_line, None), - {'amls': term_lines, 'model': rule}, - ) - - def test_amount_check_amount_last(self): - """ In case the reconciliation model can't match via text or partner matching - we do a last check to find amls with the exact amount - """ - self.rule_1.write({ - 'match_text_location_label': False, - 'match_partner': False, - 'match_partner_ids': [Command.clear()], - }) - self.bank_line_1.partner_id = None - self.bank_line_1.payment_ref = False - - self._check_statement_matching(self.rule_1, { - self.bank_line_1: { - 'amls': self.invoice_line_1, - 'model': self.rule_1, - }, - }) - - # Create bank statement in foreign currency - partner = self.env['res.partner'].create({'name': 'Bernard Gagnant'}) - invoice_line = self._create_invoice_line(300, partner, 'out_invoice', currency=self.other_currency_2) - bank_line_2 = self.env['account.bank.statement.line'].create({ - 'journal_id': self.bank_journal.id, - 'partner_id': False, - 'payment_ref': False, - 'foreign_currency_id': self.other_currency_2.id, - 'amount': 15.0, - 'amount_currency': 300.0, - }) - self._check_statement_matching(self.rule_1, { - bank_line_2: { - 'amls': invoice_line, - 'model': self.rule_1, - }, - }) - - @freeze_time('2019-01-01') - def test_matching_exact_amount_no_partner(self): - """ In case the reconciliation model can't match via text or partner matching - we do a last check to find amls with the exact amount. - """ - self.rule_1.write({ - 'match_text_location_label': False, - 'match_partner': False, - 'match_partner_ids': [Command.clear()], - }) - self.bank_line_1.partner_id = None - self.bank_line_1.payment_ref = False - - with self.subTest(test='single_currency'): - st_line = self._create_st_line(amount=100, payment_ref=None, partner_id=None) - invl = self._create_invoice_line(100, self.partner_1, 'out_invoice') - self._check_statement_matching(self.rule_1, { - st_line: { - 'amls': invl, - 'model': self.rule_1, - }, - }) - - with self.subTest(test='rounding'): - st_line = self._create_st_line(amount=-208.73, payment_ref=None, partner_id=None) - invl = self._create_invoice_line(208.73, self.partner_1, 'in_invoice') - self._check_statement_matching(self.rule_1, { - st_line: { - 'amls': invl, - 'model': self.rule_1, - }, - }) - - with self.subTest(test='multi_currencies'): - foreign_curr = self.other_currency_2 - invl = self._create_invoice_line(300, self.partner_1, 'out_invoice', currency=foreign_curr) - st_line = self._create_st_line( - amount=15.0, foreign_currency_id=foreign_curr.id, amount_currency=300.0, - payment_ref=None, partner_id=None, - ) - self._check_statement_matching(self.rule_1, { - st_line: { - 'amls': invl, - 'model': self.rule_1, - }, - }) diff --git a/addons/at_accounting/tests/test_reconciliation_widget.py b/addons/at_accounting/tests/test_reconciliation_widget.py deleted file mode 100644 index 4bae59b..0000000 --- a/addons/at_accounting/tests/test_reconciliation_widget.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon -from odoo.tests.common import tagged - - -@tagged("post_install", "-at_install") -class TestReconciliationWidget(ValuationReconciliationTestCommon): - - def test_no_stock_account_in_reconciliation_proposition(self): - """ - We check if no stock interim account is present in the reconcialiation proposition, - with both standard and custom stock accounts - """ - avco_1 = self.stock_account_product_categ.copy({'property_cost_method': 'average'}) - - # We need a product category with custom stock accounts - avco_2 = self.stock_account_product_categ.copy({ - 'property_cost_method': 'average', - 'property_stock_account_input_categ_id': self.company_data['default_account_stock_in'].copy().id, - 'property_stock_account_output_categ_id': self.company_data['default_account_stock_out'].copy().id, - 'property_stock_journal': avco_1.property_stock_journal.copy().id, - 'property_stock_valuation_account_id': self.company_data['default_account_stock_valuation'].copy().id - }) - - move_1, move_2 = self.env['account.move'].create([ - { - 'move_type': 'entry', - 'name': 'Entry 1', - 'journal_id': avco_1.property_stock_journal.id, - 'line_ids': [ - (0, 0, { - 'account_id': avco_1.property_stock_account_input_categ_id.id, - 'debit': 0.0, - 'credit': 100.0 - }), - (0, 0, { - 'account_id': avco_1.property_stock_valuation_account_id.id, - 'debit': 100.0, - 'credit': 0.0 - }) - ] - }, - { - 'move_type': 'entry', - 'name': 'Entry 2', - 'journal_id': avco_2.property_stock_journal.id, - 'line_ids': [ - (0, 0, { - 'account_id': avco_2.property_stock_account_input_categ_id.id, - 'debit': 0.0, - 'credit': 100.0 - }), - (0, 0, { - 'account_id': avco_2.property_stock_valuation_account_id.id, - 'debit': 100.0, - 'credit': 0.0 - }) - ] - }, - ]) - - (move_1 + move_2).action_post() - - statement = self.env['account.bank.statement'].create({ - 'balance_start': 0.0, - 'balance_end_real': -100.0, - 'line_ids': [(0, 0, { - 'payment_ref': 'test', - 'amount': -100.0, - 'journal_id': self.company_data['default_journal_bank'].id, - })] - }) - - wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=statement.line_ids.id).new({}) - amls = self.env['account.move.line'].search(wizard._prepare_embedded_views_data()['amls']['domain']) - stock_accounts = ( - avco_1.property_stock_account_input_categ_id + avco_2.property_stock_account_input_categ_id - + avco_1.property_stock_account_output_categ_id + avco_2.property_stock_account_output_categ_id - ) - stock_res = [line for line in amls if line.account_id in stock_accounts] - self.assertEqual(len(stock_res), 0) diff --git a/addons/at_accounting/tests/test_reevaluation_asset.py b/addons/at_accounting/tests/test_reevaluation_asset.py deleted file mode 100644 index 88237bb..0000000 --- a/addons/at_accounting/tests/test_reevaluation_asset.py +++ /dev/null @@ -1,1643 +0,0 @@ -from unittest.mock import patch -from odoo.tests.common import tagged, freeze_time -from odoo.addons.at_accounting.tests.common import TestAccountAssetCommon -from odoo import fields - - -@freeze_time('2022-06-30') -@tagged('post_install', '-at_install') -class TestAccountAssetReevaluation(TestAccountAssetCommon): - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.account_depreciation_expense = cls.company_data['default_account_assets'].copy() - cls.asset_counterpart_account_id = cls.company_data['default_account_expense'].copy() - cls.degressive_asset = cls.create_asset( - value=7200, - periodicity="monthly", - periods=60, - method="degressive", - method_progress_factor=0.35, - acquisition_date="2020-07-01", - prorata_computation_type="constant_periods" - ) - cls.degressive_then_linear_asset = cls.create_asset( - value=7200, - periodicity="monthly", - periods=60, - method="degressive_then_linear", - method_progress_factor=0.35, - acquisition_date="2020-07-01", - prorata_computation_type="constant_periods" - ) - - def test_linear_start_beginning_month_reevaluation_beginning_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-01", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-01"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=600, remaining_value=6600, depreciated_value=600, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6000, depreciated_value=1200, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5400, depreciated_value=1800, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=4800, depreciated_value=2400, state='posted'), - # 20 because we have 1 * 600 / 30 (1 day of a month of 30 days, with 600 per month) - self._get_depreciation_move_values(date='2022-06-01', depreciation_value=20, remaining_value=4780, depreciated_value=2420, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=580, remaining_value=4200, depreciated_value=3000, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=3600, depreciated_value=3600, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3000, depreciated_value=4200, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2400, depreciated_value=4800, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=1800, depreciated_value=5400, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1200, depreciated_value=6000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=600, depreciated_value=6600, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_beginning_month_reevaluation_middle_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-01", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-15") - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=600, remaining_value=6600, depreciated_value=600, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6000, depreciated_value=1200, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5400, depreciated_value=1800, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=4800, depreciated_value=2400, state='posted'), - # 300 because we have 15 * 600 / 30 (15 days of a month of 30 days, with 600 per month) - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=300, remaining_value=4500, depreciated_value=2700, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=300, remaining_value=4200, depreciated_value=3000, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=3600, depreciated_value=3600, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3000, depreciated_value=4200, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2400, depreciated_value=4800, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=1800, depreciated_value=5400, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1200, depreciated_value=6000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=600, depreciated_value=6600, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_beginning_month_reevaluation_end_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-01", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-30") - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=600, remaining_value=6600, depreciated_value=600, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6000, depreciated_value=1200, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5400, depreciated_value=1800, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=4800, depreciated_value=2400, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=600, remaining_value=4200, depreciated_value=3000, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=3600, depreciated_value=3600, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3000, depreciated_value=4200, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2400, depreciated_value=4800, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=1800, depreciated_value=5400, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1200, depreciated_value=6000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=600, depreciated_value=6600, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_middle_month_reevaluation_beginning_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-15", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-01"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=300, remaining_value=6900, depreciated_value=300, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6300, depreciated_value=900, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5700, depreciated_value=1500, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=5100, depreciated_value=2100, state='posted'), - # 20 because we have 1 * 600 / 30 (1 day of a month of 30 days, with 600 per month) - self._get_depreciation_move_values(date='2022-06-01', depreciation_value=20, remaining_value=5080, depreciated_value=2120, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=580, remaining_value=4500, depreciated_value=2700, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=3900, depreciated_value=3300, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3300, depreciated_value=3900, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2700, depreciated_value=4500, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=2100, depreciated_value=5100, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1500, depreciated_value=5700, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=900, depreciated_value=6300, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=300, depreciated_value=6900, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=300, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_middle_month_reevaluation_middle_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-15", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-15"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=300, remaining_value=6900, depreciated_value=300, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6300, depreciated_value=900, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5700, depreciated_value=1500, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=5100, depreciated_value=2100, state='posted'), - # 300 because we have 15 * 600 / 30 (15 days of a month of 30 days, with 600 per month) - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=300, remaining_value=4800, depreciated_value=2400, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=300, remaining_value=4500, depreciated_value=2700, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=3900, depreciated_value=3300, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3300, depreciated_value=3900, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2700, depreciated_value=4500, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=2100, depreciated_value=5100, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1500, depreciated_value=5700, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=900, depreciated_value=6300, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=300, depreciated_value=6900, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=300, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_middle_month_reevaluation_end_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-15", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-30"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=300, remaining_value=6900, depreciated_value=300, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6300, depreciated_value=900, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5700, depreciated_value=1500, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=5100, depreciated_value=2100, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=600, remaining_value=4500, depreciated_value=2700, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=3900, depreciated_value=3300, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3300, depreciated_value=3900, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2700, depreciated_value=4500, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=2100, depreciated_value=5100, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1500, depreciated_value=5700, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=900, depreciated_value=6300, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=300, depreciated_value=6900, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=300, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_end_month_reevaluation_beginning_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-28", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-01"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=21.43, remaining_value=7178.57, depreciated_value=21.43, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6578.57, depreciated_value=621.43, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5978.57, depreciated_value=1221.43, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=5378.57, depreciated_value=1821.43, state='posted'), - # 20 because we have 1 * 600 / 30 (1 day of a month of 30 days, with 600 per month) - self._get_depreciation_move_values(date='2022-06-01', depreciation_value=20, remaining_value=5358.57, depreciated_value=1841.43, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=580, remaining_value=4778.57, depreciated_value=2421.43, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=4178.57, depreciated_value=3021.43, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3578.57, depreciated_value=3621.43, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2978.57, depreciated_value=4221.43, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=2378.57, depreciated_value=4821.43, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1778.57, depreciated_value=5421.43, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=1178.57, depreciated_value=6021.43, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=578.57, depreciated_value=6621.43, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=578.57, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_end_month_reevaluation_middle_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-28", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-15"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=21.43, remaining_value=7178.57, depreciated_value=21.43, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6578.57, depreciated_value=621.43, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5978.57, depreciated_value=1221.43, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=5378.57, depreciated_value=1821.43, state='posted'), - # 300 because we have 15 * 600 / 30 (15 days of a month of 30 days, with 600 per month) - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=300, remaining_value=5078.57, depreciated_value=2121.43, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=300, remaining_value=4778.57, depreciated_value=2421.43, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=4178.57, depreciated_value=3021.43, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3578.57, depreciated_value=3621.43, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2978.57, depreciated_value=4221.43, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=2378.57, depreciated_value=4821.43, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1778.57, depreciated_value=5421.43, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=1178.57, depreciated_value=6021.43, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=578.57, depreciated_value=6621.43, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=578.57, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_start_end_month_reevaluation_end_month(self): - asset = self.create_asset(value=7200, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-02-28", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-30"), - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=21.43, remaining_value=7178.57, depreciated_value=21.43, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=600, remaining_value=6578.57, depreciated_value=621.43, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=600, remaining_value=5978.57, depreciated_value=1221.43, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=600, remaining_value=5378.57, depreciated_value=1821.43, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=600, remaining_value=4778.57, depreciated_value=2421.43, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=600, remaining_value=4178.57, depreciated_value=3021.43, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=600, remaining_value=3578.57, depreciated_value=3621.43, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=600, remaining_value=2978.57, depreciated_value=4221.43, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=600, remaining_value=2378.57, depreciated_value=4821.43, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=600, remaining_value=1778.57, depreciated_value=5421.43, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=600, remaining_value=1178.57, depreciated_value=6021.43, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=600, remaining_value=578.57, depreciated_value=6621.43, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=578.57, remaining_value=0, depreciated_value=7200, state='draft'), - ]) - - def test_linear_reevaluation_simple_decrease(self): - asset = self.create_asset(value=10000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-30"), - 'value_residual': 4000, # -1000 - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=833.33, remaining_value=9166.67, depreciated_value=833.33, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=833.34, remaining_value=8333.33, depreciated_value=1666.67, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=833.33, remaining_value=7500, depreciated_value=2500, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=833.33, remaining_value=6666.67, depreciated_value=3333.33, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=833.34, remaining_value=5833.33, depreciated_value=4166.67, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=833.33, remaining_value=5000, depreciated_value=5000, state='posted'), - # decrease move - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=4000, depreciated_value=6000, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=666.67, remaining_value=3333.33, depreciated_value=6666.67, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=666.66, remaining_value=2666.67, depreciated_value=7333.33, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=666.67, remaining_value=2000, depreciated_value=8000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=666.67, remaining_value=1333.33, depreciated_value=8666.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=666.66, remaining_value=666.67, depreciated_value=9333.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=666.67, remaining_value=0, depreciated_value=10000, state='draft'), - ]) - - def test_linear_reevaluation_double_decrease(self): - asset = self.create_asset(value=60000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) - 8500, - }).modify() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-30"), - 'value_residual': 18000, # -6000 - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=5000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=5000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=2500, remaining_value=42500, depreciated_value=17500, state='posted'), - # decrease move - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=8500, remaining_value=34000, depreciated_value=26000, state='posted'), - - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=2000, remaining_value=32000, depreciated_value=28000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=4000, remaining_value=28000, depreciated_value=32000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=4000, remaining_value=24000, depreciated_value=36000, state='posted'), - # decrease move - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=6000, remaining_value=18000, depreciated_value=42000, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=3000, remaining_value=15000, depreciated_value=45000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=3000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=3000, remaining_value=9000, depreciated_value=51000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=3000, remaining_value=6000, depreciated_value=54000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=3000, remaining_value=3000, depreciated_value=57000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=3000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_linear_reevaluation_double_increase(self): - asset = self.create_asset(value=60000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify_1 = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_1, - 'value_residual': asset._get_residual_value_at_date(date_modify_1) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - date_modify_2 = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_2, - 'value_residual': asset._get_residual_value_at_date(date_modify_2) + 6000, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=5000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=5000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=2500, remaining_value=42500, depreciated_value=17500, state='posted'), - - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=2500, remaining_value=40000, depreciated_value=20000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=5000, remaining_value=35000, depreciated_value=25000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=5000, remaining_value=30000, depreciated_value=30000, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=5000, remaining_value=25000, depreciated_value=35000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=5000, remaining_value=20000, depreciated_value=40000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=5000, remaining_value=15000, depreciated_value=45000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=5000, remaining_value=10000, depreciated_value=50000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=5000, remaining_value=5000, depreciated_value=55000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=5000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=500, remaining_value=8000, depreciated_value=500, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=7000, depreciated_value=1500, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=6000, depreciated_value=2500, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=5000, depreciated_value=3500, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=4000, depreciated_value=4500, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=3000, depreciated_value=5500, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=2000, depreciated_value=6500, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=1000, depreciated_value=7500, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=0, depreciated_value=8500, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[1].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=5000, depreciated_value=1000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=4000, depreciated_value=2000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=3000, depreciated_value=3000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=2000, depreciated_value=4000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=1000, depreciated_value=5000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=0, depreciated_value=6000, state='draft'), - ]) - - def test_linear_reevaluation_decrease_then_increase(self): - asset = self.create_asset(value=60000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify_1 = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_1, - 'value_residual': asset._get_residual_value_at_date(date_modify_1) - 8500, - }).modify() - - date_modify_2 = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_2, - 'value_residual': asset._get_residual_value_at_date(date_modify_2) + 6000, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=5000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=5000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=2500, remaining_value=42500, depreciated_value=17500, state='posted'), - # decrease move - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=8500, remaining_value=34000, depreciated_value=26000, state='posted'), - - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=2000, remaining_value=32000, depreciated_value=28000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=4000, remaining_value=28000, depreciated_value=32000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=4000, remaining_value=24000, depreciated_value=36000, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=4000, remaining_value=20000, depreciated_value=40000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=4000, remaining_value=16000, depreciated_value=44000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=4000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=4000, remaining_value=8000, depreciated_value=52000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=4000, remaining_value=4000, depreciated_value=56000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=4000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=5000, depreciated_value=1000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=4000, depreciated_value=2000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=3000, depreciated_value=3000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=2000, depreciated_value=4000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=1000, depreciated_value=5000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=0, depreciated_value=6000, state='draft'), - ]) - - def test_linear_reevaluation_increase_then_decrease_in_future(self): - asset = self.create_asset(value=10000, periodicity="yearly", periods=5, method="linear", acquisition_date="2018-01-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify_1 = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_1, - 'value_residual': asset._get_residual_value_at_date(date_modify_1) + 1000, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - date_modify_2 = fields.Date.to_date("2022-09-30") # This is 3 month in the future - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_2, - 'value_residual': asset._get_residual_value_at_date(date_modify_2) - 200, - 'method_period': '1', # to reflect the change on the child, we go in monthly - 'method_number': 60, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2018-12-31', depreciation_value=2000, remaining_value=8000, depreciated_value=2000, state='posted'), - self._get_depreciation_move_values(date='2019-12-31', depreciation_value=2000, remaining_value=6000, depreciated_value=4000, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=2000, remaining_value=4000, depreciated_value=6000, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=2000, remaining_value=2000, depreciated_value=8000, state='posted'), - # move before increase - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=1000, depreciated_value=9000, state='posted'), - # move before decrease - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=500, remaining_value=500, depreciated_value=9500, state='draft'), - # decrease move - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=200, remaining_value=300, depreciated_value=9700, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=100, remaining_value=200, depreciated_value=9800, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=100, remaining_value=100, depreciated_value=9900, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=100, remaining_value=0, depreciated_value=10000, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - # move before switch to monthly - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=500, remaining_value=500, depreciated_value=500, state='draft'), - - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=166.67, remaining_value=333.33, depreciated_value=666.67, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=166.66, remaining_value=166.67, depreciated_value=833.33, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=166.67, remaining_value=0, depreciated_value=1000, state='draft'), - ]) - - def test_linear_reevaluation_decrease_then_increase_with_lock_date(self): - self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2022-03-01') - asset = self.create_asset(value=60000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify_1 = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_1, - 'value_residual': asset._get_residual_value_at_date(date_modify_1) - 8500, - }).modify() - - self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2022-05-01') - - date_modify_2 = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_2, - 'value_residual': asset._get_residual_value_at_date(date_modify_2) + 6000, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=2500, remaining_value=42500, depreciated_value=17500, state='posted'), - # decrease move - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=8500, remaining_value=34000, depreciated_value=26000, state='posted'), - - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=2000, remaining_value=32000, depreciated_value=28000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=4000, remaining_value=28000, depreciated_value=32000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=4000, remaining_value=24000, depreciated_value=36000, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=4000, remaining_value=20000, depreciated_value=40000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=4000, remaining_value=16000, depreciated_value=44000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=4000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=4000, remaining_value=8000, depreciated_value=52000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=4000, remaining_value=4000, depreciated_value=56000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=4000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=5000, depreciated_value=1000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=4000, depreciated_value=2000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=3000, depreciated_value=3000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=2000, depreciated_value=4000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=1000, depreciated_value=5000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=0, depreciated_value=6000, state='draft'), - ]) - - def test_linear_reevaluation_increase_then_decrease(self): - asset = self.create_asset(value=60000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify_1 = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_1, - 'value_residual': asset._get_residual_value_at_date(date_modify_1) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - date_modify_2 = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify_2, - 'value_residual': asset._get_residual_value_at_date(date_modify_2) - 6000, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=5000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=5000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=2500, remaining_value=42500, depreciated_value=17500, state='posted'), - - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=2500, remaining_value=40000, depreciated_value=20000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=5000, remaining_value=35000, depreciated_value=25000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=5000, remaining_value=30000, depreciated_value=30000, state='posted'), - - # decrease move - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=6000, remaining_value=24000, depreciated_value=36000, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=4000, remaining_value=20000, depreciated_value=40000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=4000, remaining_value=16000, depreciated_value=44000, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=4000, remaining_value=12000, depreciated_value=48000, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=4000, remaining_value=8000, depreciated_value=52000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=4000, remaining_value=4000, depreciated_value=56000, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=4000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=500, remaining_value=8000, depreciated_value=500, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=7000, depreciated_value=1500, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=6000, depreciated_value=2500, state='posted'), - - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=5000, depreciated_value=3500, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=1000, remaining_value=4000, depreciated_value=4500, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=1000, remaining_value=3000, depreciated_value=5500, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=1000, remaining_value=2000, depreciated_value=6500, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=1000, remaining_value=1000, depreciated_value=7500, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=1000, remaining_value=0, depreciated_value=8500, state='draft'), - ]) - - def test_linear_reevaluation_decrease_then_disposal(self): - asset = self.create_asset(value=60000, periodicity="monthly", periods=12, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - self.loss_account_id = self.company_data['default_account_expense'].copy().id - - date_modify = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) - 8500, - }).modify() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'date': fields.Date.to_date("2022-06-30"), - 'modify_action': 'dispose', - 'loss_account_id': self.loss_account_id, - }).sell_dispose() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=5000, remaining_value=55000, depreciated_value=5000, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=5000, remaining_value=50000, depreciated_value=10000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=5000, remaining_value=45000, depreciated_value=15000, state='posted'), - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=2500, remaining_value=42500, depreciated_value=17500, state='posted'), - # decrease move - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=8500, remaining_value=34000, depreciated_value=26000, state='posted'), - - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=2000, remaining_value=32000, depreciated_value=28000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=4000, remaining_value=28000, depreciated_value=32000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=4000, remaining_value=24000, depreciated_value=36000, state='posted'), - - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=24000, remaining_value=0, depreciated_value=60000, state='draft'), - ]) - - def test_linear_reevaluation_increase_then_disposal(self): - asset = self.create_asset(value=36000, periodicity="yearly", periods=3, method="linear", acquisition_date="2022-01-01", prorata_computation_type="constant_periods") - asset.validate() - self.loss_account_id = self.company_data['default_account_expense'].copy().id - self.asset_counterpart_account_id = self.company_data['default_account_expense'].copy().id - - date_modify = fields.Date.to_date("2022-04-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id, - }).modify() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'date': fields.Date.to_date("2022-06-30"), - 'modify_action': 'dispose', - 'loss_account_id': self.loss_account_id, - }).sell_dispose() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-04-15', depreciation_value=3500, remaining_value=32500, depreciated_value=3500, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=2500, remaining_value=30000, depreciated_value=6000, state='posted'), - # disposal move - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=30000, remaining_value=0, depreciated_value=36000, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - # 653.85 = 8500 * (2.5 months * 30) / (32.5 months * 30) - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=653.85, remaining_value=7846.15, depreciated_value=653.85, state='posted'), - # disposal move - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=7846.15, remaining_value=0, depreciated_value=8500, state='draft'), - ]) - - def test_linear_reevaluation_increase_constant_periods(self): - asset = self.create_asset(value=1200, periodicity="monthly", periods=12, method="linear", acquisition_date="2021-10-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify = fields.Date.to_date("2022-01-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'modify_action': 'modify', - 'value_residual': asset._get_residual_value_at_date(date_modify) + 2100, - 'account_asset_counterpart_id': self.company_data['default_account_revenue'].copy().id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=100, remaining_value=1100, depreciated_value=100, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=100, remaining_value=1000, depreciated_value=200, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=100, remaining_value=900, depreciated_value=300, state='posted'), - self._get_depreciation_move_values(date='2022-01-15', depreciation_value=48.39, remaining_value=851.61, depreciated_value=348.39, state='posted'), - - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=51.61, remaining_value=800, depreciated_value=400, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=100, remaining_value=700, depreciated_value=500, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=100, remaining_value=600, depreciated_value=600, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=100, remaining_value=500, depreciated_value=700, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=100, remaining_value=400, depreciated_value=800, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=100, remaining_value=300, depreciated_value=900, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=100, remaining_value=200, depreciated_value=1000, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=100, remaining_value=100, depreciated_value=1100, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=100, remaining_value=0, depreciated_value=1200, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=127.27, remaining_value=1972.73, depreciated_value=127.27, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=246.59, remaining_value=1726.14, depreciated_value=373.86, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=246.59, remaining_value=1479.55, depreciated_value=620.45, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=246.60, remaining_value=1232.95, depreciated_value=867.05, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=246.59, remaining_value=986.36, depreciated_value=1113.64, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=246.59, remaining_value=739.77, depreciated_value=1360.23, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=246.59, remaining_value=493.18, depreciated_value=1606.82, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=246.59, remaining_value=246.59, depreciated_value=1853.41, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=246.59, remaining_value=0, depreciated_value=2100, state='draft'), - ]) - - def test_linear_reevaluation_increase_daily_computation(self): - asset = self.create_asset(value=1200, periodicity="monthly", periods=12, method="linear", acquisition_date="2021-10-01", prorata_computation_type="daily_computation") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-01-15"), - 'modify_action': 'modify', - 'value_residual': 2945.75, - 'account_asset_counterpart_id': self.company_data['default_account_revenue'].copy().id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=101.92, remaining_value=1098.08, depreciated_value=101.92, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=98.63, remaining_value=999.45, depreciated_value=200.55, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=101.92, remaining_value=897.53, depreciated_value=302.47, state='posted'), - self._get_depreciation_move_values(date='2022-01-15', depreciation_value=49.31, remaining_value=848.22, depreciated_value=351.78, state='posted'), - - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=52.60, remaining_value=795.62, depreciated_value=404.38, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=92.06, remaining_value=703.56, depreciated_value=496.44, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=101.92, remaining_value=601.64, depreciated_value=598.36, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=98.63, remaining_value=503.01, depreciated_value=696.99, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=101.91, remaining_value=401.10, depreciated_value=798.90, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=98.63, remaining_value=302.47, depreciated_value=897.53, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=101.92, remaining_value=200.55, depreciated_value=999.45, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=101.92, remaining_value=98.63, depreciated_value=1101.37, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=98.63, remaining_value=0, depreciated_value=1200, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=130.08, remaining_value=1967.45, depreciated_value=130.08, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=227.64, remaining_value=1739.81, depreciated_value=357.72, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=252.03, remaining_value=1487.78, depreciated_value=609.75, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=243.90, remaining_value=1243.88, depreciated_value=853.65, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=252.02, remaining_value=991.86, depreciated_value=1105.67, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=243.90, remaining_value=747.96, depreciated_value=1349.57, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=252.03, remaining_value=495.93, depreciated_value=1601.60, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=252.03, remaining_value=243.90, depreciated_value=1853.63, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=243.90, remaining_value=0, depreciated_value=2097.53, state='draft'), - ]) - - def test_linear_reevaluation_increase_amount_and_length(self): - """ After 5 months, extend the lifetime by 3 month and the amount by 200 """ - asset = self.create_asset(value=1200, periodicity="monthly", periods=10, method="linear", acquisition_date="2022-02-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'method_number': 10 + 3, - 'date': date_modify, - 'modify_action': 'modify', - 'value_residual': asset._get_residual_value_at_date(date_modify) + 200, - 'account_asset_counterpart_id': self.company_data['default_account_revenue'].copy().id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=120, remaining_value=1080, depreciated_value=120, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=120, remaining_value=960, depreciated_value=240, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=120, remaining_value=840, depreciated_value=360, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=120, remaining_value=720, depreciated_value=480, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=120, remaining_value=600, depreciated_value=600, state='posted'), - # After the reeval, we divide the amount to depreciate left on the amount left - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=75, remaining_value=525, depreciated_value=675, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=75, remaining_value=450, depreciated_value=750, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=75, remaining_value=375, depreciated_value=825, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=75, remaining_value=300, depreciated_value=900, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=75, remaining_value=225, depreciated_value=975, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=75, remaining_value=150, depreciated_value=1050, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=75, remaining_value=75, depreciated_value=1125, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=75, remaining_value=0, depreciated_value=1200, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=25, remaining_value=175, depreciated_value=25, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=25, remaining_value=150, depreciated_value=50, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=25, remaining_value=125, depreciated_value=75, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=25, remaining_value=100, depreciated_value=100, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=25, remaining_value=75, depreciated_value=125, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=25, remaining_value=50, depreciated_value=150, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=25, remaining_value=25, depreciated_value=175, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=25, remaining_value=0, depreciated_value=200, state='draft'), - ]) - - def test_linear_reevaluation_decrease_amount_and_increase_length(self): - """ After 5 months, extend the lifetime by 3 month and reduce the amount by 200 """ - asset = self.create_asset(value=1200, periodicity="monthly", periods=10, method="linear", acquisition_date="2022-02-01", prorata_computation_type="constant_periods") - asset.validate() - - date_modify = fields.Date.to_date("2022-06-30") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'method_number': 10 + 3, - 'date': date_modify, - 'modify_action': 'modify', - 'value_residual': asset._get_residual_value_at_date(date_modify) - 200, - 'account_asset_counterpart_id': self.company_data['default_account_revenue'].copy().id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=120, remaining_value=1080, depreciated_value=120, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=120, remaining_value=960, depreciated_value=240, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=120, remaining_value=840, depreciated_value=360, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=120, remaining_value=720, depreciated_value=480, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=120, remaining_value=600, depreciated_value=600, state='posted'), - # Decrease Move - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=200, remaining_value=400, depreciated_value=800, state='posted'), - # After the reeval, we divide the amount to depreciate left on the amount left - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=50, remaining_value=350, depreciated_value=850, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=50, remaining_value=300, depreciated_value=900, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=50, remaining_value=250, depreciated_value=950, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=50, remaining_value=200, depreciated_value=1000, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=50, remaining_value=150, depreciated_value=1050, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=50, remaining_value=100, depreciated_value=1100, state='draft'), - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=50, remaining_value=50, depreciated_value=1150, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=50, remaining_value=0, depreciated_value=1200, state='draft'), - ]) - - def test_monthly_degressive_start_beginning_month_increase_middle_month_on_degressive_part(self): - asset = self.degressive_asset - asset.validate() - - date_modify = fields.Date.to_date("2022-06-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=210.00, remaining_value=6990.00, depreciated_value=210.00, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=210.00, remaining_value=6780.00, depreciated_value=420.00, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=210.00, remaining_value=6570.00, depreciated_value=630.00, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=210.00, remaining_value=6360.00, depreciated_value=840.00, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=210.00, remaining_value=6150.00, depreciated_value=1050.00, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=210.00, remaining_value=5940.00, depreciated_value=1260.00, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=173.25, remaining_value=5766.75, depreciated_value=1433.25, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=173.25, remaining_value=5593.50, depreciated_value=1606.50, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=173.25, remaining_value=5420.25, depreciated_value=1779.75, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=173.25, remaining_value=5247.00, depreciated_value=1953.00, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=173.25, remaining_value=5073.75, depreciated_value=2126.25, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=173.25, remaining_value=4900.50, depreciated_value=2299.50, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=173.25, remaining_value=4727.25, depreciated_value=2472.75, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=173.25, remaining_value=4554.00, depreciated_value=2646.00, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=173.25, remaining_value=4380.75, depreciated_value=2819.25, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=173.25, remaining_value=4207.50, depreciated_value=2992.50, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=173.25, remaining_value=4034.25, depreciated_value=3165.75, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=173.25, remaining_value=3861.00, depreciated_value=3339.00, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=112.61, remaining_value=3748.39, depreciated_value=3451.61, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=112.61, remaining_value=3635.78, depreciated_value=3564.22, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=112.62, remaining_value=3523.16, depreciated_value=3676.84, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=112.61, remaining_value=3410.55, depreciated_value=3789.45, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=112.61, remaining_value=3297.94, depreciated_value=3902.06, state='posted'), - # Increase - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=56.31, remaining_value=3241.63, depreciated_value=3958.37, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=47.27, remaining_value=3194.36, depreciated_value=4005.64, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=94.55, remaining_value=3099.81, depreciated_value=4100.19, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=94.55, remaining_value=3005.26, depreciated_value=4194.74, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=94.55, remaining_value=2910.71, depreciated_value=4289.29, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=94.54, remaining_value=2816.17, depreciated_value=4383.83, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=94.55, remaining_value=2721.62, depreciated_value=4478.38, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=94.55, remaining_value=2627.07, depreciated_value=4572.93, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=87.57, remaining_value=2539.50, depreciated_value=4660.50, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=87.57, remaining_value=2451.93, depreciated_value=4748.07, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=87.57, remaining_value=2364.36, depreciated_value=4835.64, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=87.57, remaining_value=2276.79, depreciated_value=4923.21, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=87.56, remaining_value=2189.23, depreciated_value=5010.77, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=87.57, remaining_value=2101.66, depreciated_value=5098.34, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=87.57, remaining_value=2014.09, depreciated_value=5185.91, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=87.57, remaining_value=1926.52, depreciated_value=5273.48, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=87.57, remaining_value=1838.95, depreciated_value=5361.05, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=87.57, remaining_value=1751.38, depreciated_value=5448.62, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=87.57, remaining_value=1663.81, depreciated_value=5536.19, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=87.57, remaining_value=1576.24, depreciated_value=5623.76, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=87.57, remaining_value=1488.67, depreciated_value=5711.33, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=87.57, remaining_value=1401.10, depreciated_value=5798.90, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=87.57, remaining_value=1313.53, depreciated_value=5886.47, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=87.57, remaining_value=1225.96, depreciated_value=5974.04, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=87.56, remaining_value=1138.40, depreciated_value=6061.60, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=87.57, remaining_value=1050.83, depreciated_value=6149.17, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=87.57, remaining_value=963.26, depreciated_value=6236.74, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=87.57, remaining_value=875.69, depreciated_value=6324.31, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=87.57, remaining_value=788.12, depreciated_value=6411.88, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=87.57, remaining_value=700.55, depreciated_value=6499.45, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=87.57, remaining_value=612.98, depreciated_value=6587.02, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=87.57, remaining_value=525.41, depreciated_value=6674.59, state='draft'), - # 2025 - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=87.57, remaining_value=437.84, depreciated_value=6762.16, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=87.57, remaining_value=350.27, depreciated_value=6849.73, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=87.56, remaining_value=262.71, depreciated_value=6937.29, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=87.57, remaining_value=175.14, depreciated_value=7024.86, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=87.57, remaining_value=87.57, depreciated_value=7112.43, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=87.57, remaining_value=0.00, depreciated_value=7200.00, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=123.96, remaining_value=8376.04, depreciated_value=123.96, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=247.92, remaining_value=8128.12, depreciated_value=371.88, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=247.91, remaining_value=7880.21, depreciated_value=619.79, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=247.92, remaining_value=7632.29, depreciated_value=867.71, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=247.92, remaining_value=7384.37, depreciated_value=1115.63, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=247.91, remaining_value=7136.46, depreciated_value=1363.54, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=247.92, remaining_value=6888.54, depreciated_value=1611.46, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=229.62, remaining_value=6658.92, depreciated_value=1841.08, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=229.62, remaining_value=6429.30, depreciated_value=2070.70, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=229.61, remaining_value=6199.69, depreciated_value=2300.31, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=229.62, remaining_value=5970.07, depreciated_value=2529.93, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=229.62, remaining_value=5740.45, depreciated_value=2759.55, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=229.62, remaining_value=5510.83, depreciated_value=2989.17, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=229.62, remaining_value=5281.21, depreciated_value=3218.79, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=229.61, remaining_value=5051.60, depreciated_value=3448.40, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=229.62, remaining_value=4821.98, depreciated_value=3678.02, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=229.62, remaining_value=4592.36, depreciated_value=3907.64, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=229.62, remaining_value=4362.74, depreciated_value=4137.26, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=229.62, remaining_value=4133.12, depreciated_value=4366.88, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=229.62, remaining_value=3903.50, depreciated_value=4596.50, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=229.62, remaining_value=3673.88, depreciated_value=4826.12, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=229.61, remaining_value=3444.27, depreciated_value=5055.73, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=229.62, remaining_value=3214.65, depreciated_value=5285.35, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=229.62, remaining_value=2985.03, depreciated_value=5514.97, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=229.62, remaining_value=2755.41, depreciated_value=5744.59, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=229.61, remaining_value=2525.80, depreciated_value=5974.20, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=229.62, remaining_value=2296.18, depreciated_value=6203.82, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=229.62, remaining_value=2066.56, depreciated_value=6433.44, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=229.62, remaining_value=1836.94, depreciated_value=6663.06, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=229.62, remaining_value=1607.32, depreciated_value=6892.68, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=229.61, remaining_value=1377.71, depreciated_value=7122.29, state='draft'), - # 2025 - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=229.62, remaining_value=1148.09, depreciated_value=7351.91, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=229.62, remaining_value=918.47, depreciated_value=7581.53, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=229.62, remaining_value=688.85, depreciated_value=7811.15, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=229.61, remaining_value=459.24, depreciated_value=8040.76, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=229.62, remaining_value=229.62, depreciated_value=8270.38, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=229.62, remaining_value=0.00, depreciated_value=8500.00, state='draft'), - ]) - - def test_monthly_degressive_start_beginning_month_increase_middle_month_on_linear_part(self): - asset = self.degressive_asset - asset.write({'acquisition_date': '2019-07-01'}) - asset.validate() - - date_modify = fields.Date.to_date("2022-06-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2019-07-31', depreciation_value=210.00, remaining_value=6990.00, depreciated_value=210.00, state='posted'), - self._get_depreciation_move_values(date='2019-08-31', depreciation_value=210.00, remaining_value=6780.00, depreciated_value=420.00, state='posted'), - self._get_depreciation_move_values(date='2019-09-30', depreciation_value=210.00, remaining_value=6570.00, depreciated_value=630.00, state='posted'), - self._get_depreciation_move_values(date='2019-10-31', depreciation_value=210.00, remaining_value=6360.00, depreciated_value=840.00, state='posted'), - self._get_depreciation_move_values(date='2019-11-30', depreciation_value=210.00, remaining_value=6150.00, depreciated_value=1050.00, state='posted'), - self._get_depreciation_move_values(date='2019-12-31', depreciation_value=210.00, remaining_value=5940.00, depreciated_value=1260.00, state='posted'), - # 2020 - self._get_depreciation_move_values(date='2020-01-31', depreciation_value=173.25, remaining_value=5766.75, depreciated_value=1433.25, state='posted'), - self._get_depreciation_move_values(date='2020-02-29', depreciation_value=173.25, remaining_value=5593.50, depreciated_value=1606.50, state='posted'), - self._get_depreciation_move_values(date='2020-03-31', depreciation_value=173.25, remaining_value=5420.25, depreciated_value=1779.75, state='posted'), - self._get_depreciation_move_values(date='2020-04-30', depreciation_value=173.25, remaining_value=5247.00, depreciated_value=1953.00, state='posted'), - self._get_depreciation_move_values(date='2020-05-31', depreciation_value=173.25, remaining_value=5073.75, depreciated_value=2126.25, state='posted'), - self._get_depreciation_move_values(date='2020-06-30', depreciation_value=173.25, remaining_value=4900.50, depreciated_value=2299.50, state='posted'), - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=173.25, remaining_value=4727.25, depreciated_value=2472.75, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=173.25, remaining_value=4554.00, depreciated_value=2646.00, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=173.25, remaining_value=4380.75, depreciated_value=2819.25, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=173.25, remaining_value=4207.50, depreciated_value=2992.50, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=173.25, remaining_value=4034.25, depreciated_value=3165.75, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=173.25, remaining_value=3861.00, depreciated_value=3339.00, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=112.61, remaining_value=3748.39, depreciated_value=3451.61, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=112.61, remaining_value=3635.78, depreciated_value=3564.22, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=112.62, remaining_value=3523.16, depreciated_value=3676.84, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=112.61, remaining_value=3410.55, depreciated_value=3789.45, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=112.61, remaining_value=3297.94, depreciated_value=3902.06, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=112.61, remaining_value=3185.33, depreciated_value=4014.67, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=112.62, remaining_value=3072.71, depreciated_value=4127.29, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=112.61, remaining_value=2960.10, depreciated_value=4239.90, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=112.61, remaining_value=2847.49, depreciated_value=4352.51, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=112.61, remaining_value=2734.88, depreciated_value=4465.12, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=112.62, remaining_value=2622.26, depreciated_value=4577.74, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=112.61, remaining_value=2509.65, depreciated_value=4690.35, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=83.66, remaining_value=2425.99, depreciated_value=4774.01, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=83.65, remaining_value=2342.34, depreciated_value=4857.66, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=83.65, remaining_value=2258.69, depreciated_value=4941.31, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=83.66, remaining_value=2175.03, depreciated_value=5024.97, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=83.66, remaining_value=2091.37, depreciated_value=5108.63, state='posted'), - # Increase - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=41.82, remaining_value=2049.55, depreciated_value=5150.45, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=41.83, remaining_value=2007.72, depreciated_value=5192.28, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=83.65, remaining_value=1924.07, depreciated_value=5275.93, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=83.66, remaining_value=1840.41, depreciated_value=5359.59, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=83.65, remaining_value=1756.76, depreciated_value=5443.24, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=83.66, remaining_value=1673.10, depreciated_value=5526.90, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=83.65, remaining_value=1589.45, depreciated_value=5610.55, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=83.66, remaining_value=1505.79, depreciated_value=5694.21, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=83.66, remaining_value=1422.13, depreciated_value=5777.87, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=83.65, remaining_value=1338.48, depreciated_value=5861.52, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=83.65, remaining_value=1254.83, depreciated_value=5945.17, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=83.66, remaining_value=1171.17, depreciated_value=6028.83, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=83.65, remaining_value=1087.52, depreciated_value=6112.48, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=83.66, remaining_value=1003.86, depreciated_value=6196.14, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=83.65, remaining_value=920.21, depreciated_value=6279.79, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=83.66, remaining_value=836.55, depreciated_value=6363.45, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=83.65, remaining_value=752.90, depreciated_value=6447.10, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=83.66, remaining_value=669.24, depreciated_value=6530.76, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=83.65, remaining_value=585.59, depreciated_value=6614.41, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=83.66, remaining_value=501.93, depreciated_value=6698.07, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=83.65, remaining_value=418.28, depreciated_value=6781.72, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=83.66, remaining_value=334.62, depreciated_value=6865.38, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=83.65, remaining_value=250.97, depreciated_value=6949.03, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=83.66, remaining_value=167.31, depreciated_value=7032.69, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=83.65, remaining_value=83.66, depreciated_value=7116.34, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=83.66, remaining_value=0.00, depreciated_value=7200.00, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=173.47, remaining_value=8326.53, depreciated_value=173.47, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=346.94, remaining_value=7979.59, depreciated_value=520.41, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=346.94, remaining_value=7632.65, depreciated_value=867.35, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=346.94, remaining_value=7285.71, depreciated_value=1214.29, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=346.93, remaining_value=6938.78, depreciated_value=1561.22, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=346.94, remaining_value=6591.84, depreciated_value=1908.16, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=346.94, remaining_value=6244.90, depreciated_value=2255.10, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=346.94, remaining_value=5897.96, depreciated_value=2602.04, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=346.94, remaining_value=5551.02, depreciated_value=2948.98, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=346.94, remaining_value=5204.08, depreciated_value=3295.92, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=346.94, remaining_value=4857.14, depreciated_value=3642.86, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=346.93, remaining_value=4510.21, depreciated_value=3989.79, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=346.94, remaining_value=4163.27, depreciated_value=4336.73, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=346.94, remaining_value=3816.33, depreciated_value=4683.67, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=346.94, remaining_value=3469.39, depreciated_value=5030.61, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=346.94, remaining_value=3122.45, depreciated_value=5377.55, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=346.94, remaining_value=2775.51, depreciated_value=5724.49, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=346.94, remaining_value=2428.57, depreciated_value=6071.43, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=346.94, remaining_value=2081.63, depreciated_value=6418.37, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=346.94, remaining_value=1734.69, depreciated_value=6765.31, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=346.94, remaining_value=1387.75, depreciated_value=7112.25, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=346.94, remaining_value=1040.81, depreciated_value=7459.19, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=346.93, remaining_value=693.88, depreciated_value=7806.12, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=346.94, remaining_value=346.94, depreciated_value=8153.06, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=346.94, remaining_value=0.00, depreciated_value=8500.00, state='draft'), - ]) - - def test_monthly_degressive_start_beginning_month_decrease_middle_month(self): - asset = self.degressive_asset - asset.validate() - - date_modify = fields.Date.to_date("2022-06-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) - 500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=210.00, remaining_value=6990.00, depreciated_value=210.00, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=210.00, remaining_value=6780.00, depreciated_value=420.00, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=210.00, remaining_value=6570.00, depreciated_value=630.00, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=210.00, remaining_value=6360.00, depreciated_value=840.00, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=210.00, remaining_value=6150.00, depreciated_value=1050.00, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=210.00, remaining_value=5940.00, depreciated_value=1260.00, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=173.25, remaining_value=5766.75, depreciated_value=1433.25, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=173.25, remaining_value=5593.50, depreciated_value=1606.50, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=173.25, remaining_value=5420.25, depreciated_value=1779.75, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=173.25, remaining_value=5247.00, depreciated_value=1953.00, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=173.25, remaining_value=5073.75, depreciated_value=2126.25, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=173.25, remaining_value=4900.50, depreciated_value=2299.50, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=173.25, remaining_value=4727.25, depreciated_value=2472.75, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=173.25, remaining_value=4554.00, depreciated_value=2646.00, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=173.25, remaining_value=4380.75, depreciated_value=2819.25, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=173.25, remaining_value=4207.50, depreciated_value=2992.50, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=173.25, remaining_value=4034.25, depreciated_value=3165.75, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=173.25, remaining_value=3861.00, depreciated_value=3339.00, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=112.61, remaining_value=3748.39, depreciated_value=3451.61, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=112.61, remaining_value=3635.78, depreciated_value=3564.22, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=112.62, remaining_value=3523.16, depreciated_value=3676.84, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=112.61, remaining_value=3410.55, depreciated_value=3789.45, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=112.61, remaining_value=3297.94, depreciated_value=3902.06, state='posted'), - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=56.31, remaining_value=3241.63, depreciated_value=3958.37, state='posted'), - # Decrease - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=500.00, remaining_value=2741.63, depreciated_value=4458.37, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=39.98, remaining_value=2701.65, depreciated_value=4498.35, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=79.97, remaining_value=2621.68, depreciated_value=4578.32, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=79.96, remaining_value=2541.72, depreciated_value=4658.28, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=79.96, remaining_value=2461.76, depreciated_value=4738.24, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=79.97, remaining_value=2381.79, depreciated_value=4818.21, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=79.96, remaining_value=2301.83, depreciated_value=4898.17, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=79.97, remaining_value=2221.86, depreciated_value=4978.14, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=74.06, remaining_value=2147.80, depreciated_value=5052.20, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=74.06, remaining_value=2073.74, depreciated_value=5126.26, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=74.07, remaining_value=1999.67, depreciated_value=5200.33, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=74.06, remaining_value=1925.61, depreciated_value=5274.39, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=74.06, remaining_value=1851.55, depreciated_value=5348.45, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=74.06, remaining_value=1777.49, depreciated_value=5422.51, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=74.06, remaining_value=1703.43, depreciated_value=5496.57, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=74.07, remaining_value=1629.36, depreciated_value=5570.64, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=74.06, remaining_value=1555.30, depreciated_value=5644.70, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=74.06, remaining_value=1481.24, depreciated_value=5718.76, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=74.06, remaining_value=1407.18, depreciated_value=5792.82, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=74.06, remaining_value=1333.12, depreciated_value=5866.88, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=74.06, remaining_value=1259.06, depreciated_value=5940.94, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=74.06, remaining_value=1185.00, depreciated_value=6015.00, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=74.07, remaining_value=1110.93, depreciated_value=6089.07, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=74.06, remaining_value=1036.87, depreciated_value=6163.13, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=74.06, remaining_value=962.81, depreciated_value=6237.19, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=74.06, remaining_value=888.75, depreciated_value=6311.25, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=74.07, remaining_value=814.68, depreciated_value=6385.32, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=74.06, remaining_value=740.62, depreciated_value=6459.38, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=74.06, remaining_value=666.56, depreciated_value=6533.44, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=74.06, remaining_value=592.50, depreciated_value=6607.50, state='draft'), - self._get_depreciation_move_values(date='2024-11-30', depreciation_value=74.06, remaining_value=518.44, depreciated_value=6681.56, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=74.07, remaining_value=444.37, depreciated_value=6755.63, state='draft'), - # 2025 - self._get_depreciation_move_values(date='2025-01-31', depreciation_value=74.06, remaining_value=370.31, depreciated_value=6829.69, state='draft'), - self._get_depreciation_move_values(date='2025-02-28', depreciation_value=74.06, remaining_value=296.25, depreciated_value=6903.75, state='draft'), - self._get_depreciation_move_values(date='2025-03-31', depreciation_value=74.07, remaining_value=222.18, depreciated_value=6977.82, state='draft'), - self._get_depreciation_move_values(date='2025-04-30', depreciation_value=74.06, remaining_value=148.12, depreciated_value=7051.88, state='draft'), - self._get_depreciation_move_values(date='2025-05-31', depreciation_value=74.06, remaining_value=74.06, depreciated_value=7125.94, state='draft'), - self._get_depreciation_move_values(date='2025-06-30', depreciation_value=74.06, remaining_value=0.00, depreciated_value=7200.00, state='draft'), - ]) - - def test_monthly_degressive_then_linear_start_beginning_month_increase_middle_month_on_degressive_part(self): - asset = self.degressive_then_linear_asset - asset.validate() - - date_modify = fields.Date.to_date("2021-06-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=210.00, remaining_value=6990.00, depreciated_value=210.00, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=210.00, remaining_value=6780.00, depreciated_value=420.00, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=210.00, remaining_value=6570.00, depreciated_value=630.00, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=210.00, remaining_value=6360.00, depreciated_value=840.00, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=210.00, remaining_value=6150.00, depreciated_value=1050.00, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=210.00, remaining_value=5940.00, depreciated_value=1260.00, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=173.25, remaining_value=5766.75, depreciated_value=1433.25, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=173.25, remaining_value=5593.50, depreciated_value=1606.50, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=173.25, remaining_value=5420.25, depreciated_value=1779.75, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=173.25, remaining_value=5247.00, depreciated_value=1953.00, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=173.25, remaining_value=5073.75, depreciated_value=2126.25, state='posted'), - # Increase - self._get_depreciation_move_values(date='2021-06-15', depreciation_value=86.63, remaining_value=4987.12, depreciated_value=2212.88, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=72.73, remaining_value=4914.39, depreciated_value=2285.61, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=145.46, remaining_value=4768.93, depreciated_value=2431.07, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=145.45, remaining_value=4623.48, depreciated_value=2576.52, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=145.46, remaining_value=4478.02, depreciated_value=2721.98, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=145.46, remaining_value=4332.56, depreciated_value=2867.44, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=145.46, remaining_value=4187.10, depreciated_value=3012.90, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=145.45, remaining_value=4041.65, depreciated_value=3158.35, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=120.00, remaining_value=3921.65, depreciated_value=3278.35, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=120.00, remaining_value=3801.65, depreciated_value=3398.35, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=120.00, remaining_value=3681.65, depreciated_value=3518.35, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=120.00, remaining_value=3561.65, depreciated_value=3638.35, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=120.00, remaining_value=3441.65, depreciated_value=3758.35, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=120.00, remaining_value=3321.65, depreciated_value=3878.35, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=120.00, remaining_value=3201.65, depreciated_value=3998.35, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=120.00, remaining_value=3081.65, depreciated_value=4118.35, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=120.00, remaining_value=2961.65, depreciated_value=4238.35, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=120.00, remaining_value=2841.65, depreciated_value=4358.35, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=120.00, remaining_value=2721.65, depreciated_value=4478.35, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=120.00, remaining_value=2601.65, depreciated_value=4598.35, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=120.00, remaining_value=2481.65, depreciated_value=4718.35, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=120.00, remaining_value=2361.65, depreciated_value=4838.35, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=120.00, remaining_value=2241.65, depreciated_value=4958.35, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=120.00, remaining_value=2121.65, depreciated_value=5078.35, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=120.00, remaining_value=2001.65, depreciated_value=5198.35, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=120.00, remaining_value=1881.65, depreciated_value=5318.35, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=120.00, remaining_value=1761.65, depreciated_value=5438.35, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=120.00, remaining_value=1641.65, depreciated_value=5558.35, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=120.00, remaining_value=1521.65, depreciated_value=5678.35, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=120.00, remaining_value=1401.65, depreciated_value=5798.35, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=120.00, remaining_value=1281.65, depreciated_value=5918.35, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=120.00, remaining_value=1161.65, depreciated_value=6038.35, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=120.00, remaining_value=1041.65, depreciated_value=6158.35, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=120.00, remaining_value=921.65, depreciated_value=6278.35, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=120.00, remaining_value=801.65, depreciated_value=6398.35, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=120.00, remaining_value=681.65, depreciated_value=6518.35, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=120.00, remaining_value=561.65, depreciated_value=6638.35, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=120.00, remaining_value=441.65, depreciated_value=6758.35, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=120.00, remaining_value=321.65, depreciated_value=6878.35, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=120.00, remaining_value=201.65, depreciated_value=6998.35, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=120.00, remaining_value=81.65, depreciated_value=7118.35, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=81.65, remaining_value=0.00, depreciated_value=7200.00, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=123.96, remaining_value=8376.04, depreciated_value=123.96, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=247.92, remaining_value=8128.12, depreciated_value=371.88, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=247.91, remaining_value=7880.21, depreciated_value=619.79, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=247.92, remaining_value=7632.29, depreciated_value=867.71, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=247.92, remaining_value=7384.37, depreciated_value=1115.63, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=247.91, remaining_value=7136.46, depreciated_value=1363.54, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=247.92, remaining_value=6888.54, depreciated_value=1611.46, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=204.52, remaining_value=6684.02, depreciated_value=1815.98, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=204.52, remaining_value=6479.50, depreciated_value=2020.50, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=204.53, remaining_value=6274.97, depreciated_value=2225.03, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=204.52, remaining_value=6070.45, depreciated_value=2429.55, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=204.52, remaining_value=5865.93, depreciated_value=2634.07, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=204.53, remaining_value=5661.40, depreciated_value=2838.60, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=204.52, remaining_value=5456.88, depreciated_value=3043.12, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=204.52, remaining_value=5252.36, depreciated_value=3247.64, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=204.53, remaining_value=5047.83, depreciated_value=3452.17, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=204.52, remaining_value=4843.31, depreciated_value=3656.69, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=204.52, remaining_value=4638.79, depreciated_value=3861.21, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=204.52, remaining_value=4434.27, depreciated_value=4065.73, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=204.53, remaining_value=4229.74, depreciated_value=4270.26, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=204.52, remaining_value=4025.22, depreciated_value=4474.78, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=204.52, remaining_value=3820.70, depreciated_value=4679.30, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=204.53, remaining_value=3616.17, depreciated_value=4883.83, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=204.52, remaining_value=3411.65, depreciated_value=5088.35, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=204.52, remaining_value=3207.13, depreciated_value=5292.87, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=204.52, remaining_value=3002.61, depreciated_value=5497.39, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=204.53, remaining_value=2798.08, depreciated_value=5701.92, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=204.52, remaining_value=2593.56, depreciated_value=5906.44, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=204.52, remaining_value=2389.04, depreciated_value=6110.96, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=204.53, remaining_value=2184.51, depreciated_value=6315.49, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=204.52, remaining_value=1979.99, depreciated_value=6520.01, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=204.52, remaining_value=1775.47, depreciated_value=6724.53, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=204.52, remaining_value=1570.95, depreciated_value=6929.05, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=204.53, remaining_value=1366.42, depreciated_value=7133.58, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=204.52, remaining_value=1161.90, depreciated_value=7338.10, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=204.52, remaining_value=957.38, depreciated_value=7542.62, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=204.53, remaining_value=752.85, depreciated_value=7747.15, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=204.52, remaining_value=548.33, depreciated_value=7951.67, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=204.52, remaining_value=343.81, depreciated_value=8156.19, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=204.53, remaining_value=139.28, depreciated_value=8360.72, state='draft'), - self._get_depreciation_move_values(date='2024-10-31', depreciation_value=139.28, remaining_value=0.00, depreciated_value=8500.00, state='draft'), - ]) - - def test_monthly_degressive_then_linear_start_beginning_month_increase_middle_month_on_linear_part(self): - asset = self.degressive_then_linear_asset - asset.validate() - - date_modify = fields.Date.to_date("2022-06-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) + 8500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=210.00, remaining_value=6990.00, depreciated_value=210.00, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=210.00, remaining_value=6780.00, depreciated_value=420.00, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=210.00, remaining_value=6570.00, depreciated_value=630.00, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=210.00, remaining_value=6360.00, depreciated_value=840.00, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=210.00, remaining_value=6150.00, depreciated_value=1050.00, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=210.00, remaining_value=5940.00, depreciated_value=1260.00, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=173.25, remaining_value=5766.75, depreciated_value=1433.25, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=173.25, remaining_value=5593.50, depreciated_value=1606.50, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=173.25, remaining_value=5420.25, depreciated_value=1779.75, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=173.25, remaining_value=5247.00, depreciated_value=1953.00, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=173.25, remaining_value=5073.75, depreciated_value=2126.25, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=173.25, remaining_value=4900.50, depreciated_value=2299.50, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=173.25, remaining_value=4727.25, depreciated_value=2472.75, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=173.25, remaining_value=4554.00, depreciated_value=2646.00, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=173.25, remaining_value=4380.75, depreciated_value=2819.25, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=173.25, remaining_value=4207.50, depreciated_value=2992.50, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=173.25, remaining_value=4034.25, depreciated_value=3165.75, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=173.25, remaining_value=3861.00, depreciated_value=3339.00, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=120.00, remaining_value=3741.00, depreciated_value=3459.00, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=120.00, remaining_value=3621.00, depreciated_value=3579.00, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=120.00, remaining_value=3501.00, depreciated_value=3699.00, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=120.00, remaining_value=3381.00, depreciated_value=3819.00, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=120.00, remaining_value=3261.00, depreciated_value=3939.00, state='posted'), - # Increase - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=60.00, remaining_value=3201.00, depreciated_value=3999.00, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=60.00, remaining_value=3141.00, depreciated_value=4059.00, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=120.00, remaining_value=3021.00, depreciated_value=4179.00, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=120.00, remaining_value=2901.00, depreciated_value=4299.00, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=120.00, remaining_value=2781.00, depreciated_value=4419.00, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=120.00, remaining_value=2661.00, depreciated_value=4539.00, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=120.00, remaining_value=2541.00, depreciated_value=4659.00, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=120.00, remaining_value=2421.00, depreciated_value=4779.00, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=120.00, remaining_value=2301.00, depreciated_value=4899.00, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=120.00, remaining_value=2181.00, depreciated_value=5019.00, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=120.00, remaining_value=2061.00, depreciated_value=5139.00, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=120.00, remaining_value=1941.00, depreciated_value=5259.00, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=120.00, remaining_value=1821.00, depreciated_value=5379.00, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=120.00, remaining_value=1701.00, depreciated_value=5499.00, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=120.00, remaining_value=1581.00, depreciated_value=5619.00, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=120.00, remaining_value=1461.00, depreciated_value=5739.00, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=120.00, remaining_value=1341.00, depreciated_value=5859.00, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=120.00, remaining_value=1221.00, depreciated_value=5979.00, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=120.00, remaining_value=1101.00, depreciated_value=6099.00, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=120.00, remaining_value=981.00, depreciated_value=6219.00, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=120.00, remaining_value=861.00, depreciated_value=6339.00, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=120.00, remaining_value=741.00, depreciated_value=6459.00, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=120.00, remaining_value=621.00, depreciated_value=6579.00, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=120.00, remaining_value=501.00, depreciated_value=6699.00, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=120.00, remaining_value=381.00, depreciated_value=6819.00, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=120.00, remaining_value=261.00, depreciated_value=6939.00, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=120.00, remaining_value=141.00, depreciated_value=7059.00, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=120.00, remaining_value=21.00, depreciated_value=7179.00, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=21.00, remaining_value=0.00, depreciated_value=7200.00, state='draft'), - ]) - - self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=159.32, remaining_value=8340.68, depreciated_value=159.32, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=318.65, remaining_value=8022.03, depreciated_value=477.97, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=318.65, remaining_value=7703.38, depreciated_value=796.62, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=318.65, remaining_value=7384.73, depreciated_value=1115.27, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=318.65, remaining_value=7066.08, depreciated_value=1433.92, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=318.65, remaining_value=6747.43, depreciated_value=1752.57, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=318.65, remaining_value=6428.78, depreciated_value=2071.22, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=318.65, remaining_value=6110.13, depreciated_value=2389.87, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=318.65, remaining_value=5791.48, depreciated_value=2708.52, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=318.65, remaining_value=5472.83, depreciated_value=3027.17, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=318.65, remaining_value=5154.18, depreciated_value=3345.82, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=318.65, remaining_value=4835.53, depreciated_value=3664.47, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=318.65, remaining_value=4516.88, depreciated_value=3983.12, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=318.65, remaining_value=4198.23, depreciated_value=4301.77, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=318.65, remaining_value=3879.58, depreciated_value=4620.42, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=318.65, remaining_value=3560.93, depreciated_value=4939.07, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=318.65, remaining_value=3242.28, depreciated_value=5257.72, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=318.65, remaining_value=2923.63, depreciated_value=5576.37, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=318.65, remaining_value=2604.98, depreciated_value=5895.02, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=318.65, remaining_value=2286.33, depreciated_value=6213.67, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=318.65, remaining_value=1967.68, depreciated_value=6532.32, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=318.65, remaining_value=1649.03, depreciated_value=6850.97, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=318.65, remaining_value=1330.38, depreciated_value=7169.62, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=318.65, remaining_value=1011.73, depreciated_value=7488.27, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=318.65, remaining_value=693.08, depreciated_value=7806.92, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=318.65, remaining_value=374.43, depreciated_value=8125.57, state='draft'), - self._get_depreciation_move_values(date='2024-08-31', depreciation_value=318.65, remaining_value=55.78, depreciated_value=8444.22, state='draft'), - self._get_depreciation_move_values(date='2024-09-30', depreciation_value=55.78, remaining_value=0.00, depreciated_value=8500.00, state='draft'), - ]) - - def test_monthly_degressive_then_linear_start_beginning_month_decrease_middle_month(self): - asset = self.degressive_then_linear_asset - asset.validate() - - date_modify = fields.Date.to_date("2022-06-15") - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': date_modify, - 'value_residual': asset._get_residual_value_at_date(date_modify) - 500, - "account_asset_counterpart_id": self.asset_counterpart_account_id.id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-07-31', depreciation_value=210.00, remaining_value=6990.00, depreciated_value=210.00, state='posted'), - self._get_depreciation_move_values(date='2020-08-31', depreciation_value=210.00, remaining_value=6780.00, depreciated_value=420.00, state='posted'), - self._get_depreciation_move_values(date='2020-09-30', depreciation_value=210.00, remaining_value=6570.00, depreciated_value=630.00, state='posted'), - self._get_depreciation_move_values(date='2020-10-31', depreciation_value=210.00, remaining_value=6360.00, depreciated_value=840.00, state='posted'), - self._get_depreciation_move_values(date='2020-11-30', depreciation_value=210.00, remaining_value=6150.00, depreciated_value=1050.00, state='posted'), - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=210.00, remaining_value=5940.00, depreciated_value=1260.00, state='posted'), - # 2021 - self._get_depreciation_move_values(date='2021-01-31', depreciation_value=173.25, remaining_value=5766.75, depreciated_value=1433.25, state='posted'), - self._get_depreciation_move_values(date='2021-02-28', depreciation_value=173.25, remaining_value=5593.50, depreciated_value=1606.50, state='posted'), - self._get_depreciation_move_values(date='2021-03-31', depreciation_value=173.25, remaining_value=5420.25, depreciated_value=1779.75, state='posted'), - self._get_depreciation_move_values(date='2021-04-30', depreciation_value=173.25, remaining_value=5247.00, depreciated_value=1953.00, state='posted'), - self._get_depreciation_move_values(date='2021-05-31', depreciation_value=173.25, remaining_value=5073.75, depreciated_value=2126.25, state='posted'), - self._get_depreciation_move_values(date='2021-06-30', depreciation_value=173.25, remaining_value=4900.50, depreciated_value=2299.50, state='posted'), - self._get_depreciation_move_values(date='2021-07-31', depreciation_value=173.25, remaining_value=4727.25, depreciated_value=2472.75, state='posted'), - self._get_depreciation_move_values(date='2021-08-31', depreciation_value=173.25, remaining_value=4554.00, depreciated_value=2646.00, state='posted'), - self._get_depreciation_move_values(date='2021-09-30', depreciation_value=173.25, remaining_value=4380.75, depreciated_value=2819.25, state='posted'), - self._get_depreciation_move_values(date='2021-10-31', depreciation_value=173.25, remaining_value=4207.50, depreciated_value=2992.50, state='posted'), - self._get_depreciation_move_values(date='2021-11-30', depreciation_value=173.25, remaining_value=4034.25, depreciated_value=3165.75, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=173.25, remaining_value=3861.00, depreciated_value=3339.00, state='posted'), - # 2022 - self._get_depreciation_move_values(date='2022-01-31', depreciation_value=120.00, remaining_value=3741.00, depreciated_value=3459.00, state='posted'), - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=120.00, remaining_value=3621.00, depreciated_value=3579.00, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=120.00, remaining_value=3501.00, depreciated_value=3699.00, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=120.00, remaining_value=3381.00, depreciated_value=3819.00, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=120.00, remaining_value=3261.00, depreciated_value=3939.00, state='posted'), - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=60.00, remaining_value=3201.00, depreciated_value=3999.00, state='posted'), - # Decrease - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=500.00, remaining_value=2701.00, depreciated_value=4499.00, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=53.15, remaining_value=2647.85, depreciated_value=4552.15, state='posted'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=106.30, remaining_value=2541.55, depreciated_value=4658.45, state='draft'), - self._get_depreciation_move_values(date='2022-08-31', depreciation_value=106.30, remaining_value=2435.25, depreciated_value=4764.75, state='draft'), - self._get_depreciation_move_values(date='2022-09-30', depreciation_value=106.30, remaining_value=2328.95, depreciated_value=4871.05, state='draft'), - self._get_depreciation_move_values(date='2022-10-31', depreciation_value=106.30, remaining_value=2222.65, depreciated_value=4977.35, state='draft'), - self._get_depreciation_move_values(date='2022-11-30', depreciation_value=106.30, remaining_value=2116.35, depreciated_value=5083.65, state='draft'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=106.30, remaining_value=2010.05, depreciated_value=5189.95, state='draft'), - # 2023 - self._get_depreciation_move_values(date='2023-01-31', depreciation_value=106.30, remaining_value=1903.75, depreciated_value=5296.25, state='draft'), - self._get_depreciation_move_values(date='2023-02-28', depreciation_value=106.30, remaining_value=1797.45, depreciated_value=5402.55, state='draft'), - self._get_depreciation_move_values(date='2023-03-31', depreciation_value=106.30, remaining_value=1691.15, depreciated_value=5508.85, state='draft'), - self._get_depreciation_move_values(date='2023-04-30', depreciation_value=106.30, remaining_value=1584.85, depreciated_value=5615.15, state='draft'), - self._get_depreciation_move_values(date='2023-05-31', depreciation_value=106.30, remaining_value=1478.55, depreciated_value=5721.45, state='draft'), - self._get_depreciation_move_values(date='2023-06-30', depreciation_value=106.30, remaining_value=1372.25, depreciated_value=5827.75, state='draft'), - self._get_depreciation_move_values(date='2023-07-31', depreciation_value=106.30, remaining_value=1265.95, depreciated_value=5934.05, state='draft'), - self._get_depreciation_move_values(date='2023-08-31', depreciation_value=106.30, remaining_value=1159.65, depreciated_value=6040.35, state='draft'), - self._get_depreciation_move_values(date='2023-09-30', depreciation_value=106.30, remaining_value=1053.35, depreciated_value=6146.65, state='draft'), - self._get_depreciation_move_values(date='2023-10-31', depreciation_value=106.30, remaining_value=947.05, depreciated_value=6252.95, state='draft'), - self._get_depreciation_move_values(date='2023-11-30', depreciation_value=106.30, remaining_value=840.75, depreciated_value=6359.25, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=106.30, remaining_value=734.45, depreciated_value=6465.55, state='draft'), - # 2024 - self._get_depreciation_move_values(date='2024-01-31', depreciation_value=106.30, remaining_value=628.15, depreciated_value=6571.85, state='draft'), - self._get_depreciation_move_values(date='2024-02-29', depreciation_value=106.30, remaining_value=521.85, depreciated_value=6678.15, state='draft'), - self._get_depreciation_move_values(date='2024-03-31', depreciation_value=106.30, remaining_value=415.55, depreciated_value=6784.45, state='draft'), - self._get_depreciation_move_values(date='2024-04-30', depreciation_value=106.30, remaining_value=309.25, depreciated_value=6890.75, state='draft'), - self._get_depreciation_move_values(date='2024-05-31', depreciation_value=106.30, remaining_value=202.95, depreciated_value=6997.05, state='draft'), - self._get_depreciation_move_values(date='2024-06-30', depreciation_value=106.30, remaining_value=96.65, depreciated_value=7103.35, state='draft'), - self._get_depreciation_move_values(date='2024-07-31', depreciation_value=96.65, remaining_value=0.00, depreciated_value=7200.00, state='draft'), - ]) - - def test_linear_modify_0_value_residual(self): - """Set the value residual to 0""" - asset = self.create_asset(value=10000, periodicity="monthly", periods=10, method="linear", acquisition_date="2022-02-01", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'method_number': 4, - 'date': fields.Date.to_date("2022-06-24"), - 'modify_action': 'modify', - 'value_residual': 0, - 'account_asset_counterpart_id': self.company_data['default_account_revenue'].copy().id, - }).modify() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-02-28', depreciation_value=1000, remaining_value=9000, depreciated_value=1000, state='posted'), - self._get_depreciation_move_values(date='2022-03-31', depreciation_value=1000, remaining_value=8000, depreciated_value=2000, state='posted'), - self._get_depreciation_move_values(date='2022-04-30', depreciation_value=1000, remaining_value=7000, depreciated_value=3000, state='posted'), - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=6000, depreciated_value=4000, state='posted'), - self._get_depreciation_move_values(date='2022-06-24', depreciation_value=800, remaining_value=5200, depreciated_value=4800, state='posted'), - - self._get_depreciation_move_values(date='2022-06-24', depreciation_value=5200, remaining_value=0, depreciated_value=10000, state='posted'), - ]) - - def test_asset_modify_value_residual_after_reversal(self): - """ Tests the special case of residual amounts on a board with a reverse entry. - It keeps its focus on the computed residual amount in the modify asset wizard as for now, - the recomputation after a modify on a board with reverse entries is broken. This should be corrected in a later task.""" - - asset = self.create_asset(value=1000, periodicity="yearly", periods=5, method="linear", acquisition_date="2020-01-01", prorata_computation_type="constant_periods") - asset.validate() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=200, remaining_value=800, depreciated_value=200, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=200, remaining_value=600, depreciated_value=400, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=200, remaining_value=400, depreciated_value=600, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=200, remaining_value=200, depreciated_value=800, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=200, remaining_value=0, depreciated_value=1000, state='draft'), - ]) - - move_to_reverse = asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id))[0] - self.env['account.move.reversal']\ - .with_context(active_model="account.move", active_ids=move_to_reverse.ids)\ - .create({ - 'journal_id': move_to_reverse.journal_id.id - }).reverse_moves() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2020-12-31', depreciation_value=200, remaining_value=800, depreciated_value=200, state='posted'), - self._get_depreciation_move_values(date='2021-12-31', depreciation_value=200, remaining_value=600, depreciated_value=400, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=-200, remaining_value=800, depreciated_value=200, state='posted'), - self._get_depreciation_move_values(date='2022-12-31', depreciation_value=400, remaining_value=400, depreciated_value=600, state='draft'), - self._get_depreciation_move_values(date='2023-12-31', depreciation_value=200, remaining_value=200, depreciated_value=800, state='draft'), - self._get_depreciation_move_values(date='2024-12-31', depreciation_value=200, remaining_value=0, depreciated_value=1000, state='draft'), - ]) - - asset_modify = self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date("2022-06-30"), - 'modify_action': 'modify', - }) - - # We want to show the actual remaining value of the asset. - self.assertEqual(asset_modify.value_residual, 500, "The computation of the value_residual in asset.modify shouldn't care about the reversal.") - - def test_asset_gain_or_loss_account(self): - asset = self.create_asset(value=1000, periodicity="yearly", periods=5, method="linear", acquisition_date="2020-01-01", prorata_computation_type="constant_periods") - asset.validate() - - invoice = self.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, - 'invoice_date': '2022-06-30', - 'invoice_line_ids': [(0, 0, { - 'name': 'Asset sold', - 'tax_ids': [], - 'price_unit': 500, - })], - }) - invoice.action_post() - - asset_modify = self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'name': 'Test reason', - 'date': fields.Date.to_date('2022-06-10'), - 'modify_action': 'sell', - 'invoice_ids': invoice.ids, - 'invoice_line_ids': invoice.invoice_line_ids.ids - }) - # The remaining value of the asset on 2022-06-30 is 500: if the asset is sold before that date, it will result in a loss (and a gain if sold after) - self.assertEqual(asset_modify.gain_or_loss, 'loss') - - asset_modify.date = fields.Date.from_string('2022-07-15') - self.assertEqual(asset_modify.gain_or_loss, 'gain') - - asset_modify.date = fields.Date.from_string('2022-06-30') - self.assertEqual(asset_modify.gain_or_loss, 'no') - - def test_asset_disposal_on_hashed_journal(self): - asset = self.create_asset( - value=3000, - periodicity='monthly', - periods=3, - method='linear', - acquisition_date='2022-05-01', - prorata_computation_type='constant_periods', - ) - asset.journal_id.restrict_mode_hash_table = True - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'date': fields.Date.to_date('2022-05-15'), - 'modify_action': 'dispose', - 'loss_account_id': self.company_data['default_account_expense'].copy().id, - }).sell_dispose() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-05-15', depreciation_value=483.87, remaining_value=2516.13, depreciated_value=483.87, state='posted'), - self._get_depreciation_move_values(date='2022-05-15', depreciation_value=2516.13, remaining_value=0, depreciated_value=3000, state='draft'), - # At this point the asset is disposed, which means its 'remaining_value' is 0. - # But the next 2 depreciation moves could not be removed due to the hash on the journal. - # This results in a negative 'remaining_value' and a 'depreciated_value' that exceeds the asset's initial value. - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=-1000, depreciated_value=4000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=-2000, depreciated_value=5000, state='posted'), - # The next 2 depreciation moves are reverting the previous 2, - # bringing the 'remaining_value' back to 0 on the last one, as it should be. - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=-1000, remaining_value=-1000, depreciated_value=4000, state='posted'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=-1000, remaining_value=0, depreciated_value=3000, state='posted'), - ]) - - def test_asset_disposal_with_audit_trail(self): - asset = self.create_asset( - value=3000, - periodicity='monthly', - periods=3, - method='linear', - acquisition_date='2022-05-01', - prorata_computation_type='constant_periods', - ) - asset.validate() - - with patch.object(self.env.registry['account.move'], '_is_protected_by_audit_trail', lambda move: True): - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'date': fields.Date.to_date('2022-06-15'), - 'modify_action': 'dispose', - 'loss_account_id': self.company_data['default_account_expense'].copy().id, - }).sell_dispose() - - self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [ - self._get_depreciation_move_values(date='2022-05-31', depreciation_value=1000, remaining_value=2000, depreciated_value=1000, state='posted'), - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=500, remaining_value=1500, depreciated_value=1500, state='posted'), - self._get_depreciation_move_values(date='2022-06-15', depreciation_value=1500, remaining_value=0, depreciated_value=3000, state='draft'), - self._get_depreciation_move_values(date='2022-06-30', depreciation_value=1000, remaining_value=0, depreciated_value=3000, state='cancel'), - self._get_depreciation_move_values(date='2022-07-31', depreciation_value=1000, remaining_value=0, depreciated_value=3000, state='cancel'), - ]) - - def test_disposal_of_fully_depreciated_asset(self): - asset = self.create_asset(value=10000, periodicity="yearly", periods=2, method="degressive", acquisition_date="2020-01-01", prorata_computation_type="constant_periods") - asset.validate() - - self.env['asset.modify'].create({ - 'asset_id': asset.id, - 'date': fields.Date.to_date("2022-01-01"), - 'modify_action': 'dispose', - 'loss_account_id': self.company_data['default_account_expense'].copy().id, - }).sell_dispose() diff --git a/addons/at_accounting/tests/test_signature.py b/addons/at_accounting/tests/test_signature.py deleted file mode 100644 index 6138fd5..0000000 --- a/addons/at_accounting/tests/test_signature.py +++ /dev/null @@ -1,87 +0,0 @@ -import base64 - -from odoo import Command -from odoo.tests import tagged -from odoo.addons.account.tests.common import AccountTestInvoicingCommon - - -@tagged('post_install', '-at_install') -class TestInvoiceSignature(AccountTestInvoicingCommon): - @classmethod - def setUpClass(cls): - super().setUpClass() - if cls.env.ref('base.module_sign').state != 'installed': - cls.skipTest(cls, "`sign` module not installed") - - cls.env.company.sign_invoice = True - - cls.signature_fake_1 = base64.b64encode(b"fake_signature_1") - cls.signature_fake_2 = base64.b64encode(b"fake_signature_2") - - cls.user.sign_signature = cls.signature_fake_1 - cls.another_user = cls.env['res.users'].create({ - 'name': 'another accountant', - 'login': 'another_accountant', - 'password': 'another_accountant', - 'groups_id': [ - Command.set(cls.env.user.groups_id.ids), - ], - 'sign_signature': cls.signature_fake_2, - }) - - cls.invoice = cls.env['account.move'].create({ - 'move_type': 'out_invoice', - 'partner_id': cls.partner_a.id, - 'journal_id': cls.company_data['default_journal_sale'].id, - 'invoice_line_ids': [ - Command.create({ - 'product_id': cls.product_a.id, - 'quantity': 1, - 'price_unit': 1, - }) - ] - }) - - def test_draft_invoice_shouldnt_have_signature(self): - self.assertEqual(self.invoice.state, 'draft') - self.assertFalse(self.invoice.show_signature_area, "the signature area shouldn't appear on a draft invoice") - - def test_posted_invoice_should_have_signature(self): - self.invoice.action_post() - self.assertTrue(self.invoice.show_signature_area, - "the signature area should appear on posted invoice when the `sign_invoice` settings is True") - - def test_invoice_from_company_without_signature_settings_shouldnt_have_signature(self): - self.env.company.sign_invoice = False - self.invoice.action_post() - self.assertFalse(self.invoice.show_signature_area, - "the signature area shouldn't appear when the `sign_invoice` settings is False") - - def test_invoice_signing_user_should_be_the_user_that_posted_it(self): - self.assertFalse(self.invoice.signing_user, - "invoice that weren't created by automated action shouldn't have a signing user") - self.assertEqual(self.invoice.signature, False, "There shouldn't be any signature if there isn't a signing user") - self.invoice.action_post() - self.assertEqual(self.invoice.signing_user, self.user, "The signing user should be the user that posted the invoice") - self.assertEqual(self.invoice.signature, self.signature_fake_1, "The signature should be from `self.user`") - - self.invoice.button_draft() - self.invoice.with_user(self.another_user).action_post() - self.assertEqual(self.invoice.signing_user, self.another_user, - "The signing user should be the user that posted the invoice") - self.assertEqual(self.invoice.signature, self.signature_fake_2, "The signature should be from `self.another_user`") - - def test_invoice_signing_user_should_be_reprensative_user_if_there_is_one(self): - self.env.company.signing_user = self.another_user # set the representative user of the company - self.invoice.action_post() - self.assertEqual(self.invoice.signing_user, self.another_user, "The signing user should be the representative person set in the settings") - self.assertEqual(self.invoice.signature, self.signature_fake_2, "The signature should be from `self.another_user`, the representative user") - - def test_setting_representative_user_shouldnt_change_signer_of_already_posted_invoice(self): - # Note: Changing this behavior might not be a good idea as having all account.move updated at once - # would be very costly - self.invoice.action_post() - self.env.company.signing_user = self.another_user # set the representative user of the company - self.assertEqual(self.invoice.signing_user, self.user, - "The signing user should be the one that posted the invoice even if a representative has been added later on") - self.assertEqual(self.invoice.signature, self.signature_fake_1, "The signature should be from `self.user`") diff --git a/addons/at_accounting/tests/test_tour.py b/addons/at_accounting/tests/test_tour.py deleted file mode 100644 index 3033b44..0000000 --- a/addons/at_accounting/tests/test_tour.py +++ /dev/null @@ -1,48 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import odoo.tests - -from odoo import Command -from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon - - -@odoo.tests.tagged('post_install_l10n', 'post_install', '-at_install') -class TestAccountantTours(AccountTestInvoicingHttpCommon): - def test_account_merge_wizard_tour(self): - companies = self.env['res.company'].create([ - {'name': 'tour_company_1'}, - {'name': 'tour_company_2'}, - ]) - - self.env['account.account'].create([ - { - 'company_ids': [Command.set(companies[0].ids)], - 'code': "100001", - 'name': "Current Assets", - 'account_type': 'asset_current', - }, - { - 'company_ids': [Command.set(companies[0].ids)], - 'code': "100002", - 'name': "Non-Current Assets", - 'account_type': 'asset_non_current', - }, - { - 'company_ids': [Command.set(companies[1].ids)], - 'code': "200001", - 'name': "Current Assets", - 'account_type': 'asset_current', - }, - { - 'company_ids': [Command.set(companies[1].ids)], - 'code': "200002", - 'name': "Non-Current Assets", - 'account_type': 'asset_non_current', - }, - ]) - - self.env.ref('base.user_admin').write({ - 'company_id': companies[0].id, - 'company_ids': [Command.set(companies.ids)], - }) - self.start_tour("/odoo", 'account_merge_wizard_tour', login="admin", cookies={"cids": f"{companies[0].id}-{companies[1].id}"}) diff --git a/addons/at_accounting/tests/test_ui.py b/addons/at_accounting/tests/test_ui.py deleted file mode 100644 index 6126704..0000000 --- a/addons/at_accounting/tests/test_ui.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import logging - -from odoo import Command, fields -from odoo.addons.account.tests.common import AccountTestMockOnlineSyncCommon -import odoo.tests - -_logger = logging.getLogger(__name__) - - -@odoo.tests.tagged('-at_install', 'post_install') -class TestUi(AccountTestMockOnlineSyncCommon): - def test_accountant_tour(self): - # Reset country and fiscal country, so that fields added by localizations are - # hidden and non-required, and don't make the tour crash. - # Also remove default taxes from the company and its accounts, to avoid inconsistencies - # with empty fiscal country. - self.env.company.write({ - 'country_id': None, # Also resets account_fiscal_country_id - 'account_sale_tax_id': None, - 'account_purchase_tax_id': None, - }) - - # An unconfigured bank journal is required for the connect bank step - self.env['account.journal'].create({ - 'type': 'bank', - 'name': 'Empty Bank', - 'code': 'EBJ', - }) - - account_with_taxes = self.env['account.account'].search([('tax_ids', '!=', False), ('company_ids', '=', self.env.company.id)]) - account_with_taxes.write({ - 'tax_ids': [Command.clear()], - }) - # This tour doesn't work with demo data on runbot - all_moves = self.env['account.move'].search([('company_id', '=', self.env.company.id), ('move_type', '!=', 'entry')]) - all_moves.filtered(lambda m: not m.inalterable_hash and not m.deferred_move_ids and m.state != 'draft').button_draft() - all_moves.with_context(force_delete=True).unlink() - # We need at least two bank statement lines to reconcile for the tour. - bnk = self.env['account.account'].create({ - 'code': 'X1014', - 'name': 'Bank Current Account - (test)', - 'account_type': 'asset_cash', - }) - journal = self.env['account.journal'].create({ - 'name': 'Bank - Test', - 'code': 'TBNK', - 'type': 'bank', - 'default_account_id': bnk.id, - }) - self.env['account.bank.statement.line'].create([{ - 'journal_id': journal.id, - 'amount': 100, - 'date': fields.Date.today(), - 'payment_ref': 'stl_0001', - }, { - 'journal_id': journal.id, - 'amount': 200, - 'date': fields.Date.today(), - 'payment_ref': 'stl_0002', - }]) - self.start_tour("/odoo", 'account_accountant_tour', login="admin") diff --git a/addons/at_accounting/views/account_account_views.xml b/addons/at_accounting/views/account_account_views.xml deleted file mode 100644 index 4e5caaf..0000000 --- a/addons/at_accounting/views/account_account_views.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - Account Tags - account.account.tag - - -

    - Add a new tag -

    -
    -
    - - - Account Groups - account.group - list,form - - -

    - Create a new account group -

    -
    -
    - - - account.account.form - account.account - - - - - - - - account.account.reports.form - account.account - - - - - - - - - - - - - - - - - - - - - - - account.account.form - account.account - - - - - - - - -
    - - - - - - - - - - - diff --git a/addons/at_accounting/views/account_activity.xml b/addons/at_accounting/views/account_activity.xml deleted file mode 100644 index 68b45cf..0000000 --- a/addons/at_accounting/views/account_activity.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - account.move.form.vat.return - account.move - - - - - - - - - - - - - -
    -
    - - - - - - - -
    -
    - - - account.asset.group.list - account.asset.group - - - - - - - - -
    diff --git a/addons/at_accounting/views/account_asset_views.xml b/addons/at_accounting/views/account_asset_views.xml deleted file mode 100644 index 9db69d8..0000000 --- a/addons/at_accounting/views/account_asset_views.xml +++ /dev/null @@ -1,517 +0,0 @@ - - - - - account.asset.form - account.asset - 1 - -
    - - - - - - - - -
    -
    - -
    - - - - -
    - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - -
    -
    - - - account.asset.kanban - account.asset - - - - - -
    - -
    - -
    -
    -
    -
    - Original Value -
    -
    - -
    -
    -
    -
    - Acquisition Date -
    -
    - -
    -
    -
    -
    - Duration -
    -
    - -
    -
    -
    -
    - Model -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - - account.asset.list - account.asset - - - - - - - - - - - - - - - - - - - - - - - - - account.asset.model.list - account.asset - - - - - - - - - - - - - - - - account.asset.search - account.asset - - - - - - - - - - - - - - - - - - - - - - - - - account.asset.model.search - account.asset - 100 - - - - - - - - - - - - - - - - Assets - account.asset - - [('state', '!=', 'model'), ('parent_id', '=', False)] - -

    - Create new asset -

    -
    -
    - - - Asset Models - account.asset - list,kanban,form - - - [('state', '=', 'model')] - {'default_state': 'model'} - -

    - Create new asset model -

    -
    -
    - - - - - - Confirm - - - - code - -if records: - action = records.filtered(lambda asset: asset.state == 'draft').validate() - - - - - Compute Depreciation - - - - list - code - -if records: - action = records.filtered(lambda asset: asset.state == 'draft').compute_depreciation_board() - - - - - - - - - - - account.move.line.list.asset - account.move.line - primary - - - hide - hide - hide - hide - hide - show - - - -
    diff --git a/addons/at_accounting/views/account_bank_statement_import_view.xml b/addons/at_accounting/views/account_bank_statement_import_view.xml deleted file mode 100644 index 2fb4234..0000000 --- a/addons/at_accounting/views/account_bank_statement_import_view.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - account.journal.dashboard.kanban.inherit - account.journal - - - -
    - -
    -
    -
    -
    - - - account.bank.statement.list - account.bank.statement - - - - account_tree - - - -
    diff --git a/addons/at_accounting/views/account_fiscal_year_view.xml b/addons/at_accounting/views/account_fiscal_year_view.xml deleted file mode 100644 index d41d4bf..0000000 --- a/addons/at_accounting/views/account_fiscal_year_view.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - Fiscal Years - account.fiscal.year - list,form - -

    - Click here to create a new fiscal year. -

    -
    -
    - - - account.fiscal.year.form - account.fiscal.year - -
    - - - - - - - - - -
    -
    - - - account.fiscal.year.search - account.fiscal.year - - - - - - - - - account.fiscal.year.list - account.fiscal.year - - - - - - - - - -
    diff --git a/addons/at_accounting/views/account_journal_dashboard_views.xml b/addons/at_accounting/views/account_journal_dashboard_views.xml deleted file mode 100644 index 7220154..0000000 --- a/addons/at_accounting/views/account_journal_dashboard_views.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - account.journal.dashboard.kanban - account.journal - - - - - - - - - - - - - Last Statement - - - - - -
    - -
    - -
    -
    -
    -
    - - - -
    -
    - - account.journal.dashboard.kanban.reports - account.journal - - - - - - - - - - -
    - Balance -
    -
    - - -
    -
    - Reconciliation -
    - -
    -
    - - - - Never miss a tax deadline. - - - - - - -
    -
    -
    diff --git a/addons/at_accounting/views/account_move_views.xml b/addons/at_accounting/views/account_move_views.xml deleted file mode 100644 index 5314b41..0000000 --- a/addons/at_accounting/views/account_move_views.xml +++ /dev/null @@ -1,304 +0,0 @@ - - - - account.move.line.list - account.move.line - - extension - - - account_move_line_list - - -
    -
    - - -
    - - - - - - matching_link_widget - -
    -
    - - - account.move.line.payment.list - account.move.line - - extension - - - - - - - - - - - - - - - - account.move.line.deferral.entries.list - account.move.line - - primary - - - date - - - hide - - - hide - - - hide - - - 1 - - - 1 - - - 1 - - - - - - - account.archived.tax.tag.list - account.move.line - - primary - - - show - - - show - - - hide - - - - - - account.journal.report.audit.move.line.list - account.move.line - - primary - - - account_move_line_journal_report_list - - - - - - - - - - - - - - - - - - - account.move.line.form - account.move.line - - - - - - - - - - - - Create Asset - - - - code - - if records: - action = records.turn_as_asset() - - -
    diff --git a/addons/at_accounting/views/account_payment_views.xml b/addons/at_accounting/views/account_payment_views.xml deleted file mode 100644 index 99050a4..0000000 --- a/addons/at_accounting/views/account_payment_views.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - account.payment.form.inherit.account_accountant - account.payment - - - - account.group_account_user - - - - - - - - - - - diff --git a/addons/at_accounting/views/account_reconcile_views.xml b/addons/at_accounting/views/account_reconcile_views.xml deleted file mode 100644 index dd6f4f6..0000000 --- a/addons/at_accounting/views/account_reconcile_views.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - account.move.line.reconcile.search - account.move.line - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - account.move.line.list.reconcile - account.move.line - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - - - Reconcile automatically - account.auto.reconcile.wizard - form - new - - - - - Journal Items to reconcile - account.move.line - reconcile - list - - - [('display_type', 'not in', ('line_section', 'line_note')), ('account_id.reconcile', '=', True), ('parent_state', '=', 'posted'), ('full_reconcile_id', '=', False)] - {'journal_type': 'general', 'search_default_unreconciled': True, 'search_default_group_by_account': True, 'search_default_group_by_partner': True} - -
    diff --git a/addons/at_accounting/views/account_report_view.xml b/addons/at_accounting/views/account_report_view.xml deleted file mode 100644 index dad22bd..0000000 --- a/addons/at_accounting/views/account_report_view.xml +++ /dev/null @@ -1,398 +0,0 @@ - - - - - - account.report.list - account.report - - - - - - - - - - - - account.report.tree.configure.start.dates - account.report - - - - - - - - - - - account.report.add.sections.list - account.report - - - - - - - - - - - - account.report.search - account.report - 100 - - - - - - - - - - - - - - - account.view.coa - account.account - - - - - - - - - - - - - - - - account.report.form - account.report - -
    -
    - -
    - - - - - -
    -

    - -

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - account.report.line.form - account.report.line - -
    - -
    -

    - -

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - account.report.expression.form - account.report.expression - -
    - -
    -

    - -

    -
    - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - account.report.horizontal.group.form - account.report.horizontal.group - -
    - -
    -

    - -

    -
    - - - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - account.report.horizontal.group.list - account.report.horizontal.group - - - - - - - - - - account.report.external.value.list - account.report.external.value - - - - - - - - - - - - - - - - - - account.report.budget.list - account.report.budget - - - - - - - - - - - account.report.budget.form - account.report.budget - -
    - -
    -

    - -

    -
    - - - - - - - - - - - - -
    - -
    -
    - - - Create Composite Report - - - list - - code - -if records: - action = records.action_create_composite_report() - - - -
    -
    diff --git a/addons/at_accounting/views/account_tax_views.xml b/addons/at_accounting/views/account_tax_views.xml deleted file mode 100644 index 76079df..0000000 --- a/addons/at_accounting/views/account_tax_views.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - account.tax.unit.form - account.tax.unit - -
    - - - -
    -

    - -

    -
    - - - - - - - - - - - - - - -
    - -
    -
    - - - account.tax.unit.list - account.tax.unit - - - - - - - - - - Tax Units - account.tax.unit - list,form - - - - - -
    diff --git a/addons/at_accounting/views/at_accounting_menuitems.xml b/addons/at_accounting/views/at_accounting_menuitems.xml deleted file mode 100644 index fa596fb..0000000 --- a/addons/at_accounting/views/at_accounting_menuitems.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/addons/at_accounting/views/bank_rec_widget_views.xml b/addons/at_accounting/views/bank_rec_widget_views.xml deleted file mode 100644 index 5e8299e..0000000 --- a/addons/at_accounting/views/bank_rec_widget_views.xml +++ /dev/null @@ -1,469 +0,0 @@ - - - - - account.bank.statement.form.bank_rec_widget - account.bank.statement - 10 - -
    - - -
    - -
    - - - - - - - - - - - -
    -
    - - - - - - - - Create Statement - account.bank.statement - form - - new - {'dialog_size': 'medium', 'show_running_balance_latest': True} - - - - - account.bank.statement.line.search.bank_rec_widget - account.bank.statement.line - 999 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - account.bank.statement.line.kanban.bank_rec_widget - account.bank.statement.line - - - - - - - - - - - - - - - - - -
    - - - -
    - - - -
    - -
    -
    - - -
    - -
    - To check -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - account.bank.statement.line.list.bank_rec_widget - account.bank.statement.line - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - account.bank.statement.line.form.bank_rec_widget - account.bank.statement.line - -
    - - - - - - - - - - - - - - - - - - - - -
    -
    - -
    -
    - - - - account.bank.statement.line.form.bank_rec_widget - account.bank.statement.line - -
    - - - - - - - - - - -
    - - - New Transaction - account.bank.statement.line - form - - new - - - - - Bank Reconciliation - account.bank.statement.line - list,kanban - - - reconciliation_list - [('state', '!=', 'cancel')] - {'default_journal_id': active_id, 'search_default_journal_id': active_id} - -

    Nothing to do here!

    -

    No transactions matching your filters were found.

    -
    -
    - - - Bank Reconciliation - account.bank.statement.line - kanban,list - - - reconciliation - [('state', '!=', 'cancel')] - {'default_journal_id': active_id, 'search_default_journal_id': active_id} - -

    Nothing to do here!

    -

    No transactions matching your filters were found.

    -
    -
    - - - account.move.form.bank_rec_widget - account.move - 999 - -
    - - -
    -
    - - - - account.move.line.search.bank_rec_widget - account.move.line - 999 - - - - - - - - - - - - - - - - - - - - - - - account.move.line.list.bank_rec_widget - account.move.line - 999 - - - - - - - - - - - - - - - - - - - - - - -
    - - - - 0 - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - -
    -