Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
175
rsbu_vat/models/rsbu_invoice.py
Normal file
175
rsbu_vat/models/rsbu_invoice.py
Normal 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']
|
||||
Reference in New Issue
Block a user