Files
enterprise-addons/account_saft/models/account_general_ledger.py
T

459 lines
23 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo.exceptions import UserError
from odoo.tools import float_repr, SQL
from odoo import api, fields, models, release, _
class AccountGeneralLedgerReportHandler(models.AbstractModel):
_inherit = 'account.general.ledger.report.handler'
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
company = self.env.company
args = []
if not company.company_registry:
args.append(_('the Company ID'))
if not (company.phone):
args.append(_('the phone number'))
if not (company.zip or company.city):
args.append(_('the city or zip code'))
if args:
warnings['account_saft.company_data_warning'] = {
'alert_type': 'warning',
'args': _(', ').join(args),
}
####################################################
# ACTIONS
####################################################
def action_fill_company_details(self, options, params):
return {
'type': 'ir.actions.act_window',
'name': _('Missing company details.'),
'res_model': 'res.company',
'views': [(False, 'form')],
'res_id': self.env.company.id,
}
def _saft_get_account_type(self, account_type):
"""To be overridden if specific account types are needed.
Some countries need to specify an account type, unique to the saf-t report.
:return: False if no account type needed, otherwise a string with the account type"""
return False
def _get_report_values(self, report, options, values=None):
"""
Get the report values based on lines. While parsing the lines, check for the existence of the
undistributed earnings line, which should be added to values['errors'].
:return dict: Keys are account_ids pointing to a dict of values
- account_id:
- sum {'debit': float, 'credit': float, 'balance': float}
- (optional) initial_balance: float
"""
options = report.get_options(previous_options={**options, 'export_mode': 'file', 'ignore_totals_below_sections': True, 'unfold_all': True})
lines = report._get_lines(options)
colname_to_idx = {col['expression_label']: idx for idx, col in enumerate(options.get('columns', []))}
current_account_id = 0
report_values = defaultdict(dict)
for line in lines:
model, res_id = report._get_model_info_from_id(line['id'])
if model == 'account.account':
current_account_id = res_id
report_values[current_account_id]['sum'] = {
'debit': line['columns'][colname_to_idx['debit']]['no_format'],
'credit': line['columns'][colname_to_idx['credit']]['no_format'],
'balance': line['columns'][colname_to_idx['balance']]['no_format'],
}
elif values and report._get_markup(line['id']) == 'undistributed_profits_losses':
balance_index = [c['expression_label'] for c in options['columns']].index('balance')
currency_id = self.env.company.currency_id
if not currency_id.is_zero(line['columns'][balance_index]['no_format']):
values['errors']['undistributed_earnings'] = {
'message': _('Undistributed profits or losses may cause export rejection.'),
'level': 'info',
}
if (isinstance(res_id, str) and 'balance_line' in res_id):
report_values[current_account_id]['initial_balance'] = line['columns'][colname_to_idx['balance']]['no_format']
return report_values
@api.model
def _get_formatted_account_vals_list(self, accounts, report_values):
account_vals_list = []
for account in accounts:
account_group_value = report_values[account.id]
account_vals_list.append(
{
'account': account,
'account_type': dict(self.env['account.account']._fields['account_type']._description_selection(self.env))[account.account_type],
'saft_account_type': self._saft_get_account_type(account.account_type),
'opening_balance': account_group_value.get('initial_balance', 0.0),
'closing_balance': account_group_value.get('sum', {}).get('balance', 0.0),
}
)
return account_vals_list
@api.model
def _saft_fill_report_general_ledger_accounts(self, report, options, values):
report_values = self._get_report_values(report, options, values)
accounts = self.env['account.account'].browse(report_values.keys())
values['account_vals_list'] = self._get_formatted_account_vals_list(accounts, report_values)
def _saft_fill_report_general_ledger_entries(self, report, options, values):
res = {
'total_debit_in_period': 0.0,
'total_credit_in_period': 0.0,
'journal_vals_list': [],
'move_vals_list': [],
'tax_detail_per_line_map': {},
}
# Fill 'total_debit_in_period', 'total_credit_in_period', 'move_vals_list'.
query = report._get_report_query(options, 'strict_range')
tax_name = self.env['account.tax']._field_to_sql('tax', 'name')
journal_name = self.env['account.journal']._field_to_sql('journal', 'name')
uom_name = self.env['uom.uom']._field_to_sql('uom', 'name')
product_name = self.env['product.template']._field_to_sql('product_template', 'name')
account_code = self.env['account.account']._field_to_sql('account', 'code')
query = SQL(
'''
SELECT
account_move_line.id,
account_move_line.display_type,
account_move_line.date,
account_move_line.name,
account_move_line.account_id,
account_move_line.partner_id,
account_move_line.currency_id,
account_move_line.amount_currency,
account_move_line.debit,
account_move_line.credit,
account_move_line.balance,
account_move_line.tax_line_id,
account_move_line.quantity,
account_move_line.price_unit,
account_move_line.product_id,
account_move_line.product_uom_id,
account_move_line.tax_base_amount,
account_move.id AS move_id,
account_move.name AS move_name,
account_move.move_type AS move_type,
account_move.create_date AS move_create_date,
account_move.invoice_date AS move_invoice_date,
account_move.invoice_origin AS move_invoice_origin,
account_move.statement_line_id AS move_statement_line_id,
tax.id AS tax_id,
%(tax_name)s AS tax_name,
tax.amount AS tax_amount,
tax.amount_type AS tax_amount_type,
journal.id AS journal_id,
journal.code AS journal_code,
%(journal_name)s AS journal_name,
journal.type AS journal_type,
account.account_type AS account_type,
%(account_code)s AS account_code,
currency.name AS currency_code,
%(product_name)s AS product_name,
product.default_code AS product_default_code,
%(uom_name)s AS product_uom_name
FROM %(table_references)s
JOIN account_move ON account_move.id = account_move_line.move_id
JOIN account_journal journal ON journal.id = account_move_line.journal_id
JOIN account_account account ON account.id = account_move_line.account_id
JOIN res_currency currency ON currency.id = account_move_line.currency_id
LEFT JOIN product_product product ON product.id = account_move_line.product_id
LEFT JOIN product_template product_template ON product_template.id = product.product_tmpl_id
LEFT JOIN uom_uom uom ON uom.id = account_move_line.product_uom_id
LEFT JOIN account_tax tax ON tax.id = account_move_line.tax_line_id
WHERE %(search_condition)s
ORDER BY account_move_line.date, account_move_line.id
''',
tax_name=tax_name,
journal_name=journal_name,
account_code=account_code,
product_name=product_name,
uom_name=uom_name,
table_references=query.from_clause,
search_condition=query.where_clause,
)
self.env.cr.execute(query)
journal_vals_map = {}
move_vals_map = {}
inbound_types = self.env['account.move'].get_inbound_types(include_receipts=True)
while True:
batched_line_vals = self.env.cr.dictfetchmany(10**4)
if not batched_line_vals:
break
for line_vals in batched_line_vals:
line_vals['rate'] = abs(line_vals['amount_currency']) / abs(line_vals['balance']) if line_vals['balance'] else 1.0
line_vals['tax_detail_vals_list'] = []
journal_vals_map.setdefault(line_vals['journal_id'], {
'id': line_vals['journal_id'],
'name': line_vals['journal_name'],
'code': line_vals['journal_code'],
'type': line_vals['journal_type'],
'move_vals_map': {},
})
journal_vals = journal_vals_map[line_vals['journal_id']]
move_vals = {
'id': line_vals['move_id'],
'name': line_vals['move_name'],
'type': line_vals['move_type'],
'sign': -1 if line_vals['move_type'] in inbound_types else 1,
'invoice_date': line_vals['move_invoice_date'],
'invoice_origin': line_vals['move_invoice_origin'],
'date': line_vals['date'],
'create_date': line_vals['move_create_date'],
'partner_id': line_vals['partner_id'],
'journal_type': line_vals['journal_type'],
'statement_line_id': line_vals['move_statement_line_id'],
'line_vals_list': [],
}
move_vals_map.setdefault(line_vals['move_id'], move_vals)
journal_vals['move_vals_map'].setdefault(line_vals['move_id'], move_vals)
computed_line_name = f"[{line_vals['product_default_code']}] {line_vals['product_name']}" if line_vals['product_default_code'] else line_vals['product_name'] or ''
line_vals['name'] = computed_line_name if not line_vals['name'] else line_vals['name']
move_vals = move_vals_map[line_vals['move_id']]
move_vals['line_vals_list'].append(line_vals)
# Track the total debit/period of the whole period.
res['total_debit_in_period'] += line_vals['debit']
res['total_credit_in_period'] += line_vals['credit']
res['tax_detail_per_line_map'][line_vals['id']] = line_vals
# Fill 'journal_vals_list'.
for journal_vals in journal_vals_map.values():
journal_vals['move_vals_list'] = list(journal_vals.pop('move_vals_map').values())
res['journal_vals_list'].append(journal_vals)
res['move_vals_list'] += journal_vals['move_vals_list']
values.update(res)
@api.model
def _saft_fill_report_tax_details_values(self, report, options, values):
tax_vals_map = {}
query = report._get_report_query(options, 'strict_range')
tax_details_query = self.env['account.move.line']._get_query_tax_details(query.from_clause, query.where_clause)
tax_name = self.env['account.tax']._field_to_sql('tax', 'name')
self.env.cr.execute(SQL('''
SELECT
tax_detail.base_line_id,
tax_line.currency_id,
tax.id AS tax_id,
tax.type_tax_use AS tax_type,
tax.amount_type AS tax_amount_type,
%(tax_name)s AS tax_name,
tax.amount AS tax_amount,
tax.create_date AS tax_create_date,
SUM(tax_detail.tax_amount) AS amount,
SUM(tax_detail.tax_amount) AS amount_currency,
SUM(tax_detail.base_amount) AS tax_base_amount
FROM (%(tax_details_query)s) AS tax_detail
JOIN account_move_line tax_line ON tax_line.id = tax_detail.tax_line_id
JOIN account_tax tax ON tax.id = tax_detail.tax_id
WHERE SIGN(tax_detail.tax_amount) = SIGN(tax_detail.base_amount)
GROUP BY tax_detail.base_line_id, tax_line.currency_id, tax.id
''', tax_name=tax_name, tax_details_query=tax_details_query))
for tax_vals in self.env.cr.dictfetchall():
line_vals = values['tax_detail_per_line_map'][tax_vals['base_line_id']]
line_vals['tax_detail_vals_list'].append({
**tax_vals,
'rate': line_vals['rate'],
'currency_code': line_vals['currency_code'],
'tax_base_amount': tax_vals['tax_base_amount'],
})
tax_vals_map.setdefault(tax_vals['tax_id'], {
'id': tax_vals['tax_id'],
'name': tax_vals['tax_name'],
'amount': tax_vals['tax_amount'],
'amount_type': tax_vals['tax_amount_type'],
'type': tax_vals['tax_type'],
'create_date': tax_vals['tax_create_date']
})
# Fill 'tax_vals_list'.
values['tax_vals_list'] = list(tax_vals_map.values())
def _get_all_partners(self, values, balance_result):
return self.env['res.partner'].browse([partner_id for partner_id, *__ in balance_result])
@api.model
def _saft_fill_report_partner_ledger_values(self, report, options, values):
res = {
'customer_vals_list': [],
'supplier_vals_list': [],
'partner_detail_map': defaultdict(lambda: {
'type': False, # TODO: remove in master
'types': [],
'addresses': [],
'contacts': [],
}),
}
# Fill 'customer_vals_list' and 'supplier_vals_list'
all_entries = options.get('all_entries')
domain = [
'&', '|', '&',
('account_id.account_type', 'in', ('asset_receivable', 'liability_payable')),
('parent_state', '!=', 'cancel') if all_entries else ('parent_state', '=', 'posted'),
('account_id.account_type', 'in', ('asset_fixed', 'asset_current', 'asset_non_current')),
('partner_id', '!=', False),
]
# If "all_entries" option is not True, "_get_report_query" adds a condition to include
# posted entries only.
# However depreciation lines could still be in draft for the desired period and need
# to be retrieved to populate the customers/suppliers list.
# "all_entries" should be forced to True in the options when calling "_get_report_query"
# in order to retrieve entries that are not posted.
# Use a copy of the options to do so to not impact subsequent use of the options.
options_copy = options.copy()
options_copy['all_entries'] = True
query = report._get_report_query(options_copy, 'from_beginning', domain=domain)
alias = query.join(lhs_alias=query.table, lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account')
query.groupby = SQL.identifier(query.table, "partner_id")
query.having = SQL(
"MIN(date) FILTER (WHERE date >= %(date_from)s AND date <= %(date_to)s) IS NOT NULL",
date_from=options['date']['date_from'],
date_to=options['date']['date_to'],
)
balance_result = self.env.execute_query(query.select(
SQL.identifier(query.table, "partner_id"),
SQL("BOOL_OR(%s = 'asset_receivable') AS any_receivable", SQL.identifier(alias, "account_type")),
SQL("BOOL_OR(%s = 'liability_payable') AS any_liability", SQL.identifier(alias, "account_type")),
SQL("COALESCE(SUM(balance) FILTER (WHERE date < %s AND %s = 'asset_receivable'), 0) AS opening_receivable", options['date']['date_from'], SQL.identifier(alias, "account_type")),
SQL("COALESCE(SUM(balance) FILTER (WHERE %s = 'asset_receivable'), 0) AS closing_receivable", SQL.identifier(alias, "account_type")),
SQL("COALESCE(SUM(balance) FILTER (WHERE date < %s AND %s = 'liability_payable'), 0) AS opening_payable", options['date']['date_from'], SQL.identifier(alias, "account_type")),
SQL("COALESCE(SUM(balance) FILTER (WHERE %s = 'liability_payable'), 0) AS closing_payable", SQL.identifier(alias, "account_type")),
))
all_partners = self._get_all_partners(values, balance_result)
for partner_id, any_receivable, any_liability, opening_receivable, closing_receivable, opening_payable, closing_payable in balance_result:
partner = self.env['res.partner'].browse(partner_id).with_prefetch(all_partners._prefetch_ids)
# TODO: remove in master: keeping a valid value in type in case the users does not have the updated xml
partner_type = 'customer' if partner.customer_rank >= partner.supplier_rank else 'supplier'
res['partner_detail_map'][partner_id]['type'] = partner_type
if any_liability or partner_type == 'supplier':
res['partner_detail_map'][partner_id]['types'].append('supplier')
res['supplier_vals_list'].append({
'partner': partner,
'opening_balance': opening_payable,
'closing_balance': closing_payable,
})
if any_receivable or partner_type == 'customer':
res['partner_detail_map'][partner_id]['types'].append('customer')
res['customer_vals_list'].append({
'partner': partner,
'opening_balance': opening_receivable,
'closing_balance': closing_receivable,
})
# Fill 'partner_detail_map'.
all_partners |= values['company'].partner_id
partner_addresses_map = defaultdict(dict)
partner_contacts_map = defaultdict(lambda: self.env['res.partner'])
def _track_address(current_partner, partner):
if partner.zip and partner.city or (options.get('saft_allow_empty_address') and partner != values['company'].partner_id):
address_key = (partner.zip, partner.city)
partner_addresses_map[current_partner][address_key] = partner
def _track_contact(current_partner, partner):
partner_contacts_map[current_partner] |= partner
for partner in all_partners:
_track_address(partner, partner)
# For individual partners, they are their own ContactPerson.
# For company partners, the child contact with lowest ID is the ContactPerson.
# For the current company, all child contacts are ContactPersons
# (to give users flexibility to indicate several ContactPersons).
if partner.is_company:
children = partner.child_ids.filtered(lambda p: p.type == 'contact' and p.active and not p.is_company).sorted('id')
if partner == values['company'].partner_id:
if not children:
values['errors']['missing_company_contact'] = {
'message': _('Please define one or more Contacts belonging to your company.'),
'action_text': _('View Company Partner'),
'action': partner._get_records_action(name=_("Define Contact(s)")),
'level': 'danger',
}
for child in children:
_track_contact(partner, child)
elif children:
_track_contact(partner, children[0])
else:
_track_contact(partner, partner)
no_partner_address = self.env['res.partner']
for partner in all_partners:
res['partner_detail_map'][partner.id].update({
'partner': partner,
'addresses': list(partner_addresses_map[partner].values()),
'contacts': partner_contacts_map[partner],
})
if not res['partner_detail_map'][partner.id]['addresses']:
no_partner_address |= partner
if no_partner_address:
values['errors']['missing_partner_zip_city'] = {
'message': _('Some partners are missing at least one address (Zip/City).'),
'action_text': _('View Partners'),
'action': no_partner_address._get_records_action(name=_("Partners to be checked")),
}
# Add newly computed values to the final template values.
values.update(res)
@api.model
def _saft_prepare_report_initial_values(self, options, values):
def format_float(amount, digits=2):
return float_repr(amount or 0.0, precision_digits=digits)
def format_date(date_str, formatter):
date_obj = fields.Date.to_date(date_str)
return date_obj.strftime(formatter)
values.update({
'company': self.env.company,
'xmlns': '',
'file_version': 'undefined',
'accounting_basis': 'undefined',
'today_str': fields.Date.to_string(fields.Date.context_today(self)),
'software_version': release.version,
'date_from': options['date']['date_from'],
'date_to': options['date']['date_to'],
'format_float': format_float,
'format_date': format_date,
'errors': {},
})
@api.model
def _saft_prepare_report_values(self, report, options):
if len(options["column_groups"]) > 1:
raise UserError(_("SAF-T is only compatible with one column group."))
report._init_currency_table(options)
options["single_column_group"] = next(iter(options["column_groups"].keys()))
template_values = {}
self._saft_prepare_report_initial_values(options, template_values)
self._saft_fill_report_general_ledger_accounts(report, options, template_values)
self._saft_fill_report_general_ledger_entries(report, options, template_values)
self._saft_fill_report_tax_details_values(report, options, template_values)
self._saft_fill_report_partner_ledger_values(report, options, template_values)
return template_values