Overview
Comment:Transaction and line saving works now
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 68ac12741f33171b12d906aaba9ce24e83d5eee3
User & Date: bohwaz on 2020-09-14 00:24:15
Other Links: branch diff | manifest | tags
Context
2020-09-15
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
2020-09-10
23:22
Fix new transaction keeping submitted values check-in: 0e4d9c752d user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Accounting/Accounts.php from [a3380e0712] to [1cd9093a01].

13
14
15
16
17
18
19





20
21
22
23
24
25
26
..
82
83
84
85
86
87
88
89
90
91
	protected $em;

	public function __construct(int $chart_id)
	{
		$this->chart_id = $chart_id;
		$this->em = EntityManager::getInstance(Account::class);
	}






	/**
	 * Return common accounting accounts from current chart
	 * (will not return analytical and volunteering accounts)
	 */
	public function listCommonTypes(): array
	{
................................................................................
		}

		return $out;
	}

	public function getTypesParents(): array
	{
		return $this->em->DB()->getAssoc($this->em->formatQuery('SELECT type, code FROM @TABLE WHERE type_parent = 1 ORDER BY type;'));
	}
}







>
>
>
>
>







 







|


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
..
87
88
89
90
91
92
93
94
95
96
	protected $em;

	public function __construct(int $chart_id)
	{
		$this->chart_id = $chart_id;
		$this->em = EntityManager::getInstance(Account::class);
	}

	public function getIdFromCode(string $code): int
	{
		return $this->em->col('SELECT id FROM @TABLE WHERE code = ?;', $code);
	}

	/**
	 * Return common accounting accounts from current chart
	 * (will not return analytical and volunteering accounts)
	 */
	public function listCommonTypes(): array
	{
................................................................................
		}

		return $out;
	}

	public function getTypesParents(): array
	{
		return $this->em->DB()->getAssoc($this->em->formatQuery('SELECT type, code FROM @TABLE WHERE type_parent = 1 AND id_chart = ? ORDER BY type;', $this->chart_id));
	}
}

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

10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
..
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
	const TABLE = 'acc_transactions_lines';

	protected $id;
	protected $id_transaction;
	protected $id_account;
	protected $credit = 0;
	protected $debit = 0;
	protected $payment_reference;
	protected $reconciled;

	protected $_types = [

		'id_transaction' => 'int',
		'id_account'     => 'int',
		'credit'         => 'int',
		'debit'          => 'int',
		'reference'      => '?string',
		'label'          => '?string',
		'reconciled'     => 'int',
................................................................................
		'reference'      => 'string|max:200',
		'label'          => 'string|max:200',
		'reconciled'     => 'int|min:0|max:1',
	];

	public function filterUserValue(string $key, $value, array $source)
	{
		$value = parent::filterUserValue($key, $value);

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

			$value = $match[1] . sprintf('%02d', $match[2]);

		}



		return $value;
	}

	public function selfCheck()
	{
		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.');
	}
}







|
|


>







 







<
<


|




|
>

>
>




|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..
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
	const TABLE = 'acc_transactions_lines';

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

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

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
..
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

















































<?php

namespace Garradin\Entities\Accounting;

use Garradin\Entity;
use Garradin\validatedException;

use Garradin\DB;
use Garradin\Config;

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

................................................................................
	protected $id;
	protected $label;
	protected $notes;
	protected $reference;

	protected $date;

	protected $validated;

	protected $hash;
	protected $prev_hash;

	protected $id_year;
	protected $id_analytical;

	protected $_types = [

		'label'             => 'string',
		'notes'             => '?string',
		'reference'         => '?string',
		'date'              => 'DateTime',
		'validated'         => 'bool',
		'hash'              => '?string',
		'prev_hash'         => '?string',
		'id_year'           => 'int',
		'id_analytical'     => '?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',
		'id_analytical'     => 'integer|in_table:acc_accounts,id'
	];

	protected $lines;

	public function getLines()
	{
		if (null === $this->lines && $this->exists()) {
			$db = DB::getInstance();
			$this->lines = $db->toObject($db->get('SELECT * FROM acc_transactions_lines WHERE id_transaction = ? ORDER BY id;', $this->id), Ligne::class);
		}
		else {
			$this->lines = [];
		}

		return $this->lines;
	}

