423 lines
19 KiB
Python
423 lines
19 KiB
Python
"""Резолверы коммерческого цикла (по регламенту 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='Уменьшение расчётов с поставщиком'),
|
||
]
|