Compare commits
243 Commits
cetmix_tow
...
18.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 45eaa252bd | |||
| 45222f6bb7 | |||
| d9715fff07 | |||
| 5227767283 | |||
| f76d79a606 | |||
| 14fd3e2f1e | |||
| e3ce2be1f4 | |||
| 0329122548 | |||
| f31284a113 | |||
| dcccf8b034 | |||
| 9751faf173 | |||
| 0d94303107 | |||
| fcd0cfb26f | |||
| 48863fc6d5 | |||
| 8ba35217bf | |||
| afe168723d | |||
| 72273b580f | |||
| 095ed48ded | |||
| 3a18336cc4 | |||
| 33c3ae3585 | |||
| aa7c94b0dc | |||
| e30a37898b | |||
| 88e1fa7965 | |||
| 9f19b53414 | |||
| 26fa9e2871 | |||
| 825423b3af | |||
| 06a1e15a4f | |||
| 23c6cf64cf | |||
| ae9617dedd | |||
| 2884546072 | |||
| d5ae939266 | |||
| 89047fe79c | |||
| d323267a28 | |||
| edf915943b | |||
| d7c9aee436 | |||
| e497c2b5d0 | |||
| 582b9e1b9b | |||
| 2b1c7afdf1 | |||
| 882ba9247a | |||
| 0671d0b2e9 | |||
| 509006cce4 | |||
| 44c55e900a | |||
| db83188b88 | |||
| aa4d44164c | |||
| ea9d3ff61f | |||
| f63dd818aa | |||
| 27f8b2541e | |||
| 558a815535 | |||
| 6d6add697b | |||
| bb235faf39 | |||
| 3951faca64 | |||
| 3db314cedb | |||
| 3e4faf0dc7 | |||
| a70964ad21 | |||
| d0a79ac5a6 | |||
| 04c6e4d523 | |||
| 3d2cbfcb74 | |||
| 320f8e5bbf | |||
| 4d495e5cc5 | |||
| dbb7322fce | |||
| ad78baae06 | |||
| b3f9c9f989 | |||
| 773476029f | |||
| bdf313b39e | |||
| 0b3fafc478 | |||
| ebe63f69ab | |||
| a59bf97a36 | |||
| a64c72a5d4 | |||
| 8b0f9d6361 | |||
| aa5439325d | |||
| b1dbab9942 | |||
| 43ae1490f5 | |||
| 2a07cab00b | |||
| 08fe99a3dd | |||
| 1dae9452af | |||
| 0c15c42d4f | |||
| 7a0ffe51cd | |||
| effc02f01b | |||
| e585583bf5 | |||
| eab73a9964 | |||
| fe62898189 | |||
| dd454b6269 | |||
| 3a469c333d | |||
| 3fe3d5944a | |||
| 42ecceac62 | |||
| f5a379f683 | |||
| 834b292f73 | |||
| d29c58ac6c | |||
| 1095317b23 | |||
| 80ff9d095b | |||
| e110187874 | |||
| 0786f81d63 | |||
| dcb6ca59fc | |||
| 3eb6ee6758 | |||
| 97753affb8 | |||
| e19d374f2b | |||
| 30a33e939f | |||
| d2e38977c2 | |||
| fa3cb047c8 | |||
| 8441d1956d | |||
| 633253de00 | |||
| 9dfa1d4d03 | |||
| 4350577ab7 | |||
| 63becde73e | |||
| 65dc01b15e | |||
| d0e0bb7b0c | |||
| 26ce32efb8 | |||
| aa9004b2ef | |||
| cd8b9c7975 | |||
| 500026c640 | |||
| bd2ee6d072 | |||
| da58849aae | |||
| 570140673c | |||
| 0e56aa652c | |||
| f3b0ba4632 | |||
| 2462220921 | |||
| 17efcd0fbc | |||
| 5d3db75b14 | |||
| ea03f85024 | |||
| be600989ba | |||
| 362fe7126a | |||
| 439ebf4356 | |||
| 9b50ec37a6 | |||
| 829a0fe36f | |||
| 6fc36fd5c5 | |||
| cbc31eaf55 | |||
| d63a402aaf | |||
| 31047a2670 | |||
| 24adc03ab3 | |||
| 4e051d6c52 | |||
| 2ebb0a73c2 | |||
| 906671a8b5 | |||
| a401dc3abd | |||
| 124377f7c5 | |||
| 90836e2f2a | |||
| 90d9a2b202 | |||
| c62d637f56 | |||
| ce90945990 | |||
| bb42154f35 | |||
| 3fd4eb215e | |||
| 543f423825 | |||
| 3d2ad55cfa | |||
| 50cf4b4107 | |||
| 8b6528cb8b | |||
| a55dafa6e0 | |||
| 24c89d97e5 | |||
| a7c36be076 | |||
| 2ef98a3897 | |||
| e21d4e66a9 | |||
| 38c91ef94c | |||
| 454e6936fb | |||
| 6d49162c09 | |||
| 5bd5c7b906 | |||
| a4de8697da | |||
| ce8f7f8711 | |||
| ca0ca65f14 | |||
| 30f2cf9b0e | |||
| f68e2d23e7 | |||
| 05c33d06c3 | |||
| 46fca55d81 | |||
| 4db469f273 | |||
| 72a652a5c6 | |||
| 5523995594 | |||
| 7b7bcf73e6 | |||
| acb3564750 | |||
| bb3359a309 | |||
| 8b1544647a | |||
| f727f3ab67 | |||
| a371ac9bdc | |||
| f84c891e96 | |||
| c31947bfab | |||
| d783d0d08d | |||
| 7a9e3c56fe | |||
| 11770d4950 | |||
| 0d150fa92e | |||
| 2933122464 | |||
| 5ab0886edf | |||
| e2045b21e2 | |||
| 596bd0a761 | |||
| 2a19267d2f | |||
| 50105c5ba6 | |||
| 434b1d46ab | |||
| d4945d20ee | |||
| 6e05ee4c57 | |||
| 33ba2de5dd | |||
| bb075fe036 | |||
| 60687c611a | |||
| 776bcc1f7f | |||
| 61061bdee9 | |||
| 77d467de70 | |||
| e4a63ec0e4 | |||
| 77dc1762c0 | |||
| e2bf6c22f9 | |||
| f9b4e1582a | |||
| 53a05d2240 | |||
| 8512a1d196 | |||
| 41a130c1d6 | |||
| 13b8322e3c | |||
| d18265433c | |||
| 16e661f8e8 | |||
| 8a40edf6c6 | |||
| 831cbe1449 | |||
| aea1722933 | |||
| 51a39b2e6e | |||
| 21e6a6f3f6 | |||
| dbca7e34e1 | |||
| ac8400512b | |||
| 817b05fe06 | |||
| cb1c9a1ffa | |||
| bc7dbb494a | |||
| 7827e2f224 | |||
| c4edb6b3af | |||
| bdc66120f3 | |||
| 636938581d | |||
| 2c3cfc3978 | |||
| 0236ef0809 | |||
| de9fd7b71d | |||
| eaeb978c19 | |||
| 2e55efb312 | |||
| b786492d5e | |||
| 6086717e66 | |||
| 375466beea | |||
| 8dfe9e9874 | |||
| 385ef060ed | |||
| 6c8b3cbd74 | |||
| 90d1e47e03 | |||
| 9700c192d0 | |||
| 25596b4149 | |||
| 0885b59bb3 | |||
| fcbe9a17db | |||
| 860fedb7e0 | |||
| 85ada8e9b7 | |||
| 6b98bf8be1 | |||
| 55fb716490 | |||
|
|
ab214ac9dd | ||
|
|
304da43eb8 | ||
| 48b0b7a283 | |||
| 40c3e1d471 | |||
| c1ecf1289d | |||
| 0741834b31 | |||
| 7a2debb3d7 | |||
| 6ef9d029eb | |||
|
|
96edc0c694 |
105
addons/at_accounting/__init__.py
Normal file
105
addons/at_accounting/__init__.py
Normal file
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
178
addons/at_accounting/__manifest__.py
Normal file
178
addons/at_accounting/__manifest__.py
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
'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'],
|
||||
}
|
||||
BIN
addons/at_accounting/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/at_accounting/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
1
addons/at_accounting/controllers/__init__.py
Normal file
1
addons/at_accounting/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
89
addons/at_accounting/controllers/main.py
Normal file
89
addons/at_accounting/controllers/main.py
Normal file
@@ -0,0 +1,89 @@
|
||||
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/<models("ir.attachment"):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)
|
||||
160
addons/at_accounting/data/account_report_actions.xml
Normal file
160
addons/at_accounting/data/account_report_actions.xml
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="action_account_report_cs" model="ir.actions.client">
|
||||
<field name="name">Cash Flow Statement</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.cash_flow_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_bs" model="ir.actions.client">
|
||||
<field name="name">Balance Sheet</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">balance-sheet</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.balance_sheet')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_exec_summary" model="ir.actions.client">
|
||||
<field name="name">Executive Summary</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">executive-summary</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.executive_summary')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_pl" model="ir.actions.client">
|
||||
<field name="name">Profit and Loss</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">profit-and-loss</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.profit_and_loss')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_gt" model="ir.actions.client">
|
||||
<field name="name">Tax Return</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">tax-report</field>
|
||||
<field name="context" eval="{'report_id': ref('account.generic_tax_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_ja" model="ir.actions.client">
|
||||
<field name="name">Journal Audit</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">journal-report</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.journal_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_general_ledger" model="ir.actions.client">
|
||||
<field name="name">General Ledger</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">general-ledger</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.general_ledger_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_multicurrency_revaluation" model="ir.actions.client">
|
||||
<field name="name">Unrealized Currency Gains/Losses</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.multicurrency_revaluation_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_ar" model="ir.actions.client">
|
||||
<field name="name">Aged Receivable</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">aged-receivable</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.aged_receivable_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_ap" model="ir.actions.client">
|
||||
<field name="name">Aged Payable</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">aged-payable</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.aged_payable_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_coa" model="ir.actions.client">
|
||||
<field name="name">Trial Balance</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">trial-balance</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.trial_balance_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_partner_ledger" model="ir.actions.client">
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">partner-ledger</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.partner_ledger_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_sales" model="ir.actions.client">
|
||||
<field name="name">EC Sales List</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.generic_ec_sales_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_deferred_expense" model="ir.actions.client">
|
||||
<field name="name">Deferred Expense</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">deferred-expense</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.deferred_expense_report')}"/>
|
||||
</record>
|
||||
<record id="action_account_report_deferred_revenue" model="ir.actions.client">
|
||||
<field name="name">Deferred Revenue</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="path">deferred-revenue</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.deferred_revenue_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="account_financial_current_year_earnings0" model="account.report.line">
|
||||
<field name="action_id" ref="action_account_report_pl"/>
|
||||
</record>
|
||||
|
||||
<record id="account_financial_report_executivesummary_profitability0" model="account.report.line">
|
||||
<field name="action_id" ref="action_account_report_pl"/>
|
||||
</record>
|
||||
|
||||
<record id="account_financial_report_executivesummary_balancesheet0" model="account.report.line">
|
||||
<field name="action_id" ref="action_account_report_bs"/>
|
||||
</record>
|
||||
|
||||
<record id="action_create_report_menu" model="ir.actions.server">
|
||||
<field name="name">Create Menu Item</field>
|
||||
<field name="model_id" ref="account.model_account_report"/>
|
||||
<field name="binding_model_id" ref="account.model_account_report"/>
|
||||
<field name="state">code</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
action = records._create_menu_item_for_report()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_tree" model="ir.actions.act_window">
|
||||
<field name="name">Accounting Reports</field>
|
||||
<field name="res_model">account.report</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="account_report_tree"/>
|
||||
<field name="search_view_id" ref="view_account_report_search"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_horizontal_groups" model="ir.actions.act_window">
|
||||
<field name="name">Horizontal Groups</field>
|
||||
<field name="res_model">account.report.horizontal.group</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="account_report_horizontal_group_tree"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_bank_reconciliation" model="ir.actions.client">
|
||||
<field name="name">Bank Reconciliation</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.bank_reconciliation_report')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_report_budget_tree" model="ir.actions.act_window">
|
||||
<field name="name">Financial Budgets</field>
|
||||
<field name="res_model">account.report.budget</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="account_report_budget_tree"/>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
13
addons/at_accounting/data/account_report_actions_depr.xml
Normal file
13
addons/at_accounting/data/account_report_actions_depr.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_account_report_assets" model="ir.actions.client">
|
||||
<field name="name">Depreciation Schedule</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="context" eval="{'report_id': ref('at_accounting.assets_report')}"/>
|
||||
</record>
|
||||
<menuitem id="menu_action_account_report_assets"
|
||||
name="Depreciation Schedule"
|
||||
action="action_account_report_assets"
|
||||
parent="account.account_reports_management_menu"
|
||||
groups="account.group_account_readonly"/>
|
||||
</odoo>
|
||||
326
addons/at_accounting/data/aged_partner_balance.xml
Normal file
326
addons/at_accounting/data/aged_partner_balance.xml
Normal file
@@ -0,0 +1,326 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="aged_receivable_report" model="account.report">
|
||||
<field name="name">Aged Receivable</field>
|
||||
<field name="filter_date_range" eval="False"/>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_partner" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_account_type">receivable</field>
|
||||
<field name="filter_hierarchy">never</field>
|
||||
<field name="filter_show_draft" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="default_opening_date_filter">today</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_aged_receivable_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="aged_receivable_report_invoice_date" model="account.report.column">
|
||||
<field name="name">Invoice Date</field>
|
||||
<field name="expression_label">invoice_date</field>
|
||||
<field name="figure_type">date</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_amount_currency" model="account.report.column">
|
||||
<field name="name">Amount Currency</field>
|
||||
<field name="expression_label">amount_currency</field>
|
||||
</record>
|
||||
<record id="aged_receivable_report_currency" model="account.report.column">
|
||||
<field name="name">Currency</field>
|
||||
<field name="expression_label">currency</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="aged_receivable_report_account_name" model="account.report.column">
|
||||
<field name="name">Account</field>
|
||||
<field name="expression_label">account_name</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="aged_receivable_report_period0" model="account.report.column">
|
||||
<field name="name">At Date</field>
|
||||
<field name="expression_label">period0</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_period1" model="account.report.column">
|
||||
<field name="name">Period 1</field>
|
||||
<field name="expression_label">period1</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_period2" model="account.report.column">
|
||||
<field name="name">Period 2</field>
|
||||
<field name="expression_label">period2</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_period3" model="account.report.column">
|
||||
<field name="name">Period 3</field>
|
||||
<field name="expression_label">period3</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_period4" model="account.report.column">
|
||||
<field name="name">Period 4</field>
|
||||
<field name="expression_label">period4</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_period5" model="account.report.column">
|
||||
<field name="name">Older</field>
|
||||
<field name="expression_label">period5</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_report_total" model="account.report.column">
|
||||
<field name="name">Total</field>
|
||||
<field name="expression_label">total</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="aged_receivable_line" model="account.report.line">
|
||||
<field name="name">Aged Receivable</field>
|
||||
<field name="groupby">partner_id, id</field>
|
||||
<field name="expression_ids">
|
||||
<record id="aged_receivable_line_invoice_date" model="account.report.expression">
|
||||
<field name="label">invoice_date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">invoice_date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_amount_currency_forced_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">currency_id</field>
|
||||
</record>
|
||||
<record id="aged_receivable_line_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_account_name" model="account.report.expression">
|
||||
<field name="label">account_name</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">account_name</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_period0" model="account.report.expression">
|
||||
<field name="label">period0</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">period0</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_period1" model="account.report.expression">
|
||||
<field name="label">period1</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">period1</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_period2" model="account.report.expression">
|
||||
<field name="label">period2</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">period2</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_period3" model="account.report.expression">
|
||||
<field name="label">period3</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">period3</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_period4" model="account.report.expression">
|
||||
<field name="label">period4</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">period4</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_period5" model="account.report.expression">
|
||||
<field name="label">period5</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">period5</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_receivable_line_total" model="account.report.expression">
|
||||
<field name="label">total</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_receivable</field>
|
||||
<field name="subformula">total</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="aged_payable_report" model="account.report">
|
||||
<field name="name">Aged Payable</field>
|
||||
<field name="filter_date_range" eval="False"/>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_partner" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_account_type">payable</field>
|
||||
<field name="filter_hierarchy">never</field>
|
||||
<field name="filter_show_draft" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="default_opening_date_filter">today</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_aged_payable_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="aged_payable_report_invoice_date" model="account.report.column">
|
||||
<field name="name">Invoice Date</field>
|
||||
<field name="expression_label">invoice_date</field>
|
||||
<field name="figure_type">date</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_amount_currency" model="account.report.column">
|
||||
<field name="name">Amount Currency</field>
|
||||
<field name="expression_label">amount_currency</field>
|
||||
</record>
|
||||
<record id="aged_payable_report_currency" model="account.report.column">
|
||||
<field name="name">Currency</field>
|
||||
<field name="expression_label">currency</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="aged_payable_report_account_name" model="account.report.column">
|
||||
<field name="name">Account</field>
|
||||
<field name="expression_label">account_name</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="aged_payable_report_period0" model="account.report.column">
|
||||
<field name="name">At Date</field>
|
||||
<field name="expression_label">period0</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_period1" model="account.report.column">
|
||||
<field name="name">Period 1</field>
|
||||
<field name="expression_label">period1</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_period2" model="account.report.column">
|
||||
<field name="name">Period 2</field>
|
||||
<field name="expression_label">period2</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_period3" model="account.report.column">
|
||||
<field name="name">Period 3</field>
|
||||
<field name="expression_label">period3</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_period4" model="account.report.column">
|
||||
<field name="name">Period 4</field>
|
||||
<field name="expression_label">period4</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_period5" model="account.report.column">
|
||||
<field name="name">Older</field>
|
||||
<field name="expression_label">period5</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_report_total" model="account.report.column">
|
||||
<field name="name">Total</field>
|
||||
<field name="expression_label">total</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="aged_payable_line" model="account.report.line">
|
||||
<field name="name">Aged Payable</field>
|
||||
<field name="groupby">partner_id, id</field>
|
||||
<field name="expression_ids">
|
||||
<record id="aged_payable_line_invoice_date" model="account.report.expression">
|
||||
<field name="label">invoice_date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">invoice_date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_amount_currency_forced_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">currency_id</field>
|
||||
</record>
|
||||
<record id="aged_payable_line_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_account_name" model="account.report.expression">
|
||||
<field name="label">account_name</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">account_name</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_period0" model="account.report.expression">
|
||||
<field name="label">period0</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">period0</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_period1" model="account.report.expression">
|
||||
<field name="label">period1</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">period1</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_period2" model="account.report.expression">
|
||||
<field name="label">period2</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">period2</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_period3" model="account.report.expression">
|
||||
<field name="label">period3</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">period3</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_period4" model="account.report.expression">
|
||||
<field name="label">period4</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">period4</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_period5" model="account.report.expression">
|
||||
<field name="label">period5</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">period5</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="aged_payable_line_total" model="account.report.expression">
|
||||
<field name="label">total</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_aged_payable</field>
|
||||
<field name="subformula">total</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
70
addons/at_accounting/data/assets_reports.xml
Normal file
70
addons/at_accounting/data/assets_reports.xml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="assets_report" model="account.report">
|
||||
<field name="name">Depreciation Schedule</field>
|
||||
<field name="filter_hierarchy">optional</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_asset_report_handler"/>
|
||||
<field name="load_more_limit" eval="80"/>
|
||||
<field name="column_ids">
|
||||
<record id="assets_report_acquisition_date" model="account.report.column">
|
||||
<field name="name">Acquisition Date</field>
|
||||
<field name="expression_label">acquisition_date</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="assets_report_first_depreciation" model="account.report.column">
|
||||
<field name="name">First Depreciation</field>
|
||||
<field name="expression_label">first_depreciation</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="assets_report_first_method" model="account.report.column">
|
||||
<field name="name">Method</field>
|
||||
<field name="expression_label">method</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="assets_report_duration_rate" model="account.report.column">
|
||||
<field name="name">Duration / Rate</field>
|
||||
<field name="expression_label">duration_rate</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="assets_report_date_from" model="account.report.column">
|
||||
<field name="name">date from</field>
|
||||
<field name="expression_label">assets_date_from</field>
|
||||
</record>
|
||||
<record id="assets_report_assets_plus" model="account.report.column">
|
||||
<field name="name">+</field>
|
||||
<field name="expression_label">assets_plus</field>
|
||||
</record>
|
||||
<record id="assets_report_assets_minus" model="account.report.column">
|
||||
<field name="name">-</field>
|
||||
<field name="expression_label">assets_minus</field>
|
||||
</record>
|
||||
<record id="assets_report_assets_date_to" model="account.report.column">
|
||||
<field name="name">date to</field>
|
||||
<field name="expression_label">assets_date_to</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_date_from" model="account.report.column">
|
||||
<field name="name">date from</field>
|
||||
<field name="expression_label">depre_date_from</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_plus" model="account.report.column">
|
||||
<field name="name">+</field>
|
||||
<field name="expression_label">depre_plus</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_minus" model="account.report.column">
|
||||
<field name="name">-</field>
|
||||
<field name="expression_label">depre_minus</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_date_to" model="account.report.column">
|
||||
<field name="name">date to</field>
|
||||
<field name="expression_label">depre_date_to</field>
|
||||
</record>
|
||||
<record id="assets_report_balance" model="account.report.column">
|
||||
<field name="name">book_value</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
32
addons/at_accounting/data/at_accounting_data.xml
Normal file
32
addons/at_accounting/data/at_accounting_data.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Switch root menu "Invoicing" to "Accounting" -->
|
||||
<!-- Top menu item -->
|
||||
<menuitem name="Accounting"
|
||||
id="menu_accounting"
|
||||
groups="account.group_account_readonly,account.group_account_invoice"
|
||||
web_icon="at_accounting,static/description/icon.png"
|
||||
sequence="60"/>
|
||||
<!-- move existing submenus to point to the new parent -->
|
||||
<record id="account.menu_finance_receivables" model="ir.ui.menu">
|
||||
<field name="parent_id" ref="menu_accounting"/>
|
||||
</record>
|
||||
<record id="account.menu_finance_payables" model="ir.ui.menu">
|
||||
<field name="parent_id" ref="menu_accounting"/>
|
||||
</record>
|
||||
<record id="account.menu_finance_entries" model="ir.ui.menu">
|
||||
<field name="parent_id" ref="menu_accounting"/>
|
||||
</record>
|
||||
<record id="account.menu_finance_reports" model="ir.ui.menu">
|
||||
<field name="parent_id" ref="menu_accounting"/>
|
||||
</record>
|
||||
<record id="account.menu_finance_configuration" model="ir.ui.menu">
|
||||
<field name="parent_id" ref="menu_accounting"/>
|
||||
</record>
|
||||
<record id="account.menu_board_journal_1" model="ir.ui.menu">
|
||||
<field name="parent_id" ref="menu_accounting"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="account.menu_account_config" name="Settings" parent="account.menu_finance_configuration" sequence="0" groups="base.group_system"/>
|
||||
|
||||
</odoo>
|
||||
11
addons/at_accounting/data/at_accounting_tour.xml
Normal file
11
addons/at_accounting/data/at_accounting_tour.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="account_accountant_tour" model="web_tour.tour">
|
||||
<field name="name">account_accountant_tour</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="rainbow_man_message"><![CDATA[
|
||||
<strong><b>Good job!</b> You went through all steps of this tour.</strong>
|
||||
<br>See how to manage your customer invoices in the <b>Customers/Invoices</b> menu
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
285
addons/at_accounting/data/balance_sheet.xml
Normal file
285
addons/at_accounting/data/balance_sheet.xml
Normal file
@@ -0,0 +1,285 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="balance_sheet" model="account.report">
|
||||
<field name="name">Balance Sheet</field>
|
||||
<field name="filter_date_range" eval="False"/>
|
||||
<field name="filter_analytic_groupby" eval="True"/>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="default_opening_date_filter">today</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_balance_sheet_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="balance_sheet_balance" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="account_financial_report_total_assets0" model="account.report.line">
|
||||
<field name="name">ASSETS</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">TA</field>
|
||||
<field name="horizontal_split_side">left</field>
|
||||
<field name="aggregation_formula">CA.balance + FA.balance + PNCA.balance</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_current_assets_view0" model="account.report.line">
|
||||
<field name="name">Current Assets</field>
|
||||
<field name="code">CA</field>
|
||||
<field name="aggregation_formula">BA.balance + REC.balance + CAS.balance + PRE.balance</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_bank_view0" model="account.report.line">
|
||||
<field name="name">Bank and Cash Accounts</field>
|
||||
<field name="code">BA</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_cash')])</field>
|
||||
</record>
|
||||
<record id="account_financial_report_receivable0" model="account.report.line">
|
||||
<field name="name">Receivables</field>
|
||||
<field name="code">REC</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_receivable'), ('account_id.non_trade', '=', False)])</field>
|
||||
</record>
|
||||
<record id="account_financial_report_current_assets0" model="account.report.line">
|
||||
<field name="name">Current Assets</field>
|
||||
<field name="code">CAS</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="domain_formula">sum(['|', ('account_id.account_type', '=', 'asset_current'), '&', ('account_id.account_type', '=', 'asset_receivable'), ('account_id.non_trade', '=', True)])</field>
|
||||
</record>
|
||||
<record id="account_financial_report_prepayements0" model="account.report.line">
|
||||
<field name="name">Prepayments</field>
|
||||
<field name="code">PRE</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_prepayments')])</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_fixed_assets_view0" model="account.report.line">
|
||||
<field name="name">Plus Fixed Assets</field>
|
||||
<field name="code">FA</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_fixed')])</field>
|
||||
</record>
|
||||
<record id="account_financial_report_non_current_assets_view0" model="account.report.line">
|
||||
<field name="name">Plus Non-current Assets</field>
|
||||
<field name="code">PNCA</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_non_current')])</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_liabilities_view0" model="account.report.line">
|
||||
<field name="name">LIABILITIES</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">L</field>
|
||||
<field name="horizontal_split_side">right</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_liabilities_view0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CL.balance + NL.balance</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_current_liabilities0" model="account.report.line">
|
||||
<field name="name">Current Liabilities</field>
|
||||
<field name="code">CL</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_current_liabilities0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CL1.balance + CL2.balance</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_current_liabilities1" model="account.report.line">
|
||||
<field name="name">Current Liabilities</field>
|
||||
<field name="code">CL1</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_current_liabilities1_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="['|', ('account_id.account_type', 'in', ('liability_current', 'liability_credit_card')), '&', ('account_id.account_type', '=', 'liability_payable'), ('account_id.non_trade', '=', True)]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_current_liabilities_payable" model="account.report.line">
|
||||
<field name="name">Payables</field>
|
||||
<field name="code">CL2</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_current_liabilities_payable_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'liability_payable'), ('account_id.non_trade', '=', False)]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_non_current_liabilities0" model="account.report.line">
|
||||
<field name="name">Plus Non-current Liabilities</field>
|
||||
<field name="code">NL</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_non_current_liabilities0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'liability_non_current')]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_equity0" model="account.report.line">
|
||||
<field name="name">EQUITY</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">EQ</field>
|
||||
<field name="horizontal_split_side">right</field>
|
||||
<field name="aggregation_formula">UNAFFECTED_EARNINGS.balance + RETAINED_EARNINGS.balance</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_unaffected_earnings0" model="account.report.line">
|
||||
<field name="name">Unallocated Earnings</field>
|
||||
<field name="code">UNAFFECTED_EARNINGS</field>
|
||||
<field name="aggregation_formula">CURR_YEAR_EARNINGS.balance + PREV_YEAR_EARNINGS.balance</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_current_year_earnings0" model="account.report.line">
|
||||
<field name="name">Current Year Unallocated Earnings</field>
|
||||
<field name="code">CURR_YEAR_EARNINGS</field>
|
||||
<field name="aggregation_formula"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_current_year_earnings_pnl" model="account.report.expression">
|
||||
<field name="label">pnl</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">NEP.balance</field>
|
||||
<field name="date_scope">from_fiscalyear</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_current_year_earnings_alloc" model="account.report.expression">
|
||||
<field name="label">alloc</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'equity_unaffected')]"/>
|
||||
<field name="date_scope">from_fiscalyear</field>
|
||||
<field name="subformula">-sum</field>
|
||||
</record>
|
||||
<record id="account_financial_current_year_earnings_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CURR_YEAR_EARNINGS.pnl + CURR_YEAR_EARNINGS.alloc</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_previous_year_earnings0" model="account.report.line">
|
||||
<field name="name">Previous Years Unallocated Earnings</field>
|
||||
<field name="code">PREV_YEAR_EARNINGS</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_previous_year_earnings0_allocated_earnings" model="account.report.expression">
|
||||
<field name="label">allocated_earnings</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'equity_unaffected')]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
</record>
|
||||
<record id="account_financial_previous_year_earnings0_balance_domain" model="account.report.expression">
|
||||
<field name="label">balance_domain</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', 'in', ('income', 'income_other', 'expense_direct_cost', 'expense', 'expense_depreciation'))]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
</record>
|
||||
<record id="account_financial_previous_year_earnings0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">PREV_YEAR_EARNINGS.balance_domain + PREV_YEAR_EARNINGS.allocated_earnings - CURR_YEAR_EARNINGS.balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_retained_earnings0" model="account.report.line">
|
||||
<field name="name">Retained Earnings</field>
|
||||
<field name="code">RETAINED_EARNINGS</field>
|
||||
<field name="aggregation_formula">CURR_RETAINED_EARNINGS.balance + PREV_RETAINED_EARNINGS.balance</field>
|
||||
<field name="groupby" eval="False"/>
|
||||
<field name="foldable" eval="False"/>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_retained_earnings_line_1" model="account.report.line">
|
||||
<field name="name">Current Year Retained Earnings</field>
|
||||
<field name="code">CURR_RETAINED_EARNINGS</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_retained_earnings_current" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'equity')]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
<field name="date_scope">from_fiscalyear</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_retained_earnings_line_2" model="account.report.line">
|
||||
<field name="name">Previous Years Retained Earnings</field>
|
||||
<field name="code">PREV_RETAINED_EARNINGS</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_retained_earnings_total" model="account.report.expression">
|
||||
<field name="label">total</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'equity')]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
</record>
|
||||
<record id="account_financial_retained_earnings_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">PREV_RETAINED_EARNINGS.total - CURR_RETAINED_EARNINGS.balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_liabilities_and_equity_view0" model="account.report.line">
|
||||
<field name="name">LIABILITIES + EQUITY</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">LE</field>
|
||||
<field name="horizontal_split_side">right</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_liabilities_and_equity_view0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">L.balance + EQ.balance</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_off_sheet" model="account.report.line">
|
||||
<field name="name">OFF BALANCE SHEET ACCOUNTS</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">OS</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="hide_if_zero" eval="1"/>
|
||||
<field name="domain_formula">-sum([('account_id.account_type', '=', 'off_balance')])</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
474
addons/at_accounting/data/bank_reconciliation_report.xml
Normal file
474
addons/at_accounting/data/bank_reconciliation_report.xml
Normal file
@@ -0,0 +1,474 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="bank_reconciliation_report" model="account.report">
|
||||
<field name="name">Bank Reconciliation Report</field>
|
||||
<field name="filter_show_draft" eval="True"/>
|
||||
<field name="filter_date_range" eval="False"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_hide_0_lines">by_default</field>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="default_opening_date_filter">today</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_bank_reconciliation_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="bank_reconciliation_report_date" model="account.report.column">
|
||||
<field name="name">Date</field>
|
||||
<field name="expression_label">date</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="bank_reconciliation_report_label" model="account.report.column">
|
||||
<field name="name">Label</field>
|
||||
<field name="expression_label">label</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="bank_reconciliation_report_amount_currency" model="account.report.column">
|
||||
<field name="name">Amount Currency</field>
|
||||
<field name="expression_label">amount_currency</field>
|
||||
<field name="figure_type">monetary</field>
|
||||
</record>
|
||||
<record id="bank_reconciliation_report_currency" model="account.report.column">
|
||||
<field name="name">Currency</field>
|
||||
<field name="expression_label">currency</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="bank_reconciliation_report_amount" model="account.report.column">
|
||||
<field name="name">Amount</field>
|
||||
<field name="expression_label">amount</field>
|
||||
<field name="figure_type">monetary</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="balance_bank" model="account.report.line">
|
||||
<field name="name">Balance of Bank</field>
|
||||
<field name="code">balance_bank</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="expression_ids">
|
||||
<record id="balance_bank_expr" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">last_statement_balance.amount + transaction_without_statement.amount + misc_operations.amount</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="balance_bank_expr_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_forced_currency_amount</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="children_ids">
|
||||
<record id="last_statement_balance" model="account.report.line">
|
||||
<field name="name">Last statement balance</field>
|
||||
<field name="code">last_statement_balance</field>
|
||||
<field name="expression_ids">
|
||||
<record id="last_statement_balance_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_last_statement_balance_amount</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="last_statement_balance_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_last_statement_balance_amount</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="children_ids">
|
||||
<record id="unreconciled_last_statement_receipts" model="account.report.line">
|
||||
<field name="name">Including Unreconciled Receipts</field>
|
||||
<field name="code">last_statement_receipts</field>
|
||||
<field name="groupby">id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="unreconciled_last_statement_receipts_date" model="account.report.expression">
|
||||
<field name="label">date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_receipts_label" model="account.report.expression">
|
||||
<field name="label">label</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">label</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_receipts_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_receipts_forced_currency_amount_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">amount_currency_currency_id</field>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_receipts_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_receipts_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_receipts_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments" model="account.report.line">
|
||||
<field name="name">Including Unreconciled Payments</field>
|
||||
<field name="code">last_statement_payments</field>
|
||||
<field name="groupby">id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="unreconciled_last_statement_payments_date" model="account.report.expression">
|
||||
<field name="label">date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments_label" model="account.report.expression">
|
||||
<field name="label">label</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">label</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments_forced_currency_amount_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">amount_currency_currency_id</field>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="unreconciled_last_statement_payments_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="transaction_without_statement" model="account.report.line">
|
||||
<field name="name">Transactions without statement</field>
|
||||
<field name="code">transaction_without_statement</field>
|
||||
<field name="expression_ids">
|
||||
<record id="transaction_without_statement_expr" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_transaction_without_statement_amount</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="transaction_without_statement_expr_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_transaction_without_statement_amount</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="children_ids">
|
||||
<record id="no_statement_unreconciled_receipt" model="account.report.line">
|
||||
<field name="name">Including Unreconciled Receipts</field>
|
||||
<field name="code">unreconciled_receipt</field>
|
||||
<field name="groupby">id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="no_statement_unreconciled_receipt_date" model="account.report.expression">
|
||||
<field name="label">date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_receipt_label" model="account.report.expression">
|
||||
<field name="label">label</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">label</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_receipt_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_receipt_forced_currency_amount_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">amount_currency_currency_id</field>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_receipt_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_receipt_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_receipt_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments" model="account.report.line">
|
||||
<field name="name">Including Unreconciled Payments</field>
|
||||
<field name="code">unreconciled_payments</field>
|
||||
<field name="groupby">id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="no_statement_unreconciled_payments_date" model="account.report.expression">
|
||||
<field name="label">date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments_label" model="account.report.expression">
|
||||
<field name="label">label</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">label</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments_forced_currency_amount_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">amount_currency_currency_id</field>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="no_statement_unreconciled_payments_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_unreconciled_payments</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="misc_operations" model="account.report.line">
|
||||
<field name="name">Misc. operations</field>
|
||||
<field name="code">misc_operations</field>
|
||||
<field name="expression_ids">
|
||||
<record id="misc_operations_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_misc_operations</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="True"/>
|
||||
</record>
|
||||
<record id="misc_operations_amount_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_misc_operations</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="outstanding" model="account.report.line">
|
||||
<field name="name">Outstanding Receipts/Payments</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="expression_ids">
|
||||
<record id="outstanding_expr" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">outstanding_receipts.amount + outstanding_payments.amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_expr_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_forced_currency_amount</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="children_ids">
|
||||
<record id="outstanding_receipts" model="account.report.line">
|
||||
<field name="name">(+) Outstanding Receipts</field>
|
||||
<field name="code">outstanding_receipts</field>
|
||||
<field name="groupby">id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="outstanding_receipts_date" model="account.report.expression">
|
||||
<field name="label">date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_receipts_label" model="account.report.expression">
|
||||
<field name="label">label</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">label</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_receipts_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_receipts_forced_currency_amount_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">amount_currency_currency_id</field>
|
||||
</record>
|
||||
<record id="outstanding_receipts_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_receipts_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_receipts_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_receipts</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="outstanding_payments" model="account.report.line">
|
||||
<field name="name">(-) Outstanding Payments</field>
|
||||
<field name="code">outstanding_payments</field>
|
||||
<field name="groupby">id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="outstanding_payments_date" model="account.report.expression">
|
||||
<field name="label">date</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">date</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_payments_label" model="account.report.expression">
|
||||
<field name="label">label</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">label</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_payments_amount_currency" model="account.report.expression">
|
||||
<field name="label">amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">amount_currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_payments_forced_currency_amount_currency" model="account.report.expression">
|
||||
<field name="label">_currency_amount_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">amount_currency_currency_id</field>
|
||||
</record>
|
||||
<record id="outstanding_payments_currency" model="account.report.expression">
|
||||
<field name="label">currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">currency</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_payments_amount" model="account.report.expression">
|
||||
<field name="label">amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">amount</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="outstanding_payments_forced_currency_amount" model="account.report.expression">
|
||||
<field name="label">_currency_amount</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_outstanding_payments</field>
|
||||
<field name="subformula">amount_currency_id</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
19
addons/at_accounting/data/cash_flow_report.xml
Normal file
19
addons/at_accounting/data/cash_flow_report.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="cash_flow_report" model="account.report">
|
||||
<field name="name">Cash Flow Statement</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_date_range" eval="True"/>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="currency_translation">current</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_cash_flow_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="cash_flow_report_balance" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
43
addons/at_accounting/data/deferred_reports.xml
Normal file
43
addons/at_accounting/data/deferred_reports.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="deferred_expense_report" model="account.report">
|
||||
<field name="name">Deferred Expense Report</field>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_analytic" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="True"/>
|
||||
<field name="filter_growth_comparison" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_hierarchy">by_default</field>
|
||||
<field name="default_opening_date_filter">previous_month</field>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_deferred_expense_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="deferred_expense_current" model="account.report.column">
|
||||
<field name="name">Current</field>
|
||||
<field name="expression_label">current</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="deferred_revenue_report" model="account.report">
|
||||
<field name="name">Deferred Revenue Report</field>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_analytic" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="True"/>
|
||||
<field name="filter_growth_comparison" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_hierarchy">by_default</field>
|
||||
<field name="default_opening_date_filter">previous_month</field>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_deferred_revenue_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="deferred_revenue_current" model="account.report.column">
|
||||
<field name="name">Current</field>
|
||||
<field name="expression_label">current</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
37
addons/at_accounting/data/digest_data.xml
Normal file
37
addons/at_accounting/data/digest_data.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="digest.digest_digest_default" model="digest.digest">
|
||||
<field name="kpi_account_bank_cash">True</field>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="0">
|
||||
<record id="digest_tip_account_accountant_0" model="digest.tip">
|
||||
<field name="name">Tip: Bulk update journal items</field>
|
||||
<field name="sequence">900</field>
|
||||
<field name="group_id" ref="account.group_account_user" />
|
||||
<field name="tip_description" type="html">
|
||||
<div>
|
||||
<b class="tip_title">Tip: Bulk update journal items</b>
|
||||
<p class="tip_content">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.</p>
|
||||
<img src="https://download.odoocdn.com/digests/at_accounting/static/src/img/milk-accounting-bulk.gif" width="540" class="illustration_border" />
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
<record id="digest_tip_account_accountant_1" model="digest.tip">
|
||||
<field name="name">Tip: Find an Accountant or register your Accounting Firm</field>
|
||||
<field name="sequence">1000</field>
|
||||
<field name="group_id" ref="account.group_account_user" />
|
||||
<field name="tip_description" type="html">
|
||||
<div>
|
||||
<b class="tip_title">Tip: Find an Accountant or register your Accounting Firm</b>
|
||||
<p class="tip_content">Click here to find an accountant or if you want to list out your accounting services on Odoo</p>
|
||||
<p class="mt-3">
|
||||
<a class="tip_button" href="https://odoo.com/accounting-firms" target="_blank"><span class="tip_button_text">Find an Accountant</span></a>
|
||||
<a class="tip_button" href="https://odoo.com/accounting-firms/register" target="_blank"><span class="tip_button_text">Register your Accounting Firm</span></a>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
395
addons/at_accounting/data/executive_summary.xml
Normal file
395
addons/at_accounting/data/executive_summary.xml
Normal file
@@ -0,0 +1,395 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="executive_summary" model="account.report">
|
||||
<field name="name">Executive Summary</field>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="default_opening_date_filter">this_year</field>
|
||||
<field name="column_ids">
|
||||
<record id="executive_summary_column" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="account_financial_report_executivesummary_cash0" model="account.report.line">
|
||||
<field name="name">Cash</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_executivesummary_cash_received0" model="account.report.line">
|
||||
<field name="name">Cash received</field>
|
||||
<field name="code">CR</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_cash_received0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', 'in', ('asset_cash', 'liability_credit_card')), ('debit', '>', 0.0)]"/>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_cash_spent0" model="account.report.line">
|
||||
<field name="name">Cash spent</field>
|
||||
<field name="code">CS</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_cash_spent0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', 'in', ('asset_cash', 'liability_credit_card')), ('credit', '>', 0.0)]"/>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_cash_surplus0" model="account.report.line">
|
||||
<field name="name">Cash surplus</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_cash_surplus0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CR.balance + CS.balance</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_closing_bank_balance0" model="account.report.line">
|
||||
<field name="name">Closing bank balance</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_closing_bank_balance0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', 'in', ('asset_cash', 'liability_credit_card'))]"/>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_profitability0" model="account.report.line">
|
||||
<field name="name">Profitability</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_executivesummary_income0" model="account.report.line">
|
||||
<field name="name">Revenue</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_income0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">REV.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_direct_costs0" model="account.report.line">
|
||||
<field name="name">Cost of Revenue</field>
|
||||
<field name="code">EXEC_COS</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_direct_costs0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">COS.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_gross_profit0" model="account.report.line">
|
||||
<field name="name">Gross profit</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_gross_profit0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">GRP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_expenses0" model="account.report.line">
|
||||
<field name="name">Expenses</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_expenses0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">EXP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_profit0" model="account.report.line">
|
||||
<field name="name">Net Profit</field>
|
||||
<field name="code">EXEC_NEP</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_profit0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">NEP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_balancesheet0" model="account.report.line">
|
||||
<field name="name">Balance Sheet</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_executivesummary_debtors0" model="account.report.line">
|
||||
<field name="name">Receivables</field>
|
||||
<field name="code">DEB</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_debtors0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'asset_receivable')]"/>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_creditors0" model="account.report.line">
|
||||
<field name="name">Payables</field>
|
||||
<field name="code">CRE</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_creditors0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'liability_payable')]"/>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_net_assets0" model="account.report.line">
|
||||
<field name="name">Net assets</field>
|
||||
<field name="code">EXEC_SUMMARY_NA</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_net_assets0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">TA.balance - L.balance</field>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_performance0" model="account.report.line">
|
||||
<field name="name">Performance</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_executivesummary_gpmargin0" model="account.report.line">
|
||||
<field name="name">Gross profit margin (gross profit / operating income)</field>
|
||||
<field name="code">GPMARGIN0</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_gpmargin0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">GPMARGIN0.grp / GPMARGIN0.opinc * 100</field>
|
||||
<field name="subformula">ignore_zero_division</field>
|
||||
<field name="figure_type">percentage</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_gpmargin0_grp" model="account.report.expression">
|
||||
<field name="label">grp</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">GRP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_gpmargin0_opinc" model="account.report.expression">
|
||||
<field name="label">opinc</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">REV.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_npmargin0" model="account.report.line">
|
||||
<field name="name">Net profit margin (net profit / income)</field>
|
||||
<field name="code">NPMARGIN0</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_npmargin0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">NPMARGIN0.nep / NPMARGIN0.inc * 100</field>
|
||||
<field name="subformula">ignore_zero_division</field>
|
||||
<field name="figure_type">percentage</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_npmargin0_nep" model="account.report.expression">
|
||||
<field name="label">nep</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">NEP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_npmargin0_inc" model="account.report.expression">
|
||||
<field name="label">inc</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">INC.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_return_investment0" model="account.report.line">
|
||||
<field name="name">Return on investments (net profit / assets)</field>
|
||||
<field name="code">ROI</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_return_investment0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">ROI.nep / ROI.ta * 100</field>
|
||||
<field name="subformula">ignore_zero_division</field>
|
||||
<field name="figure_type">percentage</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_return_investment0_nep" model="account.report.expression">
|
||||
<field name="label">nep</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">NEP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_return_investment0_ta" model="account.report.expression">
|
||||
<field name="label">ta</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">TA.balance</field>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_position0" model="account.report.line">
|
||||
<field name="name">Position</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="children_ids">
|
||||
<record id="account_financial_report_executivesummary_avdebt0" model="account.report.line">
|
||||
<field name="name">Average debtors days</field>
|
||||
<field name="code">AVG_DEBT_DAYS</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_avdebt0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">DEB.balance / AVG_DEBT_DAYS.opinc * AVG_DEBT_DAYS.NDays</field>
|
||||
<field name="subformula">ignore_zero_division</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
<field name="figure_type">float</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_avdebt0_opinc" model="account.report.expression">
|
||||
<field name="label">opinc</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">REV.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_avdebt0_ndays" model="account.report.expression">
|
||||
<field name="label">NDays</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_executive_summary_ndays</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_avgcre0" model="account.report.line">
|
||||
<field name="name">Average creditors days</field>
|
||||
<field name="code">AVG_CRED_DAYS</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_avgcre0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">-CRE.balance / (AVG_CRED_DAYS.cos + AVG_CRED_DAYS.exp) * AVG_CRED_DAYS.NDays</field>
|
||||
<field name="subformula">ignore_zero_division</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
<field name="figure_type">float</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_avgcre0_cos" model="account.report.expression">
|
||||
<field name="label">cos</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">COS.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_avgcre0_exp" model="account.report.expression">
|
||||
<field name="label">exp</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">EXP.balance</field>
|
||||
<field name="date_scope">strict_range</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_avgcre0_ndays" model="account.report.expression">
|
||||
<field name="label">NDays</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_executive_summary_ndays</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_st_cash_forecast0" model="account.report.line">
|
||||
<field name="name">Short term cash forecast</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_st_cash_forecast0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">DEB.balance + CRE.balance</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_ca_to_l0" model="account.report.line">
|
||||
<field name="name">Current assets to liabilities</field>
|
||||
<field name="code">CATL</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_executivesummary_ca_to_l0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CATL.ca / CATL.cl</field>
|
||||
<field name="subformula">ignore_zero_division</field>
|
||||
<field name="figure_type">float</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_ca_to_l0_ca" model="account.report.expression">
|
||||
<field name="label">ca</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CA.balance</field>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
<record id="account_financial_report_executivesummary_ca_to_l0_cl" model="account.report.expression">
|
||||
<field name="label">cl</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">CL.balance</field>
|
||||
<field name="date_scope">from_beginning</field>
|
||||
<field name="subformula">cross_report</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
49
addons/at_accounting/data/general_ledger.xml
Normal file
49
addons/at_accounting/data/general_ledger.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="general_ledger_report" model="account.report">
|
||||
<field name="name">General Ledger</field>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_analytic" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_hide_0_lines">never</field>
|
||||
<field name="default_opening_date_filter">this_month</field>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="load_more_limit" eval="80"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_general_ledger_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="general_ledger_report_date" model="account.report.column">
|
||||
<field name="name">Date</field>
|
||||
<field name="expression_label">date</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="general_ledger_report_communication" model="account.report.column">
|
||||
<field name="name">Communication</field>
|
||||
<field name="expression_label">communication</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="general_ledger_report_partner_name" model="account.report.column">
|
||||
<field name="name">Partner</field>
|
||||
<field name="expression_label">partner_name</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="general_ledger_report_amount_currency" model="account.report.column">
|
||||
<field name="name">Currency</field>
|
||||
<field name="expression_label">amount_currency</field>
|
||||
</record>
|
||||
<record id="general_ledger_report_debit" model="account.report.column">
|
||||
<field name="name">Debit</field>
|
||||
<field name="expression_label">debit</field>
|
||||
</record>
|
||||
<record id="general_ledger_report_credit" model="account.report.column">
|
||||
<field name="name">Credit</field>
|
||||
<field name="expression_label">credit</field>
|
||||
</record>
|
||||
<record id="general_ledger_report_balance" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
14
addons/at_accounting/data/generic_tax_report.xml
Normal file
14
addons/at_accounting/data/generic_tax_report.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="account.generic_tax_report" model="account.report">
|
||||
<field name="custom_handler_model_id" ref="model_account_generic_tax_report_handler"/>
|
||||
</record>
|
||||
|
||||
<record id="account.generic_tax_report_account_tax" model="account.report">
|
||||
<field name="custom_handler_model_id" ref="model_account_generic_tax_report_handler_account_tax"/>
|
||||
</record>
|
||||
|
||||
<record id="account.generic_tax_report_tax_account" model="account.report">
|
||||
<field name="custom_handler_model_id" ref="model_account_generic_tax_report_handler_tax_account"/>
|
||||
</record>
|
||||
</odoo>
|
||||
11
addons/at_accounting/data/ir_cron.xml
Normal file
11
addons/at_accounting/data/ir_cron.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="auto_reconcile_bank_statement_line" model="ir.cron">
|
||||
<field name="name">Try to reconcile automatically your statement lines</field>
|
||||
<field name="model_id" ref="model_account_bank_statement_line"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_try_auto_reconcile_statement_lines(batch_size=100)</field>
|
||||
<field name='interval_number'>1</field>
|
||||
<field name='interval_type'>days</field>
|
||||
</record>
|
||||
</odoo>
|
||||
235
addons/at_accounting/data/journal_report.xml
Normal file
235
addons/at_accounting/data/journal_report.xml
Normal file
@@ -0,0 +1,235 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="journal_report" model="account.report">
|
||||
<field name="name">Journal Report</field>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_show_draft" eval="True"/>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_hierarchy">never</field>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_unreconciled" eval="False"/>
|
||||
<field name="filter_hide_0_lines">never</field>
|
||||
<field name="default_opening_date_filter">this_year</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_journal_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="journal_report_code" model="account.report.column">
|
||||
<field name="name">Code</field>
|
||||
<field name="expression_label">code</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="journal_report_debit" model="account.report.column">
|
||||
<field name="name">Debit</field>
|
||||
<field name="expression_label">debit</field>
|
||||
</record>
|
||||
<record id="journal_report_credit" model="account.report.column">
|
||||
<field name="name">Credit</field>
|
||||
<field name="expression_label">credit</field>
|
||||
</record>
|
||||
<record id="journal_report_balance" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="journal_report_line" model="account.report.line">
|
||||
<field name="name">Name</field>
|
||||
<field name="groupby">journal_id, account_id</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="expression_ids">
|
||||
<record id="journal_report_line_code" model="account.report.expression">
|
||||
<field name="label">code</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_journal_report</field>
|
||||
<field name="subformula">code</field>
|
||||
</record>
|
||||
<record id="journal_report_line_debit" model="account.report.expression">
|
||||
<field name="label">debit</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_journal_report</field>
|
||||
<field name="subformula">debit</field>
|
||||
</record>
|
||||
<record id="journal_report_line_credit" model="account.report.expression">
|
||||
<field name="label">credit</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_journal_report</field>
|
||||
<field name="subformula">credit</field>
|
||||
</record>
|
||||
<record id="journal_report_line_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_journal_report</field>
|
||||
<field name="subformula">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<template id="journal_report_pdf_export_main">
|
||||
<html>
|
||||
<head>
|
||||
<base t-att-href="base_url"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<t t-call-assets="at_accounting.assets_pdf_export" t-js="False"/>
|
||||
</head>
|
||||
<body t-att-dir="env['res.lang']._get_data(code=lang or env.user.lang).direction or 'ltr'">
|
||||
<div t-att-class="options['css_custom_class']">
|
||||
<header>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 o_header_font">
|
||||
<t t-call="at_accounting.company_information"/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="o_title">
|
||||
<t t-if="report.filter_show_draft and options['all_entries']">[Draft]</t>
|
||||
<t t-out="report.name"/>
|
||||
</div>
|
||||
<div class="o_subtitle">
|
||||
<t t-out="options['date']['date_from']"/> - <t t-out="options['date']['date_to']"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Journal entries -->
|
||||
<t t-foreach="document_data['journals_vals']" t-as="journal_vals">
|
||||
<section style="page-break-after: always;">
|
||||
<div class="o_section_title">
|
||||
<t t-out="journal_vals.get('name')"/>
|
||||
</div>
|
||||
<div class="d-flex align-items-start">
|
||||
<t t-call="at_accounting.journal_report_pdf_body_default"/>
|
||||
</div>
|
||||
|
||||
<t t-if="journal_vals.get('tax_summary')">
|
||||
<t t-call="at_accounting.pdf_journal_report_taxes_summary">
|
||||
<t t-set="tax_summary" t-value="journal_vals['tax_summary']"/>
|
||||
</t>
|
||||
</t>
|
||||
</section>
|
||||
</t>
|
||||
|
||||
<section t-if="document_data.get('global_tax_summary')">
|
||||
<div class="o_section_title">
|
||||
Global Tax Summary
|
||||
</div>
|
||||
<t t-call="at_accounting.pdf_journal_report_taxes_summary">
|
||||
<t t-set="tax_summary" t-value="document_data['global_tax_summary']"/>
|
||||
</t>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<template id="journal_report_pdf_body_default">
|
||||
<table class="o_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<t t-foreach="journal_vals['columns']" t-as="column">
|
||||
<th t-att-class="column.get('class', '')">
|
||||
<t t-out="column.get('name', '')"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<t t-foreach="journal_vals['lines']" t-as="line">
|
||||
<tr t-att-class="line.get('line_class', '')">
|
||||
<t t-foreach="journal_vals['columns']" t-as="column">
|
||||
<t t-if="line.get(column['label'])">
|
||||
<t t-set="cell_style" t-value="line[column['label']].get('class', '')"/>
|
||||
<t t-set="column_style" t-value="column.get('class', '')"/>
|
||||
<td t-att-class="cell_style + column_style">
|
||||
<t t-out="line[column['label']]['data']"/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<td/>
|
||||
</t>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template id="pdf_journal_report_taxes_summary">
|
||||
<div class="container tax_summary" style="page-break-inside: avoid;">
|
||||
<t t-set="taxes" t-value="tax_summary.get('tax_report_lines')"/>
|
||||
<t t-if="taxes">
|
||||
<div class="row o_section_subtitle">
|
||||
<p>Tax Applied</p>
|
||||
</div>
|
||||
<div class="row taxes">
|
||||
<t t-set="extra_columns" t-value="tax_summary.get('extra_columns')"/>
|
||||
<table class="o_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th t-if="len(taxes) > 1">Country</th>
|
||||
<th>Name</th>
|
||||
<th class="o_right_alignment">Base Amount</th>
|
||||
<th class="o_right_alignment">Tax Amount</th>
|
||||
<th t-if="tax_summary.get('tax_non_deductible_column')" class="o_right_alignment">Non-Deductible</th>
|
||||
<th t-if="tax_summary.get('tax_deductible_column')" class="o_right_alignment">Deductible</th>
|
||||
<th t-if="tax_summary.get('tax_due_column')" class="o_right_alignment">Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="taxes" t-as="country_name">
|
||||
<tr t-foreach="taxes[country_name]" t-as="tax">
|
||||
<t t-if="country_name_size > 1">
|
||||
<td>
|
||||
<t t-if="tax_index == 0" t-out="country_name"/>
|
||||
</td>
|
||||
</t>
|
||||
<td t-out="tax['name']"/>
|
||||
<td class="o_right_alignment" t-out="tax['base_amount']"/>
|
||||
<td class="o_right_alignment" t-out="tax['tax_amount']"/>
|
||||
<td t-if="tax_summary.get('tax_non_deductible_column')" class="o_right_alignment" t-out="tax['tax_non_deductible']"/>
|
||||
<td t-if="tax_summary.get('tax_deductible_column')" class="o_right_alignment" t-out="tax['tax_deductible']"/>
|
||||
<td t-if="tax_summary.get('tax_due_column')" class="o_right_alignment" t-out="tax['tax_due']"/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="grids" t-value="tax_summary.get('tax_grid_summary_lines')"/>
|
||||
<t t-if="grids">
|
||||
<div class="row o_section_subtitle">
|
||||
<p>Impacted Tax Grids</p>
|
||||
</div>
|
||||
<div class="row tax_grid">
|
||||
<table class="o_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th t-if="len(grids) > 1">Country</th>
|
||||
<th>Grid</th>
|
||||
<th class="o_right_alignment">+</th>
|
||||
<th class="o_right_alignment">-</th>
|
||||
<th class="o_right_alignment">Impact On Grid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="grids" t-as="country_name">
|
||||
<tr t-foreach="grids[country_name]" t-as="grid_name">
|
||||
<t t-if="country_name_size > 1">
|
||||
<td>
|
||||
<t t-if="grid_name_index == 0" t-out="country_name"/>
|
||||
</td>
|
||||
</t>
|
||||
<td t-out="grid_name"/>
|
||||
<td class="o_right_alignment" t-out="grids[country_name][grid_name].get('+', 0)"/>
|
||||
<td class="o_right_alignment" t-out="grids[country_name][grid_name].get('-', 0)"/>
|
||||
<td class="o_right_alignment" t-out="grids[country_name][grid_name]['impact']"/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
34
addons/at_accounting/data/mail_activity_type_data.xml
Normal file
34
addons/at_accounting/data/mail_activity_type_data.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="tax_closing_activity_type" model="mail.activity.type">
|
||||
<field name="name">Tax Report</field>
|
||||
<field name="summary">Tax Report</field>
|
||||
<field name="category">tax_report</field>
|
||||
<field name="res_model">account.journal</field>
|
||||
<field name="chaining_type">suggest</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_activity_type_tax_report_to_pay" model="mail.activity.type">
|
||||
<field name="name">Pay Tax</field>
|
||||
<field name="summary">Tax is ready to be paid</field>
|
||||
<field name="category">tax_report</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="delay_unit">days</field>
|
||||
<field name="delay_from">previous_activity</field>
|
||||
<field name="res_model">account.move</field>
|
||||
<field name="chaining_type">suggest</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_activity_type_tax_report_to_be_sent" model="mail.activity.type">
|
||||
<field name="name">Tax Report Ready</field>
|
||||
<field name="summary">Tax report is ready to be sent to the administration</field>
|
||||
<field name="category">tax_report</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="delay_unit">days</field>
|
||||
<field name="delay_from">current_date</field>
|
||||
<field name="res_model">account.move</field>
|
||||
<field name="chaining_type">suggest</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
26
addons/at_accounting/data/mail_templates.xml
Normal file
26
addons/at_accounting/data/mail_templates.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<odoo>
|
||||
<record id="email_template_customer_statement" model="mail.template">
|
||||
<field name="name">Customer Statement</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="email_from">{{ object._get_followup_responsible().email_formatted }}</field>
|
||||
<field name="subject">{{ (object.company_id or object._get_followup_responsible().company_id).name }} Statement - {{ object.commercial_company_name }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px;">
|
||||
<t t-if="object.id != object.commercial_partner_id.id">Dear <t t-out="object.name or ''"/> (<t t-out="object.commercial_partner_id.name or ''"/>),</t>
|
||||
<t t-else="">Dear <t t-out="object.name or ''"/>,</t>
|
||||
<br/>
|
||||
Please find enclosed the statement of your account.
|
||||
<br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<br/>
|
||||
Sincerely,
|
||||
<br/>
|
||||
<t t-out="object._get_followup_responsible().name if is_html_empty(object._get_followup_responsible().signature) else object._get_followup_responsible().signature"/>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.lang }}</field>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
53
addons/at_accounting/data/menuitems.xml
Normal file
53
addons/at_accounting/data/menuitems.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_action_account_report_partner_ledger" name="Partner Ledger"
|
||||
action="action_account_report_partner_ledger" groups="account.group_account_readonly"
|
||||
parent="account.account_reports_partners_reports_menu"/>
|
||||
<menuitem id="menu_action_account_report_aged_receivable" name="Aged Receivable" action="action_account_report_ar"
|
||||
groups="account.group_account_readonly" parent="account.account_reports_partners_reports_menu"/>
|
||||
<menuitem id="menu_action_account_report_aged_payable" name="Aged Payable" action="action_account_report_ap"
|
||||
groups="account.group_account_readonly" parent="account.account_reports_partners_reports_menu"/>
|
||||
<menuitem id="account_reports_audit_reports_menu" name="Audit Reports" parent="account.menu_finance_reports"
|
||||
sequence="2">
|
||||
<menuitem id="menu_action_account_report_general_ledger" name="General Ledger"
|
||||
action="action_account_report_general_ledger" groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_coa" name="Trial Balance" action="action_account_report_coa"
|
||||
groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_ja" name="Journal Audit" action="action_account_report_ja"
|
||||
groups="account.group_account_readonly"/>
|
||||
</menuitem>
|
||||
|
||||
<menuitem id="menu_action_account_report_gt" name="Tax Return" action="action_account_report_gt"
|
||||
parent="account.account_reports_legal_statements_menu" sequence="50"
|
||||
groups="account.group_account_readonly,account.group_account_basic"/>
|
||||
<menuitem id="menu_action_account_report_sales" action="action_account_report_sales"
|
||||
parent="account.account_reports_legal_statements_menu" sequence="60"
|
||||
groups="account.group_account_readonly" active="False"/>
|
||||
|
||||
<menuitem id="menu_action_account_report_multicurrency_revaluation" name="Unrealized Currency Gains/Losses"
|
||||
action="action_account_report_multicurrency_revaluation" parent="account.account_reports_management_menu"
|
||||
groups="base.group_multi_currency"/>
|
||||
<menuitem id="menu_action_account_report_balance_sheet" name="Balance Sheet" action="action_account_report_bs"
|
||||
parent="account.account_reports_legal_statements_menu" groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_profit_and_loss" name="Profit and Loss" action="action_account_report_pl"
|
||||
parent="account.account_reports_legal_statements_menu" groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_cash_flow" name="Cash Flow Statement" action="action_account_report_cs"
|
||||
parent="account.account_reports_legal_statements_menu" groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_exec_summary" name="Executive Summary"
|
||||
action="action_account_report_exec_summary" parent="account.account_reports_legal_statements_menu"
|
||||
groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_deferred_expense" name="Deferred Expense"
|
||||
action="action_account_report_deferred_expense" parent="account.account_reports_management_menu"
|
||||
groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_deferred_revenue" name="Deferred Revenue"
|
||||
action="action_account_report_deferred_revenue" parent="account.account_reports_management_menu"
|
||||
groups="account.group_account_readonly"/>
|
||||
<menuitem id="menu_action_account_report_tree" name="Accounting Reports" sequence="6"
|
||||
parent="account.account_management_menu" action="action_account_report_tree" groups="base.group_no_one"/>
|
||||
<menuitem id="menu_action_account_report_horizontal_groups" name="Horizontal Groups"
|
||||
action="action_account_report_horizontal_groups" parent="account.account_account_menu" sequence="10"
|
||||
groups="base.group_no_one"/>
|
||||
<menuitem id="menu_action_account_report_budget_tree" name="Financial Budgets"
|
||||
action="action_account_report_budget_tree" parent="account.account_account_menu" sequence="11"/>
|
||||
|
||||
</odoo>
|
||||
8
addons/at_accounting/data/menuitems_asset.xml
Normal file
8
addons/at_accounting/data/menuitems_asset.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_action_account_report_assets"
|
||||
name="Depreciation Schedule"
|
||||
action="action_account_report_assets"
|
||||
parent="account.account_reports_management_menu"
|
||||
groups="account.group_account_readonly"/>
|
||||
</odoo>
|
||||
107
addons/at_accounting/data/multicurrency_revaluation_report.xml
Normal file
107
addons/at_accounting/data/multicurrency_revaluation_report.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="multicurrency_revaluation_report" model="account.report">
|
||||
<field name="name">Unrealized Currency Gains/Losses</field>
|
||||
<field name="filter_date_range" eval="False"/>
|
||||
<field name="filter_show_draft" eval="True"/>
|
||||
<field name="default_opening_date_filter">previous_month</field>
|
||||
<field name="custom_handler_model_id" ref="model_account_multicurrency_revaluation_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="multicurrency_revaluation_report_balance_currency" model="account.report.column">
|
||||
<field name="name">Balance in Foreign Currency</field>
|
||||
<field name="expression_label">balance_currency</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_report_balance_operation" model="account.report.column">
|
||||
<field name="name">Balance at Operation Rate</field>
|
||||
<field name="expression_label">balance_operation</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_report_balance_current" model="account.report.column">
|
||||
<field name="name">Balance at Current Rate</field>
|
||||
<field name="expression_label">balance_current</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_report_adjustment" model="account.report.column">
|
||||
<field name="name">Adjustment</field>
|
||||
<field name="expression_label">adjustment</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="multicurrency_revaluation_to_adjust" model="account.report.line">
|
||||
<field name="name">Accounts To Adjust</field>
|
||||
<field name="code">multicurrency_included</field>
|
||||
<field name="groupby">currency_id, account_id, id</field>
|
||||
<field name="expression_ids">
|
||||
<record id="multicurrency_revaluation_to_adjust_balance_currency" model="account.report.expression">
|
||||
<field name="label">balance_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
|
||||
<field name="subformula">balance_currency</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_to_adjust_balance_currency_forced_currency" model="account.report.expression">
|
||||
<field name="label">_currency_balance_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
|
||||
<field name="subformula">currency_id</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_to_adjust_balance_operation" model="account.report.expression">
|
||||
<field name="label">balance_operation</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
|
||||
<field name="subformula">balance_operation</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_to_adjust_balance_current" model="account.report.expression">
|
||||
<field name="label">balance_current</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
|
||||
<field name="subformula">balance_current</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_to_adjust_adjustment" model="account.report.expression">
|
||||
<field name="label">adjustment</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
|
||||
<field name="subformula">adjustment</field>
|
||||
<field name="auditable" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="multicurrency_revaluation_excluded" model="account.report.line">
|
||||
<field name="name">Excluded Accounts</field>
|
||||
<field name="groupby">currency_id, account_id, id</field>
|
||||
<field name="expression_ids">
|
||||
<record id="multicurrency_revaluation_excluded_balance_currency" model="account.report.expression">
|
||||
<field name="label">balance_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
|
||||
<field name="subformula">balance_currency</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_excluded_balance_currency_forced_currency" model="account.report.expression">
|
||||
<field name="label">_currency_balance_currency</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
|
||||
<field name="subformula">currency_id</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_excluded_balance_operation" model="account.report.expression">
|
||||
<field name="label">balance_operation</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
|
||||
<field name="subformula">balance_operation</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_excluded_balance_current" model="account.report.expression">
|
||||
<field name="label">balance_current</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
|
||||
<field name="subformula">balance_current</field>
|
||||
</record>
|
||||
<record id="multicurrency_revaluation_excluded_adjustment" model="account.report.expression">
|
||||
<field name="label">adjustment</field>
|
||||
<field name="engine">custom</field>
|
||||
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
|
||||
<field name="subformula">adjustment</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
65
addons/at_accounting/data/partner_ledger.xml
Normal file
65
addons/at_accounting/data/partner_ledger.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="partner_ledger_report" model="account.report">
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="filter_show_draft" eval="True"/>
|
||||
<field name="filter_account_type">both</field>
|
||||
<field name="filter_partner" eval="True"/>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_unreconciled" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="filter_hide_0_lines">never</field>
|
||||
<field name="default_opening_date_filter">this_year</field>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="load_more_limit" eval="80"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_partner_ledger_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="partner_ledger_report_journal_code" model="account.report.column">
|
||||
<field name="name">Journal</field>
|
||||
<field name="expression_label">journal_code</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_account_code" model="account.report.column">
|
||||
<field name="name">Account</field>
|
||||
<field name="expression_label">account_code</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_invoicing_date" model="account.report.column">
|
||||
<field name="name">Invoice Date</field>
|
||||
<field name="expression_label">invoice_date</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_date_maturity" model="account.report.column">
|
||||
<field name="name">Due Date</field>
|
||||
<field name="expression_label">date_maturity</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_matching_number" model="account.report.column">
|
||||
<field name="name">Matching</field>
|
||||
<field name="expression_label">matching_number</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_debit" model="account.report.column">
|
||||
<field name="name">Debit</field>
|
||||
<field name="expression_label">debit</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_credit" model="account.report.column">
|
||||
<field name="name">Credit</field>
|
||||
<field name="expression_label">credit</field>
|
||||
</record>
|
||||
<record id="partner_ledger_amount" model="account.report.column">
|
||||
<field name="name">Amount</field>
|
||||
<field name="expression_label">amount</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_amount_currency" model="account.report.column">
|
||||
<field name="name">Amount Currency</field>
|
||||
<field name="expression_label">amount_currency</field>
|
||||
</record>
|
||||
<record id="partner_ledger_report_balance" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
335
addons/at_accounting/data/pdf_export_templates.xml
Normal file
335
addons/at_accounting/data/pdf_export_templates.xml
Normal file
@@ -0,0 +1,335 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="pdf_export_main">
|
||||
<html>
|
||||
<head>
|
||||
<base t-att-href="base_url"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<t t-call-assets="at_accounting.assets_pdf_export" t-js="False"/>
|
||||
</head>
|
||||
<body t-att-dir="env['res.lang']._get_data(code=lang or env.user.lang).direction or 'ltr'">
|
||||
<div t-att-class="'o_content ' + options['css_custom_class']">
|
||||
<header>
|
||||
<div class="o_title">
|
||||
<t t-if="report.filter_show_draft and options['all_entries']">[Draft]</t>
|
||||
<t t-out="report_title"/>
|
||||
</div>
|
||||
<div class="row o_header_font">
|
||||
<div class="col-8">
|
||||
<!-- All company information (name, address, vat, ...) -->
|
||||
<t t-call="{{custom_templates.get('company_information', 'at_accounting.company_information')}}"/>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<!-- All filters and options -->
|
||||
<t t-call="{{custom_templates.get('pdf_export_filters', 'at_accounting.pdf_export_filters')}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="d-flex align-items-start">
|
||||
<t t-foreach="options.get('horizontal_split') and ['left', 'right'] or [None]" t-as="split_side">
|
||||
<table t-attf-class="o_table #{options.get('horizontal_split') and 'horizontal_split_page'}">
|
||||
<!-- Header -->
|
||||
<t t-call="{{custom_templates.get('pdf_export_main_table_header', 'at_accounting.pdf_export_main_table_header')}}"/>
|
||||
|
||||
<!-- Body -->
|
||||
<tbody>
|
||||
<t t-if="lines">
|
||||
<t t-call="{{custom_templates.get('pdf_export_main_table_body', 'at_accounting.pdf_export_main_table_body')}}">
|
||||
<t t-set="lines" t-value="filter(lambda x: not split_side or split_side == x.get('horizontal_split_side', 'left'), lines)"/>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Annotations -->
|
||||
<ol class="o_annotation">
|
||||
<t t-foreach="annotations" t-as="annotation">
|
||||
<li>
|
||||
<t t-out="annotation.get('number')"/>.
|
||||
<t t-if="annotation.get('date')"><t t-out="annotation['date']"/> -</t>
|
||||
<t t-out="annotation.get('text')"/>
|
||||
</li>
|
||||
</t>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<template id="company_information">
|
||||
<t t-set="company_names" t-value="[company['name'] for company in options['companies']]"/>
|
||||
<div class="row">
|
||||
<div class="col-10" t-out="', '.join(company_names)"/>
|
||||
</div>
|
||||
|
||||
<address class="mb-0 o_text_muted" t-field="env.company.partner_id" t-options='{"widget": "contact", "fields": ["address"], "no_marker": True}'/>
|
||||
|
||||
<t t-if="options.get('tax_unit', 'company_only') == 'company_only'">
|
||||
<t t-if="env.company.account_fiscal_country_id.vat_label" t-out="env.company.account_fiscal_country_id.vat_label+':'"/>
|
||||
<t t-else="">Tax ID:</t>
|
||||
<t t-out="env.company.vat"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Tax ID: <t t-out="env['account.tax.unit'].browse(options.get('tax_unit')).vat"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="pdf_export_filters">
|
||||
<!-- Journals -->
|
||||
<t t-if="options.get('journals')">
|
||||
<div class="row" name="filter_info_template_journals">
|
||||
<t t-set="journal_group_selected" t-value="options.get('selected_journal_groups')"/>
|
||||
<t t-if="journal_group_selected">
|
||||
<div class="col-3">Multi-Ledger: </div>
|
||||
<div class="col-9 o_text_muted" t-out="journal_group_selected['title']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="journal_value" t-value="[journal.get('title') for journal in options['journals'] if journal.get('selected')]"/>
|
||||
<t t-if="journal_value">
|
||||
<div class="col-3">Journals: </div>
|
||||
<div class="col-9 o_text_muted" t-out="', '.join(journal_value)"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Partners -->
|
||||
<t t-if="options.get('partner_ids') != None">
|
||||
<div class="row">
|
||||
<t t-set="partner_value" t-value="[partner for partner in options['selected_partner_ids']]"/>
|
||||
<t t-if="partner_value">
|
||||
<div class="col-3">Partners:</div>
|
||||
<div class="col-9 o_text_muted" t-out="', '.join(partner_value)"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Partners categories -->
|
||||
<t t-if="options.get('partner_categories') != None">
|
||||
<div class="row">
|
||||
<t t-set="partner_category_value" t-value="[partner for partner in options['selected_partner_categories']]"/>
|
||||
<t t-if="partner_category_value">
|
||||
<div class="col-3">Partners Categories:</div>
|
||||
<div class="col-9 o_text_muted" t-out="', '.join(partner_category_value)"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Horizontal -->
|
||||
<t t-if="options.get('selected_horizontal_group_id')">
|
||||
<div class="row">
|
||||
<t t-set="horizontal_group" t-value="[hg['name'] for hg in options['available_horizontal_groups'] if hg['id'] == options.get('selected_horizontal_group_id')]"/>
|
||||
<t t-if="horizontal_group">
|
||||
<div class="col-3">Horizontal:</div>
|
||||
<div class="col-9 o_text_muted" t-out="horizontal_group[0]"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Currency -->
|
||||
<t t-if="options.get('company_currency')">
|
||||
<div class="row">
|
||||
<div class="col-3">Currency:</div>
|
||||
<div class="col-9 o_text_muted" t-out="options['company_currency']['currency_name']"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Filters -->
|
||||
<t t-if="options.get('aml_ir_filters') and any(opt['selected'] for opt in options['aml_ir_filters'])" name="aml_ir_filters">
|
||||
<div class="row">
|
||||
<t t-set="aml_ir_filters" t-value="opt['name'] for opt in options['aml_ir_filters'] if opt['selected']"/>
|
||||
<t t-if="aml_ir_filters">
|
||||
<div class="col-3">Filters:</div>
|
||||
<div class="col-9 o_text_muted" t-out="', '.join(aml_ir_filters)"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Extra options -->
|
||||
<div class="row" name="pdf_options_header">
|
||||
<t t-call="{{custom_templates.get('pdf_export_filter_extra_options_template', 'at_accounting.pdf_export_filter_extra_options_template')}}"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="pdf_export_filter_extra_options_template">
|
||||
<t t-set="rounding_unit_display_names" t-value="{k: v[1] for k, v in options['rounding_unit_names'].items() if v[1]}"/>
|
||||
<div class="col-3" t-if="(report.filter_show_draft and options['all_entries']) or
|
||||
(report.filter_unreconciled and options['unreconciled']) or
|
||||
options.get('include_analytic_without_aml') or
|
||||
options['rounding_unit'] in rounding_unit_display_names">
|
||||
Options:
|
||||
</div>
|
||||
<div class="col-9 o_text_muted">
|
||||
<t t-set="extra_options" t-value="[]"/>
|
||||
|
||||
<!-- All entries -->
|
||||
<t t-if="report.filter_show_draft and options['all_entries']" groups="account.group_account_readonly">
|
||||
<t t-set="label_draft_entries">With Draft Entries</t>
|
||||
<t t-set="extra_options" t-value="extra_options + [label_draft_entries]"/>
|
||||
</t>
|
||||
|
||||
<!-- Unreconciled -->
|
||||
<t t-if="report.filter_unreconciled and options['unreconciled']">
|
||||
<t t-set="label_unreconciled_entries">Unreconciled Entries</t>
|
||||
<t t-set="extra_options" t-value="extra_options + [label_unreconciled_entries]"/>
|
||||
</t>
|
||||
|
||||
<!-- Analytic -->
|
||||
<t t-if="options.get('include_analytic_without_aml')" name="include_analytic">
|
||||
<t t-set="label_analytic_simulations">Including Analytic Simulations</t>
|
||||
<t t-set="extra_options" t-value="extra_options + [label_analytic_simulations]"/>
|
||||
</t>
|
||||
|
||||
<!-- Currency Unit Amount Text -->
|
||||
<t t-if="options['rounding_unit'] in rounding_unit_display_names">
|
||||
<t t-set="rounding_unit" t-value="options.get('rounding_unit')"/>
|
||||
<t t-set="extra_options" t-value="extra_options + [rounding_unit_display_names[rounding_unit]]"/>
|
||||
</t>
|
||||
|
||||
<t t-out="', '.join(extra_options)"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="pdf_export_main_table_header">
|
||||
<thead id="table_header">
|
||||
<t t-foreach="options['column_headers']" t-as="column_header">
|
||||
<tr>
|
||||
<!-- First empty column -->
|
||||
<th/>
|
||||
|
||||
<!-- Other columns -->
|
||||
<t t-foreach="column_header * column_headers_render_data['level_repetitions'][column_header_index]" t-as="header">
|
||||
<th t-att-colspan="header.get('colspan', column_headers_render_data['level_colspan'][column_header_index]) + (1 if options.get('show_horizontal_group_total') and column_header_first else 0)" class="o_overflow_name">
|
||||
<t t-out="header.get('name')"/>
|
||||
</th>
|
||||
</t>
|
||||
|
||||
<th t-if="options.get('show_horizontal_group_total') and not column_header_first">
|
||||
<t t-out="[group['name'] for group in options['available_horizontal_groups'] if group['id'] == options['selected_horizontal_group_id']][0]"/>
|
||||
</th>
|
||||
|
||||
<th t-if="options.get('column_percent_comparison') == 'growth'">%</th>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Custom subheaders -->
|
||||
<t t-if="column_headers_render_data['custom_subheaders']">
|
||||
<tr>
|
||||
<!-- First empty column -->
|
||||
<th/>
|
||||
|
||||
<!-- Other columns -->
|
||||
<t t-foreach="column_headers_render_data['custom_subheaders']" t-as="subheader">
|
||||
<th t-att-colspan="subheader.get('colspan', 1)">
|
||||
<t t-out="subheader.get('name')"/>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
<tr>
|
||||
<!-- First empty column -->
|
||||
<th/>
|
||||
|
||||
<t t-foreach="options['columns']" t-as="subheader">
|
||||
<th>
|
||||
<t t-out="subheader.get('name')"/>
|
||||
</th>
|
||||
</t>
|
||||
<th t-if="options.get('show_horizontal_group_total')">
|
||||
<t t-out="options['columns'][0].get('name')"/>
|
||||
</th>
|
||||
<th t-if="options.get('column_percent_comparison') == 'growth'"/>
|
||||
</tr>
|
||||
</thead>
|
||||
</template>
|
||||
|
||||
<template id="pdf_export_main_table_body">
|
||||
<t t-foreach="lines" t-as="line">
|
||||
<t t-set="o_line_level" t-value="'o_line_level_' + str(line['level'])"/>
|
||||
|
||||
<t t-if="line.get('page_break') and not options.get('horizontal_split')">
|
||||
<!-- End current table -->
|
||||
<t t-out="table_end"/>
|
||||
|
||||
<!-- Append table header -->
|
||||
<t t-call="{{custom_templates.get('pdf_export_main_table_header', 'at_accounting.pdf_export_main_table_header')}}"/>
|
||||
|
||||
<!-- Start new table -->
|
||||
<t t-out="table_start"/>
|
||||
</t>
|
||||
|
||||
<!-- Adds an empty row above line with level 0 to add some spacing (it is the easiest and cleanest way) -->
|
||||
<t t-if="line_index != 0 and line['level'] == 0">
|
||||
<tr>
|
||||
<td/>
|
||||
|
||||
<t t-foreach="line.get('columns')" t-as="cell">
|
||||
<td/>
|
||||
</t>
|
||||
|
||||
<t t-if="options.get('column_percent_comparison')">
|
||||
<td/>
|
||||
</t>
|
||||
|
||||
<t t-if="options.get('show_horizontal_group_total')">
|
||||
<td/>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-set="o_bold" t-value="(' o_fw_bold' if line.get('unfolded') or 'total' in line.get('id') else '')"/>
|
||||
<t t-set="o_overflow" t-value="(' o_overflow_name' if len(line.get('name') or '') > 42 else '')"/>
|
||||
|
||||
<tr t-att-class="o_line_level + o_bold + o_overflow" name="pdf_export_main_table_body_lines_tr">
|
||||
<td t-att-colspan="line.get('colspan', '1')" class="o_line_name_level">
|
||||
<t t-out="line.get('name')"/>
|
||||
<t t-if="line.get('annotations')">
|
||||
<t t-foreach="annotations" t-as="annotation">
|
||||
<t t-if="annotation.get('number') and annotation['number'] in (line.get('annotations') or [])">
|
||||
<sup t-out="annotation['number']"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
|
||||
<t t-foreach="line.get('columns')" t-as="cell">
|
||||
<td class="o_cell_td">
|
||||
<t t-if="not env.company.totals_below_sections or options.get('ignore_totals_below_sections') or not line.get('unfolded')">
|
||||
<t t-call="{{custom_templates.get('pdf_export_cell', 'at_accounting.pdf_export_cell')}}"/>
|
||||
</t>
|
||||
</td>
|
||||
</t>
|
||||
|
||||
<t t-if="options.get('column_percent_comparison')">
|
||||
<td class="o_column_percent_comparison">
|
||||
<t t-if="line.get('column_percent_comparison_data')">
|
||||
<t t-out="line['column_percent_comparison_data'].get('name')"/>
|
||||
</t>
|
||||
</td>
|
||||
</t>
|
||||
|
||||
<t t-if="options.get('show_horizontal_group_total')">
|
||||
<td class="o_cell_td">
|
||||
<t t-if="line.get('horizontal_group_total_data')">
|
||||
<t t-set="o_classes" t-value="'o_line_cell_value_number' + (' o_muted' if line['horizontal_group_total_data'].get('no_format') == 0 else '')"/>
|
||||
<span t-att-class="o_classes" t-out="line['horizontal_group_total_data'].get('name')"/>
|
||||
</t>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="pdf_export_cell">
|
||||
<t t-if="cell.get('figure_type', '') in ['float', 'integer', 'monetary', 'percentage']">
|
||||
<t t-set="o_classes" t-value="'o_line_cell_value_number' + (' o_muted' if cell.get('is_zero') else '')"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="o_classes" t-value="'o_overflow_value'"/>
|
||||
</t>
|
||||
|
||||
<span t-att-class="o_classes" t-out="cell.get('name')"/>
|
||||
</template>
|
||||
</odoo>
|
||||
134
addons/at_accounting/data/profit_and_loss.xml
Normal file
134
addons/at_accounting/data/profit_and_loss.xml
Normal file
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="profit_and_loss" model="account.report">
|
||||
<field name="name">Profit and Loss</field>
|
||||
<field name="filter_analytic_groupby" eval="True"/>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="filter_budgets" eval="True"/>
|
||||
<field name="default_opening_date_filter">this_year</field>
|
||||
<field name="column_ids">
|
||||
<record id="profit_and_loss_column" model="account.report.column">
|
||||
<field name="name">Balance</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
<field name="line_ids">
|
||||
<record id="account_financial_report_revenue0" model="account.report.line">
|
||||
<field name="name">Revenue</field>
|
||||
<field name="code">REV</field>
|
||||
<field name="hierarchy_level">1</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_revenue0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'income')]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_cost_sales0" model="account.report.line">
|
||||
<field name="name">Less Costs of Revenue</field>
|
||||
<field name="code">COS</field>
|
||||
<field name="hierarchy_level">1</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_cost_sales0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'expense_direct_cost')]"/>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_gross_profit0" model="account.report.line">
|
||||
<field name="name">Gross Profit</field>
|
||||
<field name="code">GRP</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_gross_profit0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">REV.balance - COS.balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_expense0" model="account.report.line">
|
||||
<field name="name">Less Operating Expenses</field>
|
||||
<field name="code">EXP</field>
|
||||
<field name="hierarchy_level">1</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_expense0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'expense')]"/>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_operating_income0" model="account.report.line">
|
||||
<field name="name">Operating Income (or Loss)</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">INC</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_operating_income0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">REV.balance - COS.balance - EXP.balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_other_income0" model="account.report.line">
|
||||
<field name="name">Plus Other Income</field>
|
||||
<field name="code">OIN</field>
|
||||
<field name="hierarchy_level">1</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_other_income0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'income_other')]"/>
|
||||
<field name="subformula">-sum</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_depreciation0" model="account.report.line">
|
||||
<field name="name">Less Other Expenses</field>
|
||||
<field name="code">OEXP</field>
|
||||
<field name="hierarchy_level">1</field>
|
||||
<field name="groupby">account_id</field>
|
||||
<field name="foldable" eval="True"/>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_depreciation0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">domain</field>
|
||||
<field name="formula" eval="[('account_id.account_type', '=', 'expense_depreciation')]"/>
|
||||
<field name="subformula">sum</field>
|
||||
<field name="green_on_positive" eval="False"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
<record id="account_financial_report_net_profit0" model="account.report.line">
|
||||
<field name="name">Net Profit</field>
|
||||
<field name="hierarchy_level">0</field>
|
||||
<field name="code">NEP</field>
|
||||
<field name="expression_ids">
|
||||
<record id="account_financial_report_net_profit0_balance" model="account.report.expression">
|
||||
<field name="label">balance</field>
|
||||
<field name="engine">aggregation</field>
|
||||
<field name="formula">REV.balance + OIN.balance - COS.balance - EXP.balance - OEXP.balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
11
addons/at_accounting/data/report_send_cron.xml
Normal file
11
addons/at_accounting/data/report_send_cron.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<odoo>
|
||||
<record id="ir_cron_account_report_send" model="ir.cron">
|
||||
<field name="name">Send account reports automatically</field>
|
||||
<field name="model_id" ref="model_account_report"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_account_report_send(job_count=20)</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
</record>
|
||||
</odoo>
|
||||
36
addons/at_accounting/data/sales_report.xml
Normal file
36
addons/at_accounting/data/sales_report.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="generic_ec_sales_report" model="account.report">
|
||||
<field name="name">Generic EC Sales List</field>
|
||||
<field name="filter_show_draft" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="filter_date_range" eval="True"/>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_show_draft" eval="False"/>
|
||||
<field name="filter_unreconciled" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="default_opening_date_filter">previous_month</field>
|
||||
<field name="load_more_limit" eval="80"/>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_ec_sales_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="account_financial_report_ec_sales_country" model="account.report.column">
|
||||
<field name="name">Country Code</field>
|
||||
<field name="expression_label">country_code</field>
|
||||
<field name="figure_type">string</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="account_financial_report_ec_sales_vat" model="account.report.column">
|
||||
<field name="name">VAT Number</field>
|
||||
<field name="expression_label">vat_number</field>
|
||||
<field name="figure_type">string</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
<record id="account_financial_report_ec_sales_amount" model="account.report.column">
|
||||
<field name="name">Amount</field>
|
||||
<field name="expression_label">balance</field>
|
||||
<field name="sortable" eval="True"/>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
26
addons/at_accounting/data/trial_balance.xml
Normal file
26
addons/at_accounting/data/trial_balance.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="trial_balance_report" model="account.report">
|
||||
<field name="name">Trial Balance</field>
|
||||
<field name="filter_journals" eval="True"/>
|
||||
<field name="filter_analytic" eval="True"/>
|
||||
<field name="filter_growth_comparison" eval="False"/>
|
||||
<field name="filter_multi_company">selector</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_hierarchy">by_default</field>
|
||||
<field name="filter_hide_0_lines">never</field>
|
||||
<field name="default_opening_date_filter">this_month</field>
|
||||
<field name="search_bar" eval="True"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_trial_balance_report_handler"/>
|
||||
<field name="column_ids">
|
||||
<record id="trial_balance_report_debit" model="account.report.column">
|
||||
<field name="name">Debit</field>
|
||||
<field name="expression_label">debit</field>
|
||||
</record>
|
||||
<record id="trial_balance_report_credit" model="account.report.column">
|
||||
<field name="name">Credit</field>
|
||||
<field name="expression_label">credit</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
32
addons/at_accounting/demo/at_accounting_demo.xml
Normal file
32
addons/at_accounting/demo/at_accounting_demo.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
|
||||
</record>
|
||||
<data noupdate="1">
|
||||
<record id="account_asset_group_demo" model="account.asset.group">
|
||||
<field name="name">Odoo Office</field>
|
||||
</record>
|
||||
|
||||
<record id="account_asset_model_demo" model="account.asset">
|
||||
<field name="name">Asset - 5 Years</field>
|
||||
<field name="prorata_computation_type">none</field>
|
||||
<field name="original_value">1000</field>
|
||||
<field name="journal_id" model="account.journal" search="[
|
||||
('type', '=', 'general'),
|
||||
('id', '!=', obj().env.user.company_id.currency_exchange_journal_id.id)]"/>
|
||||
<field name="account_asset_id" model="account.account" search="[
|
||||
('account_type', '=', 'asset_fixed'),
|
||||
('company_ids', '=', ref('base.main_company'))]"/>
|
||||
<field name="account_depreciation_id" model="account.account" search="[
|
||||
('account_type', '=', 'asset_fixed'),
|
||||
('company_ids', '=', ref('base.main_company'))]"/>
|
||||
<field name="account_depreciation_expense_id" model="account.account" search="[
|
||||
('account_type', '=', 'expense'),
|
||||
('tag_ids', 'in', [ref('account.account_tag_operating')]),
|
||||
('company_ids', '=', ref('base.main_company'))]"/>
|
||||
<field name="state">open</field>
|
||||
<field name="asset_group_id" ref="at_accounting.account_asset_group_demo"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
30
addons/at_accounting/demo/partner_bank.xml
Normal file
30
addons/at_accounting/demo/partner_bank.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="ofx_partner_bank_1" model="res.partner.bank">
|
||||
<field name="acc_number">BE68539007547034</field>
|
||||
<field name="partner_id" ref="base.res_partner_2"></field>
|
||||
<field name="bank_id" ref="base.res_bank_1"/>
|
||||
</record>
|
||||
|
||||
<record id="ofx_partner_bank_2" model="res.partner.bank">
|
||||
<field name="acc_number">00987654322</field>
|
||||
<field name="partner_id" ref="base.res_partner_3"></field>
|
||||
<field name="bank_id" ref="base.res_bank_1"/>
|
||||
</record>
|
||||
|
||||
<record id="qif_partner_bank_1" model="res.partner.bank">
|
||||
<field name="acc_number">10987654320</field>
|
||||
<field name="partner_id" ref="base.res_partner_4"></field>
|
||||
<field name="bank_id" ref="base.res_bank_1"/>
|
||||
</record>
|
||||
|
||||
<record id="qif_partner_bank_2" model="res.partner.bank">
|
||||
<field name="acc_number">10987654322</field>
|
||||
<field name="partner_id" ref="base.res_partner_3"></field>
|
||||
<field name="bank_id" ref="base.res_bank_1"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
5332
addons/at_accounting/i18n/ar.po
Normal file
5332
addons/at_accounting/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
5329
addons/at_accounting/i18n/at_accounting.pot
Normal file
5329
addons/at_accounting/i18n/at_accounting.pot
Normal file
File diff suppressed because it is too large
Load Diff
5309
addons/at_accounting/i18n/zh_CN.po
Normal file
5309
addons/at_accounting/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
42
addons/at_accounting/models/__init__.py
Normal file
42
addons/at_accounting/models/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
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
|
||||
BIN
addons/at_accounting/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
addons/at_accounting/models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
addons/at_accounting/models/__pycache__/budget.cpython-312.pyc
Normal file
BIN
addons/at_accounting/models/__pycache__/budget.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
addons/at_accounting/models/__pycache__/digest.cpython-312.pyc
Normal file
BIN
addons/at_accounting/models/__pycache__/digest.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
45
addons/at_accounting/models/account_account.py
Normal file
45
addons/at_accounting/models/account_account.py
Normal file
@@ -0,0 +1,45 @@
|
||||
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
|
||||
446
addons/at_accounting/models/account_aged_partner_balance.py
Normal file
446
addons/at_accounting/models/account_aged_partner_balance.py
Normal file
@@ -0,0 +1,446 @@
|
||||
# -*- 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')
|
||||
267
addons/at_accounting/models/account_analytic_report.py
Normal file
267
addons/at_accounting/models/account_analytic_report.py
Normal file
@@ -0,0 +1,267 @@
|
||||
# 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
|
||||
1726
addons/at_accounting/models/account_asset.py
Normal file
1726
addons/at_accounting/models/account_asset.py
Normal file
File diff suppressed because it is too large
Load Diff
248
addons/at_accounting/models/account_bank_statement.py
Normal file
248
addons/at_accounting/models/account_bank_statement.py
Normal file
@@ -0,0 +1,248 @@
|
||||
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("<p class='o_view_nocontent_smiling_face'>{}</p><p>{}<br/>{}</p>").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
|
||||
712
addons/at_accounting/models/account_cash_flow_report.py
Normal file
712
addons/at_accounting/models/account_cash_flow_report.py
Normal file
@@ -0,0 +1,712 @@
|
||||
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,
|
||||
}
|
||||
53
addons/at_accounting/models/account_chart_template.py
Normal file
53
addons/at_accounting/models/account_chart_template.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- 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
|
||||
581
addons/at_accounting/models/account_deferred_reports.py
Normal file
581
addons/at_accounting/models/account_deferred_reports.py
Normal file
@@ -0,0 +1,581 @@
|
||||
# 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'
|
||||
19
addons/at_accounting/models/account_fiscal_position.py
Normal file
19
addons/at_accounting/models/account_fiscal_position.py
Normal file
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
56
addons/at_accounting/models/account_fiscal_year.py
Normal file
56
addons/at_accounting/models/account_fiscal_year.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- 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.'))
|
||||
744
addons/at_accounting/models/account_general_ledger.py
Normal file
744
addons/at_accounting/models/account_general_ledger.py
Normal file
@@ -0,0 +1,744 @@
|
||||
# -*- 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,
|
||||
}
|
||||
1221
addons/at_accounting/models/account_generic_tax_report.py
Normal file
1221
addons/at_accounting/models/account_generic_tax_report.py
Normal file
File diff suppressed because it is too large
Load Diff
299
addons/at_accounting/models/account_journal.py
Normal file
299
addons/at_accounting/models/account_journal.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# -*- 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
|
||||
48
addons/at_accounting/models/account_journal_csv.py
Normal file
48
addons/at_accounting/models/account_journal_csv.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- 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',
|
||||
}
|
||||
}
|
||||
78
addons/at_accounting/models/account_journal_dashboard.py
Normal file
78
addons/at_accounting/models/account_journal_dashboard.py
Normal file
@@ -0,0 +1,78 @@
|
||||
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
|
||||
1324
addons/at_accounting/models/account_journal_report.py
Normal file
1324
addons/at_accounting/models/account_journal_report.py
Normal file
File diff suppressed because it is too large
Load Diff
1563
addons/at_accounting/models/account_move.py
Normal file
1563
addons/at_accounting/models/account_move.py
Normal file
File diff suppressed because it is too large
Load Diff
77
addons/at_accounting/models/account_move_line.py
Normal file
77
addons/at_accounting/models/account_move_line.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- 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)
|
||||
@@ -0,0 +1,384 @@
|
||||
# -*- 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
|
||||
771
addons/at_accounting/models/account_partner_ledger.py
Normal file
771
addons/at_accounting/models/account_partner_ledger.py
Normal file
@@ -0,0 +1,771 @@
|
||||
# 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
|
||||
39
addons/at_accounting/models/account_payment.py
Normal file
39
addons/at_accounting/models/account_payment.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- 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")
|
||||
)
|
||||
557
addons/at_accounting/models/account_reconcile_model.py
Normal file
557
addons/at_accounting/models/account_reconcile_model.py
Normal file
@@ -0,0 +1,557 @@
|
||||
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 <priority_order, rule> that could be overridden in others modules.
|
||||
|
||||
:return: a mapping <priority_order, rule> 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 <st_line, partner> 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<account.bank.statement.line>):
|
||||
The statement line that needs a partner to be found
|
||||
:return Model<res.partner>:
|
||||
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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user