Overview
Comment:Working multi-lines transactions!
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 2841f04851bb95f6acfecd117728e765e6cd1aa4
User & Date: bohwaz on 2020-09-15 01:09:06
Other Links: branch diff | manifest | tags
Context
2020-09-15
02:15
List of all accounts check-in: b55259b64f user: bohwaz tags: dev
01:09
Working multi-lines transactions! check-in: 2841f04851 user: bohwaz tags: dev
2020-09-14
00:24
Transaction and line saving works now check-in: 68ac12741f user: bohwaz tags: dev
Changes

Modified src/include/init.php from [1ff1ce35fc] to [180dd6419d].

216
217
218
219
220
221
222




223
224
225
226
227
228
229
/*
 * Gestion des erreurs et exceptions
 */

class UserException extends \LogicException
{
}





// activer le gestionnaire d'erreurs/exceptions
ErrorManager::enable(SHOW_ERRORS ? ErrorManager::DEVELOPMENT : ErrorManager::PRODUCTION);
ErrorManager::setLogFile(DATA_ROOT . '/error.log');

// activer l'envoi de mails si besoin est
if (MAIL_ERRORS)







>
>
>
>







216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
 * Gestion des erreurs et exceptions
 */

class UserException extends \LogicException
{
}

class ValidationException extends UserException
{
}

// activer le gestionnaire d'erreurs/exceptions
ErrorManager::enable(SHOW_ERRORS ? ErrorManager::DEVELOPMENT : ErrorManager::PRODUCTION);
ErrorManager::setLogFile(DATA_ROOT . '/error.log');

// activer l'envoi de mails si besoin est
if (MAIL_ERRORS)

Modified src/include/lib/Garradin/Entities/Accounting/Account.php from [2bd9e816eb] to [6cd53f78f7].

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
		'description' => '?string',
		'position'    => 'int',
		'type'        => 'int',
		'type_parent' => 'int',
		'user'        => 'int',
	];

	protected $_validation_rules = [
		'id_chart'    => 'required|integer|in_table:acc_charts,id',
		'code'        => 'required|string|alpha_num|max:10',
		'label'       => 'required|string|max:200',
		'description' => 'string|max:2000',
		'position'    => 'required|integer',
		'type'        => 'required|integer|min:0',
		'type_parent' => 'integer|min:0',
		'user'        => 'integer|min:0|max:1',
	];
}







|
<




|
|
<


80
81
82
83
84
85
86
87

88
89
90
91
92
93

94
95
		'description' => '?string',
		'position'    => 'int',
		'type'        => 'int',
		'type_parent' => 'int',
		'user'        => 'int',
	];

	protected $_form_rules = [

		'code'        => 'required|string|alpha_num|max:10',
		'label'       => 'required|string|max:200',
		'description' => 'string|max:2000',
		'position'    => 'required|integer',
		'type'        => 'numeric|min:0',
		'type_parent' => 'numeric|min:0',

	];
}

Modified src/include/lib/Garradin/Entities/Accounting/Chart.php from [0d7b6f937a] to [50d0731c82].

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
        'id'       => 'integer',
        'label'    => 'string',
        'country'  => 'string',
        'code'     => '?string',
        'archived' => 'integer',
    ];

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

    public function selfCheck(): void
    {
        parent::selfCheck();
        $this->assert(Utils::getCountryName($this->country), 'Le code pays doit être un code ISO valide');

    }

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







|



<






>







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
        'id'       => 'integer',
        'label'    => 'string',
        'country'  => 'string',
        'code'     => '?string',
        'archived' => 'integer',
    ];

    protected $_form_rules = [
        'label'    => 'required|string|max:200',
        'country'  => 'required|string|size:2',
        'code'     => 'string',

    ];

    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());
    }
}

