Overview
Comment:Merge from trunk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | templates
Files: files | file ages | folders
SHA3-256: 9db269ec895d6b050a91d8676d65730621b8db5a89e33e886d30f67f35950e43
User & Date: bohwaz on 2022-08-08 01:03:44
Other Links: branch diff | manifest | tags
Context
2022-08-08
01:05
Fix config in recu_don check-in: ff2c02522a user: bohwaz tags: templates
01:03
Merge from trunk check-in: 9db269ec89 user: bohwaz tags: templates
00:44
Fix calls for CommonModifiers::input check-in: cbabcb84cc user: bohwaz tags: templates
00:18
New stable release check-in: 018a9abcbb user: bohwaz tags: trunk, stable, 1.1.28
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/Accounting/Accounts.php from [fe703b65c6] to [af4b257ad1].

57
58
59
60
61
62
63
64
65

















66
67
68
69
70
71
72
73
		return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? AND type != 0 AND type NOT IN (?) ORDER BY code COLLATE U_NOCASE;',
			$this->chart_id, Account::TYPE_ANALYTICAL);
	}

	/**
	 * Return all accounts from current chart
	 */
	public function listAll(): array
	{

















		return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? ORDER BY code COLLATE U_NOCASE;',
			$this->chart_id);
	}

	public function listForCodes(array $codes): array
	{
		return DB::getInstance()->getGrouped('SELECT code, id, label FROM acc_accounts WHERE id_chart = ?;', $this->chart_id);
	}







|

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







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
		return $this->em->all('SELECT * FROM @TABLE WHERE id_chart = ? AND type != 0 AND type NOT IN (?) ORDER BY code COLLATE U_NOCASE;',
			$this->chart_id, Account::TYPE_ANALYTICAL);
	}

	/**
	 * Return all accounts from current chart
	 */
	public function listAll(?array $targets = null): array
	{
		$where = '';

		if (!empty($targets)) {
			$position = null;

			if (in_array(Account::TYPE_EXPENSE, $targets)) {
				$position = Account::EXPENSE;
			}
			elseif (in_array(Account::TYPE_REVENUE, $targets)) {
				$position = Account::REVENUE;
			}

			if ($position) {
				$where = sprintf('AND position = %d', $position);
			}
		}

		return $this->em->all(sprintf('SELECT * FROM @TABLE WHERE id_chart = ? %s ORDER BY code COLLATE U_NOCASE;', $where),
			$this->chart_id);
	}

	public function listForCodes(array $codes): array
	{
		return DB::getInstance()->getGrouped('SELECT code, id, label FROM acc_accounts WHERE id_chart = ?;', $this->chart_id);
	}

Modified src/include/lib/Garradin/Accounting/Transactions.php from [dfd1586b01] to [bbfbff1264].

9
10
11
12
13
14
15







16
17
18
19
20
21
22
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{







	static public function get(int $id)
	{
		return EntityManager::findOneById(Transaction::class, $id);
	}

	static public function saveReconciled(\Generator $journal, ?array $checked)
	{







>
>
>
>
>
>
>







9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{
	static public function create(array $data)
	{
		$transaction = new Transaction;
		$transaction->importForm($data);
		return $transaction;
	}

	static public function get(int $id)
	{
		return EntityManager::findOneById(Transaction::class, $id);
	}

	static public function saveReconciled(\Generator $journal, ?array $checked)
	{

Modified src/include/lib/Garradin/Config.php from [0e94a62511] to [b87367aea6].

137
138
139
140
141
142
143
144
145
146
147
148
149

150

151
152
153
154
155
156
157
		}

		$this->load($config);

		$this->champs_membres = new Membres\Champs((string)$this->champs_membres);
	}

	public function save(): bool
	{
		if (!count($this->_modified)) {
			return true;
		}


		$this->selfCheck();


		$values = $this->modifiedProperties(true);

		$db = DB::getInstance();
		$db->begin();

		foreach ($values as $key => $value)







|





>
|
>







137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
		}

		$this->load($config);

		$this->champs_membres = new Membres\Champs((string)$this->champs_membres);
	}

	public function save(bool $selfcheck = true): bool
	{
		if (!count($this->_modified)) {
			return true;
		}

		if ($selfcheck) {
			$this->selfCheck();
		}

		$values = $this->modifiedProperties(true);

		$db = DB::getInstance();
		$db->begin();

		foreach ($values as $key => $value)

Modified src/include/lib/Garradin/DynamicList.php from [69e24b02bd] to [adb683fc90].

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
	}

	public function getHeaderColumns(bool $label_only = false)
	{
		$columns = [];

		foreach ($this->columns as $alias => $properties) {
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order && !$this->desc)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}







|







129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
	}

	public function getHeaderColumns(bool $label_only = false)
	{
		$columns = [];

		foreach ($this->columns as $alias => $properties) {
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
	public function iterate(bool $include_hidden = true)
	{
		$start = ($this->page - 1) * $this->per_page;
		$columns = [];

		foreach ($this->columns as $alias => $properties) {
			// Skip columns that require a certain order (eg. calculating a running sum)
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order && !$this->desc)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}







|







155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
	public function iterate(bool $include_hidden = true)
	{
		$start = ($this->page - 1) * $this->per_page;
		$columns = [];

		foreach ($this->columns as $alias => $properties) {
			// Skip columns that require a certain order (eg. calculating a running sum)
			if (isset($properties['only_with_order']) && !($properties['only_with_order'] == $this->order)) {
				continue;
			}

			// Skip columns that require a certain order AND paginated result
			if (isset($properties['only_with_order']) && $this->page > 1) {
				continue;
			}

Modified src/include/lib/Garradin/Entities/Accounting/Account.php from [283323d3dd] to [05331b225e].

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
249
250
251
		'label'       => 'string',
		'description' => '?string',
		'position'    => 'int',
		'type'        => 'int',
		'user'        => 'int',
	];

	protected $_form_rules = [
		'code'        => 'required|string|alpha_num|max:10',
		'label'       => 'required|string|max:200',
		'description' => 'string|max:2000',
	];

	protected $_position = [];

	public function selfCheck(): void
	{
		$db = DB::getInstance();









		$this->assert(!empty($this->id_chart), 'Aucun plan comptable lié');

		$where = 'code = ? AND id_chart = ?';
		$where .= $this->exists() ? sprintf(' AND id != %d', $this->id()) : '';

		if ($db->test(self::TABLE, $where, $this->code, $this->id_chart)) {
			throw new ValidationException(sprintf('Le code "%s" est déjà utilisé par un autre compte.', $this->code));
		}

		$this->assert(array_key_exists($this->type, self::TYPES_NAMES), 'Type invalide');
		$this->assert(array_key_exists($this->position, self::POSITIONS_NAMES), 'Position invalide');
		$this->assert($this->user === 0 || $this->user === 1);

		parent::selfCheck();
	}

	public function listJournal(int $year_id, bool $simple = false, ?DateTimeInterface $start = null, ?DateTimeInterface $end = null)
	{
		$db = DB::getInstance();
		$columns = self::LIST_COLUMNS;

		$tables = 'acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			LEFT JOIN acc_accounts b ON b.id = l.id_analytical';
		$conditions = sprintf('l.id_account = %d AND t.id_year = %d', $this->id(), $year_id);

		$sum = 0;
		$reverse = $this->isReversed($simple, $year_id) ? -1 : 1;

		if ($start) {
			$conditions .= sprintf(' AND t.date >= %s', $db->quote($start->format('Y-m-d')));
			$sum = $this->getSumAtDate($year_id, $start) * $reverse;
		}

		if ($end) {
			$conditions .= sprintf(' AND t.date <= %s', $db->quote($end->format('Y-m-d')));
		}

		$columns['change']['select'] = sprintf($columns['change']['select'], $reverse);

		if ($simple) {
			unset($columns['debit']['label'], $columns['credit']['label'], $columns['line_label']);
			$columns['line_reference']['label'] = 'Réf. paiement';
		}
		else {
			unset($columns['change']['label']);
		}

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', false);
		$list->setCount('COUNT(*)');
		$list->setPageSize(null);
		$list->setModifier(function (&$row) use (&$sum) {
			if (property_exists($row, 'sum')) {









				$sum += $row->change;


				$row->sum = $sum;




			}

			$row->date = \DateTime::createFromFormat('!Y-m-d', $row->date);
		});

		$list->setExportCallback(function (&$row) {
			static $columns = ['change', 'sum', 'credit', 'debit'];
			foreach ($columns as $key) {
				if (isset($row->$key)) {
					$row->$key = Utils::money_format($row->$key, '.', '', false);
				}
			}







<
<
<
<
<
<






>
>
>
>
>
>
>
>









|
















|




<

















|

|
|

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

>
>
>
>




>







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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
		'label'       => 'string',
		'description' => '?string',
		'position'    => 'int',
		'type'        => 'int',
		'user'        => 'int',
	];







	protected $_position = [];

	public function selfCheck(): void
	{
		$db = DB::getInstance();

		$this->assert(trim($this->code) !== '', 'Le numéro de compte ne peut rester vide.');
		$this->assert(trim($this->label) !== '', 'L\'intitulé de compte ne peut rester vide.');

		$this->assert(strlen($this->code) <= 20, 'Le numéro de compte ne peut faire plus de 20 caractères.');
		$this->assert(preg_match('/^[a-z0-9_]+$/i', $this->code), 'Le numéro de compte ne peut comporter que des lettres et des chiffres.');
		$this->assert(strlen($this->label) <= 200, 'L\'intitulé de compte ne peut faire plus de 200 caractères.');
		$this->assert(!isset($this->description) || strlen($this->description) <= 2000, 'La description de compte ne peut faire plus de 2000 caractères.');

		$this->assert(!empty($this->id_chart), 'Aucun plan comptable lié');

		$where = 'code = ? AND id_chart = ?';
		$where .= $this->exists() ? sprintf(' AND id != %d', $this->id()) : '';

		if ($db->test(self::TABLE, $where, $this->code, $this->id_chart)) {
			throw new ValidationException(sprintf('Le code "%s" est déjà utilisé par un autre compte.', $this->code));
		}

		$this->assert(array_key_exists($this->type, self::TYPES_NAMES), 'Type invalide: ' . $this->type);
		$this->assert(array_key_exists($this->position, self::POSITIONS_NAMES), 'Position invalide');
		$this->assert($this->user === 0 || $this->user === 1);

		parent::selfCheck();
	}

	public function listJournal(int $year_id, bool $simple = false, ?DateTimeInterface $start = null, ?DateTimeInterface $end = null)
	{
		$db = DB::getInstance();
		$columns = self::LIST_COLUMNS;

		$tables = 'acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			LEFT JOIN acc_accounts b ON b.id = l.id_analytical';
		$conditions = sprintf('l.id_account = %d AND t.id_year = %d', $this->id(), $year_id);

		$sum = null;
		$reverse = $this->isReversed($simple, $year_id) ? -1 : 1;

		if ($start) {
			$conditions .= sprintf(' AND t.date >= %s', $db->quote($start->format('Y-m-d')));

		}

		if ($end) {
			$conditions .= sprintf(' AND t.date <= %s', $db->quote($end->format('Y-m-d')));
		}

		$columns['change']['select'] = sprintf($columns['change']['select'], $reverse);

		if ($simple) {
			unset($columns['debit']['label'], $columns['credit']['label'], $columns['line_label']);
			$columns['line_reference']['label'] = 'Réf. paiement';
		}
		else {
			unset($columns['change']['label']);
		}

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', true);
		$list->setCount('COUNT(*)');
		$list->setPageSize(null); // Because with paging we can't calculate the running sum
		$list->setModifier(function (&$row) use (&$sum, &$list, $reverse, $year_id, $start, $end) {
			if (property_exists($row, 'sum')) {
				// Reverse running sum needs the last sum, first
				if ($list->desc && null === $sum) {
					$sum = $this->getSumAtDate($year_id, ($end ?? new \DateTime($row->date))->modify('+1 day')) * -1 * $reverse;
				}
				elseif (!$list->desc) {
					if (null === $sum && $start) {
						$sum = $this->getSumAtDate($year_id, $start) * -1 * $reverse;
					}

					$sum += $row->change;
				}

				$row->sum = $sum;

				if ($list->desc) {
					$sum -= $row->change;
				}
			}

			$row->date = \DateTime::createFromFormat('!Y-m-d', $row->date);
		});

		$list->setExportCallback(function (&$row) {
			static $columns = ['change', 'sum', 'credit', 'debit'];
			foreach ($columns as $key) {
				if (isset($row->$key)) {
					$row->$key = Utils::money_format($row->$key, '.', '', false);
				}
			}
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
	}

	public function chart(): Chart
	{
		return Charts::get($this->id_chart);
	}

	public function save(): bool
	{
		$c = Config::getInstance();
		$c->set('last_chart_change', time());
		$c->save();

		return parent::save();
	}
}







|





|


442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
	}

	public function chart(): Chart
	{
		return Charts::get($this->id_chart);
	}

	public function save(bool $selfcheck = true): bool
	{
		$c = Config::getInstance();
		$c->set('last_chart_change', time());
		$c->save();

		return parent::save($selfcheck);
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Chart.php from [297b8e2f31] to [7d108c460e].

23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38



39
40

41
42
43
44
45
46
47
48
49
50
51
52
		'id'       => 'int',
		'label'    => 'string',
		'country'  => 'string',
		'code'     => '?string',
		'archived' => 'int',
	];

	protected $_form_rules = [
		'label'    => 'required|string|max:200',
		'country'  => 'required|string|size:2',
		'archived' => 'numeric|min:0|max:1'
	];

	public function selfCheck(): void
	{
		parent::selfCheck();



		$this->assert(Utils::getCountryName($this->country), 'Le code pays doit être un code ISO valide');
		$this->assert($this->archived === 0 || $this->archived === 1);

	}

	public function accounts()
	{
		return new Accounts($this->id());
	}

	public function canDelete()
	{
		return !DB::getInstance()->firstColumn(sprintf('SELECT 1 FROM %s WHERE id_chart = ? LIMIT 1;', Year::TABLE), $this->id());
	}
}







<
<
<
<
<
<


<
>
>
>


>












23
24
25
26
27
28
29






30
31

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
		'id'       => 'int',
		'label'    => 'string',
		'country'  => 'string',
		'code'     => '?string',
		'archived' => 'int',
	];







	public function selfCheck(): void
	{

		$this->assert(trim($this->label) !== '', 'Le libellé ne peut rester vide.');
		$this->assert(strlen($this->label) <= 200, 'Le libellé ne peut faire plus de 200 caractères.');
		$this->assert(trim($this->country) !== '', 'Le pays ne peut rester vide.');
		$this->assert(Utils::getCountryName($this->country), 'Le code pays doit être un code ISO valide');
		$this->assert($this->archived === 0 || $this->archived === 1);
		parent::selfCheck();
	}

	public function accounts()
	{
		return new Accounts($this->id());
	}

	public function canDelete()
	{
		return !DB::getInstance()->firstColumn(sprintf('SELECT 1 FROM %s WHERE id_chart = ? LIMIT 1;', Year::TABLE), $this->id());
	}
}

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

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
		'debit'          => 'int',
		'reference'      => '?string',
		'label'          => '?string',
		'reconciled'     => 'int',
		'id_analytical'  => '?int',
	];

	protected $_form_rules = [
		'id_account'     => 'required|numeric|in_table:acc_accounts,id',
		'id_analytical'  => 'numeric|in_table:acc_accounts,id',
		'reference'      => 'string|max:200',
		'label'          => 'string|max:200',
	];

	static public function create(int $id_account, int $credit, int $debit, ?string $label = null, ?string $reference = null): Line
	{
		$line = new self;
		$line->id_account = $id_account;
		$line->credit = $credit;
		$line->debit = $debit;
		$line->label = $label;







<
<
<
<
<
<
<







30
31
32
33
34
35
36







37
38
39
40
41
42
43
		'debit'          => 'int',
		'reference'      => '?string',
		'label'          => '?string',
		'reconciled'     => 'int',
		'id_analytical'  => '?int',
	];








	static public function create(int $id_account, int $credit, int $debit, ?string $label = null, ?string $reference = null): Line
	{
		$line = new self;
		$line->id_account = $id_account;
		$line->credit = $credit;
		$line->debit = $debit;
		$line->label = $label;
65
66
67
68
69
70
71


72



73
74
75
76
77




78
79
80
81
82
83
84
85
86
87
88
89
90

		$value = parent::filterUserValue($type, $value, $key);

		return $value;
	}

	public function selfCheck(): void
	{


		parent::selfCheck();



		$this->assert($this->credit || $this->debit, 'Aucun montant au débit ou au crédit');
		$this->assert($this->credit >= 0 && $this->debit >= 0, 'Le montant ne peut être négatif');
		$this->assert(($this->credit * $this->debit) === 0 && ($this->credit + $this->debit) > 0, 'Ligne non équilibrée : crédit ou débit doit valoir zéro.');
		$this->assert($this->id_transaction, 'Aucun mouvement n\'a été indiqué pour cette ligne.');
		$this->assert($this->reconciled === 0 || $this->reconciled === 1);




	}

	public function asDetailsArray(): array
	{
		return [
			'Compte'    => $this->id_account ? Accounts::getSelectorLabel($this->id_account) : null,
			'Libellé'   => $this->label,
			'Référence' => $this->reference,
			'Crédit'    => Utils::money_format($this->credit),
			'Débit'     => Utils::money_format($this->debit),
		];
	}
}








>
>
|
>
>
>



<

>
>
>
>












|
>
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
		$value = parent::filterUserValue($type, $value, $key);

		return $value;
	}

	public function selfCheck(): void
	{
		// We don't check that the account exists here
		// The fact that the account is in the right chart is checked in Transaction::selfCheck

		$this->assert($this->reference === null || strlen($this->reference) < 200, 'La référence doit faire moins de 200 caractères.');
		$this->assert($this->label === null || strlen($this->label) < 200, 'La référence doit faire moins de 200 caractères.');
		$this->assert($this->id_account !== null, 'Aucun compte n\'a été indiqué.');
		$this->assert($this->credit || $this->debit, 'Aucun montant au débit ou au crédit');
		$this->assert($this->credit >= 0 && $this->debit >= 0, 'Le montant ne peut être négatif');
		$this->assert(($this->credit * $this->debit) === 0 && ($this->credit + $this->debit) > 0, 'Ligne non équilibrée : crédit ou débit doit valoir zéro.');

		$this->assert($this->reconciled === 0 || $this->reconciled === 1);

		$this->assert(null === $this->id_analytical || DB::getInstance()->test(Account::TABLE, 'id = ?', $this->id_analytical), 'Le projet analytique indiqué n\'existe pas.');
		$this->assert(!empty($this->id_transaction), 'Aucune écriture n\'a été indiquée pour cette ligne.');
		parent::selfCheck();
	}

	public function asDetailsArray(): array
	{
		return [
			'Compte'    => $this->id_account ? Accounts::getSelectorLabel($this->id_account) : null,
			'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 [456b2e7ada] to [01b7b4476e].

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
		'Dépense',
		'Virement',
		'Dette',
		'Créance',
	];

	protected $id;
	protected $type;
	protected $status = 0;
	protected $label;
	protected $notes;
	protected $reference;

	protected $date;








|







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
		'Dépense',
		'Virement',
		'Dette',
		'Créance',
	];

	protected $id;
	protected $type = null;
	protected $status = 0;
	protected $label;
	protected $notes;
	protected $reference;

	protected $date;

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

160
161


162
163
164
165
166
167

