240 lines
13 KiB
Python
240 lines
13 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from odoo import models, fields, api
|
|
from odoo.fields import Domain
|
|
from odoo.tools import SQL, Query
|
|
from odoo.tools.sql import table_columns
|
|
|
|
|
|
class AccountReport(models.Model):
|
|
_inherit = 'account.report'
|
|
|
|
filter_cash_basis = fields.Boolean(
|
|
string="Cash Basis",
|
|
compute=lambda x: x._compute_report_option_filter('filter_cash_basis', False), readonly=False, store=True, depends=['root_report_id', 'section_main_report_ids'],
|
|
help="Display the option to switch to cash basis mode."
|
|
)
|
|
|
|
# OVERRIDE
|
|
def get_options(self, previous_options):
|
|
options = super().get_options(previous_options)
|
|
options['filters']['show_cash_basis'] = self.filter_cash_basis
|
|
return options
|
|
|
|
def _init_options_cash_basis(self, options, previous_options):
|
|
if self.filter_cash_basis:
|
|
options['report_cash_basis'] = previous_options.get('report_cash_basis', False)
|
|
|
|
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('report_cash_basis')
|
|
|
|
@api.model
|
|
def _prepare_lines_for_cash_basis(self):
|
|
"""Prepare the cash_basis_temp_account_move_line substitute.
|
|
|
|
This method should be used once before all the SQL queries using the
|
|
table account_move_line for reports in cash basis.
|
|
It will create a new table like the account_move_line table, but with
|
|
amounts and the date relative to the cash basis.
|
|
"""
|
|
self.env.cr.execute(
|
|
"SELECT 1 FROM information_schema.tables WHERE table_name='cash_basis_temp_account_move_line'"
|
|
" AND table_schema::regnamespace = pg_my_temp_schema()")
|
|
if self.env.cr.fetchone():
|
|
return
|
|
|
|
all_fields = set(table_columns(self.env.cr, 'account_move_line'))
|
|
changed_fields = ['date', 'amount_currency', 'amount_residual', 'balance', 'debit', 'credit']
|
|
unchanged_fields = list(all_fields - set(changed_fields))
|
|
selected_journals = tuple(self.env.context.get('journal_ids', []))
|
|
sql = """ -- Create a temporary table
|
|
CREATE TEMPORARY TABLE IF NOT EXISTS cash_basis_temp_account_move_line () INHERITS (account_move_line) ON COMMIT DROP;
|
|
|
|
INSERT INTO cash_basis_temp_account_move_line ({all_fields}) SELECT
|
|
{unchanged_fields},
|
|
"account_move_line".date,
|
|
"account_move_line".amount_currency,
|
|
"account_move_line".amount_residual,
|
|
"account_move_line".balance,
|
|
"account_move_line".debit,
|
|
"account_move_line".credit
|
|
FROM ONLY account_move_line
|
|
WHERE (
|
|
"account_move_line".journal_id IN (SELECT id FROM account_journal WHERE type in ('cash', 'bank'))
|
|
OR "account_move_line".move_id NOT IN (
|
|
SELECT DISTINCT aml.move_id
|
|
FROM ONLY account_move_line aml
|
|
JOIN account_account account ON aml.account_id = account.id
|
|
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
|
|
)
|
|
)
|
|
{where_journals};
|
|
|
|
WITH payment_table AS (
|
|
SELECT
|
|
aml.move_id,
|
|
aml.account_id,
|
|
GREATEST(aml.date, aml2.date) AS date,
|
|
CASE WHEN (aml.balance = 0 OR sub_aml.total_per_account = 0)
|
|
THEN 0
|
|
ELSE part.amount / ABS(sub_aml.total_per_account)
|
|
END as matched_percentage,
|
|
CASE WHEN (aml.balance = 0 OR sub_aml_2.total_per_move = 0)
|
|
THEN 0
|
|
ELSE ABS(sub_aml.total_per_account) / ABS(sub_aml_2.total_per_move)
|
|
END as move_percentage
|
|
FROM account_partial_reconcile part
|
|
JOIN ONLY account_move_line aml ON aml.id = part.debit_move_id OR aml.id = part.credit_move_id
|
|
JOIN ONLY account_move_line aml2 ON
|
|
(aml2.id = part.credit_move_id OR aml2.id = part.debit_move_id)
|
|
AND aml.id != aml2.id
|
|
JOIN (
|
|
SELECT move_id, account_id, SUM(ABS(balance)) AS total_per_account
|
|
FROM ONLY account_move_line account_move_line
|
|
GROUP BY move_id, account_id
|
|
) sub_aml ON (aml.account_id = sub_aml.account_id AND aml.move_id=sub_aml.move_id)
|
|
JOIN (
|
|
SELECT move_id, SUM(ABS(balance)) AS total_per_move
|
|
FROM ONLY account_move_line aml_total
|
|
JOIN account_account account_total ON aml_total.account_id = account_total.id
|
|
WHERE account_total.account_type IN ('asset_receivable', 'liability_payable')
|
|
GROUP BY move_id
|
|
) sub_aml_2 ON (aml.move_id = sub_aml_2.move_id)
|
|
JOIN account_account account ON aml.account_id = account.id
|
|
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
|
|
)
|
|
INSERT INTO cash_basis_temp_account_move_line ({all_fields}) SELECT
|
|
{unchanged_fields},
|
|
ref.date,
|
|
CASE WHEN "account".id = ref.account_id
|
|
THEN ref.matched_percentage * "account_move_line".amount_currency
|
|
ELSE ref.matched_percentage * "account_move_line".amount_currency * ref.move_percentage
|
|
END,
|
|
CASE WHEN "account".id = ref.account_id
|
|
THEN ref.matched_percentage * "account_move_line".amount_residual
|
|
ELSE ref.matched_percentage * "account_move_line".amount_residual * ref.move_percentage
|
|
END,
|
|
CASE WHEN "account".id = ref.account_id
|
|
THEN ref.matched_percentage * "account_move_line".balance
|
|
ELSE ref.matched_percentage * "account_move_line".balance * ref.move_percentage
|
|
END,
|
|
CASE WHEN "account".id = ref.account_id
|
|
THEN ref.matched_percentage * "account_move_line".debit
|
|
ELSE ref.matched_percentage * "account_move_line".debit * ref.move_percentage
|
|
END,
|
|
CASE WHEN "account".id = ref.account_id
|
|
THEN ref.matched_percentage * "account_move_line".credit
|
|
ELSE ref.matched_percentage * "account_move_line".credit * ref.move_percentage
|
|
END
|
|
FROM payment_table ref
|
|
JOIN ONLY account_move_line account_move_line ON "account_move_line".move_id = ref.move_id
|
|
JOIN account_account account ON "account".id = "account_move_line".account_id
|
|
WHERE NOT (
|
|
"account_move_line".journal_id IN (SELECT id FROM account_journal WHERE type in ('cash', 'bank'))
|
|
OR "account_move_line".move_id NOT IN (
|
|
SELECT DISTINCT aml.move_id
|
|
FROM ONLY account_move_line aml
|
|
JOIN account_account account ON aml.account_id = account.id
|
|
WHERE account.account_type IN ('asset_receivable', 'liability_payable')
|
|
)
|
|
)
|
|
AND ("account".id = ref.account_id OR "account".account_type NOT IN ('asset_receivable', 'liability_payable'))
|
|
{where_journals};
|
|
|
|
-- Create an composite index to avoid seq.scan
|
|
CREATE INDEX IF NOT EXISTS cash_basis_temp_account_move_line_composite_idx on cash_basis_temp_account_move_line(date, journal_id, company_id, parent_state);
|
|
-- Update statistics for correct planning
|
|
ANALYZE cash_basis_temp_account_move_line;
|
|
""".format(
|
|
all_fields=', '.join(f'"{f}"' for f in (unchanged_fields + changed_fields)),
|
|
unchanged_fields=', '.join([f'"account_move_line"."{f}"' for f in unchanged_fields]),
|
|
where_journals=selected_journals and 'AND "account_move_line".journal_id IN %(journal_ids)s' or ''
|
|
)
|
|
params = {
|
|
'journal_ids': selected_journals,
|
|
}
|
|
self.env.cr.execute(sql, params)
|
|
|
|
@api.model
|
|
def _prepare_lines_for_analytic_groupby_with_cash_basis(self):
|
|
""" Prepare the analytic_cash_basis_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 and cash_basis_temp_account_move_line.
|
|
|
|
We will replace the values of the lines of the table cash_basis_temp_account_move_line
|
|
with the values of the analytic lines linked to these, but we will make the prorata
|
|
of the amounts with the portion of the amount paid.
|
|
"""
|
|
|
|
self.env.cr.execute(
|
|
"SELECT 1 FROM information_schema.tables WHERE table_name='analytic_cash_basis_temp_account_move_line'"
|
|
" AND table_schema::regnamespace = pg_my_temp_schema()")
|
|
if self.env.cr.fetchone():
|
|
return
|
|
|
|
line_fields = self.env['account.move.line'].fields_get()
|
|
stored_fields = {fld for fld in table_columns(self.env.cr, 'account_move_line') if fld in line_fields}
|
|
changed_equivalence_dict = {
|
|
"balance": SQL('CASE WHEN aml.balance != 0 THEN -aal.amount * cash_basis_aml.balance / aml.balance ELSE 0 END'),
|
|
"amount_currency": SQL('CASE WHEN aml.amount_currency != 0 THEN -aal.amount * cash_basis_aml.amount_currency / aml.amount_currency ELSE 0 END'),
|
|
"amount_residual": SQL('CASE WHEN aml.amount_residual != 0 THEN -aal.amount * cash_basis_aml.amount_residual / aml.amount_residual ELSE 0 END'),
|
|
"date": SQL('cash_basis_aml.date'),
|
|
"account_id": SQL('aal.general_account_id'),
|
|
"partner_id": SQL('aal.partner_id'),
|
|
"debit": SQL('CASE WHEN (aml.balance < 0) THEN -aal.amount * cash_basis_aml.balance / aml.balance ELSE 0 END'),
|
|
"credit": SQL('CASE WHEN (aml.balance > 0) THEN -aal.amount * cash_basis_aml.balance / aml.balance ELSE 0 END'),
|
|
}
|
|
|
|
selected_fields = []
|
|
for fname in stored_fields:
|
|
if fname in changed_equivalence_dict:
|
|
selected_fields.append(SQL('%s AS %s', changed_equivalence_dict[fname], SQL.identifier(fname)))
|
|
elif fname == 'analytic_distribution':
|
|
project_plan, other_plans = self.env['account.analytic.plan']._get_all_plans()
|
|
analytic_cols = SQL(', ').join(SQL.identifier('aal', n._column_name()) for n in (project_plan+other_plans))
|
|
selected_fields.append(SQL('to_jsonb(UNNEST(ARRAY_REMOVE(ARRAY[%s], NULL))) AS "analytic_distribution"', analytic_cols))
|
|
else:
|
|
selected_fields.append(SQL('aml.%s AS %s', SQL.identifier(fname), SQL.identifier(fname)))
|
|
|
|
query = SQL(
|
|
"""
|
|
-- Create a temporary table
|
|
CREATE TEMPORARY TABLE IF NOT EXISTS analytic_cash_basis_temp_account_move_line () inherits (account_move_line) ON COMMIT DROP;
|
|
|
|
INSERT INTO analytic_cash_basis_temp_account_move_line (%s)
|
|
SELECT %s
|
|
FROM ONLY cash_basis_temp_account_move_line cash_basis_aml
|
|
JOIN ONLY account_move_line aml ON aml.id = cash_basis_aml.id
|
|
JOIN account_analytic_line aal ON aml.id = aal.move_line_id;
|
|
|
|
-- Create a supporting index to avoid seq.scans
|
|
CREATE INDEX IF NOT EXISTS analytic_cash_basis_temp_account_move_line__composite_idx ON analytic_cash_basis_temp_account_move_line (analytic_distribution, journal_id, date, company_id);
|
|
-- Update statistics for correct planning
|
|
ANALYZE analytic_cash_basis_temp_account_move_line
|
|
""",
|
|
SQL(', ').join(SQL.identifier(field_name) for field_name in stored_fields),
|
|
SQL(', ').join(selected_fields),
|
|
)
|
|
|
|
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_cash_basis=options.get('report_cash_basis'))
|
|
return super(AccountReport, context_self)._get_report_query(options, date_scope, domain=domain)
|
|
|
|
def open_document(self, options, params=None):
|
|
action = super().open_document(options, params)
|
|
action['context'].pop('cash_basis', '')
|
|
return action
|
|
|
|
def action_audit_cell(self, options, params):
|
|
action = super().action_audit_cell(options, params)
|
|
# Only add the domain on the correct model (e.g. can be an account.analytic.line).
|
|
if options.get('report_cash_basis') and action['res_model'] == 'account.move.line':
|
|
action['domain'] = Domain(action['domain']) & Domain('move_id.impacting_cash_basis', '=', True)
|
|
return action
|