Modified src/include/lib/Garradin/Entities/Accounting/Line.php from [d49a52a40e] to [33bab57818].

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

	protected $id;
	protected $id_transaction;
	protected $id_account;
	protected $credit = 0;
	protected $debit = 0;
	protected $reference;

	protected $reconciled = 0;


	protected $_types = [
		'id'             => 'int',
		'id_transaction' => 'int',
		'id_account'     => 'int',
		'credit'         => 'int',
		'debit'          => 'int',
		'reference'      => '?string',
		'label'          => '?string',
		'reconciled'     => 'int',

	];

	protected $_validation_rules = [
		'id_transaction' => 'required|integer|in_table:acc_transactions,id',
		'id_account'     => 'required|integer|in_table:acc_accounts,id',
		'credit'         => 'required|integer|min:0',
		'debit'          => 'required|integer|min:0',
		'reference'      => 'string|max:200',
		'label'          => 'string|max:200',
		'reconciled'     => 'int|min:0|max:1',
	];

	public function filterUserValue(string $key, $value, array $source)
	{
		if ($key == 'credit' || $key == 'debit')
		{
			if (!preg_match('/^(\d+)(?:[,.](\d{1,2}))?$/', $value, $match))
			{
				throw new ValidationException('Le format du montant est invalide. Format accepté, exemple : 142,02');
			}

			$value = $match[1] . str_pad((int)@$match[2], 2, '0', STR_PAD_RIGHT);
			$value = (int) $value;
		}

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

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

	}
}







>

>










>


|
|
|
|
|


<


|



|
<







|










>


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

	protected $id;
	protected $id_transaction;
	protected $id_account;
	protected $credit = 0;
	protected $debit = 0;
	protected $reference;
	protected $label;
	protected $reconciled = 0;
	protected $id_analytical;

	protected $_types = [
		'id'             => 'int',
		'id_transaction' => 'int',
		'id_account'     => 'int',
		'credit'         => 'int',
		'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',
		'credit'         => 'required|money|min:0',
		'debit'          => 'required|money|min:0',
		'reference'      => 'string|max:200',
		'label'          => 'string|max:200',

	];

	public function filterUserValue(string $key, $value)
	{
		if ($key == 'credit' || $key == 'debit')
		{
			if (!preg_match('/^(\d+)(?:[,.](\d{1,2}))?$/', $value, $match)) {

				throw new ValidationException('Le format du montant est invalide. Format accepté, exemple : 142,02');
			}

			$value = $match[1] . str_pad((int)@$match[2], 2, '0', STR_PAD_RIGHT);
			$value = (int) $value;
		}

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

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

Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [4e9de82eec] to [27e8f56d39].

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

namespace Garradin\Entities\Accounting;

use Garradin\Entity;
use Garradin\Accounting\Accounts;
use LogicException;
use Garradin\DB;
use Garradin\Config;

class Transaction extends Entity
{
	const TABLE = 'acc_transactions';







|







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

namespace Garradin\Entities\Accounting;

use Garradin\Entity;
use Garradin\Accounting\Accounts;
use Garradin\ValidationException;
use Garradin\DB;
use Garradin\Config;

class Transaction extends Entity
{
	const TABLE = 'acc_transactions';

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
		'date'      => 'date',
		'validated' => 'bool',
		'hash'      => '?string',
		'prev_hash' => '?string',
		'id_year'   => 'int',
	];

	protected $_validated_rules = [
		'label'     => 'required|string|max:200',
		'notes'     => 'string|max:20000',
		'reference' => 'string|max:200',
		'date'      => 'required|date',
		'validated' => 'bool',
		'id_year'   => 'integer|in_table:acc_years,id',
	];

	protected $_lines;

	public function getLines()
	{
		if (null === $this->_lines && $this->exists()) {







|



|
<
<







34
35
36
37
38
39
40
41
42
43
44
45


46
47
48
49
50
51
52
		'date'      => 'date',
		'validated' => 'bool',
		'hash'      => '?string',
		'prev_hash' => '?string',
		'id_year'   => 'int',
	];

	protected $_form_rules = [
		'label'     => 'required|string|max:200',
		'notes'     => 'string|max:20000',
		'reference' => 'string|max:200',
		'date'      => 'required|date_format:Y-m-d',


	];

	protected $_lines;

	public function getLines()
	{
		if (null === $this->_lines && $this->exists()) {
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
	{
		$this->_lines[] = $line;
	}

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

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

		foreach ($this->_lines as &$line)
		{
			$line->id_transaction = $this->id();
			$line->save();
		}

		return true;
	}

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

		return parent::delete();
	}

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

		$db = DB::getInstance();
		$config = Config::getInstance();

		// ID d'exercice obligatoire
		if (null === $this->id_year) {
			throw new LogicException('Aucun exercice spécifié.');
		}

		if (!$db->test(Year::TABLE, 'id = ? AND start_date <= ? AND end_date >= ?;', $this->id_year, $this->date, $this->date))
		{
			throw new LogicException('La date ne correspond pas à l\'exercice sélectionné.');
		}

		$total = 0;

		$lines = $this->getLines();

		foreach ($lines as $line) {
			$total += $line->credit;
			$total -= $line->debit;
		}

		if (0 !== $total) {
			throw new LogicException('Mouvement non équilibré : déséquilibre entre débits et crédits');
		}
	}

	public function importFromSimpleForm(int $chart_id, ?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($source['type'])) {
			throw new LogicException('Type d\'écriture inconnu');
		}

		$type = $source['type'];

		$this->import();

		$accounts = new Accounts($chart_id);

		if ($type !== 'advanced') {
			$from = $accounts->getIdFromCode($source[$type . '_from']);
			$to = $accounts->getIdFromCode($source[$type . '_to']);
			$amount = $source['amount'];

			$line = new Line;
			$line->import([
				'reference'  => $source['payment_reference'],

				'debit'      => $amount,
				'id_account' => $from,
				'id_analytical' => $source['id_analytical'] ?? null,
			]);
			$this->add($line);

			$line = new Line;
			$line->import([
				'reference'  => $source['payment_reference'],
				'credit'     => $amount,

				'id_account' => $to,
				'id_analytical' => $source['id_analytical'] ?? null,
			]);
			$this->add($line);
		}
		else {
			foreach ($sources['lines'] as $line) {
				$line['id_account'] = $accounts->getIdFromCode($line['account']);





				$line = (new Line)->import($line);
				$this->add($line);
			}
		}
	}
}







