diff --git a/.gitignore b/.gitignore
index a356abd5c..978a4f8a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,4 +51,6 @@ js_dist
/compose.override.yml
/composer.phar
/data
+drivers
+chromedriver.log
diff --git a/Makefile b/Makefile
index efb9984ef..139032672 100644
--- a/Makefile
+++ b/Makefile
@@ -106,6 +106,7 @@ test-functional: data config htdocs/uploads tmp
$(DOCKER_COMP) stop dbtest apachephptest mailcatcher
$(DOCKER_COMP) up -d dbtest apachephptest mailcatcher
make clean-test-deprecated-log
+ $(DOCKER_COMP) run --no-deps --rm -u localUser apachephptest ./bin/bdi detect drivers
$(DOCKER_COMP) run --no-deps --rm -u localUser apachephptest ./bin/behat
make var/logs/test.deprecations_grouped.log
$(DOCKER_COMP) stop dbtest apachephptest mailcatcher
diff --git a/app/config/packages/backoffice_menu.yaml b/app/config/packages/backoffice_menu.yaml
index 86a668798..dd4c20040 100644
--- a/app/config/packages/backoffice_menu.yaml
+++ b/app/config/packages/backoffice_menu.yaml
@@ -218,6 +218,8 @@ parameters:
niveau: 'ROLE_ADMIN'
extra_routes:
- admin_accounting_quotations_list
+ - admin_accounting_quotations_add
+ - admin_accounting_quotations_edit
compta_facture:
nom: 'Factures'
url: '/admin/accounting/invoices/list'
diff --git a/app/config/routing/admin_accounting.yml b/app/config/routing/admin_accounting.yml
index 28f5eb33e..181bae4fd 100644
--- a/app/config/routing/admin_accounting.yml
+++ b/app/config/routing/admin_accounting.yml
@@ -14,6 +14,14 @@ admin_accounting_quotations_list:
path: /quotations/list
defaults: {_controller: AppBundle\Controller\Admin\Accounting\Quotation\ListQuotationAction}
+admin_accounting_quotations_add:
+ path: /quotations/add
+ defaults: {_controller: AppBundle\Controller\Admin\Accounting\Quotation\AddQuotationAction}
+
+admin_accounting_quotations_edit:
+ path: /quotations/edit
+ defaults: {_controller: AppBundle\Controller\Admin\Accounting\Quotation\EditQuotationAction}
+
admin_accounting_quotations_download:
path: /quotations/download
defaults: {_controller: AppBundle\Controller\Admin\Accounting\Quotation\DownloadQuotationAction}
diff --git a/behat.php b/behat.php
index adc9b8e58..da6bd3226 100644
--- a/behat.php
+++ b/behat.php
@@ -7,17 +7,53 @@
use Behat\Config\Suite;
use Behat\MinkExtension\Context\MinkContext;
use Behat\MinkExtension\ServiceContainer\MinkExtension;
+use Robertfausk\Behat\PantherExtension\ServiceContainer\PantherExtension;
return (new Config())
->withProfile(
(new Profile('default'))
+ ->withExtension(new Extension(PantherExtension::class))
->withExtension(new Extension(MinkExtension::class, [
'base_url' => 'https://apachephptest:80',
'files_path' => '%paths.base%/tests/behat/files',
- 'browserkit_http' => [
- 'http_client_parameters' => [
- 'verify_peer' => false,
- 'verify_host' => false,
+ 'default_session' => 'browserkit_http',
+ 'javascript_session' => 'panther',
+ 'sessions' => [
+ 'browserkit_http' => [
+ 'browserkit_http' => [
+ 'http_client_parameters' => [
+ 'verify_peer' => false,
+ 'verify_host' => false,
+ ],
+ ],
+ ],
+ 'panther' => [
+ 'panther' => [
+ 'options' => [
+ 'browser' => 'chrome',
+ 'webServerDir' => '%paths.base%/htdocs',
+ 'external_base_uri' => 'https://apachephptest:80',
+ ],
+ 'manager_options' => [
+ 'chromedriver_arguments' => [
+ '--log-path=/var/www/html/chromedriver.log',
+ '--verbose',
+ ],
+ 'capabilities' => [
+ 'goog:chromeOptions' => [
+ 'args' => [
+ '--headless',
+ '--disable-gpu',
+ '--no-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-extensions',
+ '--ignore-certificate-errors',
+ ],
+ ],
+ ],
+ 'external_base_uri' => 'https://apachephptest:80',
+ ],
+ ],
],
],
]))
diff --git a/compose.yml b/compose.yml
index 32903be5e..7dce889bb 100644
--- a/compose.yml
+++ b/compose.yml
@@ -35,6 +35,8 @@ services:
APP_ENV: "dev"
HOST_PWD: ${PWD}
SYMFONY_IDE: "%env(IDE_USED)%://open?url=file://%%f&line=%%l&/var/www/html/>%env(HOST_PWD)%/"
+ PANTHER_NO_SANDBOX: 1
+ PANTHER_CHROME_ARGUMENTS: '--disable-dev-shm-usage --disable-features=IsolateOrigins,site-per-process,TrackingProtection3pcd'
env_file:
.env
volumes:
diff --git a/composer.json b/composer.json
index 6d6e0e5e1..ddc5dd6be 100644
--- a/composer.json
+++ b/composer.json
@@ -139,6 +139,7 @@
"require-dev": {
"behat/behat": "^3.29",
"behat/mink-browserkit-driver": "^2.3",
+ "dbrekelmans/bdi": "^1.4",
"fakerphp/faker": "^1.24",
"friends-of-behat/mink-extension": "^2.7",
"friendsofphp/php-cs-fixer": "^3.75",
@@ -152,6 +153,7 @@
"phpstan/phpstan-webmozart-assert": "^2.0",
"phpunit/phpunit": "11.*",
"rector/rector": "^2.0",
+ "robertfausk/behat-panther-extension": "^1.2",
"smalot/pdfparser": "^0.19.0",
"symfony/debug-bundle": "7.3.*",
"symfony/json-path": "7.3.*",
diff --git a/composer.lock b/composer.lock
index f46142809..852096cfe 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "db1e2f66b63744b8f523154cb74c66ed",
+ "content-hash": "cb93da72155715c4fb1f2f11f59cce03",
"packages": [
{
"name": "algolia/algoliasearch-client-php",
@@ -11198,6 +11198,55 @@
],
"time": "2024-05-06T16:37:16+00:00"
},
+ {
+ "name": "dbrekelmans/bdi",
+ "version": "1.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dbrekelmans/bdi.git",
+ "reference": "c2b77127d7aa3fad25d57575c207b54b108ab300"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dbrekelmans/bdi/zipball/c2b77127d7aa3fad25d57575c207b54b108ab300",
+ "reference": "c2b77127d7aa3fad25d57575c207b54b108ab300",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "php": "^8.1"
+ },
+ "bin": [
+ "bdi",
+ "bdi.phar"
+ ],
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Daniël Brekelmans",
+ "homepage": "https://github.com/dbrekelmans"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/dbrekelmans/bdi/graphs/contributors"
+ }
+ ],
+ "description": "PHAR distribution of dbrekelmans/browser-driver-installer.",
+ "homepage": "https://github.com/dbrekelmans/bdi",
+ "keywords": [
+ "browser-driver-installer"
+ ],
+ "support": {
+ "source": "https://github.com/dbrekelmans/bdi/tree/1.4.1"
+ },
+ "time": "2025-11-03T11:32:28+00:00"
+ },
{
"name": "evenement/evenement",
"version": "v3.0.2",
@@ -12042,6 +12091,72 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
+ {
+ "name": "php-webdriver/webdriver",
+ "version": "1.15.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-webdriver/php-webdriver.git",
+ "reference": "998e499b786805568deaf8cbf06f4044f05d91bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf",
+ "reference": "998e499b786805568deaf8cbf06f4044f05d91bf",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "php": "^7.3 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.12",
+ "symfony/process": "^5.0 || ^6.0 || ^7.0"
+ },
+ "replace": {
+ "facebook/webdriver": "*"
+ },
+ "require-dev": {
+ "ergebnis/composer-normalize": "^2.20.0",
+ "ondram/ci-detector": "^4.0",
+ "php-coveralls/php-coveralls": "^2.4",
+ "php-mock/php-mock-phpunit": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpunit/phpunit": "^9.3",
+ "squizlabs/php_codesniffer": "^3.5",
+ "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0"
+ },
+ "suggest": {
+ "ext-SimpleXML": "For Firefox profile creation"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Exception/TimeoutException.php"
+ ],
+ "psr-4": {
+ "Facebook\\WebDriver\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
+ "homepage": "https://github.com/php-webdriver/php-webdriver",
+ "keywords": [
+ "Chromedriver",
+ "geckodriver",
+ "php",
+ "selenium",
+ "webdriver"
+ ],
+ "support": {
+ "issues": "https://github.com/php-webdriver/php-webdriver/issues",
+ "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2"
+ },
+ "time": "2024-11-21T15:12:59+00:00"
+ },
{
"name": "phpstan/extension-installer",
"version": "1.4.3",
@@ -13417,6 +13532,171 @@
],
"time": "2025-10-11T21:50:23+00:00"
},
+ {
+ "name": "robertfausk/behat-panther-extension",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/robertfausk/behat-panther-extension.git",
+ "reference": "838984a60cd53d950382bee321f8b670c3a5d120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/robertfausk/behat-panther-extension/zipball/838984a60cd53d950382bee321f8b670c3a5d120",
+ "reference": "838984a60cd53d950382bee321f8b670c3a5d120",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.0.5",
+ "friends-of-behat/mink-extension": "^2.3.0",
+ "php": ">=7.2",
+ "robertfausk/mink-panther-driver": "^1.0",
+ "symfony/config": "^3.4|^4.0|^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "g1a/composer-test-scenarios": "^3.0",
+ "matthiasnoback/symfony-config-test": "^4.1|^5.1",
+ "phpunit/phpunit": "~7.5|~9.3",
+ "roave/security-advisories": "dev-master"
+ },
+ "type": "behat-extension",
+ "extra": {
+ "scenarios": {
+ "symfony3": {
+ "require": {
+ "symfony/config": "^3.4"
+ }
+ },
+ "symfony4": {
+ "require": {
+ "symfony/config": "^4.0"
+ }
+ },
+ "symfony5": {
+ "require": {
+ "symfony/config": "^5.0"
+ }
+ },
+ "symfony6": {
+ "require": {
+ "symfony/config": "^6.0"
+ }
+ },
+ "symfony7": {
+ "require": {
+ "symfony/config": "^7.0"
+ }
+ }
+ },
+ "branch-alias": {
+ "dev-main": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Robertfausk\\Behat\\PantherExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Robert Freigang",
+ "email": "robertfreigang@gmx.de"
+ }
+ ],
+ "description": "Symfony Panther extension for Behat",
+ "keywords": [
+ "Behat",
+ "Cucumber",
+ "Panther",
+ "browser",
+ "chrome",
+ "firefox",
+ "gherkin",
+ "gui",
+ "symfony",
+ "test",
+ "web"
+ ],
+ "support": {
+ "issues": "https://github.com/robertfausk/behat-panther-extension/issues",
+ "source": "https://github.com/robertfausk/behat-panther-extension/tree/v1.2.0"
+ },
+ "time": "2025-04-04T10:09:14+00:00"
+ },
+ {
+ "name": "robertfausk/mink-panther-driver",
+ "version": "v1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/robertfausk/mink-panther-driver.git",
+ "reference": "ac95116505015a43af687220a8e00cefabd34dc0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/robertfausk/mink-panther-driver/zipball/ac95116505015a43af687220a8e00cefabd34dc0",
+ "reference": "ac95116505015a43af687220a8e00cefabd34dc0",
+ "shasum": ""
+ },
+ "require": {
+ "behat/mink": "~1.8",
+ "ext-dom": "*",
+ "php": ">=7.2",
+ "symfony/panther": "~0.7|~1.0|~2.0"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1.0",
+ "mink/driver-testsuite": "dev-master",
+ "phpunit/phpunit": "~8.5|~9.3",
+ "symfony/dom-crawler": "~4.0|~5.0|~6.0",
+ "symfony/http-kernel": "~4.0|~5.0|~6.0"
+ },
+ "suggest": {
+ "ext-gd": "*"
+ },
+ "type": "mink-driver",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Mink\\Driver\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Robert Freigang",
+ "email": "robertfreigang@gmx.de"
+ }
+ ],
+ "description": "Symfony Panther driver for Mink framework",
+ "homepage": "http://mink.behat.org/",
+ "keywords": [
+ "Mink",
+ "Panther",
+ "browser",
+ "chrome",
+ "chromium",
+ "firefox",
+ "headless",
+ "symfony",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/robertfausk/mink-panther-driver/issues",
+ "source": "https://github.com/robertfausk/mink-panther-driver/tree/v1.1.2"
+ },
+ "time": "2025-04-03T21:55:18+00:00"
+ },
{
"name": "sebastian/cli-parser",
"version": "3.0.2",
@@ -14865,6 +15145,95 @@
],
"time": "2025-09-27T15:48:31+00:00"
},
+ {
+ "name": "symfony/panther",
+ "version": "v2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/panther.git",
+ "reference": "7d96ff386394ffc02ff320253e7fb6585e3cb76e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/panther/zipball/7d96ff386394ffc02ff320253e7fb6585e3cb76e",
+ "reference": "7d96ff386394ffc02ff320253e7fb6585e3cb76e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "php": ">=8.1",
+ "php-webdriver/webdriver": "^1.8.2",
+ "symfony/browser-kit": "^6.4 || ^7.3 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.3 || ^8.0",
+ "symfony/deprecation-contracts": "^2.4 || ^3",
+ "symfony/dom-crawler": "^6.4 || ^7.3 || ^8.0",
+ "symfony/http-client": "^6.4 || ^7.0",
+ "symfony/http-kernel": "^6.4 || ^7.3 || ^8.0",
+ "symfony/process": "^6.4 || ^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^6.4 || ^7.3 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.3 || ^8.0",
+ "symfony/mime": "^6.4 || ^7.3 || ^8.0",
+ "symfony/phpunit-bridge": ">=7.3.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Panther\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.dev",
+ "homepage": "https://dunglas.dev"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A browser testing and web scraping library for PHP and Symfony.",
+ "homepage": "https://symfony.com/packages/Panther",
+ "keywords": [
+ "e2e",
+ "scraping",
+ "selenium",
+ "symfony",
+ "testing",
+ "webdriver"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/panther/issues",
+ "source": "https://github.com/symfony/panther/tree/v2.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.panthera.org/donate",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/dunglas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/panther",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-21T14:10:51+00:00"
+ },
{
"name": "symfony/process",
"version": "v7.3.4",
diff --git a/docker/dockerfiles/apachephp/Dockerfile b/docker/dockerfiles/apachephp/Dockerfile
index 1a861a74a..b549cd5fc 100644
--- a/docker/dockerfiles/apachephp/Dockerfile
+++ b/docker/dockerfiles/apachephp/Dockerfile
@@ -29,6 +29,7 @@ RUN apt-get update && \
libmcrypt4 \
libicu-dev \
nodejs \
+ chromium \
&& \
docker-php-ext-configure gd --with-freetype --with-jpeg \
&& \
diff --git a/htdocs/templates/administration/compta_facture.html b/htdocs/templates/administration/compta_facture.html
index d6a9d8775..ac6094e6e 100644
--- a/htdocs/templates/administration/compta_facture.html
+++ b/htdocs/templates/administration/compta_facture.html
@@ -106,7 +106,7 @@
Factures
Modifier une facture
diff --git a/sources/AppBundle/Accounting/Form/InvoicingRowType.php b/sources/AppBundle/Accounting/Form/InvoicingRowType.php
new file mode 100644
index 000000000..236273203
--- /dev/null
+++ b/sources/AppBundle/Accounting/Form/InvoicingRowType.php
@@ -0,0 +1,74 @@
+add('reference', TextType::class, [
+ 'label' => 'Référence',
+ 'required' => false,
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Type('string'),
+ new Assert\Length(max: 20),
+ ],
+ ])->add('designation', TextareaType::class, [
+ 'label' => 'Désignation',
+ 'required' => false,
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Type('string'),
+ new Assert\Length(max: 100),
+ ],
+ ])->add('quantity', NumberType::class, [
+ 'label' => 'Quantité',
+ 'required' => false,
+ 'scale' => 2,
+ 'constraints' => [
+ new Assert\NotBlank(),
+ ],
+ ])->add('unitPrice', NumberType::class, [
+ 'label' => 'Prix unitaire HT',
+ 'required' => false,
+ 'scale' => 2,
+ 'constraints' => [
+ new Assert\NotBlank(),
+ ],
+ ])->add('tva', ChoiceType::class, [
+ 'label' => 'Taux de TVA',
+ 'required' => false,
+ 'placeholder' => false,
+ 'choices' => ['Non soumis' => 0, '5.5%' => 5.50, '10%' => 10.00, '20%' => 20.00],
+ 'help' => 'Rappel : sponsoring 20%, place supplémentaire 10%.',
+ 'constraints' => [
+ new Assert\NotBlank(),
+ ],
+ ]);
+ $builder->get('unitPrice')->resetViewTransformers();
+ $builder->get('unitPrice')->addViewTransformer(
+ new MoneyToLocalizedStringTransformer(2, false, null, null, 'en'),
+ );
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => InvoicingDetail::class,
+ ]);
+ }
+}
diff --git a/sources/AppBundle/Accounting/Form/QuotationType.php b/sources/AppBundle/Accounting/Form/QuotationType.php
new file mode 100644
index 000000000..6c823e3a3
--- /dev/null
+++ b/sources/AppBundle/Accounting/Form/QuotationType.php
@@ -0,0 +1,187 @@
+add('quotationDate', DateType::class, [
+ 'label' => 'Date devis',
+ 'required' => true,
+ 'widget' => 'single_text',
+ ])->add('company', TextType::class, [
+ 'label' => 'Société',
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('service', TextType::class, [
+ 'label' => 'Service',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('address', TextareaType::class, [
+ 'label' => 'Adresse',
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ ],
+ ])->add('zipcode', TextType::class, [
+ 'label' => 'Code postal',
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Type('string'),
+ new Assert\Length(max: 10),
+ ],
+ ])->add('city', TextType::class, [
+ 'label' => 'Ville',
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('countryId', ChoiceType::class, [
+ 'label' => 'Pays',
+ 'choices' => array_flip($this->pays->obtenirPays()),
+ ])->add('lastname', TextType::class, [
+ 'label' => 'Nom',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('firstname', TextType::class, [
+ 'label' => 'Prénom',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('phone', TextType::class, [
+ 'label' => 'Tel',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 30),
+ ],
+ ])->add('email', EmailType::class, [
+ 'label' => 'Email (facture)',
+ 'required' => true,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Type('string'),
+ new Assert\Length(max: 100),
+ ],
+ ])->add('tvaIntra', TextType::class, [
+ 'label' => 'TVA intracommunautaire (facture)',
+ 'required' => false,
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 20),
+ ],
+ ])->add('refClt1', TextType::class, [
+ 'label' => 'Référence client',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('refClt2', TextType::class, [
+ 'label' => 'Référence client 2',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('refClt3', TextType::class, [
+ 'label' => 'Référence client 3',
+ 'required' => false,
+ 'empty_data' => '',
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ])->add('observation', TextareaType::class, [
+ 'required' => false,
+ 'empty_data' => '',
+ 'label' => 'Observation',
+ ])->add('currency', EnumType::class, [
+ 'required' => false,
+ 'class' => InvoicingCurrency::class,
+ 'attr' => ['size' => count(InvoicingCurrency::cases())],
+ 'label' => 'Monnaie de la facture',
+ 'placeholder' => false,
+ ])->add('details', CollectionType::class, [
+ 'entry_type' => InvoicingRowType::class,
+ 'keep_as_list' => true,
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'delete_empty' => $this->validate(...) ,
+ ]);
+
+ if ($options['actionType'] === 'edit') {
+ $builder->add('quotationNumber', TextType::class, [
+ 'label' => 'Numéro de devis',
+ 'required' => false,
+ 'constraints' => [
+ new Assert\Type('string'),
+ new Assert\Length(max: 50),
+ ],
+ ]);
+ }
+ }
+
+ private function validate(?InvoicingDetail $detail = null): bool
+ {
+ return null === $detail || (empty($detail->getUnitPrice()) && empty($detail->getQuantity()));
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'actionType' => 'add',
+ ]);
+
+ $resolver->addAllowedTypes('actionType', 'string');
+ }
+
+ public function buildView(FormView $view, FormInterface $form, array $options): void
+ {
+ $view->vars['actionType'] = $options['actionType'];
+ }
+}
diff --git a/sources/AppBundle/Accounting/Model/Invoicing.php b/sources/AppBundle/Accounting/Model/Invoicing.php
index 8f764a6a5..38b519048 100644
--- a/sources/AppBundle/Accounting/Model/Invoicing.php
+++ b/sources/AppBundle/Accounting/Model/Invoicing.php
@@ -19,24 +19,26 @@ class Invoicing implements NotifyPropertyInterface
private ?string $quotationNumber = null;
private ?DateTime $invoiceDate = null;
private ?string $invoiceNumber = null;
- private ?string $company = null;
- private ?string $service = null;
- private ?string $address = null;
- private ?string $zipcode = null;
- private ?string $city = null;
- private ?string $countryId = null;
- private ?string $email = null;
+ private string $company = '';
+ private string $service = '';
+ private string $address = '';
+ private string $zipcode = '';
+ private string $city = '';
+ private string $countryId = '';
+ private string $email = '';
private ?string $tvaIntra = null;
- private ?string $observation = null;
- private ?string $refClt1 = null;
- private ?string $refClt2 = null;
- private ?string $refClt3 = null;
- private ?string $lastname = null;
- private ?string $firstname = null;
- private ?string $phone = null;
+ private string $observation = '';
+ private string $refClt1 = '';
+ private string $refClt2 = '';
+ private string $refClt3 = '';
+ private string $lastname = '';
+ private string $firstname = '';
+ private string $phone = '';
private int $paymentStatus = 0;
private ?DateTime $paymentDate = null;
private ?InvoicingCurrency $currency = null;
+ /** @var InvoicingDetail[] */
+ private array $details = [];
private ?float $price = null;
@@ -104,12 +106,12 @@ public function setInvoiceNumber(?string $invoiceNumber): self
return $this;
}
- public function getCompany(): ?string
+ public function getCompany(): string
{
return $this->company;
}
- public function setCompany(?string $company): self
+ public function setCompany(string $company): self
{
$this->propertyChanged('company', $this->company, $company);
$this->company = $company;
@@ -117,12 +119,12 @@ public function setCompany(?string $company): self
return $this;
}
- public function getService(): ?string
+ public function getService(): string
{
return $this->service;
}
- public function setService(?string $service): self
+ public function setService(string $service): self
{
$this->propertyChanged('service', $this->service, $service);
$this->service = $service;
@@ -130,12 +132,12 @@ public function setService(?string $service): self
return $this;
}
- public function getAddress(): ?string
+ public function getAddress(): string
{
return $this->address;
}
- public function setAddress(?string $address): self
+ public function setAddress(string $address): self
{
$this->propertyChanged('address', $this->address, $address);
$this->address = $address;
@@ -143,12 +145,12 @@ public function setAddress(?string $address): self
return $this;
}
- public function getZipcode(): ?string
+ public function getZipcode(): string
{
return $this->zipcode;
}
- public function setZipcode(?string $zipcode): self
+ public function setZipcode(string $zipcode): self
{
$this->propertyChanged('zipcode', $this->zipcode, $zipcode);
$this->zipcode = $zipcode;
@@ -156,12 +158,12 @@ public function setZipcode(?string $zipcode): self
return $this;
}
- public function getCity(): ?string
+ public function getCity(): string
{
return $this->city;
}
- public function setCity(?string $city): self
+ public function setCity(string $city): self
{
$this->propertyChanged('city', $this->city, $city);
$this->city = $city;
@@ -182,12 +184,12 @@ public function setCountryId(?string $countryId): self
return $this;
}
- public function getEmail(): ?string
+ public function getEmail(): string
{
return $this->email;
}
- public function setEmail(?string $email): self
+ public function setEmail(string $email): self
{
$this->propertyChanged('email', $this->email, $email);
$this->email = $email;
@@ -208,12 +210,12 @@ public function setTvaIntra(?string $tvaIntra): self
return $this;
}
- public function getObservation(): ?string
+ public function getObservation(): string
{
return $this->observation;
}
- public function setObservation(?string $observation): self
+ public function setObservation(string $observation): self
{
$this->propertyChanged('observation', $this->observation, $observation);
$this->observation = $observation;
@@ -221,12 +223,12 @@ public function setObservation(?string $observation): self
return $this;
}
- public function getRefClt1(): ?string
+ public function getRefClt1(): string
{
return $this->refClt1;
}
- public function setRefClt1(?string $refClt1): self
+ public function setRefClt1(string $refClt1): self
{
$this->propertyChanged('refClt1', $this->refClt1, $refClt1);
$this->refClt1 = $refClt1;
@@ -234,12 +236,12 @@ public function setRefClt1(?string $refClt1): self
return $this;
}
- public function getRefClt2(): ?string
+ public function getRefClt2(): string
{
return $this->refClt2;
}
- public function setRefClt2(?string $refClt2): self
+ public function setRefClt2(string $refClt2): self
{
$this->propertyChanged('refClt2', $this->refClt2, $refClt2);
$this->refClt2 = $refClt2;
@@ -247,12 +249,12 @@ public function setRefClt2(?string $refClt2): self
return $this;
}
- public function getRefClt3(): ?string
+ public function getRefClt3(): string
{
return $this->refClt3;
}
- public function setRefClt3(?string $refClt3): self
+ public function setRefClt3(string $refClt3): self
{
$this->propertyChanged('refClt3', $this->refClt3, $refClt3);
$this->refClt3 = $refClt3;
@@ -260,12 +262,12 @@ public function setRefClt3(?string $refClt3): self
return $this;
}
- public function getLastname(): ?string
+ public function getLastname(): string
{
return $this->lastname;
}
- public function setLastname(?string $lastname): self
+ public function setLastname(string $lastname): self
{
$this->propertyChanged('lastname', $this->lastname, $lastname);
$this->lastname = $lastname;
@@ -273,12 +275,12 @@ public function setLastname(?string $lastname): self
return $this;
}
- public function getFirstname(): ?string
+ public function getFirstname(): string
{
return $this->firstname;
}
- public function setFirstname(?string $firstname): self
+ public function setFirstname(string $firstname): self
{
$this->propertyChanged('firstname', $this->firstname, $firstname);
$this->firstname = $firstname;
@@ -286,12 +288,12 @@ public function setFirstname(?string $firstname): self
return $this;
}
- public function getPhone(): ?string
+ public function getPhone(): string
{
return $this->phone;
}
- public function setPhone(?string $phone): self
+ public function setPhone(string $phone): self
{
$this->propertyChanged('phone', $this->phone, $phone);
$this->phone = $phone;
@@ -358,4 +360,21 @@ public function getPaymentUrlRef(): string
return urlencode(Utils::cryptFromText($this->getId()));
}
+
+ /** @return InvoicingDetail[] */
+ public function getDetails(): array
+ {
+ return $this->details;
+ }
+
+ /**
+ * @param array $details
+ */
+ public function setDetails(array $details): self
+ {
+ $this->details = $details;
+
+ return $this;
+ }
+
}
diff --git a/sources/AppBundle/Accounting/Model/InvoicingDetail.php b/sources/AppBundle/Accounting/Model/InvoicingDetail.php
new file mode 100644
index 000000000..8a90a8547
--- /dev/null
+++ b/sources/AppBundle/Accounting/Model/InvoicingDetail.php
@@ -0,0 +1,111 @@
+id;
+ }
+
+ public function setId(int $id): self
+ {
+ $this->propertyChanged('id', $this->id, $id);
+ $this->id = $id;
+ return $this;
+ }
+
+ public function getInvoicingId(): ?int
+ {
+ return $this->invoicingId;
+ }
+
+ public function setInvoicingId(?int $invoicingId): self
+ {
+ $this->propertyChanged('invoicingId', $this->invoicingId, $invoicingId);
+ $this->invoicingId = $invoicingId;
+
+ return $this;
+ }
+
+ public function getReference(): ?string
+ {
+ return $this->reference;
+ }
+
+ public function setReference(?string $ref): self
+ {
+ $this->propertyChanged('reference', $this->reference, $ref);
+ $this->reference = $ref;
+
+ return $this;
+ }
+
+ public function getDesignation(): ?string
+ {
+ return $this->designation;
+ }
+
+ public function setDesignation(?string $designation): self
+ {
+ $this->propertyChanged('designation', $this->designation, $designation);
+ $this->designation = $designation;
+
+ return $this;
+ }
+
+ public function getQuantity(): ?float
+ {
+ return $this->quantity;
+ }
+
+ public function setQuantity(?float $quantity): self
+ {
+ $this->propertyChanged('quantity', $this->quantity, $quantity);
+ $this->quantity = $quantity;
+
+ return $this;
+ }
+
+ public function getUnitPrice(): ?float
+ {
+ return $this->unitPrice;
+ }
+
+ public function setUnitPrice(?float $unitPrice): self
+ {
+ $this->propertyChanged('unitPrice', $this->unitPrice, $unitPrice);
+ $this->unitPrice = $unitPrice;
+
+ return $this;
+ }
+
+ public function getTva(): ?float
+ {
+ return $this->tva;
+ }
+
+ public function setTva(?float $tva): self
+ {
+ $this->propertyChanged('tva', $this->tva, $tva);
+ $this->tva = $tva;
+
+ return $this;
+ }
+}
diff --git a/sources/AppBundle/Accounting/Model/Repository/InvoicingDetailRepository.php b/sources/AppBundle/Accounting/Model/Repository/InvoicingDetailRepository.php
new file mode 100644
index 000000000..61a87dacb
--- /dev/null
+++ b/sources/AppBundle/Accounting/Model/Repository/InvoicingDetailRepository.php
@@ -0,0 +1,101 @@
+
+ */
+class InvoicingDetailRepository extends Repository implements MetadataInitializer
+{
+ public function getRowsIdsPerInvoicingId(int $invoicingId): array
+ {
+ $query = $this->getQuery(
+ 'SELECT id FROM afup_compta_facture_details WHERE idafup_compta_facture = :invoicingId',
+ )->setParams(['invoicingId' => $invoicingId]);
+
+ $result = [];
+ foreach ($query->query($this->getCollection(new HydratorArray())) as $row) {
+ $result[] = $row['id'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param int[] $ids
+ * @return void
+ */
+ public function removeRowsPerIds(array $ids): void
+ {
+ /**
+ * @var DeleteInterface $builder
+ */
+ $builder = $this->getQueryBuilder('delete');
+ $builder->from($this->getMetadata()->getTable())
+ ->where('id IN (:ids)');
+
+ $this->getQuery($builder)->setParams(['ids' => implode(', ', $ids)])->execute();
+ }
+
+ public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = [])
+ {
+ $metadata = new Metadata($serializerFactory);
+
+ $metadata->setEntity(InvoicingDetail::class);
+ $metadata->setConnectionName('main');
+ $metadata->setDatabase($options['database']);
+ $metadata->setTable('afup_compta_facture_details');
+
+ $metadata
+ ->addField([
+ 'columnName' => 'id',
+ 'fieldName' => 'id',
+ 'primary' => true,
+ 'autoincrement' => true,
+ 'type' => 'int',
+ ])
+ ->addField([
+ 'columnName' => 'idafup_compta_facture',
+ 'fieldName' => 'invoicingId',
+ 'type' => 'int',
+ ])
+ ->addField([
+ 'columnName' => 'ref',
+ 'fieldName' => 'reference',
+ 'type' => 'string',
+ ])
+ ->addField([
+ 'columnName' => 'designation',
+ 'fieldName' => 'designation',
+ 'type' => 'string',
+ ])
+ ->addField([
+ 'columnName' => 'quantite',
+ 'fieldName' => 'quantity',
+ 'type' => 'float',
+ ])
+ ->addField([
+ 'columnName' => 'pu',
+ 'fieldName' => 'unitPrice',
+ 'type' => 'float',
+ ])
+ ->addField([
+ 'columnName' => 'tva',
+ 'fieldName' => 'tva',
+ 'type' => 'float',
+ ])
+ ;
+
+ return $metadata;
+ }
+}
diff --git a/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php b/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php
index e371b0c11..2f32109cc 100644
--- a/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php
+++ b/sources/AppBundle/Accounting/Model/Repository/InvoicingRepository.php
@@ -4,6 +4,10 @@
namespace AppBundle\Accounting\Model\Repository;
+use CCMBenchmark\Ting\Repository\Hydrator\AggregateFrom;
+use CCMBenchmark\Ting\Repository\Hydrator\AggregateTo;
+use CCMBenchmark\Ting\Repository\Hydrator\RelationMany;
+use CCMBenchmark\Ting\Repository\HydratorRelational;
use CCMBenchmark\Ting\Serializer\DateTime;
use AppBundle\Accounting\InvoicingCurrency;
use AppBundle\Accounting\Model\Invoicing;
@@ -21,6 +25,34 @@
*/
class InvoicingRepository extends Repository implements MetadataInitializer
{
+ public function getQuotationById(int $periodId): ?Invoicing
+ {
+ /** @var Select $builder */
+ $builder = $this->getQueryBuilder(self::QUERY_SELECT);
+ $builder->cols(['acf.*', 'acfd.*'])
+ ->from('afup_compta_facture acf')
+ ->leftJoin('afup_compta_facture_details acfd', 'acfd.idafup_compta_facture = acf.id')
+ ->where('acf.id = :periodId');
+
+ $hydrator = new HydratorRelational();
+ $hydrator->addRelation(new RelationMany(new AggregateFrom('acfd'), new AggregateTo('acf'), 'setDetails'));
+ $hydrator->callableFinalizeAggregate(fn(array $row) => $row['acf']);
+
+ $collection = $this->getQuery($builder->getStatement())
+ ->setParams(['periodId' => $periodId])
+ ->query($this->getCollection($hydrator));
+
+ if ($collection->count() === 0) {
+ return null;
+ }
+
+ /** @var Invoicing $entity */
+ $entity = $collection->first();
+ $entity->setDetails(array_values($entity->getDetails()));
+
+ return $entity;
+ }
+
public function getQuotationsByPeriodId(?int $periodId = null, string $sort = 'date', string $direction = 'desc'): CollectionInterface
{
$filter = 'acf.date_devis';
diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php
new file mode 100644
index 000000000..3833ca795
--- /dev/null
+++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/AddQuotationAction.php
@@ -0,0 +1,91 @@
+init($request->query->getInt('from'));
+ $form = $this->createForm(QuotationType::class, $quotation);
+ $form->handleRequest($request);
+ if ($form->isSubmitted() && $form->isValid()) {
+ try {
+ $this->invoicingRepository->startTransaction();
+ $quotation->setQuotationNumber($this->facture->genererNumeroDevis());
+ $this->invoicingRepository->save($quotation);
+ foreach ($quotation->getDetails() as $detail) {
+ $detail->setInvoicingId($quotation->getId());
+ $this->invoicingDetailRepository->save($detail);
+ }
+ $this->invoicingRepository->commit();
+ $this->addFlash('success', 'L\'écriture a été ajoutée');
+ return $this->redirectToRoute('admin_accounting_quotations_list');
+ } catch (\Exception $e) {
+ $this->invoicingRepository->rollback();
+ $this->addFlash('error', 'L\'écriture n\'a pas pu être enregistrée');
+ }
+ }
+
+ return $this->render('admin/accounting/quotation/add.html.twig', [
+ 'quotation' => $quotation,
+ 'form' => $form->createView(),
+ 'submitLabel' => 'Ajouter',
+ ]);
+ }
+
+ private function init(int $quotationId)
+ {
+ $baseQuotation = $this->invoicingRepository->get($quotationId);
+ if (!$baseQuotation instanceof Invoicing) {
+ $quotation = new Invoicing();
+ $quotation->setQuotationDate(new \DateTime());
+ $quotation->setCountryId('FR');
+ $quotation->setDetails([new InvoicingDetail()]);
+
+ return $quotation;
+ }
+
+ $quotation = new Invoicing();
+ $quotation->setQuotationDate(new \DateTime());
+ $quotation->setInvoiceDate($baseQuotation->getInvoiceDate());
+ $quotation->setCompany($baseQuotation->getCompany());
+ $quotation->setService($baseQuotation->getService());
+ $quotation->setAddress($baseQuotation->getAddress());
+ $quotation->setZipcode($baseQuotation->getZipcode());
+ $quotation->setCity($baseQuotation->getCity());
+ $quotation->setCountryId($baseQuotation->getCountryId());
+ $quotation->setEmail($baseQuotation->getEmail());
+ $quotation->setTvaIntra($baseQuotation->getTvaIntra());
+ $quotation->setObservation($baseQuotation->getObservation());
+ $quotation->setRefClt1($baseQuotation->getRefClt1());
+ $quotation->setRefClt2($baseQuotation->getRefClt2());
+ $quotation->setRefClt3($baseQuotation->getRefClt3());
+ $quotation->setLastname($baseQuotation->getLastname());
+ $quotation->setFirstname($baseQuotation->getFirstname());
+ $quotation->setPhone($baseQuotation->getPhone());
+ $quotation->setQuotationNumber('');
+ $quotation->setInvoiceNumber('');
+ $quotation->setDetails($baseQuotation->getDetails());
+
+ return $quotation;
+ }
+}
diff --git a/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php b/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php
new file mode 100644
index 000000000..7e7c0f58e
--- /dev/null
+++ b/sources/AppBundle/Controller/Admin/Accounting/Quotation/EditQuotationAction.php
@@ -0,0 +1,66 @@
+query->getInt('quotationId');
+ $quotation = $this->invoicingRepository->getQuotationById($quotationId);
+ if ($quotation === null) {
+ throw new InvalidArgumentException("Ce devis n'existe pas");
+ }
+
+ $form = $this->createForm(QuotationType::class, $quotation, ['actionType' => 'edit']);
+ $form->handleRequest($request);
+ if ($form->isSubmitted() && $form->isValid()) {
+ try {
+ $idsToRemove = $this->invoicingDetailRepository->getRowsIdsPerInvoicingId($quotation->getId());
+ $existingIds = [];
+ $this->invoicingRepository->startTransaction();
+ $this->invoicingRepository->save($quotation);
+ foreach ($quotation->getDetails() as $detail) {
+ if ($detail->getId() !== null) {
+ $existingIds[] = $detail->getId();
+ }
+ $detail->setInvoicingId($quotation->getId());
+ $this->invoicingDetailRepository->save($detail);
+ }
+
+ $idsToRemove = array_diff($idsToRemove, $existingIds);
+ if ($idsToRemove) {
+ $this->invoicingDetailRepository->removeRowsPerIds($idsToRemove);
+ }
+ $this->invoicingRepository->save($quotation);
+ $this->invoicingRepository->commit();
+ $this->addFlash('success', 'L\'écriture a été modifiée');
+ return $this->redirectToRoute('admin_accounting_quotations_list');
+ } catch (\Exception $e) {
+ $this->invoicingRepository->rollback();
+ $this->addFlash('error', 'L\'écriture n\'a pas pu être enregistrée');
+ }
+ }
+
+ return $this->render('admin/accounting/quotation/edit.html.twig', [
+ 'quotation' => $quotation,
+ 'form' => $form->createView(),
+ 'submitLabel' => 'Modifier',
+ ]);
+ }
+}
diff --git a/templates/admin/accounting/quotation/_javascript.html.twig b/templates/admin/accounting/quotation/_javascript.html.twig
new file mode 100644
index 000000000..250c52679
--- /dev/null
+++ b/templates/admin/accounting/quotation/_javascript.html.twig
@@ -0,0 +1,52 @@
+
diff --git a/templates/admin/accounting/quotation/_prototype.html.twig b/templates/admin/accounting/quotation/_prototype.html.twig
new file mode 100644
index 000000000..d819ddff2
--- /dev/null
+++ b/templates/admin/accounting/quotation/_prototype.html.twig
@@ -0,0 +1,15 @@
+
+
+
+
Ligne __reference__
+
+
+{{ form_row(form.details.vars.prototype.reference) }}
+
+
+
Rappel : sponsoring 20%, place supplémentaire 10%.
+
+{{ form_row(form.details.vars.prototype.tva) }}
+{{ form_row(form.details.vars.prototype.designation) }}
+{{ form_row(form.details.vars.prototype.quantity) }}
+{{ form_row(form.details.vars.prototype.unitPrice) }}
diff --git a/templates/admin/accounting/quotation/add.html.twig b/templates/admin/accounting/quotation/add.html.twig
new file mode 100644
index 000000000..c27779081
--- /dev/null
+++ b/templates/admin/accounting/quotation/add.html.twig
@@ -0,0 +1,13 @@
+{% extends 'admin/base_with_header.html.twig' %}
+
+{% block content %}
+ Ajouter un devis
+
+ {% include 'admin/accounting/quotation/form.html.twig' %}
+
+{% endblock %}
+
+{% block javascript %}
+{{ parent() }}
+{% include 'admin/accounting/quotation/_javascript.html.twig' %}
+{% endblock %}
diff --git a/templates/admin/accounting/quotation/edit.html.twig b/templates/admin/accounting/quotation/edit.html.twig
new file mode 100644
index 000000000..fff52d070
--- /dev/null
+++ b/templates/admin/accounting/quotation/edit.html.twig
@@ -0,0 +1,19 @@
+{% extends 'admin/base_with_header.html.twig' %}
+
+{% block content %}
+ Modifier un devis
+
+
+
+ {% include 'admin/accounting/quotation/form.html.twig' %}
+
+{% endblock %}
+
+{% block javascript %}
+{{ parent() }}
+{% include 'admin/accounting/quotation/_javascript.html.twig' %}
+{% endblock %}
diff --git a/templates/admin/accounting/quotation/form.html.twig b/templates/admin/accounting/quotation/form.html.twig
new file mode 100644
index 000000000..968a4f4fd
--- /dev/null
+++ b/templates/admin/accounting/quotation/form.html.twig
@@ -0,0 +1,170 @@
+{% form_theme form 'form_theme_admin.html.twig' %}
+
+{{ form_start(form) }}
+
+
+
+
+
+ {{ form_row(form.quotationDate) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ form_row(form.lastname) }}
+ {{ form_row(form.firstname) }}
+ {{ form_row(form.phone) }}
+ {{ form_row(form.email) }}
+ {{ form_row(form.tvaIntra) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ form_row(form.currency) }}
+
+
+
+
+
+
+ {% set index = form.details|length > 0 ? (form.details|length) : 0 %}
+
+
+
+
+
+
+
+ * indique un champ obligatoire
+
+
+
+{{ form_end(form) }}
+
diff --git a/templates/admin/accounting/quotation/list.html.twig b/templates/admin/accounting/quotation/list.html.twig
index f470fcf96..88eea8512 100644
--- a/templates/admin/accounting/quotation/list.html.twig
+++ b/templates/admin/accounting/quotation/list.html.twig
@@ -4,7 +4,7 @@
Liste des devis