295 lines
14 KiB
Python
295 lines
14 KiB
Python
# -*- 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)
|