930 lines
45 KiB
Python
930 lines
45 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from collections import defaultdict
|
|
|
|
import stdnum.ro
|
|
|
|
from odoo import api, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_repr, SQL, Query
|
|
|
|
from odoo.addons.account_edi_ubl_cii.models.account_edi_common import UOM_TO_UNECE_CODE
|
|
|
|
|
|
class AccountGeneralLedgerReportHandler(models.AbstractModel):
|
|
_inherit = 'account.general.ledger.report.handler'
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
super()._custom_options_initializer(report, options, previous_options)
|
|
|
|
if self.env.company.account_fiscal_country_id.code != 'RO':
|
|
return
|
|
|
|
options.setdefault('buttons', []).append({
|
|
'name': _('SAF-T (D406 Declaration)'),
|
|
'sequence': 50,
|
|
'action': 'export_file',
|
|
'action_param': 'l10n_ro_export_saft_to_xml_monthly',
|
|
'file_export_type': _('XML')
|
|
})
|
|
options.setdefault('buttons', []).append({
|
|
'name': _('SAF-T (D406 Asset Declaration)'),
|
|
'sequence': 51,
|
|
'action': 'export_file',
|
|
'action_param': 'l10n_ro_export_saft_to_xml_assets',
|
|
'file_export_type': _('XML')
|
|
})
|
|
|
|
@api.model
|
|
def l10n_ro_export_saft_to_xml_monthly(self, options):
|
|
options['l10n_ro_saft_type'] = 'monthly'
|
|
options['l10n_ro_saft_required_sections'] = self._set_l10n_ro_saft_required_sections(options['l10n_ro_saft_type'])
|
|
return self.l10n_ro_export_saft_to_xml(options)
|
|
|
|
@api.model
|
|
def l10n_ro_export_saft_to_xml_assets(self, options):
|
|
options['l10n_ro_saft_type'] = 'assets'
|
|
options['l10n_ro_saft_required_sections'] = self._set_l10n_ro_saft_required_sections(options['l10n_ro_saft_type'])
|
|
return self.l10n_ro_export_saft_to_xml(options)
|
|
|
|
@api.model
|
|
def _set_l10n_ro_saft_required_sections(self, export_type):
|
|
"""Define which sections of the XML are required to export based on the
|
|
type of export, which can either be monthly, assets"""
|
|
monthly = (export_type == 'monthly')
|
|
assets = (export_type == 'assets')
|
|
|
|
return {
|
|
'master_files': {
|
|
'general_ledger_accounts': monthly or assets,
|
|
'customers': monthly,
|
|
'suppliers': monthly,
|
|
'tax_table': monthly,
|
|
'uom_table': monthly,
|
|
'analysis_type_table': monthly or assets,
|
|
'movement_type_table': False,
|
|
'products': monthly,
|
|
'physical_stocks': False,
|
|
'owners': False,
|
|
'assets': assets,
|
|
},
|
|
'general_ledger_entries': monthly,
|
|
'source_documents': {
|
|
'sales_invoices': monthly,
|
|
'purchase_invoices': monthly,
|
|
'payments': monthly,
|
|
'movement_of_goods': False,
|
|
'asset_transactions': assets,
|
|
}
|
|
}
|
|
|
|
@api.model
|
|
def l10n_ro_export_saft_to_xml(self, options):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
values = self._l10n_ro_saft_prepare_report_values(report, options)
|
|
file_data = report._generate_file_data_with_error_check(
|
|
options,
|
|
self.env['ir.qweb']._render,
|
|
{'values': values, 'template': 'l10n_ro_saft.saft_template', 'file_type': 'xml'},
|
|
values['errors'],
|
|
)
|
|
return file_data
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_check_report_values(self, values, options):
|
|
# Overwrites the values['errors'], but we still want to keep 'undistributed_earnings' warning if it exists
|
|
values['errors'] = {
|
|
'undistributed_earnings': values['errors']['undistributed_earnings']
|
|
} if 'undistributed_earnings' in values['errors'] else {}
|
|
values['errors'] |= {
|
|
**self._l10n_ro_saft_check_header_values(options, values),
|
|
**self._l10n_ro_saft_check_partner_values(values),
|
|
**self._l10n_ro_saft_check_tax_values(options, values),
|
|
**self._l10n_ro_saft_check_product_values(values),
|
|
**self._l10n_ro_saft_check_asset_values(values),
|
|
}
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_prepare_report_values(self, report, options):
|
|
options['saft_allow_empty_address'] = self.env.company.country_code == 'RO'
|
|
values = super()._saft_prepare_report_values(report, options)
|
|
|
|
# The saft template needs to know which sections are requested
|
|
values['l10n_ro_saft_required_sections'] = options['l10n_ro_saft_required_sections']
|
|
|
|
self._l10n_ro_saft_fill_header_values(options, values)
|
|
self._l10n_ro_saft_fill_partner_values(values)
|
|
self._l10n_ro_saft_fill_tax_values(options, values)
|
|
self._l10n_ro_saft_fill_uom_values(options, values)
|
|
self._l10n_ro_saft_fill_product_values(options, values)
|
|
self._l10n_ro_saft_fill_account_code_by_id(values)
|
|
self._l10n_ro_saft_fill_invoice_values(values)
|
|
self._l10n_ro_saft_fill_payment_values(values)
|
|
self._l10n_ro_saft_fill_report_assets_values(options, values)
|
|
self._l10n_ro_saft_fill_asset_transactions_values(options, values)
|
|
self._l10n_ro_saft_clean_customer_suppliers_vals_list(options, values)
|
|
self._l10n_ro_saft_clean_move_vals_list(values)
|
|
self._l10n_ro_saft_check_report_values(values, options)
|
|
|
|
return values
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_check_header_values(self, options, values):
|
|
""" Check whether the company configuration is correct for filling in the Header. """
|
|
def get_company_action(message):
|
|
return {
|
|
'message': message,
|
|
'action_text': self.env._("View Company/ies"),
|
|
'action': values['company']._get_records_action(name=self.env._('Invalid Company/ies'))
|
|
}
|
|
|
|
errors = {}
|
|
|
|
# The company must have a Tax Accounting Basis defined.
|
|
if not values['company'].l10n_ro_saft_tax_accounting_basis:
|
|
errors['settings_accounting_basis_missing'] = {
|
|
'message': _('Please set the company Tax Accounting Basis.'),
|
|
'action_text': _('View Settings'),
|
|
'action': {
|
|
'name': _("Settings"),
|
|
'type': 'ir.actions.act_url',
|
|
'target': 'self',
|
|
'url': '/odoo/settings#l10n_ro_saft_tax_accounting_basis',
|
|
}
|
|
}
|
|
|
|
# The company must have a bank account defined.
|
|
if not values['company'].bank_ids:
|
|
errors['company_bank_account_missing'] = {
|
|
'message': _('Please define a `Bank Account` for your company.'),
|
|
'action_text': _('View Company/ies'),
|
|
'action': values['company'].partner_id._get_records_action(name=_("Invalid Company/ies")),
|
|
}
|
|
|
|
# The company must have a phone number defined.
|
|
if not values['company'].partner_id.phone:
|
|
errors['company_phone_missing'] = get_company_action(_('Please define a `Phone Number` for your company.'))
|
|
|
|
# The company must either have a VAT number defined (if it is registered for VAT in Romania),
|
|
# or have its CUI number in the company_registry field (if not registered for VAT).
|
|
partner = values['company'].partner_id
|
|
if not partner.vat:
|
|
if partner.company_registry:
|
|
if not stdnum.ro.cui.is_valid(partner.company_registry):
|
|
errors['company_registry_number_invalid'] = get_company_action(_('The CUI number for your company (under `Company Registry` in the Company settings) is incorrect.'))
|
|
else:
|
|
errors['company_vat_registry_number_missing'] = get_company_action(_('In the Company settings, please set your company VAT number under `Tax ID` if registered for VAT, or your CUI number under `Company Registry`.'))
|
|
|
|
return errors
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_header_values(self, options, values):
|
|
""" Fill in header values """
|
|
# Mandatory values for the D.406 declaration
|
|
values.update({
|
|
'xmlns': 'mfp:anaf:dgti:d406:declaratie:v1',
|
|
'file_version': '2.4.8',
|
|
})
|
|
|
|
# The TaxAccountingBasis should indicate the type of CoA that is installed.
|
|
values.update({
|
|
'accounting_basis': values['company'].l10n_ro_saft_tax_accounting_basis or 'A',
|
|
})
|
|
|
|
# The RegistrationNumber field for the company must be the VAT number if the
|
|
# company is VAT-registered, otherwise, it should be the CUI number.
|
|
partner = values['company'].partner_id
|
|
if partner.vat:
|
|
values['company_registration_number'] = stdnum.ro.cf.compact(partner.vat)
|
|
elif partner.company_registry:
|
|
values['company_registration_number'] = stdnum.ro.cui.compact(partner.company_registry)
|
|
|
|
# The HeaderComment section must indicate the type of declaration:
|
|
# - L for monthly returns
|
|
# - T for quarterly returns
|
|
# - A for annual returns
|
|
# - C for returns on request (see l10n_ro_saft_stock/models/account_general_ledger.py)
|
|
# - NL for non-residents monthly
|
|
# - NT for non-residents quarterly
|
|
if values['company'].country_code == 'RO':
|
|
declaration_type = {
|
|
'month': 'L',
|
|
'quarter': 'T',
|
|
'year': 'A',
|
|
'fiscalyear': 'A',
|
|
}.get(options['date']['period_type'], 'C')
|
|
else:
|
|
declaration_type = {
|
|
'month': 'NL',
|
|
'quarter': 'NT',
|
|
}.get(options['date']['period_type'], '')
|
|
values['declaration_type'] = declaration_type
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_check_partner_values(self, values):
|
|
""" Check whether all required information on partners is there. """
|
|
def build_partners_action(partners, message):
|
|
return {
|
|
'message': message,
|
|
'action': partners._get_records_action(name=self.env._("Invalid Partner(s)")),
|
|
}
|
|
|
|
faulty_partners = defaultdict(lambda: self.env['res.partner'])
|
|
for partner_vals in values['partner_detail_map'].values():
|
|
partner = partner_vals['partner']
|
|
partner_type = partner_vals.get('type')
|
|
if not partner.name:
|
|
faulty_partners['partner_missing_name'] |= partner
|
|
# Partner addresses must include the City and Country.
|
|
if not partner.city:
|
|
faulty_partners['partner_city_missing'] |= partner
|
|
if not partner.country_code:
|
|
faulty_partners['partner_country_missing'] |= partner
|
|
# Partner country code should match the VAT prefix, if the VAT number is provided
|
|
elif (
|
|
partner_type in ('supplier', 'customer')
|
|
and partner.vat
|
|
and partner.vat[:2].isalpha()
|
|
and partner.country_code != partner._split_vat(partner.vat)[0]
|
|
):
|
|
faulty_partners['partner_vat_doesnt_match_country'] |= partner
|
|
# Romanian company partners should have their VAT number or CUI number set in the Tax ID or company_registry field.
|
|
# Foreign company partners should have their VAT number set in the Tax ID field.
|
|
if partner_type in ('supplier', 'customer') and partner.is_company:
|
|
if not partner.vat:
|
|
vat_country, vat_number = 'ro', ''
|
|
elif not partner.vat[:2].isalpha():
|
|
vat_country, vat_number = 'ro', partner.vat
|
|
else:
|
|
vat_country, vat_number = partner._split_vat(partner.vat)
|
|
if partner.country_code == 'RO' or not partner.country_code:
|
|
cui = partner.company_registry or vat_number
|
|
if not stdnum.ro.cui.is_valid(cui):
|
|
faulty_partners['partner_registry_incorrect'] |= partner
|
|
elif not partner.vat or not partner._check_vat_number(vat_country, vat_number):
|
|
faulty_partners['partner_vat_incorrect'] |= partner
|
|
elif partner.perform_vies_validation and not partner.vies_valid:
|
|
faulty_partners['partner_vies_failed'] |= partner
|
|
|
|
descriptions = {
|
|
"partner_city_missing": (_("Partners should have their city."), "warning"),
|
|
"partner_country_missing": (_("Partners should have a country"), "warning"),
|
|
"partner_vat_doesnt_match_country": (_("Partners' VAT prefix should correspond to their country."), "warning"),
|
|
"partner_registry_incorrect": (_("Some partners have missing or invalid CUI numbers in `Company Registry`. Example of a valid CUI: 18547290"), "warning"),
|
|
"partner_vat_missing": (_("Some partners have missing VAT numbers."), "warning"),
|
|
"partner_vat_incorrect": (_("Some partners have invalid VAT numbers. Example of a valid VAT: RO18547290"), "warning"),
|
|
"partner_vies_failed": (_("The VAT numbers for the following partners failed the VIES check:"), "warning"),
|
|
"partner_missing_name": (_("These partners are missing a name:"), "danger"),
|
|
}
|
|
return {
|
|
key: {
|
|
'message': descriptions[key][0],
|
|
'action_text': self.env._('View Partners'),
|
|
'action': partners._get_records_action(name=self.env._("Invalid Partner(s)")),
|
|
'level': descriptions[key][1]
|
|
}
|
|
for key, partners in faulty_partners.items()
|
|
}
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_get_registration_number(self, partner):
|
|
""" Compute the RegistrationNumber field for a partner, consisting of a two-digit type followed by the partner's ID:
|
|
00 + CUI number (without the 'RO' prefix), for economic operators registered in Romania;
|
|
01 + country code + VAT code, for economic operators from EU Member States other than Romania;
|
|
02 + country code + VAT code, for economic operators from non-EU states EU;
|
|
03 + CNP, for Romanian citizens and individuals resident in Romania, or 03 + NIF for non-resident individuals;
|
|
04 + partner ID, for customers from Romania not subject to VAT and whose CNP is unknown (e.g. e-commerce);
|
|
05 + country code + partner ID, for customers from EU Member States other than Romania not subject to VAT;
|
|
06 + country code + partner ID, for customers from non-EU states not subject to VAT;
|
|
08 + 13 zeros (080000000000000), for unidentified customers in PoS transactions. This code is restricted ONLY to such transactions;
|
|
09 + NIF for non-resident legal entities registered in Romania;
|
|
(types 10 and 11 are restricted to banks)
|
|
This function requires the country_code to be correctly set on the partner.
|
|
|
|
:param partner: the res.partner for which to generate the registration number
|
|
|
|
:return: the RegistrationNumber (a string)
|
|
"""
|
|
if not partner:
|
|
return '0'
|
|
if partner.is_company:
|
|
if not partner.vat:
|
|
vat_country, vat_number = 'ro', ''
|
|
elif not partner.vat[:2].isalpha():
|
|
vat_country, vat_number = 'ro', partner.vat
|
|
else:
|
|
vat_country, vat_number = partner._split_vat(partner.vat)
|
|
if partner.country_code == 'RO' or not partner.country_code:
|
|
# For Romanian companies, the company_registry field should contain the CUI number, which is a 8-digit number without the 'RO' prefix
|
|
# Alternatively, we can get the CUI from the VAT number by removing the 'RO' prefix.
|
|
cui = partner.company_registry or vat_number
|
|
return '00' + stdnum.ro.cui.compact(cui)
|
|
elif partner.country_id and 'EU' in partner.country_id.country_group_codes:
|
|
return '01' + vat_country + vat_number
|
|
else:
|
|
return '02' + vat_country + vat_number
|
|
else:
|
|
if partner.company_registry and stdnum.ro.cnp.is_valid(partner.company_registry):
|
|
# For individuals having a valid CNP or NIF, that should be used
|
|
return stdnum.ro.cnp.compact(partner.company_registry)
|
|
elif partner.country_code == 'RO' or not partner.country_code:
|
|
return '04' + str(partner.id)
|
|
elif partner.country_id and 'EU' in partner.country_id.country_group_codes:
|
|
return '05' + partner.country_code + str(partner.id)
|
|
else:
|
|
return '06' + partner.country_code + str(partner.id)
|
|
# Code 08 (unidentified customer in PoS transactions) not implemented because the PoS does
|
|
# not generate anonymous invoices.
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_partner_values(self, values):
|
|
""" Fill in partner-related values in the values dict, performing checks as we go. """
|
|
for partner_vals in values['partner_detail_map'].values():
|
|
partner_vals['registration_number'] = self._l10n_ro_saft_get_registration_number(partner_vals['partner'])
|
|
partner_vals['l10n_ro_saft_contacts'] = partner_vals['contacts'].filtered(
|
|
# Only provide partners which have a first name, last name and phone number.
|
|
lambda contact: ' ' in contact.name[1:-1] and contact.phone
|
|
)
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_check_tax_values(self, options, values):
|
|
""" Check whether all taxes have a Romanian SAFT tax type and tax code on them. """
|
|
|
|
errors = {}
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['tax_table']:
|
|
return errors
|
|
|
|
encountered_tax_ids = [tax_vals['id'] for tax_vals in values['tax_vals_list']]
|
|
faulty_taxes = self.env['account.tax'].search([
|
|
('id', 'in', encountered_tax_ids),
|
|
'|', ('l10n_ro_saft_tax_type_id', '=', False), ('l10n_ro_saft_tax_code', '=', False)
|
|
])
|
|
if faulty_taxes:
|
|
errors['taxes_tax_type_missing'] = {
|
|
'message': _('Some taxes are missing the "Romanian SAF-T Tax Type" '
|
|
'and/or "Romanian SAF-T Tax Code" field(s).'),
|
|
'action_text': _('View Taxes'),
|
|
'action': faulty_taxes._get_records_action(name=_("Taxes missing Tax Type or Tax Code (RO)")),
|
|
}
|
|
return errors
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_tax_values(self, options, values):
|
|
""" Fill in the Romanian tax type, tax type description (in Romanian, if available), and tax code. """
|
|
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['tax_table']:
|
|
return
|
|
|
|
encountered_tax_ids = [tax_vals['id'] for tax_vals in values['tax_vals_list']]
|
|
lang = self.env['res.lang']._get_code('ro_RO')
|
|
encountered_taxes = self.env['account.tax'].with_context({'lang': lang}).browse(encountered_tax_ids)
|
|
tax_fields_by_id = {
|
|
tax.id: {
|
|
'l10n_ro_saft_tax_type': tax.l10n_ro_saft_tax_type_id.code if tax.l10n_ro_saft_tax_type_id else '',
|
|
'l10n_ro_saft_tax_type_description': tax.l10n_ro_saft_tax_type_id.description if tax.l10n_ro_saft_tax_type_id else '',
|
|
'l10n_ro_saft_tax_code': tax.l10n_ro_saft_tax_code or '',
|
|
}
|
|
for tax in encountered_taxes
|
|
}
|
|
|
|
for line_vals in values['tax_detail_per_line_map'].values():
|
|
for tax_detail_vals in line_vals['tax_detail_vals_list']:
|
|
tax_fields = tax_fields_by_id[tax_detail_vals['tax_id']]
|
|
tax_detail_vals.update(tax_fields)
|
|
|
|
for tax_vals in values['tax_vals_list']:
|
|
tax_fields = tax_fields_by_id[tax_vals['id']]
|
|
tax_vals.update(tax_fields)
|
|
|
|
def _get_encountered_product_uom_ids(self, values):
|
|
return {
|
|
line_vals['product_uom_id']
|
|
for move_vals in values['move_vals_list']
|
|
for line_vals in move_vals['line_vals_list']
|
|
if line_vals['product_uom_id']
|
|
}
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_uom_values(self, options, values):
|
|
""" Fill UoMs and unece_code_by_uom """
|
|
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['uom_table']:
|
|
values.update({
|
|
'uoms': [],
|
|
'unece_code_by_uom': {},
|
|
})
|
|
return
|
|
|
|
encountered_product_uom_ids = sorted(self._get_encountered_product_uom_ids(values))
|
|
uoms = self.env['uom.uom'].browse(encountered_product_uom_ids)
|
|
|
|
# Provide a dict that links each UOM id to its UNECE code
|
|
uom_xmlids = uoms.get_external_id()
|
|
unece_code_by_uom = {
|
|
uom.id: UOM_TO_UNECE_CODE.get(uom_xmlids[uom.id], 'C62') for uom in uoms
|
|
}
|
|
|
|
values.update({
|
|
'uoms': uoms,
|
|
'unece_code_by_uom': unece_code_by_uom,
|
|
})
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_check_product_values(self, values):
|
|
""" Check whether each product has a ref, no products have duplicate refs,
|
|
and if the intrastat module is installed, that each product has an Intrastat Code. """
|
|
def get_product_action(message, products, level='warning'):
|
|
return {
|
|
'message': message,
|
|
'action_text': self.env._('View Product(s)'),
|
|
'action': products._get_records_action(name=self.env._("Invalid Product(s)")),
|
|
'level': level,
|
|
}
|
|
|
|
encountered_product_ids = sorted(self._get_encountered_product_ids(values))
|
|
encountered_products = self.env['product.product'].browse(encountered_product_ids)
|
|
product_refs = encountered_products.mapped('default_code')
|
|
products_no_ref = encountered_products.filtered(lambda product: not product.default_code)
|
|
products_dup_ref = (encountered_products - products_no_ref).filtered(lambda product: product_refs.count(product.default_code) >= 2)
|
|
|
|
errors = {}
|
|
if products_no_ref:
|
|
errors['product_internal_reference_missing'] = get_product_action(
|
|
_('Some products have no `Internal Reference`.'),
|
|
products_no_ref,
|
|
level='danger'
|
|
)
|
|
if products_dup_ref:
|
|
errors['product_internal_reference_duplicated'] = get_product_action(
|
|
_('Some products have duplicated `Internal Reference`, please make them unique.'),
|
|
products_dup_ref,
|
|
level='danger'
|
|
)
|
|
if 'intrastat_code_id' not in encountered_products: # intrastat module isn't installed, don't check for the instrastat code
|
|
return errors
|
|
|
|
products_without_intrastat_code = encountered_products.filtered(lambda p: p.type != 'service' and not p.intrastat_code_id)
|
|
if products_without_intrastat_code:
|
|
errors['product_intrastat_code_missing'] = get_product_action(
|
|
_("The Intrastat code isn't set on some products."),
|
|
products_without_intrastat_code
|
|
)
|
|
|
|
return errors
|
|
|
|
def _get_commodity_code(self, product):
|
|
if product.type == 'service':
|
|
return '00000000'
|
|
else:
|
|
return product.intrastat_code_id.code if 'intrastat_code_id' in product and product.intrastat_code_id else '0'
|
|
|
|
def _get_encountered_product_ids(self, values):
|
|
return {
|
|
line_vals['product_id']
|
|
for move_vals in values['move_vals_list']
|
|
for line_vals in move_vals['line_vals_list']
|
|
if line_vals['product_id']
|
|
}
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_product_values(self, options, values):
|
|
""" Fill product_vals_list """
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['products']:
|
|
values['product_vals_list'] = []
|
|
return
|
|
|
|
encountered_product_ids = sorted(self._get_encountered_product_ids(values))
|
|
encountered_products = self.env['product.product'].browse(encountered_product_ids)
|
|
product_vals_list = [
|
|
{
|
|
'id': product.id,
|
|
'name': product.name,
|
|
'default_code': product.default_code,
|
|
'uom_id': product.uom_id.id,
|
|
'product_category': product.product_tmpl_id.categ_id.name,
|
|
# The account_intrastat module is not a dependency, so this code should work regardless of whether it is installed.
|
|
'commodity_code': self._get_commodity_code(product),
|
|
}
|
|
for product in encountered_products
|
|
]
|
|
values['product_vals_list'] = product_vals_list
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_account_code_by_id(self, values):
|
|
""" Provide a mapping from account id to account code. We will need it when filling in
|
|
the general ledger and the source documents, because the Romanian authorities want
|
|
the account code not the account ID."""
|
|
|
|
account_code_by_id = {
|
|
account_vals['account'].id: account_vals['account'].code
|
|
for account_vals in values['account_vals_list']
|
|
}
|
|
values['account_code_by_id'] = account_code_by_id
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_invoice_values(self, values):
|
|
sale_invoice_vals = {
|
|
'total_debit': 0.0,
|
|
'total_credit': 0.0,
|
|
'number': 0,
|
|
'move_vals_list': [],
|
|
}
|
|
purchase_invoice_vals = {
|
|
'total_debit': 0.0,
|
|
'total_credit': 0.0,
|
|
'number': 0,
|
|
'move_vals_list': [],
|
|
}
|
|
|
|
self_invoices = self.env['account.move'].search([('l10n_ro_is_self_invoice', '=', True)])
|
|
|
|
for move_vals in values['move_vals_list']:
|
|
if move_vals['type'] not in {'out_invoice', 'out_refund', 'in_invoice', 'in_refund'}:
|
|
continue
|
|
|
|
# The invoice type is 380 for invoices, 381 for credit notes, 389 for self-invoices.
|
|
# (these codes were selected from European Norm SIST EN-16931)
|
|
if move_vals['id'] in self_invoices._ids:
|
|
l10n_ro_saft_invoice_type = '389'
|
|
l10n_ro_saft_self_billing_indicator = '389'
|
|
else:
|
|
l10n_ro_saft_invoice_type = '380' if move_vals['type'] in {'out_invoice', 'in_invoice'} else '381'
|
|
l10n_ro_saft_self_billing_indicator = '0'
|
|
|
|
move_vals.update({
|
|
'invoice_line_vals_list': [],
|
|
'l10n_ro_saft_invoice_type': l10n_ro_saft_invoice_type,
|
|
'l10n_ro_saft_self_billing_indicator': l10n_ro_saft_self_billing_indicator
|
|
})
|
|
|
|
dict_to_update = sale_invoice_vals if move_vals['type'] in {'out_invoice', 'out_refund'} else purchase_invoice_vals
|
|
for line_vals in move_vals['line_vals_list']:
|
|
if not line_vals['account_type'] in ('asset_receivable', 'liability_payable') and line_vals['display_type'] == 'product':
|
|
dict_to_update['total_debit'] += line_vals['debit']
|
|
dict_to_update['total_credit'] += line_vals['credit']
|
|
move_vals['invoice_line_vals_list'].append(line_vals)
|
|
|
|
dict_to_update['number'] += 1
|
|
dict_to_update['move_vals_list'].append(move_vals)
|
|
|
|
values.update({
|
|
'sale_invoice_vals': sale_invoice_vals,
|
|
'purchase_invoice_vals': purchase_invoice_vals,
|
|
})
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_payment_values(self, values):
|
|
payment_vals = {
|
|
'total_debit': 0.0,
|
|
'total_credit': 0.0,
|
|
'number': 0,
|
|
'move_vals_list': []
|
|
}
|
|
for move_vals in values['move_vals_list']:
|
|
if not move_vals['statement_line_id']:
|
|
continue
|
|
move_vals.update({
|
|
# Payment method '01' corresponds to cash, '03' corresponds to non-cash money transfers.
|
|
'payment_method': '01' if move_vals['journal_type'] == 'cash' else '03',
|
|
'description': move_vals['line_vals_list'][0]['name'],
|
|
'payment_line_vals_list': [],
|
|
})
|
|
|
|
for line_vals in move_vals['line_vals_list']:
|
|
if line_vals['account_type'] in ('asset_cash', 'liability_credit_card'):
|
|
move_vals['payment_line_vals_list'].append(line_vals)
|
|
payment_vals['total_debit'] += line_vals['debit']
|
|
payment_vals['total_credit'] += line_vals['credit']
|
|
|
|
payment_vals['number'] += 1
|
|
payment_vals['move_vals_list'].append(move_vals)
|
|
|
|
values['payment_vals'] = payment_vals
|
|
|
|
def _saft_get_account_type(self, account_type):
|
|
# EXTENDS account_saft/models/account_general_ledger.py
|
|
if self.env.company.account_fiscal_country_id.code != 'RO':
|
|
return super()._saft_get_account_type(account_type)
|
|
|
|
activ_types = ['asset_non_current', 'asset_fixed', 'asset_receivable', 'asset_cash', 'asset_current', 'asset_prepayments']
|
|
pasiv_types = ['equity', 'equity_unaffected', 'liability_payable', 'liability_credit_card', 'liability_current', 'liability_non_current']
|
|
|
|
if account_type in activ_types:
|
|
return 'Activ'
|
|
elif account_type in pasiv_types:
|
|
return 'Pasiv'
|
|
else: # Fallback on bifunctional if it's anything else.
|
|
return 'Bifunctional'
|
|
|
|
# ####################################################
|
|
# SAF-T ASSETS DECLARATION
|
|
####################################################
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_float_repr(self, amount):
|
|
# add 0. in case amount is -0.0 to make it positive
|
|
return float_repr(amount + 0., self.env.company.currency_id.decimal_places)
|
|
|
|
def _l10n_ro_saft_query_assets_values(self, options):
|
|
|
|
query = Query(self.env, alias='asset', table=SQL.identifier('account_asset'))
|
|
query.add_join('LEFT JOIN', alias='account', table='account_account', condition=SQL('asset.account_asset_id = account.id'))
|
|
account_code = self.env['account.account']._field_to_sql('account', 'code', query)
|
|
|
|
query = SQL(
|
|
'''
|
|
SELECT
|
|
asset.id AS asset_id,
|
|
asset.parent_id AS parent_id,
|
|
asset.name AS asset_name,
|
|
asset.original_value AS asset_original_value,
|
|
COALESCE(asset.salvage_value, 0) AS asset_salvage_value,
|
|
asset.disposal_date AS asset_disposal_date,
|
|
asset.acquisition_date AS asset_acquisition_date,
|
|
MIN(move.date) AS asset_date,
|
|
COALESCE(MIN(original_bill_aml.invoice_date), asset.acquisition_date) AS asset_purchase_date,
|
|
asset.state AS asset_state,
|
|
asset.method_period AS asset_method_period,
|
|
asset.method_number AS asset_method_number,
|
|
asset.method AS asset_method,
|
|
asset.method_progress_factor AS asset_method_progress_factor,
|
|
asset.currency_id AS asset_currency_id,
|
|
asset_category.code AS asset_category_code,
|
|
%(account_code)s AS account_code,
|
|
array_remove(array_agg(distinct partner.id), NULL) AS supplier_ids,
|
|
COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date < %(date_from)s), 0) + COALESCE(asset.already_depreciated_amount_import, 0) AS depreciated_before,
|
|
COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date BETWEEN %(date_from)s AND %(date_to)s), 0) AS depreciated_during,
|
|
COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date BETWEEN %(date_from)s AND %(date_to)s AND move.asset_number_days IS NULL), 0) AS asset_disposal_value
|
|
FROM %(from_clause)s
|
|
LEFT JOIN account_move move ON move.asset_id = asset.id AND move.state = 'posted'
|
|
LEFT JOIN asset_move_line_rel rel ON rel.asset_id = asset.id
|
|
LEFT JOIN account_move_line original_bill_aml ON original_bill_aml.id = rel.line_id
|
|
LEFT JOIN res_partner partner ON partner.id = original_bill_aml.partner_id
|
|
LEFT JOIN l10n_ro_saft_account_asset_category asset_category ON asset_category.id = asset.l10n_ro_saft_account_asset_category_id
|
|
WHERE asset.company_id = %(company_id)s
|
|
AND (asset.acquisition_date <= %(date_to)s OR move.date <= %(date_to)s)
|
|
AND (asset.disposal_date >= %(date_from)s OR asset.disposal_date IS NULL)
|
|
AND asset.state not in ('model', 'draft', 'cancelled')
|
|
AND asset.active = 't'
|
|
GROUP BY asset.id, account.id, asset_category.id, account_code
|
|
ORDER BY account_code, asset.acquisition_date, asset.id;
|
|
''',
|
|
account_code=account_code,
|
|
date_from=options['date']['date_from'],
|
|
date_to=options['date']['date_to'],
|
|
from_clause=query.from_clause,
|
|
company_id=self.env.company.id,
|
|
)
|
|
return query
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_fill_report_assets_values(self, options, values):
|
|
res = {
|
|
'assets': [],
|
|
'asset_partners': {},
|
|
}
|
|
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['assets']:
|
|
values.update(res)
|
|
return
|
|
|
|
if self.env.company.currency_id.name != 'RON':
|
|
raise UserError(_('The SAF-T Asset Declaration cannot be generated if the currency of the company is not RON'))
|
|
|
|
query = self._l10n_ro_saft_query_assets_values(options)
|
|
self.env.cr.execute(query)
|
|
asset_lines = self.env.cr.dictfetchall()
|
|
|
|
# Assign the gross increases sub assets to their main asset (parent)
|
|
parent_lines = []
|
|
children_lines = defaultdict(list)
|
|
for asset_line in asset_lines:
|
|
if asset_line['parent_id']:
|
|
children_lines[asset_line['parent_id']] += [asset_line]
|
|
else:
|
|
parent_lines += [asset_line]
|
|
|
|
for asset in parent_lines:
|
|
asset_children_lines = children_lines[asset['asset_id']]
|
|
asset_val = self.env['account.asset.report.handler']._get_parent_asset_values(options, asset, asset_children_lines)
|
|
|
|
depreciation_percentage = 0
|
|
if asset['asset_method'] == 'linear':
|
|
depreciation_percentage = round(1 / (asset['asset_method_number']), 2)
|
|
elif asset['asset_method'] in ('degressive', 'degressive_then_linear'):
|
|
depreciation_percentage = round(asset['method_progress_factor'] / asset['method_period'], 2)
|
|
|
|
asset_method = {
|
|
'linear': 'LINIARĂ',
|
|
'degressive': 'DEGRESIVĂ',
|
|
'degressive_then_linear': 'ACCELERATĂ'
|
|
}.get(asset['asset_method'], '')
|
|
|
|
# supplier id is the company if no supplier provided (self produced asset for example)
|
|
asset['supplier_ids'] = asset['supplier_ids'] or [self.env.company.partner_id.id]
|
|
|
|
asset['valuations'] = []
|
|
valuation_data = {
|
|
'asset_valuation_type': 'contabil', # Accounting
|
|
'valuation_class': asset['asset_category_code'],
|
|
'acquisition_costs_begin': self._l10n_ro_saft_float_repr(asset_val['assets_date_from']),
|
|
'acquisition_costs_end': self._l10n_ro_saft_float_repr(asset_val['assets_date_to']),
|
|
'investment_support': self._l10n_ro_saft_float_repr(asset_val['assets_date_from'] - asset['asset_original_value'] + asset_val['assets_plus']),
|
|
'asset_method_period': asset['asset_method_period'],
|
|
'asset_method_number': asset['asset_method_number'],
|
|
'asset_addition': self._l10n_ro_saft_float_repr(asset_val['assets_plus']),
|
|
'transfers': self._l10n_ro_saft_float_repr(asset_val['assets_plus'] - asset_val['assets_minus']),
|
|
'asset_disposal': self._l10n_ro_saft_float_repr(-asset_val['asset_disposal_value']),
|
|
'book_value_begin': self._l10n_ro_saft_float_repr(asset_val['assets_date_from'] - asset_val['depre_date_from']),
|
|
'depreciation_method': asset_method,
|
|
'depreciation_percentage': self._l10n_ro_saft_float_repr(depreciation_percentage),
|
|
'depreciation_for_period': self._l10n_ro_saft_float_repr(-asset_val['depre_plus']),
|
|
'appreciation_for_period': self._l10n_ro_saft_float_repr(asset_val['depre_minus']),
|
|
'accumulated_depreciation': self._l10n_ro_saft_float_repr(-asset_val['depre_date_to']),
|
|
'book_value_end': self._l10n_ro_saft_float_repr(asset_val['assets_date_to'] - asset_val['depre_date_to']),
|
|
'extraordinary_depreciation': [{
|
|
'method': 'NULL',
|
|
'amount_for_period': '0.00',
|
|
}],
|
|
}
|
|
# the valuations tag can contain multiple valuation according to the documentation. However, when
|
|
# reading more concisely the FAQ and reading more about this section, it seems that only one valuation
|
|
# per asset is possible for Romania. The Romanian SAF-T template is therefore pre-adapted to be able to
|
|
# handle a list of valuation elements, even though the list contain only a single element so far
|
|
asset['valuations'].append(valuation_data)
|
|
|
|
res['assets'].append(asset)
|
|
res['asset_partners'][asset['asset_id']] = asset['supplier_ids']
|
|
|
|
values.update(res)
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_check_asset_values(self, values):
|
|
""" Check whether assets have an asset_category or not"""
|
|
errors = {}
|
|
assets_without_category_ids = self.env['account.asset'].browse(
|
|
[asset['asset_id'] for asset in values['assets'] if not asset['asset_category_code']]
|
|
)
|
|
if assets_without_category_ids:
|
|
errors['missing_account_asset_category'] = {
|
|
'message': _('Missing category on assets'),
|
|
'action_text': _('Set asset categories'),
|
|
'action': assets_without_category_ids._get_records_action(
|
|
name=_('Products with no commodity code'),
|
|
views=[
|
|
(self.env.ref('l10n_ro_saft.account_asset_missing_l10n_ro_saft_account_asset_category').id, 'list'),
|
|
(False, 'form')
|
|
],
|
|
context={**self.env.context, 'create': False, 'delete': False, 'expand': True},
|
|
),
|
|
}
|
|
return errors
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_query_asset_transactions_values(self, options):
|
|
query = SQL(
|
|
'''
|
|
SELECT *
|
|
FROM (
|
|
-- Purchase and positive revaluation transactions
|
|
-- are retrieved from the asset_move_line_rel table
|
|
SELECT
|
|
move.id AS move_id,
|
|
move.name AS move_name,
|
|
move.date AS move_date,
|
|
move.asset_move_type AS asset_move_type,
|
|
move.depreciation_value AS depreciation_value,
|
|
aml.balance AS purchase_increase_amount,
|
|
COALESCE(asset.parent_id, asset.id) AS asset_id,
|
|
asset.original_value AS asset_original_value,
|
|
asset.net_gain_on_sale AS asset_net_gain_on_sale
|
|
FROM account_move move
|
|
LEFT JOIN account_move_line aml ON move.id = aml.move_id
|
|
INNER JOIN asset_move_line_rel rel ON aml.id = rel.line_id
|
|
INNER JOIN account_asset asset ON rel.asset_id = asset.id
|
|
WHERE move.state = 'posted'
|
|
AND move.date BETWEEN %(date_from)s AND %(date_to)s
|
|
AND move.company_id = %(company_id)s
|
|
AND asset.state not in ('model', 'draft', 'cancelled')
|
|
AND asset.active = 't'
|
|
UNION ALL
|
|
-- Depreciations, negative revaluation, disposal, and sale transactions
|
|
-- are retrieved from the asset_id field on account_move
|
|
SELECT
|
|
move.id AS move_id,
|
|
move.name AS move_name,
|
|
move.date AS move_date,
|
|
move.asset_move_type AS asset_move_type,
|
|
move.depreciation_value AS depreciation_value,
|
|
0 AS purchase_increase_amount,
|
|
COALESCE(asset.parent_id, asset.id) AS asset_id,
|
|
asset.original_value AS asset_original_value,
|
|
asset.net_gain_on_sale AS asset_net_gain_on_sale
|
|
FROM account_move move
|
|
INNER JOIN account_asset asset ON move.asset_id = asset.id
|
|
WHERE move.state = 'posted'
|
|
AND move.date BETWEEN %(date_from)s AND %(date_to)s
|
|
AND move.company_id = %(company_id)s
|
|
AND asset.state not in ('model', 'draft', 'cancelled')
|
|
AND asset.active = 't'
|
|
) asset_transactions
|
|
ORDER BY asset_id, move_date, asset_move_type, move_name
|
|
''',
|
|
date_from=options['date']['date_from'],
|
|
date_to=options['date']['date_to'],
|
|
company_id=self.env.company.id,
|
|
)
|
|
return query
|
|
|
|
def _l10n_ro_saft_fill_asset_transactions_values(self, options, values):
|
|
|
|
res = {
|
|
'asset_transactions': [],
|
|
}
|
|
|
|
if not options['l10n_ro_saft_required_sections']['source_documents']['asset_transactions']:
|
|
values.update(res)
|
|
return
|
|
|
|
asset_transaction_type = {
|
|
'purchase': 10,
|
|
'sale': 20,
|
|
'depreciation': 30,
|
|
'disposal': 50,
|
|
'negative_revaluation': 60,
|
|
'positive_revaluation': 70,
|
|
}
|
|
|
|
query = self._l10n_ro_saft_query_asset_transactions_values(options)
|
|
self.env.cr.execute(query)
|
|
for transaction in self.env.cr.dictfetchall():
|
|
transaction['asset_transaction_id'] = transaction['move_id']
|
|
transaction['asset_transaction_type'] = asset_transaction_type.get(transaction['asset_move_type'], 130)
|
|
transaction['description'] = transaction['move_name']
|
|
transaction['asset_transaction_date'] = transaction['move_date']
|
|
transaction['depreciation_value'] *= (-1) # must be provided with the opposite sign, so negative value in case of regular depreciation
|
|
transaction['transaction_id'] = transaction['move_id']
|
|
transaction['supplier_ids'] = values['asset_partners'].get(transaction['asset_id'], [self.env.company.partner_id.id])
|
|
|
|
asset_transaction_valuation = {
|
|
'asset_valuation_type': 'contabil', # Accounting
|
|
'acquisition_and_production_costs_on_transaction': (
|
|
self._l10n_ro_saft_float_repr(transaction['asset_original_value'])
|
|
if transaction['asset_move_type'] in ('purchase', 'positive_revaluation')
|
|
else "0.00"
|
|
),
|
|
'book_value_transaction': self._l10n_ro_saft_float_repr(transaction['purchase_increase_amount'] or transaction['depreciation_value']),
|
|
'asset_transaction_amount': (
|
|
self._l10n_ro_saft_float_repr(-transaction['asset_net_gain_on_sale'])
|
|
if transaction['asset_move_type'] == 'sale'
|
|
else self._l10n_ro_saft_float_repr(transaction['purchase_increase_amount'] or transaction['depreciation_value'])
|
|
),
|
|
}
|
|
|
|
transaction['asset_transaction_valuation'] = [asset_transaction_valuation]
|
|
res['asset_transactions'].append(transaction)
|
|
|
|
res['asset_transactions_number'] = len(res['asset_transactions'])
|
|
|
|
values.update(res)
|
|
|
|
@api.model
|
|
def _l10n_ro_saft_clean_customer_suppliers_vals_list(self, options, values):
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['customers']:
|
|
values['customer_vals_list'] = []
|
|
|
|
if not options['l10n_ro_saft_required_sections']['master_files']['suppliers']:
|
|
values['supplier_vals_list'] = []
|
|
|
|
def _l10n_ro_saft_clean_move_vals_list(self, values):
|
|
for journal_vals in values['journal_vals_list']:
|
|
for move_vals in journal_vals['move_vals_list']:
|
|
move_vals['line_vals_list'] = [vals for vals in move_vals['line_vals_list'] if vals['balance']]
|
|
journal_vals['move_vals_list'] = [vals for vals in journal_vals['move_vals_list'] if vals['line_vals_list']]
|
|
|
|
for move_vals in values['move_vals_list']:
|
|
move_vals['line_vals_list'] = [vals for vals in move_vals['line_vals_list'] if vals['balance']]
|
|
values['move_vals_list'] = [vals for vals in values['move_vals_list'] if vals['line_vals_list']]
|
|
|
|
@api.model
|
|
def _saft_fill_report_tax_details_values(self, report, options, values):
|
|
# no need to compute this section if not required (for example in the asset saf-t report)
|
|
if options.get('l10n_ro_saft_required_sections') and not options['l10n_ro_saft_required_sections']['master_files']['tax_table']:
|
|
values['tax_vals_list'] = []
|
|
else:
|
|
super()._saft_fill_report_tax_details_values(report, options, values)
|
|
|
|
@api.model
|
|
def _saft_fill_report_general_ledger_entries(self, report, options, values):
|
|
if options.get('l10n_ro_saft_required_sections') and not options['l10n_ro_saft_required_sections']['general_ledger_entries']:
|
|
res = {
|
|
'total_debit_in_period': 0.0,
|
|
'total_credit_in_period': 0.0,
|
|
'journal_vals_list': [],
|
|
'move_vals_list': [],
|
|
'tax_detail_per_line_map': {},
|
|
}
|
|
values.update(res)
|
|
else:
|
|
super()._saft_fill_report_general_ledger_entries(report, options, values)
|