Overview
Comment:Merge trunk changes that have been missed, probably because of the trunk on 06/12/2020 [0277842dc6]
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: 56201fa5cb86798d3e5a5c8b3072392940efe62e5cf1a4f2c4a56fb75057b459
User & Date: bohwaz on 2021-01-29 00:54:51
Other Links: branch diff | manifest | tags
Context
2021-01-29
02:23
Common use of modifiers between Smartyer and Brindille check-in: 9ab0478441 user: bohwaz tags: dev
00:54
Merge trunk changes that have been missed, probably because of the trunk on 06/12/2020 [0277842dc6] check-in: 56201fa5cb user: bohwaz tags: dev
00:11
Merge back trunk changes check-in: 587d487631 user: bohwaz tags: dev
2021-01-26
21:17
Fix overwritten variable by reference check-in: 16cb52d122 user: bohwaz tags: trunk, stable
Changes

Modified src/VERSION from [05e17b646a] to [528cf42af9].

Modified src/include/data/1.0.0_migration.sql from [ba81ccc806] to [4e16391488].

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


29
30
31
32
33
34
35
36
-------- MIGRATION COMPTA ---------
INSERT INTO acc_charts (id, country, code, label) VALUES (1, 'FR', 'PCGA1999', 'Plan comptable associatif 1999');

-- Migration comptes de code comme identifiant à ID unique
-- Inversement valeurs actif/passif et produit/charge
INSERT INTO acc_accounts (id, id_chart, code, label, position, user)
	SELECT NULL, 1, id, libelle,
	CASE position
		WHEN 1 THEN 2
		WHEN 2 THEN 1
		WHEN 3 THEN 3
		WHEN 4 THEN 5
		WHEN 8 THEN 4
		-- Suppression de la position "charge ou produit" qui n'a aucun sens


		WHEN 12 THEN 0
		ELSE 0
	END,
	CASE WHEN plan_comptable = 1 THEN 0 ELSE 1 END
	FROM compta_comptes;

-- Migrations projets vers comptes analytiques
INSERT INTO acc_accounts (id_chart, code, label, position, user, type)







|
|
|
|
|
|

>
>
|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-------- MIGRATION COMPTA ---------
INSERT INTO acc_charts (id, country, code, label) VALUES (1, 'FR', 'PCGA1999', 'Plan comptable associatif 1999');

-- Migration comptes de code comme identifiant à ID unique
-- Inversement valeurs actif/passif et produit/charge
INSERT INTO acc_accounts (id, id_chart, code, label, position, user)
	SELECT NULL, 1, id, libelle,
	CASE
		WHEN position = 1 THEN 2
		WHEN position = 2 THEN 1
		WHEN position = 3 THEN 3
		WHEN position = 4 THEN 5
		WHEN position = 8 THEN 4
		-- Suppression de la position "charge ou produit" qui n'a aucun sens
		WHEN position = 12 AND id LIKE '6%' THEN 4
		WHEN position = 12 AND id LIKE '7%' THEN 5
		WHEN position = 12 THEN 0
		ELSE 0
	END,
	CASE WHEN plan_comptable = 1 THEN 0 ELSE 1 END
	FROM compta_comptes;

-- Migrations projets vers comptes analytiques
INSERT INTO acc_accounts (id_chart, code, label, position, user, type)

