Files

749 lines
39 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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]