Overview
Comment:Implement create and edit transactions in API
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk | stable
Files: files | file ages | folders
SHA3-256: b7cb001be2479645ba595b92625344814556ef33a16711865adf110a28f50315
User & Date: bohwaz on 2022-07-30 19:33:22
Other Links: manifest | tags
Context
2022-07-30
19:35
Fix direction in debt/credit payoff check-in: b30208f10f user: bohwaz tags: trunk, stable
19:33
Implement create and edit transactions in API check-in: b7cb001be2 user: bohwaz tags: trunk, stable
18:23
Remove _form_rules feature in entities, improve and fix bugs in account add from transaction check-in: 4e2775ba8e user: bohwaz tags: trunk, stable
Changes

Modified src/include/lib/Garradin/API.php from [2f97c9a1c0] to [8fac325740].

1
2
3
4
5
6


7
8
9
10
11
12
13
<?php

namespace Garradin;

use Garradin\Membres\Session;
use Garradin\Web\Web;


use Garradin\Accounting\Reports;
use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;
use Garradin\Entities\Accounting\Transaction;

use KD2\ErrorManager;







>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Garradin;

use Garradin\Membres\Session;
use Garradin\Web\Web;
use Garradin\Accounting\Accounts;
use Garradin\Accounting\Charts;
use Garradin\Accounting\Reports;
use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;
use Garradin\Entities\Accounting\Transaction;

use KD2\ErrorManager;

162
163
164
165
166
167
168
169
170
171
172
173
174

175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

















201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221





222




223
224
225
226
227
228
229
	}

	protected function accounting(string $uri): ?array
	{
		$fn = strtok($uri, '/');
		$param = strtok('');

		if ($fn == 'transactions') {
			if ($param == '') {
				if ($this->method != 'GET') {
					throw new APIException('Wrong request method', 400);
				}


				try {
					return iterator_to_array(Reports::getJournal($_GET));
				}
				catch (\LogicException $e) {
					throw new APIException('Missing parameter for journal: ' . $e->getMessage(), 400, $e);
				}
			}
			elseif (is_numeric($param)) {
				$transaction = Transactions::get((int)$param);

				if (!$transaction) {
					throw new APIException(sprintf('Transaction #%d not found', $param), 404);
				}

				if ($this->method == 'GET') {
					return $transaction->asJournalArray();
				}
				/*
				elseif ($this->method == 'POST') {
					$this->requireAccess(Session::ACCESS_WRITE);
					$transaction->importFromEditForm();
					$transaction->save();
					return $transaction->asJournalArray();
				}
				*/
				else {

















					throw new APIException('Wrong request method', 400);
				}
			}
			/*
			elseif ($param == 'new') {
				$this->requireAccess(Session::ACCESS_WRITE);
				$transaction = new Transaction;
				$transaction->importFromNewForm();
				$transaction->save();
				return $transaction->asJournalArray();
			}
			*/
			else {
				throw new APIException('Unknown transactions action', 404);
			}
		}
		elseif ($fn == 'years' && $param == '') {
			if ($this->method != 'GET') {
				throw new APIException('Wrong request method', 400);
			}






			return Years::list();




		}
		else {
			throw new APIException('Unknown accounting action', 404);
		}
	}

	public function errors(string $uri)







|

|



>
|
|
<
|
|
<











<


|



<

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|


<
|
<
<
<
<
|

<

|


|




>
>
>
>
>
|
>
>
>
>







164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179

180
181

182
183
184
185
186
187
188
189
190
191
192

193
194
195
196
197
198

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

220




221
222

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
	}

	protected function accounting(string $uri): ?array
	{
		$fn = strtok($uri, '/');
		$param = strtok('');

		if ($fn == 'transaction') {
			if ($param == '') {
				if ($this->method != 'POST') {
					throw new APIException('Wrong request method', 400);
				}

				$this->requireAccess(Session::ACCESS_WRITE);
				$transaction = new Transaction;
				$transaction->importFromAPI();

				$transaction->save();
				return $transaction->asJournalArray();

			}
			elseif (is_numeric($param)) {
				$transaction = Transactions::get((int)$param);

				if (!$transaction) {
					throw new APIException(sprintf('Transaction #%d not found', $param), 404);
				}

				if ($this->method == 'GET') {
					return $transaction->asJournalArray();
				}

				elseif ($this->method == 'POST') {
					$this->requireAccess(Session::ACCESS_WRITE);
					$transaction->importFromNewForm();
					$transaction->save();
					return $transaction->asJournalArray();
				}

				else {
					throw new APIException('Wrong request method', 400);
				}
			}
			else {
				throw new APIException('Unknown transactions action', 404);
			}
		}
		elseif ($fn == 'years') {
			if ($this->method != 'GET') {
				throw new APIException('Wrong request method', 400);
			}

			if (preg_match('!^(\d+)/journal!', $param, $match)) {
				try {
					return iterator_to_array(Reports::getJournal(['year' => $match[1]]));
				}
				catch (\LogicException $e) {
					throw new APIException('Missing parameter for journal: ' . $e->getMessage(), 400, $e);
				}
			}

			elseif ($param == '') {




				return Years::list();
			}

			else {
				throw new APIException('Unknown years action', 404);
			}
		}
		elseif ($fn == 'charts') {
			if ($this->method != 'GET') {
				throw new APIException('Wrong request method', 400);
			}

			if (preg_match('!^(\d+)/accounts$!', $param, $match)) {
				$a = new Accounts((int)$match[1]);
				return array_map(fn($c) => $c->asArray(), $a->listAll());
			}
			elseif ($param == '') {
				return array_map(fn($c) => $c->asArray(), Charts::list());
			}
			else {
				throw new APIException('Unknown charts action', 404);
			}
		}
		else {
			throw new APIException('Unknown accounting action', 404);
		}
	}

	public function errors(string $uri)

