# -*- 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', }