[C28-33][@ant] fix!: оплата СЗ — из кооперативного пула расходов w.cap.pgexp, не с личного кошелька пайщика — зачем: payexp списывал w.cap.blago (USER_SHARED) с L3 инициатора, уменьшая его паевые взносы; topupprogexp теперь наполняет кошелёк пула (o.cap.pgtop), подотчёт w.exp.adv числится на получателе аванса; имена o.exp.*/p.exp.expns в cooptypes-словари (названия операций в UI); форма: пайщик-получатель только физлицо, поле ФИО по канону; страница расхода: кнопка «К расходам», без дубля суммы, позиции отклонённой СЗ не «ожидают оплаты»
This commit is contained in:
+1
-1
@@ -60,6 +60,6 @@ void capital::createpgexp(name coopname, checksum256 expense_hash, name creator,
|
||||
_expense,
|
||||
Names::External::CREATE_EXPENSE_PROPOSAL,
|
||||
std::make_tuple(coopname, creator, expense_hash,
|
||||
ledger2_wallets::BLAGOROST_FUND, items, callback, statement)
|
||||
ledger2_wallets::PROGRAM_EXPENSE_POOL, items, callback, statement)
|
||||
).send();
|
||||
}
|
||||
|
||||
+14
-2
@@ -4,8 +4,10 @@
|
||||
* Программные расходы — это целевые списания «Благорост», не аллоцированные ни в один проект
|
||||
* (хозяйственные расходы программы, оплата инфраструктуры программы и т.п.). Прежде чем
|
||||
* создавать запрос на программный расход, председатель пополняет дедикейтнутый пул
|
||||
* @c program_expense_pool из общего @c global_available_invest_pool — деньги остаются
|
||||
* на том же ledger2-кошельке @c BLAGOROST_FUND, меняется только разбивка по назначению.
|
||||
* @c program_expense_pool из общего @c global_available_invest_pool. Помимо счётчиков
|
||||
* state наполняется КООПЕРАТИВНЫЙ ledger2-кошелёк @c PROGRAM_EXPENSE_POOL (w.cap.pgexp) —
|
||||
* именно из него шасси expense оплачивает СЗ; личные L3-кошельки пайщиков (w.cap.blago)
|
||||
* при расходах не изменяются.
|
||||
*
|
||||
* @param coopname Кооператив (scope state)
|
||||
* @param amount Сумма пополнения (символ строго совпадает с программой)
|
||||
@@ -18,4 +20,14 @@ void capital::topupprogexp(name coopname, eosio::asset amount) {
|
||||
require_auth(coopname);
|
||||
|
||||
Capital::State::topup_program_expense_pool(coopname, amount);
|
||||
|
||||
// Кошелёк-резерв пула: ISSUE w.cap.pgexp без бухпроводки (деньги физически
|
||||
// на 51 с момента взносов — выделяется только назначение под расходы).
|
||||
// Синтетический process_hash — у пополнения нет естественного анкера.
|
||||
const uint64_t now_us = eosio::current_time_point().time_since_epoch().count();
|
||||
const std::string anchor = coopname.to_string() + ":pgtopup:" + std::to_string(now_us);
|
||||
const checksum256 process_hash = eosio::sha256(anchor.c_str(), anchor.size());
|
||||
|
||||
Ledger2::apply(_capital, coopname, operations::capital::PROGRAM_EXPENSE_TOPUP, amount,
|
||||
name{}, process_hash, "capital:topupprogexp");
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ void expense::payexp(name coopname, checksum256 proposal_hash, checksum256 item_
|
||||
|
||||
bool item_found = false;
|
||||
uint8_t paid_mechanics = 0;
|
||||
name paid_recipient{};
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
bool all_reported = true;
|
||||
@@ -227,6 +228,7 @@ void expense::payexp(name coopname, checksum256 proposal_hash, checksum256 item_
|
||||
? static_cast<uint8_t>(D::ItemStatus::REPORTED)
|
||||
: static_cast<uint8_t>(D::ItemStatus::PAID);
|
||||
paid_mechanics = i.mechanics;
|
||||
paid_recipient = i.recipient;
|
||||
item_found = true;
|
||||
}
|
||||
if (i.status != static_cast<uint8_t>(D::ItemStatus::REPORTED)) {
|
||||
@@ -244,8 +246,10 @@ void expense::payexp(name coopname, checksum256 proposal_hash, checksum256 item_
|
||||
|
||||
// ledger2-проводка по механике оплаченного item'а — выдача аванса (blgadv)
|
||||
// или прямая оплата (blgdir). СЗ может смешивать обе механики.
|
||||
// Источник — кооперативный пул расходов; username нужен ledger2 только для
|
||||
// USER_SHARED-стороны (w.exp.adv) — это ПОЛУЧАТЕЛЬ аванса, не автор СЗ.
|
||||
Ledger2::apply(get_self(), coopname, operation_code_for(paid_mechanics), actual_amount,
|
||||
it->username, proposal_hash, "expense:payexp");
|
||||
paid_recipient, proposal_hash, "expense:payexp");
|
||||
}
|
||||
|
||||
void expense::reportexp(name coopname, checksum256 proposal_hash, checksum256 item_hash) {
|
||||
@@ -260,6 +264,7 @@ void expense::reportexp(name coopname, checksum256 proposal_hash, checksum256 it
|
||||
|
||||
asset item_amount = asset{0, it->total_actual.symbol};
|
||||
bool item_found = false;
|
||||
name item_recipient{};
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
bool all_reported = true;
|
||||
@@ -271,6 +276,7 @@ void expense::reportexp(name coopname, checksum256 proposal_hash, checksum256 it
|
||||
"reportexp применим только к ADVANCE-механике");
|
||||
i.status = static_cast<uint8_t>(D::ItemStatus::REPORTED);
|
||||
item_amount = i.actual_amount;
|
||||
item_recipient = i.recipient;
|
||||
item_found = true;
|
||||
}
|
||||
if (i.status != static_cast<uint8_t>(D::ItemStatus::REPORTED)) {
|
||||
@@ -286,10 +292,11 @@ void expense::reportexp(name coopname, checksum256 proposal_hash, checksum256 it
|
||||
|
||||
// ledger2: BURN ADVANCE_HOLD на остаточный actual item'а (выдано − возвращено
|
||||
// + доплачено), без бухпроводки — canal 08/51 уже сделан на blgadv/over.
|
||||
// Подотчёт числится на получателе аванса — его username и закрывает burn.
|
||||
// Полный возврат аванса (actual == 0) — burn не нужен, ADVANCE_HOLD уже пуст.
|
||||
if (item_amount.amount > 0) {
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::ADVANCE_REPORT, item_amount,
|
||||
it->username, proposal_hash, "expense:reportexp");
|
||||
item_recipient, proposal_hash, "expense:reportexp");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +333,7 @@ void expense::returnexp(name coopname, checksum256 proposal_hash, checksum256 it
|
||||
"Возврат возможен только по СЗ в статусе PARTIALLY_PAID");
|
||||
|
||||
bool item_found = false;
|
||||
name item_recipient{};
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
@@ -343,6 +351,7 @@ void expense::returnexp(name coopname, checksum256 proposal_hash, checksum256 it
|
||||
// возврата остатка получатель штатно отчитывается reportexp по
|
||||
// фактически потраченной части (при полном возврате — actual == 0).
|
||||
i.actual_amount -= return_amount;
|
||||
item_recipient = i.recipient;
|
||||
item_found = true;
|
||||
break;
|
||||
}
|
||||
@@ -352,9 +361,10 @@ void expense::returnexp(name coopname, checksum256 proposal_hash, checksum256 it
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
|
||||
// ledger2: TRANSFER ADVANCE_HOLD → BLAGOROST_FUND, Dr 51 / Cr 08.
|
||||
// ledger2: TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL, Dr 51 / Cr 08.
|
||||
// Подотчёт снимается с получателя аванса.
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::ADVANCE_RETURN, return_amount,
|
||||
it->username, proposal_hash, "expense:returnexp");
|
||||
item_recipient, proposal_hash, "expense:returnexp");
|
||||
}
|
||||
|
||||
void expense::overspendexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
@@ -370,6 +380,7 @@ void expense::overspendexp(name coopname, checksum256 proposal_hash, checksum256
|
||||
"Перерасход возможен только по СЗ в статусе PARTIALLY_PAID");
|
||||
|
||||
bool item_found = false;
|
||||
name item_recipient{};
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
@@ -385,6 +396,7 @@ void expense::overspendexp(name coopname, checksum256 proposal_hash, checksum256
|
||||
// (теперь на полную сумму выдано + доплата) закрывается штатным reportexp,
|
||||
// который сделает burn ADVANCE_HOLD на итоговый actual.
|
||||
i.actual_amount += overspend_amount;
|
||||
item_recipient = i.recipient;
|
||||
item_found = true;
|
||||
break;
|
||||
}
|
||||
@@ -394,8 +406,8 @@ void expense::overspendexp(name coopname, checksum256 proposal_hash, checksum256
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
|
||||
// ledger2: OVERSPEND — выдача доплаты (TRANSFER BLAGOROST_FUND → ADVANCE_HOLD,
|
||||
// Dr 08 / Cr 51). Закрытие подотчёта на полную сумму — в reportexp.
|
||||
// ledger2: OVERSPEND — выдача доплаты (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD,
|
||||
// Dr 08 / Cr 51) на подотчёт получателя. Закрытие на полную сумму — в reportexp.
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::OVERSPEND, overspend_amount,
|
||||
it->username, proposal_hash, "expense:overspend");
|
||||
item_recipient, proposal_hash, "expense:overspend");
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace operations {
|
||||
inline constexpr eosio::name WITHDRAW_FROM_CAPITAL = "o.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY, без Dr/Cr).
|
||||
inline constexpr eosio::name CONVERT_TO_SHARE = "o.cap.cnvshr"_n; ///< Конвертация сегмента: РИД → главный кошелёк (TRANSFER GENERATOR_FUND → SHARE_FUND_PAY, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
|
||||
inline constexpr eosio::name CONVERT_TO_BLAGO = "o.cap.cnvbl"_n; ///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
|
||||
inline constexpr eosio::name PROGRAM_EXPENSE_TOPUP = "o.cap.pgtop"_n; ///< Пополнение пула программных расходов из инвестиций программы (ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr — деньги уже на 51, выделяется кооперативный резерв под расходы; паевые L3-кошельки пайщиков не трогаются).
|
||||
}
|
||||
|
||||
// marketplace
|
||||
@@ -102,6 +103,9 @@ namespace operations {
|
||||
//
|
||||
// Принципы (см. components/desktop/extensions/expenses/NAMING-C28-28.md):
|
||||
// - Контракт `expense` универсальный: operation_code передаётся в payload.
|
||||
// - Источник оплат — КООПЕРАТИВНЫЙ пул расходов (PROGRAM_EXPENSE_POOL),
|
||||
// пополняемый o.cap.pgtop; личные L3-кошельки пайщиков (w.cap.blago)
|
||||
// при оплатах СЗ не изменяются.
|
||||
// - При расходе из Благороста паевой фонд (80) НЕ трогается: меняется только форма
|
||||
// актива 51 → 08 (Дт 08 / Кт 51 для обеих механик).
|
||||
// - ADVANCE-отчёт (`o.exp.advrpt`) НЕ создаёт новой бухпроводки: проводка уже
|
||||
@@ -109,11 +113,11 @@ namespace operations {
|
||||
// - Callback на финализацию — переменная (`callback{contract, action, data}`),
|
||||
// заполняется при `expense::createexp`; expense ничего не знает про capital.
|
||||
namespace expense {
|
||||
inline constexpr eosio::name BLAGO_ADVANCE = "o.exp.blgadv"_n; ///< Выдача подотчётных из ЦПП «Благорост» (TRANSFER BLAGOROST_FUND → ADVANCE_HOLD, Dr 08 / Cr 51).
|
||||
inline constexpr eosio::name BLAGO_DIRECT = "o.exp.blgdir"_n; ///< Прямая оплата из ЦПП «Благорост» (BURN BLAGOROST_FUND, Dr 08 / Cr 51).
|
||||
inline constexpr eosio::name BLAGO_ADVANCE = "o.exp.blgadv"_n; ///< Выдача подотчётных из пула расходов (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51).
|
||||
inline constexpr eosio::name BLAGO_DIRECT = "o.exp.blgdir"_n; ///< Прямая оплата из пула расходов (BURN PROGRAM_EXPENSE_POOL, Dr 08 / Cr 51).
|
||||
inline constexpr eosio::name ADVANCE_REPORT = "o.exp.advrpt"_n; ///< Закрытие подотчёта пайщика (BURN ADVANCE_HOLD, без бухпроводки — canal 08/51 уже сделан на blgadv).
|
||||
inline constexpr eosio::name ADVANCE_RETURN = "o.exp.advret"_n; ///< Возврат неиспользованного подотчёта (TRANSFER ADVANCE_HOLD → BLAGOROST_FUND, Dr 51 / Cr 08).
|
||||
inline constexpr eosio::name OVERSPEND = "o.exp.over"_n; ///< Доплата сверх подотчёта (TRANSFER BLAGOROST_FUND → ADVANCE_HOLD, Dr 08 / Cr 51); сразу за ней expense вызывает ADVANCE_REPORT.
|
||||
inline constexpr eosio::name ADVANCE_RETURN = "o.exp.advret"_n; ///< Возврат неиспользованного подотчёта (TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL, Dr 51 / Cr 08).
|
||||
inline constexpr eosio::name OVERSPEND = "o.exp.over"_n; ///< Доплата сверх подотчёта (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51); сразу за ней expense вызывает ADVANCE_REPORT.
|
||||
}
|
||||
|
||||
// migration (только из migrate.cpp)
|
||||
@@ -370,6 +374,16 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
0, 0,
|
||||
"Конвертация сегмента: РИД → ЦПП «Благорост»" },
|
||||
|
||||
// 19a. Пополнение пула программных расходов: ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr.
|
||||
// Совет выделяет часть свободных инвестиций программы под целевые расходы:
|
||||
// деньги физически на 51 с момента взносов, здесь появляется кооперативный
|
||||
// резерв-кошелёк, из которого шасси expense оплачивает СЗ. Паевые L3-кошельки
|
||||
// пайщиков (w.cap.blago, счёт 80) не изменяются — права требования сохраняются.
|
||||
{ operations::capital::PROGRAM_EXPENSE_TOPUP, processes::capital::PGEXP, WalletOp::ISSUE,
|
||||
eosio::name{}, ledger2_wallets::PROGRAM_EXPENSE_POOL,
|
||||
0, 0,
|
||||
"Пополнение пула программных расходов ЦПП «Благорост»" },
|
||||
|
||||
// ----- Шасси расходов (o.exp.*) — вызываются из контракта expense -----
|
||||
//
|
||||
// Базовое состояние Благороста ДО расхода:
|
||||
@@ -380,21 +394,25 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
// Принцип: расход не уменьшает паевой фонд (80). Меняется только форма актива
|
||||
// 51 → 08 (банк уходит, появляется WIP-проект).
|
||||
|
||||
// 20. Выдача подотчётных из Благороста: Dr 08 / Cr 51, TRANSFER BLAGOROST_FUND → ADVANCE_HOLD.
|
||||
// Деньги физически уходят пайщику (Cr 51), стоимость капитализируется в WIP проекта (Dr 08).
|
||||
// ADVANCE_HOLD фиксирует ответственность пайщика (USER_SHARED) до отчёта.
|
||||
// 20. Выдача подотчётных из пула программных расходов: Dr 08 / Cr 51,
|
||||
// TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD.
|
||||
// Источник — КООПЕРАТИВНЫЙ пул расходов (пополняется o.cap.pgtop); личные
|
||||
// L3-кошельки пайщиков (w.cap.blago) при оплате СЗ не трогаются — их паевые
|
||||
// взносы в программе не уменьшаются. Деньги физически уходят пайщику (Cr 51),
|
||||
// стоимость капитализируется в WIP (Dr 08). ADVANCE_HOLD фиксирует
|
||||
// ответственность получателя аванса (USER_SHARED) до отчёта.
|
||||
{ operations::expense::BLAGO_ADVANCE, processes::expense::PROPOSAL, WalletOp::TRANSFER,
|
||||
ledger2_wallets::BLAGOROST_FUND, ledger2_wallets::ADVANCE_HOLD,
|
||||
ledger2_wallets::PROGRAM_EXPENSE_POOL, ledger2_wallets::ADVANCE_HOLD,
|
||||
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
|
||||
"Выдача подотчётных из ЦПП «Благорост»" },
|
||||
"Выдача подотчётных из пула расходов ЦПП «Благорост»" },
|
||||
|
||||
// 21. Прямая оплата из Благороста (DIRECT): Dr 08 / Cr 51, BURN BLAGOROST_FUND.
|
||||
// Прямая оплата организации; деньги уходят с 51, стоимость капитализируется в 08.
|
||||
// Кошелёк-резерв не задействован — оплата фиксируется сразу.
|
||||
// 21. Прямая оплата из пула программных расходов (DIRECT): Dr 08 / Cr 51,
|
||||
// BURN PROGRAM_EXPENSE_POOL. Оплата организации по счёту; деньги уходят с 51,
|
||||
// стоимость капитализируется в 08. Кошелёк-резерв не задействован.
|
||||
{ operations::expense::BLAGO_DIRECT, processes::expense::PROPOSAL, WalletOp::BURN,
|
||||
ledger2_wallets::BLAGOROST_FUND, eosio::name{},
|
||||
ledger2_wallets::PROGRAM_EXPENSE_POOL, eosio::name{},
|
||||
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
|
||||
"Прямая оплата из ЦПП «Благорост»" },
|
||||
"Прямая оплата из пула расходов ЦПП «Благорост»" },
|
||||
|
||||
// 22. Закрытие подотчёта пайщика по отчёту: BURN ADVANCE_HOLD, БЕЗ бухпроводки.
|
||||
// Проводка Dr 08 / Cr 51 уже сделана на BLAGO_ADVANCE при выдаче. При отчёте только
|
||||
@@ -404,18 +422,21 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
0, 0,
|
||||
"Закрытие подотчёта пайщика по отчёту" },
|
||||
|
||||
// 23. Возврат неиспользованного подотчёта: Dr 51 / Cr 08, TRANSFER ADVANCE_HOLD → BLAGOROST_FUND.
|
||||
// Зеркало BLAGO_ADVANCE: деньги возвращаются на 51, WIP-стоимость уменьшается.
|
||||
// 23. Возврат неиспользованного подотчёта: Dr 51 / Cr 08,
|
||||
// TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL.
|
||||
// Зеркало BLAGO_ADVANCE: деньги возвращаются на 51, WIP-стоимость уменьшается,
|
||||
// остаток снова доступен пулу расходов.
|
||||
{ operations::expense::ADVANCE_RETURN, processes::expense::PROPOSAL, WalletOp::TRANSFER,
|
||||
ledger2_wallets::ADVANCE_HOLD, ledger2_wallets::BLAGOROST_FUND,
|
||||
ledger2_wallets::ADVANCE_HOLD, ledger2_wallets::PROGRAM_EXPENSE_POOL,
|
||||
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::NON_CURRENT_INVESTMENTS,
|
||||
"Возврат неиспользованного подотчёта в ЦПП «Благорост»" },
|
||||
"Возврат неиспользованного подотчёта в пул расходов" },
|
||||
|
||||
// 24. Доплата сверх подотчёта (перерасход): Dr 08 / Cr 51, TRANSFER BLAGOROST_FUND → ADVANCE_HOLD.
|
||||
// 24. Доплата сверх подотчёта (перерасход): Dr 08 / Cr 51,
|
||||
// TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD.
|
||||
// Зеркало BLAGO_ADVANCE на сумму перерасхода. Контракт expense сразу за OVERSPEND
|
||||
// вызывает ADVANCE_REPORT — две последовательные записи в одной транзакции `expense::overspendexp`.
|
||||
{ operations::expense::OVERSPEND, processes::expense::PROPOSAL, WalletOp::TRANSFER,
|
||||
ledger2_wallets::BLAGOROST_FUND, ledger2_wallets::ADVANCE_HOLD,
|
||||
ledger2_wallets::PROGRAM_EXPENSE_POOL, ledger2_wallets::ADVANCE_HOLD,
|
||||
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
|
||||
"Доплата сверх подотчёта (перерасход)" },
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace processes {
|
||||
inline constexpr eosio::name PROPERTY = "p.cap.prop"_n; ///< Приём имущественного паевого взноса (одноактовый).
|
||||
inline constexpr eosio::name PREIMP = "p.cap.preimp"_n; ///< Первичный учёт РИД-взноса до перехода на электронный учёт (одноактовый, anchor = preimp register hash).
|
||||
inline constexpr eosio::name WTHCAP = "p.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый).
|
||||
inline constexpr eosio::name PGEXP = "p.cap.pgexp"_n; ///< Пул программных расходов: пополнение из инвестиций программы (o.cap.pgtop, одноактовый).
|
||||
}
|
||||
|
||||
// marketplace
|
||||
|
||||
@@ -65,6 +65,7 @@ struct ledger2_wallets {
|
||||
static constexpr eosio::name BLAGOROST_FUND = "w.cap.blago"_n; ///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
|
||||
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107)
|
||||
static constexpr eosio::name PREIMP_FUND = "w.cap.preimp"_n; ///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
|
||||
static constexpr eosio::name PROGRAM_EXPENSE_POOL = "w.cap.pgexp"_n; ///< Пул программных расходов ЦПП «Благорост» (COOPERATIVE) — кооперативный кошелёк, из которого шасси expense оплачивает СЗ; пополняется topupprogexp (o.cap.pgtop), паевые L3-кошельки пайщиков (w.cap.blago) при расходах не трогаются
|
||||
|
||||
// marketplace — выплаты
|
||||
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
|
||||
@@ -102,7 +103,7 @@ struct Ledger2WalletMeta {
|
||||
WalletKind kind;
|
||||
};
|
||||
|
||||
inline constexpr std::array<Ledger2WalletMeta, 17> LEDGER2_WALLET_REGISTRY = {{
|
||||
inline constexpr std::array<Ledger2WalletMeta, 18> LEDGER2_WALLET_REGISTRY = {{
|
||||
// USER_SHARED (7) — L3-разрез по пайщику
|
||||
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
|
||||
@@ -127,6 +128,7 @@ inline constexpr std::array<Ledger2WalletMeta, 17> LEDGER2_WALLET_REGISTRY = {{
|
||||
{ ledger2_wallets::MIN_SHARE_USED, "Использованные минимальные паевые взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::PROGRAM_EXPENSE_POOL, "Пул программных расходов ЦПП «Благорост»", WalletKind::COOPERATIVE },
|
||||
}};
|
||||
|
||||
static constexpr size_t LEDGER2_WALLET_REGISTRY_SIZE = LEDGER2_WALLET_REGISTRY.size();
|
||||
|
||||
@@ -172,6 +172,37 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
|
||||
debit: null, credit: null,
|
||||
human_name: 'Конвертация сегмента: РИД → ЦПП «Благорост»' },
|
||||
|
||||
{ code: 'o.cap.pgtop', process_type: 'p.cap.pgexp', contract: 'capital',
|
||||
name: 'PROGRAM_EXPENSE_TOPUP', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.pgexp',
|
||||
debit: null, credit: null,
|
||||
human_name: 'Пополнение пула программных расходов ЦПП «Благорост»' },
|
||||
|
||||
// expense — шасси расходов (оплата СЗ из кооперативного пула расходов)
|
||||
{ code: 'o.exp.blgadv', process_type: 'p.exp.expns', contract: 'expense',
|
||||
name: 'BLAGO_ADVANCE', wallet_op: 'TRANSFER', wallet_from: 'w.cap.pgexp', wallet_to: 'w.exp.adv',
|
||||
debit: 8, credit: 51,
|
||||
human_name: 'Выдача подотчётных из пула расходов ЦПП «Благорост»' },
|
||||
|
||||
{ code: 'o.exp.blgdir', process_type: 'p.exp.expns', contract: 'expense',
|
||||
name: 'BLAGO_DIRECT', wallet_op: 'BURN', wallet_from: 'w.cap.pgexp', wallet_to: null,
|
||||
debit: 8, credit: 51,
|
||||
human_name: 'Прямая оплата из пула расходов ЦПП «Благорост»' },
|
||||
|
||||
{ code: 'o.exp.advrpt', process_type: 'p.exp.expns', contract: 'expense',
|
||||
name: 'ADVANCE_REPORT', wallet_op: 'BURN', wallet_from: 'w.exp.adv', wallet_to: null,
|
||||
debit: null, credit: null,
|
||||
human_name: 'Закрытие подотчёта пайщика по отчёту' },
|
||||
|
||||
{ code: 'o.exp.advret', process_type: 'p.exp.expns', contract: 'expense',
|
||||
name: 'ADVANCE_RETURN', wallet_op: 'TRANSFER', wallet_from: 'w.exp.adv', wallet_to: 'w.cap.pgexp',
|
||||
debit: 51, credit: 8,
|
||||
human_name: 'Возврат неиспользованного подотчёта в пул расходов' },
|
||||
|
||||
{ code: 'o.exp.over', process_type: 'p.exp.expns', contract: 'expense',
|
||||
name: 'OVERSPEND', wallet_op: 'TRANSFER', wallet_from: 'w.cap.pgexp', wallet_to: 'w.exp.adv',
|
||||
debit: 8, credit: 51,
|
||||
human_name: 'Доплата сверх подотчёта (перерасход)' },
|
||||
|
||||
// marketplace
|
||||
{ code: 'o.mkt.supply', process_type: 'p.mkt.reqst', contract: 'marketplace',
|
||||
name: 'CONFIRM_SUPPLY', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.wal.share',
|
||||
|
||||
@@ -36,6 +36,10 @@ export const LEDGER2_PROCESS_REGISTRY: readonly ProcessMeta[] = [
|
||||
{ type: 'p.cap.prop', contract: 'capital', name: 'PROPERTY', human_name: 'Приём имущественного паевого взноса' },
|
||||
{ type: 'p.cap.preimp', contract: 'capital', name: 'PREIMP', human_name: 'Первичный учёт РИД-взноса до перехода на электронный учёт' },
|
||||
{ type: 'p.cap.wthcap', contract: 'capital', name: 'WTHCAP', human_name: 'Возврат паевого из ЦПП «Благорост» в кошелёк' },
|
||||
{ type: 'p.cap.pgexp', contract: 'capital', name: 'PGEXP', human_name: 'Пополнение пула программных расходов' },
|
||||
|
||||
// expense
|
||||
{ type: 'p.exp.expns', contract: 'expense', name: 'PROPOSAL', human_name: 'Расход по служебной записке' },
|
||||
|
||||
// marketplace
|
||||
{ type: 'p.mkt.reqst', contract: 'marketplace', name: 'REQUEST', human_name: 'Запрос маркетплейса' },
|
||||
|
||||
@@ -36,6 +36,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
|
||||
{ name: "w.sov.mnused", human_name: "Использованные минимальные паевые взносы", kind: "COOPERATIVE" },
|
||||
{ name: "w.cap.loan", human_name: "Выданные пайщикам беспроцентные займы", kind: "COOPERATIVE" },
|
||||
{ name: "w.mkt.payout", human_name: "Выплаты поставщикам", kind: "COOPERATIVE" },
|
||||
{ name: "w.cap.pgexp", human_name: "Пул программных расходов ЦПП «Благорост»", kind: "COOPERATIVE" },
|
||||
] as const
|
||||
|
||||
export interface ProgramWalletMapping {
|
||||
|
||||
+6
-1
@@ -60,10 +60,15 @@ BaseDialog(
|
||||
disabled
|
||||
)
|
||||
.col-12.col-md-6(v-if='item.recipient_type === Zeus.ExpenseRecipientType.MEMBER')
|
||||
UserSearchSelector(
|
||||
//- Аванс под отчёт выдаётся только физическим лицам — организации и ИП
|
||||
//- получают оплату по счёту (тип «Организация/ИП»).
|
||||
UserSearchSelector.base-select(
|
||||
v-model='item.recipient_account',
|
||||
label='Пайщик-получатель (по ФИО)',
|
||||
outlined,
|
||||
dense,
|
||||
reserve-hint-space,
|
||||
:types='["individual"]',
|
||||
@update:model-value='item.payment_method_id = null'
|
||||
)
|
||||
.col-12.col-md-6(v-if='item.recipient_type === Zeus.ExpenseRecipientType.ORG')
|
||||
|
||||
+117
-76
@@ -1,82 +1,90 @@
|
||||
<template lang="pug">
|
||||
q-page.program-expense-page
|
||||
template(v-if='loading && !expense')
|
||||
.skel
|
||||
q-skeleton(type='rect', height='96px')
|
||||
q-skeleton(type='rect', height='180px')
|
||||
q-skeleton(type='rect', height='180px')
|
||||
//- Липкий бар с кнопкой «Назад» — канон detail-страниц (реестр документов).
|
||||
.program-expense-page__bar
|
||||
button.expense-back(type='button', @click='goBack')
|
||||
q-icon(name='arrow_back', size='18px')
|
||||
span К расходам
|
||||
|
||||
template(v-else-if='expense')
|
||||
//- Статус + сумма — верхняя строка страницы (канон: статус сущности вверху).
|
||||
.head-row
|
||||
BaseChip(:variant='proposalStatusVariant(expense.status)') {{ proposalStatusLabel(expense.status) }}
|
||||
.head-row__amount.t-mono {{ expense.total_planned }}
|
||||
.program-expense-page__content
|
||||
template(v-if='loading && !expense')
|
||||
.skel
|
||||
q-skeleton(type='rect', height='96px')
|
||||
q-skeleton(type='rect', height='180px')
|
||||
q-skeleton(type='rect', height='180px')
|
||||
|
||||
BaseCard
|
||||
.summary
|
||||
DataRow(label='№ служебной записки', :value='shortExpenseId(expense.expense_hash)', mono, copyable)
|
||||
DataRow(label='Инициатор', :value='expense.creator_name')
|
||||
DataRow(label='Создан', :value='formatDate(expense.created_at)')
|
||||
DataRow(label='Сумма по смете', :value='expense.total_planned')
|
||||
DataRow(
|
||||
v-if='hasActual',
|
||||
label='Фактически израсходовано',
|
||||
:value='expense.total_actual'
|
||||
)
|
||||
template(v-else-if='expense')
|
||||
//- Статус сущности — вверху страницы (сумма — в сводке, не дублируем).
|
||||
.head-row
|
||||
BaseChip(:variant='proposalStatusVariant(expense.status)') {{ proposalStatusLabel(expense.status) }}
|
||||
|
||||
.section
|
||||
.t-eyebrow.t-muted Позиции расхода
|
||||
.items
|
||||
BaseCard(v-for='(item, idx) in itemRows', :key='item.item_hash')
|
||||
.item
|
||||
.item__head
|
||||
.item__title Позиция №{{ idx + 1 }} — {{ item.description }}
|
||||
BaseChip(:variant='itemStatusVariant(item.status)') {{ itemStatusLabel(item.status, item.mechanics) }}
|
||||
.item__rows
|
||||
DataRow(label='Получатель', :value='item.recipient_name')
|
||||
DataRow(label='Способ оплаты', :value='mechanicsLabel(item.mechanics)')
|
||||
DataRow(label='Сумма (план)', :value='item.planned_amount')
|
||||
DataRow(v-if='itemHasActual(item)', label='Фактически', :value='item.actual_amount')
|
||||
DataRow(
|
||||
v-if='item.requisite',
|
||||
label='Реквизиты получателя',
|
||||
:value='item.requisite.requisites'
|
||||
)
|
||||
DataRow(
|
||||
v-if='item.requisite?.payment_purpose',
|
||||
label='Назначение платежа',
|
||||
:value='item.requisite.payment_purpose'
|
||||
)
|
||||
.item__files(v-if='item.files.length')
|
||||
.t-sm.t-muted Первичные документы
|
||||
button.file-link(
|
||||
v-for='file in item.files',
|
||||
:key='file.id',
|
||||
type='button',
|
||||
:disabled='openingId === file.id',
|
||||
@click='openFile(file)'
|
||||
)
|
||||
q-icon(name='attach_file', size='16px')
|
||||
span {{ fileKindLabel(file.kind) }}: {{ fileLabel(file) }}
|
||||
q-spinner(v-if='openingId === file.id', size='14px')
|
||||
|
||||
.section
|
||||
.t-eyebrow.t-muted История состояний
|
||||
BaseCard
|
||||
ActivityTimeline(:events='timeline', group-by-date)
|
||||
.summary
|
||||
DataRow(label='№ служебной записки', :value='shortExpenseId(expense.expense_hash)', mono, copyable)
|
||||
DataRow(label='Инициатор', :value='expense.creator_name')
|
||||
DataRow(label='Создан', :value='formatDate(expense.created_at)')
|
||||
DataRow(label='Сумма по смете', :value='expense.total_planned')
|
||||
DataRow(
|
||||
v-if='hasActual',
|
||||
label='Фактически израсходовано',
|
||||
:value='expense.total_actual'
|
||||
)
|
||||
|
||||
.empty(v-else)
|
||||
EmptyState(
|
||||
title='Расход не найден',
|
||||
body='Служебная записка с таким номером отсутствует или ещё не синхронизирована.'
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name='receipt_long', size='48px')
|
||||
.section
|
||||
.t-eyebrow.t-muted Позиции расхода
|
||||
.items
|
||||
BaseCard(v-for='(item, idx) in itemRows', :key='item.item_hash')
|
||||
.item
|
||||
.item__head
|
||||
.item__title Позиция №{{ idx + 1 }} — {{ item.description }}
|
||||
//- У отклонённой СЗ позиции ничего не «ожидают» — оплат не будет.
|
||||
BaseChip(v-if='isDeclined', variant='neutral') Не будет оплачена
|
||||
BaseChip(v-else, :variant='itemStatusVariant(item.status)') {{ itemStatusLabel(item.status, item.mechanics) }}
|
||||
.item__rows
|
||||
DataRow(label='Получатель', :value='item.recipient_name')
|
||||
DataRow(label='Способ оплаты', :value='mechanicsLabel(item.mechanics)')
|
||||
DataRow(label='Сумма (план)', :value='item.planned_amount')
|
||||
DataRow(v-if='itemHasActual(item)', label='Фактически', :value='item.actual_amount')
|
||||
DataRow(
|
||||
v-if='item.requisite',
|
||||
label='Реквизиты получателя',
|
||||
:value='item.requisite.requisites'
|
||||
)
|
||||
DataRow(
|
||||
v-if='item.requisite?.payment_purpose',
|
||||
label='Назначение платежа',
|
||||
:value='item.requisite.payment_purpose'
|
||||
)
|
||||
.item__files(v-if='item.files.length')
|
||||
.t-sm.t-muted Первичные документы
|
||||
button.file-link(
|
||||
v-for='file in item.files',
|
||||
:key='file.id',
|
||||
type='button',
|
||||
:disabled='openingId === file.id',
|
||||
@click='openFile(file)'
|
||||
)
|
||||
q-icon(name='attach_file', size='16px')
|
||||
span {{ fileKindLabel(file.kind) }}: {{ fileLabel(file) }}
|
||||
q-spinner(v-if='openingId === file.id', size='14px')
|
||||
|
||||
.section
|
||||
.t-eyebrow.t-muted История состояний
|
||||
BaseCard
|
||||
ActivityTimeline(:events='timeline', group-by-date)
|
||||
|
||||
.empty(v-else)
|
||||
EmptyState(
|
||||
title='Расход не найден',
|
||||
body='Служебная записка с таким номером отсутствует или ещё не синхронизирована.'
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name='receipt_long', size='48px')
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Zeus } from '@coopenomics/sdk';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
@@ -104,7 +112,12 @@ import {
|
||||
} from 'app/extensions/capital/entities/ProgramExpense/model';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const system = useSystemStore();
|
||||
|
||||
function goBack(): void {
|
||||
void router.push({ name: 'capital-program-expenses' });
|
||||
}
|
||||
const desktopStore = useDesktopStore();
|
||||
const store = useProgramExpenseStore();
|
||||
|
||||
@@ -154,6 +167,10 @@ const hasActual = computed(
|
||||
() => parseFloat(expense.value?.total_actual ?? '0') > 0,
|
||||
);
|
||||
|
||||
const isDeclined = computed(
|
||||
() => expense.value?.status === Zeus.ExpenseProposalStatus.DECLINED,
|
||||
);
|
||||
|
||||
type ItemRow = IProgramExpenseItem & {
|
||||
requisite: IExpenseRequisite | null;
|
||||
files: IExpenseProposalFile[];
|
||||
@@ -289,7 +306,18 @@ const timeline = computed<ActivityEvent[]>(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.program-expense-page {
|
||||
/* Липкий бар во всю ширину — отступы несут бар и контент раздельно.
|
||||
top = высота фиксированного топбара (--p-topbar-h), иначе бар уезжает под него. */
|
||||
.program-expense-page__bar {
|
||||
position: sticky;
|
||||
top: var(--p-topbar-h, 56px);
|
||||
z-index: 2;
|
||||
background: var(--p-canvas);
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
padding: var(--p-3, 12px) var(--p-6, 24px);
|
||||
}
|
||||
|
||||
.program-expense-page__content {
|
||||
padding: var(--p-6, 24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -297,11 +325,30 @@ const timeline = computed<ActivityEvent[]>(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.program-expense-page {
|
||||
.program-expense-page__bar {
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
}
|
||||
.program-expense-page__content {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.expense-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--p-1, 4px);
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--p-ink-2);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
cursor: pointer;
|
||||
transition: color var(--p-dur-fast, 120ms) var(--p-ease-standard);
|
||||
}
|
||||
.expense-back:hover {
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.skel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -315,12 +362,6 @@ const timeline = computed<ActivityEvent[]>(() => {
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.head-row__amount {
|
||||
font-size: var(--p-fs-h3);
|
||||
font-weight: 700;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -50,11 +50,13 @@
|
||||
|
||||
| # | C++ константа | eosio::name | WalletOp | wallet_from → wallet_to | Dr / Cr | human_name |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3.1 | `operations::expense::BLAGO_ADVANCE` | `o.exp.blgadv` | TRANSFER | `BLAGOROST_FUND` → `w.exp.adv` | **08 / 51** | «Выдача подотчётных из ЦПП «Благорост»» |
|
||||
| 3.2 | `operations::expense::BLAGO_DIRECT` | `o.exp.blgdir` | BURN | `BLAGOROST_FUND` → ∅ | **08 / 51** | «Прямая оплата из ЦПП «Благорост» (DIRECT)» |
|
||||
| 3.1 | `operations::expense::BLAGO_ADVANCE` | `o.exp.blgadv` | TRANSFER | `PROGRAM_EXPENSE_POOL` → `w.exp.adv` | **08 / 51** | «Выдача подотчётных из пула расходов ЦПП «Благорост»» |
|
||||
| 3.2 | `operations::expense::BLAGO_DIRECT` | `o.exp.blgdir` | BURN | `PROGRAM_EXPENSE_POOL` → ∅ | **08 / 51** | «Прямая оплата из пула расходов ЦПП «Благорост»» |
|
||||
| 3.3 | `operations::expense::ADVANCE_REPORT` | `o.exp.advrpt` | BURN | `w.exp.adv` → ∅ | 0 / 0 | «Закрытие подотчёта пайщика по отчёту (canal 08/51 уже сделан на blgadv)» |
|
||||
| 3.4 | `operations::expense::ADVANCE_RETURN` | `o.exp.advret` | TRANSFER | `w.exp.adv` → `BLAGOROST_FUND` | **51 / 08** | «Возврат неиспользованного подотчёта в ЦПП «Благорост» (зеркало blgadv)» |
|
||||
| 3.5 | `operations::expense::OVERSPEND` | `o.exp.over` | TRANSFER | `BLAGOROST_FUND` → `w.exp.adv` | **08 / 51** | «Доплата сверх подотчёта (перерасход)» |
|
||||
| 3.4 | `operations::expense::ADVANCE_RETURN` | `o.exp.advret` | TRANSFER | `w.exp.adv` → `PROGRAM_EXPENSE_POOL` | **51 / 08** | «Возврат неиспользованного подотчёта в пул расходов (зеркало blgadv)» |
|
||||
| 3.5 | `operations::expense::OVERSPEND` | `o.exp.over` | TRANSFER | `PROGRAM_EXPENSE_POOL` → `w.exp.adv` | **08 / 51** | «Доплата сверх подотчёта (перерасход)» |
|
||||
|
||||
**Изменение v4 (2026-06-12, голосовое):** источник оплат — НЕ `w.cap.blago` (он USER_SHARED: списание уменьшало личный паевой кошелёк пайщика-инициатора — категорически неверно). Введён КООПЕРАТИВНЫЙ кошелёк `w.cap.pgexp` (`PROGRAM_EXPENSE_POOL`, «Пул программных расходов ЦПП «Благорост»»): пополняется `capital::topupprogexp` операцией `o.cap.pgtop` (ISSUE, без Dr/Cr — деньги физически на 51 со взносов, выделяется назначение), из него `expense::payexp` платит обе механики. `username` в ledger2-проводках шасси — получатель позиции (подотчёт `w.exp.adv` числится на получателе аванса), не автор СЗ.
|
||||
|
||||
**Process_type:** один — `processes::expense::PROPOSAL = "p.exp.proposal"_n`. Все 5 кодов привязаны к нему.
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ const props = defineProps<{
|
||||
filled?: boolean;
|
||||
outlined?: boolean;
|
||||
color?: string;
|
||||
/** Ограничить выдачу типами аккаунтов (например, только физлица: ['individual']). */
|
||||
types?: string[];
|
||||
}>();
|
||||
|
||||
// Эмиты
|
||||
@@ -127,7 +129,10 @@ const selectOptions = computed(() => {
|
||||
// console.log('searchResults.value:', searchResults.value); // Отладка
|
||||
// console.log('selectedUserData.value:', selectedUserData.value); // Отладка
|
||||
|
||||
const options = [...searchResults.value];
|
||||
const allowed = props.types;
|
||||
const options = searchResults.value.filter(
|
||||
(r) => !allowed?.length || allowed.includes(r.type),
|
||||
);
|
||||
|
||||
// Если есть выбранный пользователь, но его нет в результатах поиска, добавляем
|
||||
if (selectedUserData.value) {
|
||||
|
||||
Reference in New Issue
Block a user