Files
public/rsbu_vat/models/rsbu_invoice.py

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