Files
public/rsbu_fns_reporting/models/rsbu_upd_docx.py

295 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)