Modified src/include/lib/Garradin/Entities/Accounting/Account.php from [055e1ef171] to [cacd2727a4].

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
			unset($columns['change']);
		}

		$list = new DynamicList($columns, $tables, $conditions);
		$list->orderBy('date', false);
		$list->setCount('COUNT(*)');
		$list->setPageSize(null);
		$list->setModifier(function (&$row) use (&$sum, $reverse) {
			if (property_exists($row, 'sum')) {
				$sum += isset($row->change) ? $row->change : ($row->credit - $row->debit);
				$row->sum = $sum;
			}

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







|







209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
			unset($columns['change']);
		}

		$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 += isset($row->change) ? $row->change : ($row->credit - $row->debit);
				$row->sum = $sum;
			}

			$row->date = \DateTime::createFromFormat('!Y-m-d', $row->date);
		});
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
		$journal = iterator_to_array($journal);
		$i = 0;
		$sum = 0;

		foreach ($csv as $k => &$line) {
			try {
				$date = \DateTime::createFromFormat('!d/m/Y', $line->date);
				$line->amount = ($line->amount < 0 ? -1 : 1) * Utils::moneyToInteger($line->amount);

				if (!$date) {
					throw new UserException('Date invalide : ' . $line->date);
				}

				$line->date = $date;
			}







|







291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
		$journal = iterator_to_array($journal);
		$i = 0;
		$sum = 0;

		foreach ($csv as $k => &$line) {
			try {
				$date = \DateTime::createFromFormat('!d/m/Y', $line->date);
				$line->amount = (substr($line->amount, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->amount);

				if (!$date) {
					throw new UserException('Date invalide : ' . $line->date);
				}

				$line->date = $date;
			}
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
			$row = (object) ['csv' => null, 'journal' => $j];

			if (isset($j->debit)) {
				foreach ($csv as &$line) {
					if (!isset($line->date)) {
						 continue;
					}


					if ($j->date->format('Ymd') == $line->date->format('Ymd')
						&& ($j->credit == abs($line->amount) || $j->debit == abs($line->amount))) {

						$row->csv = $line;
						$line = null;
						break;
					}
				}
			}

			$lines[$id] = $row;
		}

























		unset($j);


		foreach ($csv as $line) {
			if (null == $line) {
				continue;
			}

			$id = $line->date->format('Ymd') . '.' . ($i++);
			$lines[$id] = (object) ['csv' => $line, 'journal' => null];







>
>

|
>










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


>







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
			$row = (object) ['csv' => null, 'journal' => $j];

			if (isset($j->debit)) {
				foreach ($csv as &$line) {
					if (!isset($line->date)) {
						 continue;
					}

					// Match date, amount and label
					if ($j->date->format('Ymd') == $line->date->format('Ymd')
						&& ($j->credit * -1 == $line->amount || $j->debit == $line->amount)
						&& strtolower($j->label) == strtolower($line->label)) {
						$row->csv = $line;
						$line = null;
						break;
					}
				}
			}

			$lines[$id] = $row;
		}

		unset($line, $row, $j);

		// Second round to match only amount and label
		foreach ($lines as $row) {
			if ($row->csv || !isset($row->journal->debit)) {
				continue;
			}

			$j = $row->journal;

			foreach ($csv as &$line) {
				if (!isset($line->date)) {
					 continue;
				}

				if ($j->date->format('Ymd') == $line->date->format('Ymd')
					&& ($j->credit * -1 == $line->amount || $j->debit == $line->amount)) {
					$row->csv = $line;
					$line = null;
					break;
				}
			}
		}

		unset($j);

		// Then add CSV lines on the right
		foreach ($csv as $line) {
			if (null == $line) {
				continue;
			}

			$id = $line->date->format('Ymd') . '.' . ($i++);
			$lines[$id] = (object) ['csv' => $line, 'journal' => null];
496
497
498
499
500
501
502

503

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

	public function save(): bool
	{

		Config::getInstance()->set('last_chart_change', time());

		return parent::save();
	}
}







>
|
>



524
525
526
527
528
529
530
531
532
533
534
535
536
	public function chart(): Chart
	{
		return Charts::get($this->id_chart);
	}

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

		return parent::save();
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Line.php from [0fde672e4f] to [c9978cd52a].

1
2
3
4

5
6
7
8
9
10
11
<?php

namespace Garradin\Entities\Accounting;


use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;

class Line extends Entity
{
	const TABLE = 'acc_transactions_lines';




>







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

namespace Garradin\Entities\Accounting;

use Garradin\DB;
use Garradin\Entity;
use Garradin\ValidationException;
use Garradin\Utils;

class Line extends Entity
{
	const TABLE = 'acc_transactions_lines';
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

		'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',
		'credit'         => 'money|min:0',
		'debit'          => 'money|min:0',
		'reference'      => 'string|max:200',
		'label'          => 'string|max:200',
	];

	public function filterUserValue(string $type, $value, string $key)
	{
		if ($key == 'credit' || $key == 'debit')
		{
			$value = Utils::moneyToInteger($value);
		}
		elseif ($key == 'id_analytical' && $value == 0) {
			$value = null;
		}

		$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 * $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);
	}





}








<
<






|
<















>



|
>
>
>
>
>
|
>
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
		'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',
	];

	public function filterUserValue(string $type, $value, string $key)
	{
		if ($key == 'credit' || $key == 'debit') {

			$value = Utils::moneyToInteger($value);
		}
		elseif ($key == 'id_analytical' && $value == 0) {
			$value = null;
		}

		$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);

		$db = DB::getInstance();
		$this->assert($db->firstColumn('SELECT 1 FROM acc_accounts a
			INNER JOIN acc_transactions t ON t.id = ?
			INNER JOIN acc_years y ON y.id = t.id_year
			WHERE a.id = ? AND a.id_chart = y.id_chart;', $this->id_transaction, $this->id_account), 'Le compte sélectionné ne correspond pas à l\'exercice');
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [df72a69383] to [53d3112cba].

26
27
28
29
30
31
32

33
34
35
36
37
38
39
	const TYPE_TRANSFER = 3;
	const TYPE_DEBT = 4;
	const TYPE_CREDIT = 5;

	const STATUS_WAITING = 1;
	const STATUS_PAID = 2;
	const STATUS_DEPOSIT = 4;


	const STATUS_NAMES = [
		1 => 'En attente de règlement',
		2 => 'Réglé',
		4 => 'Déposé en banque',
	];








>







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
	const TYPE_TRANSFER = 3;
	const TYPE_DEBT = 4;
	const TYPE_CREDIT = 5;

	const STATUS_WAITING = 1;
	const STATUS_PAID = 2;
	const STATUS_DEPOSIT = 4;
	const STATUS_ERROR = 8;

	const STATUS_NAMES = [
		1 => 'En attente de règlement',
		2 => 'Réglé',
		4 => 'Déposé en banque',
	];

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
		switch ($account_type) {
			case Account::TYPE_REVENUE:
				return self::TYPE_REVENUE;
			case Account::TYPE_EXPENSE:
				return self::TYPE_EXPENSE;
			case Account::TYPE_THIRD_PARTY:
				return self::TYPE_DEBT;




			default:
				return self::TYPE_TRANSFER;
		}
	}




	public function getLinesWithAccounts()
	{

		$em = EntityManager::getInstance(Line::class);
		return $em->DB()->get('SELECT
			l.*, a.label AS account_name, 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


			LEFT JOIN acc_accounts b ON b.id = l.id_analytical
			WHERE l.id_transaction = ? ORDER BY l.id;', $this->id);



	}

	public function getLines($with_accounts = false)
	{
		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);







>
>
>
>

|



>
>
>
|

>
|
|



|
>
>

|
>
>
>







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
		switch ($account_type) {
			case Account::TYPE_REVENUE:
				return self::TYPE_REVENUE;
			case Account::TYPE_EXPENSE:
				return self::TYPE_EXPENSE;
			case Account::TYPE_THIRD_PARTY:
				return self::TYPE_DEBT;
			case Account::TYPE_BANK:
			case Account::TYPE_CASH:
			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)
	{
		$restrict = $restrict_year ? 'AND a.id_chart = y.id_chart' : '';

		$sql = sprintf('SELECT
			l.*, a.label AS account_name, 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 = ? ORDER BY l.id;', $restrict);

		$em = EntityManager::getInstance(Line::class);
		return $em->DB()->get($sql, $this->id);
	}

	public function getLines($with_accounts = false)
	{
		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);
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
			if ($line->id === $id) {
				return $line;
			}
		}

		return null;
	}












	public function getLinesCreditSum()
	{
		$sum = 0;

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

		return $sum;
	}












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








