Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
422
rsbu_mapping/resolvers/commercial.py
Normal file
422
rsbu_mapping/resolvers/commercial.py
Normal file
@@ -0,0 +1,422 @@
|
||||
"""Резолверы коммерческого цикла (по регламенту 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='Уменьшение расчётов с поставщиком'),
|
||||
]
|
||||
Reference in New Issue
Block a user