Files
public/rsbu_fns_reporting/models/rsbu_pers_info_xml.py

195 lines
7.4 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.
Формат ФНС NO_PERSSVFL версии 5.01 (КНД 1151162). Кодировка windows-1251,
кириллические имена элементов. Структура:
Файл(ИдФайл, ВерсПрог, ВерсФорм)
└ Документ(КНД, ДатаДок, НомКорр, Период, ОтчетГод, КодНО, ПоМесту)
├ СвНП(Тлф) → НПЮЛ(НаимОрг, ИННЮЛ, КПП)
├ Подписант(ПрПодп) [→ ФИО]
└ ПерсСвФЛ[1..*](ПрАннулир?, ИННФЛ?, СНИЛС, СумВыпл) → ФИО
Период — код календарного месяца (01..12). Сумма выплат — decimal с 2
знаками. СНИЛС нормализуется к формату NNN-NNN-NNN-NN.
"""
import base64
from datetime import date
from odoo import _, api, models
from odoo.exceptions import UserError
KND_PSV = '1151162'
FORMAT_VERSION = '5.01'
PLACE_AT_LOCATION = '214'
class RsbuPersInfo(models.Model):
_inherit = 'rsbu.pers.info'
@staticmethod
def _sum(value):
return '%.2f' % (value or 0.0)
@staticmethod
def _snils(raw):
"""Нормализовать СНИЛС к формату NNN-NNN-NNN-NN (только цифры)."""
digits = ''.join(ch for ch in (raw or '') if ch.isdigit())
if len(digits) != 11:
return raw or ''
return '%s-%s-%s-%s' % (digits[0:3], digits[3:6], digits[6:9],
digits[9:11])
def _psv_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 'NO_PERSSVFL_%s_%s_%s%s_%s_%s' % (
code_no, code_no, inn, kpp, today, guid)
def _psv_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.employee_id.rsbu_snils:
errors.append(_('СНИЛС сотрудника «%s»')
% line.employee_id.name)
if errors:
raise UserError(_(
'Не заполнены обязательные реквизиты ПСВ: %s.')
% ', '.join(errors[:10]))
def _psv_build_xml(self, filename=None):
self.ensure_one()
try:
from lxml import etree
except ImportError:
raise UserError(_('Для выгрузки XML требуется библиотека lxml.')) from None
self._psv_validate_requisites()
if filename is None:
filename = self._psv_xml_filename()
company = self.company_id
partner = company.partner_id
E = etree.Element
root = E('Файл', {
'ИдФайл': filename, 'ВерсПрог': 'RuOdoo',
'ВерсФорм': FORMAT_VERSION,
})
doc = E('Документ', {
'КНД': KND_PSV,
'ДатаДок': (self.date_doc or date.today()).strftime('%d.%m.%Y'),
'НомКорр': '0',
'Период': '%02d' % self.month,
'ОтчетГод': str(self.year),
'КодНО': company.rsbu_tax_office_code or '',
'ПоМесту': PLACE_AT_LOCATION,
})
root.append(doc)
svnp = E('СвНП')
svnp.append(E('НПЮЛ', {
'НаимОрг': company.name or '',
'ИННЮЛ': (partner.inn or '').strip(),
'КПП': (partner.kpp or '').strip(),
}))
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:
emp = line.employee_id
attrs = {
'СНИЛС': self._snils(emp.rsbu_snils),
'СумВыпл': self._sum(line.amount),
}
if emp.rsbu_inn:
attrs['ИННФЛ'] = emp.rsbu_inn.strip()
if line.cancelled:
attrs['ПрАннулир'] = '1'
pers = etree.SubElement(doc, 'ПерсСвФЛ', attrs)
parts = (emp.name or '').split()
fio = {}
if parts:
fio['Фамилия'] = parts[0]
if len(parts) > 1:
fio['Имя'] = parts[1]
if len(parts) > 2:
fio['Отчество'] = parts[2]
etree.SubElement(pers, 'ФИО', fio)
return etree.tostring(
root, xml_declaration=True, encoding='windows-1251',
pretty_print=True)
@api.model
def _psv_xsd_path(self):
import os
from odoo.modules import get_module_path
return os.path.join(
get_module_path('rsbu_fns_reporting'), 'data', 'schemas',
'NO_PERSSVFL_1_297_00_05_01_02.xsd')
def _psv_validate_xml(self, xml_bytes):
from lxml import etree
schema = etree.XMLSchema(etree.parse(self._psv_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):
self.ensure_one()
self.action_compute()
filename = self._psv_xml_filename()
xml_bytes = self._psv_build_xml(filename=filename)
ok, errors = self._psv_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',
}