|


















|














|




|












|










|




|









|

>







|


>






|

>
>
>
>







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
	{
		$this->_lines[] = $line;
	}

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

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

		foreach ($this->_lines as &$line)
		{
			$line->id_transaction = $this->id();
			$line->save();
		}

		return true;
	}

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

		return parent::delete();
	}

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

		$db = DB::getInstance();
		$config = Config::getInstance();

		// ID d'exercice obligatoire
		if (null === $this->id_year) {
			throw new \LogicException('Aucun exercice spécifié.');
		}

		if (!$db->test(Year::TABLE, 'id = ? AND start_date <= ? AND end_date >= ?;', $this->id_year, $this->date, $this->date))
		{
			throw new ValidationException('La date ne correspond pas à l\'exercice sélectionné.');
		}

		$total = 0;

		$lines = $this->getLines();

		foreach ($lines as $line) {
			$total += $line->credit;
			$total -= $line->debit;
		}

		if (0 !== $total) {
			throw new ValidationException('Écriture non équilibrée : déséquilibre entre débits et crédits');
		}
	}

	public function importFromSimpleForm(int $chart_id, ?array $source = null): void
	{
		if (null === $source) {
			$source = $_POST;
		}

		if (empty($source['type'])) {
			throw new ValidationException('Type d\'écriture inconnu');
		}

		$type = $source['type'];

		$this->importForm();

		$accounts = new Accounts($chart_id);

		if ($type !== 'advanced') {
			$from = $accounts->getIdFromCode($source[$type . '_from']);
			$to = $accounts->getIdFromCode($source[$type . '_to']);
			$amount = $source['amount'];

			$line = new Line;
			$line->importForm([
				'reference'  => $source['payment_reference'],
				'credit' => '0',
				'debit'      => $amount,
				'id_account' => $from,
				'id_analytical' => $source['id_analytical'] ?? null,
			]);
			$this->add($line);

			$line = new Line;
			$line->importForm([
				'reference'  => $source['payment_reference'],
				'credit'     => $amount,
				'debit' => '0',
				'id_account' => $to,
				'id_analytical' => $source['id_analytical'] ?? null,
			]);
			$this->add($line);
		}
		else {
			foreach ($source['lines'] as $i => $line) {
				$line['id_account'] = $accounts->getIdFromCode($line['account']);

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

				$line = (new Line)->import($line);
				$this->add($line);
			}
		}
	}
}

