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