168
169
170
171
172
173
174
			case Account::TYPE_OUTSTANDING:
				return self::TYPE_TRANSFER;
			default:
				return self::TYPE_ADVANCED;
		}
	}

	/**
	 * @param  bool $restrict_year Set to TRUE to only return lines linked to the correct chart, or FALSE (deprecated/legacy) to return all lines even if they are linked to accounts in the wrong chart!
	 */
	public function getLinesWithAccounts(bool $restrict_year = true): array
	{
		$db = EntityManager::getInstance(Line::class)->DB();

		if ($restrict_year === false) {
			$restrict = $restrict_year ? 'AND a.id_chart = y.id_chart' : '';
			$sql = sprintf('SELECT
				l.*, a.label AS account_label, a.code AS account_code,
				b.label AS analytical_name
				FROM acc_transactions_lines l
				INNER JOIN acc_accounts a ON a.id = l.id_account %s
				INNER JOIN acc_transactions t ON t.id = l.id_transaction
				INNER JOIN acc_years y ON y.id = t.id_year
				LEFT JOIN acc_accounts b ON b.id = l.id_analytical
				WHERE l.id_transaction = %d ORDER BY l.id;', $restrict, $this->id());

			return $db->get($sql);
		}
		else {
			// Merge data from accounts with lines
			$accounts = [];
			$lines_with_accounts = [];

			foreach ($this->getLines() as $line) {
				if (!array_key_exists($line->id_account, $this->_accounts)) {
					$accounts[] = $line->id_account;
				}

				if ($line->id_analytical && !array_key_exists($line->id_analytical, $this->_accounts)) {
					$accounts[] = $line->id_analytical;
				}
			}

			// Remove NULL accounts
			$accounts = array_filter($accounts);

			if (count($accounts)) {
				$sql = sprintf('SELECT id, label, code FROM acc_accounts WHERE %s;', $db->where('id', 'IN', $accounts));
				$this->_accounts += $db->getGrouped($sql);
			}

			foreach ($this->getLines() as $line) {
				$account = [
					'account_code' => $this->_accounts[$line->id_account]->code ?? null,
					'account_label' => $this->_accounts[$line->id_account]->label ?? null,

					'analytical_name' => $line->id_analytical ? $this->_accounts[$line->id_analytical]->label : null,
				];



				$lines_with_accounts[] = (object) ($line->asArray() + $account);
			}

			return $lines_with_accounts;
		}

	}

	public function getLines(): array
	{
		if (null === $this->_lines && $this->exists()) {
			$em = EntityManager::getInstance(Line::class);
			$this->_lines = $em->all('SELECT * FROM @TABLE WHERE id_transaction = ? ORDER BY id;', $this->id);







<
<
<
|



<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|

|
|
|
|

|
|
|
|

|
|

|
|
|
|

|
|
|
|
>
|
<
>
>

|
|

|
|
>







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
132
133
134
135
136
137
138
139
140
141
142
143

144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
			case Account::TYPE_OUTSTANDING:
				return self::TYPE_TRANSFER;
			default:
				return self::TYPE_ADVANCED;
		}
	}




	public function getLinesWithAccounts(): array
	{
		$db = EntityManager::getInstance(Line::class)->DB();
















		// Merge data from accounts with lines
		$accounts = [];
		$lines_with_accounts = [];

		foreach ($this->getLines() as $line) {
			if (!array_key_exists($line->id_account, $this->_accounts)) {
				$accounts[] = $line->id_account;
			}

			if ($line->id_analytical && !array_key_exists($line->id_analytical, $this->_accounts)) {
				$accounts[] = $line->id_analytical;
			}
		}

		// Remove NULL accounts
		$accounts = array_filter($accounts);

		if (count($accounts)) {
			$sql = sprintf('SELECT id, label, code, position FROM acc_accounts WHERE %s;', $db->where('id', 'IN', $accounts));
			$this->_accounts = $this->_accounts + $db->getGrouped($sql);
		}

		foreach ($this->getLines() as &$line) {
			$l = (object) $line->asArray();
			$l->account_code = $this->_accounts[$line->id_account]->code ?? null;
			$l->account_label = $this->_accounts[$line->id_account]->label ?? null;
			$l->account_position = $this->_accounts[$line->id_account]->position ?? null;
			$l->analytical_name = $this->_accounts[$line->id_analytical]->label ?? null;

			$l->account_selector = [$line->id_account => sprintf('%s — %s', $l->account_code, $l->account_label)];
			$l->line =& $line;

			$lines_with_accounts[] = $l;
		}

		unset($line);

		return $lines_with_accounts;
	}

	public function getLines(): array
	{
		if (null === $this->_lines && $this->exists()) {
			$em = EntityManager::getInstance(Line::class);
			$this->_lines = $em->all('SELECT * FROM @TABLE WHERE id_transaction = ? ORDER BY id;', $this->id);
246
247
248
249
250
251
252





































253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307

		foreach ($this->getLines() as $line) {
			$sum += $line->debit;
		}

		return $sum;
	}






































	public function getAnalyticalId(): ?int
	{
		$lines = $this->getLines();

		if (!count($lines)) {
			return null;
		}

		return current($lines)->id_analytical;
	}

	public function related(): ?Transaction
	{
		return $this->_related;
	}

	public function getTypesAccounts()
	{
		if ($this->type == self::TYPE_ADVANCED) {
			return [];
		}

		$debit = null;
		$credit = null;

		$lines = $this->getLinesWithAccounts();

		foreach ($lines as $line) {
			$account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_label)];

			if ($line->debit) {
				$debit = $account;
			}
			else {
				$credit = $account;
			}
		}

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

		return [
			$type->accounts[0]->position == 'credit' ? $credit : $debit,
			$type->accounts[1]->position == 'credit' ? $credit : $debit,
		];
	}

	/**duplic
	 * Creates a new Transaction entity (not saved) from an existing one,
	 * trying to adapt to a different chart if possible
	 * @param  int    $id
	 * @param  Year   $year Target year
	 * @return Transaction
	 */
	public function duplicate(Year $year): Transaction







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

















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|







231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291






























292
293
294
295
296
297
298
299

		foreach ($this->getLines() as $line) {
			$sum += $line->debit;
		}

		return $sum;
	}

	static public function getFormLines(?array $source = null): array
	{
		if (null === $source) {
			$source = $_POST['lines'] ?? [];
		}

		if (empty($source) || !is_array($source)) {
			return [];
		}

		$lines = Utils::array_transpose($source);

		foreach ($lines as &$line) {
			if (isset($line['credit'])) {
				$line['credit'] = Utils::moneyToInteger($line['credit']);
			}
			if (isset($line['debit'])) {
				$line['debit'] = Utils::moneyToInteger($line['debit']);
			}
		}

		unset($line);

		return $lines;
	}

	public function hasReconciledLines(): bool
	{
		foreach ($this->getLines() as $line) {
			if (!empty($line->reconciled)) {
				return true;
			}
		}

		return false;
	}

	public function getAnalyticalId(): ?int
	{
		$lines = $this->getLines();

		if (!count($lines)) {
			return null;
		}

		return current($lines)->id_analytical;
	}

	public function related(): ?Transaction
	{
		return $this->_related;
	}































	/**
	 * Creates a new Transaction entity (not saved) from an existing one,
	 * trying to adapt to a different chart if possible
	 * @param  int    $id
	 * @param  Year   $year Target year
	 * @return Transaction
	 */
	public function duplicate(Year $year): Transaction
412
413
414
415
416
417
418
419
420







421
422
423
424
425
426
427
428
429
430





































431
432
433
434
435
436

437
438
439
440
441
442
443
444
445
446


447
448
449
450
451
452
453
			$sum += $line->credit;
			// Because credit == debit, we only use credit
		}

		return $sum;
	}

	public function save(): bool
	{







		if ($this->validated && !(isset($this->_modified['validated']) && $this->_modified['validated'] === 0)) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été validée');
		}

		$db = DB::getInstance();

		if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) {
			throw new ValidationException('Il n\'est pas possible de créer ou modifier une écriture dans un exercice clôturé');
		}






































		if (!parent::save()) {
			return false;
		}

		foreach ($this->getLines() as $line)
		{

			$line->id_transaction = $this->id();
			$line->save();
		}

		foreach ($this->_old_lines as $line)
		{
			if ($line->exists()) {
				$line->delete();
			}
		}



		return true;
	}

	public function removeStatus(int $property) {
		$this->set('status', $this->status & ~$property);
	}







|

>
>
>
>
>
>
>










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




|
<
>

|


|
<




>
>







404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471

472
473
474
475
476
477

478
479
480
481
482
483
484
485
486
487
488
489
490
			$sum += $line->credit;
			// Because credit == debit, we only use credit
		}

		return $sum;
	}

	public function save(bool $selfcheck = true): bool
	{
		if ($this->type == self::TYPE_DEBT || $this->type == self::TYPE_CREDIT) {
			// Debts and credits add a waiting status
			if (!$this->exists()) {
				$this->addStatus(self::STATUS_WAITING);
			}
		}

		if ($this->validated && !(isset($this->_modified['validated']) && $this->_modified['validated'] === 0)) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui a été validée');
		}

		$db = DB::getInstance();

		if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) {
			throw new ValidationException('Il n\'est pas possible de créer ou modifier une écriture dans un exercice clôturé');
		}

		$this->selfCheck();

		$lines = $this->getLinesWithAccounts();

		// Self check lines before saving Transaction
		foreach ($lines as $i => $l) {
			$line = $l->line;
			$line->id_transaction = -1; // Get around validation of id_transaction being not null

			if (empty($l->account_code)) {
				throw new ValidationException('Le compte spécifié n\'existe pas.');
			}

			if ($this->type == self::TYPE_EXPENSE && $l->account_position == Account::REVENUE) {
				throw new ValidationException('Il n\'est pas possible d\'attribuer un compte de produit à une dépense');
			}

			if ($this->type == self::TYPE_REVENUE && $l->account_position == Account::EXPENSE) {
				throw new ValidationException('Il n\'est pas possible d\'attribuer un compte de dépense à une recette');
			}

			try {
				$line->selfCheck();
			}
			catch (ValidationException $e) {
				// Add line number to message
				throw new ValidationException(sprintf('Ligne %d : %s', $i+1, $e->getMessage()), 0, $e);
			}
		}

		if ($this->exists() && $this->status & self::STATUS_ERROR) {
			// Remove error status when changed
			$this->removeStatus(self::STATUS_ERROR);
		}

		$db->begin();

		if (!parent::save()) {
			return false;
		}

		foreach ($lines as $line) {

			$line = $line->line; // Fetch real object
			$line->id_transaction = $this->id();
			$line->save(false);
		}

		foreach ($this->_old_lines as $line) {

			if ($line->exists()) {
				$line->delete();
			}
		}

		$db->commit();

		return true;
	}

	public function removeStatus(int $property) {
		$this->set('status', $this->status & ~$property);
	}
506
507
508
509
510
511
512


513
514
515
516
517
518
519
		$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;







>
>







543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
		$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;
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612


613















































614



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632


633
634
635
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
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
748
749
750
751
752
753
754
755
756
757
			$source = $_POST;
		}

		if (isset($source['id_related']) && empty($source['id_related'])) {
			$source['id_related'] = null;
		}

		return parent::importForm($source);
	}

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

		if (!isset($this->type) && !isset($source['type'])) {
			$source['type'] = self::TYPE_ADVANCED;
		}

		$type = $source['type'];

		$this->importForm($source);

		// Remove error status when changed
		$this->removeStatus(self::STATUS_ERROR);

		if (!isset($source['lines']) || !is_array($source['lines'])) {
			return;
		}

		if (self::TYPE_ADVANCED == $type) {
			try {
				$lines = Utils::array_transpose($source['lines']);
			}
			catch (\InvalidArgumentException $e) {
				throw new ValidationException('Aucun compte sélectionné pour certaines lignes.');
			}



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















































				if (empty($line['account']) || !is_array($line['account']) || !count($line['account'])) {



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

				$line['id_account'] = key($line['account']);

				$line = (new Line)->import($line);
				$this->addLine($line);
			}
		}
		else {
			$details = self::getTypesDetails();

			if (!array_key_exists($type, $details)) {
				throw new ValidationException('Type d\'écriture inconnu');
			}

			if (!empty($this->_related) && ($type == self::TYPE_DEBT || $type == self::TYPE_CREDIT)) {
				$this->set('type', self::TYPE_ADVANCED);


			}
			elseif (!$this->exists() && ($type == self::TYPE_DEBT || $type == self::TYPE_CREDIT)) {
				$this->addStatus(self::STATUS_WAITING);
			}

			if (empty($source['amount'])) {
				throw new UserException('Montant non précisé');
			}

			$amount = $source['amount'];

			// Fill lines using a pre-defined setup obtained from getTypesDetails
			foreach ($details[$type]->accounts as $k => $account) {
				$credit = $account->position == 'credit' ? $amount : 0;
				$debit = $account->position == 'debit' ? $amount : 0;

				$key = sprintf('account_%d_%d', $type, $k);

				if (empty($source[$key]) || !count($source[$key])) {
					throw new ValidationException(sprintf('Ligne %d : aucun compte n\'a été sélectionné', $k+1));
				}

				$account = key($source[$key]);

				$line = new Line;
				$line->importForm([
					'reference'     => !empty($source['payment_reference']) ? $source['payment_reference'] : null,
					'credit'        => $credit,
					'debit'         => $debit,
					'id_account'    => $account,
					'id_analytical' => !empty($source['id_analytical']) ? $source['id_analytical'] : null,
				]);
				$this->addLine($line);
			}
		}
	}



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

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




		if (isset($source['lines'])) {



			$this->resetLines();

		}



































































		$this->importFromNewForm($source);
	}

	public function importFromBalanceForm(Year $year, ?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (!isset($source['lines']) || !is_array($source['lines'])) {
			throw new ValidationException('Aucun contenu trouvé dans le formulaire.');
		}

		$this->label = 'Balance d\'ouverture';
		$this->date = $year->start_date;
		$this->id_year = $year->id();
		$this->type = self::TYPE_ADVANCED;

		try {
			$lines = Utils::array_transpose($source['lines']);
		}
		catch (\InvalidArgumentException $e) {
			throw new ValidationException('Aucun compte sélectionné pour certaines lignes.');
		}

		$debit = $credit = 0;

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

			$line['id_account'] = key($line['account']);

			try {
				$line = (new Line)->importForm($line);
				$this->addLine($line);
			}
			catch (ValidationException $e) {
				throw new ValidationException(sprintf('Ligne %d : %s', $k+1, $e->getMessage()), 0, $e);
			}

			$debit += $line->debit;
			$credit += $line->credit;
		}

		if ($debit != $credit) {
			// Add final balance line
			$line = new Line;

			if ($debit > $credit) {
				$line->credit = $debit - $credit;
			}
			else {
				$line->debit = $credit - $debit;
			}

			$open_account = EntityManager::findOne(Account::class, 'SELECT * FROM @TABLE WHERE id_chart = ? AND type = ? LIMIT 1;', $year->id_chart, Account::TYPE_OPENING);

			if (!$open_account) {
				throw new ValidationException('Aucun compte usuel de bilan d\'ouverture n\'existe dans le plan comptable');
			}

			$line->id_account = $open_account->id();

			$this->addLine($line);
		}
	}

	public function year()
	{
		return EntityManager::findOneById(Year::class, $this->id_year);
	}








<
<
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

|





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



|
|
<
<
|
<
|
<
|
<
|
|

<
<
>
>
|
<
<
<
<
|
|
|
|
<
<
<
<
<
<

<
<
<
<
<
<
<
<
|
|
<
<
<
<
<
<
|


|
>
|
>
|









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










<
<
<
<





<
|
|
<
<
<
|
<

<
<
<
<
|
<
|
<
<
<
|
<
<
|
<
<
<
<
<
<
|
|

|
|
|
|
|
|

|

|
|
|

|

|
<







613
614
615
616
617
618
619


620





621
















622
623
624
625
626
627
628
629
630
631
632
633
634
635
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
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
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806




807
808
809
810
811

812
813



814

815




816

817



818


819






820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838

839
840
841
842
843
844
845
			$source = $_POST;
		}

		if (isset($source['id_related']) && empty($source['id_related'])) {
			$source['id_related'] = null;
		}



		// Transpose lines (HTML transaction forms)





		if (!empty($source['lines']) && is_array($source['lines']) && is_string(key($source['lines']))) {
















			try {
				$source['lines'] = Utils::array_transpose($source['lines']);
			}
			catch (\InvalidArgumentException $e) {
				throw new ValidationException('Aucun compte sélectionné pour certaines lignes.');
			}

			unset($source['_lines']);
		}

		if (isset($source['type'])) {
			$this->set('type', (int)$source['type']);
		}

		// Simple two-lines transaction
		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
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($this->_related)) {
			throw new \LogicException('Cannot import pay-off if no related transaction is set');
		}

		// Just make sure we can't trigger importFromNewForm
		unset($source['type'], $source['lines']);

		if (empty($source['amount'])) {
			throw new ValidationException('Montant non précisé');
		}

		if (empty($source['account']) || !is_array($source['account'])) {
			throw new ValidationException('Aucun compte de règlement sélectionné.');
		}

		$id_account = null;
		// Reverse direction (compared with debt/credit transaction)
		$d1 = ($this->_related->type == self::TYPE_CREDIT) ? 'credit' : 'debit';
		$d2 = ($d1 == 'credit') ? 'debit' : 'credit';

		foreach ($this->_related->getLines() as $line) {
			if (($this->_related->type == self::TYPE_DEBT && $line->debit)
				|| ($this->_related->type == self::TYPE_CREDIT && $line->credit)) {
				// Skip the type of debt/credit, just keep the thirdparty account
				continue;
			}

			$id_account = $line->id_account;
			break;
		}

		if (!$id_account) {
			throw new \LogicException('Cannot find account ID of related transaction');
		}

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

		$source['lines'] = [
			// First line is third-party account
			$line + compact('id_account') + [$d1 => $source['amount']],
			// Second line is payment account
			$line + ['account_selector' => $source['account'], $d2 => $source['amount']],
		];

		$this->importFromNewForm($source);
	}

	public function importFromBalanceForm(Year $year, ?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}





		$this->label = 'Balance d\'ouverture';
		$this->date = $year->start_date;
		$this->id_year = $year->id();
		$this->type = self::TYPE_ADVANCED;


		$this->importFromNewForm($source);




		$diff = $this->getLinesCreditSum() - $this->getLinesDebitSum();






		if (!$diff) {

			return;



		}









		// Add final balance line
		$line = new Line;

		if ($diff > 0) {
			$line->debit = $diff;
		}
		else {
			$line->credit = abs($diff);
		}

		$open_account = EntityManager::findOne(Account::class, 'SELECT * FROM @TABLE WHERE id_chart = ? AND type = ? LIMIT 1;', $year->id_chart, Account::TYPE_OPENING);

		if (!$open_account) {
			throw new ValidationException('Aucun compte usuel de bilan d\'ouverture n\'existe dans le plan comptable');
		}

		$line->id_account = $open_account->id();

		$this->addLine($line);

	}

	public function year()
	{
		return EntityManager::findOneById(Year::class, $this->id_year);
	}

809
810
811
812
813
814
815



816
817




818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
















904
905
906
907
908
909
910

911











912




