Overview
Comment:Merge with trunk
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | blocks
Files: files | file ages | folders
SHA3-256: 56711fffde762852aeeb116693076a992b575a3171ea72d520671f1ede62724e
User & Date: bohwaz on 2022-05-02 11:56:09
Other Links: branch diff | manifest | tags
Context
2022-05-03
21:53
Hide blocks for now check-in: d83accac28 user: bohwaz tags: blocks
2022-05-02
11:56
Merge with trunk check-in: 56711fffde user: bohwaz tags: blocks
11:44
Fix opening balance automatic balancing check-in: 1ca75d8773 user: bohwaz tags: trunk, stable
2022-04-26
02:20
Move menu bar to bottom of screen on mobile, improve HTML for menu for navigation with text browser and accessibility check-in: 6152ca9264 user: bohwaz tags: blocks
Changes

Added src/include/lib/Garradin/Accounting/AssistedReconciliation.php version [53fcb114af].



























































































































































































































































































































































































































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

namespace Garradin\Accounting;

use Garradin\CSV_Custom;
use Garradin\UserException;
use Garradin\Utils;
use Garradin\Membres\Session;
use Garradin\Entities\Accounting\Transaction;

/**
 * Provides assisted reconciliation
 */
class AssistedReconciliation
{
	const COLUMNS = [
		'label'          => 'Libellé',
		'date'           => 'Date',
		//'notes'          => 'Remarques',
		//'reference'      => 'Numéro pièce comptable',
		//'p_reference'    => 'Référence paiement',
		'amount'         => 'Montant',
		'debit'          => 'Débit',
		'credit'         => 'Crédit',
		'balance'        => 'Solde',
	];

	protected $csv;

	public function __construct()
	{
		$this->csv = new CSV_Custom(Session::getInstance(), 'acc_reconcile_csv');
		$this->csv->setColumns(self::COLUMNS);
		$this->csv->setMandatoryColumns(['label', 'date']);
		$this->csv->setModifier(function (\stdClass $line): \stdClass {
			$date = \DateTime::createFromFormat('!d/m/Y', $line->date);

			if (!$date) {
				throw new UserException(sprintf('Date invalide : %s (format attendu : JJ/MM/AAAA)', $line->date));
			}

			$line->date = $date;

			static $has_amount = null;

			if (null === $has_amount) {
				$has_amount = in_array('amount', $this->csv->getTranslationTable());
			}

			if (!$has_amount) {
				$line->amount = $line->credit ?: '-' . ltrim($line->debit, '- \t\r\n');
			}

			$line->amount = (substr($line->amount, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->amount);

			if (!empty($line->balance)) {
				$line->balance = (substr($line->balance, 0, 1) == '-' ? -1 : 1) * Utils::moneyToInteger($line->balance);
			}

			$line->new_params = http_build_query([
				'a' => abs($line->amount)/100,
				'l' => $line->label,
				'd' => $date->format('Y-m-d'),
				't' => $line->amount < 0 ? Transaction::TYPE_EXPENSE : Transaction::TYPE_REVENUE,
			]);

			return $line;
		});
	}

	public function csv(): CSV_Custom
	{
		return $this->csv;
	}

	public function setSettings(array $translation_table, int $skip): void
	{
		$this->csv->setTranslationTable($translation_table);

		if (!((in_array('credit', $translation_table) && in_array('debit', $translation_table)) || in_array('amount', $translation_table))) {
			throw new UserException('Il est nécessaire de sélectionner une colonne "montant" ou deux colonnes "débit" et "crédit"');
		}

		$this->csv->skip($skip);
	}

	public function getStartAndEndDates(): ?array
	{
		$start = $end = null;

		if (!$this->csv->ready()) {
			return compact('start', 'end');
		}

		foreach ($this->csv->iterate() as $line) {
			if (null === $start || $line->date < $start) {
				$start = $line->date;
			}

			if (null === $end || $line->date > $end) {
				$end = $line->date;
			}
		}

		return compact('start', 'end');
	}

	public function mergeJournal(\Generator $journal)
	{
		$lines = [];

		$csv = iterator_to_array($this->csv->iterate());
		$journal = iterator_to_array($journal);
		$i = 0;
		$sum = 0;

		foreach ($journal as $j) {
			$id = $j->date->format('Ymd') . '.' . $i++;

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

		ksort($lines);
		$prev = null;

		foreach ($lines as &$line) {
			$line->add = false;

			if (isset($line->csv)) {
				$sum += $line->csv->amount;
				$line->csv->running_sum = $sum;

				if ($prev && ($prev->date->format('Ymd') != $line->csv->date->format('Ymd') || $prev->label != $line->csv->label)) {
					$prev = null;
				}
			}

			if (isset($line->csv) && isset($line->journal)) {
				$prev = null;
			}

			if (isset($line->csv) && !isset($line->journal) && !$prev) {
				$line->add = true;
				$prev = $line->csv;
			}
		}

		return $lines;
	}
}

Modified src/include/lib/Garradin/Accounting/Reports.php from [73e1a05e76] to [4754478645].

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
			INNER JOIN acc_years y ON y.id = t.id_year
			GROUP BY %s
			ORDER BY %s;';

		$order = $order_code ? 'a.code COLLATE U_NOCASE' : 'a.label COLLATE U_NOCASE';

		if ($by_year) {
			$group = 'y.id, a.id';
			$order = 'y.start_date DESC, ' . $order;
		}
		else {
			$group = 'a.id, y.id';
			$order = $order . ', y.id';
		}

		$sql = sprintf($sql, Account::EXPENSE, Account::REVENUE, $group, $order);

		$current = null;








|



|







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
			INNER JOIN acc_years y ON y.id = t.id_year
			GROUP BY %s
			ORDER BY %s;';

		$order = $order_code ? 'a.code COLLATE U_NOCASE' : 'a.label COLLATE U_NOCASE';

		if ($by_year) {
			$group = 'y.id, a.code';
			$order = 'y.start_date DESC, ' . $order;
		}
		else {
			$group = 'a.code, y.id';
			$order = $order . ', y.id';
		}

		$sql = sprintf($sql, Account::EXPENSE, Account::REVENUE, $group, $order);

		$current = null;

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
				$out->{$s} = $current->{$s};
			}

