Comment: | Invoice/quotes user templates |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | templates |
Files: | files | file ages | folders |
SHA3-256: |
81047b32dab23db066858fe67295a125 |
User & Date: | bohwaz on 2021-12-20 03:38:18 |
Other Links: | branch diff | manifest | tags |
2021-12-21
| ||
01:29 | Upgrade Skeleton and Web classes to use new skeleton path, also update templates check-in: 1e82585880 user: bohwaz tags: templates | |
2021-12-20
| ||
03:38 | Invoice/quotes user templates check-in: 81047b32da user: bohwaz tags: templates | |
2021-12-19
| ||
21:52 | Improve preview for reçu fiscal check-in: fdc88e11a6 user: bohwaz tags: templates | |
Modified src/include/lib/Garradin/UserTemplate/Document.php from [e58e47225c] to [6d2eaac668].
1 2 3 4 5 6 7 8 9 10 11 | <?php namespace Garradin\UserTemplate; use Garradin\Utils; use Garradin\UserException; use Garradin\Files\Files; use Garradin\Entities\Files\File; use KD2\Brindille_Exception; | > | 1 2 3 4 5 6 7 8 9 10 11 12 | <?php namespace Garradin\UserTemplate; use Garradin\Membres\Session; use Garradin\Utils; use Garradin\UserException; use Garradin\Files\Files; use Garradin\Entities\Files\File; use KD2\Brindille_Exception; |
︙ | ︙ | |||
33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public $config; public string $name; static public function fromURI(string $uri) { return new self(strtok($uri, '/'), strtok('')); } public function __construct(string $context, string $id) { if (!array_key_exists($context, self::CONTEXTS)) { throw new \InvalidArgumentException('Invalid context'); } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | public $config; public string $name; static public function fromURI(string $uri) { return new self(strtok($uri, '/'), strtok('')); } static public function serve(string $uri): void { $session = Session::getInstance(); if (!$session->isLogged()) { http_response_code(403); throw new UserException('Merci de vous connecter pour accéder à ce document.'); } $path = substr($uri, 0, strrpos($uri, '/')); $file = substr($uri, strrpos($uri, '/') + 1) ?: 'index.html'; $doc = self::fromURI($path); try { if (isset($_GET['pdf'])) { $doc->PDF($file); } else { $doc->display($file); } } catch (\InvalidArgumentException $e) { http_response_code(404); throw new UserException('Cette page de document n\'existe pas'); } } public function __construct(string $context, string $id) { if (!array_key_exists($context, self::CONTEXTS)) { throw new \InvalidArgumentException('Invalid context'); } |
︙ | ︙ | |||
57 58 59 60 61 62 63 | throw new UserException(sprintf('Fichier "%s" manquant dans "%s"', self::CONFIG_FILE, $path)); } $config = $f->fetch(); $this->dist = false; } else { | | > > > > > > > | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | throw new UserException(sprintf('Fichier "%s" manquant dans "%s"', self::CONFIG_FILE, $path)); } $config = $f->fetch(); $this->dist = false; } else { $config_path = ROOT . '/skel-dist/' . $path . '/' . self::CONFIG_FILE; $config = @file_get_contents($config_path); if (!$config) { throw new UserException(sprintf('Fichier "%s" manquant dans "skel-dist/%s"', self::CONFIG_FILE, $path)); } $this->dist = true; } $this->config = json_decode($config); if (!isset($this->config->name)) { throw new UserException('Le nom du document n\'est pas défini dans ' . self::CONFIG_FILE); |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Functions.php from [c049449e4f] to [b55043a38e].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\ErrorManager; use Garradin\Config; use Garradin\DB; use Garradin\Template; use Garradin\Utils; use Garradin\UserException; use Garradin\Web\Skeleton; | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin\UserTemplate; use KD2\Brindille; use KD2\Brindille_Exception; use KD2\ErrorManager; use KD2\JSONSchema; use Garradin\Config; use Garradin\DB; use Garradin\Template; use Garradin\Utils; use Garradin\UserException; use Garradin\Web\Skeleton; |
︙ | ︙ | |||
41 42 43 44 45 46 47 | static public function admin_footer(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); return $tpl->fetch('admin/_foot.tpl'); } | | > > > > > > > > > > > > > > > | 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 | static public function admin_footer(array $params): string { $tpl = Template::getInstance(); $tpl->assign($params); return $tpl->fetch('admin/_foot.tpl'); } static public function save(array $params, Brindille $tpl, int $line): void { $id = Utils::basename(Utils::dirname($tpl->_tpl_path)); if (!$id) { throw new Brindille_Exception('Unique document name could not be found'); } if (empty($params['key'])) { throw new Brindille_Exception('Saving key is empty but is mandatory'); } $key = $params['key']; unset($params['key']); if (isset($params['validate_schema'])) { $schema = self::read(['file' => $params['validate_schema']], $tpl, $line); unset($params['validate_schema']); try { $s = JSONSchema::fromString($schema); $s->validate($params); } catch (\RuntimeException $e) { throw new Brindille_Exception(sprintf("line %d: error in validating data:\n%s\n\n%s", $line, $e->getMessage(), json_encode($params, JSON_PRETTY_PRINT))); } } $params = json_encode($params); $db = DB::getInstance(); $db->preparedQuery('REPLACE INTO documents_data (document, key, value) VALUES (?, ?, ?);', $id, $key, $params); } static public function dump(array $params, Brindille $tpl) |
︙ | ︙ | |||
77 78 79 80 81 82 83 84 | return sprintf('<pre style="background: yellow; padding: 5px; overflow: auto">%s</pre>', $dump); } static public function error(array $params, Brindille $tpl) { throw new UserException($params['message']); } | | | | | | > > | | > > > > > > > | | | 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 | return sprintf('<pre style="background: yellow; padding: 5px; overflow: auto">%s</pre>', $dump); } static public function error(array $params, Brindille $tpl) { throw new UserException($params['message']); } static protected function getFilePath(array $params, string $arg_name, UserTemplate $ut, int $line) { if (empty($params[$arg_name])) { throw new Brindille_Exception(sprintf('Ligne %d: argument "%s" manquant pour la fonction "include"', $arg_name, $line)); } if (strpos($params[$arg_name], '..') !== false) { throw new Brindille_Exception(sprintf('Ligne %d: argument "%s" invalide', $line, $arg_name)); } $path = $params[$arg_name]; if (substr($path, 0, 2) == './') { $path = Utils::dirname($ut->_tpl_path) . substr($path, 1); } return $path; } static public function read(array $params, UserTemplate $ut, int $line): string { $path = self::getFilePath($params, 'file', $ut, $line); $file = Files::get(File::CONTEXT_SKELETON . '/' . $path); if ($file) { $content = $file->fetch(); } else { $content = file_get_contents(ROOT . '/skel-dist/' . $path); } if (!empty($params['base64'])) { return base64_encode($content); } return $content; |
︙ | ︙ | |||
121 122 123 124 125 126 127 | } return 'data:image/png;base64,' . base64_encode($file->fetch()); } static public function include(array $params, UserTemplate $ut, int $line): void { | < < < | < < < < < < < | | | | | | 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 | } return 'data:image/png;base64,' . base64_encode($file->fetch()); } static public function include(array $params, UserTemplate $ut, int $line): void { $path = self::getFilePath($params, 'file', $ut, $line); // Avoid recursive loops $from = $ut->get('included_from') ?? []; if (in_array($path, $from)) { throw new Brindille_Exception(sprintf('Ligne %d : boucle infinie d\'inclusion détectée : %s', $line, $path)); } try { $include = new UserTemplate($path); } catch (\InvalidArgumentException $e) { throw new Brindille_Exception(sprintf('Ligne %d : fonction "include" : le fichier à inclure "%s" n\'existe pas', $line, $path)); } $params['included_from'] = array_merge($from, [$path]); $include->assignArray($params); $include->display(); } static public function http(array $params): void { |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/Modifiers.php from [65196e3373] to [2cd6906048].
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php namespace Garradin\UserTemplate; use Garradin\Utils; class Modifiers { const PHP_MODIFIERS_LIST = [ 'strtolower', 'strtoupper', 'ucfirst', 'ucwords', | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin\UserTemplate; use Garradin\Utils; use KD2\Brindille_Exception; class Modifiers { const PHP_MODIFIERS_LIST = [ 'strtolower', 'strtoupper', 'ucfirst', 'ucwords', |
︙ | ︙ | |||
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 | 'strrev', 'strlen', 'strpos', 'strrpos', 'wordwrap', 'strip_tags', 'strlen', ]; const MODIFIERS_LIST = [ 'truncate', 'excerpt', 'protect_contact', 'atom_date', 'xml_escape', 'replace', 'regexp_replace', 'remove_leading_number', 'get_leading_number', 'spell_out_number', ]; const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/'; static public function replace($str, $find, $replace): string { return str_replace($find, $replace, $str); | > > > > > > | 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 | 'strrev', 'strlen', 'strpos', 'strrpos', 'wordwrap', 'strip_tags', 'strlen', 'boolval', 'intval', 'floatval', ]; const MODIFIERS_LIST = [ 'truncate', 'excerpt', 'protect_contact', 'atom_date', 'xml_escape', 'replace', 'regexp_replace', 'remove_leading_number', 'get_leading_number', 'spell_out_number', 'parse_date', 'math', 'money_int' => [Utils::class, 'moneyToInteger'], ]; const LEADING_NUMBER_REGEXP = '/^([\d.]+)\s*[.\)]\s*/'; static public function replace($str, $find, $replace): string { return str_replace($find, $replace, $str); |
︙ | ︙ | |||
142 143 144 145 146 147 148 | return $match[1] ?? null; } static public function spell_out_number($number, string $locale = 'fr_FR'): string { return numfmt_create($locale, \NumberFormatter::SPELLOUT)->format((float) $number); } | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | return $match[1] ?? null; } static public function spell_out_number($number, string $locale = 'fr_FR'): string { return numfmt_create($locale, \NumberFormatter::SPELLOUT)->format((float) $number); } static public function parse_date($value) { if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d'); } if (empty($value) || !is_string($value)) { return null; } if (preg_match('!^\d{2}/\d{2}/\d{2}$!', $value)) { return \DateTime::createFromFormat('!d/m/y', $value)->format('Y-m-d'); } elseif (preg_match('!^\d{2}/\d{2}/\d{4}$!', $value)) { return \DateTime::createFromFormat('!d/m/Y', $value)->format('Y-m-d'); } elseif (preg_match('!^\d{4}-\d{2}-\d{2}$!', $value)) { return $value; } else { return false; } } static public function math($start, ... $params) { $tuples = array_chunk($params, 2); foreach ($tuples as $tuple) { if (count($tuple) !== 2) { continue; } list($sign, $value) = $tuple; if (!is_numeric($value) && !is_null($value)) { throw new Brindille_Exception('Invalid numeric value for math modifier'); } if ($sign == '+') { $start += $value; } elseif ($sign == '-') { $start -= $value; } elseif ($sign == '*') { $start *= $value; } elseif ($sign == '/') { $start /= $value; } else { throw new Brindille_Exception('Invalid math operator, only + - * / are supported'); } } return $start; } } |
Modified src/include/lib/Garradin/UserTemplate/Sections.php from [9f9d5b36d0] to [14c8a701a6].
︙ | ︙ | |||
61 62 63 64 65 66 67 | if (isset($params['key'])) { $params['where'] .= ' AND key = :key'; $params['limit'] = 1; $params[':key'] = $params['key']; unset($params['key']); } | | > | > > > > > > > > | 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 | if (isset($params['key'])) { $params['where'] .= ' AND key = :key'; $params['limit'] = 1; $params[':key'] = $params['key']; unset($params['key']); } $params['select'] = isset($params['select']) ? $params['select'] : 'value AS json'; $params['tables'] = 'documents_data'; foreach (self::sql($params, $tpl, $line) as $row) { if (isset($row['json'])) { $json = json_decode($row['json'], true); if (is_array($json)) { unset($row['json']); $row = array_merge($row, $json); } } yield $row; } } static public function users(array $params, UserTemplate $tpl, int $line): \Generator { if (!array_key_exists('where', $params)) { |
︙ | ︙ |
Modified src/include/lib/Garradin/UserTemplate/UserTemplate.php from [9e3ef3d078] to [c6a815babb].
︙ | ︙ | |||
20 21 22 23 24 25 26 | use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT, ROOT}; class UserTemplate extends \KD2\Brindille { public $_tpl_path; protected $modified; | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | use const Garradin\{WWW_URL, ADMIN_URL, SHARED_USER_TEMPLATES_CACHE_ROOT, USER_TEMPLATES_CACHE_ROOT, DATA_ROOT, ROOT}; class UserTemplate extends \KD2\Brindille { public $_tpl_path; protected $modified; public $file; protected $path; static protected $root_variables; static public function getRootVariables() { if (null !== self::$root_variables) { |
︙ | ︙ | |||
96 97 98 99 100 101 102 | if ($file = Files::get(File::CONTEXT_SKELETON . '/' . $path)) { $this->file = $file; $this->modified = $file->modified->getTimestamp(); } else { $this->path = ROOT . '/skel-dist/' . $path; | | | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | if ($file = Files::get(File::CONTEXT_SKELETON . '/' . $path)) { $this->file = $file; $this->modified = $file->modified->getTimestamp(); } else { $this->path = ROOT . '/skel-dist/' . $path; if (!($this->modified = @filemtime($this->path))) { throw new \InvalidArgumentException('File not found: ' . $this->path); } } $this->assignArray(self::getRootVariables()); $this->registerAll(); |
︙ | ︙ | |||
128 129 130 131 132 133 134 | // PHP modifiers foreach (Modifiers::PHP_MODIFIERS_LIST as $name) { $this->registerModifier($name, $name); } // Local modifiers | | | | 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | // PHP modifiers foreach (Modifiers::PHP_MODIFIERS_LIST as $name) { $this->registerModifier($name, $name); } // Local modifiers foreach (Modifiers::MODIFIERS_LIST as $key => $name) { $this->registerModifier(is_int($key) ? $name : $key, is_int($key) ? [Modifiers::class, $name] : $name); } // Local functions foreach (Functions::FUNCTIONS_LIST as $name) { $this->registerFunction($name, [Functions::class, $name]); } |
︙ | ︙ |
Modified src/include/lib/Garradin/Web/Web.php from [a6bb9dd69f] to [798fd04631].
︙ | ︙ | |||
196 197 198 199 200 201 202 | } elseif (substr($uri, 0, 6) === 'admin/') { http_response_code(404); throw new UserException('Cette page n\'existe pas.'); } elseif (substr($uri, 0, 4) === 'doc/') { $uri = substr($uri, 4); | < < < | < < < < < < | 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | } elseif (substr($uri, 0, 6) === 'admin/') { http_response_code(404); throw new UserException('Cette page n\'existe pas.'); } elseif (substr($uri, 0, 4) === 'doc/') { $uri = substr($uri, 4); Document::serve($uri); return; } elseif (($file = Files::getFromURI($uri)) || ($file = self::getAttachmentFromURI($uri))) { $size = null; if ($file->image) { |
︙ | ︙ |
Added src/skel-dist/transaction/invoice/config.json version [ea7ffd724b].
> > > > > > > | 1 2 3 4 5 6 7 | { "name": "Devis", "pdf": false, "preview": false, "config": "config.html", "standalone": true } |
Added src/skel-dist/transaction/invoice/document.schema.json version [7989292e3e].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "date": { "description": "Date d'émission", "type": "string", "format": "date" }, "date_required": { "description": "Date d'échéance", "type": ["string", "null"], "format": "date" }, "date_paid": { "description": "Date de paiement", "type": ["string", "null"], "format": "date" }, "type": { "description": "Type de document", "type": "string", "enum": ["quote", "invoice"] }, "archived": { "description": "Archivé", "type": "bool" }, "total": { "description": "Montant total", "type": "integer", "minimum": 0 }, "target_user": { "type": ["null", "integer"] }, "target_client": { "type": ["null", "integer"] }, "rows": { "description": "Lignes", "type": "array", "items": { "type": "object", "properties": { "label": { "description": "Désignation de la ligne", "type": "string", "minLength": 1 }, "amount": { "description": "Montant de la ligne", "type": "integer", "minimum": 0 } }, "required": ["label", "amount"] }, "minItems": 1, "maxItems": 100 }, "id_transaction": { "type": ["null", "integer"] } }, "required": [ "type", "date", "date_required", "date_paid", "archived", "total", "rows", "id_transaction"] } |
Added src/skel-dist/transaction/invoice/edit.html version [d26196a637].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | {{if $logged_user.perm_accounting < $access_level.write}} {{:error message="Seuls les membres avec accès en écriture à la comptabilité peuvent générer ce document"}} {{/if}} {{:admin_header title="Document" current="acc"}} {{if $_GET.key}} {{#load key=$_GET.key}} {{:assign .="doc"}} {{/load}} {{/if}} {{if $doc.archived}} {{:error message="Ce document est archivé et ne peut plus être modifié"}} {{/if}} {{if !$doc}} {{#load select="COUNT(*) + 1 AS count" where="key LIKE \'doc_%\'"}} {{:assign last_number=$count}} {{/load}} {{if !$last_number}} {{:assign last_number=1}} {{/if}} {{/if}} {{if $_POST.save}} {{if !$_POST.date|trim|parse_date}} <p class="error block">Date d'émission invalide ou vide.</p> {{elseif $_POST.date_required|trim|parse_date === false}} <p class="error block">Date d'échance invalide ou vide.</p> {{else}} {{:assign number=$_POST.number|trim}} {{if $doc.paid_date}} {{:assign date_paid=$doc.paid_date}} {{elseif $_POST.paid && !$doc.paid}} {{:assign date_paid=$now}} {{else}} {{:assign date_paid=null}} {{/if}} {{if $doc.type}} {{:assign type=$doc.type}} {{else}} {{:assign type=$_POST.type}} {{/if}} {{:assign total=0}} {{#foreach from=$_POST.rows}} {{:assign amount=$value.amount|money_int}} {{:assign total=$total|math:'+':$amount}} {{:assign var="rows[]" label=$value.label|trim amount=$amount}} {{/foreach}} {{:save key="doc_%s"|args:$number validate_schema="./document.schema.json" number=$number date=$_POST.date|parse_date date_paid=$date_paid|parse_date date_required=$date_required|parse_date type=$type archived=$_POST.archived|boolval id_transaction=$doc.id_transaction total=$total rows=$rows }} <p class="block confirm">Document enregistré.</p> {{/if}} {{/if}} <form method="post" action="" id="docForm" data-type="{{$doc.type}}"> <fieldset> <legend>Document</legend> <dl> {{if !$doc}} <dt>Type</dt> {{:input type="radio" name="type" value="quote" label="Devis" default="quote"}} {{:input type="radio" name="type" value="invoice" label="Facture"}} {{/if}} {{:input required=true name="number" type="text" label="Numéro" help="Ce numéro doit être unique." source=$doc data-last-number=$last_number}} {{:input required=true name="date" type="date" label="Date d'émission" source=$doc default=$now}} </dl> <dl class="invoice-only"> {{:input required=false name="date_required" type="date" label="Date d'échéance" source=$doc}} {{:input type="checkbox" name="paid" value="1" label="Facture payée"}} </dl> <dl> {{:input type="checkbox" name="archived" value="1" label="Archivé"}} <dt><label for="f_client">Client</label></dt> <dd> <select name="client" id="f_client"> <option value="user">-- Membre de l'association</option> </select> </dd> </dl> </fieldset> <fieldset> <legend>Lignes</legend> <table class="list"> <thead> <tr> <th>Désignation</th> <td>Montant</td> <td></td> </tr> </thead> <tbody> {{if !$rows && $doc.rows}} {{:assign rows=$doc.rows}} {{/if}} {{if !$rows}} <tr> <th><textarea cols="50" rows="2" name="rows[0][label]" class="full-width"></textarea></th> <td>{{:input type="money" name="rows[0][amount]"}}</td> <td></td> </tr> <tr> <th><textarea cols="50" rows="2" name="rows[1][label]" class="full-width"></textarea></th> <td>{{:input type="money" name="rows[1][amount]"}}</td> <td></td> </tr> {{else}} {{#foreach from=$rows}} <tr> <th><textarea cols="50" rows="2" name="rows[{{$key}}][label]" class="full-width">{{$value.label}}</textarea></th> <td>{{:input type="money" name="rows[%d][amount]"|args:$key default=$value.amount}}</td> <td></td> </tr> {{/foreach}} {{/if}} </tbody> </table> </fieldset> <p class="submit"> {{:button type="submit" name="save" label="Enregistrer" shape="right" class="main"}} </p> </form> <script type="text/javascript"> (function () { const form = $('#docForm'); $('#f_type_invoice, #f_type_quote').forEach((e) => { e.onchange = () => typeChanged(e.value, true); }); function typeChanged(t, change_number) { if (change_number) { let num = t == 'quote' ? 'D' : 'F'; num += $('#f_number').dataset.lastNumber; $('#f_number').value = num; } g.toggle('.invoice-only', t == 'quote' ? false : true); } if (e = $('#f_type_invoice')) { typeChanged(e.selected ? 'invoice' : 'quote', true); } else if (form.dataset.type) { typeChanged(form.dataset.type, false); } })(); </script> {{:admin_footer}} |
Added src/skel-dist/transaction/invoice/index.html version [8f331d0c5a].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | {{if $logged_user.perm_accounting < $access_level.write}} {{:error message="Seuls les membres avec accès en écriture à la comptabilité peuvent générer ce document"}} {{/if}} {{:admin_header title="Devis et factures" current="acc"}} <nav class="tabs"> <aside> {{:linkbutton href="edit.html" label="Nouveau document" shape="plus" target="_dialog"}} </aside> <ul> <li{{if !$_GET.show}} class="current"{{/if}}><a href="./">Tous les documents</a></li> <li{{if $_GET.show == 'quote'}} class="current"{{/if}}><a href="./?show=quote">Devis</a></li> <li{{if $_GET.show == 'unpaid'}} class="current"{{/if}}><a href="./?show=unpaid">Factures en souffrance</a></li> <li{{if $_GET.show == 'paid'}} class="current"{{/if}}><a href="./?show=paid">Factures réglées</a></li> <li><a href="clients.html">Clients</a></li> </ul> </nav> {{if $_GET.show == 'quote'}} {{:assign filter="json_extract(value, '$.type') = 'quote'"}} {{elseif $_GET.show == 'paid'}} {{:assign filter="json_extract(value, '$.type') = 'invoice' AND json_extract(value, '$.date_paid') IS NOT NULL"}} {{elseif $_GET.show == 'unpaid'}} {{:assign filter="json_extract(value, '$.type') = 'invoice' AND json_extract(value, '$.date_paid') IS NULL"}} {{else}} {{:assign filter="1"}} {{/if}} <table class="list"> <thead> <tr> <th>Numéro</th> <td>Émission</td> <td>Échéance</td> <td>Tiers</td> <td class="money">Montant</td> <td>Statut</td> <td></td> </tr> </thead> <tbody> {{#load where="key != 'config' AND %s"|args:$filter select="value AS json, key, datetime(json_extract(value, '$.date')) AS date" order="date DESC"}} <tr> <th>{{$number}}</th> <td>{{$date|date_short}}</td> <td>{{$date_required|date_short}}</td> <td></td> <td class="money">{{$total|raw|money_currency}}</td> <td> {{if $archived}} Archivé {{elseif $date_paid}} Payé le {{$date_paid|date_short}} {{elseif $type == 'invoice'}} En attente {{/if}} </td> <td class="actions"> {{if !$archived}} {{:linkbutton shape="edit" label="Modifier" href="edit.html?key=%s"|args:$key}} {{/if}} </td> </tr> {{else}} <tr><td colspan="6">Aucun document</td></tr> {{/load}} </tbody> </table> {{:admin_footer}} |
Modified src/skel-dist/transaction/recu_fiscal/config.html from [62bd12b9cb] to [3b59a2f4e6].
︙ | ︙ | |||
40 41 42 43 44 45 46 47 48 | {{:input type="checkbox" name="art885" value="1" source=$cerfa_config label="Article 885-0V bis A"}} </dl> </fieldset> <p class="submit"> {{:button type="submit" name="save" label="Enregistrer" shape="right" class="main"}} </p> {{:admin_footer}} | > > | 40 41 42 43 44 45 46 47 48 49 50 | {{:input type="checkbox" name="art885" value="1" source=$cerfa_config label="Article 885-0V bis A"}} </dl> </fieldset> <p class="submit"> {{:button type="submit" name="save" label="Enregistrer" shape="right" class="main"}} </p> </form> {{:admin_footer}} |
Modified src/templates/admin/_head.tpl from [5a9c2123a8] to [73842c1113].
1 2 3 4 5 6 | <?php if (!isset($current)) { $current = null; } ?> <!DOCTYPE html> | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php if (!isset($current)) { $current = null; } ?> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"{if array_key_exists('_dialog', $_GET)} class="dialog"{/if} data-version="{$version_hash}" data-url="{$admin_url}"> <head> <meta charset="utf-8" /> <meta name="v" content="{$version_hash}" /> <title>{$title}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="{$admin_url}static/admin.css?{$version_hash}" media="all" /> <script type="text/javascript" src="{$admin_url}static/scripts/global.js?{$version_hash}"></script> |
︙ | ︙ |
Modified src/www/admin/static/scripts/global.js from [b111eaadcd] to [e05e0a53c7].
1 2 | (function () { window.g = window.garradin = { | > < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | (function () { let d = document.documentElement.dataset; window.g = window.garradin = { admin_url: d.url, static_url: d.url + 'static/', version: d.version, loaded: {} }; window.$ = function(selector) { if (!selector.match(/^[.#]?[a-z0-9_-]+$/i)) { return document.querySelectorAll(selector); |
︙ | ︙ |