Files
public/rsbu_fns_reporting/models/rsbu_ndfl_xml.py

228 lines
9.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 -*-
"""Генерация расчёта 6-НДФЛ в XML.
Формат ФНС NO_NDFL6.2 версии 5.05 (КНД 1151100). Кодировка windows-1251,
кириллические имена элементов. Структура:
Файл(ИдФайл, ВерсПрог, ВерсФорм)
└ Документ(КНД, ДатаДок, Период, ОтчетГод, КодНО, НомКорр, ПоМесту)
├ СвНП(ОКТМО, Тлф) → НПЮЛ(НаимОрг, ИННЮЛ, КПП)
├ Подписант(ПрПодп) [→ ФИО]
└ НДФЛ6.2
├ ОбязНА(КБК, СумНалУд, СумНалВоз)
│ └ СведСумНалУд(СумНал1Срок..СумНал6Срок)
└ РасчСумНал(Ставка, КБК, КолФЛ, СумНачислНач, НалБаза,
СумНалИсч, СумНалУдерж + помесячно, ...)
Налог — целые рубли (integer); доход/база/вычеты — decimal с 2 знаками.
"""
import base64
from datetime import date
from odoo import _, api, models
from odoo.exceptions import UserError
from .rsbu_ndfl_report import NDFL_PERIOD_CODE
KND_NDFL = '1151100'
FORMAT_VERSION = '5.05'
PLACE_AT_LOCATION = '214'
class RsbuNdflReport(models.Model):
_inherit = 'rsbu.ndfl.report'
@staticmethod
def _int(value):
"""Налоговый показатель — целые рубли (строкой)."""
return str(int(round(value or 0.0)))
@staticmethod
def _dec(value):
"""Доход/база/вычет — decimal с 2 знаками."""
return '%.2f' % (value or 0.0)
def _ndfl_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_NDFL6.2_%s_%s_%s%s_%s_%s' % (
code_no, code_no, inn, kpp, today, guid)
def _ndfl_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.oktmo or company.rsbu_oktmo):
errors.append(_('ОКТМО'))
if not self.line_ids:
errors.append(_('ни одной строки расчёта по ставке'))
if errors:
raise UserError(_(
'Не заполнены обязательные реквизиты 6-НДФЛ: %s.')
% ', '.join(errors[:10]))
def _ndfl_build_xml(self, filename=None):
self.ensure_one()
try:
from lxml import etree
except ImportError:
raise UserError(_('Для выгрузки XML требуется библиотека lxml.')) from None
self._ndfl_validate_requisites()
if filename is None:
filename = self._ndfl_xml_filename()
company = self.company_id
partner = company.partner_id
oktmo = self.oktmo or company.rsbu_oktmo or ''
kbk = self.kbk or company.rsbu_kbk_ndfl or ''
E = etree.Element
root = E('Файл', {
'ИдФайл': filename, 'ВерсПрог': 'RuOdoo',
'ВерсФорм': FORMAT_VERSION,
})
doc = E('Документ', {
'КНД': KND_NDFL,
'ДатаДок': date.today().strftime('%d.%m.%Y'),
'Период': NDFL_PERIOD_CODE[int(self.quarter)],
'ОтчетГод': str(self.year),
'КодНО': company.rsbu_tax_office_code or '',
'НомКорр': '0',
'ПоМесту': PLACE_AT_LOCATION,
})
root.append(doc)
svnp = E('СвНП', {'ОКТМО': oktmo})
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)
ndfl = E('НДФЛ6.2')
doc.append(ndfl)
# Раздел 1: ОбязНА + СведСумНалУд (6 сроков).
obyaz = E('ОбязНА', {
'КБК': kbk,
'СумНалУд': self._int(self.withheld_total),
'СумНалВоз': self._int(self.returned_total),
})
etree.SubElement(obyaz, 'СведСумНалУд', {
'СумНал1Срок': self._int(self.term1),
'СумНал2Срок': self._int(self.term2),
'СумНал3Срок': self._int(self.term3),
'СумНал4Срок': self._int(self.term4),
'СумНал5Срок': self._int(self.term5),
'СумНал6Срок': self._int(self.term6),
})
ndfl.append(obyaz)
# Раздел 2: РасчСумНал по каждой ставке.
for line in self.line_ids:
etree.SubElement(ndfl, 'РасчСумНал', {
'Ставка': str(line.rate),
'КБК': line.kbk or kbk,
'КолФЛ': str(line.persons_count),
'КолКвал': '0',
'СумНачислНач': self._dec(line.accrued),
'СумНачислКвал': self._dec(0.0),
'СумВыч': self._dec(line.deduction),
'НалБаза': self._dec(line.base),
'СумНалИсч': self._int(line.computed_tax),
'СумНалИсчКвал': self._int(0.0),
'СумФикс': self._int(0.0),
'СумНалПриб': self._int(0.0),
'СумНалИнГос': self._int(0.0),
'СумНалУдерж': self._int(line.withheld_tax),
'СумНалУдерж1Мес': self._int(line.wh_m1_h1),
'СумНалУдерж23_1Мес': self._int(line.wh_m1_h2),
'СумНалУдерж2Мес': self._int(line.wh_m2_h1),
'СумНалУдерж23_2Мес': self._int(line.wh_m2_h2),
'СумНалУдерж3Мес': self._int(line.wh_m3_h1),
'СумНалУдерж23_3Мес': self._int(line.wh_m3_h2),
'СумНалНеУдерж': self._int(0.0),
'СумНалИзлУдерж': self._int(0.0),
'СумНалВозвр': self._int(0.0),
'СумНалВозвр1Мес': self._int(0.0),
'СумНалВозвр23_1Мес': self._int(0.0),
'СумНалВозвр2Мес': self._int(0.0),
'СумНалВозвр23_2Мес': self._int(0.0),
'СумНалВозвр3Мес': self._int(0.0),
'СумНалВозвр23_3Мес': self._int(0.0),
})
return etree.tostring(
root, xml_declaration=True, encoding='windows-1251',
pretty_print=True)
@api.model
def _ndfl_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_NDFL6.2_1_231_00_05_05_01.xsd')
def _ndfl_validate_xml(self, xml_bytes):
from lxml import etree
schema = etree.XMLSchema(etree.parse(self._ndfl_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._ndfl_xml_filename()
xml_bytes = self._ndfl_build_xml(filename=filename)
ok, errors = self._ndfl_validate_xml(xml_bytes)
if not ok:
raise UserError(_(
'Сформированный 6-НДФЛ не прошёл проверку по схеме ФНС:\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',
}