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