Modified src/include/lib/Garradin/Entities/Accounting/Line.php from [c93ddb30c0] to [122932a477].

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
			'Libellé'   => $this->label,
			'Référence' => $this->reference,
			'Crédit'    => Utils::money_format($this->credit),
			'Débit'     => Utils::money_format($this->debit),
		];
	}

	/**
	 * Import form data into object
	 *
	 * There are 3 ways to pass account information.
	 *
	 * 1. Use the account ID: [id_account] => 1234
	 * 2. Use the account code: [account] => 512A
	 * 3. Use an interactive selector (input type=list): [account_selector] => [1234 => "512A - Compte courant"]
	 */
	public function importForm(?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (isset($source['account_selector'])) {
			if (empty($source['account_selector']) || !is_array($source['account_selector']) || empty(key($source['account_selector']))) {
				throw new ValidationException('Aucun compte n\'a été choisi.');
			}

			$source['id_account'] = (int)key($source['account_selector']);
		}
		elseif (isset($source['account'])) {
			if (empty($source['account']) || is_array($source['account'])) {
				throw new ValidationException('Aucun compte n\'a été choisi.');
			}

			// Find id from code
			// We don't check that the account belongs to the correct chart for the year of the linked transaction here
			// It is done in Transaction->selfCheck()
			$source['account'] = DB::getInstance()->firstColumn('SELECT id FROM acc_accounts WHERE code = ?;', $source['account']);

			if (empty($source['account'])) {
				throw new ValidationException('Le compte choisi n\'existe pas.');
			}
		}

		return parent::importForm($source);
	}
}







<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
85
86
87
88
89
90
91













92


























			'Libellé'   => $this->label,
			'Référence' => $this->reference,
			'Crédit'    => Utils::money_format($this->credit),
			'Débit'     => Utils::money_format($this->debit),
		];
	}














}


























Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [b5639ed20f] to [7b66ec9286].

544
545
546
547
548
549
550


