749 lines
39 KiB
Python
749 lines
39 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
import re
|
||
from odoo import api, models, _
|
||
from collections import defaultdict
|
||
|
||
|
||
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 == 'BG':
|
||
options.setdefault('buttons', []).append({
|
||
'name': _('SAF-T Monthly'),
|
||
'sequence': 50,
|
||
'action': 'export_file',
|
||
'action_param': 'l10n_bg_export_saft_to_xml',
|
||
'file_export_type': _('XML'),
|
||
})
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_fill_saft_account_by_code(self, values):
|
||
saft_account_by_code = {
|
||
account_vals['account'].code: account_vals['account'].l10n_bg_saft_account_code
|
||
for account_vals in values['account_vals_list']
|
||
}
|
||
values['saft_account_by_code'] = saft_account_by_code
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_check_header_values(self, values):
|
||
""" Check whether the company configuration is correct for filling in the Header. """
|
||
|
||
if not values['company'].l10n_bg_saft_tax_accounting_basis:
|
||
values['errors']['settings_accounting_basis_missing'] = {
|
||
'message': _('Tax Accounting Basis is not set.'),
|
||
'action_text': _('View Company'),
|
||
'action': values['company']._get_records_action(name=self.env._('Company missing Tax Accounting Basis')),
|
||
'level': 'danger',
|
||
}
|
||
|
||
if not values['company'].l10n_bg_saft_tax_entity_type:
|
||
values['errors']['company_tax_entity_type_missing'] = {
|
||
'message': _('The Tax Entity Type is not set.'),
|
||
'action_text': _('View Company'),
|
||
'action': values['company']._get_records_action(name=self.env._('Company missing Tax Entity Type')),
|
||
'level': 'danger',
|
||
}
|
||
|
||
if not values['company'].l10n_bg_saft_ownership_structure:
|
||
values['errors']['settings_ownership_structure_missing'] = {
|
||
'message': _('The Ownership Structure is not set.'),
|
||
'action_text': _('View Company'),
|
||
'action': values['company']._get_records_action(name=self.env._('Company missing Ownership Structure')),
|
||
'level': 'danger',
|
||
}
|
||
|
||
# Ultimate Owners
|
||
elif values['company'].l10n_bg_saft_ownership_structure in ('3', '4'):
|
||
# If they are part of a group, they have to declare ultimate owners
|
||
if not values['company'].l10n_bg_saft_ultimate_owner_ids:
|
||
values['errors']['ultimate_owners_missing'] = {
|
||
'message': _('Missing Ultimate Owners.'),
|
||
'action_text': _('View Company'),
|
||
'action': values['company']._get_records_action(name=self.env._('Company without Ultimate Owner')),
|
||
'level': 'danger',
|
||
}
|
||
else:
|
||
ultimate_owners = values['company'].l10n_bg_saft_ultimate_owner_ids
|
||
ultimate_owners_not_company = self.env['res.partner']
|
||
ultimate_owners_missing_country = self.env['res.partner']
|
||
bg_ultimate_owners_missing_company_registry = self.env['res.partner']
|
||
bg_ultimate_owners_missing_cyrillic_name = self.env['res.partner']
|
||
|
||
for owner in ultimate_owners:
|
||
if not owner.is_company:
|
||
ultimate_owners_not_company += owner
|
||
if not owner.country_code:
|
||
ultimate_owners_missing_country += owner
|
||
elif owner.country_code == 'BG':
|
||
if not owner.company_registry:
|
||
bg_ultimate_owners_missing_company_registry += owner
|
||
if not self._check_cyrillic_name(owner.name):
|
||
bg_ultimate_owners_missing_cyrillic_name += owner
|
||
|
||
ultimate_owner_vals_list = {
|
||
"ultimate_owner_name_cyrillic_bg": [],
|
||
"ultimate_owner_uicbg": [],
|
||
"ultimate_owner_name_cyrillic_foreign": [],
|
||
"ultimate_owner_name_latin_foreign": [],
|
||
"country_foreign": [],
|
||
}
|
||
for owner in sorted(ultimate_owners
|
||
- ultimate_owners_not_company
|
||
- ultimate_owners_missing_country
|
||
- bg_ultimate_owners_missing_company_registry,
|
||
key=lambda owner: owner.name): # Sorted for deterministic tests
|
||
ultimate_owner_vals_list["ultimate_owner_name_cyrillic_bg"].append(owner.name if owner.country_code in 'BG' else '00')
|
||
ultimate_owner_vals_list["ultimate_owner_uicbg"].append(owner.company_registry if owner.country_code in 'BG' else '00')
|
||
ultimate_owner_vals_list["ultimate_owner_name_cyrillic_foreign"].append(owner.name if self._check_cyrillic_name(owner.name) and owner.country_code not in 'BG' else '00')
|
||
ultimate_owner_vals_list["ultimate_owner_name_latin_foreign"].append(owner.name if not self._check_cyrillic_name(owner.name) and owner.country_code not in 'BG' else '00')
|
||
ultimate_owner_vals_list["country_foreign"].append(owner.country_code)
|
||
|
||
values["ultimate_owner_vals_list"] = ultimate_owner_vals_list
|
||
|
||
if ultimate_owners_not_company:
|
||
values['errors']['ultimate_owners_not_company'] = {
|
||
'message': _('Ultimate Owners that are not companies.'),
|
||
'action_text': _('View Ultimate Owners'),
|
||
'action': ultimate_owners_not_company._get_records_action(name=self.env._('Ultimate Owners not companies')),
|
||
'level': 'danger',
|
||
}
|
||
if ultimate_owners_missing_country:
|
||
values['errors']['ultimate_owners_missing_country'] = {
|
||
'message': _('Ultimate Owners without a Country.'),
|
||
'action_text': _('View Ultimate Owners'),
|
||
'action': ultimate_owners_missing_country._get_records_action(name=self.env._('Ultimate Owners without a Country')),
|
||
'level': 'danger',
|
||
}
|
||
if bg_ultimate_owners_missing_company_registry:
|
||
values['errors']['bg_ultimate_owners_missing_company_registry'] = {
|
||
'message': _('Bulgarian Ultimate 0wners without a Company ID.'),
|
||
'action_text': _('View Ultimate Owners'),
|
||
'action': bg_ultimate_owners_missing_company_registry._get_records_action(name=self.env._('Ultimate Owners without a Company ID')),
|
||
'level': 'danger',
|
||
}
|
||
if bg_ultimate_owners_missing_cyrillic_name:
|
||
values['errors']['bg_ultimate_owners_missing_cyrillic_name'] = {
|
||
'message': _('Bulgarian Ultimate Owners without a cyrillic name.'),
|
||
'action_text': _('View Ultimate Owners'),
|
||
'action': bg_ultimate_owners_missing_cyrillic_name._get_records_action(name=self.env._('Ultimate Owners without a cyrillic name')),
|
||
'level': 'warning',
|
||
}
|
||
|
||
# Beneficial Owners
|
||
if values['company'].l10n_bg_saft_beneficial_owner_ids:
|
||
|
||
beneficial_owners = values['company'].l10n_bg_saft_beneficial_owner_ids
|
||
beneficial_owners_not_person = self.env['res.partner']
|
||
beneficial_owners_missing_country = self.env['res.partner']
|
||
bg_beneficial_owners_missing_company_registry = self.env['res.partner']
|
||
bg_beneficial_owners_missing_cyrillic_name = self.env['res.partner']
|
||
foreign_beneficial_owners_missing_latin_name = self.env['res.partner']
|
||
|
||
for owner in beneficial_owners:
|
||
if owner.is_company:
|
||
beneficial_owners_not_person += owner
|
||
if not owner.country_code:
|
||
beneficial_owners_missing_country += owner
|
||
elif owner.country_code == 'BG':
|
||
if not owner.company_registry:
|
||
bg_beneficial_owners_missing_company_registry += owner
|
||
if not self._check_cyrillic_name(owner.name):
|
||
bg_beneficial_owners_missing_cyrillic_name += owner
|
||
elif owner.country_code != 'BG' and self._check_cyrillic_name(owner.name):
|
||
foreign_beneficial_owners_missing_latin_name += owner
|
||
|
||
beneficial_owner_vals_list = {
|
||
"beneficial_owner_name_cyrillic_bg": [],
|
||
"beneficial_owner_egn": [],
|
||
"beneficial_owner_name_latin_foreign": [],
|
||
"beneficial_country_foreign": [], # Citizenship of the beneficial owner - Country code
|
||
"beneficial_country_foreign_code": [], # Country of residence of the beneficial owner - Country code
|
||
}
|
||
|
||
for owner in sorted(beneficial_owners
|
||
- beneficial_owners_not_person
|
||
- beneficial_owners_missing_country
|
||
- bg_beneficial_owners_missing_company_registry,
|
||
key=lambda owner: owner.name): # Sorted for deterministic tests
|
||
|
||
beneficial_owner_vals_list["beneficial_owner_name_cyrillic_bg"].append(owner.name if owner.country_code in 'BG' else '00')
|
||
beneficial_owner_vals_list["beneficial_owner_egn"].append(owner.company_registry if owner.country_code in 'BG' else '00')
|
||
beneficial_owner_vals_list["beneficial_owner_name_latin_foreign"].append(owner.name if owner.country_code not in 'BG' else '00')
|
||
|
||
# We don't differenciate country_codes and simply use the country_code provided
|
||
# by the partner to avoid clutering the model with more fields used only in edge cases.
|
||
beneficial_owner_vals_list["beneficial_country_foreign"].append(owner.country_code) # Citizenship of the beneficial owner - Country code
|
||
beneficial_owner_vals_list["beneficial_country_foreign_code"].append(owner.country_code) # Country of residence of the beneficial owner - Country code
|
||
|
||
values["beneficial_owner_vals_list"] = beneficial_owner_vals_list
|
||
|
||
if beneficial_owners_not_person:
|
||
values['errors']['beneficial_owners_not_person'] = {
|
||
'message': _('Beneficial Owners that are not Persons.'),
|
||
'action_text': _('View Beneficial Owners'),
|
||
'action': beneficial_owners_not_person._get_records_action(name=self.env._('Beneficial Owners not persons')),
|
||
'level': 'warning',
|
||
}
|
||
if beneficial_owners_missing_country:
|
||
values['errors']['beneficial_owners_missing_country'] = {
|
||
'message': _('Beneficial Owners without a Country.'),
|
||
'action_text': _('View Beneficial Owners'),
|
||
'action': beneficial_owners_missing_country._get_records_action(name=self.env._('Beneficial Owners without a Country')),
|
||
'level': 'warning',
|
||
}
|
||
if bg_beneficial_owners_missing_company_registry:
|
||
values['errors']['bg_beneficial_owners_missing_company_registry'] = {
|
||
'message': _('Bulgarian Beneficial Owners without an EGN.'),
|
||
'action_text': _('View Beneficial Owners'),
|
||
'action': bg_beneficial_owners_missing_company_registry._get_records_action(name=self.env._('Beneficial Owners without EGN')),
|
||
'level': 'warning',
|
||
}
|
||
if bg_beneficial_owners_missing_cyrillic_name:
|
||
values['errors']['bg_beneficial_owners_missing_cyrillic_name'] = {
|
||
'message': _('Bulgarian Beneficial Owners without a cyrillic name.'),
|
||
'action_text': _('View Beneficial Owners'),
|
||
'action': bg_beneficial_owners_missing_cyrillic_name._get_records_action(name=self.env._('Bulgarian Beneficial Owners without a cyrillic name')),
|
||
'level': 'warning',
|
||
}
|
||
if foreign_beneficial_owners_missing_latin_name:
|
||
values['errors']['foreign_beneficial_owners_missing_latin_name'] = {
|
||
'message': _('Foreign Beneficial Owners without a latin name.'),
|
||
'action_text': _('View Beneficial Owners'),
|
||
'action': foreign_beneficial_owners_missing_latin_name._get_records_action(name=self.env._('Foreign Beneficial Owners without a Latin name')),
|
||
'level': 'warning',
|
||
}
|
||
|
||
if not values['company'].bank_ids:
|
||
values['errors']['company_bank_account_missing'] = {
|
||
'message': _('Company without a Bank Account.'),
|
||
'action_text': _('View Company/ies'),
|
||
'action': values['company'].partner_id._get_records_action(name=_("Companies missing Bank Account")),
|
||
'level': 'danger',
|
||
}
|
||
else:
|
||
bank_account_with_missing_bank = self.env['res.partner.bank']
|
||
bank_account_with_missing_registration_number = self.env['res.partner.bank']
|
||
bank_with_missing_country = self.env['res.bank']
|
||
for bank_account in values['company'].bank_ids:
|
||
if bank_account.acc_type != 'iban' and not bank_account.l10n_bg_saft_registration_number:
|
||
bank_account_with_missing_registration_number += bank_account
|
||
if bank_account.acc_type != 'iban' and not bank_account.bank_id:
|
||
bank_account_with_missing_bank += bank_account
|
||
elif bank_account.acc_type != 'iban' and not bank_account.bank_id.country_code:
|
||
bank_with_missing_country += bank_account.bank_id
|
||
if bank_account_with_missing_bank:
|
||
values['errors']['bank_account_with_missing_bank'] = {
|
||
'message': _('Bank Accounts without a Bank'),
|
||
'action_text': _('View Bank Accounts'),
|
||
'action': bank_account_with_missing_bank._get_records_action(name=_("Bank accounts missing bank information")),
|
||
'level': 'danger',
|
||
}
|
||
if bank_account_with_missing_registration_number:
|
||
values['errors']['bank_account_with_missing_registration_number'] = {
|
||
'message': _('Bank Accounts without a Bank Registration Number'),
|
||
'action_text': _('View Bank Accounts'),
|
||
'action': bank_account_with_missing_registration_number._get_records_action(name=_("Bank accounts missing a Bank Registration Number")),
|
||
'level': 'danger',
|
||
}
|
||
if bank_with_missing_country:
|
||
values['errors']['bank_with_missing_country'] = {
|
||
'message': _('Banks without Countries.'),
|
||
'action_text': _('View Banks'),
|
||
'action': bank_with_missing_country._get_records_action(name=_("Banks without Countries")),
|
||
'level': 'danger',
|
||
}
|
||
|
||
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')),
|
||
'level': 'danger',
|
||
}
|
||
|
||
partner = values['company'].partner_id
|
||
if not partner.company_registry:
|
||
values['errors']['company_registry_number_missing'] = get_company_action(_('Company without a CUI/EIK number under `Company Registry`.'))
|
||
|
||
if not partner.vat:
|
||
values['errors']['company_vat_number_missing'] = get_company_action(_('Company without a VAT number.'))
|
||
|
||
company_contact = self.env['res.partner']
|
||
if contacts := values['partner_detail_map'][self.env.company.partner_id.id]['contacts']:
|
||
company_contact |= contacts[0]
|
||
|
||
def get_company_contact_action(message):
|
||
return {
|
||
'message': message,
|
||
'action_text': self.env._("View Company Contact"),
|
||
'action': company_contact._get_records_action(name=self.env._('Invalid Company/ies')),
|
||
'level': 'warning',
|
||
}
|
||
|
||
if company_contact and not company_contact.phone:
|
||
values['errors']['missing_partner_phone_number'] = get_company_contact_action(_('Company Contact without a phone number.'))
|
||
|
||
if company_contact and not company_contact.function:
|
||
values['errors']['missing_partner_function'] = get_company_contact_action(_('Company Contact without a job title.'))
|
||
|
||
if company_contact and not company_contact.email:
|
||
values['errors']['missing_partner_email'] = get_company_contact_action(_('Company Contact without an email.'))
|
||
|
||
if company_contact and not re.search(r'\s', company_contact.name):
|
||
values['errors']['missing_partner_last_name'] = get_company_contact_action(_('Company Contact without a last name.'))
|
||
|
||
def _l10n_bg_saft_fill_customer_supplier_code(self, partner):
|
||
|
||
""" Unique identification number of the economic operator issued by the state administration
|
||
Unique customer/supplier code consisting of: type (two-digit code) followed by the unique customer/supplier code as follows:
|
||
10 + EIK (BG): the unique identification code for economic operators registered in Bulgaria.
|
||
11 + CC + VAT (EU non BG) : the unique VAT identification code of the relevant EU member state verifiable through the VAT Information Exchange System - Example: 11EL123456789 or 11HU12345678
|
||
12 + CC + VAT (non EU): when available, a unique VAT identification code or other code from an official register of the respective non EU country - Example: 12TK123005284
|
||
13 + EGN (BG): for natural persons/Bulgarian citizens or 13 followed by a unique personal code for natural persons residing in Bulgaria.
|
||
14 + CC + Code: a unique code of the economic operator, appended by the obliged person (usually generated by an information system).
|
||
fallback for missing unique code
|
||
15 + Code: a unique code of the economic operator added by the obliged person (usually generated by an information system).
|
||
fallback for missing country code and unique code
|
||
16 + SN: a service number provided by the NRA (starting with 307...).
|
||
|
||
With the following:
|
||
CC: 2 letters country code according to ISO 3166-1
|
||
UIC / CUI / EIK: unique 9-digit number (+4 for branch)
|
||
VAT Number: for companies registered for VAT with the NRA (BG + 9-digit EIK (e.g., BG123456789))
|
||
EGN: Uniform Civil Number: unique 10-digit number assigned to every
|
||
Bulgarian citizen at birth and to foreigners who receive permanent residency.
|
||
"""
|
||
if (partner.country_code and partner.country_code == 'BG') or (partner.vat and partner.vat.upper().startswith('BG')):
|
||
if not partner.is_company and partner.company_registry:
|
||
return '13' + partner.company_registry
|
||
if not partner.is_company and partner.vat and partner.vat.isnumeric():
|
||
return '13' + partner.vat
|
||
if not partner.is_company and partner.vat:
|
||
return '13' + partner.vat[2:]
|
||
if partner.company_registry:
|
||
return '10' + partner.company_registry
|
||
if partner.vat and partner.vat.isnumeric():
|
||
return '10' + partner.vat
|
||
if partner.vat:
|
||
return '10' + partner.vat[2:]
|
||
return '14' + 'BG' + str(partner.id)
|
||
if partner.country_id and partner.country_id.country_group_codes and \
|
||
('EU' in partner.country_id.country_group_codes or
|
||
'EU_PREFIX' in partner.country_id.country_group_codes):
|
||
if partner.vat and partner.vat.isnumeric():
|
||
return '11' + partner.country_code + partner.vat
|
||
if partner.vat:
|
||
return '11' + partner.vat
|
||
return '14' + partner.country_code + str(partner.id)
|
||
if partner.country_code:
|
||
if partner.vat and partner.vat.upper().startswith(partner.country_code):
|
||
return '12' + partner.vat
|
||
if partner.vat:
|
||
return '12' + partner.country_code + partner.vat
|
||
if partner.company_registry:
|
||
return '12' + partner.country_code + partner.company_registry
|
||
return '14' + partner.country_code + str(partner.id)
|
||
return '15' + str(partner.id)
|
||
|
||
def _l10n_bg_saft_update_partner_detail_map(self, values):
|
||
related_partners = values['company'].l10n_bg_saft_related_partner_ids.ids
|
||
faulty_partners = defaultdict(lambda: self.env['res.partner'])
|
||
|
||
for partner_info in values['partner_detail_map'].values():
|
||
|
||
partner = partner_info['partner']
|
||
|
||
if not partner.name:
|
||
faulty_partners['partner_missing_name'] |= partner
|
||
else:
|
||
partner_info['is_name_cyrillic'] = self._check_cyrillic_name(partner_info['partner'].name)
|
||
|
||
if not partner.city:
|
||
faulty_partners['partner_city_missing'] |= partner
|
||
|
||
partner_info['saft_operator_code'] = self._l10n_bg_saft_fill_customer_supplier_code(partner_info['partner'])
|
||
if (partner_info['saft_operator_code'].startswith('14')
|
||
or partner_info['saft_operator_code'].startswith('15')
|
||
or not partner.country_code
|
||
or (not partner.company_registry and not partner.vat)):
|
||
faulty_partners['partners_with_incomplete_identification'] |= partner
|
||
|
||
partner_info['is_related'] = partner_info['partner'].id in related_partners
|
||
|
||
descriptions = {
|
||
"partner_city_missing": (_("Partners without a city."), "warning"),
|
||
"partner_country_missing": (_("Partners without a country"), "danger"),
|
||
"partner_missing_name": (_("Partners without a name:"), "danger"),
|
||
"partners_with_incomplete_identification": (_("Partners without a country or identification number (Company ID or VAT)"), "warning"),
|
||
}
|
||
values['errors'] |= {
|
||
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()
|
||
}
|
||
|
||
def _l10n_bg_saft_check_saft_accounts(self, values):
|
||
|
||
encountered_account_ids = [account_vals['account']['id'] for account_vals in values['account_vals_list']]
|
||
accounts_missing_saft_code = self.env['account.account'].search([
|
||
('id', 'in', encountered_account_ids),
|
||
('l10n_bg_saft_account_code', '=', False),
|
||
'|', ('active', '=', True), ('active', '=', False), # prevent inactive accounts from being filtered out if used in a journal entry
|
||
])
|
||
|
||
if accounts_missing_saft_code:
|
||
values['errors']['accounts_missing_saft_code'] = {
|
||
'message': _('Accounts without a SAF-T Code'),
|
||
'action_text': _('View Accounts'),
|
||
'action': accounts_missing_saft_code._get_records_action(
|
||
name=_("Accounts without a SAF-T Code")),
|
||
'level': 'danger',
|
||
}
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_check_tax_values(self, values):
|
||
""" Check whether all taxes have a Bulgarian SAFT tax code on them. """
|
||
|
||
encountered_tax_ids = [tax_vals['id'] for tax_vals in values['tax_vals_list']]
|
||
taxes_missing_saft_code = self.env['account.tax'].search([
|
||
('id', 'in', encountered_tax_ids),
|
||
('l10n_bg_saft_tax_code', '=', False),
|
||
'|', ('active', '=', True), ('active', '=', False), # prevent inactive taxes from being filtered out if used in a document
|
||
])
|
||
if taxes_missing_saft_code:
|
||
values['errors']['taxes_missing_saft_code'] = {
|
||
'message': _('Taxes without a SAF-T Tax Code.'),
|
||
'action_text': _('View Taxes'),
|
||
'action': taxes_missing_saft_code._get_records_action(name=_("Taxes without a SAF-T Tax Code.")),
|
||
'level': 'danger',
|
||
}
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_fill_tax_values(self, values):
|
||
""" Fill in the Bulgarian tax type, tax type description (in Bulgarian, if available), and tax code. """
|
||
|
||
tax_type_description_per_tax_code = {
|
||
"100": "ДАНЪК ВЪРХУ ДОБАВЕНАТА СТОЙНОСТ ",
|
||
"200": "КОРПОРАТИВЕН ДАНЪК ",
|
||
"300": "ДАНЪК ВЪРХУ ПРИХОДИТЕ НА БЮДЖЕТНИТЕ ПРЕДПРИЯТИЯ ПО ЗКПО",
|
||
"400": "ДАНЪК ВЪРХУ ДОПЪЛНИТЕЛНИТЕ РАЗХОДИ НА НАРОДНИТЕ ПРЕДСТАВИТЕЛИ ПО ЗКПО",
|
||
"500": "ДАНЪК ВЪРХУ ХАЗАРТНАТА ДЕЙНОСТ ПО ЗКПО",
|
||
"600": "ДАНЪК ВЪРХУ РАЗХОДИТЕ",
|
||
"700": "ДАНЪК ПРИ ИЗТОЧНИКА",
|
||
"800": "ДАНЪК ВЪРХУ ДЕЙНОСТТА ОТ ОПЕРИРАНЕ НА КОРАБИ",
|
||
"900": "ВРЕМЕННА СОЛИДАРНА ВНОСКА ПО РЕГЛАМЕНТ 2022/1854",
|
||
"910": "ДАНЪК ВЪРХУ ЗАСТРАХОВАТЕЛНИТЕ ПРЕМИИ",
|
||
"915": "ДОПЪЛНИТЕЛЕН ДАНЪК И НАЦИОНАЛЕН ДОПЪЛНИТЕЛЕН ДАНЪК ПО ГЛАВА ПЕТА ''А'' ОТ ЗКПО",
|
||
"920": "ДАНЪК ВЪРХУ ОБЩАТА ГОДИШНА ДАНЪЧНА ОСНОВА ПО ЗДДФЛ",
|
||
"930": "ДАНЪК ВЪРХУ ГОДИШНАТА ДАНЪЧНА ОСНОВА ЗА ДОХОДИ ОТ СТОПАНСКА ДЕЙНОСТ КАТО ЕДНОЛИЧЕН ТЪРГОВЕЦ ПО ЗДДФЛ",
|
||
"940": "ДЪРЖАВНО ОБЩЕСТВЕНО ОСИГУРЯВАНЕ ",
|
||
"950": "ФОНД ДЗПО",
|
||
"960": "ЗДРАВНО ОСИГУРЯВАНЕ",
|
||
"970": "УЧИТЕЛСКИ ПЕНСИОНЕН ФОНД",
|
||
"980": "ФОНД ГВРС",
|
||
"990": "ТАКСИ ПО ЗАКОНА ЗА ХАЗАРТА",
|
||
"991": "Закон за акцизите и данъчните складове",
|
||
"111": "Общ код за лихви и санкции",
|
||
}
|
||
|
||
encountered_tax_ids = [tax_vals['id'] for tax_vals in values['tax_vals_list']]
|
||
encountered_taxes = self.env['account.tax'].browse(encountered_tax_ids)
|
||
tax_fields_by_id = {
|
||
tax.id: {
|
||
'l10n_bg_saft_tax_type': tax.l10n_bg_saft_tax_code[:3] if tax.l10n_bg_saft_tax_code else '',
|
||
'l10n_bg_saft_tax_type_description': tax_type_description_per_tax_code[tax.l10n_bg_saft_tax_code[:3]] if tax.l10n_bg_saft_tax_code else '',
|
||
'l10n_bg_saft_tax_code': tax.l10n_bg_saft_tax_code or '',
|
||
'country_code': tax.country_code,
|
||
}
|
||
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)
|
||
|
||
values['tax_type_vals_list'] = {}
|
||
|
||
for tax_vals in values['tax_vals_list']:
|
||
tax_fields = tax_fields_by_id[tax_vals['id']]
|
||
tax_vals.update(tax_fields)
|
||
values['tax_type_vals_list'].setdefault(tax_vals['l10n_bg_saft_tax_type'], {
|
||
'l10n_bg_saft_tax_type': tax_vals['l10n_bg_saft_tax_type'],
|
||
'l10n_bg_saft_tax_type_description': tax_vals['l10n_bg_saft_tax_type_description'],
|
||
'tax_vals_list': [],
|
||
})['tax_vals_list'].append(tax_vals)
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_fill_uom_values(self, values):
|
||
encountered_product_uom_ids = sorted({
|
||
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']
|
||
})
|
||
uom_vals_list = self.env['uom.uom'].browse(encountered_product_uom_ids)
|
||
values['uom_vals_list'] = uom_vals_list
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_check_uom_values(self, values):
|
||
uom_with_missing_saft_code = self.env['uom.uom']
|
||
uom_with_missing_saft_description = self.env['uom.uom']
|
||
for uom_id in values['uom_vals_list']:
|
||
if not uom_id.l10n_bg_saft_uom_code:
|
||
uom_with_missing_saft_code += uom_id
|
||
if not uom_id.l10n_bg_saft_uom_description:
|
||
uom_with_missing_saft_description += uom_id
|
||
|
||
if uom_with_missing_saft_code:
|
||
values['errors']['uom_with_missing_saft_code'] = {
|
||
'message': _('Units without a SAF-T UoM Code.'),
|
||
'action_text': _('View Units'),
|
||
'action': uom_with_missing_saft_code._get_records_action(name=_("Units without a SAF-T UoM Code")),
|
||
'level': 'danger',
|
||
}
|
||
if uom_with_missing_saft_description:
|
||
values['errors']['uom_with_missing_saft_description'] = {
|
||
'message': _('Units without a SAF-T UoM Description.'),
|
||
'action_text': _('View Units'),
|
||
'action': uom_with_missing_saft_description._get_records_action(name=_("Units without a SAF-T UoM Description")),
|
||
'level': 'danger',
|
||
}
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_fill_product_values(self, values):
|
||
""" Check whether each product has a ref, no products have duplicate refs,
|
||
and 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({
|
||
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']
|
||
})
|
||
encountered_products = self.env['product.product'].browse(encountered_product_ids)
|
||
products_without_intrastat_code = encountered_products.filtered(lambda p: p.type != 'service' and not p.intrastat_code_id)
|
||
|
||
if products_without_intrastat_code:
|
||
values['errors']['product_intrastat_code_missing'] = get_product_action(
|
||
_("Product without an Intrastat Commodity code."),
|
||
products_without_intrastat_code,
|
||
level='danger',
|
||
)
|
||
|
||
products_no_ref = encountered_products.filtered(lambda product: not product.default_code)
|
||
products_with_ref = (encountered_products - products_no_ref)
|
||
products_per_ref = products_with_ref.grouped("default_code")
|
||
products_dup_ref = self.env['product.product']
|
||
|
||
for products in products_per_ref.values():
|
||
if len(products) >= 2:
|
||
products_dup_ref |= products
|
||
|
||
if products_no_ref:
|
||
values['errors']['product_internal_reference_missing'] = get_product_action(
|
||
_('Products without an Internal Reference.'),
|
||
products_no_ref,
|
||
level='danger',
|
||
)
|
||
if products_dup_ref:
|
||
values['errors']['product_internal_reference_duplicated'] = get_product_action(
|
||
_('Products with duplicated Internal Reference.'),
|
||
products_dup_ref,
|
||
level='danger',
|
||
)
|
||
|
||
product_vals_list = [
|
||
{
|
||
'id': product.id,
|
||
'name': product.name,
|
||
'default_code': product.default_code,
|
||
'uom_base': product.uom_id.name,
|
||
'uom_standard': product.uom_id.l10n_bg_saft_uom_code,
|
||
'uom_conversion_factor': product.uom_id.l10n_bg_saft_uom_conversion_factor,
|
||
'goods_services_code': '02' if product.type == 'service' else '01',
|
||
'product_category': product.product_tmpl_id.categ_id.name,
|
||
'commodity_code': '00000000' if product.type == 'service' else
|
||
(product.intrastat_code_id.code if product.intrastat_code_id else '0'),
|
||
}
|
||
for product in encountered_products
|
||
]
|
||
values['product_vals_list'] = product_vals_list
|
||
|
||
@api.model
|
||
def _l10n_bg_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': [],
|
||
}
|
||
encountered_invoices_ids = [
|
||
move_vals['id']
|
||
for move_vals in values['move_vals_list']
|
||
if move_vals['type'] in {'out_invoice', 'out_refund', 'in_invoice', 'in_refund'}
|
||
]
|
||
encountered_invoices = self.env['account.move'].browse(encountered_invoices_ids)
|
||
|
||
invoice_detail_map = {
|
||
invoice.id: invoice
|
||
for invoice in encountered_invoices
|
||
}
|
||
|
||
for move_vals in values['move_vals_list']:
|
||
if move_vals['id'] not in invoice_detail_map:
|
||
continue
|
||
invoice_detail = invoice_detail_map[move_vals['id']]
|
||
|
||
move_vals.update({
|
||
'invoice_line_vals_list': [],
|
||
'l10n_bg_saft_invoice_type': invoice_detail['l10n_bg_document_type'],
|
||
'l10n_bg_saft_self_billing_indicator': 'Y' if invoice_detail['journal_id'].is_self_billing else 'N',
|
||
})
|
||
|
||
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 line_vals['account_type'] not 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_bg_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,
|
||
# '02' is for write off and miscellaneous transfers.
|
||
'payment_method': '01' if move_vals['journal_type'] == 'cash' else
|
||
('03' if move_vals['journal_type'] == 'bank' else '02'),
|
||
'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 _check_cyrillic_name(self, name):
|
||
return bool(re.search(r'[\u0400-\u04FF\u0500-\u052F]', name))
|
||
|
||
@api.model
|
||
def _l10n_bg_saft_prepare_report_values(self, report, options):
|
||
|
||
values = self._saft_prepare_report_values(report, options)
|
||
|
||
values['check_cyrillic_name'] = self._check_cyrillic_name
|
||
|
||
self._l10n_bg_saft_check_header_values(values)
|
||
|
||
self._l10n_bg_saft_update_partner_detail_map(values)
|
||
|
||
self._l10n_bg_saft_check_saft_accounts(values)
|
||
self._l10n_bg_saft_fill_saft_account_by_code(values)
|
||
|
||
self._l10n_bg_saft_check_tax_values(values)
|
||
self._l10n_bg_saft_fill_tax_values(values)
|
||
|
||
self._l10n_bg_saft_fill_uom_values(values)
|
||
self._l10n_bg_saft_check_uom_values(values)
|
||
|
||
self._l10n_bg_saft_fill_product_values(values)
|
||
|
||
self._l10n_bg_saft_fill_invoice_values(values)
|
||
|
||
self._l10n_bg_saft_fill_payment_values(values)
|
||
|
||
values.update({
|
||
'xmlns': 'mf:nra:dgti:dxxxx:declaration:v1',
|
||
'file_version': '1.02',
|
||
})
|
||
return values
|
||
|
||
@api.model
|
||
def l10n_bg_export_saft_to_xml(self, options):
|
||
report = self.env['account.report'].browse(options['report_id'])
|
||
values = self._l10n_bg_saft_prepare_report_values(report, options)
|
||
return report._generate_file_data_with_error_check(
|
||
options,
|
||
self.env['ir.qweb']._render,
|
||
{
|
||
'values': values,
|
||
'template': 'l10n_bg_saft.saft_template_inherit_l10n_bg_saft',
|
||
'file_type': 'xml',
|
||
},
|
||
values['errors'],
|
||
)
|
||
|
||
def _saft_get_account_type(self, account_type):
|
||
# OVERRIDE account_saft/models/account_general_ledger
|
||
if self.env.company.account_fiscal_country_id.code != 'BG':
|
||
return super()._saft_get_account_type(account_type)
|
||
|
||
# BG saf-t account types have to be identified as follows:
|
||
# Account type - "Active", "Passive", "Bifunctional"
|
||
account_type_dict = { # map between the account_types and the BG saf-t equivalent
|
||
'asset_non_current': 'Active',
|
||
'asset_fixed': 'Active',
|
||
'asset_receivable': 'Active',
|
||
'asset_cash': 'Active',
|
||
'asset_current': 'Active',
|
||
'asset_prepayments': 'Active',
|
||
'equity': 'Passive',
|
||
'equity_unaffected': 'Passive',
|
||
'liability_payable': 'Passive',
|
||
'liability_credit_card': 'Passive',
|
||
'liability_current': 'Passive',
|
||
'liability_non_current': 'Passive',
|
||
'income': 'Passive',
|
||
'income_other': 'Passive',
|
||
'expense': 'Active',
|
||
'expense_other': 'Active',
|
||
'expense_depreciation': 'Active',
|
||
'expense_direct_cost': 'Active',
|
||
'off_balance': 'Bifunctional',
|
||
}
|
||
return account_type_dict[account_type]
|