913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
	}

	public function listRelatedTransactions()
	{
		return EntityManager::getInstance(self::class)->all('SELECT * FROM @TABLE WHERE id_related = ?;', $this->id);
	}




	static public function getTypesDetails()
	{




		$details = [
			self::TYPE_REVENUE => [
				'accounts' => [
					[
						'label' => 'Type de recette',
						'targets' => [Account::TYPE_REVENUE],
						'position' => 'credit',
					],
					[
						'label' => 'Compte d\'encaissement',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_REVENUE],
			],
			self::TYPE_EXPENSE => [
				'accounts' => [
					[
						'label' => 'Type de dépense',
						'targets' => [Account::TYPE_EXPENSE],
						'position' => 'debit',
					],
					[
						'label' => 'Compte de décaissement',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'credit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_EXPENSE],
				'help' => null,
			],
			self::TYPE_TRANSFER => [
				'accounts' => [
					[
						'label' => 'De',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'credit',
					],
					[
						'label' => 'Vers',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'position' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_TRANSFER],
				'help' => 'Dépôt en banque, virement interne, etc.',
			],
			self::TYPE_DEBT => [
				'accounts' => [
					[
						'label' => 'Type de dette (dépense)',
						'targets' => [Account::TYPE_EXPENSE],
						'position' => 'debit',
					],
					[
						'label' => 'Compte de tiers',
						'targets' => [Account::TYPE_THIRD_PARTY],
						'position' => 'credit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_DEBT],
				'help' => 'Quand l\'association doit de l\'argent à un membre ou un fournisseur',
			],
			self::TYPE_CREDIT => [
				'accounts' => [
					[
						'label' => 'Type de créance (recette)',
						'targets' => [Account::TYPE_REVENUE],
						'position' => 'credit',
					],
					[
						'label' => 'Compte de tiers',
						'targets' => [Account::TYPE_THIRD_PARTY],
						'position' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_CREDIT],
				'help' => 'Quand un membre ou un fournisseur doit de l\'argent à l\'association',
			],
			self::TYPE_ADVANCED => [
				'accounts' => [],
				'label' => self::TYPES_NAMES[self::TYPE_ADVANCED],
				'help' => 'Choisir les comptes du plan comptable, ventiler une écriture sur plusieurs comptes, etc.',
			],
		];

















		foreach ($details as $key => &$type) {
			$type = (object) $type;
			$type->id = $key;
			foreach ($type->accounts as &$account) {
				$account = (object) $account;
				$account->targets_string = implode(':', $account->targets);

			}











		}





		return $details;
	}

	public function payOffFrom(int $id): ?\stdClass
	{
		$this->_related = EntityManager::findOneById(self::class, $id);

		if (!$this->_related) {
			return null;
		}

		$this->id_related = $this->_related->id();
		$this->label = ($this->_related->type == Transaction::TYPE_DEBT ? 'Règlement de dette : ' : 'Règlement de créance : ') . $this->_related->label;
		$this->type = $this->_related->type;

		$out = (object) [
			'id' => $this->_related->id,
			'sum' => $this->_related->sum(),
			'id_account' => null,
			// input name for the input containing the account ID for the expense/revenue from the related transaction
			'form_account_name' => sprintf('account_%d_%d', $this->type, 0),
			// input name for the account selector, for selecting a bank/cash account
			'form_target_name' => sprintf('account_%d_%d', $this->type, 1),
			'id_analytical' => $this->_related->getAnalyticalId(),
		];

		foreach ($this->_related->getLines() as $line) {
			if (($this->_related->type == self::TYPE_DEBT && $line->debit)
				|| ($this->_related->type == self::TYPE_CREDIT && $line->credit)) {
				// Skip the type of debt/credit, just keep the thirdparty account
				continue;
			}

			$out->id_account = $line->id_account;
			break;
		}

		return $out;
	}

	public function getTypeName(): string
	{
		return self::TYPES_NAMES[$this->type];
	}







>
>
>
|

>
>
>
>






|




|









|




|










|




|










|




|










|




|











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




|


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














|


|
|
<
<
<
<
<



<
<
<
<
<
<
<
<
<
<
<







897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058





1059
1060
1061











1062
1063
1064
1065
1066
1067
1068
	}

	public function listRelatedTransactions()
	{
		return EntityManager::getInstance(self::class)->all('SELECT * FROM @TABLE WHERE id_related = ?;', $this->id);
	}

	/**
	 * Return tuples of accounts selectors according to each "simplified" type
	 */
	public function getTypesDetails(?array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		$details = [
			self::TYPE_REVENUE => [
				'accounts' => [
					[
						'label' => 'Type de recette',
						'targets' => [Account::TYPE_REVENUE],
						'direction' => 'credit',
					],
					[
						'label' => 'Compte d\'encaissement',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'direction' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_REVENUE],
			],
			self::TYPE_EXPENSE => [
				'accounts' => [
					[
						'label' => 'Type de dépense',
						'targets' => [Account::TYPE_EXPENSE],
						'direction' => 'debit',
					],
					[
						'label' => 'Compte de décaissement',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'direction' => 'credit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_EXPENSE],
				'help' => null,
			],
			self::TYPE_TRANSFER => [
				'accounts' => [
					[
						'label' => 'De',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'direction' => 'credit',
					],
					[
						'label' => 'Vers',
						'targets' => [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING],
						'direction' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_TRANSFER],
				'help' => 'Dépôt en banque, virement interne, etc.',
			],
			self::TYPE_DEBT => [
				'accounts' => [
					[
						'label' => 'Type de dette (dépense)',
						'targets' => [Account::TYPE_EXPENSE],
						'direction' => 'debit',
					],
					[
						'label' => 'Compte de tiers',
						'targets' => [Account::TYPE_THIRD_PARTY],
						'direction' => 'credit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_DEBT],
				'help' => 'Quand l\'association doit de l\'argent à un membre ou un fournisseur',
			],
			self::TYPE_CREDIT => [
				'accounts' => [
					[
						'label' => 'Type de créance (recette)',
						'targets' => [Account::TYPE_REVENUE],
						'direction' => 'credit',
					],
					[
						'label' => 'Compte de tiers',
						'targets' => [Account::TYPE_THIRD_PARTY],
						'direction' => 'debit',
					],
				],
				'label' => self::TYPES_NAMES[self::TYPE_CREDIT],
				'help' => 'Quand un membre ou un fournisseur doit de l\'argent à l\'association',
			],
			self::TYPE_ADVANCED => [
				'accounts' => [],
				'label' => self::TYPES_NAMES[self::TYPE_ADVANCED],
				'help' => 'Choisir les comptes du plan comptable, ventiler une écriture sur plusieurs comptes, etc.',
			],
		];

		// Find out which lines are credit and debit
		$current_accounts = [];

		foreach ($this->getLinesWithAccounts() as $i => $l) {
			if ($l->debit) {
				$current_accounts[] = $l->account_selector;
			}
			elseif ($l->credit) {
				$current_accounts[] = $l->account_selector;
			}

			if (count($current_accounts) == 2) {
				break;
			}
		}

		foreach ($details as $key => &$type) {
			$type = (object) $type;
			$type->id = $key;
			foreach ($type->accounts as $i => &$account) {
				$account = (object) $account;
				$account->targets_string = implode(':', $account->targets);
				$account->selector_name = sprintf('simple[%s][%d]', $key, $i);

				// Try to find out if we can replicate the value
				// debt and credit can have same values, but not others
				// as it can lead to weird stuff
				// exception: revenue/expense can have the same payment account, no issue
				if (($type->id == $this->type)
					|| ($type->id == self::TYPE_CREDIT && $this->type == self::TYPE_DEBT)
					|| ($type->id == self::TYPE_DEBT && $this->type == self::TYPE_CREDIT)
					|| ($type->id == self::TYPE_REVENUE && $this->type == self::TYPE_EXPENSE && $i == 1)
					|| ($type->id == self::TYPE_EXPENSE && $this->type == self::TYPE_REVENUE && $i == 1)
				) {
					$account->selector_value = $source['simple'][$key][$i] ?? ($current_accounts[$i] ?? null);
				}
			}
		}

		unset($account, $type);

		return $details;
	}

	public function payOffFrom(int $id): ?\stdClass
	{
		$this->_related = EntityManager::findOneById(self::class, $id);

		if (!$this->_related) {
			return null;
		}

		$this->id_related = $this->_related->id();
		$this->label = ($this->_related->type == Transaction::TYPE_DEBT ? 'Règlement de dette : ' : 'Règlement de créance : ') . $this->_related->label;
		$this->type = self::TYPE_ADVANCED;

		$out = (object) [
			'id'            => $this->_related->id,
			'amount'        => $this->_related->sum(),





			'id_analytical' => $this->_related->getAnalyticalId(),
		];












		return $out;
	}

	public function getTypeName(): string
	{
		return self::TYPES_NAMES[$this->type];
	}
976
977
978
979
980
981
982





983




984
985
986





			'Lignes'          => $lines,
		];
	}

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





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




		return $out;
	}
}












>
>
>
>
>

>
>
>
>


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

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

		if ($this->exists()) {
			$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/Entities/Accounting/Year.php from [215a66eaee] to [76ca394f08].

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41




42
43
44
45
46
47
48

49
50
51
52
53
54
55
		'label'      => 'string',
		'start_date' => 'date',
		'end_date'   => 'date',
		'closed'     => 'int',
		'id_chart'   => 'int',
	];

	protected $_form_rules = [
		'label'      => 'required|string|max:200',
		'start_date' => 'required|date_format:d/m/Y',
		'end_date'   => 'required|date_format:d/m/Y',
	];

	public function selfCheck(): void
	{




		parent::selfCheck();
		$this->assert($this->start_date < $this->end_date, 'La date de fin doit être postérieure à la date de début');
		$this->assert($this->closed === 0 || $this->closed === 1);

		$db = DB::getInstance();

		$this->assert($this->id_chart !== null);


		if ($this->exists()) {
			$this->assert(
				!$db->test(Transaction::TABLE, 'id_year = ? AND date < ?', $this->id(), $this->start_date->format('Y-m-d')),
				'Des écritures de cet exercice ont une date antérieure à la date de début de l\'exercice.'
			);








<
<
<
<
<
<


>
>
>
>
|






>







27
28
29
30
31
32
33






34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
		'label'      => 'string',
		'start_date' => 'date',
		'end_date'   => 'date',
		'closed'     => 'int',
		'id_chart'   => 'int',
	];







	public function selfCheck(): void
	{
		$this->assert(trim($this->label) !== '', 'Le libellé ne peut rester vide.');
		$this->assert(strlen($this->label) <= 200, 'Le libellé ne peut faire plus de 200 caractères.');
		$this->assert($this->start_date instanceof \DateTime, 'La date de début de l\'exercice n\'est pas définie.');
		$this->assert($this->end_date instanceof \DateTime, 'La date de début de l\'exercice n\'est pas définie.');

		$this->assert($this->start_date < $this->end_date, 'La date de fin doit être postérieure à la date de début');
		$this->assert($this->closed === 0 || $this->closed === 1);

		$db = DB::getInstance();

		$this->assert($this->id_chart !== null);
		parent::selfCheck();

		if ($this->exists()) {
			$this->assert(
				!$db->test(Transaction::TABLE, 'id_year = ? AND date < ?', $this->id(), $this->start_date->format('Y-m-d')),
				'Des écritures de cet exercice ont une date antérieure à la date de début de l\'exercice.'
			);

Modified src/include/lib/Garradin/Entities/Services/Service_User.php from [e92524dd0e] to [db3c8cdf70].

1
2
3
4
5
6
7
8
9
10

11

12
13
14
15
16
17
18
<?php

namespace Garradin\Entities\Services;

use Garradin\DB;
use Garradin\Entity;
use Garradin\Membres;
use Garradin\ValidationException;
use Garradin\Services\Fees;
use Garradin\Services\Services;

use Garradin\Entities\Accounting\Transaction;


class Service_User extends Entity
{
	const TABLE = 'services_users';

	protected $id;
	protected $id_user;










>

>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace Garradin\Entities\Services;

use Garradin\DB;
use Garradin\Entity;
use Garradin\Membres;
use Garradin\ValidationException;
use Garradin\Services\Fees;
use Garradin\Services\Services;
use Garradin\Accounting\Transactions;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Accounting\Line;

class Service_User extends Entity
{
	const TABLE = 'services_users';

	protected $id;
	protected $id_user;
135
136
137
138
139
140
141

142
143
144
145
146
147
148
149


150
151
152
153
154
155
156
157
158

159




160


161









162



163
164
165
166
167
168
169
			throw new \RuntimeException('Cannot add a payment to a subscription that is not linked to a fee');
		}

		if (!$this->fee()->id_year) {
			throw new ValidationException('Le tarif indiqué ne possède pas d\'exercice lié');
		}


		$transaction = new Transaction;
		$transaction->id_creator = $user_id;
		$transaction->id_year = $this->fee()->id_year;

		$source['type'] = Transaction::TYPE_REVENUE;
		$key = sprintf('account_%d_', $source['type']);
		$source[$key . '0'] = [$this->fee()->id_account => ''];
		$source[$key . '1'] = isset($source['account']) ? $source['account'] : null;



		$label = $this->service()->label;

		if ($this->fee()->label != $label) {
			$label .= ' - ' . $this->fee()->label;
		}

		$label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user));


		$source['label'] = $label;




		$source['id_analytical'] = $this->fee()->id_analytical;












		$transaction->importFromNewForm($source);



		$transaction->save();
		$transaction->linkToUser($this->id_user, $this->id());

		return $transaction;
	}

	static public function createFromForm(array $users, int $creator_id, bool $from_copy = false, ?array $source = null): self







>
|
<
<
|
|
<
<
|
>
>









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







137
138
139
140
141
142
143
144
145


146
147


148
149
150
151
152
153
154
155
156
157
158
159
160
161
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
			throw new \RuntimeException('Cannot add a payment to a subscription that is not linked to a fee');
		}

		if (!$this->fee()->id_year) {
			throw new ValidationException('Le tarif indiqué ne possède pas d\'exercice lié');
		}

		if (empty($source['amount'])) {
			throw new ValidationException('Montant non précisé');


		}



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

		$label = $this->service()->label;

		if ($this->fee()->label != $label) {
			$label .= ' - ' . $this->fee()->label;
		}

		$label .= sprintf(' (%s)', (new Membres)->getNom($this->id_user));

		$transaction = Transactions::create(array_merge($source, [
			'label' => $label,
			'lines' => [
				[
					'id_account'    => $this->fee()->id_account,
					'credit'        => $source['amount'],
					'id_analytical' => $this->fee()->id_analytical,
					'reference'     => $source['payment_reference'] ?? null,
				],
				[
					'account_selector' => $source['account_selector'],
					'debit'            => $source['amount'],
					'id_analytical'    => $this->fee()->id_analytical,
					'reference'        => $source['payment_reference'] ?? null,

				],
			],
		]));

		$transaction->id_creator = $user_id;
		$transaction->id_year = $this->fee()->id_year;
		$transaction->type = Transaction::TYPE_REVENUE;

		$transaction->save();
		$transaction->linkToUser($this->id_user, $this->id());

		return $transaction;
	}

	static public function createFromForm(array $users, int $creator_id, bool $from_copy = false, ?array $source = null): self

Modified src/include/lib/Garradin/Entities/Web/Page.php from [fde50935fd] to [e6347b3ebb].

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
		}
		else {
			$content = $this->render();
			$this->file()->indexForSearch($content, $this->title, 'text/html');
		}
	}

	public function save(): bool
	{
		$change_parent = null;

		if (isset($this->_modified['uri']) || isset($this->_modified['path'])) {
			$this->set('file_path', $this->filepath(false));
			$change_parent = $this->_modified['path'];
		}

		// Update modified date if required
		if (count($this->_modified) && !isset($this->_modified['modified'])) {
			$this->set('modified', new \DateTime);
		}

		$current_path = $this->_modified['file_path'] ?? $this->file_path;
		parent::save();
		$this->syncFile($current_path);

		// Rename/move children
		if ($change_parent) {
			$db = DB::getInstance();
			$sql = sprintf('UPDATE web_pages
				SET







|














|







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
		}
		else {
			$content = $this->render();
			$this->file()->indexForSearch($content, $this->title, 'text/html');
		}
	}

	public function save(bool $selfcheck = true): bool
	{
		$change_parent = null;

		if (isset($this->_modified['uri']) || isset($this->_modified['path'])) {
			$this->set('file_path', $this->filepath(false));
			$change_parent = $this->_modified['path'];
		}

		// Update modified date if required
		if (count($this->_modified) && !isset($this->_modified['modified'])) {
			$this->set('modified', new \DateTime);
		}

		$current_path = $this->_modified['file_path'] ?? $this->file_path;
		parent::save($selfcheck);
		$this->syncFile($current_path);

		// Rename/move children
		if ($change_parent) {
			$db = DB::getInstance();
			$sql = sprintf('UPDATE web_pages
				SET

Modified src/include/lib/Garradin/Entity.php from [4b1b8772ae] to [bcf866d06b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace Garradin;

use Garradin\Form;
use KD2\DB\AbstractEntity;

class Entity extends AbstractEntity
{
	protected $_form_rules = [];

	/**
	 * Valider les champs avant enregistrement
	 * @throws ValidationException Si une erreur de validation survient
	 */
	public function importForm(array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}

		$form = new Form;

		if (!$form->validate($this->_form_rules, $source))
		{
			$messages = $form->getErrorMessages();

			throw new ValidationException(implode("\n", $messages));
		}

		return $this->import($source);
	}

	static public function filterUserDateValue(?string $value): ?\DateTime
	{
		if (!trim((string) $value)) {
			return null;









<
<










<
<
<
<
<
<
<
<
<







1
2
3
4
5
6
7
8
9


10
11
12
13
14
15
16
17
18
19









20
21
22
23
24
25
26
<?php

namespace Garradin;

use Garradin\Form;
use KD2\DB\AbstractEntity;

class Entity extends AbstractEntity
{


	/**
	 * Valider les champs avant enregistrement
	 * @throws ValidationException Si une erreur de validation survient
	 */
	public function importForm(array $source = null)
	{
		if (null === $source) {
			$source = $_POST;
		}










		return $this->import($source);
	}

	static public function filterUserDateValue(?string $value): ?\DateTime
	{
		if (!trim((string) $value)) {
			return null;
45
46
47
48
49
50
51



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
		}
		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)
	{
		if ($type == 'date') {
			return self::filterUserDateValue($value);
		}
		elseif ($type == 'DateTime') {
			if (preg_match('!^\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}$!', $value)) {
				return \DateTime::createFromFormat('d/m/Y H:i', $value);
			}
		}

		return parent::filterUserValue($type, $value, $key);
	}

	protected function assert(?bool $test, string $message = null, int $code = 0): void
	{
		if ($test) {
			return;
		}

		if (null === $message) {
			$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
			$caller_class = array_pop($backtrace);
			$caller = array_pop($backtrace);
			$message = sprintf('Entity assertion fail from class %s on line %d', $caller_class['class'], $caller['line']);
			throw new \UnexpectedValueException($message);
		}
		else {
			throw new ValidationException($message, $code);
		}
	}

	// Add plugin signals to save/delete
	public function save(): bool
	{
		$name = get_class($this);
		$name = str_replace('Garradin\Entities\\', '', $name);
		$name = 'entity.' . $name . '.save';

		// Specific entity signal
		if (Plugin::fireSignal($name . '.before', ['entity' => $this])) {
			return true;
		}

		// Generic entity signal
		if (Plugin::fireSignal('entity.save.before', ['entity' => $this])) {
			return true;
		}

		$return = parent::save();
		Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]);

		Plugin::fireSignal('entity.save.after', ['entity' => $this, 'success' => $return]);

		return $return;
	}








>
>
>



















|


















|















|







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
		}
		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)
	{
		if ($type == 'date') {
			return self::filterUserDateValue($value);
		}
		elseif ($type == 'DateTime') {
			if (preg_match('!^\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}$!', $value)) {
				return \DateTime::createFromFormat('d/m/Y H:i', $value);
			}
		}

		return parent::filterUserValue($type, $value, $key);
	}

	protected function assert($test, string $message = null, int $code = 0): void
	{
		if ($test) {
			return;
		}

		if (null === $message) {
			$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
			$caller_class = array_pop($backtrace);
			$caller = array_pop($backtrace);
			$message = sprintf('Entity assertion fail from class %s on line %d', $caller_class['class'], $caller['line']);
			throw new \UnexpectedValueException($message);
		}
		else {
			throw new ValidationException($message, $code);
		}
	}

	// Add plugin signals to save/delete
	public function save(bool $selfcheck = true): bool
	{
		$name = get_class($this);
		$name = str_replace('Garradin\Entities\\', '', $name);
		$name = 'entity.' . $name . '.save';

		// Specific entity signal
		if (Plugin::fireSignal($name . '.before', ['entity' => $this])) {
			return true;
		}

		// Generic entity signal
		if (Plugin::fireSignal('entity.save.before', ['entity' => $this])) {
			return true;
		}

		$return = parent::save($selfcheck);
		Plugin::fireSignal($name . '.after', ['entity' => $this, 'success' => $return]);

		Plugin::fireSignal('entity.save.after', ['entity' => $this, 'success' => $return]);

		return $return;
	}

Modified src/include/lib/Garradin/Membres.php from [081983cad8] to [81fa4a1e5a].

45
46
47
48
49
50
51




52


53
54
55

56
57
58
59
60
61
62
63
                }
            }

            if (isset($data[$key]))
            {
                if ($config->type == 'datetime' && trim($data[$key]) !== '')
                {




                    $dt = \DateTime::createFromFormat('Y-m-d H:i', $data[$key]);


                    if (!$dt) {
                        throw new UserException(sprintf('Format invalide pour le champ "%s": AAAA-MM-JJ HH:mm attendu.', $config->title));
                    }

                    $data[$key] = $dt->format('Y-m-d H:i');
                }
                elseif ($config->type == 'date' && trim($data[$key]) !== '')
                {
                    $dt = \DateTime::createFromFormat('Y-m-d', $data[$key]);

                    if (!$dt) {
                        $dt = \DateTime::createFromFormat('d/m/y', $data[$key]);







>
>
>
>
|
>
>

|

>
|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
                }
            }

            if (isset($data[$key]))
            {
                if ($config->type == 'datetime' && trim($data[$key]) !== '')
                {
                    $value = sprintf('%s %s', $data[$key], $_POST[$key . '_time'] ?? '');
                    $dt = null;

                    if (preg_match('!^\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}$!', $value)) {
                        $dt = \DateTime::createFromFormat('!d/m/Y H:i', $value);
                    }

                    if (!$dt) {
                        throw new UserException(sprintf('Format de date et heure invalide pour le champ "%s" : %s', $config->title, $value));
                    }

                    $data[$key] = $dt->format('Y-m-d H:i:s');
                }
                elseif ($config->type == 'date' && trim($data[$key]) !== '')
                {
                    $dt = \DateTime::createFromFormat('Y-m-d', $data[$key]);

                    if (!$dt) {
                        $dt = \DateTime::createFromFormat('d/m/y', $data[$key]);

Modified src/include/lib/Garradin/Template.php from [334bcf99d4] to [57ba04b12a].

1
2
3
4
5
6

7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace Garradin;

use KD2\Form;
use KD2\HTTP;

use KD2\Translate;

use Garradin\Membres\Session;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Users\Category;
use Garradin\UserTemplate\CommonModifiers;
use Garradin\Web\Render\Skriv;
use Garradin\Files\Files;

class Template extends \KD2\Smartyer
{
	static protected $_instance = null;

	static public function getInstance()
	{
		return self::$_instance ?: self::$_instance = new Template;
	}






>

>







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace Garradin;

use KD2\Form;
use KD2\HTTP;
use KD2\Smartyer;
use KD2\Translate;

use Garradin\Membres\Session;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Users\Category;
use Garradin\UserTemplate\CommonModifiers;
use Garradin\Web\Render\Skriv;
use Garradin\Files\Files;

class Template extends Smartyer
{
	static protected $_instance = null;

	static public function getInstance()
	{
		return self::$_instance ?: self::$_instance = new Template;
	}
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
		$couleur2 = $config->get('couleur2') ?: ADMIN_COLOR2;
		$admin_background = ADMIN_BACKGROUND_IMAGE;

		if ($url = $config->fileURL('admin_background')) {
			$admin_background = $url;
		}

		// Transformation Hexa vers décimal
		$couleur1 = implode(', ', sscanf($couleur1, '#%02x%02x%02x'));
		$couleur2 = implode(', ', sscanf($couleur2, '#%02x%02x%02x'));

		$out = '
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
			--gBgImage: url("%s");
		}
		</style>';

		if ($url = $config->fileURL('admin_css')) {
			$out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url);
		}

		return sprintf($out, $couleur1, $couleur2, $admin_background);
	}

	protected function displayChampMembre($v, $config = null)
	{
		if (is_string($config)) {
			$config = Config::getInstance()->get('champs_membres')->get($config);
		}







<
<
<
<













|







304
305
306
307
308
309
310




311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
		$couleur2 = $config->get('couleur2') ?: ADMIN_COLOR2;
		$admin_background = ADMIN_BACKGROUND_IMAGE;

		if ($url = $config->fileURL('admin_background')) {
			$admin_background = $url;
		}





		$out = '
		<style type="text/css">
		:root {
			--gMainColor: %s;
			--gSecondColor: %s;
			--gBgImage: url("%s");
		}
		</style>';

		if ($url = $config->fileURL('admin_css')) {
			$out .= "\n" . sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url);
		}

		return sprintf($out, CommonModifiers::css_hex_to_rgb($couleur1), CommonModifiers::css_hex_to_rgb($couleur2), $admin_background);
	}

	protected function displayChampMembre($v, $config = null)
	{
		if (is_string($config)) {
			$config = Config::getInstance()->get('champs_membres')->get($config);
		}
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
					htmlspecialchars($params['name']), $k, ($value & $b) ? 'checked="checked"' : '', $attributes, htmlspecialchars($v));
			}
		}
		elseif ($type == 'textarea')
		{
			$field .= '<textarea ' . $attributes . 'cols="30" rows="5">' . htmlspecialchars((string) $value, ENT_QUOTES) . '</textarea>';
		}
		elseif ($type == 'date') {
			$field = CommonModifiers::input(['required' => $config->mandatory, 'name' => $params['name'], 'value' => $value, 'type' => 'date', 'default' => $value]);
		}
		else
		{
			if ($type == 'checkbox')
			{
				if (!empty($value))
				{







|
|







494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
					htmlspecialchars($params['name']), $k, ($value & $b) ? 'checked="checked"' : '', $attributes, htmlspecialchars($v));
			}
		}
		elseif ($type == 'textarea')
		{
			$field .= '<textarea ' . $attributes . 'cols="30" rows="5">' . htmlspecialchars((string) $value, ENT_QUOTES) . '</textarea>';
		}
		elseif ($type == 'date' || $type == 'datetime') {
			$field = CommonModifiers::input(['required' => $config->mandatory, 'name' => $params['name'], 'value' => $type, 'type' => 'date', 'default' => $value]);
		}
		else
		{
			if ($type == 'checkbox')
			{
				if (!empty($value))
				{

Modified src/include/lib/Garradin/UserTemplate/CommonModifiers.php from [75f299cdcc] to [5c90ad8a19].

20
21
22
23
24
25
26

27
28
29
30
31
32
33
		'date_short',
		'date_long',
		'date_hour',
		'date',
		'strftime',
		'size_in_bytes' => [Utils::class, 'format_bytes'],
		'typo',

	];

	const FUNCTIONS_LIST = [
		'pagination',
		'input',
		'button',
		'link',







>







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
		'date_short',
		'date_long',
		'date_hour',
		'date',
		'strftime',
		'size_in_bytes' => [Utils::class, 'format_bytes'],
		'typo',
		'css_hex_to_rgb',
	];

	const FUNCTIONS_LIST = [
		'pagination',
		'input',
		'button',
		'link',
342
343
344
345
346
347
348


349


350









351
352
353
354
355
356
357
		elseif ($type == 'time' && is_object($current_value) && $current_value instanceof \DateTimeInterface) {
			$current_value = $current_value->format('H:i');
		}
		elseif ($type == 'date' && is_string($current_value)) {
			if ($v = \DateTime::createFromFormat('!Y-m-d', $current_value)) {
				$current_value = $v->format('d/m/Y');
			}


		}













		$attributes['id'] = 'f_' . str_replace(['[', ']'], '', $name);
		$attributes['name'] = $name;

		if (!isset($attributes['autocomplete']) && ($type == 'money' || $type == 'password')) {
			$attributes['autocomplete'] = 'off';
		}







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







343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
		elseif ($type == 'time' && is_object($current_value) && $current_value instanceof \DateTimeInterface) {
			$current_value = $current_value->format('H:i');
		}
		elseif ($type == 'date' && is_string($current_value)) {
			if ($v = \DateTime::createFromFormat('!Y-m-d', $current_value)) {
				$current_value = $v->format('d/m/Y');
			}
			elseif ($v = \DateTime::createFromFormat('!Y-m-d H:i:s', $current_value)) {
				$current_value = $v->format('d/m/Y');
			}
			elseif ($v = \DateTime::createFromFormat('!Y-m-d H:i', $current_value)) {
				$current_value = $v->format('d/m/Y');
			}
		}
		elseif ($type == 'time' && is_string($current_value)) {
			if ($v = \DateTime::createFromFormat('!Y-m-d H:i:s', $current_value)) {
				$current_value = $v->format('H:i');
			}
			elseif ($v = \DateTime::createFromFormat('!Y-m-d H:i', $current_value)) {
				$current_value = $v->format('H:i');
			}
		}

		$attributes['id'] = 'f_' . str_replace(['[', ']'], '', $name);
		$attributes['name'] = $name;

		if (!isset($attributes['autocomplete']) && ($type == 'money' || $type == 'password')) {
			$attributes['autocomplete'] = 'off';
		}
488
489
490
491
492
493
494






495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
			$currency = Config::getInstance()->get('monnaie');
			$input = sprintf('<nobr><input type="text" pattern="-?[0-9]*([.,][0-9]{1,2})?" inputmode="decimal" size="8" class="money" %s value="%s" /><b>%s</b></nobr>', $attributes_string, htmlspecialchars((string) $current_value), $currency);
		}
		else {
			$value = isset($attributes['value']) ? '' : sprintf(' value="%s"', htmlspecialchars((string)$current_value));
			$input = sprintf('<input type="%s" %s %s />', $type, $attributes_string, $value);
		}







		// No label? then we only want the input without the widget
		if (empty($label)) {
			if (!array_key_exists('label', $params) && ($type == 'radio' || $type == 'checkbox')) {
				$input .= sprintf('<label for="%s"></label>', $attributes['id']);
			}

			return $input;
		}

		if ($type == 'file') {
			$input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize()));
		}

		$input .= $suffix;

		$label = sprintf('<label for="%s">%s</label>', $attributes['id'], htmlspecialchars((string)$label));

		if ($type == 'radio' || $type == 'checkbox') {
			$out = sprintf('<dd>%s %s', $input, $label);

			if (isset($help)) {
				$out .= sprintf(' <em class="help">(%s)</em>', htmlspecialchars($help));







>
>
>
>
>
>










<
<
<
<
<
<







502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524






525
526
527
528
529
530
531
			$currency = Config::getInstance()->get('monnaie');
			$input = sprintf('<nobr><input type="text" pattern="-?[0-9]*([.,][0-9]{1,2})?" inputmode="decimal" size="8" class="money" %s value="%s" /><b>%s</b></nobr>', $attributes_string, htmlspecialchars((string) $current_value), $currency);
		}
		else {
			$value = isset($attributes['value']) ? '' : sprintf(' value="%s"', htmlspecialchars((string)$current_value));
			$input = sprintf('<input type="%s" %s %s />', $type, $attributes_string, $value);
		}

		if ($type == 'file') {
			$input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize()));
		}

		$input .= $suffix;

		// No label? then we only want the input without the widget
		if (empty($label)) {
			if (!array_key_exists('label', $params) && ($type == 'radio' || $type == 'checkbox')) {
				$input .= sprintf('<label for="%s"></label>', $attributes['id']);
			}

			return $input;
		}







		$label = sprintf('<label for="%s">%s</label>', $attributes['id'], htmlspecialchars((string)$label));

		if ($type == 'radio' || $type == 'checkbox') {
			$out = sprintf('<dd>%s %s', $input, $label);

			if (isset($help)) {
				$out .= sprintf(' <em class="help">(%s)</em>', htmlspecialchars($help));
625
626
627
628
629
630
631


632







		}

		$params['class'] .= ' icn-btn';

		return self::link($params);
	}



}














>
>
|
>
>
>
>
>
>
>
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
		}

		$params['class'] .= ' icn-btn';

		return self::link($params);
	}

	static public function css_hex_to_rgb($str): ?string {
		$hex = sscanf((string)$str, '#%02x%02x%02x');

		if (empty($hex)) {
			return null;
		}

		return implode(', ', $hex);
	}
}

Modified src/include/lib/Garradin/Utils.php from [65534f85cc] to [8c9dde00fc].

130
131
132
133
134
135
136



137
138
139
140
141
142
143
        }
        elseif (strlen($ts) == 10) {
            return \DateTime::createFromFormat('!Y-m-d', $ts);
        }
        elseif (strlen($ts) == 19) {
            return \DateTime::createFromFormat('Y-m-d H:i:s', $ts);
        }



        else {
            return null;
        }
    }

    static public function strftime_fr($ts, $format)
    {







>
>
>







130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
        }
        elseif (strlen($ts) == 10) {
            return \DateTime::createFromFormat('!Y-m-d', $ts);
        }
        elseif (strlen($ts) == 19) {
            return \DateTime::createFromFormat('Y-m-d H:i:s', $ts);
        }
        elseif (strlen($ts) == 16) {
            return \DateTime::createFromFormat('!Y-m-d H:i', $ts);
        }
        else {
            return null;
        }
    }

    static public function strftime_fr($ts, $format)
    {

Modified src/skel-dist/web/_foot.html from [8e09fe42c0] to [051a09b414].

1
2


3
4
5


6
7
8
</section>



<footer class="main">
	Propulsé par <a href="https://garradin.eu/" id="garradin">Garradin</a> — logiciel libre de gestion associative
</footer>



</body>
</html>


>
>

|

>
>



1
2
3
4
5
6
7
8
9
10
11
12
</section>

</main>

<footer class="main">
	Propulsé par <a href="https://garradin.eu/" target="_blank" id="garradin">Garradin</a> — logiciel libre de gestion associative
</footer>

<script type="text/javascript" src="{{$admin_url}}static/scripts/wiki_gallery.js"></script>

</body>
</html>

Modified src/skel-dist/web/_head.html from [f9b2a89dba] to [d88df21f79].

1
2
3
4
5
6






7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


24





25



26
27
28



29
30
31
32
33
34
35
36
37
38
39

40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html lang="fr">
<head>
	<meta charset="utf-8" />
	<title>{{if $title}}{{$title}} — {{/if}}{{$config.nom_asso}}</title>
	<meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />






	<link rel="stylesheet" type="text/css" href="{{$root_url}}default.css" media="screen,projection,handheld" />
	<link rel="stylesheet" type="text/css" href="{{$root_url}}content.css" media="all" />
	<link rel="alternate" type="application/atom+xml" title="{{$config.nom_asso}}" href="{{$root_url}}atom.xml" />
	<link rel="icon" type="image/png" href="{{$config.files.favicon}}" />
</head>

<body>

<header class="nav">
	<nav>
		<ul>
			<li class="current"><a href="{{$root_url}}">Accueil</a></li>
			<li><a href="{{$admin_url}}">Administration</a></li>
		</ul>
	</nav>
</header>



<header class="main">





	<h1><a href="{{$root_url}}">{{if $config.files.logo}}<img src="{{$config.files.logo}}&150px" alt="" class="logo" />{{/if}} <span>{{$config.nom_asso}}</span></a></h1>




{{if $config.adresse_asso || $config.telephone_asso || $config.email_asso}}
	<article class="contacts">



		{{if $config.adresse_asso}}
			<h4>{{$config.adresse_asso|escape}}</h4>
		{{/if}}
		{{if $config.telephone_asso}}
			<h5>{{$config.telephone_asso|raw|protect_contact}}</h5>
		{{/if}}
		{{if $config.email_asso}}
			<h5>{{$config.email_asso|raw|protect_contact}}</h5>
		{{/if}}
	</article>
{{/if}}



	<form method="get" action="{{ $root_url }}search.html" class="search-widget">
		<p>
			<input type="search" name="search" placeholder="Rechercher…" />
			<input type="submit" value="»" />
		</p>
	</form>

	<nav>
		<ul>
		{{#categories parent=null order="title"}}
			<li><a href="{{$url}}">{{ $title }}</a></li>
		{{/categories}}
		</ul>
	</nav>
</header>

<section class="page">






>
>
>
>
>
>
|











|




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

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

>







<
|
|
|
|
|
|
<


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74

75
76
<!DOCTYPE html>
<html lang="fr">
<head>
	<meta charset="utf-8" />
	<title>{{if $title}}{{$title}} — {{/if}}{{$config.nom_asso}}</title>
	<meta name="viewport" content="width=device-width, initial-scale=1.0, target-densitydpi=device-dpi" />
	<style type="text/css">
	:root {
		--first-color: {{if $config.couleur1}}{{$config.couleur1|css_hex_to_rgb}}{{else}}120, 120, 120{{/if}};
		--second-color: {{if $config.couleur2}}{{$config.couleur2|css_hex_to_rgb}}{{else}}30, 30, 30{{/if}};
	}
	</style>
	<link rel="stylesheet" type="text/css" href="{{$root_url}}default.css?2022-07" media="screen,projection,handheld" />
	<link rel="stylesheet" type="text/css" href="{{$root_url}}content.css" media="all" />
	<link rel="alternate" type="application/atom+xml" title="{{$config.nom_asso}}" href="{{$root_url}}atom.xml" />
	<link rel="icon" type="image/png" href="{{$config.files.favicon}}" />
</head>

<body>

<header class="nav">
	<nav>
		<ul>
			<li class="current"><a href="{{$root_url}}">Accueil</a></li>
			<li><a href="{{$admin_url}}">Connexion</a></li>
		</ul>
	</nav>
</header>

<main>

<header class="main{{if $home}} home{{/if}}">
	<h1>
		<a href="{{$root_url}}">
		{{if $config.files.logo}}
			<img src="{{$config.files.logo}}&500px" alt="" class="logo" />
		{{else}}
			<span>{{$config.nom_asso}}</span>
		{{/if}}
		</a>
	</h1>

	{{if ($config.adresse_asso || $config.telephone_asso || $config.email_asso)}}
		<article class="contacts">
			{{if $config.files.logo}}
			<h3>{{$config.nom_asso}}</h3>
			{{/if}}
			{{if $config.adresse_asso}}
				<h4>{{$config.adresse_asso|escape|nl2br}}</h4>
			{{/if}}
			{{if $config.telephone_asso}}
				<h5>{{$config.telephone_asso|raw|protect_contact}}</h5>
			{{/if}}
			{{if $config.email_asso}}
				<h5>{{$config.email_asso|raw|protect_contact}}</h5>
			{{/if}}
		</article>
	{{/if}}
</header>

<nav class="main">
	<form method="get" action="{{ $root_url }}search.html" class="search-widget">
		<p>
			<input type="search" name="search" placeholder="Rechercher…" />
			<input type="submit" value="»" />
		</p>
	</form>


	<ul>
	{{#categories parent=null order="title"}}
		<li><a href="{{$url}}" {{if $url == $.url}}class="current"{{/if}}>{{ $title }}</a></li>
	{{/categories}}
	</ul>
</nav>


<section class="page">

Modified src/skel-dist/web/category.html from [b8c40e1238] to [74555aa62f].

1
2
3


4
5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29
30
31
32
{{:include file="./_head.html" title=$page.title}}

{{:include file="./_breadcrumbs.html" parent=$page.path}}



<section class="subcategories">
	{{#categories parent=$page.path order="title"}}
	<ul>
		<li><a href="{{$url}}">{{$title}}</a></li>
	</ul>
	{{/categories}}
</section>


<article class="single">
	<h1>{{$page.title}}</h1>

	{{$page.html|raw}}

	{{:include file="./gallery.html" parent=$page.path}}
	{{:include file="./documents.html" parent=$page.path}}
</article>


<section class="articles">
	{{#articles parent=$page.path order="published DESC" future=false}}
	<article>
		<h3><a href="{{$url}}">{{$title}}</a></h3>
		<h5>{{$published|date_long}}</h5>
		<p>{{$html|raw|strip_tags|truncate:200}}</p>
	</article>
	{{/articles}}
</section>

{{:include file="./_foot.html"}}



>
>









>

<
<





>












1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{{:include file="./_head.html" title=$page.title}}

{{:include file="./_breadcrumbs.html" parent=$page.path}}

<h1>{{$page.title}}</h1>

<section class="subcategories">
	{{#categories parent=$page.path order="title"}}
	<ul>
		<li><a href="{{$url}}">{{$title}}</a></li>
	</ul>
	{{/categories}}
</section>

{{if $page.content|trim}}
<article class="single">


	{{$page.html|raw}}

	{{:include file="./gallery.html" parent=$page.path}}
	{{:include file="./documents.html" parent=$page.path}}
</article>
{{/if}}

<section class="articles">
	{{#articles parent=$page.path order="published DESC" future=false}}
	<article>
		<h3><a href="{{$url}}">{{$title}}</a></h3>
		<h5>{{$published|date_long}}</h5>
		<p>{{$html|raw|strip_tags|truncate:200}}</p>
	</article>
	{{/articles}}
</section>

{{:include file="./_foot.html"}}

Modified src/skel-dist/web/default.css from [0334d88d82] to [db424ff1b1].

17
18
19
20
21
22
23
24

25
26

27
28
29






30
31

















































































32
33
34

35
36
37
38

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
132
133
134
135
136
137
138
139
140
141
142
143

144
145
146
147
148
149
150
151
152
153
154





155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; }

/* CORPS */
body {
    font-size: 100.01%;
    color: #000;
    font-family: "Trebuchet MS", Helvetica, Sans-serif;
    background: #fff;

}


header.main, footer.main, section.page {
    max-width: 950px;
    margin: 0 auto;






}


















































































/* NAVIGATION EN HAUT DE LA PAGE */
header.nav {
    background: #ddd;

    border-bottom: 1px solid #999;
    border-top: .3em solid #666;
    text-align: center;
    padding-top: .3em;

}

header.nav li {
    display: inline-block;
    padding: .3em .5em;
    margin-bottom: -1px;
}

header.nav li a {
    padding: .3em .5em;
    text-transform: uppercase;
    color: #666;
    text-decoration: none;
}

header.nav li.current a {
    background: #fff;
    border: .1em solid #999;
    border-bottom: none;
    border-top-left-radius: .3em;
    border-top-right-radius: .3em;
}

header.nav li a:hover {
    color: #000;
}

/* ENTÊTE (AVEC LE NOM DE L'ASSOCIATION) */
header.main h1 {
    color: #9c4f15;
    padding: .2em 0 .1em 0;
    font-size: 4em;
    font-family: Georgia, "Times New Roman", Times, serif;
    font-weight: normal;
}

header.main h1 a {
    display: flex;
    flex-direction: row-reverse;
    align-items: center;
    justify-content: space-between;
    color: #9c4f15;
    text-decoration: none;
}

header.main h1 a span {
    margin-right: auto;
}

header.main {
    margin-bottom: 1em;
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXMAAADPAQMAAAA9C6NrAAAABlBMVEXx9PD+//zYDo7WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gEIER05obn78wAACb1JREFUaN7V2TuOHLkZAGBSHKgmMEyFCgRRR1CoYD2lo+gI42wAyyoKCjbUEXZvshQUONwb2BwoULhcOHDJLhT9v/io6u7ZrXAFaKa76xsWi4+fP9kq13+K/+m84o8POUcN/+XTkU1UzculIS/o3+ecwC/i3alfypWEHmzCi54/tadeLk14I4N+htdYSqC7nvrE1cG/M6tCP/6c848WvTnjMxePv8gvavxcPtY7H7EJEhW/4l3Yu5j5/d7Ts8K9lby2K9xoVTZOWC2+2PvYtRk+hit+lMfCenaemsa0vx1XepAhQjuGrsPEr6q7J16fVgTgh9Lvrvdz/xneK5P3JppS1sbXoSBeV6+Lt73HKrwsnp6EfNB43XdliccPbqQMan746XCARh4etTHY0y1d4DZYqLLkk4rw2Xziqbfc0p59ZD/DUHN4d7iP7vzcjVh6k4t/jR8D8kp1PrYHyjz6sW9lZHyRB5yaD13v4h8P85TR06OXIdd5r65q79JI9ewz+bE8U/VQeB1RfDPxOHNwCKXSwVEGMDTAxq9T9WHgz2z1cDe9wHtqE+7OdfKW66aD5v5sPknlZ/I0m9axecX9OVQfpfKJnon8wh5LCnCtznjyQZ4eO5/LMosLVvoOu3guMziWkATyZyypeEt+IW9ljBcv0816iU9QV/H4x6/VEKZZHjEqKRHu7VSJDsrOhjzPRRPqjI+KBTx8fK5UHXuzjrbFgbjzz2nyPWvezSraOhJ1rBEiUpjkWz+jR0riX9gaUlWsMx59oq5Y1CtqYhLjLP05n/ER3ccEE96UGlTPa8LrGiHY67zYCH4ofkrd5C9+EB/AJwjyL+gTarvM/j91eem9h1ceOusFFdn7VILnpv54CZpUahhoqIgfpYE27U/R+3kpwdNQFD+UyNqNT3ikvyzQ+NIjnmqKfp1SjZ7d/IUm+zarp7237MdZhn4L9uT1fVJPSg3rEmwXN/OrbXyelfmAnSVF8H3IW5zUS188+8cwAF+c96asDW29TuraBPK6eBo3boZFVW+XX/QY/3xtsZUvkjeZb/m6xqbiW4fzakEeWtOXPKblMNhgQ2uClX+Tx2vunH/cBgj6MWCrOLq3PfFQ96vWg+ihIuynVFeFc36qycUCfgS/1FWh+TYAc/Nv2a+1Ic94XSegmW/wKeCGXu39uh2w4p+Lj7XlT32b4CZZ8XOdVcUvm/km4TPB+jR5SoFkDp3xU+e1R49RT+ZozSfn7nFjCbe0glC4DiVmnHgc6r2HWkxzrjFs3NfHQTEllaieWzn286tMuEyds0iqorBjpoUrkjb5SZDWT9VDU8Jrpf6+cDOkfr7LIoLVMeIn8XcrV3xW+/xBUdwa8i/V4+S6y+wXtckHsEo/4Yc2f+RE17O/nch9Xbbre82T3Dos1BOeMmjwHt2XvF3fWx6zDLz4iY8j9ew9VVjvPA6SmbzF15BxqzBiMIBurw3UeWrSgbNDTzGbvclxWvt4XjN6m+PALc5ee4gR6Mc6Ipqf8QM/cI96ytA1rqtQb8zIfFvvyuNqTJhpL4VXqQ6O3kZXcoDOc2aKfug8/kBfEqDmObQaegxqK4507Fe19yu+XclTbbX4gN7WLU/1Cye16POXr0EZ8dBAU7S8DPd+VjS7wet1+Fw87SUnWCYlKW0+cdJvqCHeB3pDjw71Ax8lvlYfuPXIWx8oGyUf0P+S9p5yvuIhwXDFJ/T/KDOgeqqf+OeyY+Aqov+8ygzYeHg4gxe+a9tOrCcsfJ/zztMyt75l/4rbrnnzKcseuN9vmuWO/a0q66jBhgEfqIE6P1OSKuW/4b6vfjGeElbTPG0I5lfsA3uc9F+LX0/9mNhrz2OXfCIPzRU4Inf73wm8Ri/BA/1nSmwe0eZw52FePKXyH7VMy3yYeebTrk01T1UWfyVzFRf+9zNPAiebkG5/DWP9mp9XYgF6vbC3uRxNVA9RoXmp42NcDsmvew+N5a+K11vvpfyxeu6gK4qyLVaqP6mJ/bD3dcC0pTiR9zSJB9nktPMBHsCmLB4fZVWZ0ML/f9GE6rzp/Zht2QlgChTV8DH2fuHRr6Se8NrKKjRiCgT++7T3U/OY3xc/U3Y88ETdlD9XD11tv4lf2eu19xSGm8f9wDdZWrPCJP0x54UnniOZg91yKj5g0HpMa33vTechsNjEi4r4K5oB1WN0+1+qHrdkxUcJdXSc0nn7a/W4HcGTouqzHHls/H0qSx3eFr2vnob73DzO6lA9Lic2up3npUm1Mwea87TU4QCWRegmcdA98dnH4mnLFGltUe8Shyla7zsP6csbbj4eeeixgLe3HKTV8AMtZSU/gYn1hmZc4D3LGGlTJR77agi9t83TCdEke7U78lCAwV1z87C2vKR5HmhHDN7QgLjjXSuesLzqfSavOeHEksTfvpTY+AgmJyyV7D3e/Al7KztcnnDxZduF7jxH4DDR4+eZj1niE2XrPr55U/195h3xzGlHYC9Z7Vg87oY47nzNvCNeNNXCP1Xt4KJ6Jd6WaOR+WhRHefF+58fOw3PejCu3q7pW3UGE23lXZv/dzTtVTh+6g4VLXvubqSTrctOd/1tqh6hY7LuxnNxIIeGcn5p3fqqZeDszrP6ve2/9VAptGYOt55m/thyf2mI1YdoeG8eNv48UN2dXvHpdNgqupSRD9Z8i7zHKkctKlZt7P/c+cEIRZDuk2S/dVwU5/7d57XkP5PXGrxvfnd+yd7wfFT+2o7lTL9FByXmfWesaf9Ybme1Ui87Hdi668Y/owCKf+PSQ1+Jl9zKWhe2MLxvsvV/bue6pt7LOe84xRuzth7zL3J68guHqT4n2RT/tfTA5bk8UNh7bWxLPkY5aPC6aFz0EQBdNOfxGjwd/lz3EKJs4uFLcpd5etycivYeczq5jacOAqYCi0HfBTzz6Kbn5hofF5MMlr3Pzw6d/Fx8vefg8lTPMP+uUxM/DBW87/8Skl+KXSx739a68M/GaRje8tRc8/p6ap9mAbf/xvG+P1bzfnS713p7xwZz3eTNL+VsOasvhss9n/OwuerPzu3F/4t32a0IblH7QT8e83n0N6X7D250fozIP+fGY93nnp6iGB/z3e58f9j/svD49KT33/Wxum6VjHnM9d8Bb+S7p93r3z9sDHkLPs6e7g9KH/dvvrs8Ozwtex1fqiMcvX/Uhf3vERzxSMId8OD98HvD2kPdHfMLU2h304xH/6KDfZQ2/4WelLgyHy14f9OagHw74ZbN8/D7vDvrxgF8vNv9ln495fcTni81/0Q8HvTvox0PeX2rOiz4f8+aYD8NB7w768ZiP0zF/n4/5rwd9/uP4/wOHV4ghb+WqVQAAAABJRU5ErkJggg==") no-repeat top right;
}

/* LISTE DES CATÉGORIES EN DESSOUS DU NOM DE L'ASSOCIATION */
header.main nav, .subcategories {
    font-size: 1.2em;
    margin: 1em 0;
}

header.main nav ul li, .subcategories li {
    display: inline-block;
    margin: .1rem .2rem;
    padding: 0;
}

header.main nav ul li a, .subcategories li a {
    display: inline-block;
    margin: 0;
    padding: .2em .5em;
    color: #006;
    text-decoration: none;
    background: #ddd;

    border-radius: .5rem;
}





header.main nav ul li a:hover, .subcategories li a:hover {
    color: darkred;
    background: #eee;
}

.contacts * {





    font-size: 1rem;

    display: inline-block;
    font-weight: normal;

    margin: 0;

    margin-right: 1em;







}








/* PIED DE PAGE */
footer.main {
    color: #999;
    margin-top: 1em;
    text-align: center;
}

footer.main a {
    text-decoration: none;
    font-weight: bold;
    color: #666;
}

footer.main a:hover {
    color: #006;

}

footer.main a#garradin {
    padding-left: 20px;
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAGFBMVEX///+qjnmcTxWgYzWeWyiysrGzs7Ozs7MzVhJAAAAAB3RSTlMAGtv2e9p5oKuvKgAAAH1JREFUCNcdzLEOwjAMBNCrFDoXEGK3zA906RoifwBLmpVUSrMTpf59DNM9nU4HYNhjzJNlUaUPAzddN54NKU5uJsGoL4CYce9vA3mUJiLEHrURiRUoXQLRM6N2F4jrajcXm9YDJ+Xwx5AegX/Addn82eA4tah6QPwYNe75C+4yHrwP6fqUAAAAAElFTkSuQmCC") no-repeat left top;
    min-height: 16px;
    display: inline-block;
}

/* CHEMIN VERS L'ARTICLE (BREADCRUMBS), affiche les catégories parentes */
.breadcrumbs {





    margin: 1em 0;
}

.breadcrumbs ul li {
    display: inline-block;
}

.breadcrumbs ul li::before {
    content: "»";
    color: #ccc;
    margin: .5em;
}

.breadcrumbs ul li:nth-child(1)::before {
    content: "";
}

.breadcrumbs a {
    color: #999;
}


/* MESSAGES ALERTE ET ERREUR (par exemple : page non trouvée) */
.error {
    border-bottom: .2em solid #c00;
    border-radius: .5em;







|
>


>
|


>
>
>
>
>
>


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


>




>




|
|



|

















<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|


|
|
|



|
<



|

>



>
>
>
>
|




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


>
>
>
>
>
>



|
|






|




>




|






>
>
>
>
>
|








|




|



|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156



























157
158
159
160
161
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
article, aside, figure, footer, header, hgroup, menu, nav, section { display: block; }

/* CORPS */
body {
    font-size: 100.01%;
    color: #000;
    font-family: "Trebuchet MS", Helvetica, Sans-serif;
    background: #eee;
    background: rgba(var(--first-color), 0.2);
}

/* DISPOSITION GÉNÉRALE DE LA PAGE */
main {
    max-width: 950px;
    margin: 0 auto;
    display: grid;
    grid-gap: 1em;
    grid-template-columns: 1.4fr 0.6fr;
    grid-template-areas:
        "header header"
        "content nav"
}

header.main {
    grid-area: header;
    display: grid;
    grid-gap: 1em;
    grid-template-columns: 1.4fr 0.6fr;
}
nav.main { grid-area: nav; }
section.page { grid-area: content; }

header.main h1, header.main .contacts, nav.main {
    background: #fff;
    padding: 1em;
    border-radius: .5em
}

header.main .contacts {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

section.page article {
    margin-bottom: 1em;
    padding: 1em;
    background: #fff;
    clear: both;
    border-radius: .5em;
}

/* ENTÊTE AVEC LOGO ET CONTACTS */
header.main h1 {
    padding: 0;
    background: none;
    border-radius: 0;
}

header.main h1 a {
    display: flex;
    padding: 1rem;
    background: #fff;
    border-bottom-left-radius: .5rem;
    border-bottom-right-radius: .5rem;
    text-decoration: none;
    height: calc(100% - 2rem);
    align-items: center;
    justify-content: center;
}

header.main h1 a span {
    font-size: 1.5em;
    font-weight: normal;
    color: rgb(var(--first-color));
    display: block;
}

header.main h1 a:hover span {
    color: rgb(var(--second-color));
}

header.main h1 a:hover img {
    opacity: 0.8;
}

header.main h1 a img {
    max-height: 200px;
}

header.main.home h1 a img {
    max-height: 300px;
}

header.main.home h1 a span {
    font-size: 2em;
}

header.main .contacts {
    font-size: 1.3em;
    display: flex;
    justify-content: flex-end;
    flex-direction: column;
}

/* NAVIGATION EN HAUT DE LA PAGE (ACCUEIL/CONNEXION) */
header.nav {
    background: #ddd;
    background: rgba(var(--second-color), 0.2);
    border-bottom: 1px solid #999;
    border-top: .3em solid #666;
    text-align: center;
    padding-top: .3em;
    border-color: rgba(var(--first-color), 1);
}

header.nav li {
    display: inline-block;
    padding: .2em .5em;
    margin-bottom: -3px;
}

header.nav li a {
    padding: .1em .5em;
    text-transform: uppercase;
    color: #666;
    text-decoration: none;
}

header.nav li.current a {
    background: #fff;
    border: .1em solid #999;
    border-bottom: none;
    border-top-left-radius: .3em;
    border-top-right-radius: .3em;
}

header.nav li a:hover {
    color: #000;
}




























/* LISTE DES CATÉGORIES RACINES */
.subcategories {
    text-align: center;
    margin: 2em 0;
}

nav.main ul li, .subcategories li {
    font-size: 1.4em;
    margin: .8em 0;
    padding: 0;
}

nav.main ul li a, .subcategories li a {

    margin: 0;
    padding: .2em .5em;
    color: #006;
    text-decoration: underline;
    background: #ddd;
    background: rgba(var(--second-color), 0.2);
    border-radius: .5rem;
}

.subcategories li a {
    background: #fff;
}

nav.main ul li a:hover, .subcategories li a:hover {
    color: darkred;
    background: #eee;
}


/* Formulaire de recherche */
.search article p b {
    background: #ff9;
    padding: .2em 0;
}

.search-widget p {
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-bottom: 1em;
}

.search-widget input {
    font-size: 1.2em;
    padding: .2em .5em;
    border: 1px solid #999;
    border-radius: .3rem;
    line-height: 1rem;
    max-width: 10em;
}

.search-widget input[type=submit] {
    margin-left: .5em;
    background: #999;
    color: #fff;
    cursor: pointer;
}

/* PIED DE PAGE */
footer.main {
    color: #666;
    margin: 1em;
    text-align: center;
}

footer.main a {
    text-decoration: none;
    font-weight: bold;
    color: #333;
}

footer.main a:hover {
    color: #006;
    text-decoration: underline;
}

footer.main a#garradin {
    padding-left: 20px;
    background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAGFBMVEX///+qjnmcTxWgYzWeWyiysrGzs7Ozs7MzVhJAAAAAB3RSTlMAGtv2e9p5oKuvKgAAAH1JREFUCNcdzLEOwjAMBNCrFDoXEGK3zA906RoifwBLmpVUSrMTpf59DNM9nU4HYNhjzJNlUaUPAzddN54NKU5uJsGoL4CYce9vA3mUJiLEHrURiRUoXQLRM6N2F4jrajcXm9YDJ+Xwx5AegX/Addn82eA4tah6QPwYNe75C+4yHrwP6fqUAAAAAElFTkSuQmCC") no-repeat left center;
    min-height: 16px;
    display: inline-block;
}

/* CHEMIN VERS L'ARTICLE (BREADCRUMBS), affiche les catégories parentes */
.breadcrumbs {
    margin-bottom: 1em;
    text-align: center;
}

.breadcrumbs ul {
    margin: 0;
}

.breadcrumbs ul li {
    display: inline-block;
}

.breadcrumbs ul li::before {
    content: "»";
    color: #999;
    margin: .5em;
}

.breadcrumbs ul li:nth-child(1)::before {
    display: none;
}

.breadcrumbs a {
    color: #666;
}


/* MESSAGES ALERTE ET ERREUR (par exemple : page non trouvée) */
.error {
    border-bottom: .2em solid #c00;
    border-radius: .5em;
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208







209
210
211
212
213
214
215
    padding: .5em;
    margin-bottom: 1em;
    font-size: 1.2em;
    color: #990;
}

/* AFFICHAGE D'UN ARTICLE */
section.articles article {
    clear: both;
    border-left: .2em solid #ccc;
    border-radius: .5em;
    padding-left: 1em;
}

section.articles article h3, section.articles article h1 {
    margin-bottom: .3em;
}








section.articles article h1 a {
    color: #000;
    text-decoration: none;
    font-weight: normal;
}








<
<
<
<
<
<
<



>
>
>
>
>
>
>







287
288
289
290
291
292
293







294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
    padding: .5em;
    margin-bottom: 1em;
    font-size: 1.2em;
    color: #990;
}

/* AFFICHAGE D'UN ARTICLE */







section.articles article h3, section.articles article h1 {
    margin-bottom: .3em;
}

section.articles article::after, article.single::after {
    content: "";
    display: block;
    height: 0px;
    clear: both;
}

section.articles article h1 a {
    color: #000;
    text-decoration: none;
    font-weight: normal;
}

225
226
227
228
229
230
231
232

233
234
235
236






237
238
239
240
241
242
243
section.articles article h5 {
    color: #666;
    font-weight: normal;
    font-size: .8em;
    margin-bottom: .3em;
}

section.page article {

    margin-bottom: 1em;
}

/* CONTENU DE L'ARTICLE */






article ul, article ol, article blockquote {
    margin-left: 2em;
}

article ul {
    list-style-type: disc;
}







|
>




>
>
>
>
>
>







320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
section.articles article h5 {
    color: #666;
    font-weight: normal;
    font-size: .8em;
    margin-bottom: .3em;
}

section.page > h1 {
    text-align: center;
    margin-bottom: 1em;
}

/* CONTENU DE L'ARTICLE */
article > h4 {
    margin-bottom: 1em;
    color: #666;
    font-weight: normal;
}

article ul, article ol, article blockquote {
    margin-left: 2em;
}

article ul {
    list-style-type: disc;
}
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354




355
356
357
358
359
360
361


362
363
364
365
366
367

368
369
370

371
372
373
374
375
376
377


378

379
380
381





382


383







384
385


386





387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
    box-shadow: 0px 0px 5px 2px orange;
}

fieldset dl dd {
    margin: .5em 1em;
}

/* Formulaire de recherche */
.search article p b {
    background: #ff9;
    padding: .2em 0;
}

.search-widget {
    float: right;
    margin-top: -1rem;
}

.search-widget input {
    padding: .2em;
    border: 1px solid #999;
    border-radius: .3rem;
    line-height: 1rem;
    line-height: .8rem;
    display: inline-block;
    vertical-align: middle;
}

.search-widget input[type=submit] {
    background: #999;
    font-size: 1.2rem;
    line-height: .8rem;
    color: #fff;
    cursor: pointer;
}

/* Modifications du style pour les petits écrans */
@media handheld, screen and (max-width: 980px) {
    body {
        padding: 0;
    }





    header.nav {
        font-size: .9em;
        margin: 0;
    }

    header.main {


        padding: 0 .2em;
        background-position: center top;
        text-align: center;
    }

    header.main h1 {

        font-size: 2em;
    }


    header.main .contacts {
        text-align: center;
        margin: .5em 0;
    }

    header.main .contacts * {
        font-size: .8em;


        display: block;

        margin: 0;
    }






    .search-widget {


        float: none;







        margin: .5em;
    }








    header.main nav {
        font-size: 1em;
        background: none;
    }

    section.page {
        margin: 0 .3em;
    }

    section.page h1 { font-size: 1.5em; }
    section.page h2 { font-size: 1.3em; }
    section.page h3 { font-size: 1.2em; }
    section.page h4 { font-size: 1em; }
    section.page h5 { font-size: .9em; }
    section.page h6 { font-size: .8em; }

    footer.main {
        background: #eee;
        padding: .2em;
        font-size: .8em;
    }
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<





>
>
>
>






|
>
>
|
<
<


|
>
|


>
|

|


|
|
>
>
|
>
|


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

>
>
|
>
>
>
>
>
|




|
|










<
<



416
417
418
419
420
421
422





























423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441


442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506


507
508
509
    box-shadow: 0px 0px 5px 2px orange;
}

fieldset dl dd {
    margin: .5em 1em;
}






























/* Modifications du style pour les petits écrans */
@media handheld, screen and (max-width: 980px) {
    body {
        padding: 0;
    }

    main, header.main {
        display: block;
    }

    header.nav {
        font-size: .9em;
        margin: 0;
    }

    header.main h1 a, header.main .contacts {
        border-radius: 0;
        margin: .2em 0;
        padding: 0;


    }

    header.main {
        background: #fff;
        padding: 1em;
    }


    header.main .contacts * {
        text-align: center;
        font-size: .8em;
    }

    header.main h1 a img {
        max-height: 100px;
        max-width: 100%;
    }

    header.main.home h1 a img {
        max-height: 300px;
    }

    .search-widget p {
        display: block;
        text-align: center;
    }

    .search-widget input {
        font-size: 1em;
    }

    nav.main ul, .subcategories {
        text-align: center;
    }

    nav.main ul li, .subcategories li {
        font-size: 1.2em;
        display: inline-block;
        margin: .3em;
    }
    nav.main ul li a, .subcategories li a {
        background: #fff;
    }

    .breadcrumbs {
        display: none;
    }

    nav.main {
        font-size: 1em;
        background: none;
    }

    section.page article {
        border-radius: 0;
    }

    section.page h1 { font-size: 1.5em; }
    section.page h2 { font-size: 1.3em; }
    section.page h3 { font-size: 1.2em; }
    section.page h4 { font-size: 1em; }
    section.page h5 { font-size: .9em; }
    section.page h6 { font-size: .8em; }

    footer.main {


        font-size: .8em;
    }
}

Modified src/skel-dist/web/gallery.html from [3e079fae32] to [29b010ae23].

1
2
3
4
5
6
7
8
9
<section class="gallery">
{{#images order="name" parent=$parent except_in_text=true}}
	<figure>
		<a href="{{$url}}" class="internal-image"><img src="{{$thumb_url}}" alt="{{$name}}" /></a>
	</figure>
{{/images}}
</section>

<script type="text/javascript" src="{{$admin_url}}static/scripts/wiki_gallery.js"></script>







<
<
1
2
3
4
5
6
7


<section class="gallery">
{{#images order="name" parent=$parent except_in_text=true}}
	<figure>
		<a href="{{$url}}" class="internal-image"><img src="{{$thumb_url}}" alt="{{$name}}" /></a>
	</figure>
{{/images}}
</section>


Modified src/skel-dist/web/index.html from [c2f019d97e] to [0ecb313040].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{{:include file="./_head.html"}}

{{#articles order="published DESC" future=false limit=1}}
<section class="articles main">
	<article>
		<h1><a href="{{ $url }}">{{ $title }}</a></h1>
		<h5>Posté : {{ $published|relative_date }}</h5>
		<p>{{ $html|raw }}</p>
	</article>
</section>
{{/articles}}

{{#articles order="published DESC" future=false begin=1 limit=9}}
<section class="articles">
	<article>
		<h3><a href="{{ $url }}">{{ $title }}</a></h3>
		<h5>Posté : {{ $published|relative_date }}</h5>
		<p>{{ $html|raw|strip_tags|truncate:200 }}</p>
	</article>
</section>
{{/articles}}

{{:include file="./_foot.html"}}
|





|









|






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{{:include file="./_head.html" home=true}}

{{#articles order="published DESC" future=false limit=1}}
<section class="articles main">
	<article>
		<h1><a href="{{ $url }}">{{ $title }}</a></h1>
		<h5>{{ $published|relative_date }}</h5>
		<p>{{ $html|raw }}</p>
	</article>
</section>
{{/articles}}

{{#articles order="published DESC" future=false begin=1 limit=9}}
<section class="articles">
	<article>
		<h3><a href="{{ $url }}">{{ $title }}</a></h3>
		<h5>{{ $published|relative_date }}</h5>
		<p>{{ $html|raw|strip_tags|truncate:200 }}</p>
	</article>
</section>
{{/articles}}

{{:include file="./_foot.html"}}

Modified src/templates/acc/accounts/journal.tpl from [a4a61762be] to [e73e00104c].

145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
			{/if}
			{if !$simple}<td></td>{/if}
			{if null !== $sum}
				{if !$simple}
				<td><b>Total</b></td>
				<td class="money">{$sum.debit|raw|money:false}</td>
				<td class="money">{$sum.credit|raw|money:false}</td>
				<td class="money">{$line.sum|raw|money:false}</td>
				{else}
				<td></td>
				<td colspan="2"><b>Total</b></td>
				<td class="money">{$line.sum|raw|money:false}</td>
				{/if}
			{else}
				<td colspan="4"></td>
			{/if}
			{if !$simple}<td></td>{/if}
			<td class="actions" colspan="5">
				{if $can_edit}







|



|







145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
			{/if}
			{if !$simple}<td></td>{/if}
			{if null !== $sum}
				{if !$simple}
				<td><b>Total</b></td>
				<td class="money">{$sum.debit|raw|money:false}</td>
				<td class="money">{$sum.credit|raw|money:false}</td>
				<td class="money"><strong>{$sum.balance|raw|money:false}</strong></td>
				{else}
				<td></td>
				<td colspan="2"><b>Total</b></td>
				<td class="money"><strong>{$sum.balance|raw|money:false}</strong></td>
				{/if}
			{else}
				<td colspan="4"></td>
			{/if}
			{if !$simple}<td></td>{/if}
			<td class="actions" colspan="5">
				{if $can_edit}

Modified src/templates/acc/charts/accounts/_account_form.tpl from [adc16430f8] to [4c02e8d69a].

1
2
3
4
5
6
7

8


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
<dl>
	{if !$account.type || !$create}
		{input type="select" label="Type de compte usuel" name="type" source=$account required=true options=$types}
		<dd class="help">Le statut de compte usuel est utilisé pour les écritures <em>«&nbsp;simplifiées&nbsp;»</em> (recettes, dépenses, dettes, créances, virements), pour la liste des comptes, et également pour proposer certaines fonctionnalités (rapprochement pour les comptes bancaires, règlement rapide de dette et créance, dépôt de chèques).</dd>
		<dd class="help">Un compte qui n'a pas de type usuel ne pourra être utilisé que dans une saisie avancée, et ne sera visible que dans les rapports de l'exercice.</dd>
	{else}
	<dt>Type de compte</dt>

	<dd><?php $t = $types[$account->type]; ?> {$t}</dd>


	{/if}

	{if !$account.type || $account.type == $account::TYPE_VOLUNTEERING}
		<dt><label for="f_position_0">Position au bilan ou résultat</label>{if !$edit_disabled} <b>(obligatoire)</b>{/if}</dt>
		<dd class="help">La position permet d'indiquer dans quelle partie du bilan ou du résultat doit figurer le compte.</dd>
		<dd class="help">Les comptes inscrits en actif ou passif figureront dans le bilan, alors que ceux inscrits en produit ou charge figureront au compte de résultat.</dd>
		{input type="radio" label="Ne pas utiliser ce compte au bilan ni au résultat" name="position" value=0 source=$account disabled=$edit_disabled}
		{if $account.type != $account::TYPE_VOLUNTEERING}
		{input type="radio" label="Bilan : actif" name="position" value=Entities\Accounting\Account::ASSET source=$account help="ce que possède l'association : stocks, locaux, soldes bancaires, etc." disabled=$edit_disabled}
		{input type="radio" label="Bilan : passif" name="position" value=Entities\Accounting\Account::LIABILITY source=$account help="ce que l'association doit : dettes, provisions, réserves, etc." disabled=$edit_disabled}
		{input type="radio" label="Bilan : actif ou passif" name="position" value=Entities\Accounting\Account::ASSET_OR_LIABILITY source=$account help="le compte sera placé à l'actif si son solde est débiteur, ou au passif s'il est créditeur" disabled=$edit_disabled}
		{/if}
		{input type="radio" label="Résultat : charge" name="position" value=Entities\Accounting\Account::EXPENSE source=$account help="dépenses" disabled=$edit_disabled}
		{input type="radio" label="Résultat : produit" name="position" value=Entities\Accounting\Account::REVENUE source=$account help="recettes" disabled=$edit_disabled}
	{/if}

	{input type="text" label="Code" maxlength="10" name="code" source=$account required=true help="Le code du compte sert à trier le compte dans le plan comptable, attention à choisir un code qui correspond au plan comptable." disabled=$edit_disabled}

	{input type="text" label="Libellé" name="label" source=$account required=true disabled=$edit_disabled}
	{input type="textarea" label="Description" name="description" source=$account}

	{if $create && in_array($account.type, [$account::TYPE_BANK, $account::TYPE_CASH, $account::TYPE_OUTSTANDING, $account::TYPE_THIRD_PARTY]) && !empty($current_year)}
		{input type="money" name="opening_amount" label="Solde d'ouverture" help="Si renseigné, ce solde sera inscrit dans l'exercice « %s »."|args:$current_year.label}
	{/if}
</dl>







>
|
>
>
















|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<dl>
	{if !$account.type || !$create}
		{input type="select" label="Type de compte usuel" name="type" source=$account required=true options=$types}
		<dd class="help">Le statut de compte usuel est utilisé pour les écritures <em>«&nbsp;simplifiées&nbsp;»</em> (recettes, dépenses, dettes, créances, virements), pour la liste des comptes, et également pour proposer certaines fonctionnalités (rapprochement pour les comptes bancaires, règlement rapide de dette et créance, dépôt de chèques).</dd>
		<dd class="help">Un compte qui n'a pas de type usuel ne pourra être utilisé que dans une saisie avancée, et ne sera visible que dans les rapports de l'exercice.</dd>
	{else}
	<dt>Type de compte</dt>
	<dd>
		<?php $t = $types[$account->type]; ?> {$t}
		<input type="hidden" name="type" value="{$account.type}" />
	</dd>
	{/if}

	{if !$account.type || $account.type == $account::TYPE_VOLUNTEERING}
		<dt><label for="f_position_0">Position au bilan ou résultat</label>{if !$edit_disabled} <b>(obligatoire)</b>{/if}</dt>
		<dd class="help">La position permet d'indiquer dans quelle partie du bilan ou du résultat doit figurer le compte.</dd>
		<dd class="help">Les comptes inscrits en actif ou passif figureront dans le bilan, alors que ceux inscrits en produit ou charge figureront au compte de résultat.</dd>
		{input type="radio" label="Ne pas utiliser ce compte au bilan ni au résultat" name="position" value=0 source=$account disabled=$edit_disabled}
		{if $account.type != $account::TYPE_VOLUNTEERING}
		{input type="radio" label="Bilan : actif" name="position" value=Entities\Accounting\Account::ASSET source=$account help="ce que possède l'association : stocks, locaux, soldes bancaires, etc." disabled=$edit_disabled}
		{input type="radio" label="Bilan : passif" name="position" value=Entities\Accounting\Account::LIABILITY source=$account help="ce que l'association doit : dettes, provisions, réserves, etc." disabled=$edit_disabled}
		{input type="radio" label="Bilan : actif ou passif" name="position" value=Entities\Accounting\Account::ASSET_OR_LIABILITY source=$account help="le compte sera placé à l'actif si son solde est débiteur, ou au passif s'il est créditeur" disabled=$edit_disabled}
		{/if}
		{input type="radio" label="Résultat : charge" name="position" value=Entities\Accounting\Account::EXPENSE source=$account help="dépenses" disabled=$edit_disabled}
		{input type="radio" label="Résultat : produit" name="position" value=Entities\Accounting\Account::REVENUE source=$account help="recettes" disabled=$edit_disabled}
	{/if}

	{input type="text" label="Numéro" maxlength="20" pattern="[A-Z0-9]+" name="code" source=$account required=true help="Le numéro du compte sert à trier le compte dans le plan comptable, attention à choisir un numéro qui correspond au plan comptable." disabled=$edit_disabled}
	<dd class="help">Le numéro ne doit contenir que des chiffres et des lettres majuscules.</dd>
	{input type="text" label="Libellé" name="label" source=$account required=true disabled=$edit_disabled}
	{input type="textarea" label="Description" name="description" source=$account}

	{if $create && in_array($account.type, [$account::TYPE_BANK, $account::TYPE_CASH, $account::TYPE_OUTSTANDING, $account::TYPE_THIRD_PARTY]) && !empty($current_year)}
		{input type="money" name="opening_amount" label="Solde d'ouverture" help="Si renseigné, ce solde sera inscrit dans l'exercice « %s »."|args:$current_year.label}
	{/if}
</dl>

Modified src/templates/acc/charts/accounts/_nav.tpl from [26818190bb] to [aee27c26ea].

1
2
3




4

5
6
7
8
9

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<nav class="tabs">
{if $dialog}
	{* JS trick to get back to the original iframe URL! *}




	<aside>{linkbutton shape="left" label="Retour à la sélection de compte" href="#" onclick="g.reloadParentDialog(); return false;"}</aside>

	<ul>
{else}
	<ul>

		<li class="current">{link href="!acc/charts/" label="Plans comptables"}</li>

		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
		<li>{link href="!acc/charts/import.php" label="Importer un plan comptable"}</li>
		{/if}
	</ul>
	<ul class="sub">
		<li class="title">{$chart.label}</li>
{/if}

		<li{if $current == 'favorites'} class="current"{/if}>{link href="!acc/charts/accounts/?id=%d"|args:$chart.id label="Comptes usuels"}</li>
		<li{if $current == 'all'} class="current"{/if}>{link href="!acc/charts/accounts/all.php?id=%d"|args:$chart.id label="Tous les comptes"}</li>
		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
			<li{if $current == 'new'} class="current"{/if}><strong>{link href="!acc/charts/accounts/new.php?id=%d"|args:$chart.id label="Ajouter un compte"}</strong></li>
		{/if}
	</ul>
</nav>



>
>
>
>
|
>



<

>
|
|
|
<






<
<
<


1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17

18
19
20
21
22
23



24
25
<nav class="tabs">
{if $dialog}
	{* JS trick to get back to the original iframe URL! *}
	<aside>
		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
			{linkbutton href="!acc/charts/accounts/new.php?id=%d"|args:$chart.id label="Ajouter un compte" shape="plus"}
		{/if}
		{linkbutton shape="left" label="Retour à la sélection de compte" href="#" onclick="g.reloadParentDialog(); return false;"}
	</aside>
	<ul>
{else}
	<ul>

		<li class="current">{link href="!acc/charts/" label="Plans comptables"}</li>
	</ul>
	{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
		<aside>{linkbutton href="!acc/charts/accounts/new.php?id=%d"|args:$chart.id label="Ajouter un compte" shape="plus"}</aside>
	{/if}

	<ul class="sub">
		<li class="title">{$chart.label}</li>
{/if}

		<li{if $current == 'favorites'} class="current"{/if}>{link href="!acc/charts/accounts/?id=%d"|args:$chart.id label="Comptes usuels"}</li>
		<li{if $current == 'all'} class="current"{/if}>{link href="!acc/charts/accounts/all.php?id=%d"|args:$chart.id label="Tous les comptes"}</li>



	</ul>
</nav>

Modified src/templates/acc/charts/accounts/new.tpl from [da7b7cc99e] to [7f64222281].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Nouveau compte" current="acc/charts"}

{include file="acc/charts/accounts/_nav.tpl" current="new"}

{form_errors}

{if null === $type}

<form method="get" action="{$self_url}" data-focus="1">
	<fieldset>
		<legend>Créer un nouveau compte</legend>
		<dl><label for="f_type">Type de compte</label></dl>
		{foreach from=$types_create item="t" key="v"}
			{input type="radio-btn" name="type" value=$v label=$t.label help=$t.help}
		{/foreach}
	</fieldset>






|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{include file="admin/_head.tpl" title="Nouveau compte" current="acc/charts"}

{include file="acc/charts/accounts/_nav.tpl" current="new"}

{form_errors}

{if !isset($account->type)}

<form method="post" action="{$self_url}" data-focus="1">
	<fieldset>
		<legend>Créer un nouveau compte</legend>
		<dl><label for="f_type">Type de compte</label></dl>
		{foreach from=$types_create item="t" key="v"}
			{input type="radio-btn" name="type" value=$v label=$t.label help=$t.help}
		{/foreach}
	</fieldset>

Modified src/templates/acc/charts/accounts/selector.tpl from [f3ae24f199] to [b961bcab9e].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18



19
20

21
22
23


24
25
26
27
28
29
30
{include file="admin/_head.tpl" title="Sélectionner un compte"}

{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
	<?php $page = isset($grouped_accounts) ? '' : 'all.php'; ?>
	<nav class="tabs">
		<aside>{linkbutton label="Modifier les comptes" href="!acc/charts/accounts/%s?id=%d"|args:$page,$chart.id shape="edit"}</aside>
	</nav>
{/if}

{if empty($grouped_accounts) && empty($accounts)}
	<p class="block alert">Le plan comptable ne comporte aucun compte de ce type.<br />
		{linkbutton href="!acc/charts/accounts/new.php?id=%s&type=%s"|args:$chart.id,$targets[0] label="Créer un compte" shape="plus"}
	</p>

{else}


	<h2 class="ruler">
		<input type="text" placeholder="Recherche rapide" id="lookup" />



		{if !isset($grouped_accounts)}
		<label>{input type="checkbox" name="typed_only" value=0 default=0 default=$all} N'afficher que les comptes usuels</label>

		{/if}
	</h2>




	{if isset($grouped_accounts)}
		<?php $index = 1; ?>
		{foreach from=$grouped_accounts item="group"}
			<h2 class="ruler">{$group.label}</h2>

			<table class="list">


<
<
|
<
<
<








>
|
|
>
>
>
|
<
>

<

>
>







1
2


3



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20

21
22
23
24
25
26
27
28
29
30
{include file="admin/_head.tpl" title="Sélectionner un compte"}



<div class="selector">




{if empty($grouped_accounts) && empty($accounts)}
	<p class="block alert">Le plan comptable ne comporte aucun compte de ce type.<br />
		{linkbutton href="!acc/charts/accounts/new.php?id=%s&type=%s"|args:$chart.id,$targets[0] label="Créer un compte" shape="plus"}
	</p>

{else}

	<header>
		<h2>
			<input type="text" placeholder="Recherche rapide" id="lookup" />
		</h2>

		{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
			<?php $page = isset($grouped_accounts) ? '' : 'all.php'; ?>

			<p class="edit">{linkbutton label="Modifier les comptes" href="!acc/charts/accounts/%s?id=%d"|args:$page,$chart.id shape="edit"}</aside></p>
		{/if}


		<p><label>{input type="checkbox" name="typed_only" value=0 default=0 default=$all} N'afficher que les comptes usuels</label></p>
	</header>

	{if isset($grouped_accounts)}
		<?php $index = 1; ?>
		{foreach from=$grouped_accounts item="group"}
			<h2 class="ruler">{$group.label}</h2>

			<table class="list">
63
64
65
66
67
68
69


70
71
72
73
				</tr>
			{/foreach}
			</tbody>
		</table>

	{/if}
{/if}



<script type="text/javascript" src="{$admin_url}static/scripts/selector.js?{$version_hash}"></script>

{include file="admin/_foot.tpl"}







>
>




63
64
65
66
67
68
69
70
71
72
73
74
75
				</tr>
			{/foreach}
			</tbody>
		</table>

	{/if}
{/if}

</div>

<script type="text/javascript" src="{$admin_url}static/scripts/selector.js?{$version_hash}"></script>

{include file="admin/_foot.tpl"}

Added src/templates/acc/transactions/_form.tpl version [ab4d1203c7].

































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php

$is_new = empty($_POST) && !isset($transaction->type) && !$transaction->exists() && !$transaction->label;
$is_quick = count(array_intersect_key($_GET, array_flip(['a', 'l', 'd', 't', 'account']))) > 0;

?>
<form method="post" action="{$self_url}" data-focus="{if $is_new || $is_quick}1{else}#f_date{/if}">
	{form_errors}

	<fieldset>
		<legend>Type d'écriture</legend>
		<dl>
		{foreach from=$types_details item="type"}
			<dd class="radio-btn">
				{input type="radio" name="type" value=$type.id source=$transaction label=null}
				<label for="f_type_{$type.id}">
					<div>
						<h3>{$type.label}</h3>
						{if !empty($type.help)}
							<p class="help">{$type.help}</p>
						{/if}
					</div>
				</label>
			</dd>
		{/foreach}
		</dl>
	</fieldset>

	<fieldset{if $is_new} class="hidden"{/if}>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" label="Date" required=1 source=$transaction}
			{input type="text" name="label" label="Libellé" required=1 source=$transaction}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc." source=$transaction}
		</dl>
		<dl data-types="all-but-advanced">
			{input type="money" name="amount" label="Montant" required=1 default=$amount}
		</dl>
	</fieldset>

	{foreach from=$types_details item="type"}
		<fieldset data-types="t{$type.id}"{if $is_new} class="hidden"{/if}>
			<legend>{$type.label}</legend>
			{if $type.id == $transaction::TYPE_ADVANCED}
				{* Saisie avancée *}
				{include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart}
			{else}
				<dl>
				{foreach from=$type.accounts key="key" item="account"}
					{input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name=$account.selector_name label=$account.label required=1 default=$account.selector_value}
				{/foreach}
				</dl>
			{/if}
		</fieldset>
	{/foreach}

	<fieldset{if $is_new} class="hidden"{/if}>
		<legend>Détails facultatifs</legend>
		<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$transaction->payment_reference()}
		</dl>
		<dl>
			{input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php" default=$linked_users}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30 source=$transaction}
		</dl>
		<dl data-types="t{$transaction::TYPE_ADVANCED}">
			{input type="number" name="id_related" label="Lier à l'écriture numéro" source=$transaction help="Indiquer ici un numéro d'écriture pour faire le lien par exemple avec une dette"}
		</dl>
		<dl data-types="all-but-advanced">
			{if count($analytical_accounts) > 1}
				{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$id_analytical}
			{/if}
		</dl>
	</fieldset>

	<p class="submit{if $is_new} hidden{/if}">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
	<p class="submit help{if $is_new} hidden{/if}">
		Vous pourrez ajouter des fichiers à cette écriture une fois qu'elle aura été enregistrée.
	</p>
{/if}

</form>

<script type="text/javascript" async="async">
let is_new = {$is_new|escape:'json'};
{literal}
window.addEventListener('load', () => {
	g.script('scripts/accounting.js', () => { initTransactionForm(is_new && !$('.block').length); });
});
</script>
{/literal}

Modified src/templates/acc/transactions/_lines_form.tpl from [8a44524a1d] to [bb94f0b609].

1
2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
<?php
assert(is_array($lines));
assert(is_array($analytical_accounts));
assert(!isset($lines_accounts) || is_array($lines_accounts));
?>

<table class="list transaction-lines">
	<thead>
		<tr>
			<td>Compte</td>
			<td>Débit</td>
			<td>Crédit</td>
			<td>Réf. ligne</td>
			<td>Libellé ligne</td>

			{if count($analytical_accounts) > 1}
				<td>Projet</td>
			{/if}
			<td></td>
		</tr>
	</thead>
	<tbody>
	{foreach from=$lines key="k" item="line"}
		<tr>
			<td>
				{input type="list" target="!acc/charts/accounts/selector.php?chart=%d"|args:$chart_id name="lines[account][]" default=$line.account}
			</td>
			<td class="money">{input type="money" name="lines[debit][]" default=$line.debit size=5}</td>
			<td class="money">{input type="money" name="lines[credit][]" default=$line.credit size=5}</td>
			<td>{input type="text" name="lines[reference][]" default=$line.reference size=10}</td>
			<td>{input type="text" name="lines[label][]" default=$line.label}</td>

			{if count($analytical_accounts) > 1}
				<td>{input default=$line.id_analytical type="select" name="lines[id_analytical][]" options=$analytical_accounts}</td>
			{/if}
			<td>{button label="Enlever" title="Enlever la ligne" shape="minus" min="2" name="remove_line"}</td>
		</tr>
	{/foreach}
	</tbody>












<

>









|
|



<

>







1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36
37
<?php
assert(is_array($lines));
assert(is_array($analytical_accounts));
assert(!isset($lines_accounts) || is_array($lines_accounts));
?>

<table class="list transaction-lines">
	<thead>
		<tr>
			<td>Compte</td>
			<td>Débit</td>
			<td>Crédit</td>

			<td>Libellé ligne</td>
			<td>Réf. ligne</td>
			{if count($analytical_accounts) > 1}
				<td>Projet</td>
			{/if}
			<td></td>
		</tr>
	</thead>
	<tbody>
	{foreach from=$lines key="k" item="line"}
		<tr>
			<td class="account">
				{input type="list" target="!acc/charts/accounts/selector.php?chart=%d"|args:$chart_id name="lines[account_selector][]" default=$line.account_selector}
			</td>
			<td class="money">{input type="money" name="lines[debit][]" default=$line.debit size=5}</td>
			<td class="money">{input type="money" name="lines[credit][]" default=$line.credit size=5}</td>

			<td>{input type="text" name="lines[label][]" default=$line.label}</td>
			<td>{input type="text" name="lines[reference][]" default=$line.reference size=10}</td>
			{if count($analytical_accounts) > 1}
				<td>{input default=$line.id_analytical type="select" name="lines[id_analytical][]" options=$analytical_accounts}</td>
			{/if}
			<td>{button label="Enlever" title="Enlever la ligne" shape="minus" min="2" name="remove_line"}</td>
		</tr>
	{/foreach}
	</tbody>

Modified src/templates/acc/transactions/edit.tpl from [a98552bace] to [4750e1b87b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
{include file="admin/_head.tpl" title="Modification d'une écriture" current="acc/simple"}

<form method="post" action="{$self_url}" data-focus="#f_date">
	{form_errors}

	{if $has_reconciled_lines}
	<p class="alert block">
		Attention, cette écriture contient des lignes qui ont été rapprochées. La modification de cette écriture entraînera la perte du rapprochement.
	</p>
	{/if}

	<fieldset>
		<legend>Type d'écriture</legend>
		<dl>
		{foreach from=$types_details item="type"}
			<dd class="radio-btn">
				{input type="radio" name="type" value=$type.id source=$transaction label=null}
				<label for="f_type_{$type.id}">
					<div>
						<h3>{$type.label}</h3>
						{if !empty($type.help)}
							<p>{$type.help}</p>
						{/if}
					</div>
				</label>
			</dd>
		{/foreach}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" label="Date" required=1 source=$transaction}
			{input type="text" name="label" label="Libellé" required=1 source=$transaction}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc." source=$transaction}
		</dl>
		<dl data-types="all-but-advanced">
			{input type="money" name="amount" label="Montant" required=1 default=$amount}
		</dl>
	</fieldset>

	{foreach from=$types_details item="type"}
		<fieldset data-types="t{$type.id}">
			<legend>{$type.label}</legend>
			{if $type.id == $transaction::TYPE_ADVANCED}
				{* Saisie avancée *}
				{include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart}
			{else}
				<dl>
				{foreach from=$type.accounts key="key" item="account"}
					<?php $selected = $types_accounts[$key] ?? null; ?>
					{input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1 default=$selected}
				{/foreach}
				</dl>
			{/if}
		</fieldset>
	{/foreach}

	<fieldset>
		<legend>Détails facultatifs</legend>
		<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$first_line.reference}
		</dl>
		<dl>
			{input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php" default=$linked_users}
			{input type="textarea" name="notes" label="Remarques" rows=2 cols=30 source=$transaction}
			{input type="number" name="id_related" label="Lier à l'écriture numéro" source=$transaction help="Indiquer ici un numéro d'écriture pour faire le lien par exemple avec une dette"}
		</dl>
		<dl data-types="all-but-advanced">
			{if count($analytical_accounts) > 1}
				{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$first_line.id_analytical}
			{/if}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_edit_%d"|args:$transaction.id}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

</form>

{literal}
<script type="text/javascript" defer="defer" async="async">
g.script('scripts/accounting.js', () => { initTransactionForm(); });
</script>
{/literal}

{include file="admin/_foot.tpl"}


<
<
<
|
|
|
|
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

1
2



3
4
5
6
7
8




































9










10






























11
{include file="admin/_head.tpl" title="Modification d'une écriture" current="acc/simple"}




{if $has_reconciled_lines}
<p class="alert block">
	Attention, cette écriture contient des lignes qui ont été rapprochées. La modification de cette écriture entraînera la perte du rapprochement.
</p>
{/if}





































{include file="./_form.tpl"}









































{include file="admin/_foot.tpl"}

Modified src/templates/acc/transactions/new.tpl from [a11127bdf1] to [80c4d6b42d].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"}

{include file="acc/_year_select.tpl"}

<form method="post" action="{$self_url}" data-focus="1">
	{form_errors}

	<fieldset>
		<legend>Type d'écriture</legend>
		<dl>
		{foreach from=$types_details item="type"}
			<dd class="radio-btn">
				{input type="radio" name="type" value=$type.id source=$transaction label=null}
				<label for="f_type_{$type.id}">
					<div>
						<h3>{$type.label}</h3>
						{if !empty($type.help)}
							<p class="help">{$type.help}</p>
						{/if}
					</div>
				</label>
			</dd>
		{/foreach}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" label="Date" required=1 source=$transaction}
			{input type="text" name="label" label="Libellé" required=1 source=$transaction}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc." source=$transaction}
		</dl>
		<dl data-types="all-but-advanced">
			{input type="money" name="amount" label="Montant" required=1 default=$amount}
		</dl>
	</fieldset>

	{foreach from=$types_details item="type"}
		<fieldset data-types="t{$type.id}">
			<legend>{$type.label}</legend>
			{if $type.id == $transaction::TYPE_ADVANCED}
				{* Saisie avancée *}
				{include file="acc/transactions/_lines_form.tpl" chart_id=$current_year.id_chart}
			{else}
				<dl>
				{foreach from=$type.accounts key="key" item="account"}
					<?php $selected = $types_accounts[$key] ?? null; ?>
					{input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$account.targets_string,$chart_id name="account_%d_%d"|args:$type.id,$key label=$account.label required=1 default=$selected}
				{/foreach}
				</dl>
			{/if}
		</fieldset>
	{/foreach}

	<fieldset>
		<legend>Détails facultatifs</legend>
		<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$transaction->payment_reference()}
		</dl>
		<dl>
			{input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php"}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}
		</dl>
		<dl data-types="t{$transaction::TYPE_ADVANCED}">
			{input type="number" name="id_related" label="Lier à l'écriture numéro" source=$transaction help="Indiquer ici un numéro d'écriture pour faire le lien par exemple avec une dette"}
		</dl>
		<dl data-types="all-but-advanced">
			{if count($analytical_accounts) > 1}
				{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$id_analytical}
			{/if}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="acc_transaction_new"}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
	</p>

{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE)}
	<p class="submit help">
		Vous pourrez ajouter des fichiers à cette écriture une fois qu'elle aura été enregistrée.
	</p>
{/if}

</form>

<script type="text/javascript" async="async">
let is_new = {if null !== $transaction->type}false{else}true{/if};
{literal}
window.addEventListener('load', () => {
	g.script('scripts/accounting.js', () => { initTransactionForm(is_new && !$('.block').length); });
});
</script>
{/literal}

{include file="admin/_foot.tpl"}




<
<
<
<
<
<
<
<
<
<
<
<
|
|
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<


<
|
<
<
<
<
<
<
<
<


1
2
3
4












5
6







7



















































8





9
10

11








12
13
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"}

{include file="acc/_year_select.tpl"}













{if !empty($duplicate_from)}
<p class="help block">







	Cette saisie est dupliquée depuis l'écriture {link class="num" href="details.php?id=%d"|args:$duplicate_from label="#%d"|args:$duplicate_from}



















































</p>





{/if}


{include file="./_form.tpl"}









{include file="admin/_foot.tpl"}

Modified src/templates/acc/transactions/payoff.tpl from [aaa45e359f] to [1f17fcac23].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"}

{include file="acc/_year_select.tpl"}

<form method="post" action="{$self_url}" data-focus="1">
	{form_errors}

	<input type="hidden" name="type" value="{$transaction.type}" />
	<input type="hidden" name="{$payoff_for.form_account_name}[{$payoff_for.id_account}]" value="-" />
	<fieldset>
		<legend>{if $payoff_for.type == $transaction::TYPE_DEBT}Règlement de dette{else}Règlement de créance{/if}</legend>
		<dl>
			<dt>Écriture d'origine</dt>
			<dd>{link class="num" href="!acc/transactions/details.php?id=%d"|args:$payoff_for.id label="#%d"|args:$payoff_for.id}</dd>
			{input type="checkbox" name="mark_paid" value="1" default="1" label="Marquer comme payée"}
			{input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$payoff_targets,$chart_id name=$payoff_for.form_target_name label="Compte de règlement" required=1}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" label="Date" required=1 source=$transaction}







<
<






|







1
2
3
4
5
6
7


8
9
10
11
12
13
14
15
16
17
18
19
20
21
{include file="admin/_head.tpl" title="Saisie d'une écriture" current="acc/new"}

{include file="acc/_year_select.tpl"}

<form method="post" action="{$self_url}" data-focus="1">
	{form_errors}



	<fieldset>
		<legend>{if $payoff_for.type == $transaction::TYPE_DEBT}Règlement de dette{else}Règlement de créance{/if}</legend>
		<dl>
			<dt>Écriture d'origine</dt>
			<dd>{link class="num" href="!acc/transactions/details.php?id=%d"|args:$payoff_for.id label="#%d"|args:$payoff_for.id}</dd>
			{input type="checkbox" name="mark_paid" value="1" default="1" label="Marquer comme payée"}
			{input type="list" target="!acc/charts/accounts/selector.php?targets=%s&chart=%d"|args:$payoff_targets,$chart_id name="account" label="Compte de règlement" required=1}
		</dl>
	</fieldset>

	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="date" name="date" label="Date" required=1 source=$transaction}

Modified src/templates/acc/years/balance.tpl from [e9fb58eac7] to [e51bf24f9d].

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
						<td>
							{$line.code} — {$line.label}
							<input type="hidden" name="lines[code][]" value="{$line.code}" />
							<input type="hidden" name="lines[label][]" value="{$line.label}" />
						</td>
					{/if}
					<th>
						{input type="list" target="!acc/charts/accounts/selector.php?chart=%d"|args:$year.id_chart name="lines[account][]" default=$line.account}
					</th>
					<td>{input type="money" name="lines[debit][]" default=$line.debit size=5}</td>
					<td>{input type="money" name="lines[credit][]" default=$line.credit size=5}</td>
					<td>{button label="Enlever la ligne" shape="minus" min="1" name="remove_line"}</td>
				</tr>
			{/foreach}
			</tbody>







|







77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
						<td>
							{$line.code} — {$line.label}
							<input type="hidden" name="lines[code][]" value="{$line.code}" />
							<input type="hidden" name="lines[label][]" value="{$line.label}" />
						</td>
					{/if}
					<th>
						{input type="list" target="!acc/charts/accounts/selector.php?chart=%d"|args:$year.id_chart name="lines[account_selector][]" default=$line.account}
					</th>
					<td>{input type="money" name="lines[debit][]" default=$line.debit size=5}</td>
					<td>{input type="money" name="lines[credit][]" default=$line.credit size=5}</td>
					<td>{button label="Enlever la ligne" shape="minus" min="1" name="remove_line"}</td>
				</tr>
			{/foreach}
			</tbody>

Modified src/templates/admin/_head.tpl from [8850055686] to [457f93d308].

123
124
125
126
127
128
129

130

131
132
133
134
135
136
137
        {if $help_url}
        <li>
            <h3><a href="{$help_url}" target="_dialog"><b data-icn="{icon html=false shape="help"}"></b><span>Aide</span></a></h3>
        </li>
        {/if}

    {elseif !defined('Garradin\INSTALL_PROCESS')}

        <li><a href="{if $config.site_asso}{$config.site_asso}{else}{$www_url}{/if}">&larr; Retour au site</a></li>

        <li><a href="{$admin_url}">Connexion</a>
            <ul>
                <li><a href="{$admin_url}password.php">Mot de passe perdu</a>
            </ul>
        </li>
    {/if}
    </ul>







>

>







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
        {if $help_url}
        <li>
            <h3><a href="{$help_url}" target="_dialog"><b data-icn="{icon html=false shape="help"}"></b><span>Aide</span></a></h3>
        </li>
        {/if}

    {elseif !defined('Garradin\INSTALL_PROCESS')}
    {if $config.site_asso || !$config.site_disabled}
        <li><a href="{if $config.site_asso}{$config.site_asso}{else}{$www_url}{/if}">&larr; Retour au site</a></li>
    {/if}
        <li><a href="{$admin_url}">Connexion</a>
            <ul>
                <li><a href="{$admin_url}password.php">Mot de passe perdu</a>
            </ul>
        </li>
    {/if}
    </ul>

Modified src/templates/services/user/_service_user_form.tpl from [018a3a1cfd] to [c41c9e6aa9].

39
40
41
42
43
44
45


46
47
48
49
50
51
52
53
54
55
56
57
58
			<dd><em>{if $copy_service_only_paid}(seulement les inscriptions marquées comme payées){else}(toutes les inscriptions){/if}</em><input type="hidden" name="copy_service_only_paid" value="{$copy_service_only_paid}" /></dd>
		{/if}

			<dt><label for="f_service_ID">Activité</label> <b>(obligatoire)</b></dt>

			{if $has_past_services}
			<dd>


				{if $current_only}
					Seules les activités courantes sont affichées.
					{button name="past_services" value="1" shape="reset" type="submit" label="Inscrire à une activité passée"}
				{else}
					Seules les activités passées sont affichées.
					{button name="past_services" value="0" shape="left" type="submit" label="Inscrire à une activité courante"}
				{/if}
			</dd>
			{/if}


			{foreach from=$grouped_services item="service"}
				<dd class="radio-btn">







>
>


|


|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
			<dd><em>{if $copy_service_only_paid}(seulement les inscriptions marquées comme payées){else}(toutes les inscriptions){/if}</em><input type="hidden" name="copy_service_only_paid" value="{$copy_service_only_paid}" /></dd>
		{/if}

			<dt><label for="f_service_ID">Activité</label> <b>(obligatoire)</b></dt>

			{if $has_past_services}
			<dd>
				{* We can't use a button type="submit" here because it would trigger when user presses Enter, instead of the true submit button *}
				<input type="hidden" name="past_services" value="{$current_only}" />
				{if $current_only}
					Seules les activités courantes sont affichées.
					{button value="1" shape="reset" type="button" onclick="this.form.past_services=this.value; this.form.submit();" label="Inscrire à une activité passée"}
				{else}
					Seules les activités passées sont affichées.
					{button value="0" shape="left" type="button"  onclick="this.form.past_services=this.value; this.form.submit();" label="Inscrire à une activité courante"}
				{/if}
			</dd>
			{/if}


			{foreach from=$grouped_services item="service"}
				<dd class="radio-btn">
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

		{if !empty($users)}
		<p class="help">Une écriture sera créée pour chaque membre inscrit.</p>
		{/if}

		<dl>
			{input type="money" name="amount" label="Montant réglé par le membre" required=true help="En cas de règlement en plusieurs fois il sera possible d'ajouter des règlements via la page de suivi des activités de ce membre."}
			{input type="list" target="!acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=true}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."}
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."}
			{input type="textarea" name="notes" label="Remarques"}
		</dl>
	</fieldset>
	{/if}








|







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

		{if !empty($users)}
		<p class="help">Une écriture sera créée pour chaque membre inscrit.</p>
		{/if}

		<dl>
			{input type="money" name="amount" label="Montant réglé par le membre" required=true help="En cas de règlement en plusieurs fois il sera possible d'ajouter des règlements via la page de suivi des activités de ce membre."}
			{input type="list" target="!acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account_selector" label="Compte de règlement" required=true}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."}
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."}
			{input type="textarea" name="notes" label="Remarques"}
		</dl>
	</fieldset>
	{/if}

Modified src/templates/services/user/payment.tpl from [d2a8c9e74c] to [87ebf9ce37].

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
		<dl>
			<dt>Membre sélectionné</dt>
			<dd><h3>{$user_name}</h3></dd>
			<dt><strong>Inscription</strong></dt>
			{input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"}
			{input type="date" name="date" label="Date" required=1 source=$su}
			{input type="money" name="amount" label="Montant réglé par le membre" required=1}
			{input type="list" target="!acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account" label="Compte de règlement" required=1}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."}
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}
		{button type="submit" name="save_and_add_payment" label="Enregistrer et ajouter un autre règlement" shape="plus"}
	</p>

</form>

{include file="admin/_foot.tpl"}







|








<





10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26
27
28
29
30
		<dl>
			<dt>Membre sélectionné</dt>
			<dd><h3>{$user_name}</h3></dd>
			<dt><strong>Inscription</strong></dt>
			{input type="checkbox" name="paid" value="1" default=$su.paid label="Marquer cette inscription comme payée"}
			{input type="date" name="date" label="Date" required=1 source=$su}
			{input type="money" name="amount" label="Montant réglé par le membre" required=1}
			{input type="list" target="!acc/charts/accounts/selector.php?targets=%s"|args:$account_targets name="account_selector" label="Compte de règlement" required=1}
			{input type="text" name="reference" label="Numéro de pièce comptable" help="Numéro de facture, de reçu, de note de frais, etc."}
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc."}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="save" label="Enregistrer" shape="right" class="main"}

	</p>

</form>

{include file="admin/_foot.tpl"}

Modified src/www/admin/acc/charts/accounts/new.php from [427dda7d05] to [78ea9bc506].

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$account = new Account;
$account->position = Account::ASSET_OR_LIABILITY;

$types = $account::TYPES_NAMES;
$types[0] = '-- Pas un compte usuel';

// Simple creation with pre-determined account type
if (qg('type') !== null) {
	$account->type = (int)qg('type');
	$account->position = Accounts::getPositionFromType($account->type);
	$account->code = $accounts->getNextCodeForType($account->type);
}

$form->runIf('save', function () use ($account, $accounts, $chart, $current_year) {
	$db = DB::getInstance();








|
|







27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$account = new Account;
$account->position = Account::ASSET_OR_LIABILITY;

$types = $account::TYPES_NAMES;
$types[0] = '-- Pas un compte usuel';

// Simple creation with pre-determined account type
if (f('type') !== null) {
	$account->type = (int)f('type');
	$account->position = Accounts::getPositionFromType($account->type);
	$account->code = $accounts->getNextCodeForType($account->type);
}

$form->runIf('save', function () use ($account, $accounts, $chart, $current_year) {
	$db = DB::getInstance();

69
70
71
72
73
74
75

76
77
78
79
80
81
82
83

	$page = '';

	if (!$account->type) {
		$page = 'all.php';
	}


	Utils::redirect(sprintf('%sacc/charts/accounts/%s?id=%d', ADMIN_URL, $page, $account->id_chart));
}, 'acc_accounts_new');

$types_create = [
	Account::TYPE_BANK => [
		'label' => Account::TYPES_NAMES[Account::TYPE_BANK],
		'help' => 'Compte bancaire, livret, ou intermédiaire financier (type HelloAsso, Paypal, Stripe, SumUp, etc.)',
	],







>
|







69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

	$page = '';

	if (!$account->type) {
		$page = 'all.php';
	}

	$url = sprintf('!acc/charts/accounts/%s?id=%d', $page, $account->id_chart);
	Utils::redirect($url);
}, 'acc_accounts_new');

$types_create = [
	Account::TYPE_BANK => [
		'label' => Account::TYPES_NAMES[Account::TYPE_BANK],
		'help' => 'Compte bancaire, livret, ou intermédiaire financier (type HelloAsso, Paypal, Stripe, SumUp, etc.)',
	],
112
113
114
115
116
117
118
119
120
121
	Account::TYPE_NONE => [
		'label' => 'Autre type de compte',
	],
];

$type = $account->type;

$tpl->assign(compact('types', 'types_create', 'account', 'chart', 'type'));

$tpl->display('acc/charts/accounts/new.tpl');







|


113
114
115
116
117
118
119
120
121
122
	Account::TYPE_NONE => [
		'label' => 'Autre type de compte',
	],
];

$type = $account->type;

$tpl->assign(compact('types', 'types_create', 'account', 'chart'));

$tpl->display('acc/charts/accounts/new.tpl');

Modified src/www/admin/acc/charts/accounts/selector.php from [0b96176cb9] to [7a48c2d2f1].

13
14
15
16
17
18
19








20
21
22
23
24
25
26
27
28
29
$targets = qg('targets');
$targets = $targets ? explode(':', $targets) : [];
$chart = (int) qg('chart') ?: null;

$targets = array_map('intval', $targets);
$targets_str = implode(':', $targets);









// Cache the page until the charts have changed
$last_change = Config::getInstance()->get('last_chart_change') ?: time();
$hash = sha1($targets_str . $chart . $last_change);

// Exit if there's no need to reload
Utils::HTTPCache($hash, null, 10);

if ($chart) {
	$chart = Charts::get($chart);
}







>
>
>
>
>
>
>
>


|







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$targets = qg('targets');
$targets = $targets ? explode(':', $targets) : [];
$chart = (int) qg('chart') ?: null;

$targets = array_map('intval', $targets);
$targets_str = implode(':', $targets);

$all = qg('all');

if (null !== $all) {
	$session->set('account_selector_all', (bool) $all);
}

$all = (bool) $session->get('account_selector_all');

// Cache the page until the charts have changed
$last_change = Config::getInstance()->get('last_chart_change') ?: time();
$hash = sha1($targets_str . $chart . $last_change . '=' . $all);

// Exit if there's no need to reload
Utils::HTTPCache($hash, null, 10);

if ($chart) {
	$chart = Charts::get($chart);
}
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59



60
61
62
63
64
65
66
	throw new UserException('Aucun exercice ouvert disponible');
}

$accounts = $chart->accounts();

$tpl->assign(compact('chart', 'targets', 'targets_str'));

$all = qg('all');

if (null !== $all) {
	$session->set('account_selector_all', (bool) $all);
}

$all = (bool) $session->get('account_selector_all');

if (!count($targets)) {
	$tpl->assign('accounts', !$all ? $accounts->listCommonTypes() : $accounts->listAll());
}



else {
	$tpl->assign('grouped_accounts', $accounts->listCommonGrouped($targets));
}

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

$tpl->display('acc/charts/accounts/selector.tpl');







<
<
<
<
<
<
<
<



>
>
>







50
51
52
53
54
55
56








57
58
59
60
61
62
63
64
65
66
67
68
69
	throw new UserException('Aucun exercice ouvert disponible');
}

$accounts = $chart->accounts();

$tpl->assign(compact('chart', 'targets', 'targets_str'));









if (!count($targets)) {
	$tpl->assign('accounts', !$all ? $accounts->listCommonTypes() : $accounts->listAll());
}
elseif ($all) {
	$tpl->assign('accounts', $accounts->listAll($targets));
}
else {
	$tpl->assign('grouped_accounts', $accounts->listCommonGrouped($targets));
}

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

$tpl->display('acc/charts/accounts/selector.tpl');

Modified src/www/admin/acc/transactions/details.php from [0d689a34de] to [5aa851b584].

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

$form->runIf('mark_paid', function () use ($transaction) {
	$transaction->markPaid();
	$transaction->save();
}, $csrf_key, Utils::getSelfURI());

$variables = compact('csrf_key', 'transaction') + [
	'transaction_lines'    => $transaction->getLinesWithAccounts(false),
	'transaction_year'     => $transaction->year(),
	'files'                => $transaction->listFiles(),
	'creator_name'         => $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null,
	'files_edit'           => $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE),
	'file_parent'          => $transaction->getAttachementsDirectory(),
	'related_users'        => $transaction->listLinkedUsers(),
	'related_transactions' => $transaction->listRelatedTransactions()
];

$tpl->assign($variables);
$tpl->assign('snippets', UserForms::getSnippets(UserForm::SNIPPET_TRANSACTION, $variables));

$tpl->display('acc/transactions/details.tpl');







|













17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

$form->runIf('mark_paid', function () use ($transaction) {
	$transaction->markPaid();
	$transaction->save();
}, $csrf_key, Utils::getSelfURI());

$variables = compact('csrf_key', 'transaction') + [
	'transaction_lines'    => $transaction->getLinesWithAccounts(),
	'transaction_year'     => $transaction->year(),
	'files'                => $transaction->listFiles(),
	'creator_name'         => $transaction->id_creator ? (new Membres)->getNom($transaction->id_creator) : null,
	'files_edit'           => $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_WRITE),
	'file_parent'          => $transaction->getAttachementsDirectory(),
	'related_users'        => $transaction->listLinkedUsers(),
	'related_transactions' => $transaction->listRelatedTransactions()
];

$tpl->assign($variables);
$tpl->assign('snippets', UserForms::getSnippets(UserForm::SNIPPET_TRANSACTION, $variables));

$tpl->display('acc/transactions/details.tpl');

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

26
27
28
29
30
31
32


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
if ($year->closed) {
	throw new UserException('Cette écriture ne peut être modifiée car elle appartient à un exercice clôturé');
}

$chart = $year->chart();
$accounts = $chart->accounts();



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

$rules = [
	'lines' => 'array|required',
];

if (f('save') && $form->check('acc_edit_' . $transaction->id(), $rules)) {
	try {
		$transaction->importFromEditForm();
		$transaction->save();

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

		Utils::redirect(ADMIN_URL . 'acc/transactions/details.php?id=' . $transaction->id());
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$types_accounts = [];
$lines = [];

if (!empty($_POST['lines']) && is_array($_POST['lines'])) {
	$lines = Utils::array_transpose($_POST['lines']);

	foreach ($lines as &$line) {
		$line = (object) $line;
		$line->credit = Utils::moneyToInteger($line->credit);
		$line->debit = Utils::moneyToInteger($line->debit);
	}

	unset($line);
}
else {
	$lines = $transaction->getLinesWithAccounts();

	foreach ($lines as $k => &$line) {
		$line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_label)];
	}

	unset($line);
}

$has_reconciled_lines = false;

foreach ($lines as $line) {
	if (!empty($line->reconciled)) {
		$has_reconciled_lines = true;
		break;
	}
}

$first_line = $transaction->getFirstLine();

if ($transaction->type != Transaction::TYPE_ADVANCED) {
	$types_accounts = $transaction->getTypesAccounts();
}

$amount = $transaction->getLinesCreditSum();

$tpl->assign(compact('transaction', 'lines', 'types_accounts', 'amount', 'first_line', 'has_reconciled_lines'));

$tpl->assign('types_details', Transaction::getTypesDetails());
$tpl->assign('chart_id', $chart->id());
$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->assign('linked_users', $transaction->listLinkedUsersAssoc());

$tpl->display('acc/transactions/edit.tpl');







>
>


<
<
<
|
<
<
|
|

|
|
|
|
|
|
|
|
<
|
<
<
<
<
<


<
|
<
<

<
<
<
<
<
<
<
<
<
|
|
<
<
<
|
<
<
<
|

<
<
<
<
<
<
|
<

<
<
<
<
<
<
<
<
<




|
26
27
28
29
30
31
32
33
34
35
36



37


38
39
40
41
42
43
44
45
46
47
48

49





50
51

52


53









54
55



56



57
58






59

60









61
62
63
64
65
if ($year->closed) {
	throw new UserException('Cette écriture ne peut être modifiée car elle appartient à un exercice clôturé');
}

$chart = $year->chart();
$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 {
		// Remove all
		$transaction->updateLinkedUsers([]);
	}

}, $csrf_key, '!acc/transactions/details.php?id=' . $transaction->id());






$types_accounts = [];

$lines = isset($_POST['lines']) ? Transaction::getFormLines() : $transaction->getLinesWithAccounts();












$amount = $transaction->getLinesCreditSum();
$types_details = $transaction->getTypesDetails();



$id_analytical = $transaction->getAnalyticalId();



$has_reconciled_lines = $transaction->hasReconciledLines();







$tpl->assign(compact('csrf_key', 'transaction', 'lines', 'amount', 'has_reconciled_lines', 'types_details', 'id_analytical'));











$tpl->assign('chart_id', $chart->id());
$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->assign('linked_users', $transaction->listLinkedUsersAssoc());

$tpl->display('acc/transactions/new.tpl');

Modified src/www/admin/acc/transactions/new.php from [3cd5475ec6] to [6424d9b0e5].

15
16
17
18
19
20
21

22
23
24
25
26



27
28
29
30
31
32
33
if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$chart = $current_year->chart();
$accounts = $chart->accounts();


$transaction = new Transaction;
$lines = [[], []];
$amount = 0;
$types_accounts = null;
$id_analytical = null;




// Quick-fill transaction from query parameters
if (qg('a')) {
	$amount = Utils::moneyToInteger(qg('a'));
}

if (qg('l')) {







>

<

<

>
>
>







15
16
17
18
19
20
21
22
23

24

25
26
27
28
29
30
31
32
33
34
35
if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$chart = $current_year->chart();
$accounts = $chart->accounts();

$csrf_key = 'acc_transaction_new';
$transaction = new Transaction;

$amount = 0;

$id_analytical = null;
$linked_users = null;
$lines = isset($_POST['lines']) ? Transaction::getFormLines() : [[], []];
$types_details = $transaction->getTypesDetails();

// Quick-fill transaction from query parameters
if (qg('a')) {
	$amount = Utils::moneyToInteger(qg('a'));
}

if (qg('l')) {
47
48
49
50
51
52
53


54


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
	$old = Transactions::get((int)qg('copy'));

	if (!$old) {
		throw new UserException('Cette écriture n\'existe pas (ou plus).');
	}

	$transaction = $old->duplicate($current_year);


	$lines = $transaction->getLinesWithAccounts(true);


	$id_analytical = $old->getAnalyticalId();
	$amount = $transaction->getLinesCreditSum();
	$types_accounts = $transaction->getTypesAccounts();
	$transaction->resetLines();

	foreach ($lines as $k => &$line) {
		$line->account = [$line->id_account => sprintf('%s — %s', $line->account_code, $line->account_label)];
	}

	unset($line);
}

// Set last used date
if (empty($transaction->date) && $session->get('acc_last_date') && $date = \DateTime::createFromFormat('!Y-m-d', $session->get('acc_last_date'))) {
	$transaction->date = $date;
}
// Set date of the day if no date was set







>
>
|
>
>


<
<
|
<
<
|
|
<







49
50
51
52
53
54
55
56
57
58
59
60
61
62


63


64
65

66
67
68
69
70
71
72
	$old = Transactions::get((int)qg('copy'));

	if (!$old) {
		throw new UserException('Cette écriture n\'existe pas (ou plus).');
	}

	$transaction = $old->duplicate($current_year);

	if (empty($_POST)) {
		$lines = $transaction->getLinesWithAccounts();
	}

	$id_analytical = $old->getAnalyticalId();
	$amount = $transaction->getLinesCreditSum();


	$linked_users = $old->listLinkedUsersAssoc();



	$tpl->assign('duplicate_from', $old->id());

}

// Set last used date
if (empty($transaction->date) && $session->get('acc_last_date') && $date = \DateTime::createFromFormat('!Y-m-d', $session->get('acc_last_date'))) {
	$transaction->date = $date;
}
// Set date of the day if no date was set
84
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

	if (!$account || $account->id_chart != $current_year->id_chart) {
		throw new UserException('Ce compte ne correspond pas à l\'exercice comptable ou n\'existe pas');
	}

	$transaction->type = Transaction::getTypeFromAccountType($account->type);
	$index = $transaction->type == Transaction::TYPE_DEBT || $transaction->type == Transaction::TYPE_CREDIT ? 1 : 0;
	$key = sprintf('account_%d_%d', $transaction->type, $index);

	if (!isset($_POST[$key])) {
		$lines[0]['account'] = $_POST[$key] = [$account->id => sprintf('%s — %s', $account->code, $account->label)];
	}
}
elseif (!empty($_POST['lines']) && is_array($_POST['lines'])) {
	$lines = Utils::array_transpose($_POST['lines']);

	foreach ($lines as &$line) {
		$line['credit'] = Utils::moneyToInteger($line['credit']);
		$line['debit'] = Utils::moneyToInteger($line['debit']);
	}
}

$form->runIf('save', function () use ($transaction, $session, $current_year) {
	$transaction->importFromNewForm();
	$transaction->id_year = $current_year->id();
	$transaction->id_creator = $session->getUser()->id;
	$transaction->save();

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

	$session->set('acc_last_date', $transaction->date->format('Y-m-d'));

	Utils::redirect(sprintf('!acc/transactions/details.php?id=%d&created', $transaction->id()));
}, 'acc_transaction_new');

$tpl->assign(compact('transaction', 'amount', 'lines', 'types_accounts', 'id_analytical'));

$tpl->assign('types_details', Transaction::getTypesDetails());
$tpl->assign('chart_id', $chart->id());

$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());

$tpl->display('acc/transactions/new.tpl');







<
<
<
|
<
<
<
<
<
<
<
<
<
















|

|

<

<

>

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

	if (!$account || $account->id_chart != $current_year->id_chart) {
		throw new UserException('Ce compte ne correspond pas à l\'exercice comptable ou n\'existe pas');
	}

	$transaction->type = Transaction::getTypeFromAccountType($account->type);
	$index = $transaction->type == Transaction::TYPE_DEBT || $transaction->type == Transaction::TYPE_CREDIT ? 1 : 0;



	$types_details[$transaction->type]->accounts[$index]->selector_value = [$account->id => sprintf('%s — %s', $account->code, $account->label)];









}

$form->runIf('save', function () use ($transaction, $session, $current_year) {
	$transaction->importFromNewForm();
	$transaction->id_year = $current_year->id();
	$transaction->id_creator = $session->getUser()->id;
	$transaction->save();

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

	$session->set('acc_last_date', $transaction->date->format('Y-m-d'));

	Utils::redirect(sprintf('!acc/transactions/details.php?id=%d&created', $transaction->id()));
}, $csrf_key);

$tpl->assign(compact('csrf_key', 'transaction', 'amount', 'lines', 'id_analytical', 'types_details', 'linked_users'));


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

$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());

$tpl->display('acc/transactions/new.tpl');

Modified src/www/admin/acc/transactions/payoff.php from [8f8a39cbc6] to [56988c3eb1].

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Quick pay-off for debts and credits, directly from a debt/credit details page
$payoff_for = $transaction->payOffFrom((int) qg('for'));

if (!$payoff_for) {
	throw new UserException('Écriture inconnue');
}

$amount = $payoff_for->sum;

$chart = $current_year->chart();
$accounts = $chart->accounts();

$date = new \DateTime;

if ($session->get('acc_last_date')) {
	$date = \DateTime::createFromFormat('!d/m/Y', $session->get('acc_last_date'));
}

if (!$date || ($date < $current_year->start_date || $date > $current_year->end_date)) {
	$date = $current_year->start_date;
}

$transaction->date = $date;

$form->runIf('save', function () use ($transaction, $session, $current_year) {
	// Force type
	$_POST['type'] = $transaction->type;

	$transaction->importFromNewForm();
	$transaction->id_year = $current_year->id();
	$transaction->id_creator = $session->getUser()->id;
	$transaction->save();

	if (f('mark_paid')) {
		$transaction->related()->markPaid();
		$transaction->related()->save();
	}

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

	$session->set('acc_last_date', f('date'));

	Utils::redirect('!acc/transactions/new.php?ok=' . $transaction->id());
}, 'acc_transaction_new');

$id_analytical = $payoff_for->id_analytical;

$tpl->assign(compact('transaction', 'payoff_for', 'amount', 'id_analytical'));
$tpl->assign('payoff_targets', implode(':', [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]));

$tpl->assign('types_details', Transaction::getTypesDetails());
$tpl->assign('chart_id', $chart->id());

$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->display('acc/transactions/payoff.tpl');







|

















<
<
<
|
















|







<




19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
// Quick pay-off for debts and credits, directly from a debt/credit details page
$payoff_for = $transaction->payOffFrom((int) qg('for'));

if (!$payoff_for) {
	throw new UserException('Écriture inconnue');
}

$amount = $payoff_for->amount;

$chart = $current_year->chart();
$accounts = $chart->accounts();

$date = new \DateTime;

if ($session->get('acc_last_date')) {
	$date = \DateTime::createFromFormat('!d/m/Y', $session->get('acc_last_date'));
}

if (!$date || ($date < $current_year->start_date || $date > $current_year->end_date)) {
	$date = $current_year->start_date;
}

$transaction->date = $date;

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



	$transaction->importFromPayoffForm();
	$transaction->id_year = $current_year->id();
	$transaction->id_creator = $session->getUser()->id;
	$transaction->save();

	if (f('mark_paid')) {
		$transaction->related()->markPaid();
		$transaction->related()->save();
	}

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

	$session->set('acc_last_date', f('date'));

	Utils::redirect('!acc/transactions/details.php?created&id=' . $transaction->id());
}, 'acc_transaction_new');

$id_analytical = $payoff_for->id_analytical;

$tpl->assign(compact('transaction', 'payoff_for', 'amount', 'id_analytical'));
$tpl->assign('payoff_targets', implode(':', [Account::TYPE_BANK, Account::TYPE_CASH, Account::TYPE_OUTSTANDING]));


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

$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->display('acc/transactions/payoff.tpl');

Modified src/www/admin/services/user/payment.php from [9913d7fba3] to [9a741468bb].

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

39
40
41
42
43
44
45
$form->runIf(f('save') || f('save_and_add_payment'), function () use ($su, $session) {
	$su->addPayment($session->getUser()->id);

	if ($su->paid != (bool) f('paid')) {
		$su->paid = (bool) f('paid');
		$su->save();
	}

	if (f('save_and_add_payment')) {
		$url = ADMIN_URL . 'services/user/payment.php?id=' . $su->id;
	}
	else {
		$url = ADMIN_URL . 'services/user/?id=' . $su->id_user;
	}

	Utils::redirect($url);
}, $csrf_key);


$types_details = Transaction::getTypesDetails();
$account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string;

$tpl->assign(compact('csrf_key', 'account_targets', 'user_name', 'su'));

$tpl->display('services/user/payment.tpl');







<
<
<
<
<
|
|
|
<
|
>

<





22
23
24
25
26
27
28





29
30
31

32
33
34

35
36
37
38
39
$form->runIf(f('save') || f('save_and_add_payment'), function () use ($su, $session) {
	$su->addPayment($session->getUser()->id);

	if ($su->paid != (bool) f('paid')) {
		$su->paid = (bool) f('paid');
		$su->save();
	}





}, $csrf_key, '!services/user/?id=' . $su->id_user);

$t = new Transaction;

$t->type = $t::TYPE_REVENUE;
$types_details = $t->getTypesDetails();


$account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string;

$tpl->assign(compact('csrf_key', 'account_targets', 'user_name', 'su'));

$tpl->display('services/user/payment.tpl');

Modified src/www/admin/services/user/subscribe.php from [e4d11cfc89] to [d9219f2d3d].

62
63
64
65
66
67
68


69
70
71
72
73
74
75
76
	else {
		$url = ADMIN_URL . 'services/user/?id=' . $su->id_user;
	}

	Utils::redirect($url);
}, $csrf_key);



$types_details = Transaction::getTypesDetails();
$account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string;

$service_user = null;

$tpl->assign(compact('csrf_key', 'users', 'account_targets', 'service_user'));

$tpl->display('services/user/subscribe.tpl');







>
>
|







62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
	else {
		$url = ADMIN_URL . 'services/user/?id=' . $su->id_user;
	}

	Utils::redirect($url);
}, $csrf_key);

$t = new Transaction;
$t->type = $t::TYPE_REVENUE;
$types_details = $t->getTypesDetails();
$account_targets = $types_details[Transaction::TYPE_REVENUE]->accounts[1]->targets_string;

$service_user = null;

$tpl->assign(compact('csrf_key', 'users', 'account_targets', 'service_user'));

$tpl->display('services/user/subscribe.tpl');

Modified src/www/admin/static/scripts/selector.js from [09cbc2d747] to [5c426733c3].

54
55
56
57
58
59
60













61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
		}
	}

	if (!available.length) {
		return false;
	}














	if (evt.key == 'ArrowUp') { // Previous item
		idx--;
	}
	else if (evt.key == 'ArrowDown') {
		idx++;
	}
	else if (evt.key == 'PageUp') {
		idx-=10;
	}
	else if (evt.key == 'PageDown') {
		idx+=10;
	}
	else if (evt.key == 'Home') {
		idx = 0;
	}
	else if (evt.key == 'End') {
		idx = available.length;
	}
	else {
		return true;
	}

	if (idx < 0) {
		idx = 0;
	}







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











<
<
<
<
<
<







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85






86
87
88
89
90
91
92
		}
	}

	if (!available.length) {
		return false;
	}

	// Do not intercept home/end inside text input
	if ((evt.key == 'Home' || evt.key == 'End')
		&& document.activeElement instanceof HTMLInputElement
		&& document.activeElement.type == 'text') {
		return;
	}

	if (evt.key == 'Home') {
		idx = 0;
	}
	else if (evt.key == 'End') {
		idx = available.length;
	}
	else if (evt.key == 'ArrowUp') { // Previous item
		idx--;
	}
	else if (evt.key == 'ArrowDown') {
		idx++;
	}
	else if (evt.key == 'PageUp') {
		idx-=10;
	}
	else if (evt.key == 'PageDown') {
		idx+=10;
	}






	else {
		return true;
	}

	if (idx < 0) {
		idx = 0;
	}

Modified src/www/admin/static/styles/03-forms.css from [b01f09fb90] to [da6a895d27].

697
698
699
700
701
702
703













704
705
706
707
708
709
710
    width: 100%;
    height: 1em;
    filter: none !important;
    color: #000;
}

@keyframes spin { to { transform: rotate(360deg); } }














@media screen and (max-width: 1279px) {
    #queryBuilder table tr {
        display: flex;
        flex-wrap: wrap;
        padding: .5em 0;
        margin-left: 6rem;







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







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
    width: 100%;
    height: 1em;
    filter: none !important;
    color: #000;
}

@keyframes spin { to { transform: rotate(360deg); } }

.selector header {
    margin-bottom: 2em;
}

.selector header p.edit {
    float: right;
    margin: 0;
}

.selector header h2 input {
    width: calc(100% - 1em);
}

@media screen and (max-width: 1279px) {
    #queryBuilder table tr {
        display: flex;
        flex-wrap: wrap;
        padding: .5em 0;
        margin-left: 6rem;