551
552
553
554
555
556
557
		$this->assert($count > 0, 'Cette écriture ne comporte aucune ligne.');
		$this->assert($count >= 2, 'Cette écriture comporte moins de deux lignes.');
		$this->assert($count == 2 ||  $this->type == self::TYPE_ADVANCED, sprintf('Une écriture de type "%s" ne peut comporter que deux lignes au maximum.', self::TYPES_NAMES[$this->type]));

		$accounts_ids = [];

		foreach ($lines as $k => $line) {


			$this->assert($line->credit || $line->debit, sprintf('Ligne %d: Aucun montant au débit ou au crédit', $k));
			$this->assert($line->credit >= 0 && $line->debit >= 0, sprintf('Ligne %d: Le montant ne peut être négatif', $k));
			$this->assert(($line->credit * $line->debit) === 0 && ($line->credit + $line->debit) > 0, sprintf('Ligne %d: non équilibrée, crédit ou débit doit valoir zéro.', $k));

			$accounts_ids = [$line->id_account];
			$total += $line->credit;
			$total -= $line->debit;







>
>







544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
		$this->assert($count > 0, 'Cette écriture ne comporte aucune ligne.');
		$this->assert($count >= 2, 'Cette écriture comporte moins de deux lignes.');
		$this->assert($count == 2 ||  $this->type == self::TYPE_ADVANCED, sprintf('Une écriture de type "%s" ne peut comporter que deux lignes au maximum.', self::TYPES_NAMES[$this->type]));

		$accounts_ids = [];

		foreach ($lines as $k => $line) {
			$k = $k+1;
			$this->assert(!empty($line->id_account), sprintf('Ligne %d: aucun compte n\'est défini', $k));
			$this->assert($line->credit || $line->debit, sprintf('Ligne %d: Aucun montant au débit ou au crédit', $k));
			$this->assert($line->credit >= 0 && $line->debit >= 0, sprintf('Ligne %d: Le montant ne peut être négatif', $k));
			$this->assert(($line->credit * $line->debit) === 0 && ($line->credit + $line->debit) > 0, sprintf('Ligne %d: non équilibrée, crédit ou débit doit valoir zéro.', $k));

			$accounts_ids = [$line->id_account];
			$total += $line->credit;
			$total -= $line->debit;
636
637
638
639
640
641
642

643
644
645

646
647
648
649
650
651
652
653
654


655




656


657
658
659
660
661
662
663
664

665
666
667



668
669
















670
671
672
673
674
675
676
677
678
679
680
681
682
683
684




685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
		if (isset($source['amount']) && $this->type != self::TYPE_ADVANCED && isset($this->type)) {
			if (empty($source['amount'])) {
				throw new ValidationException('Montant non précisé');
			}

			$accounts = $this->getTypesDetails($source)[$this->type]->accounts;


			foreach ($accounts as $account) {
				if (empty($account->selector_value)) {
					throw new ValidationException(sprintf('%s : aucun compte n\'a été sélectionné', $account->label));

				}
			}

			$line = [
				'id_analytical' => $source['id_analytical'] ?? null,
				'reference' => $source['payment_reference'] ?? null,
			];

			$source['lines'] = [


				$line + [$accounts[0]->direction => $source['amount'], 'account_selector' => $accounts[0]->selector_value],




				$line + [$accounts[1]->direction => $source['amount'], 'account_selector' => $accounts[1]->selector_value],


			];

			unset($line, $accounts, $account, $source['simple']);
		}

		// Add lines
		if (isset($source['lines']) && is_array($source['lines'])) {
			$this->resetLines();


			foreach ($source['lines'] as $i => $line) {
				if (empty($line['account']) && empty($line['id_account']) && empty($line['account_selector'])) {



					throw new ValidationException(sprintf('Ligne %d : aucun compte n\'a été sélectionné', $i + 1));
				}

















				$l = new Line;
				$l->importForm($line);
				$this->addLine($l);
			}
		}

		return parent::importForm($source);
	}

	public function importFromNewForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}





		$type = $source['type'] ?? ($this->type ?? self::TYPE_ADVANCED);

		if (self::TYPE_ADVANCED != $type) {
			if (!isset($source['amount'])) {
				throw new UserException('Montant non précisé');
			}
		}

		$this->importForm($source);
	}

	public function importFromEditForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($source['id_related'])) {
			unset($source['id_related']);
		}

		$this->importFromNewForm($source);
	}

	public function importFromPayoffForm(?array $source = null): void
	{







>
|
|
|
>









>
>
|
>
>
>
>
|
>
>








>


|
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>















>
>
>
>












|





|
|







638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
		if (isset($source['amount']) && $this->type != self::TYPE_ADVANCED && isset($this->type)) {
			if (empty($source['amount'])) {
				throw new ValidationException('Montant non précisé');
			}

			$accounts = $this->getTypesDetails($source)[$this->type]->accounts;

			if (!isset($source['debit'], $source['credit'])) {
				foreach ($accounts as $account) {
					if (empty($account->selector_value)) {
						throw new ValidationException(sprintf('%s : aucun compte n\'a été sélectionné', $account->label));
					}
				}
			}

			$line = [
				'id_analytical' => $source['id_analytical'] ?? null,
				'reference' => $source['payment_reference'] ?? null,
			];

			$source['lines'] = [
				$line + [
					$accounts[0]->direction => $source['amount'],
					'account_selector' => $accounts[0]->selector_value,
					'account' => $source[$accounts[0]->direction] ?? null,
				],
				$line + [
					$accounts[1]->direction => $source['amount'],
					'account_selector' => $accounts[1]->selector_value,
					'account' => $source[$accounts[1]->direction] ?? null,
				],
			];

			unset($line, $accounts, $account, $source['simple']);
		}

		// Add lines
		if (isset($source['lines']) && is_array($source['lines'])) {
			$this->resetLines();
			$db = DB::getInstance();

			foreach ($source['lines'] as $i => $line) {
				if (empty($line['account'])
					&& empty($line['id_account'])
					&& (empty($line['account_selector'])
						|| !is_array($line['account_selector']) || empty(key($line['account_selector'])))) {
					throw new ValidationException(sprintf('Ligne %d : aucun compte n\'a été sélectionné', $i + 1));
				}

				if (isset($line['account_selector'])) {
					$line['id_account'] = (int)key($line['account_selector']);
				}
				elseif (isset($line['account'])) {
					if (empty($this->id_year) && empty($source['id_year'])) {
						throw new ValidationException('L\'identifiant de l\'exercice comptable n\'est pas précisé.');
					}

					$id_chart = $id_chart ?? $db->firstColumn('SELECT id_chart FROM acc_years WHERE id = ?;', $source['id_year'] ?? $this->id_year);
					$line['id_account'] = $db->firstColumn('SELECT id FROM acc_accounts WHERE code = ? AND id_chart = ?;', $line['account'], $id_chart);

					if (empty($line['id_account'])) {
						throw new ValidationException('Le compte choisi n\'existe pas.');
					}
				}

				$l = new Line;
				$l->importForm($line);
				$this->addLine($l);
			}
		}

		return parent::importForm($source);
	}

	public function importFromNewForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($source['id_related'])) {
			unset($source['id_related']);
		}

		$type = $source['type'] ?? ($this->type ?? self::TYPE_ADVANCED);

		if (self::TYPE_ADVANCED != $type) {
			if (!isset($source['amount'])) {
				throw new UserException('Montant non précisé');
			}
		}

		$this->importForm($source);
	}

	public function importFromAPI(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (isset($source['type']) && ctype_alpha($source['type']) && defined(self::class . '::TYPE_' . strtoupper($source['type']))) {
			$source['type'] = constant(self::class . '::TYPE_' . strtoupper($source['type']));
		}

		$this->importFromNewForm($source);
	}

	public function importFromPayoffForm(?array $source = null): void
	{
1052
1053
1054
1055
1056
1057
1058

1059




1060
1061
1062





			'Lignes'          => $lines,
		];
	}

	public function asJournalArray(): array
	{
		$out = $this->asArray();

		$out['lines'] = $this->getLinesWithAccounts();




		return $out;
	}
}












