Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
"""Генерация уведомления об исчисленных суммах (ЕНП) в XML.
Формат ФНС UT_UVISCHSUMNAL версии 5.03 (КНД 1110355). Кодировка
windows-1251, кириллические имена элементов. Структура:
Файл(ИдФайл, ВерсПрог, ВерсФорм)
└ Документ(КНД, ДатаДок, КодНО)
├ СвНП → НПЮЛ(ИННЮЛ, КПП)
├ Подписант(ПрПодп) [→ ФИО]
└ УвИсчСумНалог[1..*](КППДекл, ОКТМО, КБК, СумНалогАванс,
Период, НомерМесКварт, Год)
Суммы — decimal с 2 знаками. Реквизиты — из настроек компании.
"""
import base64
from datetime import date
from odoo import _, api, models
from odoo.exceptions import UserError
KND_ENP = '1110355'
FORMAT_VERSION = '5.03'
class RsbuEnpNotification(models.Model):
_inherit = 'rsbu.enp.notification'
@staticmethod
def _sum(value):
return '%.2f' % (value or 0.0)
def _enp_xml_filename(self):
self.ensure_one()
import uuid
company = self.company_id
code_no = company.rsbu_tax_office_code or '0000'
inn = (company.partner_id.inn or '').strip()
kpp = (company.partner_id.kpp or '').strip()
today = date.today().strftime('%Y%m%d')
guid = str(uuid.uuid4())
return 'UT_UVISCHSUMNAL_%s_%s_%s%s_%s_%s' % (
code_no, code_no, inn, kpp, today, guid)
def _enp_validate_requisites(self):
self.ensure_one()
company = self.company_id
partner = company.partner_id
errors = []
if not partner.inn:
errors.append(_('ИНН организации'))
if not partner.kpp:
errors.append(_('КПП организации'))
if not company.rsbu_tax_office_code:
errors.append(_('код налогового органа'))
if not self.line_ids:
errors.append(_('ни одной строки с исчисленными суммами'))
for line in self.line_ids:
if not line.oktmo:
errors.append(_('ОКТМО в строке «%s»') % line.kind)
if not line.kbk:
errors.append(_('КБК в строке «%s»') % line.kind)
if errors:
raise UserError(_(
'Не заполнены обязательные реквизиты уведомления: %s.')
% ', '.join(errors[:10]))
def _enp_build_xml(self, filename=None):
self.ensure_one()
try:
from lxml import etree
except ImportError:
raise UserError(_('Для выгрузки XML требуется библиотека lxml.')) from None
self._enp_validate_requisites()
if filename is None:
filename = self._enp_xml_filename()
company = self.company_id
partner = company.partner_id
kpp = (partner.kpp or '').strip()
E = etree.Element
root = E('Файл', {
'ИдФайл': filename,
'ВерсПрог': 'RuOdoo',
'ВерсФорм': FORMAT_VERSION,
})
doc = E('Документ', {
'КНД': KND_ENP,
'ДатаДок': (self.date_doc or date.today()).strftime('%d.%m.%Y'),
'КодНО': company.rsbu_tax_office_code or '',
})
root.append(doc)
svnp = E('СвНП')
svnp.append(E('НПЮЛ', {
'ИННЮЛ': (partner.inn or '').strip(),
'КПП': kpp,
}))
doc.append(svnp)
pr_podp = '2' if company.rsbu_signatory_is_representative else '1'
signer = E('Подписант', {'ПрПодп': pr_podp})
signatory = company.rsbu_signatory_id
if signatory:
parts = (signatory.name or '').split()
fio = {}
if parts:
fio['Фамилия'] = parts[0]
if len(parts) > 1:
fio['Имя'] = parts[1]
if len(parts) > 2:
fio['Отчество'] = parts[2]
if fio:
signer.append(E('ФИО', fio))
doc.append(signer)
# Строки «Данные» — по каждому исчисленному налогу/взносу.
for line in self.line_ids:
attrs = {
'ОКТМО': line.oktmo or '',
'КБК': line.kbk or '',
'СумНалогАванс': self._sum(line.amount),
'Период': line.period_code or '',
'НомерМесКварт': line.month_in_quarter or '',
'Год': str(line.report_year or self.year),
}
# КППДекл обязателен при наличии НПЮЛ (schematron XSD).
attrs['КППДекл'] = line.kpp_decl or kpp
etree.SubElement(doc, 'УвИсчСумНалог', attrs)
return etree.tostring(
root, xml_declaration=True, encoding='windows-1251',
pretty_print=True)
@api.model
def _enp_xsd_path(self):
import os
from odoo.modules import get_module_path
return os.path.join(
get_module_path('rsbu_fns_reporting'), 'data', 'schemas',
'UT_UVISCHSUMNAL_1_263_00_05_03_01.xsd')
def _enp_validate_xml(self, xml_bytes):
from lxml import etree
schema = etree.XMLSchema(etree.parse(self._enp_xsd_path()))
doc = etree.fromstring(xml_bytes)
ok = schema.validate(doc)
errors = [e.message for e in schema.error_log] if not ok else []
return ok, errors
def action_export_xml(self):
"""Пересчитать, собрать XML, провалидировать и вернуть вложением."""
self.ensure_one()
self.action_compute()
filename = self._enp_xml_filename()
xml_bytes = self._enp_build_xml(filename=filename)
ok, errors = self._enp_validate_xml(xml_bytes)
if not ok:
raise UserError(_(
'Сформированное уведомление не прошло проверку по схеме '
'ФНС:\n%s') % '\n'.join(errors[:10]))
attachment = self.env['ir.attachment'].create({
'name': filename + '.xml',
'res_model': self._name,
'res_id': self.id,
'type': 'binary',
'datas': base64.b64encode(xml_bytes),
'mimetype': 'application/xml',
})
return {
'type': 'ir.actions.act_url',
'url': '/web/content/%s?download=true' % attachment.id,
'target': 'self',
}