Files
public/rsbu_fns_reporting/models/rsbu_enp_xml.py

176 lines
6.8 KiB
Python
Raw 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.
# -*- 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',
}