>
>
>
>
>
>
>
>
>
>
>











>
>
>
>
>
>
>
>
>
>
>







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
			if ($line->id === $id) {
				return $line;
			}
		}

		return null;
	}

	public function getFirstLine()
	{
		$lines = $this->getLines();

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

		return reset($lines);
	}

	public function getLinesCreditSum()
	{
		$sum = 0;

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

		return $sum;
	}

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

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

		return current($lines)->id_analytical;
	}

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

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
308
309
310
311
		}

		return $sum;
	}

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

		$db = DB::getInstance();

		if ($db->test(Year::TABLE, 'id = ? AND closed = 1', $this->id_year)) {
			throw new ValidationException('Il n\'est pas possible de modifier une écriture qui fait partie d\'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)
		{
			$line->delete();
		}

		// Remove flag
		if ((self::TYPE_DEBT == $this->type || self::TYPE_CREDIT == $this->type) && $this->_related) {
			$this->_related->removeStatus(self::STATUS_WAITING);
			$this->_related->addStatus(self::STATUS_PAID);
			$this->_related->save();
		}

		return true;
	}

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

	public function addStatus(int $property) {
		$this->set('status', $this->status | $property);
	}






	public function delete(): bool
	{
		if ($this->validated) {
			throw new ValidationException('Il n\'est pas possible de supprimer une écriture qui a été validée');
		}








|
|





|



















<
|













>
>
>
>
>







292
293
294
295
296
297
298
299
300
301
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
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
		}

		return $sum;
	}

	public function save(): bool
	{
		if ($this->validated && empty($this->_modified['validated'])) {
			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)
		{
			$line->delete();
		}

		// Remove flag
		if ((self::TYPE_DEBT == $this->type || self::TYPE_CREDIT == $this->type) && $this->_related) {

			$this->_related->markPaid();
			$this->_related->save();
		}

		return true;
	}

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

	public function addStatus(int $property) {
		$this->set('status', $this->status | $property);
	}

	public function markPaid() {
		$this->removeStatus(self::STATUS_WAITING);
		$this->addStatus(self::STATUS_PAID);
	}

	public function delete(): bool
	{
		if ($this->validated) {
			throw new ValidationException('Il n\'est pas possible de supprimer une écriture qui a été validée');
		}

394
395
396
397
398
399
400




401
402
403
404
405
406
407
		}

		$type = $source['type'];

		$this->importForm($source);

		if (self::TYPE_ADVANCED == $type) {




			$lines = Utils::array_transpose($source['lines']);

			foreach ($lines as $i => $line) {
				$line['id_account'] = @count($line['account']) ? key($line['account']) : null;

				if (!$line['id_account']) {
					throw new ValidationException('Numéro de compte invalide sur la ligne ' . ($i+1));







>
>
>
>







434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
		}

		$type = $source['type'];

		$this->importForm($source);

		if (self::TYPE_ADVANCED == $type) {
			if (!isset($source['lines']) || !is_array($source['lines'])) {
				throw new ValidationException('Aucune ligne dans la saisie');
			}

			$lines = Utils::array_transpose($source['lines']);

			foreach ($lines as $i => $line) {
				$line['id_account'] = @count($line['account']) ? key($line['account']) : null;

				if (!$line['id_account']) {
					throw new ValidationException('Numéro de compte invalide sur la ligne ' . ($i+1));
415
416
417
418
419
420
421
422



423
424
425
426
427
428
429
			$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->status = self::STATUS_WAITING;



			}

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

			$amount = $source['amount'];







|
>
>
>







459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
			$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->addStatus(self::STATUS_WAITING);
			}
			else {
				$this->removeStatus(self::STATUS_WAITING);
			}

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

			$amount = $source['amount'];
442
443
444
445
446
447
448



449
450
451
452
453
454
455
					'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;
		}







>
>
>







489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
					'debit'         => $debit,
					'id_account'    => $account,
					'id_analytical' => !empty($source['id_analytical']) ? $source['id_analytical'] : null,
				]);
				$this->addLine($line);
			}
		}

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

	public function importFromEditForm(?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}
