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,294 @@
# -*- coding: utf-8 -*-
"""Данные для печатной формы УПД (DOCX) из документа РСБУ-слоя.
Печатный УПД (docx_report) строится из документа «Счёт-фактура» РСБУ-слоя
(rsbu.invoice, отгрузочный), как и XML-выгрузка (Вариант B):
* счёт-фактурная часть (номер/дата, ставка, суммы/итоги) — из rsbu.invoice;
* товарные позиции — из связанного инвойса-источника (_source_invoice);
* стороны и подписант — из res.company / res.partner.
Логика пересчёта НДС по строке совпадает с rsbu_upd_xml (НДС = база ×
ставка РСБУ). Методы публичные, т.к. вызываются из docxtpl-шаблона как
``docs.upd_docx_*()`` (Jinja не обращается к «приватным» _методам).
"""
from odoo import models
# Переиспользуем справочники ставок из XML-генератора.
from .rsbu_upd_xml import VAT_RATE_LABEL, VAT_RATE_PCT
class RsbuRealizationUpdDocx(models.Model):
_inherit = 'rsbu.realization'
# ------------------------------------------------------------------
@staticmethod
def _upd_fio(user_or_partner):
"""ФИО и должность подписанта/ответственного лица."""
partner = getattr(user_or_partner, 'partner_id', user_or_partner)
name = (getattr(user_or_partner, 'name', '') or
getattr(partner, 'name', '') or '')
function = getattr(partner, 'function', '') or ''
return {'name': name, 'function': function}
def upd_vat_label(self):
"""Текстовая метка ставки НДС (напр. «5%», «без НДС»).
Ставка берётся из подчинённого СФ при наличии, иначе «без НДС».
"""
self.ensure_one()
rate = self.invoice_id.vat_rate if self.invoice_id else 'vat_exempt'
return VAT_RATE_LABEL.get(rate, 'без НДС')
def upd_docx_status(self):
"""Статус УПД: «1» — счёт-фактура + передаточный документ;
«2» — только передаточный документ (без НДС).
Структурный драйвер — наличие подчинённого отгрузочного СФ
(self.invoice_id), тот же, что в XML (Функция)."""
self.ensure_one()
return '1' if self.invoice_id else '2'
@staticmethod
def _bank_requisites(partner):
"""Банковские реквизиты стороны (Р/с, Банк, БИК, К/с) одной строкой."""
bank = False
if partner.bank_ids:
bank = partner.bank_ids[0]
elif partner.parent_id and partner.parent_id.bank_ids:
bank = partner.parent_id.bank_ids[0]
if not bank:
return ''
parts = []
if bank.acc_number:
parts.append('Р/с %s' % bank.acc_number)
name = getattr(bank, 'bank_name', '') or (
bank.bank_id.name if bank.bank_id else '')
if name:
parts.append(name)
bic = getattr(bank, 'bank_bic', '') or (
bank.bank_id.bic if bank.bank_id else '')
if bic:
parts.append('БИК %s' % bic)
corr = getattr(bank, 'bank_corr_acc', '') or ''
if corr:
parts.append('к/с %s' % corr)
return ', '.join(parts)
def upd_docx_lines(self):
"""Товарные позиции УПД (16 колонок счёта-фактуры, Пост. № 1137).
А(код), 1(№), 1а(наимен), 1б(код вида), 2(ОКЕИ), 2а(ед.изм.),
3(кол-во), 4(цена), 5(без НДС), 6(акциз), 7(ставка), 8(НДС),
9(с НДС), 10/10а(страна), 11(ДТ).
Суммы берутся ФАКТИЧЕСКИЕ из строки инвойса-источника: база =
price_subtotal, НДС = price_total − price_subtotal (корректно и для
ставки «в том числе», где налог выделяется из цены, а не начисляется
сверху), стоимость с НДС = price_total. Если у строки нет налогов,
но в rsbu.invoice задана ставка — НДС пересчитывается по ставке
(fallback для источников без налогов Odoo).
"""
self.ensure_one()
source = self._source_invoice()
sf_vat_rate = self.invoice_id.vat_rate if self.invoice_id \
else 'vat_exempt'
vat_pct = VAT_RATE_PCT.get(sf_vat_rate, 0.0)
rate_label = self.upd_vat_label()
lines = source.invoice_line_ids.filtered(
lambda l: l.display_type in (False, 'product')) if source \
else self.env['account.move.line']
rows = []
for idx, line in enumerate(lines, start=1):
subtotal = line.price_subtotal or 0.0
if line.tax_ids:
# Фактический НДС строки (учитывает «в т.ч.»/«сверху»).
line_vat = round((line.price_total or 0.0) - subtotal, 2)
total = round(line.price_total or 0.0, 2)
else:
# Нет налогов в источнике — пересчёт по ставке РСБУ.
line_vat = round(subtotal * vat_pct, 2)
total = round(subtotal + line_vat, 2)
qty = line.quantity or 0.0
uom = line.product_uom_id
product = line.product_id
rows.append({
'num': idx,
'code': product.default_code or '', # А
'name': line.name or (product.name or 'Товар'), # 1а
'type_code': '', # 1б (прослеж.)
'okei': getattr(uom, 'okei', '') or '', # 2
'uom': uom.name or '', # 2а
'qty': qty, # 3
'price': round(subtotal / qty, 2) if qty else 0.0, # 4
'subtotal': subtotal, # 5
'excise': 'Без акциза', # 6
'vat_label': rate_label if line_vat else 'без НДС', # 7
'vat': line_vat, # 8
'vat_cell': ('%.2f' % line_vat) if line_vat else 'без НДС',
'total': total, # 9
'country_code': '', # 10
'country': '', # 10а
'customs_decl': '', # 11
})
return rows
def upd_docx_totals(self):
"""Итоги УПД: без НДС, НДС, с НДС (по фактическим суммам строк)."""
self.ensure_one()
rows = self.upd_docx_lines()
wo_vat = round(sum(r['subtotal'] for r in rows), 2)
vat = round(sum(r['vat'] for r in rows), 2)
with_vat = round(sum(r['total'] for r in rows), 2)
return {'wo_vat': wo_vat, 'vat': vat, 'with_vat': with_vat}
@staticmethod
def _clean_address(partner):
"""Почтовый адрес без наименования организации (для строк 2а/6а).
contact_address начинается с названия партнёра — убираем первую
строку с именем и собираем адрес из полей.
"""
parts = []
if partner.zip:
parts.append(partner.zip)
if getattr(partner, 'state_id', False) and partner.state_id:
parts.append(partner.state_id.name)
if partner.city:
parts.append(partner.city)
if partner.street:
parts.append(partner.street)
if partner.street2:
parts.append(partner.street2)
if parts:
return ', '.join(parts)
# Фолбэк: contact_address без первой строки (имени).
addr = (partner.contact_address or '').split('\n')
tail = [a.strip() for a in addr[1:] if a.strip()]
return ', '.join(tail)
def _party_dict(self, partner, name=None):
return {
'name': name or partner.name or '',
'inn': (partner.inn or '').strip(),
'kpp': (getattr(partner, 'kpp', '') or '').strip(),
'okpo': getattr(partner, 'okpo', '') or '',
'address': self._clean_address(partner),
'bank': self._bank_requisites(partner),
}
def upd_docx_seller(self):
"""Реквизиты продавца (из организации) с банковскими реквизитами."""
self.ensure_one()
return self._party_dict(self.company_id.partner_id,
self.company_id.name)
def upd_docx_buyer(self):
"""Реквизиты покупателя (из контрагента) с банковскими реквизитами."""
self.ensure_one()
return self._party_dict(self.partner_id)
def upd_docx_shipper(self):
"""Грузоотправитель (3): задан явно или «он же» (= продавец)."""
self.ensure_one()
if self.upd_shipper_id and self.upd_shipper_id != \
self.company_id.partner_id:
p = self.upd_shipper_id
return '%s, %s' % (p.name or '', self._clean_address(p))
return 'он же'
def upd_docx_consignee(self):
"""Грузополучатель (4): задан явно или покупатель."""
self.ensure_one()
p = self.upd_consignee_id or self.partner_id
return '%s, %s' % (p.name or '', self._clean_address(p))
def upd_docx_basis(self):
"""Основание передачи (10): договор/документ-основание."""
self.ensure_one()
return self.upd_basis or (self.name or '')
def upd_docx_shipment_doc(self):
"""Документ об отгрузке (5а)."""
self.ensure_one()
return 'Универсальный передаточный документ № %s от %s' % (
self.name or '', self.date.strftime('%d.%m.%Y') if self.date
else '')
def upd_docx_receiver(self):
"""Лицо со стороны покупателя, принявшее груз (17/20)."""
self.ensure_one()
if not self.upd_received_by_id:
return {'name': '', 'function': ''}
data = self._upd_fio(self.upd_received_by_id)
if not data['function']:
data['function'] = 'Директор'
return data
def upd_docx_seller_subject(self):
"""Наименование экономического субъекта — продавца (16)."""
self.ensure_one()
p = self.company_id.partner_id
return '%s, ИНН/КПП %s/%s' % (
self.company_id.name or '', (p.inn or '').strip(),
(getattr(p, 'kpp', '') or '').strip())
def upd_docx_buyer_subject(self):
"""Наименование экономического субъекта — покупателя (21)."""
self.ensure_one()
p = self.partner_id
kpp = (getattr(p, 'kpp', '') or '').strip()
return '%s%s' % (p.name or '',
', ИНН/КПП %s/%s' % ((p.inn or '').strip(), kpp)
if p.inn else '')
def upd_docx_signer(self):
"""Подписант (руководитель организации-продавца)."""
self.ensure_one()
signer = self.company_id.rsbu_signatory_id or getattr(
self.company_id, 'chief_id', False)
if not signer:
return {'name': '', 'function': 'Руководитель'}
data = self._upd_fio(signer)
if not data['function']:
data['function'] = (
self.company_id.rsbu_signatory_title
if hasattr(self.company_id, 'rsbu_signatory_title')
else 'Руководитель') or 'Руководитель'
return data
def upd_docx_transferer(self):
"""Лицо, передавшее товар (кладовщик) — для «Товар передал»."""
self.ensure_one()
if not self.upd_transferred_by_id:
return {'name': '', 'function': ''}
data = self._upd_fio(self.upd_transferred_by_id)
if not data['function']:
data['function'] = 'Кладовщик'
return data
# ------------------------------------------------------------------
def _upd_docx_report(self, with_stamp=True):
xmlid = ('rsbu_fns_reporting.docx_template_rsbu_upd' if with_stamp
else 'rsbu_fns_reporting.docx_template_rsbu_updn')
template = self.env.ref(xmlid, raise_if_not_found=False)
return template.report_id if template else False
def action_print_upd_docx(self):
"""Печать УПД (DOCX, с печатями) из отгрузочного СФ."""
self.ensure_one()
from odoo.exceptions import UserError
from odoo import _
report = self._upd_docx_report(with_stamp=True)
if not report:
raise UserError(_('Не найден DOCX-шаблон УПД.'))
return report.report_action(self)
def action_print_updn_docx(self):
"""Печать УПД без печатей (DOCX) из отгрузочного СФ."""
self.ensure_one()
from odoo.exceptions import UserError
from odoo import _
report = self._upd_docx_report(with_stamp=False)
if not report:
raise UserError(_('Не найден DOCX-шаблон УПД.'))
return report.report_action(self)