Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
4
ruodoo_rsbu_act/models/__init__.py
Normal file
4
ruodoo_rsbu_act/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import ruodoo_rsbu_act
|
||||
from . import ruodoo_rsbu_act_xml
|
||||
from . import ruodoo_rsbu_act_docx
|
||||
245
ruodoo_rsbu_act/models/ruodoo_rsbu_act.py
Normal file
245
ruodoo_rsbu_act/models/ruodoo_rsbu_act.py
Normal file
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Акт сверки взаиморасчётов по РСБУ-слою.
|
||||
|
||||
Собирает сальдо и обороты по контрагенту за период из журнала РСБУ
|
||||
(rsbu.move.line) по счетам расчётов (активно-пассивные счета с субконто
|
||||
«Контрагент» — 60/62/76). Строки акта — хозяйственные операции по этому
|
||||
контрагенту с суммами по дебету/кредиту (с позиции нашей организации).
|
||||
|
||||
Выгрузка: печатная форма DOCX (docx_report) и электронный XML обмена
|
||||
(приказ ФНС 405, КНД 1110332) — в отдельных файлах-примесях.
|
||||
"""
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
# Счета бухучёта, участвующие в акте сверки с контрагентом (по коду):
|
||||
# 60 — расчёты с поставщиками и подрядчиками;
|
||||
# 62 — расчёты с покупателями и заказчиками;
|
||||
# 76 — расчёты с разными дебиторами и кредиторами;
|
||||
# 66/67 — кредиты и займы (реже); 58 — предоставленные займы.
|
||||
# Берутся сам счёт и его расчётные субсчета, КРОМЕ технических субсчетов
|
||||
# авансового НДС/спецсчетов с суффиксом «.АВ» (напр. 76.АВ «НДС по авансам»,
|
||||
# 62.АВ) — они к задолженности контрагента не относятся.
|
||||
RECONCILE_ACCOUNT_PREFIXES = ('60', '62', '76', '66', '67', '58')
|
||||
RECONCILE_EXCLUDE_SUFFIX = '.АВ'
|
||||
|
||||
|
||||
class RuodooRsbuAct(models.Model):
|
||||
_name = 'ruodoo.rsbu.act'
|
||||
_description = 'РСБУ — Акт сверки взаиморасчётов'
|
||||
_order = 'date_to desc, id desc'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(string='Номер акта', default='/', copy=False,
|
||||
index=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Организация', required=True, index=True,
|
||||
default=lambda self: self.env.company)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Контрагент', required=True, index=True)
|
||||
date_from = fields.Date(
|
||||
string='Период с', required=True,
|
||||
default=lambda self: fields.Date.today().replace(month=1, day=1))
|
||||
date_to = fields.Date(
|
||||
string='Период по', required=True,
|
||||
default=fields.Date.context_today)
|
||||
account_ids = fields.Many2many(
|
||||
'rsbu.account', string='Счета расчётов',
|
||||
domain="[('subconto_partner', '=', True), ('is_group', '=', False)]",
|
||||
help='Счета расчётов для сверки (активно-пассивные с субконто '
|
||||
'«Контрагент»: 60/62/76). Пусто — берутся все такие счета.')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', related='company_id.currency_id')
|
||||
state = fields.Selection(
|
||||
selection=[('draft', 'Черновик'), ('computed', 'Рассчитан')],
|
||||
default='draft', string='Состояние', copy=False)
|
||||
line_ids = fields.One2many(
|
||||
'ruodoo.rsbu.act.line', 'act_id', string='Операции')
|
||||
|
||||
# Сальдо и обороты (с позиции нашей организации по счетам расчётов).
|
||||
opening_debit = fields.Monetary(
|
||||
string='Сальдо нач. (дебет)', currency_field='currency_id',
|
||||
compute='_compute_totals', store=True)
|
||||
opening_credit = fields.Monetary(
|
||||
string='Сальдо нач. (кредит)', currency_field='currency_id',
|
||||
compute='_compute_totals', store=True)
|
||||
turnover_debit = fields.Monetary(
|
||||
string='Оборот по дебету', currency_field='currency_id',
|
||||
compute='_compute_totals', store=True)
|
||||
turnover_credit = fields.Monetary(
|
||||
string='Оборот по кредиту', currency_field='currency_id',
|
||||
compute='_compute_totals', store=True)
|
||||
closing_debit = fields.Monetary(
|
||||
string='Сальдо кон. (дебет)', currency_field='currency_id',
|
||||
compute='_compute_totals', store=True)
|
||||
closing_credit = fields.Monetary(
|
||||
string='Сальдо кон. (кредит)', currency_field='currency_id',
|
||||
compute='_compute_totals', store=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', '/') == '/':
|
||||
seq = self.env['ir.sequence'].with_company(
|
||||
vals.get('company_id') or self.env.company.id)
|
||||
vals['name'] = (seq.next_by_code('ruodoo.rsbu.act')
|
||||
or 'АКТ-%s' % fields.Date.today())
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.depends('line_ids', 'line_ids.debit', 'line_ids.credit',
|
||||
'opening_debit', 'opening_credit')
|
||||
def _compute_totals(self):
|
||||
for act in self:
|
||||
t_deb = sum(act.line_ids.mapped('debit'))
|
||||
t_cre = sum(act.line_ids.mapped('credit'))
|
||||
act.turnover_debit = t_deb
|
||||
act.turnover_credit = t_cre
|
||||
# Конечное сальдо = начальное + обороты (развёрнуто по знаку).
|
||||
raw = ((act.opening_debit - act.opening_credit)
|
||||
+ (t_deb - t_cre))
|
||||
act.closing_debit = raw if raw >= 0 else 0.0
|
||||
act.closing_credit = -raw if raw < 0 else 0.0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _settlement_accounts(self):
|
||||
"""Счета расчётов для сверки.
|
||||
|
||||
Если счета заданы вручную (account_ids) — используются они. Иначе
|
||||
отбираются счета расчётов с контрагентом по коду (60/62/76/66/67/58
|
||||
и их расчётные субсчета), исключая технические субсчета авансового
|
||||
НДС/спецсчета с суффиксом «.АВ» — они не отражают задолженность
|
||||
и засоряют акт.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.account_ids:
|
||||
return self.account_ids
|
||||
accounts = self.env['rsbu.account'].search([
|
||||
('subconto_partner', '=', True),
|
||||
('is_group', '=', False),
|
||||
])
|
||||
return accounts.filtered(lambda a: self._is_reconcile_account(a.code))
|
||||
|
||||
@staticmethod
|
||||
def _is_reconcile_account(code):
|
||||
"""Код счёта относится к сверяемым расчётам (не технический аванс)."""
|
||||
code = (code or '').strip()
|
||||
if RECONCILE_EXCLUDE_SUFFIX in code:
|
||||
return False
|
||||
root = code.split('.')[0]
|
||||
return root in RECONCILE_ACCOUNT_PREFIXES
|
||||
|
||||
def _move_line_domain(self, date_from, date_to):
|
||||
self.ensure_one()
|
||||
domain = [
|
||||
('company_id', '=', self.company_id.id),
|
||||
('partner_id', '=', self.partner_id.id),
|
||||
('account_id', 'in', self._settlement_accounts().ids),
|
||||
('state', '=', 'posted'),
|
||||
]
|
||||
if date_from:
|
||||
domain.append(('date', '>=', date_from))
|
||||
if date_to:
|
||||
domain.append(('date', '<=', date_to))
|
||||
return domain
|
||||
|
||||
def _opening_balance(self):
|
||||
"""Сальдо на начало периода (развёрнуто дебет/кредит)."""
|
||||
self.ensure_one()
|
||||
Line = self.env['rsbu.move.line']
|
||||
lines = Line.search(self._move_line_domain(None, False) + [
|
||||
('date', '<', self.date_from)])
|
||||
raw = sum(lines.mapped('debit')) - sum(lines.mapped('credit'))
|
||||
raw = round(raw, 2)
|
||||
return (raw, 0.0) if raw >= 0 else (0.0, -raw)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def action_compute(self):
|
||||
"""Собрать операции и сальдо по контрагенту за период."""
|
||||
Line = self.env['ruodoo.rsbu.act.line']
|
||||
MoveLine = self.env['rsbu.move.line']
|
||||
for act in self:
|
||||
act.line_ids.unlink()
|
||||
od, oc = act._opening_balance()
|
||||
act.opening_debit = od
|
||||
act.opening_credit = oc
|
||||
ml = MoveLine.search(
|
||||
act._move_line_domain(act.date_from, act.date_to),
|
||||
order='date, move_id, id')
|
||||
vals = []
|
||||
for line in ml:
|
||||
move = line.move_id
|
||||
vals.append({
|
||||
'act_id': act.id,
|
||||
'date': line.date,
|
||||
'name': line.name or move.ref or move.name,
|
||||
'move_id': move.id,
|
||||
'doc_name': move.ref or 'Операция',
|
||||
'doc_number': move.name,
|
||||
'doc_date': move.date,
|
||||
'contract_id': line.contract_id.id,
|
||||
'debit': line.debit,
|
||||
'credit': line.credit,
|
||||
})
|
||||
if vals:
|
||||
Line.create(vals)
|
||||
act.state = 'computed'
|
||||
# Если по одному акту ничего не собралось — явно предупредить
|
||||
# (чаще всего выбран не тот контрагент/период или нет проводок на
|
||||
# счетах расчётов), чтобы пустой акт не выглядел молчаливой ошибкой.
|
||||
if len(self) == 1 and not self.line_ids and not (
|
||||
self.opening_debit or self.opening_credit):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Акт сверки пуст'),
|
||||
'message': _(
|
||||
'За период %(df)s — %(dt)s по контрагенту «%(p)s» '
|
||||
'не найдено проводок на счетах расчётов (60/62/76). '
|
||||
'Проверьте выбранного контрагента, период и наличие '
|
||||
'проведённых операций.',
|
||||
df=self.date_from, dt=self.date_to,
|
||||
p=self.partner_id.name),
|
||||
'type': 'warning',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
return True
|
||||
|
||||
def _check_requisites(self):
|
||||
self.ensure_one()
|
||||
errors = []
|
||||
company_partner = self.company_id.partner_id
|
||||
if not (company_partner.inn or '').strip():
|
||||
errors.append('ИНН организации')
|
||||
if not (self.partner_id.inn or '').strip():
|
||||
errors.append('ИНН контрагента')
|
||||
if not self.line_ids and not (self.opening_debit or
|
||||
self.opening_credit):
|
||||
errors.append('нет операций и сальдо за период')
|
||||
if errors:
|
||||
raise UserError(
|
||||
'Не удалось сформировать акт сверки: %s.'
|
||||
% ', '.join(errors))
|
||||
|
||||
|
||||
class RuodooRsbuActLine(models.Model):
|
||||
_name = 'ruodoo.rsbu.act.line'
|
||||
_description = 'РСБУ — Операция акта сверки'
|
||||
_order = 'act_id, date, id'
|
||||
|
||||
act_id = fields.Many2one(
|
||||
'ruodoo.rsbu.act', string='Акт', required=True,
|
||||
ondelete='cascade', index=True)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', related='act_id.currency_id')
|
||||
date = fields.Date(string='Дата операции')
|
||||
name = fields.Char(string='Содержание операции')
|
||||
move_id = fields.Many2one('rsbu.move', string='Проводка РСБУ')
|
||||
doc_name = fields.Char(string='Документ')
|
||||
doc_number = fields.Char(string='Номер документа')
|
||||
doc_date = fields.Date(string='Дата документа')
|
||||
contract_id = fields.Many2one(
|
||||
'partner.contract.customer', string='Договор')
|
||||
debit = fields.Monetary(string='Дебет', currency_field='currency_id')
|
||||
credit = fields.Monetary(string='Кредит', currency_field='currency_id')
|
||||
91
ruodoo_rsbu_act/models/ruodoo_rsbu_act_docx.py
Normal file
91
ruodoo_rsbu_act/models/ruodoo_rsbu_act_docx.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Данные для печатной формы акта сверки (DOCX, движок docx_report).
|
||||
|
||||
Публичные методы act_docx_* вызываются из docxtpl-шаблона (через кастомные
|
||||
переменные), чтобы собрать стороны, строки операций и итоги.
|
||||
"""
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class RuodooRsbuActDocx(models.Model):
|
||||
_inherit = 'ruodoo.rsbu.act'
|
||||
|
||||
@staticmethod
|
||||
def _party(partner, name=None):
|
||||
return {
|
||||
'name': name or partner.name or '',
|
||||
'inn': (partner.inn or '').strip(),
|
||||
'kpp': (getattr(partner, 'kpp', '') or '').strip(),
|
||||
'ogrn': (getattr(partner, 'ogrn', '') or '').strip(),
|
||||
}
|
||||
|
||||
def act_docx_seller(self):
|
||||
self.ensure_one()
|
||||
return self._party(self.company_id.partner_id, self.company_id.name)
|
||||
|
||||
def act_docx_buyer(self):
|
||||
self.ensure_one()
|
||||
return self._party(self.partner_id)
|
||||
|
||||
def act_docx_lines(self):
|
||||
"""Операции акта для таблицы DOCX."""
|
||||
self.ensure_one()
|
||||
rows = []
|
||||
for idx, line in enumerate(self.line_ids, start=1):
|
||||
rows.append({
|
||||
'num': idx,
|
||||
'date': line.date.strftime('%d.%m.%Y') if line.date else '',
|
||||
'name': line.name or line.doc_name or '',
|
||||
'doc': ('%s %s' % (line.doc_name or '', line.doc_number or '')
|
||||
).strip(),
|
||||
'debit': line.debit,
|
||||
'credit': line.credit,
|
||||
})
|
||||
return rows
|
||||
|
||||
def act_docx_totals(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'opening_debit': self.opening_debit,
|
||||
'opening_credit': self.opening_credit,
|
||||
'turnover_debit': self.turnover_debit,
|
||||
'turnover_credit': self.turnover_credit,
|
||||
'closing_debit': self.closing_debit,
|
||||
'closing_credit': self.closing_credit,
|
||||
}
|
||||
|
||||
def act_docx_signer(self):
|
||||
self.ensure_one()
|
||||
signer = self.company_id.rsbu_signatory_id or getattr(
|
||||
self.company_id, 'chief_id', False)
|
||||
name = signer.partner_id.name if signer else ''
|
||||
function = (signer.partner_id.function if signer else '') or (
|
||||
getattr(self.company_id, 'rsbu_signatory_title', '')
|
||||
or 'Руководитель')
|
||||
return {'name': name, 'function': function}
|
||||
|
||||
def act_docx_summary(self):
|
||||
"""Итоговая формулировка задолженности на конец периода."""
|
||||
self.ensure_one()
|
||||
if self.closing_debit:
|
||||
return ('По данным %s задолженность в пользу %s составляет '
|
||||
'%.2f руб.' % (self.company_id.name,
|
||||
self.company_id.name, self.closing_debit))
|
||||
if self.closing_credit:
|
||||
return ('По данным %s задолженность в пользу %s составляет '
|
||||
'%.2f руб.' % (self.company_id.name,
|
||||
self.partner_id.name, self.closing_credit))
|
||||
return 'На конец периода задолженность отсутствует.'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def action_print_docx(self):
|
||||
"""Печать акта сверки в DOCX."""
|
||||
self.ensure_one()
|
||||
if self.state == 'draft':
|
||||
self.action_compute()
|
||||
template = self.env.ref(
|
||||
'ruodoo_rsbu_act.docx_template_act', raise_if_not_found=False)
|
||||
if not template:
|
||||
raise UserError(_('Не найден DOCX-шаблон акта сверки.'))
|
||||
return template.report_id.report_action(self)
|
||||
258
ruodoo_rsbu_act/models/ruodoo_rsbu_act_xml.py
Normal file
258
ruodoo_rsbu_act/models/ruodoo_rsbu_act_xml.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Электронный акт сверки в XML — формат обмена ФНС.
|
||||
|
||||
Приказ ФНС России от 13.05.2022 № ЕД-7-26/405@, КНД 1110332, версия 5.01,
|
||||
схема ON_AKTSVEROTP (файл отправителя). Кодировка windows-1251,
|
||||
кириллические имена элементов (латинских namespace нет — используется lxml,
|
||||
как для прочих форм ФНС). Структура:
|
||||
|
||||
Файл(ИдФайл, ВерсФорм=5.01, ВерсПрог)
|
||||
└ Документ(КНД, ДатаИнфОтпр, ВрИнфОтпр, ДатаНачПер, ДатаОкПер, НомерАкт)
|
||||
├ СвДокКрАкт(КодОКВ) → СвОтпр / СвПол (УчастникТип)
|
||||
├ ТаблАкт(сальдо нач/оборот/кон Деб/Кр)
|
||||
│ └ ДогСв(договор + сальдо) → ДокСв(документ) → СвОпер(операция)
|
||||
└ Подписант(ФИО, Должн)
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.modules import get_module_path
|
||||
|
||||
KND_ACT = '1110332'
|
||||
ACT_FORMAT_VERSION = '5.01'
|
||||
|
||||
|
||||
class RuodooRsbuActXml(models.Model):
|
||||
_inherit = 'ruodoo.rsbu.act'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def _d(value):
|
||||
"""Дата в формате ДД.ММ.ГГГГ (как требует XSD, ДатаТип)."""
|
||||
return value.strftime('%d.%m.%Y') if value else ''
|
||||
|
||||
@staticmethod
|
||||
def _amt(value):
|
||||
"""Сумма: decimal с 2 знаками (totalDigits 15, fractionDigits 2)."""
|
||||
return '%.2f' % round(value or 0.0, 2)
|
||||
|
||||
def _act_filename(self):
|
||||
self.ensure_one()
|
||||
company = self.company_id
|
||||
operator = (company.rsbu_edo_operator_code or '000').strip()
|
||||
sender = (company.rsbu_edo_participant_id or '').strip() \
|
||||
or str(uuid.uuid4())
|
||||
receiver = (self.partner_id.rsbu_edo_participant_id or '').strip() \
|
||||
or str(uuid.uuid4())
|
||||
today = date.today().strftime('%Y%m%d')
|
||||
guid = str(uuid.uuid4())
|
||||
return 'ON_AKTSVEROTP_%s%s_%s%s_%s_%s' % (
|
||||
operator, receiver, operator, sender, today, guid)
|
||||
|
||||
def _party_node(self, etree, tag, partner, company_name=None):
|
||||
"""Узел участника (СвОтпр/СвПол) по res.partner.
|
||||
|
||||
ЮЛ (ИНН 10) → СвЮЛУч (НаимОрг, ИННЮЛ, КПП, ОГРН — все обязательны);
|
||||
ИП (ИНН 12) → СвИП (ФИО, ИННФЛ, ОГРНИП); иначе → СвФЛ.
|
||||
"""
|
||||
attrs = {}
|
||||
if getattr(partner, 'okpo', False):
|
||||
attrs['ОКПО'] = partner.okpo
|
||||
node = etree.Element(tag, attrs)
|
||||
idsv = etree.SubElement(node, 'ИдСв')
|
||||
inn = (partner.inn or '').strip()
|
||||
name = company_name or partner.name or ''
|
||||
if len(inn) == 12:
|
||||
svip = etree.SubElement(idsv, 'СвИП', {
|
||||
'ИННФЛ': inn,
|
||||
'ОГРНИП': (getattr(partner, 'ogrnip', '') or
|
||||
getattr(partner, 'ogrn', '') or '').strip(),
|
||||
})
|
||||
self._fio_node(etree, svip, partner.name or name)
|
||||
elif len(inn) == 10:
|
||||
etree.SubElement(idsv, 'СвЮЛУч', {
|
||||
'НаимОрг': name,
|
||||
'ИННЮЛ': inn,
|
||||
'КПП': (getattr(partner, 'kpp', '') or '').strip(),
|
||||
'ОГРН': (getattr(partner, 'ogrn', '') or '').strip(),
|
||||
})
|
||||
else:
|
||||
svfl = etree.SubElement(idsv, 'СвФЛ', {'ИННФЛ': inn})
|
||||
self._fio_node(etree, svfl, partner.name or name)
|
||||
# Контакт — обязательный элемент (атрибуты опциональны).
|
||||
contact = {}
|
||||
if getattr(partner, 'phone', False):
|
||||
contact['Тлф'] = partner.phone
|
||||
if getattr(partner, 'email', False):
|
||||
contact['ЭлПочта'] = partner.email
|
||||
etree.SubElement(node, 'Контакт', contact)
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def _fio_node(etree, parent, full_name):
|
||||
parts = (full_name or '').split()
|
||||
attrs = {
|
||||
'Фамилия': parts[0] if parts else (full_name or '-'),
|
||||
'Имя': parts[1] if len(parts) > 1 else '-',
|
||||
}
|
||||
if len(parts) > 2:
|
||||
attrs['Отчество'] = ' '.join(parts[2:])
|
||||
return etree.SubElement(parent, 'ФИО', attrs)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _act_build_xml(self, filename=None):
|
||||
"""Собрать XML акта сверки (ON_AKTSVEROTP). Возвращает bytes."""
|
||||
self.ensure_one()
|
||||
try:
|
||||
from lxml import etree
|
||||
except ImportError:
|
||||
raise UserError(_('Для выгрузки акта требуется библиотека lxml.')) \
|
||||
from None
|
||||
self._check_requisites()
|
||||
if not self.line_ids:
|
||||
raise UserError(_(
|
||||
'Электронный акт сверки требует хотя бы одной операции за '
|
||||
'период. Нажмите «Рассчитать».'))
|
||||
if filename is None:
|
||||
filename = self._act_filename()
|
||||
|
||||
company = self.company_id
|
||||
seller = company.partner_id
|
||||
buyer = self.partner_id
|
||||
now = datetime.now()
|
||||
|
||||
E = etree.Element
|
||||
root = E('Файл', {
|
||||
'ИдФайл': filename,
|
||||
'ВерсФорм': ACT_FORMAT_VERSION,
|
||||
'ВерсПрог': 'RuOdoo',
|
||||
})
|
||||
doc = E('Документ', {
|
||||
'КНД': KND_ACT,
|
||||
'ДатаИнфОтпр': self._d(date.today()),
|
||||
'ВрИнфОтпр': now.strftime('%H:%M:%S'),
|
||||
'ДатаНачПер': self._d(self.date_from),
|
||||
'ДатаОкПер': self._d(self.date_to),
|
||||
'НомерАкт': self.name or '/',
|
||||
})
|
||||
root.append(doc)
|
||||
|
||||
# Стороны.
|
||||
svdok = E('СвДокКрАкт', {'КодОКВ': '643'})
|
||||
svdok.append(self._party_node(etree, 'СвОтпр', seller,
|
||||
company_name=company.name))
|
||||
svdok.append(self._party_node(etree, 'СвПол', buyer))
|
||||
doc.append(svdok)
|
||||
|
||||
# Таблица акта: один договорный блок (все операции периода),
|
||||
# сальдо/обороты на уровне ТаблАкт и ДогСв совпадают.
|
||||
totals = {
|
||||
'СальдоНачДеб': self._amt(self.opening_debit),
|
||||
'СальдоНачКр': self._amt(self.opening_credit),
|
||||
'ОборотДеб': self._amt(self.turnover_debit),
|
||||
'ОборотКр': self._amt(self.turnover_credit),
|
||||
'СальдоКонДеб': self._amt(self.closing_debit),
|
||||
'СальдоКонКр': self._amt(self.closing_credit),
|
||||
}
|
||||
tabl = E('ТаблАкт', totals)
|
||||
doc.append(tabl)
|
||||
|
||||
dogsv_attrs = dict(totals)
|
||||
# Договор — из первой операции с договором либо «Основной договор».
|
||||
contract = self.line_ids.mapped('contract_id')[:1]
|
||||
dogsv_attrs['ОписТипДог'] = 'Договор'
|
||||
if contract:
|
||||
dogsv_attrs['НомДог'] = contract.display_name[:50]
|
||||
dogsv = etree.SubElement(tabl, 'ДогСв', dogsv_attrs)
|
||||
|
||||
# Документы и операции. Группируем операции по документу (проводке).
|
||||
by_doc = {}
|
||||
order = []
|
||||
for line in self.line_ids:
|
||||
key = line.move_id.id or 0
|
||||
if key not in by_doc:
|
||||
by_doc[key] = []
|
||||
order.append(key)
|
||||
by_doc[key].append(line)
|
||||
for key in order:
|
||||
group = by_doc[key]
|
||||
first = group[0]
|
||||
doksv = etree.SubElement(dogsv, 'ДокСв', {
|
||||
'НаимДок': (first.doc_name or 'Операция')[:100],
|
||||
'НомДок': (first.doc_number or '-')[:50],
|
||||
'ДатаДок': self._d(first.doc_date or first.date),
|
||||
})
|
||||
for idx, line in enumerate(group, start=1):
|
||||
oper = {
|
||||
'НомСтр': str(idx),
|
||||
'ДатаОпер': self._d(line.date),
|
||||
'НаимОпер': (line.name or first.doc_name
|
||||
or 'Операция')[:500],
|
||||
}
|
||||
if line.debit:
|
||||
oper['СумДебет'] = self._amt(line.debit)
|
||||
if line.credit:
|
||||
oper['СумКредит'] = self._amt(line.credit)
|
||||
etree.SubElement(doksv, 'СвОпер', oper)
|
||||
|
||||
# Подписант.
|
||||
signer = company.rsbu_signatory_id or getattr(
|
||||
company, 'chief_id', False)
|
||||
podp = E('Подписант', {'СтатПодп': '1'})
|
||||
if signer and signer.partner_id.function:
|
||||
podp.set('Должн', signer.partner_id.function)
|
||||
elif getattr(company, 'rsbu_signatory_title', False):
|
||||
podp.set('Должн', company.rsbu_signatory_title)
|
||||
self._fio_node(etree, podp,
|
||||
signer.partner_id.name if signer else (seller.name or ''))
|
||||
doc.append(podp)
|
||||
|
||||
return etree.tostring(
|
||||
root, xml_declaration=True, encoding='windows-1251',
|
||||
pretty_print=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def _act_xsd_path(self):
|
||||
return os.path.join(
|
||||
get_module_path('ruodoo_rsbu_act'), 'data', 'schemas',
|
||||
'ON_AKTSVEROTP_1_972_01_05_01_01.xsd')
|
||||
|
||||
def _act_validate_xml(self, xml_bytes):
|
||||
"""Проверить XML по XSD. Возвращает (ok, [ошибки])."""
|
||||
from lxml import etree
|
||||
schema = etree.XMLSchema(etree.parse(self._act_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):
|
||||
"""Сформировать акт сверки в XML, провалидировать, вернуть вложением."""
|
||||
self.ensure_one()
|
||||
if self.state == 'draft':
|
||||
self.action_compute()
|
||||
filename = self._act_filename()
|
||||
xml_bytes = self._act_build_xml(filename=filename)
|
||||
ok, errors = self._act_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',
|
||||
}
|
||||
Reference in New Issue
Block a user