479
480
481
482
483
484
485
486
487


488
489




490
491
492
493
494
495
496
		}
		catch (\LogicException $e) {
			throw new ValidationException('Aucun compte sélectionné pour certaines lignes.');
		}

		$debit = $credit = 0;

		foreach ($lines as $line) {
			$line['id_account'] = @count($line['account']) ? key($line['account']) : null;


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





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

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







|

>
>
|
|
>
>
>
>







529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
		}
		catch (\LogicException $e) {
			throw new ValidationException('Aucun compte sélectionné pour certaines lignes.');
		}

		$debit = $credit = 0;

		foreach ($lines as $k => $line) {
			$line['id_account'] = @count($line['account']) ? key($line['account']) : null;

			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
695
696
697
698
699
700
701
702






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

		return $out;
	}
}












|
>
>
>
>
>
751
752
753
754
755
756
757
758
759
760
761
762
763

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

		return $out;
	}

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

Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [67edc38661] to [42cf3b1884].

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
	];

	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);
		$this->assert($this->closed == 1 || !isset($this->_modified['closed']), 'Il est interdit de réouvrir un exercice clôturé');

		$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 mouvements de cet exercice ont une date antérieure à la date de début de l\'exercice.'
			);

			$this->assert(
				!$db->test(Transaction::TABLE, 'id_year = ? AND date > ?', $this->id(), $this->end_date->format('Y-m-d')),
				'Des mouvements de cet exercice ont une date postérieure à la date de fin de l\'exercice.'
			);
		}
	}

	public function close(int $user_id): void
	{
		if ($this->closed) {
			throw new \LogicException('Cet exercice est déjà clôturé');
		}

		$this->set('closed', 1);
		$this->save();
	}
































































	public function delete(): bool
	{
		// Manual delete of transactions, as there is a voluntary safeguard in SQL: no cascade
		DB::getInstance()->preparedQuery('DELETE FROM acc_transactions WHERE id_year = ?;', $this->id());

		// Clean up files







<








|




|













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







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
	];

	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.'
			);

			$this->assert(
				!$db->test(Transaction::TABLE, 'id_year = ? AND date > ?', $this->id(), $this->end_date->format('Y-m-d')),
				'Des écritures de cet exercice ont une date postérieure à la date de fin de l\'exercice.'
			);
		}
	}

	public function close(int $user_id): void
	{
		if ($this->closed) {
			throw new \LogicException('Cet exercice est déjà clôturé');
		}

		$this->set('closed', 1);
		$this->save();
	}

	public function reopen(int $user_id): void
	{
		if (!$this->closed) {
			throw new \LogicException('This year is already open');
		}

		$closing_id = $this->accounts()->getClosingAccountId();

		if (!$closing_id) {
			throw new UserException('Aucun compte n\'est indiqué comme compte de clôture dans le plan comptable');
		}

		$this->set('closed', 0);
		$this->save();

		// Create validated transaction to show that someone has reopened the year
		$t = new Transaction;
		$t->import([
			'id_year'    => $this->id(),
			'label'      => sprintf('Exercice réouvert le %s', date('d/m/Y à H:i:s')),
			'type'       => Transaction::TYPE_ADVANCED,
			'date'       => $this->end_date->format('d/m/Y'),
			'id_creator' => $user_id,
			'validated'  => 1,
		]);

		$line = new Line;
		$line->import([
			'debit' => 0,
			'credit' => 1,
			'id_account' => $closing_id,
		]);
		$t->addLine($line);

		$line = new Line;
		$line->import([
			'debit'      => 1,
			'credit'     => 0,
			'id_account' => $closing_id,
		]);
		$t->addLine($line);

		$t->save();
	}

	/**
	 * Splits an accounting year between the current year and another one, at a given date
	 * Any transaction after the given date will be moved to the target year.
	 */
	public function split(\DateTime $date, Year $target): void
	{
		if ($this->closed) {
			throw new \LogicException('Cet exercice est déjà clôturé');
		}

		if ($target->closed) {
			throw new \LogicException('L\'exercice cible est déjà clôturé');
		}

		DB::getInstance()->preparedQuery('UPDATE acc_transactions SET id_year = ? WHERE id_year = ? AND date > ?;',
			$target->id(), $this->id(), $date->format('Y-m-d'));
	}

	public function delete(): bool
	{
		// Manual delete of transactions, as there is a voluntary safeguard in SQL: no cascade
		DB::getInstance()->preparedQuery('DELETE FROM acc_transactions WHERE id_year = ?;', $this->id());

		// Clean up files

Modified src/include/lib/Garradin/Services/Services.php from [7ad7d0ccb6] to [743242c68f].

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

		return $out;
	}

	static public function listWithStats()
	{
		$db = DB::getInstance();
		$hidden_cats = array_keys((new Categories)->listHidden());

		$condition = sprintf('SELECT COUNT(DISTINCT su.id_user) FROM services_users su
			INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) su2 ON su2.id = su.id
			INNER JOIN membres m ON m.id = su.id_user WHERE su.id_service = s.id AND m.category_id NOT IN (%s)',
			implode(',', $hidden_cats));

		$sql = sprintf('SELECT s.*,







|







51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

		return $out;
	}

	static public function listWithStats()
	{
		$db = DB::getInstance();
		$hidden_cats = array_keys(Categories::listHidden());

		$condition = sprintf('SELECT COUNT(DISTINCT su.id_user) FROM services_users su
			INNER JOIN (SELECT id, MAX(date) FROM services_users GROUP BY id_user, id_service) su2 ON su2.id = su.id
			INNER JOIN membres m ON m.id = su.id_user WHERE su.id_service = s.id AND m.category_id NOT IN (%s)',
			implode(',', $hidden_cats));

		$sql = sprintf('SELECT s.*,

Modified src/include/lib/Garradin/Upgrade.php from [55859a3a96] to [7af5640d3e].

50
51
52
53
54
55
56
















57
58
59
60
61
62
63

		Static_Cache::store('upgrade', 'Mise à jour en cours.');

		// Créer une sauvegarde automatique
		$backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version());

		try {
















			if (version_compare($v, '1.1.0', '<='))
			{
				// Missing trigger
				$db->beginSchemaUpdate();
				$champs = new Champs($db->firstColumn('SELECT valeur FROM config WHERE cle = \'champs_membres\';'));
				$db->createFunction('sha1', 'sha1');
				$db->import(ROOT . '/include/data/1.1.0_migration.sql');







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







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

		Static_Cache::store('upgrade', 'Mise à jour en cours.');

		// Créer une sauvegarde automatique
		$backup_name = (new Sauvegarde)->create('pre-upgrade-' . garradin_version());

		try {
			if (version_compare($v, '1.0.1', '<'))
			{
				// Missing trigger
				$db->begin();
				$db->import(ROOT . '/include/data/1.0.1_migration.sql');
				$db->commit();
			}

			if (version_compare($v, '1.0.3', '<'))
			{
				// Missing trigger
				$db->begin();
				$db->import(ROOT . '/include/data/1.0.3_migration.sql');
				$db->commit();
			}

			if (version_compare($v, '1.1.0', '<='))
			{
				// Missing trigger
				$db->beginSchemaUpdate();
				$champs = new Champs($db->firstColumn('SELECT valeur FROM config WHERE cle = \'champs_membres\';'));
				$db->createFunction('sha1', 'sha1');
				$db->import(ROOT . '/include/data/1.1.0_migration.sql');

Modified src/templates/acc/transactions/details.tpl from [f60d018a35] to [b59c192305].

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
	<dt>Remarques</dt>
	<dd>{if trim($transaction.notes)}{$transaction.notes|escape|nl2br}{else}-{/if}</dd>

	<dt>Fichiers joints</dt>
	{foreach from=$files item="file"}
	<dd>
		<aside class="file">
			<a target="_blank" href="{$file.url}">{$file.nom}</a>
			<small>({$file.type}, {$file.taille|format_bytes})</small>
			{linkbutton shape="download" href=$file.url target="_blank" label="Télécharger"}
			{linkbutton shape="delete" href="!acc/transactions/delete_file.php?id=%d&from=%d"|args:$file.id,$transaction.id label="Supprimer"}
		</aside>
	</dd>
	{foreachelse}
	<dd>-</dd>
	{/foreach}
</dl>







|
|
|







87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
	<dt>Remarques</dt>
	<dd>{if trim($transaction.notes)}{$transaction.notes|escape|nl2br}{else}-{/if}</dd>

	<dt>Fichiers joints</dt>
	{foreach from=$files item="file"}
	<dd>
		<aside class="file">
			<a target="_blank" href="{$file.url}">{$file.name}</a>
			<small>({$file.type}, {$file.size|format_bytes})</small>
			{linkbutton shape="download" href=$file->url() target="_blank" label="Télécharger"}
			{linkbutton shape="delete" href="!acc/transactions/delete_file.php?id=%d&from=%d"|args:$file.id,$transaction.id label="Supprimer"}
		</aside>
	</dd>
	{foreachelse}
	<dd>-</dd>
	{/foreach}
</dl>

Modified src/templates/common/delete_file.tpl from [42a5f97ba7] to [35c6b0709e].

1
2
3
4
5
6
7
8
{include file="admin/_head.tpl" title="Supprimer un fichier" current=null}

{include file="common/delete_form.tpl"
	legend="Supprimer ce fichier ?"
	warning="Êtes-vous sûr de vouloir supprimer le fichier « %s » ?"|args:$file.nom
}

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




|



1
2
3
4
5
6
7
8
{include file="admin/_head.tpl" title="Supprimer un fichier" current=null}

{include file="common/delete_form.tpl"
	legend="Supprimer ce fichier ?"
	warning="Êtes-vous sûr de vouloir supprimer le fichier « %s » ?"|args:$file.name
}

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

Modified src/templates/common/search/advanced.tpl from [2ec6ea6165] to [71b8bf21e4].

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
<?php
assert(isset($columns));
assert(isset($action_url));
assert(isset($query));
assert(isset($is_admin));
$sql_disabled = !$is_admin || (!$session->canAccess('config', $session::ACCESS_ADMIN) && $is_unprotected);
?>

{form_errors}

<form method="post" action="{$action_url}" id="queryBuilderForm">
	<fieldset>
	{if $sql_query && !$sql_disabled}
		<legend>Schéma des tables SQL</legend>
		<pre class="sql_schema">{foreach from=$schema item="table"}{$table}<br />{/foreach}</pre>
		<dl>
			{input type="textarea" name="sql_query" cols="100" rows="7" required=1 label="Requête SQL" help="Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée." default=$sql_query}
			{if $session->canAccess('config', $session::ACCESS_ADMIN)}
				{input type="checkbox" name="unprotected" value=1 label="Autoriser l'accès à toutes les tables de la base de données" default=$is_unprotected}
				<dd class="help">Attention : en cochant cette case vous autorisez la requête à lire toutes les données de toutes les tables de la base de données&nbsp;!</dd>
			{/if}
		</dl>
		<p class="submit">
			{button type="submit" name="run" label="Exécuter" shape="search" class="main"}
			<input type="hidden" name="id" value="{$search.id}" />





|











|







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
<?php
assert(isset($columns));
assert(isset($action_url));
assert(isset($query));
assert(isset($is_admin));
$sql_disabled = !$is_admin || (!$session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN) && $is_unprotected);
?>

{form_errors}

<form method="post" action="{$action_url}" id="queryBuilderForm">
	<fieldset>
	{if $sql_query && !$sql_disabled}
		<legend>Schéma des tables SQL</legend>
		<pre class="sql_schema">{foreach from=$schema item="table"}{$table}<br />{/foreach}</pre>
		<dl>
			{input type="textarea" name="sql_query" cols="100" rows="7" required=1 label="Requête SQL" help="Si aucune limite n'est précisée, une limite de 100 résultats sera appliquée." default=$sql_query}
			{if $session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN)}
				{input type="checkbox" name="unprotected" value=1 label="Autoriser l'accès à toutes les tables de la base de données" default=$is_unprotected}
				<dd class="help">Attention : en cochant cette case vous autorisez la requête à lire toutes les données de toutes les tables de la base de données&nbsp;!</dd>
			{/if}
		</dl>
		<p class="submit">
			{button type="submit" name="run" label="Exécuter" shape="search" class="main"}
			<input type="hidden" name="id" value="{$search.id}" />

Modified src/www/admin/acc/accounts/journal.php from [f7ee3c3084] to [da00ba6416].

26
27
28
29
30
31
32






33
34
35
36
37
38
39

	if (!$year) {
		throw new UserException("L'exercice demandé n'existe pas.");
	}

	$tpl->assign('year', $year);
}







// The account has a different chart after changing the current year:
// get back to the list of accounts to select a new account!
if ($account->id_chart != $year->id_chart) {
	Utils::redirect(ADMIN_URL . 'acc/accounts/?chart_change');
}








>
>
>
>
>
>







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

	if (!$year) {
		throw new UserException("L'exercice demandé n'existe pas.");
	}

	$tpl->assign('year', $year);
}

// The account has a different chart after changing the current year:
// get back to the list of accounts to select a new account!
if ($account->id_chart != $year->id_chart) {
	Utils::redirect(ADMIN_URL . 'acc/accounts/?chart_change');
}

// The account has a different chart after changing the current year:
// get back to the list of accounts to select a new account!
if ($account->id_chart != $year->id_chart) {
	Utils::redirect(ADMIN_URL . 'acc/accounts/?chart_change');
}

Modified src/www/admin/acc/charts/accounts/index.php from [5e00b51e32] to [bd5f5966a0].

18
19
20
21
22
23
24
25
26
if (!$chart) {
	throw new UserException('Aucun plan comptable spécifié');
}

$accounts = $chart->accounts();

$tpl->assign('chart', $chart);
$tpl->assign('accounts_grouped', $accounts->listCommonGrouped());
$tpl->display('acc/charts/accounts/index.tpl');







|

18
19
20
21
22
23
24
25
26
if (!$chart) {
	throw new UserException('Aucun plan comptable spécifié');
}

$accounts = $chart->accounts();

$tpl->assign('chart', $chart);
$tpl->assign('accounts_grouped', $accounts->listCommonGrouped(null, true));
$tpl->display('acc/charts/accounts/index.tpl');

Modified src/www/admin/acc/charts/accounts/selector.php from [17dde2f21a] to [250e69ae57].

11
12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
header('X-Frame-Options: SAMEORIGIN', true);

$targets = qg('targets');
$chart = qg('chart');

// Cache the page until the charts have changed
$hash = sha1($targets . $chart);
$expiry = Config::getInstance()->get('last_chart_change') ?: time();


Utils::HTTPCache($hash, $expiry);

if ($chart) {
	$chart = Charts::get((int)qg('chart'));
}
elseif (qg('year')) {
	$year = Years::get((int)qg('year'));








|

>
|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
header('X-Frame-Options: SAMEORIGIN', true);

$targets = qg('targets');
$chart = qg('chart');

// Cache the page until the charts have changed
$hash = sha1($targets . $chart);
$last_change = Config::getInstance()->get('last_chart_change') ?: time();

// Exit if there's no need to reload
Utils::HTTPCache($hash, $last_change);

if ($chart) {
	$chart = Charts::get((int)qg('chart'));
}
elseif (qg('year')) {
	$year = Years::get((int)qg('year'));

Modified src/www/admin/acc/transactions/edit.php from [1c19c2e07b] to [ae476319b4].

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if (f('save') && $form->check('acc_edit_' . $transaction->id(), $rules)) {
	try {
		$transaction->importFromEditForm();
		$transaction->save();

		// Append file
		if (!empty($_FILES['file']['name'])) {
			File::upload($_FILES['file'], File::CONTEXT_TRANSACTION, $transaction->id());
		}

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







|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if (f('save') && $form->check('acc_edit_' . $transaction->id(), $rules)) {
	try {
		$transaction->importFromEditForm();
		$transaction->save();

		// Append file
		if (!empty($_FILES['file']['name'])) {
			File::upload('file', File::CONTEXT_TRANSACTION, $transaction->id());
		}

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

Modified src/www/admin/common/search.php from [c7d3eff3a5] to [30c5ed1e7d].

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
		$query->limit = (int) f('limit') ?: $query->limit;
	}
}

// Recherche SQL
if (f('sql_query')) {
	// Only admins can run custom queries, others can only run saved queries
	$session->requireAccess($target, $session::ACCESS_ADMIN);
	$sql_query = f('sql_query');

	if ($session->canAccess('config', $session::ACCESS_ADMIN)) {
		$is_unprotected = (bool) f('unprotected');
	}
	else {
		$is_unprotected = false;
	}
}








|


|







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
		$query->limit = (int) f('limit') ?: $query->limit;
	}
}

// Recherche SQL
if (f('sql_query')) {
	// Only admins can run custom queries, others can only run saved queries
	$session->requireAccess($target == 'compta' ? $session::SECTION_ACCOUNTING : $session::SECTION_USERS, $session::ACCESS_ADMIN);
	$sql_query = f('sql_query');

	if ($session->canAccess($session::SECTION_CONFIG, $session::ACCESS_ADMIN)) {
		$is_unprotected = (bool) f('unprotected');
	}
	else {
		$is_unprotected = false;
	}
}

Modified src/www/admin/membres/ajouter.php from [c9a09edd99] to [39f6545e73].

42
43
44
45
46
47
48


49
50
51
52
53
54
55
56
57
58
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}



$tpl->assign('id_field_name', $config->get('champ_identifiant'));

$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $champs->getAll());

$tpl->assign('membres_cats', Categories::listSimple());
$tpl->assign('current_cat', f('category_id') ?: $config->get('categorie_membres'));

$tpl->display('admin/membres/ajouter.tpl');







>
>










42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
        }
        catch (UserException $e)
        {
            $form->addError($e->getMessage());
        }
    }
}

