290 lines
14 KiB
Python
290 lines
14 KiB
Python
from datetime import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import _, api, fields, models, release
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, SQL
|
|
|
|
DATEFORMAT_SIE4 = '%Y%m%d'
|
|
DATEFORMAT_MAIN = DEFAULT_SERVER_DATE_FORMAT
|
|
|
|
|
|
class GeneralLedgerCustomHandler(models.AbstractModel):
|
|
_inherit = 'account.general.ledger.report.handler'
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options=None):
|
|
super()._custom_options_initializer(report, options, previous_options)
|
|
if self.env.company.account_fiscal_country_id.code == 'SE':
|
|
options.setdefault('buttons', []).append({
|
|
'name': _("SIE 4"),
|
|
'sequence': 50,
|
|
'action': 'export_file',
|
|
'action_param': 'export_l10n_se_sie4_file',
|
|
'file_export_type': _("SIE")
|
|
})
|
|
|
|
@api.model
|
|
def _get_l10n_se_sie4_dates(self, options, use_sie4_format=False):
|
|
""" Returns a dictionary of date strings from previous year's start to next year's end in the desired format.
|
|
|
|
:param options: ``account.report`` options dictionary
|
|
:param use_sie4_format: desired format. ex: without sie4_format '2024-01-01' -> with sie4_format '20240101'
|
|
"""
|
|
result_strf = DATEFORMAT_SIE4 if use_sie4_format else DATEFORMAT_MAIN
|
|
datetime_from = datetime.strptime(options['date']['date_from'], DATEFORMAT_MAIN)
|
|
datetime_to = datetime.strptime(options['date']['date_to'], DATEFORMAT_MAIN)
|
|
previous_fiscal_year = self.env['account.fiscal.year'].search([
|
|
('date_to', '<=', options['date']['date_from'])
|
|
], order='date_to desc', limit=1)
|
|
prev_date_from = previous_fiscal_year.date_from if previous_fiscal_year else (datetime_from - relativedelta(years=1))
|
|
prev_date_to = previous_fiscal_year.date_to if previous_fiscal_year else (datetime_from - relativedelta(days=1))
|
|
return {
|
|
'prev_date_from': prev_date_from.strftime(result_strf),
|
|
'prev_date_to': prev_date_to.strftime(result_strf),
|
|
'curr_date_from': datetime_from.strftime(result_strf),
|
|
'curr_date_to': datetime_to.strftime(result_strf),
|
|
'next_date_from': (datetime_to + relativedelta(days=1)).strftime(result_strf),
|
|
'next_date_to': (datetime_to + relativedelta(years=1)).strftime(result_strf),
|
|
}
|
|
|
|
@api.model
|
|
def _get_l10n_se_sie4_ktyp(self, account_type):
|
|
group_asset = {'asset_receivable', 'asset_cash', 'asset_current', 'asset_non_current', 'asset_prepayments', 'asset_fixed'}
|
|
group_debt = {'liability_payable', 'liability_credit_card', 'liability_current', 'liability_non_current', 'equity',
|
|
'equity_unaffected', 'off_balance'}
|
|
group_cost = {'expense', 'expense_depreciation', 'expense_direct_cost'}
|
|
|
|
if account_type in group_asset:
|
|
return 'T'
|
|
if account_type in group_debt:
|
|
return 'S'
|
|
if account_type in group_cost:
|
|
return 'K'
|
|
return 'I' # income type
|
|
|
|
@api.model
|
|
def _get_l10n_se_sie4_options(self, report, date_from, date_to):
|
|
return report.get_options({
|
|
'date': {
|
|
'date_from': date_from,
|
|
'date_to': date_to,
|
|
'mode': 'range',
|
|
'filter': 'custom',
|
|
},
|
|
'export_mode': 'file',
|
|
})
|
|
|
|
@api.model
|
|
def _export_l10n_se_sie4_identification(self, options):
|
|
dates = self._get_l10n_se_sie4_dates(options, use_sie4_format=True)
|
|
|
|
return [
|
|
'#FLAGGA 0',
|
|
'#FORMAT PC8',
|
|
'#SIETYP 4',
|
|
f'#PROGRAM "Odoo" {release.version}',
|
|
f'#GEN {fields.Date.context_today(self).strftime(DATEFORMAT_SIE4)}',
|
|
f'#FNAMN "{options["companies"][0]["name"]}"',
|
|
f'#RAR -1 {dates["prev_date_from"]} {dates["prev_date_to"]}',
|
|
f'#RAR 0 {dates["curr_date_from"]} {dates["curr_date_to"]}',
|
|
]
|
|
|
|
@api.model
|
|
def _export_l10n_se_sie4_chart_of_account(self, options):
|
|
sie4_coa_lines = []
|
|
company_id = options['companies'][0]['id']
|
|
accounts = self.env['account.account'].with_company(company_id).search([])
|
|
|
|
for account in accounts:
|
|
sie4_coa_lines.extend((
|
|
f'#KONTO {account.code} "{account.name}"',
|
|
f'#KTYP {account.code} {self._get_l10n_se_sie4_ktyp(account.account_type)}',
|
|
))
|
|
|
|
return sie4_coa_lines
|
|
|
|
@api.model
|
|
def _l10n_se_sie4_get_initial_balances_values(self, report, options, filtered_accounts):
|
|
def get_dict_values_from_report_line(line):
|
|
return {
|
|
'balance': line['columns'][colname_to_idx['balance']]['no_format'],
|
|
'debit': line['columns'][colname_to_idx['debit']]['no_format'],
|
|
'credit': line['columns'][colname_to_idx['credit']]['no_format'],
|
|
}
|
|
options = {
|
|
**options,
|
|
'unfold_all': True,
|
|
}
|
|
colname_to_idx = {col['expression_label']: idx for idx, col in enumerate(options.get('columns', []))}
|
|
lines = report._get_lines(options)
|
|
initial_balances = {}
|
|
account_by_ids = {account.id: account for account in filtered_accounts}
|
|
for line in lines:
|
|
_model, res_id = report._get_model_info_from_id(line['id'])
|
|
|
|
if isinstance(res_id, str) and 'balance_line' in res_id:
|
|
account_id = report._get_res_id_from_line_id(line['id'], 'account.account')
|
|
account = account_by_ids.get(account_id, False)
|
|
if account:
|
|
initial_balances[account] = get_dict_values_from_report_line(line)
|
|
|
|
return initial_balances
|
|
|
|
@api.model
|
|
def _export_l10n_se_sie4_bs_balance(self, options):
|
|
""" Generates the opening/closing balance lines and return them as a list of string line.
|
|
In SIE 4, the opening/closing balance lines are saved with the following keywords:
|
|
|
|
- "IB" -> Opening balance
|
|
- "UB" -> Closing balance
|
|
|
|
The number to the next of them refers to what year they are referring to.
|
|
"0" means current year, "-1" means previous year.
|
|
Consequently, this also means that "#UB -1" is the exact same as "#IB 0".
|
|
"""
|
|
sie4_bs_balance_lines = []
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
dates = self._get_l10n_se_sie4_dates(options)
|
|
company_id = options['companies'][0]['id']
|
|
bs_accounts = self.env['account.account'].with_company(company_id).search_fetch(
|
|
domain=[('include_initial_balance', '=', True)],
|
|
field_names=['id'],
|
|
)
|
|
seen_bs_account_codes = set()
|
|
prev_ib, prev_ub, curr_ib, curr_ub = {}, {}, {}, {} # dict[str, str] | {account_code: balance_str_line}
|
|
|
|
prev_year_options = self._get_l10n_se_sie4_options(report, dates['prev_date_from'], dates['prev_date_to'])
|
|
next_year_options = self._get_l10n_se_sie4_options(report, dates['next_date_from'], dates['next_date_to'])
|
|
report._init_currency_table(prev_year_options)
|
|
report._init_currency_table(options)
|
|
report._init_currency_table(next_year_options)
|
|
prev_ib_values = self.with_company(company_id)._l10n_se_sie4_get_initial_balances_values(report, prev_year_options, bs_accounts)
|
|
curr_ib_values = self.with_company(company_id)._l10n_se_sie4_get_initial_balances_values(report, options, bs_accounts)
|
|
next_ib_values = self.with_company(company_id)._l10n_se_sie4_get_initial_balances_values(report, next_year_options, bs_accounts)
|
|
|
|
for ib_values in (prev_ib_values, curr_ib_values, next_ib_values):
|
|
for account, ib_item in ib_values.items():
|
|
if ib_item != {}:
|
|
seen_bs_account_codes.add(account.code)
|
|
if ib_values is prev_ib_values: # for [IB/-1] (previous year opening balance)
|
|
prev_ib[account.code] = f'#IB -1 {account.code} {ib_item["balance"]}'
|
|
elif ib_values is curr_ib_values: # for [UB/-1] and [IB/0] (previous year closing balance + current year opening balance)
|
|
prev_ub[account.code] = f'#UB -1 {account.code} {ib_item["balance"]}'
|
|
curr_ib[account.code] = f'#IB 0 {account.code} {ib_item["balance"]}'
|
|
else: # ib_values is next_ib_values # for [UB/0] (current year closing balance)
|
|
curr_ub[account.code] = f'#UB 0 {account.code} {ib_item["balance"]}'
|
|
|
|
default_ib_values = ('#IB -1', '#UB -1', '#IB 0', '#UB 0')
|
|
for account_code in sorted(seen_bs_account_codes):
|
|
for idx, period_ib in enumerate((prev_ib, prev_ub, curr_ib, curr_ub)):
|
|
if account_code in period_ib:
|
|
sie4_bs_balance_lines.append(period_ib[account_code])
|
|
else: # if the mentioned account doesn't exist in other periods, fill it with a default value
|
|
default_ib_value = default_ib_values[idx]
|
|
sie4_bs_balance_lines.append(f'{default_ib_value} {account_code} 0.0')
|
|
|
|
return sie4_bs_balance_lines
|
|
|
|
@api.model
|
|
def _export_l10n_se_sie4_pl_balance(self, options):
|
|
sie4_pl_balance_lines = []
|
|
dates = self._get_l10n_se_sie4_dates(options)
|
|
company_id = options['companies'][0]['id']
|
|
|
|
common_domain = [
|
|
('account_id.include_initial_balance', '=', False),
|
|
('display_type', 'not in', ('line_section', 'line_subsection', 'line_note')),
|
|
('move_id.state', '!=', 'cancel'),
|
|
]
|
|
prev_year_domain = common_domain + [('date', '>=', dates['prev_date_from']), ('date', '<=', dates['prev_date_to'])]
|
|
curr_year_domain = common_domain + [('date', '>=', dates['curr_date_from']), ('date', '<=', dates['curr_date_to'])]
|
|
|
|
prev_account_sum_group = self.env['account.move.line'].with_company(company_id)._read_group(prev_year_domain, ['account_id'], ['balance:sum'])
|
|
curr_account_sum_group = self.env['account.move.line'].with_company(company_id)._read_group(curr_year_domain, ['account_id'], ['balance:sum'])
|
|
prev_code_sum_map = {account.code: str(account_sum) for account, account_sum in prev_account_sum_group}
|
|
curr_code_sum_map = {account.code: str(account_sum) for account, account_sum in curr_account_sum_group}
|
|
seen_account_code = set(prev_code_sum_map.keys()).union(curr_code_sum_map.keys())
|
|
|
|
for account_code in sorted(seen_account_code):
|
|
sie4_pl_balance_lines.extend((
|
|
f'#RES -1 {account_code} {prev_code_sum_map.get(account_code, "0.0")}',
|
|
f'#RES 0 {account_code} {curr_code_sum_map.get(account_code, "0.0")}',
|
|
))
|
|
|
|
return sie4_pl_balance_lines
|
|
|
|
@api.model
|
|
def _export_l10n_se_sie4_verification(self, options):
|
|
sie4_verification_lines = []
|
|
report = self.env['account.report'].browse(options.get('report_id'))
|
|
company_ids = report.get_report_company_ids(options)
|
|
query = report._get_report_query(options, 'strict_range', [])
|
|
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
|
|
account_env = self.env['account.account'].with_context(allowed_company_ids=company_ids)
|
|
account_code = account_env._field_to_sql(account_alias, 'code', query)
|
|
sql_query = SQL(
|
|
"""
|
|
SELECT
|
|
m.id AS move_id,
|
|
m.name AS move_name,
|
|
m.date AS move_date,
|
|
%(account_code)s AS account_code,
|
|
account_move_line.balance AS balance
|
|
FROM %(table)s
|
|
JOIN account_move m ON m.id = account_move_line.move_id
|
|
WHERE %(where_clause)s
|
|
ORDER BY m.date ASC, m.name ASC, m.id ASC, account_move_line.id ASC
|
|
""",
|
|
account_code=account_code,
|
|
table=query.from_clause,
|
|
where_clause=query.where_clause,
|
|
)
|
|
self.env.flush_all()
|
|
self.env.cr.execute(sql_query)
|
|
last_move_id = None
|
|
verification_idx = 0
|
|
for row in self.env.cr.dictfetchall():
|
|
current_move_id = row['move_id']
|
|
if current_move_id != last_move_id:
|
|
if last_move_id is not None:
|
|
sie4_verification_lines.append('}')
|
|
verification_idx += 1
|
|
move_date_str = row['move_date'].strftime(DATEFORMAT_SIE4)
|
|
sie4_verification_lines.extend((
|
|
f'#VER A {verification_idx} {move_date_str} "{row["move_name"]}"',
|
|
'{'
|
|
))
|
|
last_move_id = current_move_id
|
|
sie4_verification_lines.append(f' #TRANS {row["account_code"]} {{}} {row["balance"]}')
|
|
if last_move_id is not None:
|
|
sie4_verification_lines.append('}')
|
|
return sie4_verification_lines
|
|
|
|
def export_l10n_se_sie4_file(self, options):
|
|
if options['date']['period_type'] != 'fiscalyear':
|
|
raise UserError(_("You must set the period type to be in fiscal year in order to export SIE 4 file."))
|
|
if len(options['companies']) > 1:
|
|
selected_companies = self.env['res.company'].search_fetch(
|
|
domain=[('id', 'in', [company_dict['id'] for company_dict in options['companies']])],
|
|
field_names=['chart_template'],
|
|
)
|
|
if len(set(selected_companies.mapped('chart_template'))) >= 2: # we can only export one chart template
|
|
raise UserError(_("You can't export multiple companies with different templates."))
|
|
else:
|
|
# filter other selected company/ies in the options
|
|
options['companies'] = [company_dict for company_dict in options['companies'] if company_dict['id'] == self.env.company.id]
|
|
|
|
content_lines = [
|
|
*self._export_l10n_se_sie4_identification(options),
|
|
*self._export_l10n_se_sie4_chart_of_account(options),
|
|
*self._export_l10n_se_sie4_bs_balance(options),
|
|
*self._export_l10n_se_sie4_pl_balance(options),
|
|
*self._export_l10n_se_sie4_verification(options),
|
|
'', # to make sure the file ends with a new line
|
|
]
|
|
return {
|
|
'file_name': f'odoo_sie4_{fields.Date.context_today(self).strftime(DATEFORMAT_SIE4)}.se',
|
|
'file_content': '\n'.join(content_lines).encode('437'),
|
|
'file_type': 'txt',
|
|
}
|