176 lines
8.6 KiB
Python
176 lines
8.6 KiB
Python
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']
|