			return $out;
		};

		foreach (DB::getInstance()->iterate($sql) as $row) {
			$id = $by_year ? $row->id_year : $row->id_account;

			if (null !== $current && $current->id !== $id) {
				$current->items[] = $total($current, $by_year);

				yield $current;
				$current = null;
			}

			if (null === $current) {
				$current = (object) [

					'id' => $by_year ? $row->id_year : $row->id_account,
					'label' => $by_year ? $row->year_label : ($order_code ? $row->account_code . ' - ' : '') . $row->account_label,
					'description' => !$by_year ? $row->account_description : null,
					'items' => []
				];

				foreach ($sums as $s) {







|

|








>







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
				$out->{$s} = $current->{$s};
			}

			return $out;
		};

		foreach (DB::getInstance()->iterate($sql) as $row) {
			$id = $by_year ? $row->id_year : $row->account_code;

			if (null !== $current && $current->selector !== $id) {
				$current->items[] = $total($current, $by_year);

				yield $current;
				$current = null;
			}

			if (null === $current) {
				$current = (object) [
					'selector' => $id,
					'id' => $by_year ? $row->id_year : $row->id_account,
					'label' => $by_year ? $row->year_label : ($order_code ? $row->account_code . ' - ' : '') . $row->account_label,
					'description' => !$by_year ? $row->account_description : null,
					'items' => []
				];

				foreach ($sums as $s) {
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
		$balances = DB::getInstance()->getAssoc($sql);

		return ($balances[Account::REVENUE] ?? 0) - ($balances[Account::EXPENSE] ?? 0);
	}

	static public function getBalancesSQL(array $parts = [])
	{
		return sprintf('SELECT %s id_year, id, label, code, type, debit, credit,




				CASE -- 3 = dynamic asset or liability depending on balance
					WHEN position = 3 AND (debit - credit) > 0 THEN 1 -- 1 = Asset (actif) comptes fournisseurs, tiers créditeurs
					WHEN position = 3 THEN 2 -- 2 = Liability (passif), comptes clients, tiers débiteurs
					ELSE position
				END AS position,
				CASE
					WHEN position IN (1, 4) -- 1 = asset, 4 = expense
						OR (position = 3 AND (debit - credit) > 0)
					THEN
						debit - credit
					ELSE
						credit - debit
				END AS balance,
				CASE WHEN debit - credit > 0 THEN 1 ELSE 0 END AS is_debt
			FROM (
				SELECT %s t.id_year, a.id, a.label, a.code, a.position, a.type,
					SUM(l.credit) AS credit,
					SUM(l.debit) AS debit
				FROM acc_transactions_lines l
				INNER JOIN acc_transactions t ON t.id = l.id_transaction
				INNER JOIN acc_accounts a ON a.id = l.id_account
				%s
				%s
				GROUP BY %s
			)

			%s
			ORDER BY %s',
			isset($parts['select']) ? $parts['select'] . ',' : '',
			isset($parts['inner_select']) ? $parts['inner_select'] . ',' : '',
			$parts['inner_join'] ?? '',
			isset($parts['inner_where']) ? 'WHERE ' . $parts['inner_where'] : '',
			$parts['inner_group'] ?? 'a.id, t.id_year',

			isset($parts['group']) ? 'GROUP BY ' . $parts['group'] : '',
			$order ?? 'code'
		);
	}

	/**
	 * Returns accounts balances according to $criterias







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







>







>







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
		$balances = DB::getInstance()->getAssoc($sql);

		return ($balances[Account::REVENUE] ?? 0) - ($balances[Account::EXPENSE] ?? 0);
	}

	static public function getBalancesSQL(array $parts = [])
	{
		return sprintf('SELECT %s id_year, id, label, code, type, debit, credit, position, balance, is_debt
			FROM (
				SELECT %s t.id_year, a.id, a.label, a.code, a.type,
					SUM(l.credit) AS credit,
					SUM(l.debit) AS debit,
					CASE -- 3 = dynamic asset or liability depending on balance
						WHEN position = 3 AND SUM(l.debit - l.credit) > 0 THEN 1 -- 1 = Asset (actif) comptes fournisseurs, tiers créditeurs
						WHEN position = 3 THEN 2 -- 2 = Liability (passif), comptes clients, tiers débiteurs
						ELSE position
					END AS position,
					CASE
						WHEN position IN (1, 4) -- 1 = asset, 4 = expense
							OR (position = 3 AND SUM(l.debit - l.credit) > 0)
						THEN
							SUM(l.debit - l.credit)
						ELSE
							SUM(l.credit - l.debit)
					END AS balance,
					CASE WHEN SUM(l.debit - l.credit) > 0 THEN 1 ELSE 0 END AS is_debt




				FROM acc_transactions_lines l
				INNER JOIN acc_transactions t ON t.id = l.id_transaction
				INNER JOIN acc_accounts a ON a.id = l.id_account
				%s
				%s
				GROUP BY %s
			)
			%s
			%s
			ORDER BY %s',
			isset($parts['select']) ? $parts['select'] . ',' : '',
			isset($parts['inner_select']) ? $parts['inner_select'] . ',' : '',
			$parts['inner_join'] ?? '',
			isset($parts['inner_where']) ? 'WHERE ' . $parts['inner_where'] : '',
			$parts['inner_group'] ?? 'a.id, t.id_year',
			isset($parts['where']) ? 'WHERE ' . $parts['where'] : '',
			isset($parts['group']) ? 'GROUP BY ' . $parts['group'] : '',
			$order ?? 'code'
		);
	}

	/**
	 * Returns accounts balances according to $criterias
319
320
321
322
323
324
325













326
327

328
329
330
331
332
333
334
335
336
		if (empty($criterias['analytical']) && empty($criterias['user']) && empty($criterias['creator']) && empty($criterias['subscription'])) {
			$table = 'acc_accounts_balances';
		}

		// Specific queries that can't rely on acc_accounts_balances
		if (!$table)
		{













			$where = self::getWhereClause($criterias, 't', 'l', 'a');
			$remove_zero = $remove_zero ? ', ' . $remove_zero : '';


			$sql = self::getBalancesSQL(['group' => 'code ' . $having, 'order' => $order, 'inner_where' => $where]);
		}
		else {
			$where = self::getWhereClause($criterias);

			$query = 'SELECT *, SUM(credit) AS credit, SUM(debit) AS debit, SUM(balance) AS balance FROM %s
				WHERE %s
				GROUP BY %s %s







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

>

|







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
		if (empty($criterias['analytical']) && empty($criterias['user']) && empty($criterias['creator']) && empty($criterias['subscription'])) {
			$table = 'acc_accounts_balances';
		}

		// Specific queries that can't rely on acc_accounts_balances
		if (!$table)
		{
			$where = null;

			// The position
			if (!empty($criterias['position'])) {
				$criterias['position'] = (array)$criterias['position'];

				if (in_array(Account::LIABILITY, $criterias['position'])
					|| in_array(Account::ASSET, $criterias['position'])) {
					$where = self::getWhereClause(['position' => $criterias['position']]);
					$criterias['position'][] = Account::ASSET_OR_LIABILITY;
				}
			}

			$inner_where = self::getWhereClause($criterias, 't', 'l', 'a');
			$remove_zero = $remove_zero ? ', ' . $remove_zero : '';
			$inner_group = empty($criterias['year']) ? 'a.id' : null;

			$sql = self::getBalancesSQL(['group' => 'code ' . $having] + compact('order', 'inner_where', 'where', 'inner_group'));
		}
		else {
			$where = self::getWhereClause($criterias);

			$query = 'SELECT *, SUM(credit) AS credit, SUM(debit) AS debit, SUM(balance) AS balance FROM %s
				WHERE %s
				GROUP BY %s %s

Modified src/include/lib/Garradin/Accounting/Transactions.php from [da53e119ac] to [d402e5412a].

35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
		'date'      => 'Date',
		'notes'     => 'Remarques',
		'reference' => 'Numéro pièce comptable',

		// Lines
		'line_id'        => 'Numéro ligne',
		'account'        => 'Compte',
		'credit'         => 'Crédit',
		'debit'          => 'Débit',

		'line_reference' => 'Référence ligne',
		'line_label'     => 'Libellé ligne',
		'reconciled'     => 'Rapprochement',
		'analytical'     => 'Compte analytique',
		'linked_users'   => 'Membres associés',
	];








<

>







35
36
37
38
39
40
41

42
43
44
45
46
47
48
49
50
		'date'      => 'Date',
		'notes'     => 'Remarques',
		'reference' => 'Numéro pièce comptable',

		// Lines
		'line_id'        => 'Numéro ligne',
		'account'        => 'Compte',

		'debit'          => 'Débit',
		'credit'         => 'Crédit',
		'line_reference' => 'Référence ligne',
		'line_label'     => 'Libellé ligne',
		'reconciled'     => 'Rapprochement',
		'analytical'     => 'Compte analytique',
		'linked_users'   => 'Membres associés',
	];

130
131
132
133
134
135
136

137
138
139
140
141
142
143
				}

				$ids[] = (int)$row->id;

				$line = new Line;
				$line->importForm([
					'reference'  => $row->line_reference,

					'id_account' => $row->id_account,
				]);

				$line->credit = $row->debit;

				$transaction->addLine($line);
			}







>







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

				$ids[] = (int)$row->id;

				$line = new Line;
				$line->importForm([
					'reference'  => $row->line_reference,
					'label'      => $row->line_label,
					'id_account' => $row->id_account,
				]);

				$line->credit = $row->debit;

				$transaction->addLine($line);
			}
366
367
368
369
370
371
372














373
374
375
376
377
378
379
						throw new UserException(sprintf('le type "%s" est inconnu', $row->type));
					}

					$transaction->type = $types[$row->type];
					$fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference']));

					$transaction->importForm($fields);














				}

				$data = [];

				if (!empty($row->analytical)) {
					$id_analytical = $accounts->getIdFromCode($row->analytical);








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







367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
						throw new UserException(sprintf('le type "%s" est inconnu', $row->type));
					}

					$transaction->type = $types[$row->type];
					$fields = array_intersect_key((array)$row, array_flip(['label', 'date', 'notes', 'reference']));

					$transaction->importForm($fields);

					// Set status
					if (!empty($row->status)) {
						$status_list = array_map('trim', explode(',', $row->status));
						$status = 0;

						foreach (Transaction::STATUS_NAMES as $k => $v) {
							if (in_array($v, $status_list)) {
								$status |= $k;
							}
						}

						$transaction->status = $status;
					}
				}

				$data = [];

				if (!empty($row->analytical)) {
					$id_analytical = $accounts->getIdFromCode($row->analytical);

Modified src/include/lib/Garradin/CSV_Custom.php from [1994e729ee] to [329155a591].

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

15

16
17
18
19
20
21
22
<?php

namespace Garradin;

use KD2\UserSession;

class CSV_Custom
{
	protected $session;
	protected $key;
	protected $csv;
	protected $translation;
	protected $columns;
	protected $mandatory_columns = [];

	protected $skip = 1;


	public function __construct(UserSession $session, string $key)
	{
		$this->session = $session;
		$this->key = $key;
		$this->csv = $this->session->get($this->key);
		$this->translation = $this->session->get($this->key . '_translation') ?: [];













|
>
|
>







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

namespace Garradin;

use KD2\UserSession;

class CSV_Custom
{
	protected $session;
	protected $key;
	protected $csv;
	protected $translation;
	protected $columns;
	protected array $mandatory_columns = [];
	protected int $skip = 1;
	protected $modifier = null;
	protected array $_default;

	public function __construct(UserSession $session, string $key)
	{
		$this->session = $session;
		$this->key = $key;
		$this->csv = $this->session->get($this->key);
		$this->translation = $this->session->get($this->key . '_translation') ?: [];
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
			throw new \LogicException('No file has been loaded');
		}

		if (!$this->columns || !$this->translation) {
			throw new \LogicException('Missing columns or translation table');
		}


		$default = array_map(function ($a) { return null; }, array_flip($this->translation));


		$i = 0;



		foreach ($this->csv as $k => $line) {


			if ($i++ < $this->skip) {


				continue;


			}

			$row = $default;

			foreach ($line as $col => $value) {
				if (!isset($this->translation[$col])) {
					continue;
				}

				$row[$this->translation[$col]] = trim($value);
			}

			$row = (object) $row;
			yield $k => $row;



		}






	}

	public function getFirstLine(): array
	{
		if (empty($this->csv)) {
			throw new \LogicException('No file has been loaded');
		}

		return reset($this->csv);





	}

	public function getSelectedTable(?array $source = null): array
	{
		if (null === $source && isset($_POST['translation_table'])) {
			$source = $_POST['translation_table'];
		}







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

|

|
|
|
|

|
|

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




|



|
>
>
>
>
>







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
			throw new \LogicException('No file has been loaded');
		}

		if (!$this->columns || !$this->translation) {
			throw new \LogicException('Missing columns or translation table');
		}

		for ($i = 0; $i < count($this->csv); $i++) {
			if ($i <= $this->skip) {
				continue;
			}

			yield $i => $this->getLine($i);
		}
	}

	public function getLine(int $i): ?\stdClass
	{
		if (!isset($this->csv[$i])) {
			return null;
		}

		if (!isset($this->_default)) {
			$this->_default = array_map(function ($a) { return null; }, array_flip($this->translation));
		}

		$row = $this->_default;

		foreach ($this->csv[$i] as $col => $value) {
			if (!isset($this->translation[$col])) {
				continue;
			}

			$row[$this->translation[$col]] = trim($value);
		}

		$row = (object) $row;

		if (null !== $this->modifier) {
			try {
				$row = call_user_func($this->modifier, $row);
			}
			catch (UserException $e) {
				throw new UserException(sprintf('Ligne %d : %s', $i, $e->getMessage()));
			}
		}

		return $row;
	}

	public function getFirstLine(): array
	{
		if (!$this->loaded()) {
			throw new \LogicException('No file has been loaded');
		}

		return current($this->csv);
	}

	public function setModifier(callable $callback): void
	{
		$this->modifier = $callback;
	}

	public function getSelectedTable(?array $source = null): array
	{
		if (null === $source && isset($_POST['translation_table'])) {
			$source = $_POST['translation_table'];
		}

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

328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
		}

		if (!$only_non_reconciled) {
			yield (object) ['sum' => $sum, 'reconciled_sum' => $reconciled_sum, 'date' => $end_date];
		}
	}

	public function mergeReconcileJournalAndCSV(\Generator $journal, CSV_Custom $csv)
	{
		$lines = [];

		$csv = iterator_to_array($csv->iterate());
		$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;
			}
			catch (UserException $e) {
				throw new UserException(sprintf('Ligne %d : %s', $k, $e->getMessage()));
			}
		}
		unset($line);

		foreach ($journal as $j) {
			$id = $j->date->format('Ymd') . '.' . $i++;

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

		ksort($lines);
		$prev = null;

		foreach ($lines as &$line) {
			$line->add = false;

			if (isset($line->csv)) {
				$sum += $line->csv->amount;
				$line->csv->running_sum = $sum;

				if ($prev && ($prev->date->format('Ymd') != $line->csv->date->format('Ymd') || $prev->label != $line->csv->label)) {
					$prev = null;
				}
			}

			if (isset($line->csv) && isset($line->journal)) {
				$prev = null;
			}

			if (isset($line->csv) && !isset($line->journal) && !$prev) {
				$line->add = true;
				$prev = $line->csv;
			}
		}

		return $lines;
	}

	public function getDepositJournal(int $year_id, array $checked = []): \Generator
	{
		$res = DB::getInstance()->iterate('SELECT l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, l.reconciled, l.id AS id_line, l.id_account
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			WHERE t.id_year = ? AND l.id_account = ? AND l.credit = 0 AND NOT (t.status & ?)
			ORDER BY t.date, t.id;',







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







328
329
330
331
332
333
334



















































































































335
336
337
338
339
340
341
		}

		if (!$only_non_reconciled) {
			yield (object) ['sum' => $sum, 'reconciled_sum' => $reconciled_sum, 'date' => $end_date];
		}
	}




















































































































	public function getDepositJournal(int $year_id, array $checked = []): \Generator
	{
		$res = DB::getInstance()->iterate('SELECT l.debit, l.credit, t.id, t.date, t.reference, l.reference AS line_reference, t.label, l.label AS line_label, l.reconciled, l.id AS id_line, l.id_account
			FROM acc_transactions_lines l
			INNER JOIN acc_transactions t ON t.id = l.id_transaction
			WHERE t.id_year = ? AND l.id_account = ? AND l.credit = 0 AND NOT (t.status & ?)
			ORDER BY t.date, t.id;',

Modified src/include/lib/Garradin/Entities/Accounting/Transaction.php from [da7db0f220] to [f2a97d2dca].

295
296
297
298
299
300
301
302
303
304
305
306
307
308
309

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

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







|







295
296
297
298
299
300
301
302
303
304
305
306
307
308
309

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

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


351
352
353
354
355
356
357
			$new->addLine($line);
		}

		// Only set date if valid
		if ($this->date >= $year->start_date && $this->date <= $year->end_date) {
			$new->date = clone $this->date;
		}



		return $new;
	}

	public function payment_reference(): ?string
	{
		$line = current($this->getLines());







>
>







344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
			$new->addLine($line);
		}

		// Only set date if valid
		if ($this->date >= $year->start_date && $this->date <= $year->end_date) {
			$new->date = clone $this->date;
		}

		$new->status = 0;

		return $new;
	}

	public function payment_reference(): ?string
	{
		$line = current($this->getLines());
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
		}

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

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

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

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







|


|







697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
		}

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

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

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

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

Modified src/include/lib/Garradin/Membres.php from [38f3fd4508] to [bedba4d423].

120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
                            }
                        }

                        $data[$key] = $binary;
                    }
                    elseif (!is_numeric($data[$key]) || $data[$key] < 0 || $data[$key] > PHP_INT_MAX)
                    {
                        throw new UserException('Le champs "%s" ne contient pas une valeur binaire.');
                    }
                }

                // Un champ texte vide c'est un champ NULL
                if (is_string($data[$key]) && trim($data[$key]) === '')
                {
                    $data[$key] = null;







|







120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
                            }
                        }

                        $data[$key] = $binary;
                    }
                    elseif (!is_numeric($data[$key]) || $data[$key] < 0 || $data[$key] > PHP_INT_MAX)
                    {
                        throw new UserException(sprintf('Le champs "%s" ne contient pas une valeur binaire.', $key));
                    }
                }

                // Un champ texte vide c'est un champ NULL
                if (is_string($data[$key]) && trim($data[$key]) === '')
                {
                    $data[$key] = null;

Modified src/templates/acc/accounts/reconcile_assist.tpl from [d7fc6d0db8] to [283d27b0f7].

13
14
15
16
17
18
19
20
21
22
23




24
25
26
27
28
29
30

<form method="post" action="{$self_url}" enctype="multipart/form-data">
	{if !$csv->loaded()}
		<fieldset>
			<legend>Relevé de compte</legend>
			<p class="help block">
				Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br />
				<a href="https://garradin.eu/Rapprochement_assiste" target="_blank">Aide détaillée</a>
			</p>
			<dl>
				{include file="common/_csv_help.tpl"}




				{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			</dl>
			<p class="submit">
				{csrf_field key=$csrf_key}
				{button type="submit" name="upload" label="Envoyer le fichier" class="main" shape="upload"}
			</p>
		</fieldset>







|



>
>
>
>







13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

<form method="post" action="{$self_url}" enctype="multipart/form-data">
	{if !$csv->loaded()}
		<fieldset>
			<legend>Relevé de compte</legend>
			<p class="help block">
				Le rapprochement assisté permet de s'aider d'un relevé de compte au format CSV pour trouver les écritures manquantes ou erronées.<br />
				<a href="https://garradin.eu/rapprochement_assiste" target="_blank">Aide détaillée</a>
			</p>
			<dl>
				{include file="common/_csv_help.tpl"}
				<dd class="help">
					Le fichier doit également disposer soit d'une colonne <strong>Montant</strong>, soit de deux colonnes <strong>Débit</strong>
					et <strong>Crédit</strong>.
				</dd>
				{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}
			</dl>
			<p class="submit">
				{csrf_field key=$csrf_key}
				{button type="submit" name="upload" label="Envoyer le fichier" class="main" shape="upload"}
			</p>
		</fieldset>
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
								{$line.journal.debit|raw|money}
							{/if}
						</td>
						<td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|money:false}</td>
						<th style="text-align: right">{$line.journal.label}</th>
					{else}
						<td colspan="5"></td>
						<td style="text-align: right">
							{if $line.add}
							{* FIXME later add ability to pre-fill multi-line transactions in new.php
								{linkbutton label="Créer cette écriture" target="_blank" href="%s&create=%s"|args:$self_url,$line_id shape="plus"}
							*}
							{/if}
						</td>
					{/if}
						<td class="separator">
						{if $line->journal && $line->csv}
							==
						{else}
							<b class="icn">⚠</b>
						{/if}
						</td>
					{if isset($line->csv)}
						<th class="separator">{$line.csv.label}</th>
						<td class="money">
							{$line.csv.amount|raw|money}
						</td>
						<td class="money">{$line.csv.running_sum|raw|money}</td>
						<td>{$line.csv.date|date_short}</td>
					{else}
						<td colspan="4" class="separator"></td>
					{/if}
				</tr>
				{/if}
			{/foreach}







|

<
|
<















|







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
								{$line.journal.debit|raw|money}
							{/if}
						</td>
						<td class="money">{if $line.journal.running_sum > 0}-{/if}{$line.journal.running_sum|abs|raw|money:false}</td>
						<th style="text-align: right">{$line.journal.label}</th>
					{else}
						<td colspan="5"></td>
						<td class="actions">
							{if $line.add}

								{linkbutton label="Saisir cette écriture" target="_dialog" href="!acc/transactions/new.php?%s"|args:$line.csv.new_params shape="plus"}

							{/if}
						</td>
					{/if}
						<td class="separator">
						{if $line->journal && $line->csv}
							==
						{else}
							<b class="icn">⚠</b>
						{/if}
						</td>
					{if isset($line->csv)}
						<th class="separator">{$line.csv.label}</th>
						<td class="money">
							{$line.csv.amount|raw|money}
						</td>
						<td class="money">{if $line.csv.balance}{$line.csv.balance|raw|money}{else}{$line.csv.running_sum|raw|money}{/if}</td>
						<td>{$line.csv.date|date_short}</td>
					{else}
						<td colspan="4" class="separator"></td>
					{/if}
				</tr>
				{/if}
			{/foreach}

Modified src/templates/acc/transactions/details.tpl from [3757af2106] to [424b045f3a].

57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
	<dd><a class="num" href="?id={$transaction.id_related}">#{$transaction.id_related}</a>
		{if $transaction.type == $transaction::TYPE_DEBT || $transaction.type == $transaction::TYPE_CREDIT}(en règlement de){/if}
	</dd>
	{/if}
	{if count($related_transactions)}
	<dt>Écritures liées</dt>
	{foreach from=$related_transactions item="related"}
		<dd><a href="?id={$related.id}" class="num">#{$related.id}</a>
		du {$related.date|date_short}
		</dd>
	{/foreach}
	{/if}
	<dt>Date</dt>
	<dd>{$transaction.date|date:'l j F Y (d/m/Y)'}</dd>
	<dt>Numéro pièce comptable</dt>
	<dd>{if $transaction.reference}{$transaction.reference}{else}-{/if}</dd>








|
<
<







57
58
59
60
61
62
63
64


65
66
67
68
69
70
71
	<dd><a class="num" href="?id={$transaction.id_related}">#{$transaction.id_related}</a>
		{if $transaction.type == $transaction::TYPE_DEBT || $transaction.type == $transaction::TYPE_CREDIT}(en règlement de){/if}
	</dd>
	{/if}
	{if count($related_transactions)}
	<dt>Écritures liées</dt>
	{foreach from=$related_transactions item="related"}
		<dd><a href="?id={$related.id}" class="num">#{$related.id}</a> — {$related.label} — {$related.date|date_short}</dd>


	{/foreach}
	{/if}
	<dt>Date</dt>
	<dd>{$transaction.date|date:'l j F Y (d/m/Y)'}</dd>
	<dt>Numéro pièce comptable</dt>
	<dd>{if $transaction.reference}{$transaction.reference}{else}-{/if}</dd>

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<table class="list">
	<thead>
		<tr>
			<td class="num">N° compte</td>
			<th>Compte</th>
			<td class="money">Débit</td>
			<td class="money">Crédit</td>
			<td>Libellé</td>
			<td>Référence</td>
			<td>Projet</td>
		</tr>
	</thead>
	<tbody>
		{foreach from=$transaction->getLinesWithAccounts(false) item="line"}
		<tr>
			<td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$line.id_account}&amp;year={$transaction.id_year}">{$line.account_code}</a></td>







|
|







108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<table class="list">
	<thead>
		<tr>
			<td class="num">N° compte</td>
			<th>Compte</th>
			<td class="money">Débit</td>
			<td class="money">Crédit</td>
			<td>Libellé ligne</td>
			<td>Référence ligne</td>
			<td>Projet</td>
		</tr>
	</thead>
	<tbody>
		{foreach from=$transaction->getLinesWithAccounts(false) item="line"}
		<tr>
			<td class="num"><a href="{$admin_url}acc/accounts/journal.php?id={$line.id_account}&amp;year={$transaction.id_year}">{$line.account_code}</a></td>

Modified src/templates/acc/transactions/new.tpl from [0187576c59] to [5d2788881b].

66
67
68
69
70
71
72



73
74
75
76
77
78
79
		<dl data-types="t{$transaction::TYPE_REVENUE} t{$transaction::TYPE_EXPENSE} t{$transaction::TYPE_TRANSFER}">
			{input type="text" name="payment_reference" label="Référence de paiement" help="Numéro de chèque, numéro de transaction CB, etc." default=$transaction->payment_reference()}
		</dl>
		<dl>
			{input type="list" multiple=true name="users" label="Membres associés" target="!membres/selector.php"}
			{input type="textarea" name="notes" label="Remarques" rows=4 cols=30}
		</dl>



		<dl data-types="all-but-advanced">
			{if count($analytical_accounts) > 1}
				{input type="select" name="id_analytical" label="Projet (compte analytique)" options=$analytical_accounts default=$id_analytical}
			{/if}
		</dl>
	</fieldset>








>
>
>







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

Modified src/templates/admin/config/backup/restore.tpl from [0ec7bed822] to [03715cceb0].

76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
						<td>Date</td>
						<td>Version</td>
						<td></td>
					</tr>
				</thead>
			{foreach from=$list item="backup"}
				<tr>
					<td class="check">{input type="radio" name="selected" value=$backup.filename}</td>
					<th><label for="f_selected_{$backup.filename}">{$backup.name}</label></th>
					<td>{$backup.size|size_in_bytes}</td>
					<td>{$backup.date|date_short:true}</td>
					<td>{$backup.version}{if !$backup.can_restore} — <span class="alert">Version trop ancienne pour pouvoir être restaurée</span>{/if}</td>
					<td class="actions">
						{linkbutton href="?download=%s"|args:$backup.filename label="Télécharger" shape="download"}
					</td>







|







76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
						<td>Date</td>
						<td>Version</td>
						<td></td>
					</tr>
				</thead>
			{foreach from=$list item="backup"}
				<tr>
					<td class="check">{if $backup.can_restore}{input type="radio" name="selected" value=$backup.filename}{/if}</td>
					<th><label for="f_selected_{$backup.filename}">{$backup.name}</label></th>
					<td>{$backup.size|size_in_bytes}</td>
					<td>{$backup.date|date_short:true}</td>
					<td>{$backup.version}{if !$backup.can_restore} — <span class="alert">Version trop ancienne pour pouvoir être restaurée</span>{/if}</td>
					<td class="actions">
						{linkbutton href="?download=%s"|args:$backup.filename label="Télécharger" shape="download"}
					</td>

Modified src/www/admin/acc/accounts/reconcile_assist.php from [b1a074fbc8] to [b465b25a48].

1
2
3
4
5

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

104
105
106
107
108
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Transactions;


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

$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$account = Accounts::get((int)qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$csrf_key = 'acc_reconcile_assist_' . $account->id();
$csv = new CSV_Custom($session, 'acc_reconcile_csv');

$csv->setColumns([
	'label'          => 'Libellé',
	'date'           => 'Date',
	'notes'          => 'Remarques',
	'reference'      => 'Numéro pièce comptable',
	'p_reference'    => 'Référence paiement',
	'amount'         => 'Montant',
]);

$csv->setMandatoryColumns(['label', 'date', 'amount']);

$form->runIf('cancel', function () use ($csv) {
	$csv->clear();
}, $csrf_key, Utils::getSelfURI());

$form->runIf(f('upload') && isset($_FILES['file']['name']), function () use ($csv) {
	$csv->load($_FILES['file']);
}, $csrf_key, Utils::getSelfURI());

$form->runIf('assign', function () use ($csv) {
	$csv->setTranslationTable(f('translation_table'));
	$csv->skip((int)f('skip_first_line'));
}, $csrf_key, Utils::getSelfURI());

$start = null;
$end = null;
$journal = null;

if ($csv->ready()) {
	foreach ($csv->iterate() as $line => $row) {
		$date = \DateTime::createFromFormat('!d/m/Y', $row->date);
		if (!$date) {
			$form->addError(sprintf('Ligne %d : format de date invalide (%s)', $line, $row->date));
			continue;
		}

		if ($date < $start) {
			$start = $date;
		}

		if ($date > $end) {
			$end = $date;
		}
	}

	if ($start < $current_year->start_date || $start > $current_year->end_date) {
		$start = clone $current_year->start_date;
	}

	if ($end < $current_year->start_date || $end > $current_year->end_date) {
		$end = clone $current_year->end_date;
	}
}

if ($start && $end) {
	$journal = $account->getReconcileJournal(CURRENT_YEAR_ID, $start, $end);
}

// Enregistrement des cases cochées
$form->runIf('save', function () use ($journal, $csv) {
	Transactions::saveReconciled($journal, f('reconcile'));
	$csv->clear();
}, $csrf_key, Utils::getSelfURI());

$lines = null;

if ($journal && $csv->ready()) {
	try {
		$lines = $account->mergeReconcileJournalAndCSV($journal, $csv);
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact(
	'account',
	'start',
	'end',
	'lines',

	'csv',
	'csrf_key'
));

$tpl->display('acc/accounts/reconcile_assist.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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
namespace Garradin;

use Garradin\Accounting\Accounts;
use Garradin\Accounting\Transactions;
use Garradin\Accounting\AssistedReconciliation;

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

$session->requireAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN);

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

$account = Accounts::get((int)qg('id'));

if (!$account) {
	throw new UserException("Le compte demandé n'existe pas.");
}

$csrf_key = 'acc_reconcile_assist_' . $account->id();










$ar = new AssistedReconciliation;
$csv = $ar->csv();

$form->runIf('cancel', function () use ($csv) {
	$csv->clear();
}, $csrf_key, Utils::getSelfURI());

$form->runIf(f('upload') && isset($_FILES['file']['name']), function () use ($csv) {
	$csv->load($_FILES['file']);
}, $csrf_key, Utils::getSelfURI());

$form->runIf('assign', function () use ($ar) {

	$ar->setSettings(f('translation_table'), (int)f('skip_first_line'));
}, $csrf_key, Utils::getSelfURI());




extract($ar->getStartAndEndDates());







$journal = null;



if ($start && $end) {





	if ($start < $current_year->start_date || $start > $current_year->end_date) {
		$start = clone $current_year->start_date;
	}

	if ($end < $current_year->start_date || $end > $current_year->end_date) {
		$end = clone $current_year->end_date;
	}



	$journal = $account->getReconcileJournal(CURRENT_YEAR_ID, $start, $end);
}

// Enregistrement des cases cochées
$form->runIf('save', function () use ($journal, $csv) {
	Transactions::saveReconciled($journal, f('reconcile'));
	$csv->clear();
}, $csrf_key, Utils::getSelfURI());

$lines = null;

if ($journal && $csv->ready()) {
	try {
		$lines = $ar->mergeJournal($journal);
	}
	catch (UserException $e) {
		$form->addError($e->getMessage());
	}
}

$tpl->assign(compact(
	'account',
	'start',
	'end',
	'lines',
	'ar',
	'csv',
	'csrf_key'
));

$tpl->display('acc/accounts/reconcile_assist.tpl');

Modified src/www/admin/acc/transactions/new.php from [be9d05a5cb] to [4b7865c3e6].

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

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Files\File;

use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

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

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

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

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

$transaction = new Transaction;
$lines = [[], []];
$amount = 0;
$types_accounts = null;
$id_analytical = null;


















// Duplicate transaction
if (qg('copy')) {
	$old = Transactions::get((int)qg('copy'));

	if (!$old) {
		throw new UserException('Cette écriture n\'existe pas (ou plus).');






>



















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







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

use Garradin\Entities\Accounting\Account;
use Garradin\Entities\Accounting\Transaction;
use Garradin\Entities\Files\File;
use Garradin\Accounting\AssistedReconciliation;
use Garradin\Accounting\Transactions;
use Garradin\Accounting\Years;

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

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

if (!CURRENT_YEAR_ID) {
	Utils::redirect(ADMIN_URL . 'acc/years/?msg=OPEN');
}

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

$transaction = new Transaction;
$lines = [[], []];
$amount = 0;
$types_accounts = null;
$id_analytical = null;

// Quick-fill transaction from query parameters
if (qg('a')) {
	$amount = Utils::moneyToInteger(qg('a'));
}

if (qg('l')) {
	$transaction->label = qg('l');
}

if (qg('d')) {
	$transaction->date = new \DateTime(qg('d'));
}

if (qg('t')) {
	$transaction->type = (int) qg('t');
}

// Duplicate transaction
if (qg('copy')) {
	$old = Transactions::get((int)qg('copy'));

	if (!$old) {
		throw new UserException('Cette écriture n\'existe pas (ou plus).');