$tpl->assign('id_field_name', $config->get('champ_identifiant'));

$tpl->assign('id_field_name', $config->get('champ_identifiant'));

$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $champs->getAll());

$tpl->assign('membres_cats', Categories::listSimple());
$tpl->assign('current_cat', f('category_id') ?: $config->get('categorie_membres'));

$tpl->display('admin/membres/ajouter.tpl');

Modified src/www/admin/services/save.php from [7a6f7b643f] to [0acb3f484c].

1
2
3
4
5
6
7
8
9
10
11






12
13
14
15
16
17
18
<?php
namespace Garradin;

use Garradin\Services\Services;
use Garradin\Entities\Services\Service_User;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/_inc.php';

$session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE);







$count_all = Services::count();

if (!$count_all) {
	Utils::redirect(ADMIN_URL . 'services/?CREATE');
}












>
>
>
>
>
>







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 Garradin\Services\Services;
use Garradin\Entities\Services\Service_User;
use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/_inc.php';

$session->requireAccess($session::SECTION_USERS, $session::ACCESS_WRITE);

$count_all = Services::count();

if (!$count_all) {
	Utils::redirect(ADMIN_URL . 'services/?CREATE');
}

$count_all = Services::count();

if (!$count_all) {
	Utils::redirect(ADMIN_URL . 'services/?CREATE');
}

