diff --git a/app/AccountancyModule/EducationModule/presenters/BudgetPresenter.php b/app/AccountancyModule/EducationModule/presenters/BudgetPresenter.php
new file mode 100644
index 000000000..1ffdb482f
--- /dev/null
+++ b/app/AccountancyModule/EducationModule/presenters/BudgetPresenter.php
@@ -0,0 +1,82 @@
+aid) {
+ return;
+ }
+
+ $this->flashMessage('Musíš vybrat akci', 'danger');
+ $this->redirect('Default:');
+ }
+
+ public function renderDefault(int $aid): void
+ {
+ if (! $this->authorizator->isAllowed(Education::ACCESS_BUDGET, $this->aid)) {
+ $this->flashMessage('Nemáte právo prohlížet rozpočet akce', 'danger');
+ $this->redirect('Education:');
+ }
+
+ $educationId = new SkautisEducationId($aid);
+
+ $inconsistentTotals = $this->queryBus->handle(new InconsistentEducationCategoryTotalsQuery($educationId));
+ $this->template->setParameters([
+ 'isConsistent' => count($inconsistentTotals) === 0,
+ 'toRepair' => $inconsistentTotals,
+ 'budgetEntries' => $this->queryBus->handle(new EducationBudgetQuery($educationId, $this->event->grantId)),
+ 'categoriesSummary' => $this->queryBus->handle(new CategoriesSummaryQuery($this->getCashbookId($aid))),
+ 'isUpdateStatementAllowed' => $this->authorizator->isAllowed(Education::UPDATE_REAL_BUDGET_SPENDING, $aid),
+ ]);
+ if (! $this->isAjax()) {
+ return;
+ }
+
+ $this->redrawControl('contentSnip');
+ }
+
+ /**
+ * přepočte hodnoty v jednotlivých kategorich
+ */
+ public function handleConvert(int $aid): void
+ {
+ $this->editableOnly();
+
+ $this->commandBus->handle(new UpdateEducationCategoryTotals($this->getCashbookId($aid)));
+ $this->flashMessage('Kategorie byly přepočítány.');
+
+ if ($this->isAjax()) {
+ $this->redrawControl('flash');
+ } else {
+ $this->redirect('this', $aid);
+ }
+ }
+
+ private function getCashbookId(int $educationId): CashbookId
+ {
+ return $this->queryBus->handle(new EducationCashbookIdQuery(new SkautisEducationId($educationId)));
+ }
+}
diff --git a/app/AccountancyModule/EducationModule/templates/Budget/default.latte b/app/AccountancyModule/EducationModule/templates/Budget/default.latte
new file mode 100644
index 000000000..5d81182c8
--- /dev/null
+++ b/app/AccountancyModule/EducationModule/templates/Budget/default.latte
@@ -0,0 +1,88 @@
+{block #title}{$event->getDisplayName()} - rozpočet{/block}
+
+{define #budgetTable $entries, $income}
+
+
+
+ Položka |
+ Částka |
+
+
+
+ {$entry->name} |
+
+ {$entry->total|price}
+ |
+
+
+{/define}
+
+{define #categoriesTable $categoriesSummary, $income}
+
+
+
+ Položka |
+ Částka |
+
+
+ {var $balance = 0}
+
+ {do $balance += (float)$categorySummary->total->getAmount()/100}
+ {$categorySummary->name} |
+
+ {$categorySummary->total|price}
+ |
+
+
+ Celkem |
+ {$balance|price} |
+
+
+{/define}
+
+{block #content}
+
+{include ../header.latte}
+
+
+
Nekonzistentní data!
+
Součet paragonů v kategoriích neodpovídá částkám uvedeným ve SkautISu.
+ {if $isEditable}
+
Hospodaření může aktualizovat data ve SkautISu tak, aby byla shodná s evidencí plateb.
+
+ {if $isUpdateStatementAllowed}
+
+
+ Aktualizovat data ve SkautISu
+
+ {else}
+
+
+ Nemáte oprávnění pro úpravu částek v rozpočtu uvedených ve skautisu.
+
+ {/if}
+ {/if}
+
+
+
+
+
Skutečné náklady
+ {include #categoriesTable $categoriesSummary, FALSE}
+
+
+
Skutečné výnosy
+ {include #categoriesTable $categoriesSummary, TRUE}
+
+
+
+
+
+
Předpokl. náklady
+ {include #budgetTable $budgetEntries, FALSE}
+
+
+
Předpokládané výnosy
+ {include #budgetTable $budgetEntries, TRUE}
+
+
diff --git a/app/AccountancyModule/EducationModule/templates/header.latte b/app/AccountancyModule/EducationModule/templates/header.latte
index 01dae47ed..4a1a59d32 100644
--- a/app/AccountancyModule/EducationModule/templates/header.latte
+++ b/app/AccountancyModule/EducationModule/templates/header.latte
@@ -16,6 +16,10 @@
Evidence plateb
+
+ Rozpočet
+
-
diff --git a/app/config/config.neon b/app/config/config.neon
index b72af4351..2f54f178a 100644
--- a/app/config/config.neon
+++ b/app/config/config.neon
@@ -191,6 +191,10 @@ services:
bus: queryBus
# Skautis read model
+ - factory: Model\Skautis\ReadModel\QueryHandlers\EducationBudgetQueryHandler(@skautis.cached.grants)
+ tags:
+ messenger.messageHandler:
+ bus: queryBus
- factory: Model\Skautis\ReadModel\QueryHandlers\CampBudgetQueryHandler(@skautis.cached.event)
tags:
messenger.messageHandler:
diff --git a/app/model/Cashbook/Commands/Cashbook/UpdateEducationCategoryTotals.php b/app/model/Cashbook/Commands/Cashbook/UpdateEducationCategoryTotals.php
new file mode 100644
index 000000000..09ce82e36
--- /dev/null
+++ b/app/model/Cashbook/Commands/Cashbook/UpdateEducationCategoryTotals.php
@@ -0,0 +1,25 @@
+cashbookId;
+ }
+}
diff --git a/app/model/Cashbook/Handlers/Cashbook/UpdateEducationCategoryTotalHandler.php b/app/model/Cashbook/Handlers/Cashbook/UpdateEducationCategoryTotalHandler.php
new file mode 100644
index 000000000..0c45b44cb
--- /dev/null
+++ b/app/model/Cashbook/Handlers/Cashbook/UpdateEducationCategoryTotalHandler.php
@@ -0,0 +1,38 @@
+cashbooks->find($command->getCashbookId());
+
+ $totals = [];
+ foreach ($this->queryBus->handle(new CategoriesSummaryQuery($cashbook->getId())) as $category) {
+ assert($category instanceof CategorySummary);
+ $totals[$category->getId()] = MoneyFactory::toFloat($category->getTotal());
+ }
+
+ $this->updater->updateCategories(
+ $cashbook->getId(),
+ $totals,
+ );
+ }
+}
diff --git a/app/model/Cashbook/ReadModel/Queries/InconsistentEducationCategoryTotalsQuery.php b/app/model/Cashbook/ReadModel/Queries/InconsistentEducationCategoryTotalsQuery.php
new file mode 100644
index 000000000..9e96cc4d7
--- /dev/null
+++ b/app/model/Cashbook/ReadModel/Queries/InconsistentEducationCategoryTotalsQuery.php
@@ -0,0 +1,25 @@
+educationId;
+ }
+}
diff --git a/app/model/Cashbook/ReadModel/QueryHandlers/InconsistentEducationCategoryTotalsQueryHandler.php b/app/model/Cashbook/ReadModel/QueryHandlers/InconsistentEducationCategoryTotalsQueryHandler.php
new file mode 100644
index 000000000..56c39d5f4
--- /dev/null
+++ b/app/model/Cashbook/ReadModel/QueryHandlers/InconsistentEducationCategoryTotalsQueryHandler.php
@@ -0,0 +1,49 @@
+queryBus->handle(new EducationCashbookIdQuery($query->getEducationId()));
+ $categories = $this->queryBus->handle(new CategoriesSummaryQuery($cashbookId));
+
+ $skautisTotals = [];
+
+ foreach ($this->educationCategories->findForEducation($query->getEducationId()->toInt()) as $educationCategory) {
+ $id = $educationCategory->getId();
+ $total = $educationCategory->getTotal();
+ $category = $categories[$id];
+
+ assert($category instanceof CategorySummary);
+
+ $isConsistent = $category->getTotal()->equals($total);
+
+ if ($isConsistent) {
+ continue;
+ }
+
+ $skautisTotals[$id] = MoneyFactory::toFloat($total);
+ }
+
+ return $skautisTotals;
+ }
+}
diff --git a/app/model/Cashbook/Services/IEducationCategoryUpdater.php b/app/model/Cashbook/Services/IEducationCategoryUpdater.php
new file mode 100644
index 000000000..8a383a680
--- /dev/null
+++ b/app/model/Cashbook/Services/IEducationCategoryUpdater.php
@@ -0,0 +1,20 @@
+ $cashbookTotals Category totals indexed by category ID
+ *
+ * @throws InvalidArgumentException
+ */
+ public function updateCategories(CashbookId $cashbookId, array $cashbookTotals): void;
+}
diff --git a/app/model/Grant/SkautisGrantId.php b/app/model/Grant/SkautisGrantId.php
index b5bf8557d..301636a08 100644
--- a/app/model/Grant/SkautisGrantId.php
+++ b/app/model/Grant/SkautisGrantId.php
@@ -6,7 +6,7 @@
final class SkautisGrantId
{
- public function __construct(private int $value)
+ public function __construct(private int|null $value)
{
}
diff --git a/app/model/Skautis/Cashbook/EducationCategoryUpdater.php b/app/model/Skautis/Cashbook/EducationCategoryUpdater.php
new file mode 100644
index 000000000..986e5698f
--- /dev/null
+++ b/app/model/Skautis/Cashbook/EducationCategoryUpdater.php
@@ -0,0 +1,88 @@
+ $cashbookTotals */
+ public function updateCategories(CashbookId $cashbookId, array $cashbookTotals): void
+ {
+ $educationSkautisId = $this->educationRepository->findByCashbookId($cashbookId)->getSkautisId();
+ $skautisTotals = $this->getSkautisTotals($educationSkautisId);
+
+ // Update categories that are not in cashbook, has total > 0 in Skautis
+ $categoriesOnlyInSkautis = array_diff(array_keys($skautisTotals), array_keys($cashbookTotals));
+ $categoriesOnlyInSkautis = array_filter($categoriesOnlyInSkautis, function (float $total) {
+ return $total === 0.0;
+ });
+
+ // Update categories that have different total in cashbook and Skautis
+ $cashbookTotals = array_filter($cashbookTotals, function (float $total, int $categoryId) use ($skautisTotals) {
+ return isset($skautisTotals[$categoryId]) && $skautisTotals[$categoryId] !== $total;
+ }, ARRAY_FILTER_USE_BOTH);
+
+ $cashbookTotals += array_fill_keys($categoriesOnlyInSkautis, 0);
+
+ if (count($cashbookTotals) === 0) {
+ return;
+ }
+
+ try {
+ foreach ($cashbookTotals as $categoryId => $total) {
+ $this->skautis->Grants->StatementUpdate([
+ 'ID' => $categoryId,
+ 'ID_EventEducation' => $educationSkautisId->toInt(),
+ 'Ammount' => -300,
+ 'IsBudget' => false,
+ ], 'statement');
+ }
+ } catch (WsdlException $exc) {
+ if (! preg_match('/Chyba validace \(EventStatement_EnterAmmountGreatherThanZero\)/', $exc->getMessage())) {
+ throw $exc;
+ }
+
+ throw new AmountMustBeGreaterThanZero();
+ }
+ }
+
+ /** @return float[] */
+ private function getSkautisTotals(SkautisEducationId $educationSkautisId): array
+ {
+ $categories = $this->educationCategories->findForEducation($educationSkautisId->toInt());
+ $totals = [];
+
+ foreach ($categories as $category) {
+ $totals[$category->getId()] = MoneyFactory::toFloat($category->getTotal());
+ }
+
+ return $totals;
+ }
+}
diff --git a/app/model/Skautis/Cashbook/Repositories/EducationCategoryRepository.php b/app/model/Skautis/Cashbook/Repositories/EducationCategoryRepository.php
index c929affbb..fc4033dad 100644
--- a/app/model/Skautis/Cashbook/Repositories/EducationCategoryRepository.php
+++ b/app/model/Skautis/Cashbook/Repositories/EducationCategoryRepository.php
@@ -27,7 +27,6 @@ public function findForEducation(int $educationId): array
$skautisCategories = $this->grantsWebService->StatementAll([
'ID_EventEducation' => $educationId,
'IsBudget' => false,
- 'Year' => '',
]);
if (is_object($skautisCategories)) {
diff --git a/app/model/Skautis/ReadModel/Queries/EducationBudgetQuery.php b/app/model/Skautis/ReadModel/Queries/EducationBudgetQuery.php
new file mode 100644
index 000000000..9d9bbc71c
--- /dev/null
+++ b/app/model/Skautis/ReadModel/Queries/EducationBudgetQuery.php
@@ -0,0 +1,27 @@
+educationId;
+ }
+
+ public function getGrantId(): SkautisGrantId
+ {
+ return $this->grantId;
+ }
+}
diff --git a/app/model/Skautis/ReadModel/QueryHandlers/EducationBudgetQueryHandler.php b/app/model/Skautis/ReadModel/QueryHandlers/EducationBudgetQueryHandler.php
new file mode 100644
index 000000000..ae6ad131d
--- /dev/null
+++ b/app/model/Skautis/ReadModel/QueryHandlers/EducationBudgetQueryHandler.php
@@ -0,0 +1,39 @@
+grantWebService->StatementAll([
+ 'ID_EventEducation' => $query->getEducationId()->toInt(),
+ 'ID_Grant' => $query->getGrantId()->toInt(),
+ 'IsBudget' => true,
+ ]);
+
+ return array_map(function (stdClass $category): BudgetEntry {
+ return new BudgetEntry(
+ $category->StatementType,
+ MoneyFactory::fromFloat((float) $category->Ammount),
+ $category->IsRevenue,
+ );
+ }, $skautisCategories);
+ }
+}
diff --git a/app/router/RouterFactory.php b/app/router/RouterFactory.php
index d9e17f843..787495282 100644
--- a/app/router/RouterFactory.php
+++ b/app/router/RouterFactory.php
@@ -135,6 +135,7 @@ private function createEducationRoutes(RouteList $parent): void
Route::FILTER_TABLE => [
'ucastnici' => 'Participant',
'kniha' => 'Cashbook',
+ 'rozpocet' => 'Budget',
],
],
'action' => 'default',