Overview
Comment:Accounting import of transactions: refactor code to have less duplicate code, also change formats to have simple, full and grouped exports, and allow import of both simple and grouped exports
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk | stable
Files: files | file ages | folders
SHA3-256: 3d03dcd348864e13f8525ae29ac0e67f26d6e4ef3b2d8399e7b978a47cce4d1e
User & Date: bohwaz on 2022-02-18 00:53:55
Other Links: manifest | tags
Context
2022-02-18
03:24
Remove option to delete users from services and fees check-in: adfd9ac629 user: bohwaz tags: trunk, stable
00:53
Accounting import of transactions: refactor code to have less duplicate code, also change formats to have simple, full and grouped exports, and allow import of both simple and grouped exports check-in: 3d03dcd348 user: bohwaz tags: trunk, stable
2022-02-17
17:54
Fix for PHP 8.1: detect_auto_line_endings is deprecated check-in: 358c98d7b8 user: bohwaz tags: trunk, stable
Changes

Modified src/include/lib/Garradin/Accounting/Transactions.php from [ac00c7ad5c] to [607299f698].

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
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{
	const EXPORT_RAW = 'raw';



	const EXPORT_FULL = 'full';













	const EXPECTED_CSV_COLUMNS_SELF = ['id', 'type', 'status', 'label', 'date', 'notes', 'reference',





		'line_id', 'account', 'credit', 'debit', 'line_reference', 'line_label', 'reconciled'];






	const POSSIBLE_CSV_COLUMNS = [



		'id'             => 'Numéro d\'écriture',


		'label'          => 'Libellé',
		'date'           => 'Date',
		'notes'          => 'Remarques',
		'reference'      => 'Numéro pièce comptable',
		'p_reference'    => 'Référence paiement',
		'debit_account'  => 'Compte de débit',
		'credit_account' => 'Compte de crédit',
		'amount'         => 'Montant',
		'analytical'     => 'Compte analytique',


	];

	const MANDATORY_CSV_COLUMNS = ['label', 'date', 'credit_account', 'debit_account', 'amount'];

















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

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







|
>
>
>
|
>
>
>
>

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

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


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







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
use Garradin\DB;
use Garradin\DynamicList;
use Garradin\Utils;
use Garradin\UserException;

class Transactions
{
	const EXPORT_FULL = 'full';
	const EXPORT_GROUPED = 'grouped';
	const EXPORT_SIMPLE = 'simple';

	const EXPORT_NAMES = [
		self::EXPORT_FULL => 'Complet',
		self::EXPORT_GROUPED => 'Groupé',
		self::EXPORT_SIMPLE => 'Simplifié',
	];

	const EXPORT_COLUMNS_FULL = [
		'id'        => 'Numéro d\'écriture',
		'type'      => 'Type',
		'status'    => 'Statut',
		'label'     => 'Libellé',
		'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',
	];

	const EXPORT_COLUMNS = [
		self::EXPORT_GROUPED => self::EXPORT_COLUMNS_FULL,
		self::EXPORT_FULL => self::EXPORT_COLUMNS_FULL,
		self::EXPORT_SIMPLE => [
			'id'             => 'Numéro d\'écriture',
			'type'           => 'Type',
			'status'         => 'Statut',
			'label'          => 'Libellé',
			'date'           => 'Date',
			'notes'          => 'Remarques',
			'reference'      => 'Numéro pièce comptable',
			'p_reference'    => 'Référence paiement',
			'debit_account'  => 'Compte de débit',
			'credit_account' => 'Compte de crédit',
			'amount'         => 'Montant',
			'analytical'     => 'Compte analytique',
			'linked_users'   => 'Membres associés',
		],
	];

	const MANDATORY_COLUMNS = [
		self::EXPORT_GROUPED => [
			'type',
			'label',
			'date',
			'account',
			'credit',
			'debit',
		],
		self::EXPORT_SIMPLE => [
			'label',
			'date',
			'credit_account',
			'debit_account',
			'amount'
		],
	];

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

	static public function saveReconciled(\Generator $journal, ?array $checked)
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
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}

	/**
	 * Return all transactions from year
	 */
	static public function export(Year $year, string $format, string $type = self::EXPORT_RAW): void
	{
		$header = null;

		if (self::EXPORT_FULL == $type) {
			$header = ['Numéro d\'écriture', 'Type', 'Statut', 'Libellé', 'Date', 'Remarques', 'Numéro pièce comptable', 'Numéro ligne', 'Compte', 'Débit', 'Crédit', 'Référence ligne', 'Libellé ligne', 'Rapprochement', 'Compte analytique', 'Membres associés'];
		}

		CSV::export(
			$format,
			sprintf('Export comptable - %s - %s', Config::getInstance()->get('nom_asso'), $year->label),
			self::iterateExport($year->id(), $type),
			$header
		);
	}

	static protected function iterateExport(int $year_id, string $type): \Generator
	{
		$sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference,
			l.id AS line_id, a.code AS account, l.debit AS debit, l.credit AS credit,
			l.reference AS line_reference, l.label AS line_label, l.reconciled,
			a2.code AS analytical,
			GROUP_CONCAT(u.%s) AS linked_users
			FROM acc_transactions t
			INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
			INNER JOIN acc_accounts a ON a.id = l.id_account
			LEFT JOIN acc_accounts a2 ON a2.id = l.id_analytical
			LEFT JOIN acc_transactions_users tu ON tu.id_transaction = t.id
			LEFT JOIN membres u ON u.id = tu.id_user
			WHERE t.id_year = ?



			GROUP BY t.id, l.id


			ORDER BY t.date, t.id, l.id;';


		$id_field = Config::getInstance()->get('champ_identite');








































		$sql = sprintf($sql, $id_field);


		$res = DB::getInstance()->iterate($sql, $year_id);

		$previous_id = null;

		foreach ($res as $row) {
			if ($previous_id === $row->id && $type == self::EXPORT_RAW) {

				$row->id = $row->type = $row->status = $row->label = $row->date = $row->notes = $row->reference = null;
			}
			else {
				$row->type = Transaction::TYPES_NAMES[$row->type];

				$status = [];

				foreach (Transaction::STATUS_NAMES as $k => $v) {
					if ($row->status & $k) {
						$status[] = $v;
					}
				}

				$row->status = implode(', ', $status);
				$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);
				$row->date = $row->date->format('d/m/Y');
				$previous_id = $row->id;
			}





			$row->credit = Utils::money_format($row->credit, ',', '');
			$row->debit = Utils::money_format($row->debit, ',', '');


			yield $row;
		}
	}

	static public function importCSV(Year $year, array $file, int $user_id)
	{




		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}

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

		$accounts = $year->accounts();
		$transaction = null;
		$types = array_flip(Transaction::TYPES_NAMES);

		$l = 1;

		try {
			foreach (CSV::importUpload($file, self::EXPECTED_CSV_COLUMNS_SELF) as $l => $row) {
				$row = (object) $row;




				$has_transaction = !empty($row->id) || !empty($row->type) || !empty($row->status) || !empty($row->label) || !empty($row->date) || !empty($row->notes) || !empty($row->reference);









				if (null !== $transaction && $has_transaction) {
					$transaction->save();












					$transaction = null;
				}


				if (null === $transaction) {
					if (!$has_transaction) {
						throw new UserException('cette ligne n\'est reliée à aucune écriture');
					}

					if ($row->id) {
						$transaction = self::get((int)$row->id);

						if (!$transaction) {
							throw new UserException(sprintf('l\'écriture #%d est introuvable', $row->id));
						}

						if ($transaction->id_year != $year->id()) {







|



|
|




|

|



|

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

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






|
>



















>
>
>
>
|
|
>





|

>
>
>
>














|


>
>
>
|
>
>
>
>
>
>
>

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



>

<
<
<
<
|







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343




344
345
346
347
348
349
350
351
	{
		return DB::getInstance()->count('acc_transactions', 'id_creator = ?', $user_id);
	}

	/**
	 * Return all transactions from year
	 */
	static public function export(Year $year, string $format, string $type): void
	{
		$header = null;

		if (!array_key_exists($type, self::EXPORT_COLUMNS)) {
			throw new \InvalidArgumentException('Unknown type: ' . $type);
		}

		CSV::export(
			$format,
			sprintf('Export comptable %s - %s - %s', strtolower(self::EXPORT_NAMES[$type]), Config::getInstance()->get('nom_asso'), $year->label),
			self::iterateExport($year->id(), $type),
			array_values(self::EXPORT_COLUMNS[$type])
		);
	}

	static public function getExportExamples(Year $year)
	{
		$out = [];

		foreach (self::EXPORT_NAMES as $type => $label) {
			$i = 0;
			$out[$type] = [self::EXPORT_COLUMNS[$type]];

			foreach (self::iterateExport($year->id(), $type) as $row) {
				$out[$type][] = $row;

				if (++$i > 1) {

					break;
				}
			}
		}

		return $out;
	}

	static protected function iterateExport(int $year_id, string $type): \Generator
	{
		$id_field = Config::getInstance()->get('champ_identite');

		if (self::EXPORT_SIMPLE == $type) {
			$sql =  'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference,
				l1.reference AS p_reference,
				a1.code AS debit_account,
				a2.code AS credit_account,
				l1.debit AS amount,
				a3.code AS analytical,
				GROUP_CONCAT(u.%s) AS linked_users
				FROM acc_transactions t
				INNER JOIN acc_transactions_lines l1 ON l1.id_transaction = t.id AND l1.debit != 0
				INNER JOIN acc_transactions_lines l2 ON l2.id_transaction = t.id AND l2.credit != 0
				INNER JOIN acc_accounts a1 ON a1.id = l1.id_account
				INNER JOIN acc_accounts a2 ON a2.id = l2.id_account
				LEFT JOIN acc_accounts a3 ON a3.id = l1.id_analytical
				LEFT JOIN acc_transactions_users tu ON tu.id_transaction = t.id
				LEFT JOIN membres u ON u.id = tu.id_user
				WHERE t.id_year = ?
					AND t.type != %d
				GROUP BY t.id
				ORDER BY t.date, t.id;';

			$sql = sprintf($sql, $id_field, Transaction::TYPE_ADVANCED);
		}
		else {
			$sql = 'SELECT t.id, t.type, t.status, t.label, t.date, t.notes, t.reference,
				l.id AS line_id, a.code AS account, l.debit AS debit, l.credit AS credit,
				l.reference AS line_reference, l.label AS line_label, l.reconciled,
				a2.code AS analytical,
				GROUP_CONCAT(u.%s) AS linked_users
				FROM acc_transactions t
				INNER JOIN acc_transactions_lines l ON l.id_transaction = t.id
				INNER JOIN acc_accounts a ON a.id = l.id_account
				LEFT JOIN acc_accounts a2 ON a2.id = l.id_analytical
				LEFT JOIN acc_transactions_users tu ON tu.id_transaction = t.id
				LEFT JOIN membres u ON u.id = tu.id_user
				WHERE t.id_year = ?
				GROUP BY t.id, l.id
				ORDER BY t.date, t.id, l.id;';

			$sql = sprintf($sql, $id_field);
		}

		$res = DB::getInstance()->iterate($sql, $year_id);

		$previous_id = null;

		foreach ($res as $row) {
			if ($previous_id === $row->id && $type == self::EXPORT_GROUPED) {
				// Remove transaction data to differentiate lines and transactions
				$row->id = $row->type = $row->status = $row->label = $row->date = $row->notes = $row->reference = null;
			}
			else {
				$row->type = Transaction::TYPES_NAMES[$row->type];

				$status = [];

				foreach (Transaction::STATUS_NAMES as $k => $v) {
					if ($row->status & $k) {
						$status[] = $v;
					}
				}

				$row->status = implode(', ', $status);
				$row->date = \DateTime::createFromFormat('Y-m-d', $row->date);
				$row->date = $row->date->format('d/m/Y');
				$previous_id = $row->id;
			}

			if ($type == self::EXPORT_SIMPLE) {
				$row->amount = Utils::money_format($row->amount, ',', '');
			}
			else {
				$row->credit = Utils::money_format($row->credit, ',', '');
				$row->debit = Utils::money_format($row->debit, ',', '');
			}

			yield $row;
		}
	}

	static public function import(string $type, Year $year, CSV_Custom $csv, int $user_id, bool $ignore_ids = false)
	{
		if ($type != self::EXPORT_GROUPED && $type != self::EXPORT_SIMPLE) {
			throw new \InvalidArgumentException('Invalid type value');
		}

		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}

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

		$accounts = $year->accounts();
		$transaction = null;
		$types = array_flip(Transaction::TYPES_NAMES);

		$l = 1;

		try {
			foreach ($csv->iterate() as $l => $row) {
				$row = (object) $row;

				// Import grouped transactions
				if ($type == self::EXPORT_GROUPED) {
					// If a line doesn't have any transaction info: this is a line following the previous transaction
					$has_transaction = !(empty($row->id)
						&& empty($row->type)
						&& empty($row->status)
						&& empty($row->label)
						&& empty($row->date)
						&& empty($row->notes)
						&& empty($row->reference)
					);

					// New transaction, save previous one
					if (null !== $transaction && $has_transaction) {
						$transaction->save();
						$transaction = null;
					}

					if (!$has_transaction && null === $transaction) {
						throw new UserException('cette ligne n\'est reliée à aucune écriture');
					}
				}
				else {
					if (empty($row->type)) {
						$row->type = $types[Transaction::TYPE_ADVANCED];
					}

					$transaction = null;
				}

				// Find or create transaction
				if (null === $transaction) {




					if ($row->id && !$ignore_ids) {
						$transaction = self::get((int)$row->id);

						if (!$transaction) {
							throw new UserException(sprintf('l\'écriture #%d est introuvable', $row->id));
						}

						if ($transaction->id_year != $year->id()) {
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280






































































281
282
283
284
285
286
287
288
289
290
291
292
293

294
295

296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
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

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

					$transaction->importForm($fields);
				}

				$id_account = $accounts->getIdFromCode($row->account);

				if (!$id_account) {
					throw new UserException(sprintf('le compte "%s" n\'existe pas dans le plan comptable', $row->account));
				}

				$row->line_id = trim($row->line_id);
				$id_analytical = null;
				$data = [
					'credit'     => $row->credit ?: 0,
					'debit'      => $row->debit ?: 0,
					'id_account' => $id_account,
					'reference'  => $row->line_reference,
					'label'      => $row->line_label,
					'reconciled' => $row->reconciled,
				];

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

					if (!$id_analytical) {
						throw new UserException(sprintf('le compte analytique "%s" n\'existe pas dans le plan comptable', $row->analytical));
					}

					$data['id_analytical'] = $id_analytical;
				}
				elseif (property_exists($row, 'analytical')) {
					$data['id_analytical'] = null;
				}







































































				if ($row->line_id) {
					$line = $transaction->getLine((int)$row->line_id);

					if (!$line) {
						throw new UserException(sprintf('le numéro de ligne "%s" n\'existe pas dans l\'écriture "%s"', $row->line_id, $transaction->id ?: 'à créer'));
					}
				}
				else {
					$line = new Line;
				}

				$line->importForm($data);


				if (!$row->line_id) {
					$transaction->addLine($line);

				}
			}

			if (null !== $transaction) {
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();
			$e->setMessage(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));

			if (null !== $transaction) {
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();
	}

	static public function importCustom(Year $year, CSV_Custom $csv, int $user_id)
	{
		if ($year->closed) {
			throw new \InvalidArgumentException('Closed year');
		}

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

		$accounts = $year->accounts();
		$l = 0;

		try {
			foreach ($csv->iterate() as $l => $row) {
				if (!isset($row->credit_account, $row->debit_account, $row->amount)) {
					throw new UserException('Une des colonnes compte de crédit, compte de débit ou montant est manquante.');
				}

				if (!empty($row->id)) {
					$transaction = self::get((int)$row->id);

					if (!$transaction) {
						throw new UserException(sprintf('l\'écriture n°%d est introuvable', $row->id));
					}

					if ($transaction->validated) {
						throw new UserException(sprintf('l\'écriture n°%d est validée et ne peut être modifiée', $row->id));
					}

					$transaction->resetLines();
				}
				else {
					$transaction = new Transaction;
					$transaction->type = Transaction::TYPE_ADVANCED;
					$transaction->id_creator = $user_id;
					$transaction->id_year = $year->id();
				}

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

				$credit_account = $accounts->getIdFromCode($row->credit_account);
				$debit_account = $accounts->getIdFromCode($row->debit_account);

				if (!$credit_account) {
					throw new UserException(sprintf('Compte de crédit "%s" inconnu dans le plan comptable', $row->credit_account));
				}

				if (!$debit_account) {
					throw new UserException(sprintf('Compte de débit "%s" inconnu dans le plan comptable', $row->debit_account));
				}

				$id_analytical = null;

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

					if (!$id_analytical) {
						throw new UserException(sprintf('le compte analytique "%s" n\'existe pas dans le plan comptable', $row->analytical));
					}
				}

				$line = new Line;
				$line->importForm([
					'credit'     => $row->amount,
					'debit'      => 0,
					'id_account' => $credit_account,
					'reference'  => isset($row->p_reference) ? $row->p_reference : null,
					'id_analytical' => $id_analytical,
				]);
				$transaction->addLine($line);

				$line = new Line;
				$line->importForm([
					'credit'     => 0,
					'debit'      => $row->amount,
					'id_account' => $debit_account,
					'reference'  => isset($row->p_reference) ? $row->p_reference : null,
					'id_analytical' => $id_analytical,
				]);
				$transaction->addLine($line);
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();

			$e->setMessage(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));

			if (null !== $transaction) {
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}

		$db->commit();
	}

	static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null)
	{
		$db = DB::getInstance();








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














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

|
|
|
|
|
|
|

|

>
|
|
>

















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







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
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493




































































































494
495
496
497
498
499
500

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

					if (!$id_analytical) {
						throw new UserException(sprintf('le compte analytique "%s" n\'existe pas dans le plan comptable', $row->analytical));
					}

					$data['id_analytical'] = $id_analytical;
				}
				elseif (property_exists($row, 'analytical')) {
					$data['id_analytical'] = null;
				}

				// Add two transaction lines for each CSV line
				if ($type == self::EXPORT_SIMPLE) {
					$credit_account = $accounts->getIdFromCode($row->credit_account);
					$debit_account = $accounts->getIdFromCode($row->debit_account);

					if (!$credit_account) {
						throw new UserException(sprintf('Compte de crédit "%s" inconnu dans le plan comptable', $row->credit_account));
					}

					if (!$debit_account) {
						throw new UserException(sprintf('Compte de débit "%s" inconnu dans le plan comptable', $row->debit_account));
					}

					$data['reference'] = isset($row->p_reference) ? $row->p_reference : null;

					if (!$transaction->exists()) {
						$l1 = new Line;
						$l2 = new Line;
						$transaction->addLine($l1);
						$transaction->addLine($l2);
					}
					else {
						$lines = $transaction->getLines();

						if (count($lines) != 2) {
							throw new UserException('cette écriture comporte plus de deux lignes et ne peut donc être modifiée par un import simplifié');
						}

						// Find correct debit/credit lines
						if ($lines[0]->credit != 0) {
							$l1 = $lines[0];
							$l2 = $lines[1];
						}
						else {
							$l1 = $lines[1];
							$l2 = $lines[0];
						}
					}

					$l1->importForm($data + [
						'credit'     => $row->amount,
						'debit'      => 0,
						'id_account' => $credit_account,
					]);

					$l2->importForm($data + [
						'credit'     => 0,
						'debit'      => $row->amount,
						'id_account' => $debit_account,
					]);

					$transaction->save();
					$transaction = null;
				}
				else {
					$id_account = $accounts->getIdFromCode($row->account);

					if (!$id_account) {
						throw new UserException(sprintf('le compte "%s" n\'existe pas dans le plan comptable', $row->account));
					}

					$data = $data + [
						'credit'     => $row->credit ?: 0,
						'debit'      => $row->debit ?: 0,
						'id_account' => $id_account,
						'reference'  => $row->line_reference,
						'label'      => $row->line_label,
						'reconciled' => $row->reconciled,
					];

					if ($row->line_id && !$ignore_ids) {
						$line = $transaction->getLine((int)$row->line_id);

						if (!$line) {
							throw new UserException(sprintf('le numéro de ligne "%s" n\'existe pas dans l\'écriture "%s"', $row->line_id, $transaction->id ?: 'à créer'));
						}
					}
					else {
						$line = new Line;
					}

					$line->importForm($data);

					// If a line_id was supplied, just changing the object is enough, no need to add it to the transaction
					if (!$row->line_id) {
						$transaction->addLine($line);
					}
				}
			}

			if (null !== $transaction) {
				$transaction->save();
			}
		}
		catch (UserException $e) {
			$db->rollback();
			$e->setMessage(sprintf('Erreur sur la ligne %d : %s', $l, $e->getMessage()));

			if (null !== $transaction) {
				$e->setDetails($transaction->asDetailsArray());
			}

			throw $e;
		}




































































































		$db->commit();
	}

	static public function setAnalytical(?int $id_analytical, ?array $transactions = null, ?array $lines = null)
	{
		$db = DB::getInstance();

Modified src/include/lib/Garradin/CSV_Custom.php from [d12e2645d9] to [2caee2fd52].

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
			$row = $default;

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

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

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








|







60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
			$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;
		}
	}

Modified src/include/lib/Garradin/Membres/Import.php from [e73b1e22a1] to [efbfcdc67d].

248
249
250
251
252
253
254




255
256
257
258
259
260
261

	public function exportRow(\stdClass $row) {
		if (null === $this->champs) {
			$this->champs = Config::getInstance()->get('champs_membres')->getAll();
		}

		foreach ($this->champs as $id => $config) {




			if ($config->type == 'date') {
				$row->$id = \DateTime::createFromFormat('!Y-m-d', $row->$id);
			}
			elseif ($config->type == 'datetime') {
				$row->$id = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->$id);
			}
			// convertir les champs à choix multiple de binaire vers liste séparée par des points virgules







>
>
>
>







248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265

	public function exportRow(\stdClass $row) {
		if (null === $this->champs) {
			$this->champs = Config::getInstance()->get('champs_membres')->getAll();
		}

		foreach ($this->champs as $id => $config) {
			if (!isset($row->$id)) {
				continue;
			}

			if ($config->type == 'date') {
				$row->$id = \DateTime::createFromFormat('!Y-m-d', $row->$id);
			}
			elseif ($config->type == 'datetime') {
				$row->$id = \DateTime::createFromFormat('!Y-m-d H:i:s', $row->$id);
			}
			// convertir les champs à choix multiple de binaire vers liste séparée par des points virgules

Modified src/templates/acc/index.tpl from [7cbbdbb32f] to [70f092a209].

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<section class="year-infos">
	<h2 class="ruler">{$year.label} —
		Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2>

	<nav class="tabs">
		<aside>
			{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
				{linkbutton shape="upload" href="!acc/years/import.php?id=%d"|args:$year.id label="Import & export"}
			{/if}
			{linkbutton shape="search" href="!acc/search.php?year=%d"|args:$year.id label="Recherche"}
		</aside>
		<ul>
			<li><a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a></li>
			<li><a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a></li>
			<li><a href="{$admin_url}acc/reports/journal.php?year={$year.id}">Journal général</a></li>







|







20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<section class="year-infos">
	<h2 class="ruler">{$year.label} —
		Du {$year.start_date|date_short} au {$year.end_date|date_short}</h2>

	<nav class="tabs">
		<aside>
			{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
				{linkbutton shape="upload" href="!acc/years/import.php?year=%d"|args:$year.id label="Import & export"}
			{/if}
			{linkbutton shape="search" href="!acc/search.php?year=%d"|args:$year.id label="Recherche"}
		</aside>
		<ul>
			<li><a href="{$admin_url}acc/reports/graphs.php?year={$year.id}">Graphiques</a></li>
			<li><a href="{$admin_url}acc/reports/trial_balance.php?year={$year.id}">Balance générale</a></li>
			<li><a href="{$admin_url}acc/reports/journal.php?year={$year.id}">Journal général</a></li>

Modified src/templates/acc/years/export.tpl from [640274763b] to [43da2a1c1b].

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
{include file="admin/_head.tpl" title="Export d'exercice" current="acc/years"}

<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		{if !$year.closed}
		<li><a href="{$admin_url}acc/years/import.php?id={$year.id}">Import</a></li>
		{/if}
		<li class="current"><a href="{$admin_url}acc/years/import.php?id={$year.id}">Export</a></li>
	</ul>
</nav>

{form_errors}

<form method="get" action="{$self_url}">

<fieldset>
	<legend>Export du journal général</legend>
	<dl>
		<dt>Format d'export</dt>
		{input type="radio" name="format" value="ods" default="ods" label="Tableur" help="pour LibreOffice ou autre tableur"}
		{input type="radio" name="format" value="csv" label="CSV"}
		<dt>Type d'export</dt>
		{input type="radio" name="type" value="full" label="Export comptable complet" default="full" help="conseillé pour transfert vers un autre logiciel"}
		{input type="radio" name="type" value="raw" label="Export natif Garradin"}













	</dl>
</fieldset>

<p class="submit">
	<input type="hidden" name="id" value="{$year.id}" />
	{button type="submit" name="load" label="Télécharger" shape="download" class="main"}
</p>



</form>

{include file="admin/_foot.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
{include file="admin/_head.tpl" title="Export d'exercice" current="acc/years"}

<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		{if !$year.closed}
		<li><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li>
		{/if}
		<li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Export</a></li>
	</ul>
</nav>

{form_errors}

<form method="get" action="{$self_url}">

<fieldset>
	<legend>Export du journal général</legend>
	<dl>
		<dt>Format d'export</dt>
		{input type="radio" name="format" value="ods" default="ods" label="Tableur" help="pour LibreOffice ou autre tableur"}
		{input type="radio" name="format" value="csv" label="CSV"}
		<dt>Type d'export</dt>
		{foreach from=$types key="type" item="info"}
		{input type="radio-btn" name="type" value=$type label=$info.label help=$info.help default="full"}
		<dd class="help example">
			Exemple :
			<table class="list auto">
				{foreach from=$examples[$type] item="row"}
				<tr>
					{foreach from=$row item="v"}
					<td>{$v}</td>
					{/foreach}
				</tr>
				{/foreach}
			</table>
		</dd>
		{/foreach}
	</dl>
</fieldset>

<p class="submit">
	<input type="hidden" name="id" value="{$year.id}" />
	{button type="submit" name="load" label="Télécharger" shape="download" class="main"}
</p>



</form>

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

Modified src/templates/acc/years/import.tpl from [1a8a438acd] to [13879d8b76].

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
{include file="admin/_head.tpl" title="Importer des écritures" current="acc/years"}

<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/years/import.php?id={$year.id}">Import</a></li>
		<li><a href="{$admin_url}acc/years/export.php?id={$year.id}">Export</a></li>
	</ul>
</nav>

{form_errors}

<form method="post" action="{$self_url}" enctype="multipart/form-data">

{if $csv->loaded()}

		{include file="common/_csv_match_columns.tpl"}

		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="cancel" value="1" label="Annuler" shape="left"}
			{button type="submit" name="assign" label="Continuer" class="main" shape="right"}
		</p>

{else}


	<fieldset>
		<legend>Import d'écritures</legend>
		<dl>
			<dt><label for="f_type_garradin">Format de fichier</label></dt>
			{input type="radio" name="type" value="csv" label="Journal au format CSV libre"  default="csv"}
			<dd class="help">Ce format ne permet d'importer que des écritures simples (un débit et un crédit par écriture) mais convient à la plupart des utilisations.</dd>
			{include file="common/_csv_help.tpl"}
			{input type="radio" name="type" value="garradin" label="Journal général au format CSV Garradin"}
			<dd class="help">Ce format permet d'importer des écritures comportant plusieurs lignes. Le format attendu est identique à l'export de journal général qui peut servir d'exemple.</dd>

			{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=1}



			<dd class="help block">
				- Les lignes comportant un numéro d'écriture existant mettront à jour les écritures correspondant à ces numéros.<br />
				- Les lignes comportant un numéro inexistant renverront une erreur.<br />
				- Les lignes sans numéro créeront de nouvelles écritures.<br />
				- Si le fichier comporte des écritures dont la date est en dehors de l'exercice courant, elles seront ignorées.












































			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{button type="submit" name="load" label="Importer" shape="upload" class="main"}
	</p>


{/if}

</form>

{include file="admin/_foot.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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

105
106
{include file="admin/_head.tpl" title="Importer des écritures" current="acc/years"}

<nav class="acc-year">
	<h4>Exercice sélectionné&nbsp;:</h4>
	<h3>{$year.label} — {$year.start_date|date_short} au {$year.end_date|date_short}</h3>
</nav>

<nav class="tabs">
	<ul>
		<li class="current"><a href="{$admin_url}acc/years/import.php?year={$year.id}">Import</a></li>
		<li><a href="{$admin_url}acc/years/export.php?year={$year.id}">Export</a></li>
	</ul>
</nav>

{form_errors}



{if $type_name && $csv->loaded()}
<form method="post" action="{$self_url}">
		{include file="common/_csv_match_columns.tpl"}

		<p class="submit">
			{csrf_field key=$csrf_key}
			{button type="submit" name="cancel" value="1" label="Annuler" shape="left"}
			{button type="submit" name="assign" label="Continuer" class="main" shape="right"}
		</p>
</form>
{elseif $type_name}
<form method="post" action="{$self_url}" enctype="multipart/form-data">

	<fieldset>
		<legend>Import d'écritures</legend>
		<dl>
			<dt>

				Type d'import
			</dt>
			<dd>
				{$type_name}
			</dd>
			{input type="file" name="file" label="Fichier CSV" accept=".csv,text/csv" required=true}
			{include file="common/_csv_help.tpl" csv=$csv}
			{input type="checkbox" name="ignore_ids" value="1" label="Ne pas tenir compte des numéros d'écritures" help="Si coché, les écritures importées seront créées, même si un numéro d'écriture est fourni et qu'il existe déjà. Cela peut mener à avoir des écritures en doublon."}
		</dl>
		<p class="help block">
			- Les lignes comportant un numéro d'écriture existant mettront à jour les écritures correspondant à ces numéros.<br />
			- Les lignes comportant un numéro inexistant renverront une erreur.<br />
			- Les lignes dont le numéro est vide créeront de nouvelles écritures.<br />
			- Si le fichier comporte des écritures dont la date est en dehors de l'exercice courant, elles seront ignorées.
		</p>

	</fieldset>

	<p class="submit">
		{csrf_field key=$csrf_key}
		{linkbutton href="?year=%d"|args:$year.id label="Annuler" shape="left"}
		{button type="submit" name="load" label="Importer" shape="upload" class="main"}
	</p>

</form>

{else}

<form method="get" action="{$self_url_no_qs}">
	<fieldset>
		<legend>Import d'écritures</legend>
		<dl>
			<dt><label for="f_type_garradin">Type de fichier à importer</label></dt>
			{input type="radio-btn" name="type" value="simple" label="Simplifié (comptabilité de trésorerie)" default="simple" help="Chaque ligne représente une écriture, comme dans un cahier. Les écritures avancées ne peuvent pas être importées dans ce format."}
			<dd class="help example">
				Exemple :
				<table class="list auto">
					{foreach from=$examples.simple item="row"}
					<tr>
						{foreach from=$row item="v"}
						<td>{$v}</td>
						{/foreach}
					</tr>
					{/foreach}
				</table>
			</dd>
			{input type="radio-btn" name="type" value="grouped" label="Complet groupé (comptabilité d'engagement)" help="Permet d'avoir des écritures avancées. Les 7 premières colonnes de chaque ligne sont vides pour indiquer les lignes suivantes de l'écriture."}
			<dd class="help example">
				Exemple :
				<table class="list auto">
					{foreach from=$examples.grouped item="row"}
					<tr>
						{foreach from=$row item="v"}
						<td>{$v}</td>
						{/foreach}
					</tr>
					{/foreach}
				</table>
			</dd>
		</dl>
	</fieldset>

	<p class="submit">
		<input type="hidden" name="year" value="{$year.id}" />
		{button type="submit" label="Continuer" shape="right" class="main"}
	</p>
</form>

{/if}



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

Modified src/templates/acc/years/index.tpl from [1e4ce07d41] to [b2a2e93dd9].

8
9
10
11
12
13
14






15
16
17
18
19
20
21
		{linkbutton shape="search" href="!acc/search.php" label="Recherche"}
	</aside>
	<ul>
		<li class="current"><a href="{$self_url}">Exercices</a></li>
		<li><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li>
	</ul>
</nav>







{if $_GET.msg == 'OPEN'}
<p class="block error">
	Il n'existe aucun exercice ouvert.
	{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
		Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures.
	{/if}







>
>
>
>
>
>







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
		{linkbutton shape="search" href="!acc/search.php" label="Recherche"}
	</aside>
	<ul>
		<li class="current"><a href="{$self_url}">Exercices</a></li>
		<li><a href="{$admin_url}acc/reports/projects.php">Projets <em>(compta analytique)</em></a></li>
	</ul>
</nav>

{if $_GET.msg == 'IMPORT'}
<p class="block confirm">
	L'import s'est bien déroulé.
</p>
{/if}

{if $_GET.msg == 'OPEN'}
<p class="block error">
	Il n'existe aucun exercice ouvert.
	{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
		Merci d'en <a href="{$admin_url}acc/years/new.php">créer un nouveau</a> pour pouvoir saisir des écritures.
	{/if}
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
					| <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a>
				</td>
			</tr>
			<tr>
				<td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td>
				<td>
				{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
					{linkbutton label="Export" shape="export" href="export.php?id=%d"|args:$year.id}
					{if !$year.closed}
						{linkbutton label="Import" shape="upload" href="import.php?id=%d"|args:$year.id}
						{linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id}
						{linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id}
						{linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id}
						{linkbutton label="Supprimer" shape="delete" href="delete.php?id=%d"|args:$year.id}
					{/if}
				{/if}
				</td>







|

|







69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
					| <a href="{$admin_url}acc/reports/balance_sheet.php?year={$year.id}">Bilan</a>
				</td>
			</tr>
			<tr>
				<td><em>{if $year.closed}Clôturé{else}En cours{/if}</em></td>
				<td>
				{if $session->canAccess($session::SECTION_ACCOUNTING, $session::ACCESS_ADMIN)}
					{linkbutton label="Export" shape="export" href="export.php?year=%d"|args:$year.id}
					{if !$year.closed}
						{linkbutton label="Import" shape="upload" href="import.php?year=%d"|args:$year.id}
						{linkbutton label="Balance d'ouverture" shape="reset" href="balance.php?id=%d"|args:$year.id}
						{linkbutton label="Modifier" shape="edit" href="edit.php?id=%d"|args:$year.id}
						{linkbutton label="Clôturer" shape="lock" href="close.php?id=%d"|args:$year.id}
						{linkbutton label="Supprimer" shape="delete" href="delete.php?id=%d"|args:$year.id}
					{/if}
				{/if}
				</td>

Modified src/templates/common/_csv_match_columns.tpl from [09427edd39] to [a4fa33eaf0].

1
2
3
4
5
6
7
8
9
<fieldset>
	<legend>Importer depuis un fichier CSV générique</legend>
	<dl>
		<dd class="help">{$csv->count()} lignes trouvées dans le fichier</dd>
		<dt>{input type="checkbox" name="skip_first_line" value="1" label="Ne pas importer la première ligne" help="Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes, mais des données" default=1}
		<dt><label>Correspondance des colonnes</label></dt>
		<dd>
			<table class="list auto">
				<thead>

|







1
2
3
4
5
6
7
8
9
<fieldset>
	<legend>Importer depuis un fichier CSV</legend>
	<dl>
		<dd class="help">{$csv->count()} lignes trouvées dans le fichier</dd>
		<dt>{input type="checkbox" name="skip_first_line" value="1" label="Ne pas importer la première ligne" help="Décocher cette case si la première ligne ne contient pas l'intitulé des colonnes, mais des données" default=1}
		<dt><label>Correspondance des colonnes</label></dt>
		<dd>
			<table class="list auto">
				<thead>

Modified src/www/admin/acc/years/export.php from [5fa4e489df] to [2f3ec0ba23].

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

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

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

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

$year_id = (int) qg('id') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);
}

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

$format = qg('format');
$type = qg('type');

if (null !== $format && null !== $type) {
	Transactions::export($year, $format, $type);
	exit;
}


















$tpl->assign(compact('year'));

$tpl->display('acc/years/export.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
<?php
namespace Garradin;

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

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

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

$year_id = (int) qg('year') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);
}

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

$format = qg('format');
$type = qg('type');

if (null !== $format && null !== $type) {
	Transactions::export($year, $format, $type);
	exit;
}

$examples = Transactions::getExportExamples($year);

$types = [
	Transactions::EXPORT_FULL => [
		'label' => 'Complet (comptabilité d\'engagement)',
		'help' => '(Conseillé pour transfert vers un autre logiciel) Chaque ligne reprend toutes les informations de la ligne et de l\'écriture.',
	],
	Transactions::EXPORT_GROUPED => [
		'label' => 'Complet groupé',
		'help' => 'Idem, sauf que les lignes suivantes ne mentionnent pas les informations de l\'écriture.',
	],
	Transactions::EXPORT_SIMPLE => [
		'label' => 'Simplifié (comptabilité de trésorerie)',
		'help' => 'Les écritures avancées ne sont pas inclues dans cet export.',
	],
];

$tpl->assign(compact('year', 'examples', 'types'));

$tpl->display('acc/years/export.tpl');

Modified src/www/admin/acc/years/import.php from [70de879a56] to [1f7549983a].

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

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

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

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

$year_id = (int) qg('id') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);
}










|







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

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

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

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

$year_id = (int) qg('year') ?: CURRENT_YEAR_ID;

if ($year_id === CURRENT_YEAR_ID) {
	$year = $current_year;
}
else {
	$year = Years::get($year_id);
}
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
	exit;
}

if ($year->closed) {
	throw new UserException('Impossible de modifier un exercice clôturé.');
}





$csv = new CSV_Custom($session, 'acc_import_year');

$csv->setColumns(Transactions::POSSIBLE_CSV_COLUMNS);
$csv->setMandatoryColumns(Transactions::MANDATORY_CSV_COLUMNS);


if (f('cancel')) {
	$csv->clear();

	Utils::redirect(Utils::getSelfURI());
}

$csrf_key = 'acc_years_import_' . $year->id();






$form->runIf(f('assign') && $csv->loaded(), function () use ($csv, $year, $user) {
	$csv->skip((int)f('skip_first_line'));
	$csv->setTranslationTable(f('translation_table'));

	Transactions::importCustom($year, $csv, $user->id);
	$csv->clear();
}, $csrf_key, ADMIN_URL . 'acc/years/');

$form->runIf('load', function () use ($csv, $year, $user) {
	if (f('type') == 'garradin') {
		Transactions::importCSV($year, $_FILES['file'], $user->id);
		Utils::redirect(ADMIN_URL . 'acc/years/');
	}
	elseif (isset($_FILES['file']['tmp_name'])) {
		$csv->load($_FILES['file']);
		Utils::redirect(Utils::getSelfURI());

	}
	else {
		throw new UserException('Fichier invalide');


	}
}, $csrf_key, Utils::getSelfURI());

$tpl->assign(compact('csv', 'year', 'csrf_key'));

$tpl->display('acc/years/import.tpl');







>
>
>
>

>
|
<
>



>
|


<
>
>
>
>
>

|
|
|

|
|
|

<
<
<
<
<
|

|
>
|
|
<
>
>
|
<

|


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

40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
55
56
57
58
59
60
61





62
63
64
65
66
67

68
69
70

71
72
73
74
	exit;
}

if ($year->closed) {
	throw new UserException('Impossible de modifier un exercice clôturé.');
}

$type = qg('type');
$type_name = Transactions::EXPORT_NAMES[$type] ?? null;
$csrf_key = 'acc_years_import_' . $year->id();
$examples = null;
$csv = new CSV_Custom($session, 'acc_import_year');
$ignore_ids = (bool) (f('ignore_ids') ?? qg('ignore_ids'));


$params = ['year' => $year->id(), 'ignore_ids' => (int) $ignore_ids, 'type' => $type];

if (f('cancel')) {
	$csv->clear();
	unset($params['type']);
	Utils::redirect(Utils::getSelfURI($params));
}


if ($type && $type_name) {
	$columns = Transactions::EXPORT_COLUMNS[$type];
	unset($columns['linked_users']);
	$csv->setColumns($columns);
	$csv->setMandatoryColumns(Transactions::MANDATORY_COLUMNS[$type]);

	$form->runIf(f('assign') && $csv->loaded(), function () use ($type, $csv, $year, $user, $ignore_ids) {
		$csv->skip((int)f('skip_first_line'));
		$csv->setTranslationTable(f('translation_table'));

		Transactions::import($type, $year, $csv, $user->id, (bool) $ignore_ids);
		$csv->clear();
	}, $csrf_key, ADMIN_URL . 'acc/years/?msg=IMPORT');






	$form->runIf(f('load') && isset($_FILES['file']['tmp_name']), function () use ($type, $csv, $year, $params) {
		$csv->load($_FILES['file']);
		Utils::redirect(Utils::getSelfURI($params));
	}, $csrf_key);
}
else {

	$csv->clear();
	$examples = Transactions::getExportExamples($year);
}


$tpl->assign(compact('csv', 'year', 'csrf_key', 'examples', 'type', 'type_name', 'ignore_ids'));

$tpl->display('acc/years/import.tpl');

Modified src/www/admin/static/styles/03-forms.css from [2fe952f326] to [a6e6244e8a].

415
416
417
418
419
420
421





422
423
424
425
426
427
428
form span.password_check.medium { background-color: #ccf; }
form span.password_check.ok { background-color: #cfc; }

dd.help input[type=text] {
    cursor: pointer;
    font-family: monospace;
}






form p.actions {
    float: right;
}

/** Datepicker widget */
.datepicker-parent {







>
>
>
>
>







415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
form span.password_check.medium { background-color: #ccf; }
form span.password_check.ok { background-color: #cfc; }

dd.help input[type=text] {
    cursor: pointer;
    font-family: monospace;
}

dd.help.example {
    margin-left: 2.5em;
    font-size: .9em;
}

form p.actions {
    float: right;
}

/** Datepicker widget */
.datepicker-parent {