Modified src/www/admin/static/styles/03-forms.css from [cb1bad2db4] to [f5a97ac502].

Modified src/www/admin/static/styles/10-accounting.css from [c404c68c91] to [dc89fc1883].

100
101
102
103
104
105
106
107
108
table.accounts th { font-weight: normal; }
table.accounts .account-level-1 th { font-size: 1.6em; }
table.accounts .account-level-2 th { padding-left: 1em; font-size: 1.3em; }
table.accounts .account-level-3 th { padding-left: 2em; }
table.accounts .account-level-4 th { padding-left: 3em; }
table.accounts .account-level-5 th { padding-left: 4em; }
table.accounts .account-level-6 th { padding-left: 5em; }

table.projects tr.title p.help { font-weight: normal; text-align: center; }







<

100
101
102
103
104
105
106

107
table.accounts th { font-weight: normal; }
table.accounts .account-level-1 th { font-size: 1.6em; }
table.accounts .account-level-2 th { padding-left: 1em; font-size: 1.3em; }
table.accounts .account-level-3 th { padding-left: 2em; }
table.accounts .account-level-4 th { padding-left: 3em; }
table.accounts .account-level-5 th { padding-left: 4em; }
table.accounts .account-level-6 th { padding-left: 5em; }

table.projects tr.title p.help { font-weight: normal; text-align: center; }