Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View 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',
}