"""Декларация по НДС (КНД 1151001, УСН + спецставка НДS 5%/7%). Комплект из трёх XML: корневая декларация (раздел «НДС» — СумУплНП/СумУпл164) и разделы 8 (книга покупок) и 9 (книга продаж). Расчёт — из готовых регистров НДС: книги (rsbu.vat.book.line) и итоги (rsbu.vat.summary). Начисленный НДС раскладывается на реализацию (РеалТов5) и авансы (ОплПредПост): авансовый НДС определяется по наличию в проводке-источнике начисления строки счёта авансового НДС (76.АВ). Вычет — книга покупок (вычет авансового НДС при отгрузке). Постоянная модель (не транзиент): сохраняется, переоткрывается, хранит рассчитанные показатели. XML — в rsbu_nds_xml. Свежесть: выгрузка XML всегда выполняет пересчёт (см. rsbu_nds_xml.action_export_xml). """ from odoo import api, fields, models # Расчётная доля НДС в авансе по ставочному режиму (для базы авансов). _ADVANCE_VAT_FRACTION = {'vat_5': 5.0 / 105.0, 'vat_7': 7.0 / 107.0} # Код счёта авансового НДС — признак авансового начисления в проводке. VAT_ADVANCE_CODE = '76.АВ' class RsbuNdsDeclaration(models.Model): _name = 'rsbu.nds.declaration' _description = 'РСБУ — Декларация НДС' _order = 'date_to desc, id desc' _check_company_auto = True name = fields.Char(string='Наименование', compute='_compute_name', store=True) company_id = fields.Many2one( 'res.company', string='Компания', required=True, index=True, default=lambda self: self.env.company) date_from = fields.Date(string='Период с', required=True) date_to = fields.Date(string='Период по', required=True) currency_id = fields.Many2one( 'res.currency', related='company_id.currency_id') state = fields.Selection( selection=[('draft', 'Черновик'), ('computed', 'Рассчитана')], default='draft', string='Состояние', copy=False) # --- Начисление (раздел НДС / СумУпл164) --- vat_base_realization = fields.Monetary( string='База реализации (РеалТов5)', currency_field='currency_id') vat_tax_realization = fields.Monetary( string='НДС с реализации', currency_field='currency_id') vat_base_advance = fields.Monetary( string='База авансов (ОплПредПост)', currency_field='currency_id') vat_tax_advance = fields.Monetary( string='НДС с авансов', currency_field='currency_id') vat_charged_total = fields.Monetary( string='НДС начислен всего (НалВосстОбщ)', currency_field='currency_id') # --- Вычет и итог --- vat_deduction = fields.Monetary( string='Вычет (СумНалВыч)', currency_field='currency_id') vat_payable = fields.Monetary( string='НДС к уплате (СумПУ_173.1)', currency_field='currency_id') oktmo = fields.Char(string='ОКТМО') kbk = fields.Char(string='КБК') @api.depends('date_from', 'date_to') def _compute_name(self): for rec in self: rec.name = 'Декларация НДС %s — %s' % ( rec.date_from or '?', rec.date_to or '?') # ------------------------------------------------------------------ def _vat_advance_account(self): """Счёт авансового НДС (76.АВ) — признак авансового начисления.""" self.ensure_one() return self.env['rsbu.account'].sudo().search([ ('code', '=', VAT_ADVANCE_CODE), ('company_id', '=', self.company_id.id)], limit=1) def _split_sales_vat(self): """Разложить начисленный НДС книги продаж на реализацию и авансы. Авансовое начисление — проводка-источник строки книги содержит строку счёта 76.АВ (Дт). Возвращает (нДС_реализации, НДС_авансов). """ self.ensure_one() BookLine = self.env['rsbu.vat.book.line'].sudo() adv_acc = self._vat_advance_account() sales = BookLine.search([ ('company_id', '=', self.company_id.id), ('book', '=', 'sales'), ('date', '>=', self.date_from), ('date', '<=', self.date_to), ]) real_vat = 0.0 adv_vat = 0.0 for line in sales: move = line.move_id is_advance = adv_acc and any( l.account_id == adv_acc and l.debit > 0 for l in move.line_ids) if is_advance: adv_vat += line.vat_amount else: real_vat += line.vat_amount return round(real_vat, 2), round(adv_vat, 2) def action_compute(self): """Пересобрать книги/итоги НДС и рассчитать показатели декларации. Идемпотентно. Начислено — из книги продаж (разделено на реализацию и авансы), вычет — из книги покупок. Реквизиты — из настроек. """ BookLine = self.env['rsbu.vat.book.line'].sudo() Summary = self.env['rsbu.vat.summary'].sudo() for rec in self: company = rec.company_id BookLine.recompute_period(rec.date_from, rec.date_to, company) Summary.recompute_period(rec.date_from, rec.date_to, company) real_vat, adv_vat = rec._split_sales_vat() # База по расчётной ставке: реализация = НДС/долю − НДС... но для # спецставки 5% база реализации = НДС × (1/доля − 1) = НДС × 20. fraction = _ADVANCE_VAT_FRACTION.get(company.rsbu_vat_mode, 0.0) inv = (1.0 / fraction - 1.0) if fraction else 0.0 real_base = round(real_vat * inv, 2) # База авансов (ОплПредПост/НалБаза) — сумма аванса С НДС по # расчётной ставке (в 1С: НалБаза = НДС / доля = НДС × 105/5), # в отличие от реализации, где база — без НДС. adv_base = round(adv_vat / fraction, 2) if fraction else 0.0 deduction = round(sum(BookLine.search([ ('company_id', '=', company.id), ('book', '=', 'purchases'), ('date', '>=', rec.date_from), ('date', '<=', rec.date_to), ]).mapped('vat_amount')), 2) charged = round(real_vat + adv_vat, 2) rec.vat_base_realization = real_base rec.vat_tax_realization = real_vat rec.vat_base_advance = adv_base rec.vat_tax_advance = adv_vat rec.vat_charged_total = charged rec.vat_deduction = deduction rec.vat_payable = round(charged - deduction, 2) if not rec.oktmo: rec.oktmo = company.rsbu_oktmo or '' if not rec.kbk: rec.kbk = company.rsbu_kbk_nds or '' rec.state = 'computed' return True