/*
	public function getHash()
	{
		if (!$this->id_year) {
			throw new \LogicException('Il n\'est pas possible de hasher un mouvement qui n\'est pas associé à un exercice');
................................................................................
		$hash = hash_init('sha256');
		$values = $this->asArray();
		$values = array_intersect_key($values, $keep_keys);

		hash_update($hash, implode(',', array_keys($values)));
		hash_update($hash, implode(',', $values));

		foreach ($this->getLines() as $ligne) {
			hash_update($hash, implode(',', [$ligne->compte, $ligne->debit, $ligne->credit]));
		}

		return hash_final($hash, false);
	}

	public function checkHash()
	{
		return hash_equals($this->getHash(), $this->hash);
	}
*/

	public function add(Ligne $line)
	{
		$this->lines[] = $line;
	}

	public function transfer(int $amount, int $from, int $to)
	{
		$ligne1 = new Ligne;
		$ligne1->compte = $from;
		$ligne1->debit = $amount;
		$ligne1->credit = 0;

		$ligne2 = new Ligne;
		$ligne1->compte = $to;
		$ligne1->debit = 0;
		$ligne1->credit = $amount;

		return $this->add($ligne1) && $this->add($ligne2);
	}

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

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

		foreach ($this->lines as $ligne)
		{
			$ligne->id_transaction = $this->id;
			$ligne->save();
		}

		return true;
	}

	public function delete(): bool
	{
		if ($this->validated) {
			throw new validatedException('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 s'il existe déjà des exercices
		if (null === $this->id_year && $db->firstColumn('SELECT 1 FROM acc_years LIMIT 1;')) {
			throw new validatedException('Aucun exercice spécifié.');
		}

		if (null !== $this->id_year
			&& !$db->test('acc_years', 'id = ? AND start_date <= ? AND end_date >= ?;', $this->id_year, $this->date, $this->date))
		{
			throw new validatedException('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 validatedException('Mouvement non équilibré : déséquilibre entre débits et crédits');
		}
	}
}






















































|
>







 







|





<


>
|
|
|
|
|
|
|
|
<



|
|
|
|
|
|
<


|



|

|

|
|


|







 







|
|











|

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





|






|

|
|








|












|
|
|


<
|

|












|


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

................................................................................
	protected $id;
	protected $label;
	protected $notes;
	protected $reference;

	protected $date;

	protected $validated = 0;

	protected $hash;
	protected $prev_hash;

	protected $id_year;


	protected $_types = [
		'id'        => 'int',
		'label'     => 'string',
		'notes'     => '?string',
		'reference' => '?string',
		'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()) {
			$db = DB::getInstance();
			$this->_lines = $db->toObject($db->get('SELECT * FROM ' . Line::TABLE . ' WHERE id_transaction = ? ORDER BY id;', $this->id), Ligne::class);
		}
		elseif (null === $this->_lines) {
			$this->_lines = [];
		}

		return $this->_lines;
	}

/*
	public function getHash()
	{
		if (!$this->id_year) {
			throw new \LogicException('Il n\'est pas possible de hasher un mouvement qui n\'est pas associé à un exercice');
................................................................................
		$hash = hash_init('sha256');
		$values = $this->asArray();
		$values = array_intersect_key($values, $keep_keys);

		hash_update($hash, implode(',', array_keys($values)));
		hash_update($hash, implode(',', $values));

		foreach ($this->getLines() as $line) {
			hash_update($hash, implode(',', [$line->compte, $line->debit, $line->credit]));
		}

		return hash_final($hash, false);
	}

	public function checkHash()
	{
		return hash_equals($this->getHash(), $this->hash);
	}
*/

	public function add(Line $line)
	{
		$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);
			}
		}
	}
}

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

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    protected $end_date;
    protected $closed = 0;
    protected $id_chart;

    protected $_types = [
        'id'         => 'integer',
        'label'      => 'string',
        'start_date' => 'DateTime',
        'end_date'   => 'DateTime',
        'closed'     => 'integer',
        'id_chart'   => 'integer',
    ];

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







|
|







17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    protected $end_date;
    protected $closed = 0;
    protected $id_chart;

    protected $_types = [
        'id'         => 'integer',
        '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',

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

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
...
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
			{input type="list" target="%sacc/accounts/selector.php?target=common"|args:$admin_url name="expense_from" label="Compte de décaissement" required=1}
		</dl>
	</fieldset>

	<fieldset data-types="debt">
		<legend>Dette</legend>
		<dl>
			{input type="list" target="%sacc/accounts/selector.php?target=thirdparty"|args:$admin_url name="debt_to" label="Compte de tiers" required=1}
			{input type="list" target="%sacc/accounts/selector.php?target=expense"|args:$admin_url name="debt_from" label="Type de dette (dépense)" required=1}
		</dl>
	</fieldset>

	<fieldset data-types="credit">
		<legend>Créance</legend>
		<dl>
			{input type="list" target="%sacc/accounts/selector.php?target=thirdparty"|args:$admin_url name="credit_to" label="Compte de tiers" required=1}
................................................................................
		<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="reference_paiement" 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">
................................................................................
			</tfoot>
		</table>
	</fieldset>

	<fieldset>
		<legend>Détails</legend>
		<dl>
			{input type="list" multiple=true name="membre" label="Membre associé" target="%smembres/selector.php"|args:$admin_url}
			{input type="text" name="numero_piece" label="Numéro de pièce comptable"}
			{input type="textarea" name="remarques" 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>








|
|







 







|







 







|
|
|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
..
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
...
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
			{input type="list" target="%sacc/accounts/selector.php?target=common"|args:$admin_url name="expense_from" label="Compte de décaissement" required=1}
		</dl>
	</fieldset>

	<fieldset data-types="debt">
		<legend>Dette</legend>
		<dl>
			{input type="list" target="%sacc/accounts/selector.php?target=thirdparty"|args:$admin_url name="debt_from" label="Compte de tiers" required=1}
			{input type="list" target="%sacc/accounts/selector.php?target=expense"|args:$admin_url name="debt_to" label="Type de dette (dépense)" required=1}
		</dl>
	</fieldset>

	<fieldset data-types="credit">
		<legend>Créance</legend>
		<dl>
			{input type="list" target="%sacc/accounts/selector.php?target=thirdparty"|args:$admin_url name="credit_to" label="Compte de tiers" required=1}
................................................................................
		<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">
................................................................................
			</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>

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

9
10
11
12
13
14
15


16

17
18
19
20
21
22
23
24
25

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

$transaction = new Transaction;

if (f('save')) {


    $transaction->import();

}

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







>
>
|
>









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

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