Modified src/include/lib/Garradin/Entities/Accounting/Year.php from [57ce849b11] to [2087129994].

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
        'label'      => 'string',
        'start_date' => 'date',
        'end_date'   => 'date',
        'closed'     => 'integer',
        'id_chart'   => 'integer',
    ];

    protected $_validation_rules = [
        'label'      => 'required|string|max:200',
        'start_date' => 'required|date|before:end_date',
        'end_date'   => 'required|date|after:start_date',
        'closed'     => 'int|min:0|max:1',
        'id_chart'   => 'required|integer',
    ];

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

        $db = DB::getInstance();



        // Vérifier qu'on ne crée pas 2 exercices qui se recoupent
        if ($this->exists()) {
            $this->assert(
                !$db->test(self::TABLE, 'id != :id AND ((start_date <= :start_date AND end_date >= :start_date) OR (start_date <= :end_date AND end_date >= :start_date))',
                    ['id' => $this->id(), 'start_date' => $this->start_date, 'end_date' => $this->end_date]),
                'La date de début ou de fin se recoupe avec un exercice existant.'







|



<
<






>



>
>







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
        'label'      => 'string',
        'start_date' => 'date',
        'end_date'   => 'date',
        'closed'     => 'integer',
        'id_chart'   => 'integer',
    ];

    protected $_form_rules = [
        'label'      => 'required|string|max:200',
        'start_date' => 'required|date|before:end_date',
        'end_date'   => 'required|date|after:start_date',


    ];

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

        // Vérifier qu'on ne crée pas 2 exercices qui se recoupent
        if ($this->exists()) {
            $this->assert(
                !$db->test(self::TABLE, 'id != :id AND ((start_date <= :start_date AND end_date >= :start_date) OR (start_date <= :end_date AND end_date >= :start_date))',
                    ['id' => $this->id(), 'start_date' => $this->start_date, 'end_date' => $this->end_date]),
                'La date de début ou de fin se recoupe avec un exercice existant.'

Modified src/include/lib/Garradin/Entity.php from [c7993d892d] to [8182e740f9].

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
<?php

namespace Garradin;

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

class Entity extends AbstractEntity
{


	/**
	 * Valider les champs avant enregistrement
	 * @throws ValidationException Si une erreur de validation survient
	 */
	public function selfValidate()
	{
		$errors = [];

		if (!Form::validate($this->_fields, $errors, $this->toArray()))
		{
			$messages = [];

			foreach ($errors as $error)

			{
				$messages[] = $this->getValidationMessage($error);
			}




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




|




>
>




|

|

|

|

|
>
|
|


>
>
>
|



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
<?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)
	{
		$form = new Form;

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

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

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

	protected function assert(bool $test, string $message = null): void
	{
		if (null !== $message && !$test) {
			throw new ValidationException($message);
		}
	}
}

Modified src/include/lib/Garradin/Form.php from [5a5ce58fea] to [2e98f18492].

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
		{
			return false;
		}

		return true;
	}

	public function validate(Array $rules)
	{
		return \KD2\Form::validate($rules, $this->errors, $_POST);
	}

	public function hasErrors()
	{
		return (count($this->errors) > 0);
	}








|

|







33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
		{
			return false;
		}

		return true;
	}

	public function validate(Array $rules, array $source = null)
	{
		return \KD2\Form::validate($rules, $this->errors, $source);
	}

	public function hasErrors()
	{
		return (count($this->errors) > 0);
	}

Modified src/include/lib/Garradin/Template.php from [1c52f5e1ed] to [642442e512].

281
282
283
284
285
286
287




288
289
290
291
292
293
294
				'shape' => $multiple ? 'plus' : 'menu',
				'value' => $attributes['target'],
				'label' => $multiple ? 'Ajouter' : 'Sélectionner'
			]);

			$input = sprintf('<span id="%s_container" class="input-list">%s<input type="hidden" value="%s" %s /><span class="label">%3$s</span></span>', $this->escape($attributes['id']), $button, $this->escape($current_value), $attributes_string);
		}




		else {
			$input = sprintf('<input type="%s" %s value="%s" />', $type, $attributes_string, $this->escape($current_value));
		}

		// No label? then we only want the input without the widget
		if (empty($label)) {
			return $input;







>
>
>
>







281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
				'shape' => $multiple ? 'plus' : 'menu',
				'value' => $attributes['target'],
				'label' => $multiple ? 'Ajouter' : 'Sélectionner'
			]);

			$input = sprintf('<span id="%s_container" class="input-list">%s<input type="hidden" value="%s" %s /><span class="label">%3$s</span></span>', $this->escape($attributes['id']), $button, $this->escape($current_value), $attributes_string);
		}
		elseif ($type == 'money') {
			$currency = Config::getInstance()->get('monnaie');
			$input = sprintf('<input type="text" pattern="[0-9.,]*" inputmode="decimal" size="8" class="money" %s value="%s" /><b>%s</b>', $attributes_string, $this->escape($current_value), $currency);
		}
		else {
			$input = sprintf('<input type="%s" %s value="%s" />', $type, $attributes_string, $this->escape($current_value));
		}

		// No label? then we only want the input without the widget
		if (empty($label)) {
			return $input;

Modified src/templates/acc/transactions/new.tpl from [0bffff6151] to [07d0485204].

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

<form method="post" action="{$self_url}">
	{form_errors}

	{if $ok}
		<p class="confirm">
			L'opération numéro <a href="{$admin_url}compta/operations/voir.php?id={$ok}">{$ok}</a> a été ajoutée.
			(<a href="{$admin_url}compta/operations/voir.php?id={$ok}">Voir l'opération</a>)
		</p>
	{/if}

	<fieldset>
		<legend>Type d'écriture</legend>
		<dl>
			{input type="radio" name="type" value="revenue" label="Recette"}







|
|







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

<form method="post" action="{$self_url}">
	{form_errors}

	{if $ok}
		<p class="confirm">
			L'opération numéro <a href="details.php?id={$ok}">{$ok}</a> a été ajoutée.
			(<a href="details.php?id={$ok}">Voir l'opération</a>)
		</p>
	{/if}

	<fieldset>
		<legend>Type d'écriture</legend>
		<dl>
			{input type="radio" name="type" value="revenue" label="Recette"}
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
	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="text" name="label" label="Libellé" required=1}
			{input type="date" name="date" default=$date label="Date" required=1}
		</dl>
		<dl data-types="all-but-advanced">
			{input type="number" name="amount" label="Montant (%s)"|args:$config.monnaie min="0.00" step="0.01" default="0" required=1}
			{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>



	{* Saisie avancée *}
	<fieldset data-types="advanced">
		<table class="list transaction-lines">
			<thead>
				<tr>
					<th>Compte</th>
					<td>Débit</td>
					<td>Crédit</td>
					<td>Réf. pièce</td>
					<td>Libellé ligne</td>
					<td></td>
				</tr>
			</thead>
			<tbody>
			{foreach from=$lines key="line_number" item="line"}
				<tr>
					<th>{input type="list" target="%sacc/accounts/selector.php?target=all"|args:$admin_url name="lines[%d][account]"|args:$line_number value=$line.id_account}</th>
					<td>{input type="number" name="lines[%d][debit]"|args:$line_number min="0.00" step="0.01" value=$line.debit size=5}</td>
					<td>{input type="number" name="lines[%d][credit]"|args:$line_number min="0.00" step="0.01" value=$line.credit size=5}</td>
					<td>{input type="text" name="lines[%d][reference]"|args:$line_number size=10}</td>
					<td>{input type="text" name="lines[%d][label]"|args:$line_number}</td>
					<td>{button label="Enlever la ligne" shape="minus"}</td>
				</tr>
			{/foreach}
			</tbody>
			<tfoot>
				<tr>
					<th></th>
					<td><input type="number" id="lines_debit_total" readonly="readonly" size="5" tabindex="-1" /></td>
					<td><input type="number" id="lines_credit_total" readonly="readonly" size="5" tabindex="-1" /></td>
					<td colspan="2"></td>
					<td>{button label="Ajouter une ligne" shape="plus"}</td>
				</tr>
			</tfoot>
		</table>
	</fieldset>

	<fieldset>
		<legend>Détails</legend>
		<dl>
			{input type="list" multiple=true name="users" label="Membre associé" target="%smembres/selector.php"|args:$admin_url}
			{input type="text" name="reference" label="Numéro de pièce comptable"}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}

			{if count($analytical_accounts) > 0}
				{input type="select" name="analytical_account" label="Compte analytique (projet)" options=$analytical_accounts}
			{/if}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="compta_saisie"}
		<input type="submit" name="save" value="Enregistrer &rarr;" />
	</p>

</form>

{literal}
<script type="text/javascript">

function initForm() {

	function hideAllTypes() {
		var sections = $('fieldset[data-types]');

		sections.forEach((e) => {
			e.style.display = 'none';
		});
	}


	function selectType(v) {
		hideAllTypes();
		$('[data-types=' + v + ']')[0].style.display = 'block';
		$('[data-types=all-but-advanced]')[0].style.display = v == 'advanced' ? 'none' : 'block';





	}

	var radios = $('fieldset input[type=radio][name=type]');

	radios.forEach((e) => {
		e.onchange = () => {
			selectType(e.value);
		};
	});

	hideAllTypes();

	// In case of a pre-filled form
	var current = document.querySelector('input[name=type]:checked');
	if (current) {
		selectType(current.value);
	}


	var lines = $('.transaction-lines tbody tr');

	function initLine(e) {
		e.querySelector('button:nth-child(1)').onclick = () => {
			var count = $('.transaction-lines tbody tr').length;

			if (count <= 2) {
				alert("Il n'est pas possible d'avoir moins de deux lignes dans une écriture.");
				return false;
			}

			e.parentNode.removeChild(e);

		};
	}






















	lines.forEach((e) => {
		initLine(e);



















	});










	$('.transaction-lines tfoot button')[0].onclick = () => {
		var line = $('.transaction-lines tbody tr')[0];
		var n = line.cloneNode(true);
		n.querySelectorAll('input').forEach((e) => {
			e.value = '';
		});
		n.querySelector('.input-list .label').innerHTML = '';







|




















|

|
|
|
|
|
|






|
|
|














|















>








>




>
>
>
>
>












|





>


|
|







|
>

|
>
>

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


>
>
>
>
>
>
>
>
>







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
	<fieldset>
		<legend>Informations</legend>
		<dl>
			{input type="text" name="label" label="Libellé" required=1}
			{input type="date" name="date" default=$date label="Date" required=1}
		</dl>
		<dl data-types="all-but-advanced">
			{input type="money" name="amount" label="Montant" required=1}
			{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>



	{* Saisie avancée *}
	<fieldset data-types="advanced">
		<table class="list transaction-lines">
			<thead>
				<tr>
					<th>Compte</th>
					<td>Débit</td>
					<td>Crédit</td>
					<td>Réf. pièce</td>
					<td>Libellé ligne</td>
					<td></td>
				</tr>
			</thead>
			<tbody>
			{foreach from=$lines item="line"}
				<tr>
					<th>{input type="list" target="%sacc/accounts/selector.php?target=all"|args:$admin_url name="lines[account][]" value=$line.account}</th>
					<td>{input type="money" name="lines[debit][]" value=$line.debit size=5}</td>
					<td>{input type="money" name="lines[credit][]" value=$line.credit size=5}</td>
					<td>{input type="text" name="lines[reference][]" value=$line.label size=10}</td>
					<td>{input type="text" name="lines[label][]"}</td>
					<td>{button label="Enlever la ligne" shape="minus" name="remove_line"}</td>
				</tr>
			{/foreach}
			</tbody>
			<tfoot>
				<tr>
					<th></th>
					<td>{input type="money" name="debit_total" readonly="readonly" tabindex="-1" }</td>
					<td>{input type="money" name="credit_total" readonly="readonly" tabindex="-1" }</td>
					<td colspan="2" id="lines_message"></td>
					<td>{button label="Ajouter une ligne" shape="plus"}</td>
				</tr>
			</tfoot>
		</table>
	</fieldset>

	<fieldset>
		<legend>Détails</legend>
		<dl>
			{input type="list" multiple=true name="users" label="Membre associé" target="%smembres/selector.php"|args:$admin_url}
			{input type="text" name="reference" label="Numéro de pièce comptable"}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}

			{if count($analytical_accounts) > 0}
				{input type="select" name="id_analytical" label="Compte analytique (projet)" options=$analytical_accounts}
			{/if}
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key="compta_saisie"}
		<input type="submit" name="save" value="Enregistrer &rarr;" />
	</p>

</form>

{literal}
<script type="text/javascript">

function initForm() {
	// Hide type specific parts of the form
	function hideAllTypes() {
		var sections = $('fieldset[data-types]');

		sections.forEach((e) => {
			e.style.display = 'none';
		});
	}

	// Toggle parts of the form when a type is selected
	function selectType(v) {
		hideAllTypes();
		$('[data-types=' + v + ']')[0].style.display = 'block';
		$('[data-types=all-but-advanced]')[0].style.display = v == 'advanced' ? 'none' : 'block';
		// Disable required form elements, or the form won't be able to be submitted
		$('[data-types=all-but-advanced] input[required]').forEach((e) => {
			e.disabled = v == 'advanced' ? true : false;
		});

	}

	var radios = $('fieldset input[type=radio][name=type]');

	radios.forEach((e) => {
		e.onchange = () => {
			selectType(e.value);
		};
	});

	hideAllTypes();

	// In case of a pre-filled form: show the correct part of the form
	var current = document.querySelector('input[name=type]:checked');
	if (current) {
		selectType(current.value);
	}

	// Advanced transaction: line management
	var lines = $('.transaction-lines tbody tr');

	function initLine(row) {
		row.querySelector('button[name="remove_line"]').onclick = () => {
			var count = $('.transaction-lines tbody tr').length;

			if (count <= 2) {
				alert("Il n'est pas possible d'avoir moins de deux lignes dans une écriture.");
				return false;
			}

			row.parentNode.removeChild(row);
			updateTotals();
		};

		// Update totals and disable other amount input
		var money_inputs = row.querySelectorAll('input.money');

		money_inputs.forEach((i) => {
			i.onkeyup = (e) => {
				if (!e.key.match(/^([0-9,.]|Separator|Backspace)$/i)) {
					return true;
				}

				var v = i.value.replace(/[^0-9.,]/);
				var ro = (v.length == 0 || v == 0) ? false : true;

				money_inputs.forEach((i2) => {
					i2.readOnly = i2 === i ? false : ro;
					if (i2 !== i) { i2.value = ''; }
				});

				updateTotals();
			};
		});
	}

	lines.forEach(initLine);

	function updateTotals() {
		var amounts = $('.transaction-lines tbody input.money');
		var debit = credit = 0;

		amounts.forEach((i) => {
			var v = i.value.replace(/[^0-9.,]/, '');
			if (v.length == 0) return;

			v = v.split(/[,.]/);
			var d = v.length == 2 ? v[1] : '0';
			v = v[0] + (d + '00').substr(0, 2);
			v = parseInt(v, 10);

			if (i.name.match(/debit/)) {
				debit += v;
			}
			else {
				credit += v;
			}
	});

		$('#lines_message').innerHTML = (debit === credit) ? '' : '<span class="alert">Écriture non équilibrée</span>';

		debit = debit ? debit + '' : '000';
		credit = credit ? credit + '' : '000';
		$('#f_debit_total').value = debit.substr(0, debit.length-2) + ',' + debit.substr(-2);
		$('#f_credit_total').value = credit.substr(0, credit.length-2) + ',' + credit.substr(-2);
	}

	// Add row button
	$('.transaction-lines tfoot button')[0].onclick = () => {
		var line = $('.transaction-lines tbody tr')[0];
		var n = line.cloneNode(true);
		n.querySelectorAll('input').forEach((e) => {
			e.value = '';
		});
		n.querySelector('.input-list .label').innerHTML = '';

Modified src/www/admin/acc/transactions/new.php from [c51ff736f0] to [bb1d5d40e0].

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
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ECRITURE);

$chart = $year->chart();
$accounts = $chart->accounts();

$transaction = new Transaction;


if (f('save')) {




























    $transaction->id_year = $year->id();
    $transaction->importFromSimpleForm($chart->id());
    $transaction->save();
    //echo '<pre>'; print_r($transaction); exit;




}

$tpl->assign('date', $session->get('context_compta_date') ?: false);
$tpl->assign('ok', (int) qg('ok'));

$tpl->assign('lines', $transaction->getLines() ?: [[]]);

$tpl->assign('analytical_accounts', ['label' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->display('acc/transactions/new.tpl');













>


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



|
>
>
>
>





|

|

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
<?php
namespace Garradin;

use Garradin\Entities\Accounting\Transaction;

require_once __DIR__ . '/../_inc.php';

$session->requireAccess('compta', Membres::DROIT_ECRITURE);

$chart = $year->chart();
$accounts = $chart->accounts();

$transaction = new Transaction;
$lines = [[], []];

if (f('save')) {
    try {
        if (f('type') == 'advanced' && $lines = f('lines')) {
            $max = count($lines['label']);

            if ($max != count($lines['debit'])
                || $max != count($lines['credit'])
                || $max != count($lines['reference'])
                || $max != count($lines['account']))
            {
                throw new UserException('Erreur dans les lignes de l\'écriture');
            }

            $out = [];

            // Reorder the POST data as a proper array
            for ($i = 0; $i < $max; $i++) {
                $out[] = [
                    'debit'      => $lines['debit'][$i],
                    'credit'     => $lines['credit'][$i],
                    'reference'  => $lines['reference'][$i],
                    'label'      => $lines['label'][$i],
                    'account'    => $lines['account'][$i],
                ];
            }

            $_POST['lines'] = $lines = $out;
        }

    $transaction->id_year = $year->id();
    $transaction->importFromSimpleForm($chart->id());
    $transaction->save();
        Utils::redirect(Utils::getSelfURL(false) . '?ok=' . $transaction->id());
    }
    catch (UserException $e) {
        $form->addError($e->getMessage());
    }
}

$tpl->assign('date', $session->get('context_compta_date') ?: false);
$tpl->assign('ok', (int) qg('ok'));

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

$tpl->assign('analytical_accounts', ['' => '-- Aucun'] + $accounts->listAnalytical());
$tpl->display('acc/transactions/new.tpl');

Modified src/www/admin/static/admin.css from [f452a06028] to [51a75ddf45].

387
388
389
390
391
392
393















394
395
396
397
398
399
400
form .input-list button {
    padding: .4rem .6rem;
    margin: 0;
    border: none;
    border-right: 1px solid #ccc;
    border-radius: 0;
}
















#dialog {
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    right: 0;







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







387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
form .input-list button {
    padding: .4rem .6rem;
    margin: 0;
    border: none;
    border-right: 1px solid #ccc;
    border-radius: 0;
}

input[readonly] {
    color: #666;
    background-color: #eee;
}

input.money {
    text-align: right;
}

input.money + b {
    padding: .4rem .6rem;
    line-height: 1.5rem;
    color: #999;
}

#dialog {
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    right: 0;