Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
227
rsbu_fns_reporting/models/rsbu_ndfl_xml.py
Normal file
227
rsbu_fns_reporting/models/rsbu_ndfl_xml.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# -*- 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',
|
||||
}
|
||||
Reference in New Issue
Block a user