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,175 @@
from odoo import api, fields, models
class RsbuInvoice(models.Model):
"""Счёт-фактура РСБУ — источник расчёта НДС.
Типы: авансовый (при получении аванса), отгрузочный (при УПД),
корректировочный (возврат / изменение стоимости).
Авансовый СФ при подтверждении порождает РСБУ-проводку начисления
авансового НДС (Дт 76.АВ — Кт 68.НДС) через rsbu.posting.mixin, как
отдельный документ (аналог «Счёта-фактуры выданного» на аванс в 1С).
"""
_name = 'rsbu.invoice'
_description = 'РСБУ — Счёт-фактура'
_order = 'date desc, name desc, id desc'
_inherit = ['rsbu.posting.mixin']
_check_company_auto = True
name = fields.Char(string='Номер СФ', default='/', copy=False,
index=True)
date = fields.Date(string='Дата СФ', required=True, index=True,
default=fields.Date.context_today)
invoice_type = fields.Selection(
selection=[
('advance', 'Авансовый'),
('shipment', 'Отгрузочный (УПД)'),
('correction', 'Корректировочный'),
],
string='Тип СФ', required=True, default='advance')
partner_id = fields.Many2one('res.partner', string='Контрагент',
required=True)
company_id = fields.Many2one(
'res.company', string='Компания', required=True, index=True,
default=lambda self: self.env.company)
currency_id = fields.Many2one(
'res.currency', string='Валюта', compute='_compute_currency_id',
store=True)
# Связь с источником (rsbu_move_id приходит из rsbu.posting.mixin).
source_model = fields.Char(string='Модель источника', copy=False)
source_res_id = fields.Integer(string='ID источника', copy=False)
# Проводка-аванс (документ-основание) — для дроблёного вычета НДС.
origin_move_id = fields.Many2one(
'rsbu.move', string='Документ-основание (аванс)', copy=False,
help='Проводка получения аванса, по которому начислен авансовый НДС.')
vat_rate = fields.Selection(
selection=[('vat_exempt', 'Без НДС'), ('vat_5', '5%'),
('vat_7', '7%'), ('vat_20', '20%'), ('vat_10', '10%')],
string='Ставка НДС', default='vat_5')
base_amount = fields.Monetary(string='Налоговая база',
currency_field='currency_id')
vat_amount = fields.Monetary(string='Сумма НДС',
currency_field='currency_id')
state = fields.Selection(
selection=[('draft', 'Черновик'), ('confirmed', 'Подтверждён'),
('cancelled', 'Отменён')],
string='Состояние', default='draft', copy=False)
# Связь с документом реализации (у отгрузочного СФ). У авансового и
# корректировочного — NULL. Передаточная часть УПД (upd_*) перенесена на
# rsbu.realization.
realization_id = fields.Many2one(
'rsbu.realization', string='Документ реализации',
copy=False, ondelete='set null')
# Исходный СФ для корректировочного СФ (КСФ) — схема-задел, без поведения.
origin_invoice_id = fields.Many2one(
'rsbu.invoice', string='Исходный счёт-фактура (для КСФ)',
copy=False, ondelete='set null')
@api.depends('company_id')
def _compute_currency_id(self):
for inv in self:
inv.currency_id = inv.company_id.currency_id
@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('rsbu.invoice') or '/'
return super().create(vals_list)
# ------------------------------------------------------------------
# Расчётная доля НДС в авансе по режиму НДС компании.
_ADVANCE_VAT_FRACTION = {'vat_5': 5.0 / 105.0, 'vat_7': 7.0 / 107.0}
def _role_account(self, role):
"""Счёт РСБУ по роли маппинга (vat_output/vat_advance) для компании."""
self.ensure_one()
return self._role_account_for_company(role, self.company_id)
@api.model
def _role_account_for_company(self, role, company):
"""Счёт РСБУ по роли маппинга для указанной компании (без записи)."""
Role = self.env['rsbu.operation.template.role'].sudo()
rec = Role.search([
('role', '=', role),
('company_id', 'in', (False, company.id)),
], limit=1)
return rec.account_id
# ------------------------------------------------------------------
# Точки rsbu.posting.mixin
# ------------------------------------------------------------------
def _rsbu_is_applicable(self):
"""Проводку порождает только авансовый СФ с НДС и заданными счетами."""
self.ensure_one()
if self.invoice_type != 'advance':
return False
if not self._role_account('vat_advance') \
or not self._role_account('vat_output'):
return False
has_vat = bool(self.vat_amount) or bool(
self._ADVANCE_VAT_FRACTION.get(self.company_id.rsbu_vat_mode))
return has_vat
def _rsbu_move_date(self):
self.ensure_one()
return self.date or fields.Date.context_today(self)
def _rsbu_build_lines(self):
"""Начисление авансового НДС: Дт 76.АВ — Кт 68.НДС.
Сумма — vat_amount, если задан, иначе base_amount×доля режима НДС.
origin строки 76.АВ = проводка-аванс (origin_move_id) для дроблёного
вычета в регламенте книги покупок.
"""
self.ensure_one()
vat_advance = self._role_account('vat_advance')
vat_output = self._role_account('vat_output')
if not vat_advance or not vat_output:
return []
vat = self.vat_amount
if not vat:
fraction = self._ADVANCE_VAT_FRACTION.get(
self.company_id.rsbu_vat_mode, 0.0)
vat = round((self.base_amount or 0.0) * fraction, 2)
if vat <= 0:
return []
dt = {'account_id': vat_advance.id, 'debit': vat,
'name': 'НДС с аванса'}
if self.partner_id:
dt['partner_id'] = self.partner_id.id
if self.origin_move_id:
dt['origin_move_id'] = self.origin_move_id.id
kt = {'account_id': vat_output.id, 'credit': vat,
'name': 'НДС с аванса начислен'}
return [dt, kt]
def action_confirm(self):
self.write({'state': 'confirmed'})
for inv in self:
inv._rsbu_generate()
def action_cancel(self):
self._rsbu_remove()
self.write({'state': 'cancelled'})
# ------------------------------------------------------------------
# Связь с инвойсом-источником (для товарных позиций УПД, Вариант B)
# ------------------------------------------------------------------
def _source_invoice(self):
"""Вернуть документ-источник (account.move), если СФ создан из него.
Товарные позиции УПД (наименование/кол-во/ОКЕИ/ГТИН) берутся из
инвойса Odoo, а финансовые показатели СФ — из РСБУ-слоя (Вариант B).
"""
self.ensure_one()
if self.source_model == 'account.move' and self.source_res_id:
move = self.env['account.move'].browse(self.source_res_id)
return move if move.exists() else self.env['account.move']
return self.env['account.move']