"""Резолверы коммерческого цикла (по регламенту docs/research/08). Каждый резолвер получает документ-родитель (account.move / account.payment) и шаблон rsbu.operation.template (роли счетов). Возвращает список значений строк rsbu.move.line, сбалансированный по Дт=Кт. Ставка НДС берётся из инвойса (налоговые строки) либо из поля rsbu_vat_amount платежа. """ from .registry import resolver def _line(account, debit=0.0, credit=0.0, partner=None, product=None, user=None, employee=None, name=None, origin=None, origin_self=False, contract=None): vals = { 'account_id': account.id, 'debit': debit, 'credit': credit, } if partner: vals['partner_id'] = partner.id if product: vals['product_id'] = product.id if user: vals['user_id'] = user.id if employee: vals['employee_id'] = employee.id if name: vals['name'] = name # Субконто «Документ-основание» (аванс): либо явная проводка-основание, # либо маркер origin_self (ссылка на саму создаваемую проводку — # проставляется миксином после create). if origin is not None: vals['origin_move_id'] = origin.id if origin_self: vals['origin_self'] = True # Субконто «Договор». if contract is not None: vals['contract_id'] = contract.id return vals def _doc_contract(doc): """Договор документа-источника (partner.contract.customer) или None.""" return getattr(doc, 'mt_contract_id', False) or None @resolver('advance_received') def advance_received(doc, template): """Получение аванса от покупателя (входящий платёж). Дт bank — Кт advance_received (сумма платежа); Дт vat_advance — Кт vat_output (авансовый НДС из rsbu_vat_amount). """ partner = doc.partner_id amount = abs(doc.amount) vat = abs(getattr(doc, 'rsbu_vat_amount', 0.0) or 0.0) contract = _doc_contract(doc) bank = template._account('bank') advance = template._account('advance_received') lines = [ _line(bank, debit=amount, name='Поступление аванса'), # origin_self: аванс ссылается на свою проводку — для дроблёного # зачёта по основанию в shipment_upd (get_origin_balances). _line(advance, credit=amount, partner=partner, contract=contract, origin_self=True, name='Аванс полученный'), ] if vat: vat_advance = template._account('vat_advance') vat_output = template._account('vat_output') lines += [ _line(vat_advance, debit=vat, partner=partner, name='НДС с аванса'), _line(vat_output, credit=vat, name='НДС с аванса начислен'), ] return lines @resolver('advance_refund') def advance_refund(doc, template): """Возврат аванса покупателю. Дт advance_received — Кт bank; вычет авансового НДС: Дт vat_output — Кт vat_advance. """ partner = doc.partner_id amount = abs(doc.amount) vat = abs(getattr(doc, 'rsbu_vat_amount', 0.0) or 0.0) bank = template._account('bank') advance = template._account('advance_received') lines = [ _line(advance, debit=amount, partner=partner, name='Возврат аванса'), _line(bank, credit=amount, name='Возврат аванса с р/с'), ] if vat: vat_advance = template._account('vat_advance') vat_output = template._account('vat_output') lines += [ _line(vat_output, debit=vat, name='Вычет НДС с аванса'), _line(vat_advance, credit=vat, partner=partner, name='Сторно НДС с аванса'), ] return lines @resolver('shipment_upd') def shipment_upd(doc, template): """Отгрузка / выполнение работ / УПД (исходящий инвойс). Базовая реализация (брутто): Дт 62.01 — Кт 90.01 на полную сумму (выручка с НДС); Дт 90.03 — Кт 68.НДС на сумму НДС (НДС в составе продаж). Дополнительно, если по контрагенту есть незакрытый аванс (кредитовый остаток 62.АВ), формируются проводки зачёта аванса и вычета авансового НДС (по регламенту 08, ч.1 п.6): Дт 62.АВ — Кт 62.01 на сумму зачёта; Дт 68.НДС — Кт 76.АВ на авансовый НДС, приходящийся на зачёт. """ partner = doc.partner_id company_id = doc.company_id.id tax = abs(doc.amount_tax) total = abs(doc.amount_total) contract = _doc_contract(doc) receivable = template._account('receivable') revenue = template._account('revenue') lines = [ _line(receivable, debit=total, partner=partner, contract=contract, name='Задолженность покупателя'), _line(revenue, credit=total, name='Выручка'), ] if tax: revenue_vat = template._account('revenue_vat') vat_output = template._account('vat_output') lines += [ _line(revenue_vat, debit=tax, name='НДС с реализации'), _line(vat_output, credit=tax, name='НДС с реализации начислен'), ] # --- Зачёт аванса, ДРОБЛЁННЫЙ по каждому авансу-основанию (FIFO) --- # Как в 1С: Дт 62.02 — Кт 62.01 отдельной строкой на каждый аванс, # с субконто «Документ-основание» = проводка аванса, в разрезе договора. # Вычет авансового НДС здесь НЕ делается — он выполняется регламентом # «Формирование записей книги покупок» в конце периода (68.02 — 76.АВ). advance = template._account_or_none('advance_received') if advance and partner: subconto = {'partner': partner.id} if contract: subconto['contract'] = contract.id origins = advance.get_origin_balances( date_to=doc.date, company_id=company_id, subconto=subconto) remaining = total for data in origins: if remaining <= 0: break # 62.02 пассивный: кредитовое сальдо = незакрытый аванс. available = data['balance'] if data['side'] == 'credit' else 0.0 if available <= 0: continue take = min(available, remaining) take = round(take, 2) if take <= 0: continue origin_move = data['origin'] lines += [ _line(advance, debit=take, partner=partner, contract=contract, origin=origin_move, name='Зачёт аванса покупателя'), _line(receivable, credit=take, partner=partner, contract=contract, name='Зачёт аванса в счёт задолженности'), ] remaining -= take return lines @resolver('payment_after_shipment') def payment_after_shipment(doc, template): """Постоплата: поступление денег после отгрузки. Дт bank — Кт receivable. """ partner = doc.partner_id amount = abs(doc.amount) bank = template._account('bank') receivable = template._account('receivable') return [ _line(bank, debit=amount, name='Оплата от покупателя'), _line(receivable, credit=amount, partner=partner, name='Погашение задолженности'), ] # Расчётная ставка НДС с аванса по режиму НДС компании (доля налога в сумме). _ADVANCE_VAT_FRACTION = { 'vat_5': 5.0 / 105.0, 'vat_7': 7.0 / 107.0, } @resolver('customer_payment') def customer_payment(doc, template): """Поступление денег от покупателя: аванс, постоплата или смешанно. Только движение денег (как в 1С; авансовый НДС начисляется отдельным документом «Счёт-фактура на аванс», а не здесь). Тип определяется сальдо дебиторки 62.01 по контрагенту на дату платежа: * есть незакрытая дебиторка → Дт 51 — Кт 62.01 на min(долг, платёж) (постоплата, гасит долг по каждому основанию); * остаток сверх долга → аванс: Дт 51 — Кт 62.02 с субконто «Документ-основание» = сама проводка платежа (origin_self), чтобы затем вести зачёт по каждому авансу отдельно. """ partner = doc.partner_id company_id = doc.company_id.id amount = abs(doc.amount) bank = template._account('bank') contract = _doc_contract(doc) lines = [_line(bank, debit=amount, name='Оплата от покупателя')] # Долг покупателя на дату платежа — в разрезе (контрагент, договор), # как в 1С (зачёт и погашение ведутся по договору-основанию). receivable = template._account_or_none('receivable') debt = 0.0 if receivable and partner: subconto = {'partner': partner.id} if contract: subconto['contract'] = contract.id bal = receivable.get_balance( date_to=doc.date, company_id=company_id, subconto=subconto) # 62.01 активный: дебетовое сальдо = долг покупателя по договору. debt = bal['balance'] if bal['side'] == 'debit' else 0.0 pay_debt = min(debt, amount) advance = round(amount - pay_debt, 2) # Погашение задолженности (постоплата) — по тому же договору. if pay_debt > 0 and receivable: lines.append(_line(receivable, credit=pay_debt, partner=partner, contract=contract, name='Погашение задолженности покупателя')) # Остаток — аванс (субконто: договор + основание = сама проводка). if advance > 0: advance_acc = template._account('advance_received') lines.append(_line(advance_acc, credit=advance, partner=partner, contract=contract, origin_self=True, name='Аванс полученный')) return lines @resolver('sale_credit_note') def sale_credit_note(doc, template): """Возврат от клиента / кредит-нота (out_refund). Сторно реализации: Дт revenue — Кт receivable (сумма без НДС); сторно НДС: Дт vat_output — Кт revenue_vat. """ partner = doc.partner_id tax = abs(doc.amount_tax) total = abs(doc.amount_total) receivable = template._account('receivable') revenue = template._account('revenue') # Зеркально shipment_upd: Дт 90.01 — Кт 62.01 на полную сумму; # Дт 68.НДС — Кт 90.03 на сумму НДС (сторно начисления). lines = [ _line(revenue, debit=total, name='Сторно выручки'), _line(receivable, credit=total, partner=partner, name='Уменьшение задолженности покупателя'), ] if tax: revenue_vat = template._account('revenue_vat') vat_output = template._account('vat_output') lines += [ _line(vat_output, debit=tax, name='Сторно НДС с реализации'), _line(revenue_vat, credit=tax, name='Сторно НДС в продажах'), ] return lines @resolver('purchase_bill') def purchase_bill(doc, template): """Счёт/поступление поставщика (in_invoice). Дт expense(26) — Кт payable(60.01) на полную сумму (на УСН входящий НДС к вычету не принимается и включается в стоимость). Плюс зачёт аванса выданного, ДРОБЛЁННЫЙ по каждому авансу-основанию (FIFO) в разрезе договора, как в 1С: Дт 60.01 — Кт 60.02 отдельной строкой на каждый аванс (субконто «Документ-основание» = проводка аванса). """ partner = doc.partner_id company_id = doc.company_id.id total = abs(doc.amount_total) contract = _doc_contract(doc) expense = template._account('expense') payable = template._account('payable') lines = [ _line(expense, debit=total, name='Затраты по счёту поставщика'), _line(payable, credit=total, partner=partner, contract=contract, name='Задолженность перед поставщиком'), ] # --- Зачёт аванса выданного (60.02), дроблёный по основаниям --- advance_paid = template._account_or_none('advance_paid') if advance_paid and partner: subconto = {'partner': partner.id} if contract: subconto['contract'] = contract.id origins = advance_paid.get_origin_balances( date_to=doc.date, company_id=company_id, subconto=subconto) remaining = total for data in origins: if remaining <= 0: break # 60.02 активный: дебетовое сальдо = незачтённый аванс выданный. available = data['balance'] if data['side'] == 'debit' else 0.0 if available <= 0: continue take = round(min(available, remaining), 2) if take <= 0: continue origin_move = data['origin'] lines += [ _line(payable, debit=take, partner=partner, contract=contract, name='Зачёт аванса выданного'), _line(advance_paid, credit=take, partner=partner, contract=contract, origin=origin_move, name='Зачёт аванса в счёт поставки'), ] remaining -= take return lines @resolver('purchase_credit_note') def purchase_credit_note(doc, template): """Возврат поставщику / кредит-нота поставщика (in_refund). Дт payable — Кт expense (сторно затрат и задолженности). """ partner = doc.partner_id total = abs(doc.amount_total) expense = template._account('expense') payable = template._account('payable') return [ _line(payable, debit=total, partner=partner, name='Уменьшение задолженности перед поставщиком'), _line(expense, credit=total, name='Сторно затрат'), ] @resolver('supplier_payment') def supplier_payment(doc, template): """Оплата поставщику (исходящий платёж): постоплата и/или аванс выданный. Зеркально customer_payment. Тип определяется сальдо кредиторки 60.01 по (поставщик, договор) на дату платежа: * есть незакрытый долг перед поставщиком → Дт 60.01 — Кт 51 на min(долг, платёж) (погашение задолженности); * остаток сверх долга → аванс выданный: Дт 60.02 — Кт 51 с субконто «Документ-основание» = сама проводка платежа (origin_self), договор. """ partner = doc.partner_id company_id = doc.company_id.id amount = abs(doc.amount) bank = template._account('bank') payable = template._account('payable') contract = _doc_contract(doc) # Долг перед поставщиком на дату — в разрезе (поставщик, договор). debt = 0.0 if partner: subconto = {'partner': partner.id} if contract: subconto['contract'] = contract.id bal = payable.get_balance( date_to=doc.date, company_id=company_id, subconto=subconto) # 60.01 пассивный: кредитовое сальдо = наш долг поставщику. debt = bal['balance'] if bal['side'] == 'credit' else 0.0 pay_debt = min(debt, amount) advance = round(amount - pay_debt, 2) lines = [] # Погашение задолженности (постоплата). if pay_debt > 0: lines += [ _line(payable, debit=pay_debt, partner=partner, contract=contract, name='Оплата поставщику'), _line(bank, credit=pay_debt, name='Списание с р/с'), ] # Остаток — аванс выданный (60.02, origin_self, договор). if advance > 0: advance_paid = template._account('advance_paid') lines += [ _line(advance_paid, debit=advance, partner=partner, contract=contract, origin_self=True, name='Аванс выданный поставщику'), _line(bank, credit=advance, name='Списание с р/с (аванс)'), ] return lines @resolver('supplier_refund') def supplier_refund(doc, template): """Возврат денег от поставщика (входящий платёж). Дт bank — Кт payable. """ partner = doc.partner_id amount = abs(doc.amount) payable = template._account('payable') bank = template._account('bank') return [ _line(bank, debit=amount, name='Возврат от поставщика'), _line(payable, credit=amount, partner=partner, name='Уменьшение расчётов с поставщиком'), ]