>

>
>
>
>


|
>
>
>
>
>
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
			'Lignes'          => $lines,
		];
	}

	public function asJournalArray(): array
	{
		$out = $this->asArray();
		$out['url'] = $this->url();
		$out['lines'] = $this->getLinesWithAccounts();
		foreach ($out['lines'] as &$line) {
			unset($line->line);
		}
		unset($line);
		return $out;
	}

	public function url(): string
	{
		return Utils::getLocalURL('!acc/transactions/details.php?id=' . $this->id());
	}
}

Modified src/include/lib/Garradin/Entity.php from [8d420902ea] to [dcb2b4c09d].

33
34
35
36
37
38
39



40
41
42
43
44
45
46
			return \DateTime::createFromFormat('d/m/Y', $value);
		}
		elseif (preg_match('!^\d{4}/\d{2}/\d{2}$!', $value)) {
			return \DateTime::createFromFormat('Y/m/d', $value);
		}
		elseif (preg_match('!^20\d{2}[01]\d[0123]\d$!', $value)) {
			return \DateTime::createFromFormat('Ymd', $value);



		}
		elseif (null !== $value) {
			throw new ValidationException('Format de date invalide (merci d\'utiliser le format JJ/MM/AAAA) : ' . $value);
		}
	}

	protected function filterUserValue(string $type, $value, string $key)







>
>
>







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
			return \DateTime::createFromFormat('d/m/Y', $value);
		}
		elseif (preg_match('!^\d{4}/\d{2}/\d{2}$!', $value)) {
			return \DateTime::createFromFormat('Y/m/d', $value);
		}
		elseif (preg_match('!^20\d{2}[01]\d[0123]\d$!', $value)) {
			return \DateTime::createFromFormat('Ymd', $value);
		}
		elseif (preg_match('!^\d{4}-\d{2}-\d{2}$!', $value)) {
			return \DateTime::createFromFormat('Y-m-d', $value);
		}
		elseif (null !== $value) {
			throw new ValidationException('Format de date invalide (merci d\'utiliser le format JJ/MM/AAAA) : ' . $value);
		}
	}

	protected function filterUserValue(string $type, $value, string $key)

Modified src/www/admin/acc/transactions/edit.php from [114f3af54e] to [775fbad222].

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$accounts = $chart->accounts();

$csrf_key = 'acc_transaction_edit_' . $transaction->id();

$tpl->assign('chart', $chart);

$form->runIf('save', function() use ($transaction, $session) {
	$transaction->importFromEditForm();
	$transaction->save();

	// Link members
	if (null !== f('users') && is_array(f('users'))) {
		$transaction->updateLinkedUsers(array_keys(f('users')));
	}
	else {







|







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$accounts = $chart->accounts();

$csrf_key = 'acc_transaction_edit_' . $transaction->id();

$tpl->assign('chart', $chart);

$form->runIf('save', function() use ($transaction, $session) {
	$transaction->importFromNewForm();
	$transaction->save();

	// Link members
	if (null !== f('users') && is_array(f('users'))) {
		$transaction->updateLinkedUsers(array_keys(f('users')));
	}
	else {