Compare commits

...

19 Commits

Author SHA1 Message Date
982423d766 Merge branch 'feat/pedidos_system' into 'main'
vista de pedidos casi terminada (a falta de acciones delos botones, cambio de...

See merge request jjimenez/erp-imprimelibros!25
2025-12-28 11:25:51 +00:00
5b5ce7ccd7 terminado consultas de pedidos a safekat 2025-12-28 12:01:40 +01:00
61be8d6d3b aceptando ferro 2025-12-27 19:19:17 +01:00
3a00702bb1 trbajando en las funciones de leer los estados 2025-12-27 10:50:09 +01:00
b94a099e01 sistema de pedidos pendientes de pago hechos 2025-12-23 17:41:05 +01:00
d4120bb486 haciendo pagos pendientes 2025-12-22 20:41:21 +01:00
4cc47b4249 añadidos limites cuando lomo interior es menor que 10 2025-12-12 18:27:24 +01:00
cf73801dbe reimpresiones hechas correctamente 2025-12-11 19:46:01 +01:00
3b9f446195 reimpresion a SK 2025-12-01 22:31:40 +01:00
c6e2322132 implementado duplicar en la lista 2025-11-29 23:30:22 +01:00
58fd4815c6 vista de pedidos casi terminada (a falta de acciones delos botones, cambio de estados, etc). Trabajando en el presupuesto para modificar las reimpresiones 2025-11-29 13:42:57 +01:00
9baf880022 terminando pedidos 2025-11-29 00:07:51 +01:00
25a7bcf0b8 trbajando en pedidos 2025-11-28 08:10:25 +01:00
58f0eee5d9 trabajando en la vista del pedido 2025-11-18 21:32:16 +01:00
997741c3c9 trabajando en la vista de peiddos 2025-11-17 22:52:35 +01:00
6ff5250d1b listado de pedidos admin hecho 2025-11-17 10:35:04 +01:00
73676f60b9 trabajando en la vista de pedidos 2025-11-16 22:36:47 +01:00
18a43ea121 trabajando en la vista de pedidos 2025-11-16 22:36:35 +01:00
d19cd1923c añadidas las direcciones de pedido 2025-11-16 21:56:03 +01:00
80 changed files with 4223 additions and 1198 deletions

View File

@ -1,23 +1,23 @@
2025-11-15 08:19:06 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 13129 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:19:06 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:19:10 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 08:19:10 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@ea50607
2025-11-15 08:19:10 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 08:19:11 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:19:11 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:19:11 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Previously run: 51
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Total change sets: 51
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:19:11 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:19:12 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:19:12 INFO [restartedMain] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 08:19:12 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:19:13 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
2025-12-28 11:38:17 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.9 with PID 10066 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-12-28 11:38:17 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-12-28 11:38:22 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-12-28 11:38:22 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6ed6a1c8
2025-12-28 11:38:22 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-12-28 11:38:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-12-28 11:38:23 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-12-28 11:38:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Run: 0
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Previously run: 62
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - -------------------------------
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Total change sets: 62
2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Update summary generated
2025-12-28 11:38:23 INFO [restartedMain] liquibase.command - Command execution complete
2025-12-28 11:38:24 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-12-28 11:38:24 INFO [restartedMain] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.39.Final
2025-12-28 11:38:24 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-12-28 11:38:24 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
@ -25,33 +25,29 @@
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:19:14 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:19:19 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 13.772 seconds (process running for 14.889)
2025-11-15 08:24:35 WARN [http-nio-8080-exec-9] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
2025-11-15 08:24:35 ERROR [http-nio-8080-exec-9] o.h.e.jdbc.spi.SqlExceptionHelper - Duplicate entry '000000' for key 'payment_transactions.uq_tx_gateway_txid'
2025-11-15 08:25:30 WARN [http-nio-8080-exec-8] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
2025-11-15 08:25:30 ERROR [http-nio-8080-exec-8] o.h.e.jdbc.spi.SqlExceptionHelper - Duplicate entry '000000' for key 'payment_transactions.uq_tx_gateway_txid'
2025-11-15 08:29:20 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 08:29:20 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 08:29:20 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 13129 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:29:20 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:29:21 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Starting...
2025-11-15 08:29:21 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-2 - Added connection com.mysql.cj.jdbc.ConnectionImpl@5582ad4
2025-11-15 08:29:21 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Start completed.
2025-11-15 08:29:21 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:29:21 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:29:21 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Previously run: 51
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Total change sets: 51
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:29:21 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:29:22 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:29:22 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:29:22 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
2025-12-28 11:38:26 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-12-28 11:38:31 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 14.361 seconds (process running for 15.893)
2025-12-28 11:54:36 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-12-28 11:54:36 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-12-28 11:54:36 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.9 with PID 10066 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-12-28 11:54:36 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-12-28 11:54:38 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Starting...
2025-12-28 11:54:38 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-2 - Added connection com.mysql.cj.jdbc.ConnectionImpl@4a1a1522
2025-12-28 11:54:38 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Start completed.
2025-12-28 11:54:38 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-12-28 11:54:39 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-12-28 11:54:39 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Run: 0
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Previously run: 62
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - -------------------------------
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Total change sets: 62
2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Update summary generated
2025-12-28 11:54:39 INFO [restartedMain] liquibase.command - Command execution complete
2025-12-28 11:54:39 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-12-28 11:54:39 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-12-28 11:54:39 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-2)']
Database driver: undefined/unknown
Database version: 8.0.43
@ -59,673 +55,5 @@
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:29:22 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:29:23 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 3.113 seconds (process running for 618.77)
2025-11-15 08:29:39 WARN [http-nio-8080-exec-5] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
2025-11-15 08:29:39 ERROR [http-nio-8080-exec-5] o.h.e.jdbc.spi.SqlExceptionHelper - Duplicate entry '091619' for key 'payment_transactions.uq_tx_gateway_txid'
2025-11-15 08:29:53 WARN [http-nio-8080-exec-6] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
2025-11-15 08:29:53 ERROR [http-nio-8080-exec-6] o.h.e.jdbc.spi.SqlExceptionHelper - Duplicate entry '091606' for key 'payment_transactions.uq_tx_gateway_txid'
2025-11-15 08:35:22 INFO [Thread-7] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Shutdown initiated...
2025-11-15 08:35:22 INFO [Thread-7] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Shutdown completed.
2025-11-15 08:35:22 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 13129 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:35:22 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:35:22 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-3 - Starting...
2025-11-15 08:35:22 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-3 - Added connection com.mysql.cj.jdbc.ConnectionImpl@28cd0857
2025-11-15 08:35:22 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-3 - Start completed.
2025-11-15 08:35:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:35:23 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:35:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - Previously run: 51
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - Total change sets: 51
2025-11-15 08:35:23 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:35:23 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:35:23 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:35:23 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:35:23 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-3)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:35:23 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:35:24 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 2.476 seconds (process running for 979.836)
2025-11-15 08:36:03 INFO [Thread-11] com.zaxxer.hikari.HikariDataSource - HikariPool-3 - Shutdown initiated...
2025-11-15 08:36:03 INFO [Thread-11] com.zaxxer.hikari.HikariDataSource - HikariPool-3 - Shutdown completed.
2025-11-15 08:36:03 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 13129 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:36:03 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:36:03 ERROR [restartedMain] o.s.boot.SpringApplication - Application run failed
java.lang.NoClassDefFoundError: List
at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3578) ~[na:na]
at java.base/java.lang.Class.privateGetPublicMethods(Class.java:3603) ~[na:na]
at java.base/java.lang.Class.getMethods(Class.java:2185) ~[na:na]
at org.springframework.data.util.ReactiveWrappers.usesReactiveType(ReactiveWrappers.java:141) ~[spring-data-commons-3.5.5.jar:3.5.5]
at org.springframework.data.repository.core.support.AbstractRepositoryMetadata.isReactiveRepository(AbstractRepositoryMetadata.java:135) ~[spring-data-commons-3.5.5.jar:3.5.5]
at org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport.useRepositoryConfiguration(RepositoryConfigurationExtensionSupport.java:344) ~[spring-data-commons-3.5.5.jar:3.5.5]
at org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport.getRepositoryConfigurations(RepositoryConfigurationExtensionSupport.java:98) ~[spring-data-commons-3.5.5.jar:3.5.5]
at org.springframework.data.repository.config.RepositoryConfigurationDelegate.registerRepositoriesIn(RepositoryConfigurationDelegate.java:170) ~[spring-data-commons-3.5.5.jar:3.5.5]
at org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport.registerBeanDefinitions(AbstractRepositoryConfigurationSourceSupport.java:62) ~[spring-boot-autoconfigure-3.5.7.jar:3.5.7]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:409) ~[spring-context-6.2.12.jar:6.2.12]
at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:986) ~[na:na]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:408) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:148) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:430) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:290) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:349) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:118) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:791) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:609) ~[spring-context-6.2.12.jar:6.2.12]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.5.7.jar:3.5.7]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) ~[spring-boot-3.5.7.jar:3.5.7]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.5.7.jar:3.5.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) ~[spring-boot-3.5.7.jar:3.5.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) ~[spring-boot-3.5.7.jar:3.5.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) ~[spring-boot-3.5.7.jar:3.5.7]
at com.imprimelibros.erp.ErpApplication.main(ErpApplication.java:14) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:50) ~[spring-boot-devtools-3.5.7.jar:3.5.7]
Caused by: java.lang.ClassNotFoundException: List
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na]
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) ~[na:na]
at java.base/java.lang.Class.forName0(Native Method) ~[na:na]
at java.base/java.lang.Class.forName(Class.java:534) ~[na:na]
at java.base/java.lang.Class.forName(Class.java:513) ~[na:na]
at org.springframework.boot.devtools.restart.classloader.RestartClassLoader.loadClass(RestartClassLoader.java:121) ~[spring-boot-devtools-3.5.7.jar:3.5.7]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) ~[na:na]
... 31 common frames omitted
2025-11-15 08:40:59 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 36905 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:40:59 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:41:03 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 08:41:03 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@7d70349b
2025-11-15 08:41:03 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 08:41:04 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:41:04 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:41:04 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - Previously run: 52
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - Total change sets: 52
2025-11-15 08:41:04 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:41:04 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:41:05 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:41:05 INFO [restartedMain] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 08:41:05 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:41:05 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:41:07 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:41:13 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 13.881 seconds (process running for 15.488)
2025-11-15 08:50:21 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 08:50:21 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 08:50:21 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 36905 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:50:21 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:50:22 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Starting...
2025-11-15 08:50:22 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-2 - Added connection com.mysql.cj.jdbc.ConnectionImpl@7a12d5f7
2025-11-15 08:50:22 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Start completed.
2025-11-15 08:50:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:50:23 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:50:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - Previously run: 52
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - Total change sets: 52
2025-11-15 08:50:23 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:50:23 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:50:23 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:50:23 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:50:23 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-2)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:50:24 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:50:25 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 3.873 seconds (process running for 567.976)
2025-11-15 08:50:39 INFO [Thread-7] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Shutdown initiated...
2025-11-15 08:50:39 INFO [Thread-7] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Shutdown completed.
2025-11-15 08:50:40 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 36905 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:50:40 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:50:40 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-3 - Starting...
2025-11-15 08:50:40 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-3 - Added connection com.mysql.cj.jdbc.ConnectionImpl@393c3fa3
2025-11-15 08:50:40 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-3 - Start completed.
2025-11-15 08:50:40 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:50:41 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:50:41 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - Previously run: 52
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - Total change sets: 52
2025-11-15 08:50:41 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:50:41 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:50:41 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:50:41 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:50:41 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-3)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:50:41 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:50:42 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 2.774 seconds (process running for 585.241)
2025-11-15 08:51:28 WARN [http-nio-8080-exec-1] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
2025-11-15 08:51:28 ERROR [http-nio-8080-exec-1] o.h.e.jdbc.spi.SqlExceptionHelper - Duplicate entry '341823' for key 'refunds.uq_refund_gateway_id'
2025-11-15 08:51:57 WARN [http-nio-8080-exec-2] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
2025-11-15 08:51:57 ERROR [http-nio-8080-exec-2] o.h.e.jdbc.spi.SqlExceptionHelper - Duplicate entry '091606' for key 'refunds.uq_refund_gateway_id'
2025-11-15 08:56:05 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Starting ErpApplication using Java 21.0.8 with PID 53621 (/home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros/target/classes started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 08:56:05 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:56:09 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 08:56:09 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@40832d28
2025-11-15 08:56:09 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 08:56:10 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:56:11 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:56:11 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - Previously run: 53
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - Total change sets: 53
2025-11-15 08:56:11 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:56:11 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:56:11 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:56:11 INFO [restartedMain] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 08:56:11 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:56:11 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 08:56:13 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 08:56:19 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 14.185 seconds (process running for 15.876)
2025-11-15 09:22:03 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 81704 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:22:03 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:22:05 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:22:05 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@12214f2f
2025-11-15 09:22:05 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:22:06 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:22:07 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:22:07 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:22:07 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:22:07 INFO [main] liquibase.util - Run: 0
2025-11-15 09:22:07 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:22:07 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:22:07 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:22:07 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:22:07 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:22:07 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:22:07 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:22:07 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:22:07 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:22:08 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:22:10 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:22:15 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.758 seconds (process running for 14.115)
2025-11-15 09:22:22 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:22:22 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:28:43 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 89069 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:28:43 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:28:46 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:28:46 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@7c421952
2025-11-15 09:28:46 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:28:47 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:28:48 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:28:48 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:28:48 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:28:48 INFO [main] liquibase.util - Run: 0
2025-11-15 09:28:48 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:28:48 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:28:48 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:28:48 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:28:48 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:28:48 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:28:48 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:28:48 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:28:48 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:28:48 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:28:50 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:28:56 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.922 seconds (process running for 14.229)
2025-11-15 09:29:00 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:29:00 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:36:40 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 97006 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:36:40 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:36:43 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:36:43 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@7c421952
2025-11-15 09:36:43 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:36:44 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:36:44 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:36:44 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:36:44 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:36:44 INFO [main] liquibase.util - Run: 0
2025-11-15 09:36:44 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:36:44 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:36:44 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:36:44 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:36:44 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:36:44 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:36:44 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:36:44 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:36:44 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:36:45 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:36:47 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:36:52 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.437 seconds (process running for 13.709)
2025-11-15 09:36:55 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:36:55 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:40:43 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 101329 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:40:43 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:40:45 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:40:45 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@17d2b646
2025-11-15 09:40:45 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:40:46 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:40:47 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:40:47 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:40:47 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:40:47 INFO [main] liquibase.util - Run: 0
2025-11-15 09:40:47 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:40:47 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:40:47 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:40:47 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:40:47 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:40:47 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:40:47 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:40:47 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:40:47 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:40:48 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:40:50 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:40:55 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.997 seconds (process running for 14.277)
2025-11-15 09:40:58 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:40:58 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:42:43 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 103589 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:42:43 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:42:45 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:42:46 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6cd2cb2
2025-11-15 09:42:46 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:42:46 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:42:47 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:42:47 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:42:47 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:42:47 INFO [main] liquibase.util - Run: 0
2025-11-15 09:42:47 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:42:47 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:42:47 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:42:47 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:42:47 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:42:47 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:42:47 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:42:47 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:42:47 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:42:47 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:42:49 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:42:55 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.227 seconds (process running for 13.496)
2025-11-15 09:43:09 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:43:09 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:43:16 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 104336 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:43:16 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:43:18 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:43:19 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6f289728
2025-11-15 09:43:19 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:43:20 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:43:20 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:43:20 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:43:20 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:43:20 INFO [main] liquibase.util - Run: 0
2025-11-15 09:43:20 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:43:20 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:43:20 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:43:20 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:43:20 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:43:20 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:43:20 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:43:20 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:43:20 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:43:20 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:43:22 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:43:27 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.406 seconds (process running for 13.785)
2025-11-15 09:44:27 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:44:27 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:44:34 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 105914 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:44:34 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:44:36 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:44:37 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6f289728
2025-11-15 09:44:37 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:44:38 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:44:38 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:44:38 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:44:38 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:44:38 INFO [main] liquibase.util - Run: 0
2025-11-15 09:44:38 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:44:38 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:44:38 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:44:38 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:44:38 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:44:38 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:44:38 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:44:38 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:44:38 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:44:39 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:44:40 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:44:45 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.126 seconds (process running for 13.405)
2025-11-15 09:45:44 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:45:44 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 09:45:50 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 107469 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 09:45:50 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 09:45:52 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 09:45:53 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@33215ffb
2025-11-15 09:45:53 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 09:45:54 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:45:54 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 09:45:54 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 09:45:54 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 09:45:54 INFO [main] liquibase.util - Run: 0
2025-11-15 09:45:54 INFO [main] liquibase.util - Previously run: 53
2025-11-15 09:45:54 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 09:45:54 INFO [main] liquibase.util - -------------------------------
2025-11-15 09:45:54 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 09:45:54 INFO [main] liquibase.util - Update summary generated
2025-11-15 09:45:54 INFO [main] liquibase.command - Command execution complete
2025-11-15 09:45:54 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 09:45:54 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 09:45:54 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 09:45:54 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 09:45:56 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 09:46:01 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.001 seconds (process running for 13.345)
2025-11-15 09:46:17 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 09:46:17 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:04:46 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 126526 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:04:46 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:04:49 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:04:49 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6cd2cb2
2025-11-15 10:04:49 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:04:50 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:04:50 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:04:50 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:04:50 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:04:50 INFO [main] liquibase.util - Run: 0
2025-11-15 10:04:50 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:04:50 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:04:50 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:04:50 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:04:50 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:04:50 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:04:50 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:04:50 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:04:50 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:04:51 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:04:53 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:04:58 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.902 seconds (process running for 14.284)
2025-11-15 10:05:48 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:05:48 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:09:03 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 131295 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:09:03 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:09:06 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:09:06 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@7de6549d
2025-11-15 10:09:06 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:09:07 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:09:07 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:09:07 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:09:07 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:09:07 INFO [main] liquibase.util - Run: 0
2025-11-15 10:09:07 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:09:07 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:09:07 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:09:07 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:09:07 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:09:07 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:09:07 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:09:07 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:09:08 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:09:08 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:09:10 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:09:15 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.299 seconds (process running for 13.566)
2025-11-15 10:10:21 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:10:21 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:10:53 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 133492 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:10:53 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:10:55 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:10:55 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@33215ffb
2025-11-15 10:10:55 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:10:56 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:10:57 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:10:57 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:10:57 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:10:57 INFO [main] liquibase.util - Run: 0
2025-11-15 10:10:57 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:10:57 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:10:57 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:10:57 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:10:57 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:10:57 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:10:57 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:10:57 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:10:57 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:10:57 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:10:59 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:11:05 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.798 seconds (process running for 14.082)
2025-11-15 10:11:36 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:11:36 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:12:13 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 135015 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:12:13 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:12:16 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:12:16 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@7ffcb232
2025-11-15 10:12:16 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:12:18 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:12:18 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:12:18 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:12:18 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:12:18 INFO [main] liquibase.util - Run: 0
2025-11-15 10:12:18 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:12:18 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:12:18 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:12:18 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:12:18 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:12:18 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:12:18 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:12:18 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:12:18 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:12:19 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:12:21 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:12:26 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 13.246 seconds (process running for 14.87)
2025-11-15 10:22:31 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:22:31 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:22:40 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 146328 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:22:40 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:22:43 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:22:43 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6f289728
2025-11-15 10:22:43 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:22:44 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:22:44 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:22:44 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:22:44 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:22:44 INFO [main] liquibase.util - Run: 0
2025-11-15 10:22:44 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:22:44 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:22:44 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:22:44 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:22:44 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:22:44 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:22:44 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:22:44 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:22:44 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:22:45 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:22:47 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:22:52 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.681 seconds (process running for 13.963)
2025-11-15 10:24:16 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:24:16 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:24:21 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 148308 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:24:21 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:24:24 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:24:24 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6cd2cb2
2025-11-15 10:24:24 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:24:25 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:24:25 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:24:25 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:24:25 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:24:25 INFO [main] liquibase.util - Run: 0
2025-11-15 10:24:25 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:24:25 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:24:25 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:24:25 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:24:25 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:24:25 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:24:25 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:24:25 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:24:25 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:24:26 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:24:28 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:24:33 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.138 seconds (process running for 13.409)
2025-11-15 10:24:42 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:24:42 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 10:29:55 INFO [main] c.i.erp.cart.envioCarroTest - Starting envioCarroTest using Java 21.0.8 with PID 154243 (started by jjimenez in /home/jjimenez/DEVELOPMENT/01_PROGRAMMING/erp-imprimelibros)
2025-11-15 10:29:55 INFO [main] c.i.erp.cart.envioCarroTest - The following 1 profile is active: "dev"
2025-11-15 10:29:58 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-11-15 10:29:58 INFO [main] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@33215ffb
2025-11-15 10:29:58 INFO [main] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-11-15 10:29:59 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:29:59 INFO [main] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 10:29:59 INFO [main] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 10:29:59 INFO [main] liquibase.util - UPDATE SUMMARY
2025-11-15 10:29:59 INFO [main] liquibase.util - Run: 0
2025-11-15 10:29:59 INFO [main] liquibase.util - Previously run: 53
2025-11-15 10:29:59 INFO [main] liquibase.util - Filtered out: 0
2025-11-15 10:29:59 INFO [main] liquibase.util - -------------------------------
2025-11-15 10:29:59 INFO [main] liquibase.util - Total change sets: 53
2025-11-15 10:29:59 INFO [main] liquibase.util - Update summary generated
2025-11-15 10:29:59 INFO [main] liquibase.command - Command execution complete
2025-11-15 10:29:59 INFO [main] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 10:29:59 INFO [main] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-15 10:29:59 INFO [main] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 10:30:00 INFO [main] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 8.0.43
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
2025-11-15 10:30:02 INFO [main] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-11-15 10:30:07 INFO [main] c.i.erp.cart.envioCarroTest - Started envioCarroTest in 12.337 seconds (process running for 13.593)
2025-11-15 10:30:11 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2025-11-15 10:30:11 INFO [SpringApplicationShutdownHook] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-12-28 11:54:40 INFO [restartedMain] o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-12-28 11:54:41 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 5.124 seconds (process running for 986.129)

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.7</version>
<version>3.5.9</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.imprimelibros</groupId>

View File

@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -20,41 +21,41 @@ import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
import com.imprimelibros.erp.cart.dto.DireccionShipment;
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.common.email.EmailService;
import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.pedidos.PedidoRepository;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
@Service
public class CartService {
private final EmailService emailService;
private final CartRepository cartRepo;
private final CartDireccionRepository cartDireccionRepo;
private final CartItemRepository itemRepo;
private final MessageSource messageSource;
private final PresupuestoRepository presupuestoRepo;
private final Utils utils;
private final DireccionService direccionService;
private final skApiClient skApiClient;
private final PedidoService pedidoService;
private final PresupuestoService presupuestoService;
private final PedidoRepository pedidoRepository;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
CartDireccionRepository cartDireccionRepo, MessageSource messageSource,
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo,
Utils utils, DireccionService direccionService, skApiClient skApiClient,
PedidoService pedidoService, PresupuestoService presupuestoService) {
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PedidoRepository pedidoRepository,
DireccionService direccionService, skApiClient skApiClient,PresupuestoService presupuestoService, EmailService emailService) {
this.cartRepo = cartRepo;
this.itemRepo = itemRepo;
this.cartDireccionRepo = cartDireccionRepo;
this.messageSource = messageSource;
this.presupuestoRepo = presupuestoRepo;
this.utils = utils;
this.direccionService = direccionService;
this.skApiClient = skApiClient;
this.pedidoService = pedidoService;
this.presupuestoService = presupuestoService;
this.emailService = emailService;
this.pedidoRepository = pedidoRepository;
}
public Cart findById(Long cartId) {
@ -89,7 +90,7 @@ public class CartService {
Presupuesto p = item.getPresupuesto();
Map<String, Object> elemento = getElementoCart(p, locale);
Map<String, Object> elemento = presupuestoService.getPresupuestoInfoForCard(p, locale);
elemento.put("cartItemId", item.getId());
resultados.add(elemento);
}
@ -159,38 +160,6 @@ public class CartService {
return itemRepo.findByCartId(cart.getId()).size();
}
private Map<String, Object> getElementoCart(Presupuesto presupuesto, Locale locale) {
Map<String, Object> resumen = new HashMap<>();
resumen.put("titulo", presupuesto.getTitulo());
resumen.put("imagen",
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
resumen.put("imagen_alt",
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
resumen.put("presupuestoId", presupuesto.getId());
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
resumen.put("hasSample", true);
} else {
resumen.put("hasSample", false);
}
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
resumen.put("tirada", presupuesto.getSelectedTirada());
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
resumen.put("base", presupuesto.getBaseImponible());
resumen.put("iva4", presupuesto.getIvaImporte4());
resumen.put("iva21", presupuesto.getIvaImporte21());
resumen.put("resumen", detalles);
return resumen;
}
public Map<String, Object> getCartSummaryRaw(Cart cart, Locale locale) {
double base = 0.0;
@ -298,7 +267,7 @@ public class CartService {
}
double totalBeforeDiscount = base + iva4 + iva21 + shipment;
int fidelizacion = pedidoService.getDescuentoFidelizacion();
int fidelizacion = this.getDescuentoFidelizacion(cart.getUserId());
double descuento = totalBeforeDiscount * fidelizacion / 100.0;
double total = totalBeforeDiscount - descuento;
@ -325,6 +294,27 @@ public class CartService {
return summary;
}
public int getDescuentoFidelizacion(Long userId) {
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
// ultimo año)
Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60);
double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno);
if (totalGastado < 1200) {
return 0;
} else if (totalGastado >= 1200 && totalGastado < 1999) {
return 1;
} else if (totalGastado >= 2000 && totalGastado < 2999) {
return 2;
} else if (totalGastado >= 3000 && totalGastado < 3999) {
return 3;
} else if (totalGastado >= 4000 && totalGastado < 4999) {
return 4;
} else if (totalGastado >= 5000) {
return 5;
}
return 0;
}
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
Map<String, Object> raw = getCartSummaryRaw(cart, locale);
@ -445,175 +435,6 @@ public class CartService {
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
}
@Transactional
public Long crearPedido(Long cartId, Locale locale) {
Cart cart = this.getCartById(cartId);
List<CartItem> items = cart.getItems();
List<Map<String, Object>> presupuestoRequests = new ArrayList<>();
List<Long> presupuestoIds = new ArrayList<>();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
Presupuesto pCart = item.getPresupuesto();
// Asegurarnos de trabajar con la entidad gestionada por JPA
Presupuesto p = presupuestoRepo.findById(pCart.getId())
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(p, true);
data_to_send.put("createPedido", 0);
// Recuperar el mapa anidado datosCabecera
@SuppressWarnings("unchecked")
Map<String, Object> datosCabecera = (Map<String, Object>) data_to_send.get("datosCabecera");
if (datosCabecera != null) {
Object tituloOriginal = datosCabecera.get("titulo");
datosCabecera.put(
"titulo",
"[" + (i + 1) + "/" + items.size() + "] " + (tituloOriginal != null ? tituloOriginal : ""));
}
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
data_to_send.put("direcciones", direcciones_presupuesto.get("direcciones"));
data_to_send.put("direccionesFP1", direcciones_presupuesto.get("direccionesFP1"));
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
if (result.containsKey("error")) {
System.out.println("Error al guardar presupuesto en SK");
System.out.println("-------------------------");
System.out.println(result.get("error"));
// decide si seguir con otros items o abortar:
// continue; o bien throw ...
continue;
}
Object dataObj = result.get("data");
if (!(dataObj instanceof Map<?, ?> dataRaw)) {
System.out.println("Formato inesperado de 'data' en savePresupuesto: " + result);
continue;
}
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) dataRaw;
Long presId = ((Number) dataMap.get("id")).longValue();
String skin = ((String) dataMap.get("iskn")).toString();
p.setProveedor("Safekat");
p.setProveedorRef1(skin);
p.setProveedorRef2(presId);
p.setEstado(Presupuesto.Estado.aceptado);
presupuestoRepo.save(p);
presupuestoIds.add(p.getId());
presupuestoRequests.add(dataMap);
}
// Crear el pedido en base a los presupuestos guardados
if (presupuestoRequests.isEmpty()) {
throw new IllegalStateException("No se pudieron guardar los presupuestos en SK.");
} else {
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
for (Map<String, Object> presData : presupuestoRequests) {
Long presId = ((Number) presData.get("id")).longValue();
presupuestoSkIds.add(presId);
}
Map<String, Object> ids = new HashMap<>();
ids.put("presupuesto_ids", presupuestoSkIds);
Long pedidoId = skApiClient.crearPedido(ids);
if (pedidoId == null) {
throw new IllegalStateException("No se pudo crear el pedido en SK.");
}
Pedido pedidoInterno = pedidoService.crearPedido(presupuestoIds, this.getCartSummaryRaw(cart, locale),
"Safekat", String.valueOf(pedidoId), cart.getUserId());
return pedidoInterno.getId();
}
}
public Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
if (cart.getOnlyOneShipment()) {
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
if (!direcciones.isEmpty()) {
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada()-4,
presupuesto.getPeso(),
direcciones.get(0).getIsPalets(),
false));
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
}
else {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada(),
presupuesto.getPeso(),
direcciones.get(0).getIsPalets(),
false));
}
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
direccionesPrueba.add(direcciones.get(0).toSkMap(
1,
presupuesto.getPeso(),
false,
true));
}
Map<String, Object> direccionesRet = new HashMap<>();
direccionesRet.put("direcciones", direccionesPresupuesto);
if (!direccionesPrueba.isEmpty())
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
else {
direccionesRet.put("direccionesFP1", new ArrayList<>());
}
return direccionesRet;
}
} else {
List<CartDireccion> direcciones = cart.getDirecciones().stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
.toList();
for (CartDireccion cd : direcciones) {
// direccion de ejemplar de prueba
if (cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
continue;
}
if (cd.getUnidades() == null || cd.getUnidades() <= 0) {
direccionesPrueba.add(cd.toSkMap(
1,
presupuesto.getPeso(),
false,
true));
} else {
direccionesPresupuesto.add(cd.toSkMap(
cd.getUnidades(),
presupuesto.getPeso(),
cd.getIsPalets(),
false));
}
}
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
CartDireccion cd = new CartDireccion();
direccionesPresupuesto.add(cd.toSkMapDepositoLegal());
}
}
Map<String, Object> direccionesRet = new HashMap<>();
direccionesRet.put("direcciones", direccionesPresupuesto);
if (!direccionesPrueba.isEmpty())
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
else {
direccionesRet.put("direccionesFP1", new ArrayList<>());
}
return direccionesRet;
}
/***************************************
* MÉTODOS PRIVADOS

View File

@ -4,7 +4,9 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.Principal;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
@ -12,6 +14,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import org.springframework.context.MessageSource;
@ -357,4 +360,62 @@ public class Utils {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale);
return dateTime.format(formatter);
}
public static String formatDate(LocalDateTime dateTime, Locale locale) {
if (dateTime == null) {
return "";
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy", locale);
return dateTime.format(formatter);
}
public static String formatInstant(Instant instant, Locale locale) {
if (instant == null) {
return "";
}
ZoneId zone = zoneIdForLocale(locale);
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("dd/MM/yyyy HH:mm", locale)
.withZone(zone);
return formatter.format(instant);
}
/*********************
* Metodos auxiliares
*/
private static ZoneId zoneIdForLocale(Locale locale) {
if (locale == null || locale.getCountry().isEmpty()) {
return ZoneId.of("UTC");
}
// Buscar timezones cuyo ID termine con el country code
// Ej: ES -> Europe/Madrid
String country = locale.getCountry();
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for (String id : zoneIds) {
// TimeZone# getID() no funciona por país, pero sí el prefijo + país
if (id.endsWith("/" + country) || id.contains("/" + country)) {
return ZoneId.of(id);
}
}
// fallback por regiones comunes (manual pero muy útil)
Map<String, String> fallback = Map.of(
"ES", "Europe/Madrid",
"MX", "America/Mexico_City",
"AR", "America/Argentina/Buenos_Aires",
"US", "America/New_York",
"GB", "Europe/London",
"FR", "Europe/Paris");
if (fallback.containsKey(country)) {
return ZoneId.of(fallback.get(country));
}
return ZoneId.systemDefault(); // último fallback
}
}

View File

@ -229,6 +229,7 @@ public class skApiClient {
Long id = ((Integer) responseBody.get("id")).longValue();
if (success != null && id != null && success) {
return Map.of("data", id);
} else {
// Tu lógica actual: si success es true u otra cosa → error 2
@ -247,7 +248,7 @@ public class skApiClient {
return (Long) result.get("data");
}
public Integer getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
public Map<String, Object> getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
try {
String jsonResponse = performWithRetry(() -> {
String url = this.skApiUrl + "api/calcular-solapas";
@ -288,7 +289,11 @@ public class skApiClient {
messageSource.getMessage("presupuesto.errores.error-interior", new Object[] { 1 }, locale));
}
return root.get("data").asInt();
Integer maxSolapas = root.get("data").asInt();
Double lomo = root.get("lomo").asDouble();
return Map.of(
"maxSolapas", maxSolapas,
"lomo", lomo);
} catch (JsonProcessingException e) {
// Fallback al 80% del ancho
@ -301,7 +306,9 @@ public class skApiClient {
throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody);
else {
int ancho = (int) tamanio.get("ancho");
return (int) Math.floor(ancho * 0.8); // 80% del ancho
return Map.of(
"maxSolapas", (int) (ancho * 0.8),
"lomo", 0.0);
}
}
}
@ -388,6 +395,187 @@ public class skApiClient {
}
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
try {
String jsonResponse = performWithRetry(() -> {
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(authService.getToken()); // token actualizado
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class);
return response.getBody();
});
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonResponse);
if (root.get("data") == null) {
throw new RuntimeException(
"Sin respuesta desde el servidor del proveedor");
}
String estado = root.get("data").asText();
return Map.of(
"estado", estado);
} catch (JsonProcessingException e) {
// Fallback al 80% del ancho
return Map.of(
"estado", null);
}
}
public Map<String, Object> getFilesTypes(Long presupuestoId, Locale locale) {
try {
Map<String, Object> result = performWithRetryMap(() -> {
String url = this.skApiUrl + "api/files-presupuesto/" + presupuestoId;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(authService.getToken()); // token actualizado
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class);
ObjectMapper mapper = new ObjectMapper();
try {
Map<String, Object> responseBody = mapper.readValue(
response.getBody(),
new TypeReference<Map<String, Object>>() {
});
// Si la API devuelve "error" a nivel raíz
if (responseBody.get("error") != null) {
// Devolvemos un mapa con sólo el error para que el caller decida
return Map.of("error", responseBody.get("error"));
}
Boolean hasError = (Boolean) (responseBody.get("error") == null
|| responseBody.get("error") == "null" ? false : true);
Map<String, Boolean> files = (Map<String, Boolean>) responseBody.get("data");
if (files != null && !hasError) {
return Map.of("data", files);
} else {
// Tu lógica actual: si success es true u otra cosa → error 2
return Map.of("error", 2);
}
} catch (JsonProcessingException e) {
e.printStackTrace();
return Map.of("error", 1);
}
});
if (result.get("error") != null) {
throw new RuntimeException(
messageSource.getMessage("pedido.errors.connecting-server-error", null, locale));
}
Map<String, Object> data = (Map<String, Object>) result.get("data");
return data;
} catch (RuntimeException e) {
throw new RuntimeException(
messageSource.getMessage("pedido.errors.connecting-server-error", null, locale));
}
}
public byte[] downloadFile(Long presupuestoId, String fileType, Locale locale) {
return performWithRetryBytes(() -> {
String normalized = (fileType == null) ? "" : fileType.trim().toLowerCase();
String endpoint = switch (normalized) {
case "ferro" -> "api/get-ferro/" + presupuestoId;
case "cubierta" -> "api/get-cubierta/" + presupuestoId;
case "tapa" -> "api/get-tapa/" + presupuestoId;
default -> throw new IllegalArgumentException("Tipo de fichero no soportado: " + fileType);
};
// OJO: skApiUrl debería terminar en "/" para que concatene bien
String url = this.skApiUrl + endpoint;
HttpHeaders headers = new HttpHeaders();
// Si tu CI4 requiere Bearer, mantenlo. Si NO lo requiere, puedes quitar esta
// línea.
headers.setBearerAuth(authService.getToken());
headers.setAccept(List.of(MediaType.APPLICATION_PDF, MediaType.APPLICATION_OCTET_STREAM));
try {
ResponseEntity<byte[]> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(headers),
byte[].class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody(); // bytes del PDF
}
return null;
} catch (HttpClientErrorException.NotFound e) {
// CI4 no tiene ese fichero
return null;
}
});
}
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
String result = performWithRetry(() -> {
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(authService.getToken());
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
entity,
String.class);
try {
Map<String, Object> responseBody = new ObjectMapper().readValue(
response.getBody(),
new TypeReference<Map<String, Object>>() {
});
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
return success.toString();
} catch (JsonProcessingException e) {
e.printStackTrace();
return "false"; // Fallback en caso de error
}
});
return Boolean.parseBoolean(result);
}
/******************
* PRIVATE METHODS
******************/
@ -419,6 +607,19 @@ public class skApiClient {
}
}
private byte[] performWithRetryBytes(Supplier<byte[]> request) {
try {
return request.get();
} catch (HttpClientErrorException.Unauthorized e) {
authService.invalidateToken();
try {
return request.get();
} catch (HttpClientErrorException ex) {
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
}
}
}
private static BigDecimal calcularMargen(
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
BigDecimal margenMax, BigDecimal margenMin) {

View File

@ -69,4 +69,18 @@ public class PaisesService {
}
}
public String getPaisNombrePorCode3(String code3, Locale locale) {
if (code3 == null || code3.isEmpty()) {
return "";
}
Optional<Paises> opt = repo.findByCode3(code3);
if (opt.isPresent()) {
Paises pais = opt.get();
String key = pais.getKeyword();
return messageSource.getMessage("paises." + key, null, key, locale);
} else {
return "";
}
}
}

View File

@ -16,6 +16,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser;
@ -98,7 +100,7 @@ public class PaymentController {
Specification<PaymentTransaction> base = Specification.allOf(
(root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded));
base = base.and((root, query, cb) -> cb.equal(root.get("type"), PaymentTransactionType.CAPTURE));
base = base.and((root, query, cb) -> cb.notEqual(root.join("payment").get("gateway"), "bank_transfer"));
String clientSearch = dt.getColumnSearch("client");
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
@ -229,10 +231,12 @@ public class PaymentController {
})
.add("transfer_id", pago -> {
if (pago.getPayment() != null) {
return "TRANSF-" + pago.getPayment().getOrderId();
} else {
return "";
Long pedido = pago.getPayment().getOrderId();
if (pedido != null) {
return "TRANSF-" + pedido;
}
}
return "";
})
.add("order_id", pago -> {
if (pago.getStatus() != PaymentTransactionStatus.pending) {

View File

@ -13,9 +13,13 @@ import com.imprimelibros.erp.redsys.RedsysService.RedsysNotification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoLinea;
import com.imprimelibros.erp.pedidos.PedidoService;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@Service
@ -28,18 +32,61 @@ public class PaymentService {
private final WebhookEventRepository webhookEventRepo;
private final ObjectMapper om = new ObjectMapper();
private final CartService cartService;
private final PedidoService pedidoService;
public PaymentService(PaymentRepository payRepo,
PaymentTransactionRepository txRepo,
RefundRepository refundRepo,
RedsysService redsysService,
WebhookEventRepository webhookEventRepo, CartService cartService) {
WebhookEventRepository webhookEventRepo,
CartService cartService,
PedidoService pedidoService) {
this.payRepo = payRepo;
this.txRepo = txRepo;
this.refundRepo = refundRepo;
this.redsysService = redsysService;
this.webhookEventRepo = webhookEventRepo;
this.cartService = cartService;
this.pedidoService = pedidoService;
}
public Payment findFailedPaymentByOrderId(Long orderId) {
return payRepo.findFirstByOrderIdAndStatusOrderByIdDesc(orderId, PaymentStatus.failed)
.orElse(null);
}
public Map<String, Long> getPaymentTransactionData(Long paymentId) {
PaymentTransaction tx = txRepo.findByPaymentIdAndType(
paymentId,
PaymentTransactionType.CAPTURE)
.orElse(null);
if (tx == null) {
return null;
}
String resp_payload = tx.getResponsePayload();
try {
ObjectMapper om = new ObjectMapper();
var node = om.readTree(resp_payload);
Long cartId = null;
Long dirFactId = null;
if (node.has("Ds_MerchantData")) {
// format: "Ds_MerchantData": "{&#34;dirFactId&#34;:3,&#34;cartId&#34;:90}"
String merchantData = node.get("Ds_MerchantData").asText();
merchantData = merchantData.replace("&#34;", "\"");
var mdNode = om.readTree(merchantData);
if (mdNode.has("cartId")) {
cartId = mdNode.get("cartId").asLong();
}
if (mdNode.has("dirFactId")) {
dirFactId = mdNode.get("dirFactId").asLong();
}
}
return Map.of(
"cartId", cartId,
"dirFactId", dirFactId);
} catch (Exception e) {
return null;
}
}
/**
@ -47,14 +94,15 @@ public class PaymentService {
* oficial (ApiMacSha256).
*/
@Transactional
public FormPayload createRedsysPayment(Long cartId, long amountCents, String currency, String method)
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method, Long orderId)
throws Exception {
Payment p = new Payment();
p.setOrderId(null);
p.setOrderId(orderId);
Cart cart = this.cartService.findById(cartId);
if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId());
this.cartService.lockCartById(cartId);
}
p.setCurrency(currency);
p.setAmountTotalCents(amountCents);
@ -62,10 +110,6 @@ public class PaymentService {
p.setStatus(PaymentStatus.requires_payment_method);
p = payRepo.saveAndFlush(p);
// ANTES:
// String dsOrder = String.format("%012d", p.getId());
// AHORA: timestamp
long now = System.currentTimeMillis();
String dsOrder = String.format("%012d", now % 1_000_000_000_000L);
@ -73,7 +117,7 @@ public class PaymentService {
payRepo.save(p);
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
"Compra en Imprimelibros", cartId);
"Compra en Imprimelibros", cartId, dirFactId);
if ("bizum".equalsIgnoreCase(method)) {
return redsysService.buildRedirectFormBizum(req);
@ -207,13 +251,12 @@ public class PaymentService {
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
p.setAuthorizedAt(LocalDateTime.now());
p.setCapturedAt(LocalDateTime.now());
pedidoService.setOrderAsPaid(p.getOrderId());
} else {
p.setStatus(PaymentStatus.failed);
p.setFailedAt(LocalDateTime.now());
}
if (authorized) {
processOrder(notif.cartId, locale);
pedidoService.markPedidoAsPaymentDenied(p.getOrderId());
}
payRepo.save(p);
@ -308,15 +351,13 @@ public class PaymentService {
}
@Transactional
public Payment createBankTransferPayment(Long cartId, long amountCents, String currency) {
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency, Locale locale, Long orderId) {
Payment p = new Payment();
p.setOrderId(null);
Cart cart = this.cartService.findById(cartId);
if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId());
// En el orderId de la transferencia pendiente guardamos el ID del carrito
p.setOrderId(cartId);
// Se bloquea el carrito para evitar modificaciones mientras se procesa el pago
this.cartService.lockCartById(cartId);
}
@ -325,6 +366,9 @@ public class PaymentService {
p.setAmountTotalCents(amountCents);
p.setGateway("bank_transfer");
p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso
if (orderId != null) {
p.setOrderId(orderId);
}
p = payRepo.save(p);
// Crear transacción pendiente
@ -334,6 +378,18 @@ public class PaymentService {
tx.setStatus(PaymentTransactionStatus.pending);
tx.setAmountCents(amountCents);
tx.setCurrency(currency);
String payload = "";
if (cartId != null) {
payload = "{\"cartId\":" + cartId + "}";
}
if (dirFactId != null) {
if (!payload.isEmpty()) {
payload = payload.substring(0, payload.length() - 1) + ",\"dirFactId\":" + dirFactId + "}";
} else {
payload = "{\"dirFactId\":" + dirFactId + "}";
}
}
tx.setResponsePayload(payload);
// tx.setProcessedAt(null); // la dejas nula hasta que se confirme
txRepo.save(tx);
@ -374,12 +430,37 @@ public class PaymentService {
p.setAmountCapturedCents(p.getAmountTotalCents());
p.setCapturedAt(LocalDateTime.now());
p.setStatus(PaymentStatus.captured);
payRepo.save(p);
// 4) Procesar el pedido asociado al carrito (si existe)
if (p.getOrderId() != null) {
processOrder(p.getOrderId(), locale);
Long cartId = null;
Long dirFactId = null;
try {
// Intentar extraer cartId del payload de la transacción
if (tx.getResponsePayload() != null && !tx.getResponsePayload().isBlank()) {
ObjectMapper om = new ObjectMapper();
var node = om.readTree(tx.getResponsePayload());
if (node.has("cartId")) {
cartId = node.get("cartId").asLong();
}
if (node.has("dirFactId")) {
dirFactId = node.get("dirFactId").asLong();
}
}
} catch (Exception e) {
// ignorar
}
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
if(p.getOrderId() != null) {
pedidoService.setOrderAsPaid(p.getOrderId());
}
/*else if (cartId != null) {
// Se procesa el pedido dejando el estado calculado en processOrder
Long orderId = processOrder(cartId, dirFactId, locale, null);
if (orderId != null) {
p.setOrderId(orderId);
}
}*/
payRepo.save(p);
}
/**
@ -474,29 +555,5 @@ public class PaymentService {
return code >= 0 && code <= 99;
}
/**
* Procesa el pedido asociado al carrito:
* - bloquea el carrito
* - crea el pedido a partir del carrito
*
*/
@Transactional
private Boolean processOrder(Long cartId, Locale locale) {
Cart cart = this.cartService.findById(cartId);
if (cart != null) {
// Bloqueamos el carrito
this.cartService.lockCartById(cart.getId());
// Creamos el pedido
Long orderId = this.cartService.crearPedido(cart.getId(), locale);
if (orderId == null) {
return false;
} else {
// envio de correo de confirmacion de pedido podria ir aqui
}
}
return true;
}
}

View File

@ -2,10 +2,13 @@
package com.imprimelibros.erp.payments.repo;
import com.imprimelibros.erp.payments.model.Payment;
import com.imprimelibros.erp.payments.model.PaymentStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByGatewayAndGatewayOrderId(String gateway, String gatewayOrderId);
Optional<Payment> findFirstByOrderIdAndStatusOrderByIdDesc(Long orderId, PaymentStatus status);
}

View File

@ -14,6 +14,10 @@ import java.util.Optional;
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
Optional<PaymentTransaction> findByPaymentIdAndType(
Long paymentId,
PaymentTransactionType type
);
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
Long paymentId,
PaymentTransactionType type,

View File

@ -1,11 +1,15 @@
package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
@Entity
@Table(name = "pedidos")
public class Pedido {
public class Pedido extends AbstractAuditedEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ -37,27 +41,8 @@ public class Pedido {
@Column(name = "proveedor_ref", length = 100)
private String proveedorRef;
// Auditoría básica (coincidiendo con las columnas que se ven en la captura)
@Column(name = "created_by")
private Long createdBy;
@Column(name = "updated_by")
private Long updatedBy;
@Column(name = "deleted_by")
private Long deletedBy;
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = false)
private List<PedidoLinea> lineas = new ArrayList<>();
// --- Getters y setters ---
@ -132,60 +117,4 @@ public class Pedido {
public void setProveedorRef(String proveedorRef) {
this.proveedorRef = proveedorRef;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public Long getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
public Long getDeletedBy() {
return deletedBy;
}
public void setDeletedBy(Long deletedBy) {
this.deletedBy = deletedBy;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@ -0,0 +1,308 @@
package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
import com.imprimelibros.erp.paises.Paises;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Entity
@Table(name = "pedidos_direcciones")
public class PedidoDireccion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// FK a pedidos_lineas.id (nullable, on delete set null)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pedido_linea_id")
private PedidoLinea pedidoLinea;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pedido_id")
private Pedido pedido;
@Column(name = "unidades")
private Integer unidades;
@Column(name = "is_facturacion", nullable = false)
private boolean facturacion = false;
@Column(name = "is_ejemplar_prueba", nullable = false)
private boolean ejemplarPrueba = false;
@Column(name = "email", length = 255)
private String email;
@Column(name = "att", nullable = false, length = 150)
private String att;
@Column(name = "direccion", nullable = false, length = 255)
private String direccion;
@Column(name = "cp", nullable = false)
private Integer cp;
@Column(name = "ciudad", nullable = false, length = 100)
private String ciudad;
@Column(name = "provincia", nullable = false, length = 100)
private String provincia;
@Column(name = "pais_code3", nullable = false, length = 3)
private String paisCode3 = "esp";
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
private Paises pais;
@Transient
private String paisNombre;
@Column(name = "telefono", nullable = false, length = 30)
private String telefono;
@Column(name = "instrucciones", length = 255)
private String instrucciones;
@Column(name = "razon_social", length = 150)
private String razonSocial;
@Enumerated(EnumType.STRING)
@Column(name = "tipo_identificacion_fiscal", nullable = false, length = 20)
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
@Column(name = "identificacion_fiscal", length = 50)
private String identificacionFiscal;
@Column(name = "is_palets", nullable = false)
private boolean palets = false;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
// ===== GETTERS & SETTERS =====
public Long getId() {
return id;
}
public PedidoLinea getPedidoLinea() {
return pedidoLinea;
}
public void setPedidoLinea(PedidoLinea pedidoLinea) {
this.pedidoLinea = pedidoLinea;
}
public Pedido getPedido() {
return pedido;
}
public void setPedido(Pedido pedido) {
this.pedido = pedido;
}
public Integer getUnidades() {
return unidades;
}
public void setUnidades(Integer unidades) {
this.unidades = unidades;
}
public boolean isFacturacion() {
return facturacion;
}
public void setFacturacion(boolean facturacion) {
this.facturacion = facturacion;
}
public boolean isEjemplarPrueba() {
return ejemplarPrueba;
}
public void setEjemplarPrueba(boolean ejemplarPrueba) {
this.ejemplarPrueba = ejemplarPrueba;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAtt() {
return att;
}
public void setAtt(String att) {
this.att = att;
}
public String getDireccion() {
return direccion;
}
public void setDireccion(String direccion) {
this.direccion = direccion;
}
public Integer getCp() {
return cp;
}
public void setCp(Integer cp) {
this.cp = cp;
}
public String getCiudad() {
return ciudad;
}
public void setCiudad(String ciudad) {
this.ciudad = ciudad;
}
public String getProvincia() {
return provincia;
}
public void setProvincia(String provincia) {
this.provincia = provincia;
}
public String getPaisCode3() {
return paisCode3;
}
public void setPaisCode3(String paisCode3) {
this.paisCode3 = paisCode3;
}
public Paises getPais() {
return pais;
}
public void setPais(Paises pais) {
this.pais = pais;
}
public String getTelefono() {
return telefono;
}
public void setTelefono(String telefono) {
this.telefono = telefono;
}
public String getInstrucciones() {
return instrucciones;
}
public void setInstrucciones(String instrucciones) {
this.instrucciones = instrucciones;
}
public String getRazonSocial() {
return razonSocial;
}
public void setRazonSocial(String razonSocial) {
this.razonSocial = razonSocial;
}
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
return tipoIdentificacionFiscal;
}
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipoIdentificacionFiscal) {
this.tipoIdentificacionFiscal = tipoIdentificacionFiscal;
}
public String getIdentificacionFiscal() {
return identificacionFiscal;
}
public void setIdentificacionFiscal(String identificacionFiscal) {
this.identificacionFiscal = identificacionFiscal;
}
public boolean isPalets() {
return palets;
}
public void setPalets(boolean palets) {
this.palets = palets;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public String getPaisNombre() {
return paisNombre;
}
public void setPaisNombre(String paisNombre) {
this.paisNombre = paisNombre;
}
public Map<String, Object> toSkMap(Double pesoKg) {
Map<String, Object> direccion = new HashMap<>();
direccion.put("cantidad", this.getUnidades());
direccion.put("peso", pesoKg);
direccion.put("att", this.getAtt());
direccion.put("email", this.getEmail());
direccion.put("direccion", this.getDireccion());
direccion.put("pais_code3", this.getPaisCode3());
direccion.put("cp", this.getCp());
direccion.put("municipio", this.getCiudad());
direccion.put("provincia", this.getProvincia());
direccion.put("telefono", this.getTelefono());
direccion.put("entregaPieCalle", this.isPalets() ? 1 : 0);
direccion.put("is_ferro_prototipo", this.isEjemplarPrueba() ? 1 : 0);
direccion.put("num_ferro_prototipo", this.isEjemplarPrueba() ? 1 : 0);
Map<String, Object> map = new HashMap<>();
map.put("direccion", direccion);
map.put("unidades", this.getUnidades());
map.put("entregaPalets", this.isPalets() ? 1 : 0);
return map;
}
public static Map<String, Object> toSkMapDepositoLegal() {
Map<String, Object> direccion = new HashMap<>();
direccion.put("cantidad", 4);
direccion.put("peso", 0);
direccion.put("att", "Unidades para Depósito Legal (sin envío)");
direccion.put("email", "");
direccion.put("direccion", "");
direccion.put("pais_code3", "esp");
direccion.put("cp", "");
direccion.put("municipio", "");
direccion.put("provincia", "");
direccion.put("telefono", "");
direccion.put("entregaPieCalle", 0);
direccion.put("is_ferro_prototipo", 0);
direccion.put("num_ferro_prototipo", 0);
Map<String, Object> map = new HashMap<>();
map.put("direccion", direccion);
map.put("unidades", 4);
map.put("entregaPalets", 0);
return map;
}
}

View File

@ -0,0 +1,26 @@
package com.imprimelibros.erp.pedidos;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PedidoDireccionRepository extends JpaRepository<PedidoDireccion, Long> {
// Todas las direcciones de una línea de pedido
List<PedidoDireccion> findByPedidoLinea_Id(Long pedidoLineaId);
// Si en tu código sueles trabajar con el objeto:
List<PedidoDireccion> findByPedidoLinea(PedidoLinea pedidoLinea);
PedidoDireccion findByPedidoIdAndFacturacionTrue(Long pedidoId);
@Query("""
select distinct d
from PedidoDireccion d
join d.pedidoLinea pl
where d.pedidoLinea.id = :pedidoLineaId
""")
List<PedidoDireccion> findByPedidoLineaId(Long pedidoLineaId);
}

View File

@ -9,6 +9,36 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
@Table(name = "pedidos_lineas")
public class PedidoLinea {
public enum Estado {
pendiente_pago("pedido.estado.pendiente_pago", 1),
procesando_pago("pedido.estado.procesando_pago", 2),
denegado_pago("pedido.estado.denegado_pago", 3),
aprobado("pedido.estado.aprobado", 4),
maquetacion("pedido.estado.maquetacion", 5),
haciendo_ferro("pedido.estado.haciendo_ferro", 6),
esperando_aceptacion_ferro("pedido.estado.esperando_aceptacion_ferro", 7),
ferro_cliente("pedido.estado.ferro_cliente", 8),
produccion("pedido.estado.produccion", 9),
terminado("pedido.estado.terminado", 10),
cancelado("pedido.estado.cancelado", 11);
private final String messageKey;
private final int priority;
Estado(String messageKey, int priority) {
this.messageKey = messageKey;
this.priority = priority;
}
public String getMessageKey() {
return messageKey;
}
public int getPriority() {
return priority;
}
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ -21,6 +51,16 @@ public class PedidoLinea {
@JoinColumn(name = "presupuesto_id", nullable = false)
private Presupuesto presupuesto;
@Enumerated(EnumType.STRING)
@Column(name = "estado", nullable = false)
private Estado estado = Estado.aprobado;
@Column(name = "estado_manual", nullable = false)
private Boolean estadoManual;
@Column(name = "fecha_entrega")
private LocalDateTime fechaEntrega;
@Column(name = "created_at")
private LocalDateTime createdAt;
@ -53,6 +93,30 @@ public class PedidoLinea {
this.presupuesto = presupuesto;
}
public Estado getEstado() {
return estado;
}
public void setEstado(Estado estado) {
this.estado = estado;
}
public Boolean getEstadoManual() {
return estadoManual;
}
public void setEstadoManual(Boolean estadoManual) {
this.estadoManual = estadoManual;
}
public LocalDateTime getFechaEntrega() {
return fechaEntrega;
}
public void setFechaEntrega(LocalDateTime fechaEntrega) {
this.fechaEntrega = fechaEntrega;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}

View File

@ -9,6 +9,7 @@ import java.util.List;
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
List<PedidoLinea> findByPedidoId(Long pedidoId);
List<PedidoLinea> findByPedidoIdOrderByIdAsc(Long pedidoId);
List<PedidoLinea> findByPresupuestoId(Long presupuestoId);
}

View File

@ -1,10 +1,23 @@
package com.imprimelibros.erp.pedidos;
import java.time.Instant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface PedidoRepository extends JpaRepository<Pedido, Long> {
// aquí podrás añadir métodos tipo:
// List<Pedido> findByDeletedFalse();
public interface PedidoRepository extends JpaRepository<Pedido, Long>, JpaSpecificationExecutor<Pedido> {
// Suma de "total" para un "createdBy" desde una fecha dada
@Query("""
SELECT COALESCE(SUM(p.total), 0)
FROM Pedido p
WHERE p.createdBy.id = :userId
AND p.createdAt >= :afterDate
""")
Double sumTotalByCreatedByAndCreatedAtAfter(@Param("userId") Long userId,
@Param("afterDate") Instant afterDate);
}

View File

@ -1,14 +1,30 @@
package com.imprimelibros.erp.pedidos;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.cart.Cart;
import com.imprimelibros.erp.cart.CartDireccion;
import com.imprimelibros.erp.cart.CartItem;
import com.imprimelibros.erp.cart.CartService;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.direcciones.Direccion;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
import com.imprimelibros.erp.users.UserService;
import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
@Service
public class PedidoService {
@ -16,50 +32,45 @@ public class PedidoService {
private final PedidoRepository pedidoRepository;
private final PedidoLineaRepository pedidoLineaRepository;
private final PresupuestoRepository presupuestoRepository;
private final PedidoDireccionRepository pedidoDireccionRepository;
private final DireccionService direccionService;
private final UserService userService;
private final PresupuestoService presupuestoService;
private final CartService cartService;
private final skApiClient skApiClient;
private final PresupuestoRepository presupuestoRepo;
private final MessageSource messageSource;
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
PresupuestoRepository presupuestoRepository) {
PresupuestoRepository presupuestoRepository, PedidoDireccionRepository pedidoDireccionRepository,
DireccionService direccionService, UserService userService, PresupuestoService presupuestoService,
CartService cartService, skApiClient skApiClient, PresupuestoRepository presupuestoRepo,
MessageSource messageSource) {
this.pedidoRepository = pedidoRepository;
this.pedidoLineaRepository = pedidoLineaRepository;
this.presupuestoRepository = presupuestoRepository;
this.pedidoDireccionRepository = pedidoDireccionRepository;
this.direccionService = direccionService;
this.userService = userService;
this.presupuestoService = presupuestoService;
this.cartService = cartService;
this.skApiClient = skApiClient;
this.presupuestoRepo = presupuestoRepo;
this.messageSource = messageSource;
}
public int getDescuentoFidelizacion() {
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
// ultimo año)
double totalGastado = 1600.0; // Ejemplo, deberías obtenerlo del historial del cliente
if (totalGastado < 1200) {
return 0;
} else if (totalGastado >= 1200 && totalGastado < 1999) {
return 1;
} else if (totalGastado >= 2000 && totalGastado < 2999) {
return 2;
} else if (totalGastado >= 3000 && totalGastado < 3999) {
return 3;
} else if (totalGastado >= 4000 && totalGastado < 4999) {
return 4;
} else if (totalGastado >= 5000) {
return 5;
}
return 0;
}
/**
* Crea un pedido a partir de:
* - lista de IDs de presupuesto
* - resumen numérico del carrito (getCartSummaryRaw)
* - datos de proveedor
* - usuario que crea el pedido
*/
@Transactional
public Pedido crearPedido(List<Long> presupuestoIds,
Map<String, Object> cartSummaryRaw,
public Pedido crearPedido(
Long cartId,
Long direccionFacturacionId,
String proveedor,
String proveedorRef,
Long userId) {
String proveedorRef) {
Pedido pedido = new Pedido();
Cart cart = cartService.getCartById(cartId);
Map<String, Object> cartSummaryRaw = cartService.getCartSummaryRaw(cart, Locale.getDefault());
// Datos económicos (ojo con las claves, son las del summaryRaw)
pedido.setBase((Double) cartSummaryRaw.getOrDefault("base", 0.0d));
pedido.setEnvio((Double) cartSummaryRaw.getOrDefault("shipment", 0.0d));
@ -69,33 +80,567 @@ public class PedidoService {
pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
// Proveedor
pedido.setProveedor(proveedor);
pedido.setProveedorRef(proveedorRef);
if (proveedor != null && proveedorRef != null) {
pedido.setProveedor(proveedor);
pedido.setProveedorRef(proveedorRef);
}
// Auditoría mínima
pedido.setCreatedBy(userId);
pedido.setCreatedAt(LocalDateTime.now());
Long userId = cart.getUserId();
pedido.setCreatedBy(userService.findById(userId));
pedido.setCreatedAt(Instant.now());
pedido.setDeleted(false);
pedido.setUpdatedAt(LocalDateTime.now());
pedido.setUpdatedBy(userId);
pedido.setUpdatedAt(Instant.now());
pedido.setUpdatedBy(userService.findById(userId));
// Guardamos el pedido
Pedido saved = pedidoRepository.save(pedido);
Pedido pedidoGuardado = pedidoRepository.save(pedido);
// Crear líneas del pedido
for (Long presupuestoId : presupuestoIds) {
Presupuesto presupuesto = presupuestoRepository.getReferenceById(presupuestoId);
List<CartItem> items = cart.getItems();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
Presupuesto pCart = item.getPresupuesto();
// Asegurarnos de trabajar con la entidad gestionada por JPA
Presupuesto p = presupuestoRepository.findById(pCart.getId())
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
p.setEstado(Presupuesto.Estado.aceptado);
presupuestoRepository.save(p);
PedidoLinea linea = new PedidoLinea();
linea.setPedido(saved);
linea.setPresupuesto(presupuesto);
linea.setPedido(pedidoGuardado);
linea.setPresupuesto(p);
linea.setCreatedBy(userId);
linea.setCreatedAt(LocalDateTime.now());
linea.setEstado(PedidoLinea.Estado.pendiente_pago);
linea.setEstadoManual(false);
pedidoLineaRepository.save(linea);
// Guardar las direcciones asociadas a la línea del pedido
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
saveDireccionesPedidoLinea(direcciones_presupuesto, pedidoGuardado, linea, direccionFacturacionId);
}
return pedidoGuardado;
}
public Boolean markPedidoAsProcesingPayment(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if (pedido == null) {
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
linea.setEstado(PedidoLinea.Estado.procesando_pago);
pedidoLineaRepository.save(linea);
}
return saved;
return true;
}
public Boolean markPedidoAsPaymentDenied(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if (pedido == null) {
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
linea.setEstado(PedidoLinea.Estado.denegado_pago);
pedidoLineaRepository.save(linea);
}
return true;
}
public Pedido findById(Long pedidoId) {
return pedidoRepository.findById(pedidoId).orElse(null);
}
/** Lista de los items del pedido preparados para la vista */
@Transactional
public List<Map<String, Object>> getLineas(Long pedidoId, Locale locale) {
Pedido p = pedidoRepository.findById(pedidoId).orElse(null);
if (p == null) {
return new ArrayList<>();
}
List<Map<String, Object>> resultados = new ArrayList<>();
List<PedidoLinea> items = pedidoLineaRepository.findByPedidoIdOrderByIdAsc(p.getId());
for (PedidoLinea item : items) {
Presupuesto presupuesto = item.getPresupuesto();
Map<String, Object> elemento = presupuestoService.getPresupuestoInfoForCard(presupuesto, locale);
elemento.put("estado", item.getEstado());
elemento.put("fechaEntrega",
item.getFechaEntrega() != null ? Utils.formatDate(item.getFechaEntrega(), locale) : "");
elemento.put("lineaId", item.getId());
resultados.add(elemento);
}
return resultados;
}
public PedidoDireccion getDireccionFacturacionPedido(Long pedidoId) {
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
}
public List<PedidoDireccion> getDireccionesEntregaPedidoLinea(Long pedidoLineaId) {
return pedidoDireccionRepository.findByPedidoLinea_Id(pedidoLineaId);
}
public Boolean setOrderAsPaid(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if (pedido == null) {
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
List<Map<String, Object>> referenciasProveedor = new ArrayList<>();
Integer total = lineas.size();
Integer counter = 1;
for (PedidoLinea linea : lineas) {
if (linea.getEstado() == Estado.pendiente_pago
|| linea.getEstado() == Estado.denegado_pago) {
Presupuesto presupuesto = linea.getPresupuesto();
linea.setEstado(getEstadoInicial(presupuesto));
pedidoLineaRepository.save(linea);
// Save presupuesto in SK
Map<String, Object> result = savePresupuestoSK(linea.getId(), presupuesto, counter, total);
if (result == null) {
return false;
}
referenciasProveedor.add(result);
counter++;
}
}
if (referenciasProveedor.isEmpty()) {
return false;
}
// Save pedido in SK
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
for (Map<String, Object> presData : referenciasProveedor) {
Long presId = ((Number) presData.get("id")).longValue();
presupuestoSkIds.add(presId);
}
Map<String, Object> ids = new HashMap<>();
ids.put("presupuesto_ids", presupuestoSkIds);
Long skPedidoId = skApiClient.crearPedido(ids);
if (skPedidoId == null) {
System.out.println("No se pudo crear el pedido en SK.");
return false;
}
pedido.setProveedor("Safekat");
pedido.setProveedorRef(skPedidoId.toString());
pedidoRepository.save(pedido);
return true;
}
public Map<String, Object> actualizarEstado(Long pedidoLineaId, Locale locale) {
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
if (pedidoLinea == null) {
return Map.of("success", false,
"message", messageSource.getMessage("pedido.errors.linea-not-found", null, locale));
}
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.haciendo_ferro.getPriority() &&
pedidoLinea.getEstado().getPriority() < PedidoLinea.Estado.terminado.getPriority()) {
PedidoLinea.Estado estadoOld = pedidoLinea.getEstado();
Map<String, Object> result = skApiClient.checkPedidoEstado(
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
if (result == null || !result.containsKey("estado")) {
return Map.of(
"success", false,
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
}
PedidoLinea.Estado estadoSk = PedidoLinea.Estado.valueOf((String) result.get("estado"));
if (estadoOld == estadoSk) {
return Map.of(
"success", true,
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
"stateKey", estadoSk.name(),
"message", messageSource.getMessage("pedido.success.same-estado", null, locale));
}
pedidoLinea.setEstado(estadoSk);
pedidoLineaRepository.save(pedidoLinea);
return Map.of(
"success", true,
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
"stateKey", estadoSk.name(),
"message", messageSource.getMessage("pedido.success.estado-actualizado", null, locale));
}
return Map.of(
"success", false,
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
}
public Boolean markPedidoAsMaquetacionDone(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if (pedido == null) {
return false;
}
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
for (PedidoLinea linea : lineas) {
if (linea.getEstado() == Estado.maquetacion) {
linea.setEstado(Estado.haciendo_ferro);
pedidoLineaRepository.save(linea);
}
}
return true;
}
public Map<String, Object> getFilesType(Long pedidoLineaId, Locale locale) {
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
if (pedidoLinea == null) {
return Map.of("success", false, "message", "Línea de pedido no encontrada.");
}
Map<String, Object> files = skApiClient.getFilesTypes(
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
return files;
}
public byte[] getFerroFileContent(Long pedidoLineaId, Locale locale) {
return downloadFile(pedidoLineaId, "ferro", locale);
}
public byte[] getCubiertaFileContent(Long pedidoLineaId, Locale locale) {
return downloadFile(pedidoLineaId, "cubierta", locale);
}
public byte[] getTapaFileContent(Long pedidoLineaId, Locale locale) {
return downloadFile(pedidoLineaId, "tapa", locale);
}
public Boolean aceptarFerro(Long pedidoLineaId, Locale locale) {
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
if (pedidoLinea == null) {
return false;
}
if (pedidoLinea.getEstado() != PedidoLinea.Estado.esperando_aceptacion_ferro) {
return false;
}
Boolean result = skApiClient.aceptarFerro(
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
if (!result) {
return false;
}
pedidoLinea.setEstado(PedidoLinea.Estado.produccion);
pedidoLineaRepository.save(pedidoLinea);
return true;
}
/***************************
* MÉTODOS PRIVADOS
***************************/
private byte[] downloadFile(Long pedidoLineaId, String fileType, Locale locale) {
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
if (pedidoLinea == null) {
return null;
}
byte[] fileData = skApiClient.downloadFile(
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()),
fileType,
locale);
return fileData;
}
@Transactional
private Map<String, Object> savePresupuestoSK(Long pedidoLineaId, Presupuesto presupuesto, Integer counter,
Integer total) {
Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(presupuesto, true);
data_to_send.put("createPedido", 0);
// Recuperar el mapa anidado datosCabecera
@SuppressWarnings("unchecked")
Map<String, Object> datosCabecera = (Map<String, Object>) data_to_send.get("datosCabecera");
if (datosCabecera != null) {
Object tituloOriginal = datosCabecera.get("titulo");
datosCabecera.put(
"titulo",
"[" + (counter) + "/" + total + "] " + (tituloOriginal != null ? tituloOriginal : ""));
}
List<PedidoDireccion> direccionesPedidoLinea = pedidoDireccionRepository
.findByPedidoLineaId(pedidoLineaId);
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
List<Map<String, Object>> direccionEjemplarPrueba = new ArrayList<>();
for (PedidoDireccion pd : direccionesPedidoLinea) {
if (pd.isEjemplarPrueba()) {
direccionEjemplarPrueba.add(
pd.toSkMap(presupuesto.getPeso()));
} else {
direccionesPresupuesto.add(
pd.toSkMap(presupuesto.getPeso() * pd.getUnidades()));
}
}
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("deposito-legal")) {
direccionesPresupuesto.add(
PedidoDireccion.toSkMapDepositoLegal());
}
data_to_send.put("direcciones", direccionesPresupuesto);
if (direccionEjemplarPrueba.size() > 0)
data_to_send.put("direccionesFP1", direccionEjemplarPrueba.get(0));
else {
data_to_send.put("direccionesFP1", new ArrayList<>());
}
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
if (result.containsKey("error")) {
System.out.println("Error al guardar presupuesto en SK");
System.out.println("-------------------------");
System.out.println(result.get("error"));
// decide si seguir con otros items o abortar:
// continue; o bien throw ...
return null;
}
Object dataObj = result.get("data");
if (!(dataObj instanceof Map<?, ?> dataRaw)) {
System.out.println("Formato inesperado de 'data' en savePresupuesto: " + result);
return null;
}
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) dataRaw;
Long presId = ((Number) dataMap.get("id")).longValue();
String skin = ((String) dataMap.get("iskn")).toString();
presupuesto.setProveedor("Safekat");
presupuesto.setProveedorRef1(skin);
presupuesto.setProveedorRef2(presId);
presupuesto.setEstado(Presupuesto.Estado.aceptado);
presupuestoRepo.save(presupuesto);
return dataMap;
}
// Obtener las direcciones de envío asociadas a un presupuesto en el carrito
private Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
if (cart.getOnlyOneShipment()) {
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
if (!direcciones.isEmpty()) {
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada(),
presupuesto.getPeso(),
direcciones.get(0).getIsPalets(),
false));
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
} else {
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
presupuesto.getSelectedTirada(),
presupuesto.getPeso(),
direcciones.get(0).getIsPalets(),
false));
}
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
direccionesPrueba.add(direcciones.get(0).toSkMap(
1,
presupuesto.getPeso(),
false,
true));
}
Map<String, Object> direccionesRet = new HashMap<>();
direccionesRet.put("direcciones", direccionesPresupuesto);
if (!direccionesPrueba.isEmpty())
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
else {
direccionesRet.put("direccionesFP1", new ArrayList<>());
}
return direccionesRet;
}
} else {
List<CartDireccion> direcciones = cart.getDirecciones().stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
.toList();
for (CartDireccion cd : direcciones) {
// direccion de ejemplar de prueba
if (cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
continue;
}
if (cd.getUnidades() == null || cd.getUnidades() <= 0) {
direccionesPrueba.add(cd.toSkMap(
1,
presupuesto.getPeso(),
false,
true));
} else {
direccionesPresupuesto.add(cd.toSkMap(
cd.getUnidades(),
presupuesto.getPeso(),
cd.getIsPalets(),
false));
}
}
if (presupuesto.getServiciosJson() != null
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
CartDireccion cd = new CartDireccion();
direccionesPresupuesto.add(cd.toSkMapDepositoLegal());
}
}
Map<String, Object> direccionesRet = new HashMap<>();
direccionesRet.put("direcciones", direccionesPresupuesto);
if (!direccionesPrueba.isEmpty())
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
else {
direccionesRet.put("direccionesFP1", new ArrayList<>());
}
return direccionesRet;
}
@Transactional
private void saveDireccionesPedidoLinea(
Map<String, Object> direcciones,
Pedido pedido,
PedidoLinea linea, Long direccionFacturacionId) {
String email = pedido.getCreatedBy().getUserName();
// direccion prueba
if (direcciones.containsKey("direccionesFP1")) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> fp1 = (Map<String, Object>) direcciones.get("direccionesFP1");
@SuppressWarnings("unchecked")
PedidoDireccion direccion = saveDireccion(
email,
false,
(HashMap<String, Object>) fp1.get("direccion"),
pedido,
linea,
true,
false);
pedidoDireccionRepository.save(direccion);
} catch (Exception e) {
// Viene vacio
}
}
if (direcciones.containsKey("direcciones")) {
List<?> dirs = (List<?>) direcciones.get("direcciones");
for (Object dir : dirs) {
@SuppressWarnings("unchecked")
HashMap<String, Object> direccionEnvio = (HashMap<String, Object>) ((HashMap<String, Object>) dir)
.get("direccion");
if (direccionEnvio.get("cantidad") != null && (Integer) direccionEnvio.get("cantidad") == 4
&& direccionEnvio.get("att").toString().contains("Depósito Legal")) {
continue; // Saltar la dirección de depósito legal
}
@SuppressWarnings("unchecked")
PedidoDireccion direccion = saveDireccion(
email,
((Number) ((HashMap<String, Object>) dir)
.getOrDefault("entregaPalets", 0))
.intValue() == 1,
(HashMap<String, Object>) ((HashMap<String, Object>) dir).get("direccion"),
pedido,
linea, false,
false);
pedidoDireccionRepository.save(direccion);
}
}
if (direccionFacturacionId != null) {
Direccion dirFact = direccionService.findById(direccionFacturacionId).orElse(null);
if (dirFact != null) {
HashMap<String, Object> dirFactMap = new HashMap<>();
dirFactMap.put("att", dirFact.getAtt());
dirFactMap.put("direccion", dirFact.getDireccion());
dirFactMap.put("cp", dirFact.getCp());
dirFactMap.put("municipio", dirFact.getCiudad());
dirFactMap.put("provincia", dirFact.getProvincia());
dirFactMap.put("pais_code3", dirFact.getPaisCode3());
dirFactMap.put("telefono", dirFact.getTelefono());
dirFactMap.put("instrucciones", dirFact.getInstrucciones());
dirFactMap.put("razon_social", dirFact.getRazonSocial());
dirFactMap.put("tipo_identificacion_fiscal", dirFact.getTipoIdentificacionFiscal().name());
dirFactMap.put("identificacion_fiscal", dirFact.getIdentificacionFiscal());
PedidoDireccion direccion = saveDireccion(
email,
false,
dirFactMap,
pedido,
linea,
false,
true);
pedidoDireccionRepository.save(direccion);
}
}
}
private PedidoDireccion saveDireccion(
String email,
Boolean palets,
HashMap<String, Object> dir,
Pedido pedido,
PedidoLinea linea,
Boolean isEjemplarPrueba,
Boolean isFacturacion) {
PedidoDireccion direccion = new PedidoDireccion();
direccion.setEmail(email);
direccion.setPalets(isEjemplarPrueba || isFacturacion ? false : palets);
direccion.setPedidoLinea(isFacturacion ? null : linea);
if (isFacturacion) {
direccion.setUnidades(null);
direccion.setFacturacion(true);
direccion.setPedido(pedido);
} else {
if (isEjemplarPrueba) {
direccion.setUnidades(1);
direccion.setEjemplarPrueba(true);
} else {
direccion.setUnidades((Integer) dir.getOrDefault("cantidad", 1));
direccion.setEjemplarPrueba(false);
}
direccion.setFacturacion(false);
}
direccion.setAtt((String) dir.getOrDefault("att", ""));
direccion.setDireccion((String) dir.getOrDefault("direccion", ""));
direccion.setCp((Integer) dir.getOrDefault("cp", 0));
direccion.setCiudad((String) dir.getOrDefault("municipio", ""));
direccion.setProvincia((String) dir.getOrDefault("provincia", ""));
direccion.setPaisCode3((String) dir.getOrDefault("pais_code3", "esp"));
direccion.setTelefono((String) dir.getOrDefault("telefono", ""));
direccion.setInstrucciones((String) dir.getOrDefault("instrucciones", ""));
direccion.setRazonSocial((String) dir.getOrDefault("razon_social", ""));
direccion.setTipoIdentificacionFiscal(Direccion.TipoIdentificacionFiscal
.valueOf((String) dir.getOrDefault("tipo_identificacion_fiscal",
Direccion.TipoIdentificacionFiscal.DNI.name())));
direccion.setIdentificacionFiscal((String) dir.getOrDefault("identificacion_fiscal", ""));
return direccion;
}
private Estado getEstadoInicial(Presupuesto p) {
if (presupuestoService.hasMaquetacion(p)) {
return Estado.maquetacion;
} else {
return Estado.haciendo_ferro;
}
}
}

View File

@ -0,0 +1,402 @@
package com.imprimelibros.erp.pedidos;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import java.security.Principal;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser;
import com.imprimelibros.erp.datatables.DataTablesRequest;
import com.imprimelibros.erp.datatables.DataTablesResponse;
import com.imprimelibros.erp.i18n.TranslationService;
import com.imprimelibros.erp.paises.PaisesService;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
import com.imprimelibros.erp.users.UserDao;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@Controller
@RequestMapping("/pedidos")
public class PedidosController {
private final PresupuestoService presupuestoService;
private final PedidoRepository repoPedido;
private final PedidoService pedidoService;
private final UserDao repoUser;
private final MessageSource messageSource;
private final PedidoLineaRepository repoPedidoLinea;
private final PaisesService paisesService;
private final TranslationService translationService;
public PedidosController(PedidoRepository repoPedido, PedidoService pedidoService, UserDao repoUser,
MessageSource messageSource, TranslationService translationService,
PedidoLineaRepository repoPedidoLinea, PaisesService paisesService, PresupuestoService presupuestoService) {
this.repoPedido = repoPedido;
this.pedidoService = pedidoService;
this.repoUser = repoUser;
this.messageSource = messageSource;
this.translationService = translationService;
this.repoPedidoLinea = repoPedidoLinea;
this.paisesService = paisesService;
this.presupuestoService = presupuestoService;
}
@GetMapping
public String listarPedidos(Model model, Locale locale) {
List<String> keys = List.of(
"app.cancelar",
"app.seleccionar",
"app.yes",
"checkout.payment.card",
"checkout.payment.bizum",
"checkout.payment.bank-transfer",
"checkout.error.select-method");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
if (Utils.isCurrentUserAdmin()) {
return "imprimelibros/pedidos/pedidos-list";
}
return "imprimelibros/pedidos/pedidos-list-cliente";
}
@GetMapping(value = "datatable", produces = "application/json")
@ResponseBody
public DataTablesResponse<Map<String, Object>> getDatatable(
HttpServletRequest request,
Principal principal,
Locale locale) {
DataTablesRequest dt = DataTablesParser.from(request);
Boolean isAdmin = Utils.isCurrentUserAdmin();
Long currentUserId = Utils.currentUserId(principal);
List<String> searchable = List.of(
"id");
// Campos ordenables
List<String> orderable = List.of(
"id",
"createdBy.fullName",
"createdAt",
"total",
"estado");
Specification<Pedido> base = (root, query, cb) -> cb.conjunction();
if (!isAdmin) {
base = base.and((root, query, cb) -> cb.equal(root.get("createdBy").get("id"), currentUserId));
}
String clientSearch = dt.getColumnSearch("cliente");
String estadoSearch = dt.getColumnSearch("estado");
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
if (clientSearch != null) {
List<Long> userIds = repoUser.findIdsByFullNameLike(clientSearch.trim());
if (userIds.isEmpty()) {
// Ningún usuario coincide → forzamos 0 resultados
base = base.and((root, query, cb) -> cb.disjunction());
} else {
base = base.and((root, query, cb) -> root.get("createdBy").in(userIds));
}
}
if (estadoSearch != null && !estadoSearch.isBlank()) {
try {
PedidoLinea.Estado estadoEnum = PedidoLinea.Estado.valueOf(estadoSearch.trim());
base = base.and((root, query, cb) -> {
// Evitar duplicados de pedidos si el provider usa joins
if (Pedido.class.equals(query.getResultType())) {
query.distinct(true);
}
Join<Pedido, PedidoLinea> lineas = root.join("lineas", JoinType.INNER);
return cb.equal(lineas.get("estado"), estadoEnum);
});
} catch (IllegalArgumentException ex) {
// Valor de estado no válido → forzamos 0 resultados
base = base.and((root, query, cb) -> cb.disjunction());
}
}
Long total = repoPedido.count(base);
return DataTable
.of(repoPedido, Pedido.class, dt, searchable)
.orderable(orderable)
.add("id", Pedido::getId)
.add("created_at", pedido -> Utils.formatInstant(pedido.getCreatedAt(), locale))
.add("cliente", pedido -> {
if (pedido.getCreatedBy() != null) {
return pedido.getCreatedBy().getFullName();
}
return "";
})
.add("total", pedido -> {
if (pedido.getTotal() != null) {
return Utils.formatCurrency(pedido.getTotal(), locale);
} else {
return "";
}
})
.add("estado", pedido -> {
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
if (lineas.isEmpty()) {
return "";
}
// concatenar los estados de las líneas, ordenados por prioridad
StringBuilder sb = new StringBuilder();
lineas.stream()
.map(PedidoLinea::getEstado)
.distinct()
.sorted(Comparator.comparingInt(PedidoLinea.Estado::getPriority))
.forEach(estado -> {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(messageSource.getMessage(estado.getMessageKey(), null, locale));
});
String text = sb.toString();
return text;
})
.add("actions", pedido -> {
String data = "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
+ "\' style=\'cursor: pointer;\'>"
+ messageSource.getMessage("app.view", null, locale) + "</span>";
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
boolean hasDenegadoPago = lineas.stream()
.anyMatch(linea -> PedidoLinea.Estado.denegado_pago.equals(linea.getEstado()));
if (hasDenegadoPago) {
data += " <span class='badge bg-danger btn-pay' data-amount='" + (int) (pedido.getTotal() * 100)
+ "' data-id='" + pedido.getId() + "' style='cursor: pointer;'>"
+ messageSource.getMessage("app.pay", null, locale) + "</span>";
}
return data;
})
.where(base)
.toJson(total);
}
@GetMapping("/view/{id}")
public String verPedido(
@PathVariable(name = "id", required = true) Long id,
Model model, Locale locale) {
Boolean isAdmin = Utils.isCurrentUserAdmin();
if (isAdmin) {
model.addAttribute("isAdmin", true);
} else {
model.addAttribute("isAdmin", false);
}
PedidoDireccion direccionFacturacion = pedidoService.getDireccionFacturacionPedido(id);
if (direccionFacturacion != null) {
String paisNombre = paisesService.getPaisNombrePorCode3(direccionFacturacion.getPaisCode3(), locale);
direccionFacturacion.setPaisNombre(paisNombre);
}
model.addAttribute("direccionFacturacion", direccionFacturacion);
List<Map<String, Object>> lineas = pedidoService.getLineas(id, locale);
for (Map<String, Object> linea : lineas) {
PedidoLinea pedidoLinea = repoPedidoLinea.findById(
((Number) linea.get("lineaId")).longValue()).orElse(null);
if (pedidoLinea != null) {
Map<String, Boolean> buttons = new HashMap<>();
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.esperando_aceptacion_ferro.getPriority()
&& pedidoLinea.getEstado().getPriority() <= PedidoLinea.Estado.produccion.getPriority()) {
if (pedidoLinea.getEstado() == PedidoLinea.Estado.esperando_aceptacion_ferro) {
buttons.put("aceptar_ferro", true);
} else {
buttons.put("aceptar_ferro", false);
}
Map<String, Object> filesType = pedidoService.getFilesType(pedidoLinea.getId(), locale);
if (filesType == null || filesType.get("error") != null) {
throw new RuntimeException(
messageSource.getMessage("pedido.errors.update-server-error", null, locale));
}
for (String key : filesType.keySet()) {
buttons.put(key, (Integer) filesType.get(key) == 1 ? true : false);
}
linea.put("buttons", buttons);
}
}
List<PedidoDireccion> dirEntrega = pedidoService.getDireccionesEntregaPedidoLinea(
((Number) linea.get("lineaId")).longValue());
if (dirEntrega != null && !dirEntrega.isEmpty()) {
for (PedidoDireccion direccion : dirEntrega) {
String paisNombre = paisesService.getPaisNombrePorCode3(direccion.getPaisCode3(), locale);
direccion.setPaisNombre(paisNombre);
}
}
linea.put("direccionesEntrega", dirEntrega);
}
model.addAttribute("lineas", lineas);
model.addAttribute("id", id);
return "imprimelibros/pedidos/pedidos-view";
}
// -------------------------------------
// Acciones sobre las lineas de pedido
// -------------------------------------
@PostMapping("/linea/{id}/update-status")
@ResponseBody
public Map<String, Object> updateStatus(
@PathVariable(name = "id", required = true) Long id, Locale locale) {
Map<String, Object> result = pedidoService.actualizarEstado(id, locale);
return result;
}
@PostMapping("/linea/{id}/update-maquetacion")
@ResponseBody
public Map<String, Object> updateMaquetacion(
@PathVariable(name = "id", required = true) Long id,
Locale locale) {
PedidoLinea entity = repoPedidoLinea.findById(id).orElse(null);
if (entity == null) {
String errorMsg = messageSource.getMessage("pedido.errors.linea-not-found", null, locale);
return Map.of(
"success", false,
"message", errorMsg);
}
if (entity.getEstado() != PedidoLinea.Estado.maquetacion) {
String errorMsg = messageSource.getMessage("pedido.errors.state-error", null, locale);
return Map.of(
"success", false,
"message", errorMsg);
}
entity.setEstado(PedidoLinea.Estado.haciendo_ferro);
repoPedidoLinea.save(entity);
String successMsg = messageSource.getMessage("pedido.success.estado-actualizado", null, locale);
return Map.of(
"success", true,
"message", successMsg,
"state", messageSource.getMessage(entity.getEstado().getMessageKey(), null, locale));
}
@GetMapping("/linea/{id}/download-ferro")
public ResponseEntity<Resource> downloadFerro(@PathVariable(name = "id", required = true) Long id, Locale locale) {
byte[] ferroFileContent = pedidoService.getFerroFileContent(id, locale);
if (ferroFileContent == null) {
return ResponseEntity.notFound().build();
}
ByteArrayResource resource = new ByteArrayResource(ferroFileContent);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=ferro_" + id + ".pdf")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
}
@GetMapping("/linea/{id}/download-cub")
public ResponseEntity<Resource> downloadCubierta(@PathVariable(name = "id", required = true) Long id,
Locale locale) {
byte[] cubFileContent = pedidoService.getCubiertaFileContent(id, locale);
if (cubFileContent == null) {
return ResponseEntity.notFound().build();
}
ByteArrayResource resource = new ByteArrayResource(cubFileContent);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=cubierta_" + id + ".pdf")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
}
@GetMapping("/linea/{id}/download-tapa")
public ResponseEntity<Resource> downloadTapa(@PathVariable(name = "id", required = true) Long id, Locale locale) {
byte[] tapaFileContent = pedidoService.getTapaFileContent(id, locale);
if (tapaFileContent == null) {
return ResponseEntity.notFound().build();
}
ByteArrayResource resource = new ByteArrayResource(tapaFileContent);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=tapa_" + id + ".pdf")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
}
@PostMapping("/linea/{id}/aceptar-ferro")
@ResponseBody
public Map<String, Object> aceptarFerro(@PathVariable(name = "id", required = true) Long id,
Locale locale) {
PedidoLinea entity = repoPedidoLinea.findById(id).orElse(null);
if (entity == null) {
String errorMsg = messageSource.getMessage("pedido.errors.linea-not-found", null, locale);
return Map.of(
"success", false,
"message", errorMsg);
}
if (entity.getEstado() != PedidoLinea.Estado.esperando_aceptacion_ferro) {
String errorMsg = messageSource.getMessage("pedido.errors.state-error", null, locale);
return Map.of(
"success", false,
"message", errorMsg);
}
Boolean result = pedidoService.aceptarFerro(id, locale);
if (result) {
String successMsg = messageSource.getMessage("pedido.success.estado-actualizado", null, locale);
return Map.of(
"success", true,
"message", successMsg,
"state", messageSource.getMessage(entity.getEstado().getMessageKey(), null, locale));
} else {
String errorMsg = messageSource.getMessage("pedido.errors.update-server-error", null, locale);
return Map.of(
"success", false,
"message", errorMsg);
}
}
}

View File

@ -147,7 +147,9 @@ public class PresupuestoController {
return ResponseEntity.badRequest().body(errores);
}
Map<String, Object> resultado = new HashMap<>();
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
resultado.put("solapas", datosInterior.get("maxSolapas"));
resultado.put("lomo", datosInterior.get("lomo"));
resultado.putAll(presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale));
return ResponseEntity.ok(resultado);
}
@ -267,7 +269,10 @@ public class PresupuestoController {
}
}
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
resultado.put("solapas", datosInterior.get("maxSolapas"));
resultado.put("lomo", datosInterior.get("lomo"));
return ResponseEntity.ok(resultado);
}
@ -300,7 +305,10 @@ public class PresupuestoController {
presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción
}
}
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
resultado.put("solapas", datosInterior.get("maxSolapas"));
resultado.put("lomo", datosInterior.get("lomo"));
return ResponseEntity.ok(resultado);
}
@ -323,7 +331,10 @@ public class PresupuestoController {
}
Map<String, Object> resultado = new HashMap<>();
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
resultado.put("solapas", datosInterior.get("maxSolapas"));
resultado.put("lomo", datosInterior.get("lomo"));
return ResponseEntity.ok(resultado);
}
@ -492,7 +503,8 @@ public class PresupuestoController {
String sessionId = request.getSession(true).getId();
String ip = IpUtils.getClientIp(request);
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode, locale, sessionId, ip);
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode,
locale, sessionId, ip);
return ResponseEntity.ok(resumen);
}
@ -519,7 +531,27 @@ public class PresupuestoController {
"presupuesto.add.cancel",
"presupuesto.add.select-client",
"presupuesto.add.error.options",
"presupuesto.add.error.options-client");
"presupuesto.add.error.options-client",
"presupuesto.duplicar.title",
"presupuesto.duplicar.text",
"presupuesto.duplicar.confirm",
"presupuesto.duplicar.cancelar",
"presupuesto.duplicar.aceptar",
"presupuesto.duplicar.required",
"presupuesto.duplicar.success.title",
"presupuesto.duplicar.success.text",
"presupuesto.duplicar.error.title",
"presupuesto.duplicar.error.internal",
"presupuesto.reimprimir.title",
"presupuesto.reimprimir.text",
"presupuesto.reimprimir.confirm",
"presupuesto.reimprimir.cancelar",
"presupuesto.reimprimir.aceptar",
"presupuesto.reimprimir.success.title",
"presupuesto.reimprimir.success.text",
"presupuesto.reimprimir.error.title",
"presupuesto.reimprimir.error.internal"
);
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
@ -543,7 +575,26 @@ public class PresupuestoController {
"presupuesto.exito.guardado",
"presupuesto.add.error.save.title",
"presupuesto.iva-reducido",
"presupuesto.iva-reducido-descripcion");
"presupuesto.iva-reducido-descripcion",
"presupuesto.duplicar.title",
"presupuesto.duplicar.text",
"presupuesto.duplicar.confirm",
"presupuesto.duplicar.cancelar",
"presupuesto.duplicar.aceptar",
"presupuesto.duplicar.required",
"presupuesto.duplicar.success.title",
"presupuesto.duplicar.success.text",
"presupuesto.duplicar.error.title",
"presupuesto.duplicar.error.internal",
"presupuesto.reimprimir.title",
"presupuesto.reimprimir.text",
"presupuesto.reimprimir.confirm",
"presupuesto.reimprimir.cancelar",
"presupuesto.reimprimir.aceptar",
"presupuesto.reimprimir.success.title",
"presupuesto.reimprimir.success.text",
"presupuesto.reimprimir.error.title",
"presupuesto.reimprimir.error.internal");
Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
@ -562,15 +613,15 @@ public class PresupuestoController {
return "redirect:/presupuesto";
}
if(presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado){
if (presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado) {
Map<String, Object> resumen = presupuestoService.getTextosResumen(
presupuestoOpt.get(),
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
locale);
presupuestoOpt.get(),
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
locale);
model.addAttribute("resumen", resumen);
model.addAttribute("presupuesto", presupuestoOpt.get());
return "imprimelibros/presupuestos/presupuestador-view";
@ -595,6 +646,7 @@ public class PresupuestoController {
model.addAttribute("appMode", "edit");
}
model.addAttribute("id", presupuestoOpt.get().getId());
model.addAttribute("presupuesto", presupuestoOpt.get());
return "imprimelibros/presupuestos/presupuesto-form";
}
@ -780,4 +832,30 @@ public class PresupuestoController {
}
}
@PostMapping("/{id}/comentario")
@ResponseBody
public String actualizarComentario(@PathVariable Long id,
@RequestParam String comentario) {
presupuestoService.updateComentario(id, comentario);
return "OK";
}
@PostMapping("/api/duplicar/{id}")
@ResponseBody
public Map<String, Object> duplicarPresupuesto(
@PathVariable Long id,
@RequestParam(name = "titulo", defaultValue = "") String titulo) {
Long entity = presupuestoService.duplicarPresupuesto(id, titulo);
return Map.of("id", entity);
}
@PostMapping("/api/reimprimir/{id}")
@ResponseBody
public Map<String, Object> reimprimirPresupuesto(@PathVariable Long id) {
Long entity = presupuestoService.reimprimirPresupuesto(id);
return Map.of("id", entity);
}
}

View File

@ -86,7 +86,7 @@ public class PresupuestoDatatableService {
.addIf(publico, "ciudad", Presupuesto::getCiudad)
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
.add("actions", this::generarBotones)
.add("actions", p -> generarBotones(p, locale))
.where(base)
.toJson(count);
}
@ -115,18 +115,27 @@ public class PresupuestoDatatableService {
return df.format(instant);
}
private String generarBotones(Presupuesto p) {
private String generarBotones(Presupuesto p, Locale locale) {
boolean borrador = p.getEstado() == Presupuesto.Estado.borrador;
String id = String.valueOf(p.getId());
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" +
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" +
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\"></i></a>";
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\" " +
"data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg(p.getEstado() == Presupuesto.Estado.aceptado ? "presupuesto.ver" : "presupuesto.editar", locale) + "\"></i></a>";
String duplicarBtn = !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-success btn-duplicate-privado fs-15\"><i class=\"ri-file-copy-2-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg("presupuesto.duplicar", locale) + "\"></i></a>" : "";
String reimprimirBtn = p.getEstado() == Presupuesto.Estado.aceptado && !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-success btn-reprint-privado fs-15\"><i class=\"ri-printer-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg("presupuesto.reimprimir", locale) + "\"></i></a>" : "";
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-danger btn-delete-"
+ (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado")
+ " fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>" : "";
+ " fs-15\"><i class=\"ri-delete-bin-5-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
msg("presupuesto.borrar", locale) + "\"></i></a>" : "";
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + deleteBtn + "</div>";
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + duplicarBtn + reimprimirBtn + deleteBtn + "</div>";
}
}

View File

@ -25,6 +25,7 @@ public class PresupuestoPapeles {
);
private static final Map<String, String> CABEZADA_COLOR_KEYS = Map.of(
"NOCABE", "presupuesto.cabezada-sin-cabezada",
"WHI", "presupuesto.cabezada-blanca",
"GRE", "presupuesto.cabezada-verde",
"BLUE", "presupuesto.cabezada-azul",

View File

@ -387,6 +387,9 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
@Column(name = "proveedor_ref2")
private Long proveedorRef2;
@Column(name = "is_reimpresion", nullable = false)
private Boolean isReimpresion = false;
// ====== MÉTODOS AUX ======
public String resumenPresupuesto() {
@ -965,6 +968,14 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
this.proveedorRef2 = proveedorRef2;
}
public Boolean getIsReimpresion() {
return isReimpresion;
}
public void setIsReimpresion(Boolean isReimpresion) {
this.isReimpresion = isReimpresion;
}
public void setId(Long id) {
this.id = id;
}

View File

@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal;
import java.math.RoundingMode;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.common.web.IpUtils;
import com.imprimelibros.erp.configurationERP.VariableService;
import com.imprimelibros.erp.presupuesto.GeoIpService;
@ -71,14 +72,16 @@ public class PresupuestoService {
private final skApiClient apiClient;
private final GeoIpService geoIpService;
private final UserDao userRepo;
private final Utils utils;
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo) {
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo, Utils utils) {
this.presupuestadorItems = presupuestadorItems;
this.presupuestoFormatter = presupuestoFormatter;
this.apiClient = apiClient;
this.geoIpService = geoIpService;
this.userRepo = userRepo;
this.utils = utils;
}
public boolean validateDatosGenerales(int[] tiradas) {
@ -348,6 +351,11 @@ public class PresupuestoService {
body.put("interior", interior);
body.put("cubierta", cubierta);
body.put("guardas", null);
// Para las reimpresiones
if(presupuesto.getIsReimpresion() != null && presupuesto.getIsReimpresion()) {
body.put("reimpresion", 1);
body.put("iskn", presupuesto.getProveedorRef1());
}
if (presupuesto.getSobrecubierta()) {
Map<String, Object> sobrecubierta = new HashMap<>();
sobrecubierta.put("papel", presupuesto.getPapelSobrecubiertaId());
@ -1020,6 +1028,7 @@ public class PresupuestoService {
resumen.put("iva_importe_4", presupuesto.getIvaImporte4());
resumen.put("iva_importe_21", presupuesto.getIvaImporte21());
resumen.put("total_con_iva", presupuesto.getTotalConIva());
resumen.put("isReimpresion", presupuesto.getIsReimpresion());
return resumen;
}
@ -1143,6 +1152,7 @@ public class PresupuestoService {
if(presupuesto.getSelectedTirada() != null && presupuesto.getSelectedTirada().equals(tirada))
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
} catch (Exception ignore) {
System.out.println("Error guardando servicios JSON: " + ignore.getMessage());
}
}
@ -1222,6 +1232,18 @@ public class PresupuestoService {
HashMap<String, Object> result = new HashMap<>();
try {
Presupuesto presupuestoExistente = null;
if (id != null) {
presupuestoExistente = presupuestoRepository.findById(id).orElse(null);
}
if (presupuestoExistente != null) {
// merge de datos que no están en el formulario
presupuesto.setIsReimpresion(presupuestoExistente.getIsReimpresion());
presupuesto.setProveedor(presupuestoExistente.getProveedor());
presupuesto.setProveedorRef1(presupuestoExistente.getProveedorRef1());
presupuesto.setProveedorRef2(presupuestoExistente.getProveedorRef2());
}
presupuesto.setDatosMaquetacionJson(
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
presupuesto.setDatosMarcapaginasJson(
@ -1310,6 +1332,93 @@ public class PresupuestoService {
}
return true;
}
public Boolean hasMaquetacion(Presupuesto presupuesto) {
if (presupuesto.getServiciosJson() != null && !presupuesto.getServiciosJson().isEmpty()) {
if(presupuesto.getServiciosJson().contains("maquetacion")) {
return true;
}
}
return false;
}
public Map<String, Object> getPresupuestoInfoForCard(Presupuesto presupuesto, Locale locale) {
Map<String, Object> resumen = new HashMap<>();
resumen.put("titulo", presupuesto.getTitulo());
resumen.put("imagen",
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
resumen.put("imagen_alt",
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
resumen.put("presupuestoId", presupuesto.getId());
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
resumen.put("hasSample", true);
} else {
resumen.put("hasSample", false);
}
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
resumen.put("tirada", presupuesto.getSelectedTirada());
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
resumen.put("base", presupuesto.getBaseImponible());
resumen.put("iva4", presupuesto.getIvaImporte4());
resumen.put("iva21", presupuesto.getIvaImporte21());
resumen.put("total", Utils.formatCurrency(presupuesto.getTotalConIva(), locale));
resumen.put("resumen", detalles);
return resumen;
}
public Presupuesto findPresupuestoById(Long id) {
return presupuestoRepository.findById(id).orElse(null);
}
public void updateComentario(Long presupuestoId, String comentario) {
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
if (presupuesto != null) {
presupuesto.setComentario(comentario);
presupuestoRepository.saveAndFlush(presupuesto);
}
}
public long duplicarPresupuesto(Long presupuestoId, String titulo) {
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
if (presupuesto != null) {
Presupuesto nuevo = presupuesto.clone();
nuevo.setId(null); // para que se genere uno nuevo
nuevo.setEstado(Presupuesto.Estado.borrador);
nuevo.setTitulo(titulo != null && !titulo.isEmpty() ? titulo : "[D] " + presupuesto.getTitulo());
nuevo.setIsReimpresion(false);
nuevo.setProveedor(null);
nuevo.setProveedorRef1(null);
nuevo.setProveedorRef2(null);
presupuestoRepository.saveAndFlush(nuevo);
return nuevo.getId();
}
return -1;
}
public long reimprimirPresupuesto(Long presupuestoId) {
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
if (presupuesto != null) {
Presupuesto nuevo = presupuesto.clone();
nuevo.setId(null); // para que se genere uno nuevo
nuevo.setEstado(Presupuesto.Estado.borrador);
nuevo.setTitulo("[R] " + presupuesto.getTitulo());
nuevo.setIsReimpresion(true);
presupuestoRepository.saveAndFlush(nuevo);
return nuevo.getId();
}
return -1;
}
// =======================================================================
// Métodos privados

View File

@ -1,10 +1,15 @@
package com.imprimelibros.erp.redsys;
import com.imprimelibros.erp.cart.Cart;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.payments.PaymentService;
import com.imprimelibros.erp.payments.model.Payment;
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
import groovy.util.logging.Log;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -27,6 +32,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@Controller
@ -37,26 +43,37 @@ public class RedsysController {
private final MessageSource messageSource;
private final SpringTemplateEngine templateEngine;
private final ServletContext servletContext;
private final PedidoService pedidoService;
public RedsysController(PaymentService paymentService, MessageSource messageSource,
SpringTemplateEngine templateEngine, ServletContext servletContext) {
SpringTemplateEngine templateEngine, ServletContext servletContext,
PedidoService pedidoService) {
this.paymentService = paymentService;
this.messageSource = messageSource;
this.templateEngine = templateEngine;
this.servletContext = servletContext;
this.pedidoService = pedidoService;
}
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public ResponseEntity<byte[]> crearPago(@RequestParam("amountCents") Long amountCents,
@RequestParam("method") String method, @RequestParam("cartId") Long cartId,
@RequestParam(value = "dirFactId", required = false) Long dirFactId,
HttpServletRequest request,
HttpServletResponse response, Locale locale)
throws Exception {
// Creamos el pedido inteno
Pedido order = pedidoService.crearPedido(cartId, dirFactId, null, null);
if ("bank-transfer".equalsIgnoreCase(method)) {
// 1) Creamos el Payment interno SIN orderId (null)
Payment p = paymentService.createBankTransferPayment(cartId, amountCents, "EUR");
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale,
order.getId());
pedidoService.markPedidoAsProcesingPayment(order.getId());
// 1⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
@ -88,7 +105,104 @@ public class RedsysController {
}
// Tarjeta o Bizum (Redsys)
FormPayload form = paymentService.createRedsysPayment(cartId, amountCents, "EUR", method);
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method,
order.getId());
String html = """
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
<body onload="document.forms[0].submit()">
<form action="%s" method="post">
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
<input type="hidden" name="Ds_Signature" value="%s"/>
<input type="hidden" name="cartId" value="%d"/>
<noscript>
<p>Haz clic en pagar para continuar</p>
<button type="submit">Pagar</button>
</noscript>
</form>
</body></html>
""".formatted(
form.action(),
form.signatureVersion(),
form.merchantParameters(),
form.signature(), cartId);
byte[] body = html.getBytes(StandardCharsets.UTF_8);
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(body);
}
@PostMapping(value = "/reintentar", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public ResponseEntity<byte[]> reintentarPago(@RequestParam("amountCents") Long amountCents,
@RequestParam("method") String method, @RequestParam("orderId") Long orderId,
HttpServletRequest request,
HttpServletResponse response, Locale locale)
throws Exception {
// Creamos el pedido inteno
Pedido order = pedidoService.findById(orderId);
// Find the payment with orderId = order.getId() and status = failed
Payment failedPayment = paymentService.findFailedPaymentByOrderId(order.getId());
if (failedPayment == null) {
throw new Exception("No se encontró un pago fallido para el pedido " + order.getId());
}
Long cartId = null;
Long dirFactId = null;
// Find payment transaction details from failedPayment if needed
try {
Map<String, Long> transactionDetails = paymentService.getPaymentTransactionData(failedPayment.getId());
cartId = transactionDetails.get("cartId");
dirFactId = transactionDetails.get("dirFactId");
} catch (Exception e) {
throw new Exception(
"No se pudieron obtener los detalles de la transacción para el pago " + failedPayment.getId());
}
if ("bank-transfer".equalsIgnoreCase(method)) {
// 1) Creamos el Payment interno SIN orderId (null)
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale,
order.getId());
pedidoService.markPedidoAsProcesingPayment(order.getId());
// 1⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
// 2⃣ Construir el intercambio web desde request/response
response.setContentType("text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
IWebExchange exchange = app.buildExchange(request, response);
// 3⃣ Crear el contexto WebContext con Locale
WebContext ctx = new WebContext(exchange, locale);
String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale);
ctx.setVariable("importe", importeFormateado);
ctx.setVariable("concepto", "TRANSF-" + p.getOrderId());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
boolean isAuth = auth != null
&& auth.isAuthenticated()
&& !(auth instanceof AnonymousAuthenticationToken);
ctx.setVariable("isAuth", isAuth);
// 3) Renderizamos la plantilla a HTML
String html = templateEngine.process("imprimelibros/pagos/transfer", ctx);
byte[] body = html.getBytes(StandardCharsets.UTF_8);
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(body);
}
// Tarjeta o Bizum (Redsys)
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method,
order.getId());
String html = """
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>

View File

@ -49,7 +49,7 @@ public class RedsysService {
// ---------- RECORDS ----------
// Pedido a Redsys
public record PaymentRequest(String order, long amountCents, String description, Long cartId) {
public record PaymentRequest(String order, long amountCents, String description, Long cartId, Long dirFactId) {
}
// Payload para el formulario
@ -84,7 +84,10 @@ public class RedsysService {
// Si tu PaymentRequest no lo lleva todavía, puedes pasarlo en description o
// crear otro campo.
JSONObject ctx = new JSONObject();
ctx.put("cartId", req.cartId()); // o req.cartId() si decides añadirlo al record
ctx.put("cartId", req.cartId());
if (req.dirFactId() != null) {
ctx.put("dirFactId", req.dirFactId());
}
api.setParameter("DS_MERCHANT_MERCHANTDATA", ctx.toString());
if (req.description() != null && !req.description().isBlank()) {
@ -195,6 +198,7 @@ public class RedsysService {
public final long amountCents;
public final String currency;
public final Long cartId;
public final Long dirFactId;
public final String processedPayMethod; // Ds_ProcessedPayMethod
public final String bizumIdOper; // Ds_Bizum_IdOper
public final String authorisationCode; // Ds_AuthorisationCode
@ -206,6 +210,7 @@ public class RedsysService {
this.currency = str(raw.get("Ds_Currency"));
this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
this.cartId = extractCartId(raw.get("Ds_MerchantData"));
this.dirFactId = extractDirFactId(raw.get("Ds_MerchantData"));
this.processedPayMethod = str(raw.get("Ds_ProcessedPayMethod"));
this.bizumIdOper = str(raw.get("Ds_Bizum_IdOper"));
this.authorisationCode = str(raw.get("Ds_AuthorisationCode"));
@ -228,6 +233,24 @@ public class RedsysService {
}
}
private static Long extractDirFactId(Object merchantDataObj) {
if (merchantDataObj == null)
return null;
try {
String json = String.valueOf(merchantDataObj);
// 👇 DES-ESCAPAR las comillas HTML que vienen de Redsys
json = json.replace("&#34;", "\"");
org.json.JSONObject ctx = new org.json.JSONObject(json);
long v = ctx.optLong("dirFactId", 0L);
return v != 0L ? v : null;
} catch (Exception e) {
e.printStackTrace(); // te ayudará si vuelve a fallar
return null;
}
}
public boolean authorized() {
try {
int r = Integer.parseInt(response);

View File

@ -17,4 +17,5 @@ public interface UserService extends UserDetailsService {
* @return página de usuarios
*/
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable);
User findById(Long id);
}

View File

@ -31,4 +31,8 @@ public class UserServiceImpl implements UserService {
if (query == null || query.isBlank()) query = null;
return userDao.searchUsers(role, query, pageable);
}
public User findById(Long id) {
return userDao.findById(id).orElse(null);
}
}

View File

@ -0,0 +1,151 @@
databaseChangeLog:
- changeSet:
id: 0014-create-pedidos-direcciones
author: jjo
changes:
- createTable:
tableName: pedidos_direcciones
columns:
- column:
name: id
type: BIGINT AUTO_INCREMENT
constraints:
primaryKey: true
nullable: false
- column:
name: pedido_linea_id
type: BIGINT
constraints:
nullable: true
- column:
name: pedido_id
type: BIGINT
constraints:
nullable: true
- column:
name: unidades
type: MEDIUMINT UNSIGNED
constraints:
nullable: true
- column:
name: is_facturacion
type: TINYINT(1)
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: is_ejemplar_prueba
type: TINYINT(1)
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: att
type: VARCHAR(150)
constraints:
nullable: false
- column:
name: direccion
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: cp
type: MEDIUMINT UNSIGNED
constraints:
nullable: false
- column:
name: ciudad
type: VARCHAR(100)
constraints:
nullable: false
- column:
name: provincia
type: VARCHAR(100)
constraints:
nullable: false
- column:
name: pais_code3
type: CHAR(3)
defaultValue: esp
constraints:
nullable: false
- column:
name: telefono
type: VARCHAR(30)
constraints:
nullable: false
- column:
name: instrucciones
type: VARCHAR(255)
constraints:
nullable: true
- column:
name: razon_social
type: VARCHAR(150)
constraints:
nullable: true
- column:
name: tipo_identificacion_fiscal
type: ENUM('DNI','NIE','CIF','Pasaporte','VAT_ID')
defaultValue: DNI
constraints:
nullable: false
- column:
name: identificacion_fiscal
type: VARCHAR(50)
constraints:
nullable: true
- column:
name: created_at
type: TIMESTAMP
defaultValueComputed: CURRENT_TIMESTAMP
constraints:
nullable: false
- addForeignKeyConstraint:
baseTableName: pedidos_direcciones
baseColumnNames: pedido_linea_id
referencedTableName: pedidos_lineas
referencedColumnNames: id
constraintName: fk_pedidos_direcciones_pedido_linea
onDelete: SET NULL
onUpdate: CASCADE
- addForeignKeyConstraint:
baseTableName: pedidos_direcciones
baseColumnNames: pedido_id
referencedTableName: pedidos
referencedColumnNames: id
constraintName: fk_pedidos_direcciones_pedidos
onDelete: SET NULL
onUpdate: CASCADE
- createIndex:
tableName: pedidos_direcciones
indexName: idx_pedidos_direcciones_pedido_linea_id
columns:
- column:
name: pedido_linea_id
rollback:
- dropTable:
tableName: pedidos_direcciones
cascadeConstraints: true

View File

@ -0,0 +1,48 @@
databaseChangeLog:
- changeSet:
id: 0015-alter-pedidos-lineas-and-presupuesto-estados
author: jjo
changes:
# Añadir columnas a pedidos_lineas
- addColumn:
tableName: pedidos_lineas
columns:
- column:
name: estado
type: "ENUM('aprobado','maquetación','haciendo_ferro','producción','terminado','cancelado')"
defaultValue: aprobado
constraints:
nullable: false
afterColumn: presupuesto_id
- column:
name: estado_manual
type: TINYINT(1)
defaultValueNumeric: 0
constraints:
nullable: false
afterColumn: estado
# Añadir columna a presupuesto
- addColumn:
tableName: presupuesto
columns:
- column:
name: is_reimpresion
type: TINYINT(1)
defaultValueNumeric: 0
constraints:
nullable: false
rollback:
- dropColumn:
tableName: pedidos_lineas
columnName: estado
- dropColumn:
tableName: pedidos_lineas
columnName: estado_manual
- dropColumn:
tableName: presupuesto
columnName: is_reimpresion

View File

@ -0,0 +1,37 @@
databaseChangeLog:
- changeSet:
id: 0016-fix-enum-estado-pedidos-lineas
author: jjo
changes:
# 1) Convertir valores existentes "maquetación" → "maquetacion"
- update:
tableName: pedidos_lineas
columns:
- column:
name: estado
value: "maquetacion"
where: "estado = 'maquetación'"
# 2) Cambiar ENUM quitando tilde
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: "ENUM('aprobado','maquetacion','haciendo_ferro','producción','terminado','cancelado')"
rollback:
# 1) Volver a convertir "maquetacion" → "maquetación"
- update:
tableName: pedidos_lineas
columns:
- column:
name: estado
value: "maquetación"
where: "estado = 'maquetacion'"
# 2) Restaurar ENUM original
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: "ENUM('aprobado','maquetación','haciendo_ferro','producción','terminado','cancelado')"

View File

@ -0,0 +1,19 @@
databaseChangeLog:
- changeSet:
id: add-fecha-entrega-to-pedidos-lineas
author: jjo
changes:
- addColumn:
tableName: pedidos_lineas
columns:
- column:
name: fecha_entrega
type: datetime
constraints:
nullable: true
afterColumn: estado_manual
rollback:
- dropColumn:
tableName: pedidos_lineas
columnName: fecha_entrega

View File

@ -0,0 +1,24 @@
databaseChangeLog:
- changeSet:
id: 0018-change-presupuesto-ch-3
author: jjo
preConditions:
- dbms:
type: mysql
changes:
- sql:
splitStatements: false
stripComments: true
sql: |
ALTER TABLE presupuesto
DROP CHECK presupuesto_chk_3;
rollback:
- sql:
splitStatements: false
stripComments: true
sql: |
ALTER TABLE presupuesto
ADD CONSTRAINT presupuesto_chk_3
CHECK (tipo_cubierta BETWEEN 0 AND 2);

View File

@ -0,0 +1,32 @@
databaseChangeLog:
- changeSet:
id: 0019-add-estados-pago-to-pedidos-lineas
author: jjo
changes:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)
rollback:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)

View File

@ -0,0 +1,35 @@
databaseChangeLog:
- changeSet:
id: 0020-add-estados-pago-to-pedidos-lineas-2
author: jjo
changes:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'denegado_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)
rollback:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)

View File

@ -0,0 +1,23 @@
databaseChangeLog:
- changeSet:
id: 0021-add-email-and-is-palets-to-pedidos-direcciones
author: jjo
changes:
- sql:
dbms: mysql
splitStatements: false
stripComments: true
sql: >
ALTER TABLE pedidos_direcciones
ADD COLUMN is_palets TINYINT(1) NOT NULL DEFAULT 0 AFTER identificacion_fiscal,
ADD COLUMN email VARCHAR(255) NULL AFTER is_ejemplar_prueba;
rollback:
- sql:
dbms: mysql
splitStatements: false
stripComments: true
sql: >
ALTER TABLE pedidos_direcciones
DROP COLUMN is_palets,
DROP COLUMN email;

View File

@ -0,0 +1,37 @@
databaseChangeLog:
- changeSet:
id: 0022-add-estados-pago-to-pedidos-lineas-3
author: jjo
changes:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'denegado_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'esperando_aceptacion_ferro',
'produccion',
'terminado',
'cancelado'
)
rollback:
- modifyDataType:
tableName: pedidos_lineas
columnName: estado
newDataType: >
enum(
'pendiente_pago',
'procesando_pago',
'denegado_pago',
'aprobado',
'maquetacion',
'haciendo_ferro',
'produccion',
'terminado',
'cancelado'
)

View File

@ -24,4 +24,22 @@ databaseChangeLog:
- include:
file: db/changelog/changesets/0012--drop-unique-gateway-txid-2.yml
- include:
file: db/changelog/changesets/0013-drop-unique-refund-gateway-id.yml
file: db/changelog/changesets/0013-drop-unique-refund-gateway-id.yml
- include:
file: db/changelog/changesets/0014-create-pedidos-direcciones.yml
- include:
file: db/changelog/changesets/0015-alter-pedidos-lineas-and-presupuesto-estados.yml
- include:
file: db/changelog/changesets/0016-fix-enum-estado-pedidos-lineas.yml
- include:
file: db/changelog/changesets/0017-add-fecha-entrega-to-pedidos-lineas.yml
- include:
file: db/changelog/changesets/0018-change-presupuesto-ch-3.yml
- include:
file: db/changelog/changesets/0019-add-estados-pago-to-pedidos-lineas.yml
- include:
file: db/changelog/changesets/0020-add-estados-pago-to-pedidos-lineas-2.yml
- include:
file: db/changelog/changesets/0021-add-email-and-is-palets-to-pedidos-direcciones.yml
- include:
file: db/changelog/changesets/0022-add-estados-pago-to-pedidos-lineas-3.yml

View File

@ -10,6 +10,8 @@ app.add=Añadir
app.back=Volver
app.eliminar=Eliminar
app.imprimir=Imprimir
app.view=Ver
app.pay=Pagar
app.acciones.siguiente=Siguiente
app.acciones.anterior=Anterior
@ -20,6 +22,7 @@ app.logout=Cerrar sesión
app.sidebar.inicio=Inicio
app.sidebar.presupuestos=Presupuestos
app.sidebar.pedidos=Pedidos
app.sidebar.configuracion=Configuración
app.sidebar.usuarios=Usuarios
app.sidebar.direcciones=Mis Direcciones

View File

@ -35,6 +35,8 @@ direcciones.pasaporte=Pasaporte
direcciones.cif=C.I.F.
direcciones.vat_id=VAT ID
direcciones.direccionFacturacion=Dirección de facturación
direcciones.delete.title=Eliminar dirección
direcciones.delete.button=Si, ELIMINAR
direcciones.delete.text=¿Está seguro de que desea eliminar esta dirección?<br>Esta acción no se puede deshacer.

View File

@ -27,6 +27,7 @@ pdf.datos-maquetacion=Datos de maquetación:
pdf.datos-marcapaginas=Datos de marcapáginas:
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península.
pdf.presupuesto-validez=Validez del presupuesto: 30 días desde la fecha de emisión.
pdf.politica-privacidad=Política de privacidad
pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574

View File

@ -13,6 +13,54 @@ checkout.payment.bizum=Bizum
checkout.payment.bank-transfer=Transferencia bancaria
checkout.error.payment=Error al procesar el pago: el pago ha sido cancelado o rechazado Por favor, inténtelo de nuevo.
checkout.success.payment=Pago realizado con éxito. Gracias por su compra.
checkout.error.select-method=Por favor, seleccione un método de pago.
checkout.make-payment=Realizar el pago
checkout.authorization-required=Certifico que tengo los derechos para imprimir los archivos incluidos en mi pedido y me hago responsable en caso de reclamación de los mismos
checkout.authorization-required=Certifico que tengo los derechos para imprimir los archivos incluidos en mi pedido y me hago responsable en caso de reclamación de los mismos
pedido.estado.pendiente_pago=Pendiente de pago
pedido.estado.procesando_pago=Procesando pago
pedido.estado.denegado_pago=Pago denegado
pedido.estado.aprobado=Aprobado
pedido.estado.maquetacion=Maquetación
pedido.estado.haciendo_ferro=Haciendo ferro
pedido.estado.esperando_aceptacion_ferro=Esperando aceptación de ferro
pedido.estado.ferro_cliente=Esperando aprobación de ferro
pedido.estado.produccion=Producción
pedido.estado.terminado=Terminado
pedido.estado.cancelado=Cancelado
pedido.module-title=Pedidos
pedido.pedido=Pedido
pedido.fecha-entrega=Fecha de entrega
pedido.cancelar=Cancelar pedido
pedido.update-estado=Actualizar estado
pedido.maquetacion_finalizada=Maquetación finalizada
pedido.ferro=Ferro
pedido.cubierta=Cubierta
pedido.tapa=Tapa
pedido.aceptar_ferro=Aceptar ferro
pedido.shipping-addresses=Direcciones de envío
pedido.prueba=Prueba
pedido.table.id=Num. Pedido
pedido.table.cliente=Cliente
pedido.table.fecha=Fecha
pedido.table.importe=Importe
pedido.table.estado=Estado
pedido.table.acciones=Acciones
pedido.view.tirada=Tirada
pedido.view.view-presupuesto=Ver presupuesto
pedido.view.aceptar-ferro=Aceptar ferro
pedido.view.ferro-download=Descargar ferro
pedido.view.cub-download=Descargar cubierta
pedido.view.tapa-download=Descargar tapa
pedido.errors.linea-not-found=No se ha encontrado la línea de pedido.
pedido.errors.state-error=Estado de línea no válido.
pedido.errors.update-server-error=Error al actualizar el estado desde el servidor externo.
pedido.errors.connecting-server-error=Error al conectar con el servidor externo.
pedido.errors.cannot-update=No se puede actualizar el estado de una línea con ese estado inicial.
pedido.success.estado-actualizado=Estado del pedido actualizado correctamente.
pedido.success.same-estado=Sin cambios en el estado.

View File

@ -11,6 +11,12 @@ presupuesto.add-to-presupuesto=Añadir al presupuesto
presupuesto.calcular=Calcular
presupuesto.add=Añadir presupuesto
presupuesto.guardar=Guardar
presupuesto.duplicar=Duplicar
presupuesto.reimprimir=Reimprimir
presupuesto.reimpresion=Reimpresión
presupuesto.editar=Editar
presupuesto.ver=Ver
presupuesto.borrar=Eliminar
presupuesto.add-to-cart=Añadir a la cesta
presupuesto.nav.presupuestos-cliente=Presupuestos cliente
@ -37,6 +43,8 @@ presupuesto.tabla.region=Región
presupuesto.tabla.ciudad=Ciudad
presupuesto.tabla.acciones=Acciones
presupuesto.comentario-administrador=Comentarios
# Pestaña datos generales de presupuesto
presupuesto.informacion-libro=Información del libro
presupuesto.datos-generales-descripcion=Datos generales del presupuesto
@ -130,6 +138,7 @@ presupuesto.papel-guardas=Papel de guardas
presupuesto.guardas-impresas=Guardas impresas
presupuesto.no=No
presupuesto.cabezada=Cabezada
presupuesto.cabezada-sin-cabezada=Sin cabezada
presupuesto.cabezada-blanca=Blanca
presupuesto.cabezada-verde=Verde
presupuesto.cabezada-azul=Azul
@ -294,6 +303,30 @@ presupuesto.error.delete-permission-denied=No se puede eliminar: permiso denegad
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
# Mensajes de duplicar presupuesto
presupuesto.duplicar.title=Duplicar presupuesto
presupuesto.duplicar.confirm=Si, DUPLICAR
presupuesto.duplicar.cancelar=Cancelar
presupuesto.duplicar.text=¿Está seguro de que desea duplicar este presupuesto?<br>Se creará una copia exacta del mismo en estado Borrador con el título introducido a continuación.
presupuesto.duplicar.required=El título es obligatorio.
presupuesto.duplicar.success.title=Presupuesto duplicado
presupuesto.duplicar.success.text=El presupuesto ha sido duplicado con éxito.
presupuesto.duplicar.aceptar=Aceptar
presupuesto.duplicar.error.title=Error al duplicar presupuesto
presupuesto.duplicar.error.internal=No se puede duplicar: error interno.
# Mensajes de reimprimir presupuesto
presupuesto.reimprimir.title=Reimprimir presupuesto
presupuesto.reimprimir.confirm=Si, REIMPRIMIR
presupuesto.reimprimir.cancelar=Cancelar
presupuesto.reimprimir.text=¿Está seguro de que desea reimprimir este presupuesto?<br>Se generará una nuevo presupuesto usando los mismos ficheros para su impresión.
presupuesto.reimprimir.success.title=Presupuesto generado
presupuesto.reimprimir.success.text=El presupuesto ha sido generado con éxito.
presupuesto.reimprimir.aceptar=Aceptar
presupuesto.reimprimir.error.title=Error al generar el presupuesto
presupuesto.reimprimir.error.internal=No se puede generar el nuevo presupuesto: error interno.
# Añadir presupuesto
presupuesto.add.tipo=Tipo de presupuesto
presupuesto.add.anonimo=Anónimo

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 KiB

View File

@ -21,6 +21,7 @@ $(() => {
$(this).find('.direccion-id').attr('name', 'direcciones[' + i + '].id');
$(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp');
$(this).find('.direccion-pais-code3').attr('name', 'direcciones[' + i + '].paisCode3');
$(this).find('.is-palets').attr('name', 'direcciones[' + i + '].isPalets');
if ($(this).find('.presupuesto-id').length > 0 && $(this).find('.presupuesto-id').val() !== null
&& $(this).find('.presupuesto-id').val() !== "")
$(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId');

View File

@ -140,6 +140,7 @@ $(() => {
let uri = `/checkout/get-address/${direccionId}`;
const response = await fetch(uri);
if (response.ok) {
$('#dirFactId').val(direccionId);
const html = await response.text();
$('#direccion-div').append(html);
$('#addBillingAddressBtn').addClass('d-none');

View File

@ -0,0 +1,70 @@
import { normalizeNumericFilter } from '../utils.js';
$(() => {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (window.$ && csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
const language = document.documentElement.lang || 'es-ES';
const tablePedidos = $('#pedidos-datatable').DataTable({
processing: true,
serverSide: true,
orderCellsTop: true,
pageLength: 50,
lengthMenu: [10, 25, 50, 100, 500],
order: [[5, 'desc']], // Ordena por fecha por defecto
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true,
dom: 'lBrtip',
buttons: {
dom: {
button: {
className: 'btn btn-sm btn-outline-primary me-1'
},
buttons: [
{ extend: 'copy' },
{ extend: 'csv' },
{ extend: 'excel' },
{ extend: 'pdf' },
{ extend: 'print' },
{ extend: 'colvis' }
],
}
},
ajax: {
url: '/pedidos/datatable',
method: 'GET',
},
order: [[0, 'desc']],
columns: [
{ data: 'id', name: 'id', orderable: true },
{ data: 'cliente', name: 'createdBy.fullName', orderable: true },
{ data: 'created_at', name: 'createdAt', orderable: true },
{ data: 'total', name: 'total', orderable: true },
{ data: 'estado', name: 'estado', orderable: true },
{ data: 'actions', name: 'actions', orderable: false, searchable: false }
],
});
tablePedidos.on("keyup change", ".input-filter", function () {
const colName = $(this).data("col");
const colIndex = tablePedidos.settings()[0].aoColumns.findIndex(c => c.name === colName);
if (colIndex >= 0) {
tablePedidos
.column(colIndex)
.search(normalizeNumericFilter(this.value))
.draw();
}
});
})

View File

@ -0,0 +1,97 @@
$(() => {
$(document).on('click', '.btn-view', function () {
let pedidoId = $(this).data('id');
let url = `/pedidos/view/${pedidoId}`;
window.location.href = url;
});
$(document).on('click', '.btn-pay', async function () {
const pedidoId = parseInt($(this).data('id'));
const amount = parseInt($(this).data('amount'));
const result = await swalMetodoPago();
if (!result.isConfirmed) return;
const method = result.value;
// crear y enviar un form normal (NO ajax)
const form = document.createElement('form');
form.method = 'POST';
form.action = '/pagos/redsys/reintentar';
// CSRF (Spring Security)
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfParam = document.querySelector('meta[name="_csrf_parameter"]')?.getAttribute('content') || '_csrf';
const add = (name, value) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = String(value);
form.appendChild(input);
};
add('amountCents', amount);
add('orderId', pedidoId);
add('method', method);
if (csrfToken) add(csrfParam, csrfToken);
document.body.appendChild(form);
form.submit();
});
function swalMetodoPago() {
return Swal.fire({
title: window.languageBundle['checkout.payment'] || 'Método de pago',
width: '32rem',
html: `
<div style="width: 100%;" class="g-3 text-start">
<div class="form-check card-radio">
<input id="swalPaymentCard" name="paymentMethod" type="radio" class="form-check-input" value="card" checked>
<label class="form-check-label" for="swalPaymentCard">
${window.languageBundle['checkout.payment.card'] || 'Tarjeta'}
</label>
</div>
<div class="form-check card-radio">
<input id="swalPaymentBizum" name="paymentMethod" type="radio" class="form-check-input" value="bizum">
<label class="form-check-label" for="swalPaymentBizum">
${window.languageBundle['checkout.payment.bizum'] || 'Bizum'}
</label>
</div>
<div class="form-check card-radio">
<input id="swalPaymentTransfer" name="paymentMethod" type="radio" class="form-check-input" value="bank-transfer">
<label class="form-check-label" for="swalPaymentTransfer">
${window.languageBundle['checkout.payment.bank-transfer'] || 'Transferencia bancaria'}
</label>
</div>
</div>
`,
focusConfirm: false,
showCancelButton: true,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
preConfirm: () => {
const selected = document.querySelector('input[name="paymentMethod"]:checked');
if (!selected) {
Swal.showValidationMessage(
window.languageBundle['checkout.error.select-method'] || 'Selecciona un método de pago'
);
return false;
}
return selected.value;
}
});
}
})

View File

@ -0,0 +1,146 @@
$(() => {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (window.$ && csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
const language = document.documentElement.lang || 'es-ES';
$(document).on('click', '.update-status-item', function () {
const lineaId = $(this).data('linea-id');
if (!lineaId) {
console.error('No se ha encontrado el ID de la línea del pedido.');
return;
}
// Llamada AJAX para actualizar el estado del pedido
$.ajax({
url: `/pedidos/linea/${lineaId}/update-status`,
type: 'POST',
success: function (response) {
if (!response || !response.success) {
Swal.fire({
icon: 'error',
title: response.message || "Error",
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
showConfirmButton: false
});
}
else {
const estadoSpan = $(`.estado-linea[data-linea-id='${lineaId}']`);
if (estadoSpan.length) {
estadoSpan.text(response.state);
}
if (response.stateKey === 'terminado' || response.stateKey === 'cancelado') {
$(`.update-estado-button[data-linea-id='${lineaId}']`)
.closest('.update-estado-button')
.addClass('d-none');
}
Swal.fire({
icon: 'success',
title: response.message || "Exito",
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
showConfirmButton: false
}).then((result) => {
if (result.dismiss === Swal.DismissReason.timer) {
location.reload();
}
});
;
}
},
error: function (xhr, status, error) {
console.error('Error al actualizar el estado del pedido:', error);
Swal.fire({
icon: 'error',
title: xhr.responseJSON?.message || 'Error',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
}
});
}
});
});
$(document).on('click', '.maquetacion-ok', function () {
const lineaId = $(this).data('linea-id');
if (!lineaId) {
console.error('No se ha encontrado el ID de la línea del pedido.');
return;
}
// Llamada AJAX para marcar la maquetación como OK
$.ajax({
url: `/pedidos/linea/${lineaId}/update-maquetacion`,
type: 'POST',
success: function (response) {
if (!response || !response.success) {
Swal.fire({
icon: 'error',
title: response.message || "Error",
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
showConfirmButton: false
});
}
else {
const estadoSpan = $(`.estado-linea[data-linea-id='${lineaId}']`);
if (estadoSpan.length) {
estadoSpan.text(response.state);
// hide the maquetacion-ok button
$(`.maquetacion-ok[data-linea-id='${lineaId}']`)
.closest('.maquetacion-ok-button')
.addClass('d-none');
}
Swal.fire({
icon: 'success',
title: response.message || "Exito",
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
showConfirmButton: false
});
}
},
error: function (xhr, status, error) {
console.error('Error al actualizar la maquetación del pedido:', error);
Swal.fire({
icon: 'error',
title: xhr.responseJSON?.message || 'Error',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
}
});
}
});
});
})

View File

@ -0,0 +1,86 @@
$(() => {
if ($(".btn-download-ferro").length) {
$(document).on('click', '.btn-download-ferro', function () {
const lineaId = $(this).data('linea-id');
window.open(`/pedidos/linea/${lineaId}/download-ferro`, '_blank');
});
}
if ($(".btn-download-cub").length) {
$(document).on('click', '.btn-download-cub', function () {
const lineaId = $(this).data('linea-id');
window.open(`/pedidos/linea/${lineaId}/download-cub`, '_blank');
});
}
if ($(".btn-download-tapa").length) {
$(document).on('click', '.btn-download-tapa', function () {
const lineaId = $(this).data('linea-id');
window.open(`/pedidos/linea/${lineaId}/download-tapa`, '_blank');
});
}
if ($(".btn-aceptar-ferro").length) {
$(document).on('click', '.btn-aceptar-ferro', function () {
const lineaId = $(this).data('linea-id');
$.ajax({
url: `/pedidos/linea/${lineaId}/aceptar-ferro`,
type: 'POST',
success: function (response) {
if (!response || !response.success) {
Swal.fire({
icon: 'error',
title: response.message || "Error",
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
showConfirmButton: false
});
}
else {
const estadoSpan = $(`.estado-linea[data-linea-id='${lineaId}']`);
if (estadoSpan.length) {
estadoSpan.text(response.state);
}
$(`.btn-aceptar-ferro[data-linea-id='${lineaId}']`)
.closest('.btn-aceptar-ferro')
.addClass('d-none');
Swal.fire({
icon: 'success',
title: response.message || "Exito",
timer: 1800,
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2',
cancelButton: 'btn btn-light'
},
showConfirmButton: false
}).then((result) => {
if (result.dismiss === Swal.DismissReason.timer) {
location.reload();
}
});
;
}
},
error: function (xhr, status, error) {
console.error('Error al aceptar el ferro del pedido:', error);
Swal.fire({
icon: 'error',
title: xhr.responseJSON?.message || 'Error',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
}
});
}
});
});
}
});

View File

@ -0,0 +1,68 @@
import { normalizeNumericFilter } from '../utils.js';
$(() => {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (window.$ && csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
const language = document.documentElement.lang || 'es-ES';
const tablePedidos = $('#pedidos-datatable').DataTable({
processing: true,
serverSide: true,
orderCellsTop: true,
pageLength: 50,
lengthMenu: [10, 25, 50, 100, 500],
order: [[5, 'desc']], // Ordena por fecha por defecto
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
responsive: true,
dom: 'lBrtip',
buttons: {
dom: {
button: {
className: 'btn btn-sm btn-outline-primary me-1'
},
buttons: [
{ extend: 'copy' },
{ extend: 'csv' },
{ extend: 'excel' },
{ extend: 'pdf' },
{ extend: 'print' },
{ extend: 'colvis' }
],
}
},
ajax: {
url: '/pedidos/datatable',
method: 'GET',
},
order: [[0, 'desc']],
columns: [
{ data: 'id', name: 'id', orderable: true },
{ data: 'created_at', name: 'createdAt', orderable: true },
{ data: 'total', name: 'total', orderable: true },
{ data: 'estado', name: 'estado', orderable: true },
{ data: 'actions', name: 'actions', orderable: false, searchable: false }
],
});
tablePedidos.on("keyup change", ".input-filter", function () {
const colName = $(this).data("col");
const colIndex = tablePedidos.settings()[0].aoColumns.findIndex(c => c.name === colName);
if (colIndex >= 0) {
tablePedidos
.column(colIndex)
.search(normalizeNumericFilter(this.value))
.draw();
}
});
})

View File

@ -0,0 +1,82 @@
$(() => {
var snowEditor = document.querySelectorAll(".snow-editor");
if (snowEditor) {
Array.from(snowEditor).forEach(function (item) {
var snowEditorData = {};
var issnowEditorVal = item.classList.contains("snow-editor");
if (issnowEditorVal == true) {
snowEditorData.theme = 'snow',
snowEditorData.modules = {
'toolbar': [
[{
'font': []
}, {
'size': []
}],
['bold', 'italic', 'underline', 'strike'],
[{
'color': []
}, {
'background': []
}],
[{
'script': 'super'
}, {
'script': 'sub'
}],
[{
'header': [false, 1, 2, 3, 4, 5, 6]
}, 'blockquote', 'code-block'],
[{
'list': 'ordered'
}, {
'list': 'bullet'
}, {
'indent': '-1'
}, {
'indent': '+1'
}],
['direction', {
'align': []
}]
]
}
}
var quill = new Quill(item, snowEditorData);
var initialContent = item.dataset.contenido || "";
// Contenido inicial desde Thymeleaf
var initialContent = item.dataset.contenido || "";
if (initialContent) {
if(initialContent.trim() !== "" && initialContent.trim() !== "<p><br></p>")
$('.badge-comentario').removeClass('d-none');
quill.clipboard.dangerouslyPasteHTML(initialContent);
}
quill.root.addEventListener("blur", function () {
let contenido = quill.root.innerHTML;
if(contenido.trim() !== "" && contenido.trim() !== "<p><br></p>"){
$('.badge-comentario').removeClass('d-none');
} else {
$('.badge-comentario').addClass('d-none');
}
if ($('#presupuesto_id').length > 0 && $('#presupuesto_id').val() !== "") {
$.ajax({
url: "/presupuesto/" + $('#presupuesto_id').val() + "/comentario",
method: "POST",
data: {
comentario: contenido
},
success: function () {
}
});
}
});
});
}
});

View File

@ -402,8 +402,6 @@ export default class PresupuestoWizard {
...this.#getInteriorData(),
...this.#getCubiertaData(),
selectedTirada: this.formData.selectedTirada
};
const sobrecubierta = data.sobrecubierta;
@ -927,6 +925,7 @@ export default class PresupuestoWizard {
this.#changeTab('pills-general-data');
} else {
const maxSolapas = data.solapas ?? 120;
const lomo = data.lomo ?? 0;
$('.solapas-presupuesto').attr('max', maxSolapas);
$('.max-solapa-text').text(function (_, textoActual) {
return textoActual.replace(/\d+/, maxSolapas);
@ -951,6 +950,20 @@ export default class PresupuestoWizard {
this.acabadoSobrecubierta.val(this.formData.cubierta.sobrecubierta.acabado);
this.fajaSobrecubierta.val(this.formData.cubierta.faja.acabado);
if(lomo < 10){
this.formData.cubierta.cabezada = "NOCAB";
this.cabezada.val("NOCAB");
this.cabezada.prop("disabled", true);
if(this.formData.cubierta.tipoCubierta === 'tapaDuraLomoRedondo'){
this.formData.cubierta.tipoCubierta = 'tapaDura';
}
$("#tapaDuraLomoRedondo").addClass("d-none");
}
else{
this.cabezada.prop("disabled", false);
$("#tapaDuraLomoRedondo").removeClass("d-none");
}
this.#loadCubiertaData();
this.summaryTableCubierta.removeClass('d-none');
if (this.sobrecubierta.hasClass('active')) {
@ -1683,7 +1696,7 @@ export default class PresupuestoWizard {
const body = {
presupuesto: this.#getPresupuestoData(),
save: !this.opts.canSave,
save: this.opts.canSave,
mode: this.opts.mode,
servicios: servicios,
datosMaquetacion: this.formData.servicios.datosMaquetacion,

View File

@ -0,0 +1,16 @@
import { duplicar, reimprimir } from './presupuesto-utils.js';
$(()=> {
$('.duplicar-btn').on('click', function(e) {
e.preventDefault();
const id = $('#presupuesto_id').val();
const tituloOriginal = $('#titulo').text().trim() == '' ? $('#titulo').val().trim() : $('#titulo').text().trim();
duplicar(id, tituloOriginal);
})
$('.reimprimir-btn').on('click', function(e) {
e.preventDefault();
const id = $('#presupuesto_id').val();
reimprimir(id);
})
})

View File

@ -1,4 +1,4 @@
import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
import { preguntarTipoPresupuesto, duplicar, reimprimir } from './presupuesto-utils.js';
(() => {
// si jQuery está cargado, añade CSRF a AJAX
@ -200,6 +200,26 @@ import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
}
});
$('#presupuestos-clientes-datatable').on('click', '.btn-duplicate-privado', function (e) {
e.preventDefault();
const id = $(this).data('id');
let data = table_clientes.row($(this).parents('tr')).data();
const tituloOriginal = data.titulo;
duplicar(id, tituloOriginal);
});
$('#presupuestos-clientes-datatable').on('click', '.btn-reprint-privado', function (e) {
e.preventDefault();
const id = $(this).data('id');
let data = table_clientes.row($(this).parents('tr')).data();
const tituloOriginal = data.titulo;
reimprimir(id, tituloOriginal);
});
$('#presupuestos-clientes-datatable').on('click', '.btn-delete-privado', function (e) {
e.preventDefault();

View File

@ -75,3 +75,171 @@ export async function preguntarTipoPresupuesto() {
}
}).then((r) => (r.isConfirmed ? r.value : null));
}
export function duplicar(id, titulo) {
// swal with input
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.title']) || 'Duplicar presupuesto',
html: window.languageBundle.get(['presupuesto.duplicar.text']) || `¿Deseas duplicar el presupuesto "${titulo}"?`,
icon: 'question',
input: 'text',
inputValue: `[D] ${titulo}`,
showCancelButton: true,
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.confirm']) || 'Sí, duplicar',
cancelButtonText: window.languageBundle.get(['presupuesto.duplicar.cancelar']) || 'Cancelar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
inputValidator: (value) => {
if (!value) {
return window.languageBundle.get(['presupuesto.duplicar.required']) || 'El título no puede estar vacío';
}
},
}).then((result) => {
if (result.isConfirmed) {
const tituloNuevo = result.value;
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (window.$ && csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
$.ajax({
url: `/presupuesto/api/duplicar/${id}`,
data: {
titulo: tituloNuevo,
},
method: 'POST',
success: function (response) {
if (response.id && response.id > 0) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.success.title']) || 'Presupuesto duplicado',
text: window.languageBundle.get(['presupuesto.duplicar.success.text']) || `El presupuesto "${titulo}" ha sido duplicado correctamente.`,
icon: 'success',
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
}).then(() => {
// Recargar la página o redirigir a la lista de presupuestos
window.location.href = '/presupuesto/edit/' + response.id;
});
} else {
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.error.title']) || 'Error al duplicar',
text: response.message || window.languageBundle.get(['presupuesto.duplicar.error.internal']) || 'Ha ocurrido un error al duplicar el presupuesto.',
icon: 'error',
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
});
}
},
error: function (xhr) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.duplicar.error.title']) || 'Error al duplicar',
text: xhr.responseJSON?.message || window.languageBundle.get(['presupuesto.duplicar.error.internal']) || 'Ha ocurrido un error al duplicar el presupuesto.',
icon: 'error',
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
});
}
});
};
});
}
export function reimprimir(id) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.reimprimir.title']) || 'Reimprimir presupuesto',
html: window.languageBundle.get(['presupuesto.reimprimir.text']) || `¿Deseas reimprimir el presupuesto?`,
icon: 'question',
showCancelButton: true,
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.confirm']) || 'Sí, reimprimir',
cancelButtonText: window.languageBundle.get(['presupuesto.reimprimir.cancelar']) || 'Cancelar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
cancelButton: 'btn btn-light' // clases para cancelar
},
}).then((result) => {
if (result.isConfirmed) {
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
if (window.$ && csrfToken && csrfHeader) {
$.ajaxSetup({
beforeSend: function (xhr) {
xhr.setRequestHeader(csrfHeader, csrfToken);
}
});
}
$.ajax({
url: `/presupuesto/api/reimprimir/${id}`,
method: 'POST',
success: function (response) {
if (response.id && response.id > 0) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.reimprimir.success.title']) || 'Presupuesto reimpreso',
text: window.languageBundle.get(['presupuesto.reimprimir.success.text']) || `El presupuesto ha sido reimpreso correctamente.`,
icon: 'success',
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
}).then(() => {
// Recargar la página o redirigir a la lista de presupuestos
window.location.href = '/presupuesto/edit/' + response.id;
});
} else {
Swal.fire({
title: window.languageBundle.get(['presupuesto.reimprimir.error.title']) || 'Error al reimprimir',
text: response.message || window.languageBundle.get(['presupuesto.reimprimir.error.internal']) || 'Ha ocurrido un error al reimprimir el presupuesto.',
icon: 'error',
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
});
}
},
error: function (xhr) {
Swal.fire({
title: window.languageBundle.get(['presupuesto.reimprimir.error.title']) || 'Error al reimprimir',
text: xhr.responseJSON?.message || window.languageBundle.get(['presupuesto.reimprimir.error.internal']) || 'Ha ocurrido un error al reimprimir el presupuesto.',
icon: 'error',
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.aceptar']) || 'Aceptar',
buttonsStyling: false,
customClass: {
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
},
});
}
});
};
});
}

View File

@ -31,7 +31,7 @@ $(() => {
e.preventDefault();
// obtén el id de donde lo tengas (data-attr o variable global)
const id = $('#presupuesto-id').val();
const id = $('#presupuesto_id').val();
const url = `/api/pdf/presupuesto/${id}?mode=download`;
@ -47,7 +47,7 @@ $(() => {
$('.add-cart-btn').on('click', async () => {
const presupuestoId = $('#presupuesto-id').val();
const presupuestoId = $('#presupuesto_id').val();
const res = await $.ajax({
url: `/cart/add/${presupuestoId}`,
method: 'POST',

View File

@ -50,6 +50,7 @@
<input type="hidden" name="amountCents" th:value="${summary.amountCents}" />
<input type="hidden" name="method" value="card" />
<input type="hidden" name="cartId" th:value="${summary.cartId}" />
<input type="hidden" id="dirFactId" name="dirFactId" value="" />
<button id="btn-checkout" type="submit" class="btn btn-secondary w-100 mt-2"
th:text="#{checkout.make-payment}" disabled>Checkout</button>
</form>

View File

@ -7,7 +7,7 @@
<input type="hidden" class="direccion-cp" th:value="${direccion.cp}" />
<input type="hidden" class="direccion-pais-code3" th:value="${direccion.pais.code3}" />
<input type="hidden" class="item-tirada" th:value="${unidades != null ? unidades : ''}" />
<input type="hidden" class="is-palets" th:value="${isPalets != null ? isPalets : ''}" />
<input type="hidden" class="is-palets" th:value="${isPalets != null and (isPalets==1 or isPalets==true)? 1 : 0}" />
<div class="row g-3 align-items-start flex-nowrap">
<div class="col">

View File

@ -0,0 +1,40 @@
<div th:fragment="direccionEnvioCard(direccion, pais)" name="direccion"
class="card card border mb-3 direccion-card mx-2">
<div class="card-body position-relative">
<th:block th:if="${direccion.unidades != null}">
<div class="position-absolute top-0 end-0 mt-2 me-3 text-end">
<div th:if="${direccion.isEjemplarPrueba}">
<span class="mb-2 fw-semibold d-block text-muted text-uppercase"
th:text="#{pedido.prueba}"></span>
</div>
<div th:if="${!direccion.isEjemplarPrueba}">
<!-- singular -->
<span class="mb-2 fw-semibold d-block text-muted text-uppercase"
th:if="${direccion.unidades == 1}"
th:text="|${direccion.unidades} #{cart.shipping.ud}|"></span>
<!-- plural -->
<span class="mb-2 fw-semibold d-block text-muted text-uppercase"
th:unless="${direccion.unidades == 1}"
th:text="|${direccion.unidades} #{cart.shipping.uds}|"></span>
</div>
</div>
</th:block>
<!-- TEXTO: usa TODO el ancho -->
<span class="fs-14 mb-1 d-block text-break" th:text="${direccion.att}"></span>
<span class="text-muted fw-normal text-wrap mb-1 d-block text-break"
th:text="${direccion.direccion}"></span>
<span class="text-muted fw-normal text-wrap mb-1 d-block text-break"
th:text="${direccion.cp} + ', ' + ${direccion.ciudad} + ', (' + ${direccion.provincia} + ')'">
</span>
<span class="text-muted fw-normal d-block text-break" th:text="${pais}"></span>
<span class="text-muted fw-normal d-block text-break"
th:text="#{'direcciones.telefono'} + ': ' + ${direccion.telefono}"></span>
<span class="fw-normal d-block text-break" th:text="${direccion.razonSocial}"></span>
<span class="fw-normal d-block text-break" th:text="${direccion.identificacionFiscal}"></span>
</div>
</div>
</div>

View File

@ -0,0 +1,28 @@
<div th:fragment="direccionFacturacionCard(direccion, pais)" name="direccionFacturacion"
class="card card border mb-3 direccion-facturacion-card">
<div class="card-header bg-light">
<span class="fs-16" th:text="#{'direcciones.direccionFacturacion'}"></span>
</div>
<div class="card-body">
<div class="d-flex align-items-start g-3 flex-nowrap w-auto">
<div class="flex-shrink-0">
<img src="/assets/images/billing_address2.gif" style="width: 100px; height: auto;"
alt="Billing Address">
</div>
<div class="flex-nowrap">
<span class="fs-14 mb-1 d-block text-break" th:text="${direccion.att}"></span>
<span class="text-muted fw-normal text-wrap mb-1 d-block text-break"
th:text="${direccion.direccion}"></span>
<span class="text-muted fw-normal text-wrap mb-1 d-block text-break"
th:text="${direccion.cp} + ', ' + ${direccion.ciudad} + ', (' + ${direccion.provincia} + ')'">
</span>
<span class="text-muted fw-normal d-block text-break" th:text="${pais}"></span>
<span class="text-muted fw-normal d-block text-break"
th:text="#{'direcciones.telefono'} + ': ' + ${direccion.telefono}"></span>
<span class="fw-normal d-block text-break" th:text="${direccion.razonSocial}"></span>
<span class="fw-normal d-block text-break" th:text="${direccion.identificacionFiscal}"></span>
</div>
</div>
</div>
</div>

View File

@ -43,6 +43,11 @@
<i class="ri-file-paper-2-line"></i> <span th:text="#{app.sidebar.presupuestos}">Presupuestos</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link menu-link" href="/pedidos">
<i class="ri-book-3-line"></i> <span th:text="#{app.sidebar.pedidos}">Pedidos</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link menu-link" href="/direcciones">
<i class="ri-truck-line"></i>

View File

@ -139,6 +139,7 @@
<div class="footer">
<div class="fw-bold fs-6 mb-1" th:text="#{pdf.incluye-envio}">El presupuesto incluye el envío a una dirección de la
península.</div>
<div class="fw-bold fs-6 mb-1" th:text="#{pdf.presupuesto-validez}">Validez del presupuesto: 30 días desde la fecha de emisión.</div>
<div class="privacy">
<div class="pv-title" th:text="#{pdf.politica-privacidad}">Política de privacidad</div>
<div class="pv-text" th:text="#{pdf.politica-privacidad.responsable}">Responsable: Impresión Imprime Libros -

View File

@ -0,0 +1,167 @@
<div th:fragment="pedido-linea (item, isAdmin)">
<input type="hidden" id="lineaId" th:value="${item.lineaId}" />
<div class="mb-3">
<div class="card p-3 mb-0">
<div class="row g-3 align-items-start">
<!-- Col 1: imagen -->
<div class="col-auto">
<div class="avatar-lg bg-light rounded p-1">
<img th:src="${item.imagen != null ? item.imagen : '/assets/images/products/placeholder.png'}"
alt="portada" class="img-fluid d-block rounded">
</div>
</div>
<!-- Col 2: detalles -->
<div class="col">
<h5 class="fs-18 text-truncate mb-1">
<span class="text-dark"
th:text="${item.titulo != null ? item.titulo : 'Presupuesto #'}">Presupuesto</span>
</h5>
<h5 class="fs-14 text-truncate mb-1">
<span th:text="#{cart.item.presupuesto-numero}">Presupuesto #</span>
<span th:text="${item.presupuestoId != null ? item.presupuestoId : ''}">#</span>
<a th:href="@{|/presupuesto/edit/${item.presupuestoId}|}"
th:text="#{pedido.view.view-presupuesto}" class="badge bg-secondary">Ver presupuesto</a>
</h5>
<ul class="list-unstyled text-muted mb-1 ps-0">
<li th:each="linea : ${item.resumen.lineas}" class="mb-1">
<span th:utext="${linea['descripcion']}"></span>
</li>
</ul>
<ul class="list-unstyled text-muted mb-1" th:if="${item.resumen.servicios != null}">
<li>
<span th:utext="#{pdf.servicios-adicionales}">Servicios adicionales:</span>
<span class="spec-label" th:text="${item.resumen.servicios}"></span>
</li>
</ul>
<ul class="list-unstyled text-muted mb-1"
th:if="${item.resumen != null and #maps.containsKey(item.resumen,'datosMaquetacion') and item.resumen['datosMaquetacion'] != null}">
<li class="spec-row mb-1">
<span th:text="#{pdf.datos-maquetacion}">Datos de maquetación:</span>
<span th:utext="${item.resumen.datosMaquetacion}"></span>
</li>
</ul>
<ul class="list-unstyled text-muted mb-1"
th:if="${item.resumen != null and #maps.containsKey(item.resumen,'datosMarcapaginas') and item.resumen['datosMarcapaginas'] != null}">
<li class="spec-row mb-1">
<span th:text="#{pdf.datos-marcapaginas}">Datos de marcapáginas:</span>
<span th:utext="${item.resumen.datosMarcapaginas}"></span>
</li>
</ul>
</div>
<!-- Col 3: precio -->
<div class="col-auto ms-auto text-end">
<p class="text-muted mb-1" th:text="#{cart.precio}">Precio</p>
<h5 class="fs-14 mb-0">
<span th:text="${item.baseTotal != null ? item.total : '-'}">0,00</span>
</h5>
<p class="text-muted mt-4 mb-1" th:text="#{pedido.table.estado}">Estado</p>
<h5 class="fs-14 mb-0">
<span class="estado-linea" th:attr="data-linea-id=${item.lineaId}" th:text="${item.estado != null} ?
#{__${'pedido.estado.' + item.estado}__} : '-'">
</span>
</h5>
<div class="col-12 d-grid gap-2 mt-2">
<button th:if="${item.estado.name == 'esperando_aceptacion_ferro'}" type="button"
class="btn btn-secondary w-100 btn-aceptar-ferro" th:text="#{pedido.view.aceptar-ferro}"
th:attr="data-linea-id=${item.lineaId}">
Aceptar ferro
</button>
<button th:if="${item.estado.priority >= 7 && item.estado.priority < 11 && item.buttons.ferro}"
type="button" class="btn btn-light w-100 btn-download-ferro"
th:text="#{pedido.view.ferro-download}"
th:attr="data-linea-id=${item.lineaId}">
Descargar ferro
</button>
<button th:if="${item.estado.priority >= 7 && item.estado.priority < 11 && item.buttons.cub}"
type="button" class="btn btn-light w-100 btn-download-cub"
th:text="#{pedido.view.cub-download}"
th:attr="data-linea-id=${item.lineaId}">
Descargar cubierta
</button>
<button th:if="${item.estado.priority >= 7 && item.estado.priority < 11 && item.buttons.tapa}"
type="button" class="btn btn-light w-100 btn-download-tapa"
th:text="#{pedido.view.tapa-download}"
th:attr="data-linea-id=${item.lineaId}">
Descargar tapa
</button>
</div>
<th:block th:if="${item.fechaEntrega != null and item.fechaEntrega != ''}">
<p class="text-muted mt-4 mb-1" th:text="#{pedido.fecha-entrega}">Fecha de entrega</p>
<h5 class="fs-14 mb-0">
<span th:text="${item.fechaEntrega}">-</span>
</h5>
</th:block>
</div>
</div>
<div card class="mt-3">
<div class="card-header bg-light p-3">
<span class="mb-0 fs-16" th:text="#{pedido.shipping-addresses}">Direcciones de envío</span>
</div>
<div class="card-body p-3">
<div class="row g-3">
<div class="col-auto d-none d-md-block">
<div class="flex-shrink-0">
<img src="/assets/images/delivery-truck.gif" style="width: 120px; height: auto;"
alt="delivery">
</div>
</div>
<div class="col">
<div class="row g-3">
<div th:each="direccionEnvio : ${item.direccionesEntrega}"
class="mb-3 col-xl-4 col-lg-6 col-md-12 col-sm-12">
<div th:insert="~{imprimelibros/direcciones/direccionEnvioCard :: direccionEnvioCard(
direccion=${direccionEnvio},
pais=${direccionEnvio.paisNombre}
)}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div th:if="${isAdmin}" class="card-footer bg-light p-3">
<div class="row align-items-center gy-3">
<div class="col-sm">
<div class="d-flex flex-wrap my-n1">
<!-- Botón cancelar -->
<div th:if="${item.estado.name != 'cancelado' && item.estado.name != 'terminado'}">
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 cancel-item"
th:attr="data-linea-id=${item.lineaId}">
<i class="ri-delete-bin-fill text-muted align-bottom me-1"><span
th:text="#{pedido.cancelar}">Cancelar Pedido</span></i>
</a>
</div>
<!-- Actualizar estado-->
<div class="update-estado-button"
th:if="${item.estado.name != 'cancelado' && item.estado.name != 'maquetacion' && item.estado.name != 'terminado'}">
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 update-status-item"
th:attr="data-linea-id=${item.lineaId}">
<i class="ri-refresh-line text-muted align-bottom me-1"><span
th:text="#{pedido.update-estado}">Actualizar estado</span></i>
</a>
</div>
<div class="maquetacion-ok-button" th:if="${item.estado.name == 'maquetacion'}">
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 maquetacion-ok"
th:attr="data-linea-id=${item.lineaId}">
<i class="ri-check-double-line text-muted align-bottom me-1"><span
th:text="#{pedido.maquetacion_finalizada}"> Maquetación finalizada</span></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,93 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{imprimelibros/layout}">
<head>
<th:block layout:fragment="pagetitle" />
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
<th:block layout:fragment="pagecss">
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
</th:block>
</head>
<body>
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}"
sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<th:block layout:fragment="content">
<div th:if="${#authorization.expression('isAuthenticated()')}">
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
<li class="breadcrumb-item active" aria-current="page" th:text="#{pedido.module-title}">
Pedidos</li>
</ol>
</nav>
</div>
<div class="container-fluid">
<table id="pedidos-datatable" class="table table-striped table-nowrap responsive w-100">
<thead>
<tr>
<th class="text-start" scope="col" th:text="#{pedido.table.id}">Num. Pedido</th>
<th class="text-start" scope="col" th:text="#{pedido.table.fecha}">Fecha</th>
<th class="text-start" scope="col" th:text="#{pedido.table.importe}">Importe</th>
<th class="text-start" scope="col" th:text="#{pedido.table.estado}">Estado</th>
<th class="text-start" scope="col" th:text="#{pedido.table.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm input-filter" data-col="id" /></th>
<th></th>
<th></th>
<th>
<select class="form-select form-select-sm input-filter" data-col="estado">
<option value=""></option>
<option th:text="#{pedido.estado.aprobado}" value="aprobado"></option>
<option th:text="#{pedido.estado.maquetacion}" value="maquetacion"></option>
<option th:text="#{pedido.estado.haciendo_ferro}" value="haciendo_ferro"></option>
<option th:text="#{pedido.estado.produccion}" value="produccion"></option>
<option th:text="#{pedido.estado.terminado}" value="terminado"></option>
<option th:text="#{pedido.estado.cancelado}" value="cancelado"></option>
</select>
</th>
<th></th> <!-- Acciones (sin filtro) -->
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
<!-- JS de Buttons y dependencias -->
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.html5.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pedidos/pedidos-common.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pedidos/pedidos.js}"></script>
</th:block>
</body>
</html>

View File

@ -0,0 +1,95 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{imprimelibros/layout}">
<head>
<th:block layout:fragment="pagetitle" />
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
<th:block layout:fragment="pagecss">
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
</th:block>
</head>
<body>
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}"
sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<th:block layout:fragment="content">
<div th:if="${#authorization.expression('isAuthenticated()')}">
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
<li class="breadcrumb-item active" aria-current="page" th:text="#{pedido.module-title}">
Pedidos</li>
</ol>
</nav>
</div>
<div class="container-fluid">
<table id="pedidos-datatable" class="table table-striped table-nowrap responsive w-100">
<thead>
<tr>
<th class="text-start" scope="col" th:text="#{pedido.table.id}">Num. Pedido</th>
<th class="text-start" scope="col" th:text="#{pedido.table.cliente}">Cliente</th>
<th class="text-start" scope="col" th:text="#{pedido.table.fecha}">Fecha</th>
<th class="text-start" scope="col" th:text="#{pedido.table.importe}">Importe</th>
<th class="text-start" scope="col" th:text="#{pedido.table.estado}">Estado</th>
<th class="text-start" scope="col" th:text="#{pedido.table.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm input-filter" data-col="id" /></th>
<th><input type="text" class="form-control form-control-sm input-filter" data-col="createdBy.fullName" /></th>
<th></th>
<th></th>
<th>
<select class="form-select form-select-sm input-filter" data-col="estado">
<option value=""></option>
<option th:text="#{pedido.estado.aprobado}" value="aprobado"></option>
<option th:text="#{pedido.estado.maquetacion}" value="maquetacion"></option>
<option th:text="#{pedido.estado.haciendo_ferro}" value="haciendo_ferro"></option>
<option th:text="#{pedido.estado.produccion}" value="produccion"></option>
<option th:text="#{pedido.estado.terminado}" value="terminado"></option>
<option th:text="#{pedido.estado.cancelado}" value="cancelado"></option>
</select>
</th>
<th></th> <!-- Acciones (sin filtro) -->
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
<!-- JS de Buttons y dependencias -->
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.html5.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pedidos/pedidos-common.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pedidos/pedidos-admin.js}"></script>
</th:block>
</body>
</html>

View File

@ -0,0 +1,83 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{imprimelibros/layout}">
<head>
<th:block layout:fragment="pagetitle" />
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
<th:block layout:fragment="pagecss">
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
</th:block>
</head>
<body>
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}"
sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<th:block layout:fragment="content">
<div th:if="${#authorization.expression('isAuthenticated()')}">
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
<li class="breadcrumb-item" aria-current="page" th:text="#{pedido.module-title}"><a
href="/pedidos"></a>
Pedidos</li>
<li class="breadcrumb-item active" aria-current="page">
<span th:text="#{pedido.pedido} + ' '">Pedido </span><span th:text="${id}"></span>
</li>
</ol>
</nav>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md-auto">
<div th:insert="~{imprimelibros/direcciones/direccionFacturacionCard ::
direccionFacturacionCard(
direccion=${direccionFacturacion},
pais=${direccionFacturacion != null ? direccionFacturacion.paisNombre : ''}
)}">
</div>
</div>
</div>
<th:block th:each="linea: ${lineas}">
<div
th:insert="~{imprimelibros/pedidos/pedidos-linea :: pedido-linea (item=${linea}, isAdmin=${isAdmin})}">
</div>
</th:block>
</div>
</div>
</th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
<!-- JS de Buttons y dependencias -->
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.html5.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/pedidos/pedidos-view.js}"></script>
<script th:if="${isAdmin}" type="module" th:src="@{/assets/js/pages/imprimelibros/pedidos/pedidos-view-admin.js}"></script>
</th:block>
</body>
</html>

View File

@ -13,6 +13,12 @@
<span th:text="#{app.imprimir}">Imprimir</span>
</button>
<button th:if="${appMode == 'edit'}" type="button"
class="btn btn-secondary d-flex align-items-center mx-2 duplicar-btn">
<i class="ri-file-copy-2-line me-2"></i>
<span th:text="#{presupuesto.duplicar}">Duplicar</span>
</button>
<button th:if="${appMode == 'add' or appMode == 'edit'}" type="button"
class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn">
<i class="ri-shopping-cart-line me-2"></i>

View File

@ -151,6 +151,7 @@
<div class="col-auto mb-3">
<label for="cabezada" class="form-label" th:text="#{presupuesto.cabezada}">Cabezada</label>
<select class="form-select select2 datos-cubierta tapa-cubierta-summary" id="cabezada">
<option value="NOCAB" th:text="#{presupuesto.cabezada-sin-cabezada}">Sin cabezada</option>
<option value="WHI" th:text="#{presupuesto.cabezada-blanca}" selected>Blanca</option>
<option value="GRE" th:text="#{presupuesto.cabezada-verde}">Verde</option>
<option value="BLUE" th:text="#{presupuesto.cabezada-azul}">Azul</option>

View File

@ -16,6 +16,8 @@
<div class="col-9 mx-auto mt-4">
<h5 id="resumen-titulo" class="text-center"></h5>
<h6 th:if="${presupuesto?.isReimpresion}" th:text="#{presupuesto.reimpresion}"
class="text-uppercase bg-danger text-white text-center">REIMPRESION</h6>
<table id="resumen-tabla-final" class="table table-borderless table-striped mt-3"
th:data-currency="#{app.currency}">
<thead>

View File

@ -4,6 +4,8 @@
<div class="d-flex">
<div class="flex-grow-1">
<h5 th:text="#{presupuesto.resumen-presupuesto}">Resumen presupuesto</h5>
<h6 th:if="${presupuesto?.isReimpresion}" th:text="#{presupuesto.reimpresion}"
class="text-uppercase bg-danger text-white text-center">REIMPRESION</h6>
</div>
</div>
</div>

View File

@ -42,18 +42,21 @@
<div class="container-fluid">
<input type="hidden" id="presupuesto-id" th:value="${presupuesto.id}" />
<input type="hidden" id="presupuesto_id" th:value="${presupuesto.id}" />
<div class="row" id="card presupuesto-row animate-fadeInUpBounce">
<div class="card">
<div class="card-header">
<h4 class="card-title mb-0 text-uppercase" th:text="${resumen.titulo}">Resumen del
<h4 id="titulo" class="card-title mb-0 text-uppercase" th:text="${resumen.titulo}">Resumen del
presupuesto</h4>
</div>
<div class="card-body">
<div th:if="${presupuesto.isReimpresion}" class="row">
<h6 th:text="#{presupuesto.reimpresion}" class="bg-danger py-2 text-center text-white text-uppercase">REIMPRESION</h6>
</div>
<div class="card col-12 col-sm-9 mx-auto">
<h5 id="resumen-titulo" class="text-center"></h5>
@ -77,7 +80,9 @@
</thead>
<tbody>
<tr th:if="${resumen['linea0']}">
<td><img style="max-width: 60px; height: auto;" th:src="${resumen['imagen']}" th:alt="${resumen['imagen_alt']}" class="img-fluid" /></td>
<td><img style="max-width: 60px; height: auto;"
th:src="${resumen['imagen']}" th:alt="${resumen['imagen_alt']}"
class="img-fluid" /></td>
<td class="text-start" th:utext="${resumen['linea0'].descripcion}">
Descripción 1</td>
<td class="text-end" th:text="${resumen['linea0'].cantidad}">1</td>
@ -159,12 +164,50 @@
</button>
<button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn">
<i class="ri-shopping-cart-line me-2"></i>
<span th:text="#{presupuesto.add-to-cart}">Añadir a la cesta</span>
class="btn btn-secondary d-flex align-items-center mx-2 duplicar-btn">
<i class="ri-file-copy-2-line me-2"></i>
<span th:text="#{presupuesto.duplicar}">Duplicar</span>
</button>
<button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 reimprimir-btn">
<i class="ri-printer-line me-2"></i>
<span th:text="#{presupuesto.reimprimir}">Reimprimir</span>
</button>
</div>
<div sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<div class="accordion lefticon-accordion custom-accordionwithicon accordion-border-box mt-3"
id="accordionlefticon">
<div class="accordion-item material-shadow">
<h2 class="accordion-header" id="accordionComentario">
<button class="accordion-button collapsed" type="button"
data-bs-toggle="collapse"
data-bs-target="#accor_accordionComentario" aria-expanded="false"
aria-controls="accor_accordionComentario">
<span
th:text="#{presupuesto.comentario-administrador}">Comentario</span>
<span class="d-none badge badge-comentario bg-danger ms-1">!</span>
</button>
</h2>
<div id="accor_accordionComentario" class="accordion-collapse collapse"
aria-labelledby="accordionComentario"
data-bs-parent="#accordionlefticon">
<div class="accordion-body">
<div class="snow-editor" id="comentario" name="comentario"
th:attr="data-contenido=${presupuesto.comentario} "
style=" height: 300px;">
</div> <!-- end Snow-editor-->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -189,6 +232,7 @@
</div>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestos/resumen-view.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestos/duplicate-reprint.js}"></script>
</th:block>
</body>

View File

@ -33,7 +33,8 @@
aria-controls="pills-general-data" aria-selected="true">
<i
class="ri-information-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
<label class="fs-13 my-2" th:text="#{presupuesto.datos-generales}">Datos Generales</label>
<label class="fs-13 my-2" th:text="#{presupuesto.datos-generales}">Datos
Generales</label>
</button>
</li>
<li class="nav-item" role="presentation">
@ -60,7 +61,8 @@
aria-controls="pills-seleccion-tirada" aria-selected="false">
<i
class="ri-add-box-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></i>
<label class="fs-13 my-2" th:text="#{presupuesto.seleccion-tirada}">Seleccion de tirada</label>
<label class="fs-13 my-2" th:text="#{presupuesto.seleccion-tirada}">Seleccion de
tirada</label>
</button>
</li>
<li class="nav-item" role="presentation">
@ -137,6 +139,34 @@
</div>
<!-- end tab content -->
<div sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<div class="accordion lefticon-accordion custom-accordionwithicon accordion-border-box mt-3"
id="accordionlefticon">
<div class="accordion-item material-shadow">
<h2 class="accordion-header" id="accordionComentario">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#accor_accordionComentario" aria-expanded="false"
aria-controls="accor_accordionComentario">
<span th:text="#{presupuesto.comentario-administrador}">Comentario</span>
<span class="d-none badge badge-comentario bg-danger ms-1">!</span>
</button>
</h2>
<div id="accor_accordionComentario" class="accordion-collapse collapse"
aria-labelledby="accordionComentario" data-bs-parent="#accordionlefticon">
<div class="accordion-body">
<div class="snow-editor" id="comentario" name="comentario"
th:attr="data-contenido=${presupuesto.comentario} "
style=" height: 300px;">
</div> <!-- end Snow-editor-->
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- end card body -->

View File

@ -10,6 +10,8 @@
</th:block>
<th:block layout:fragment="pagecss">
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet" />
<link sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
th:href="@{/assets/libs/quill/quill.snow.css}" rel="stylesheet" type="text/css" />
</th:block>
</head>
@ -19,62 +21,77 @@
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}"
sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')">
<th:block layout:fragment="content">
<div th:if="${#authorization.expression('isAuthenticated()')}">
<th:block layout:fragment="content">
<div th:if="${#authorization.expression('isAuthenticated()')}">
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
<li class="breadcrumb-item"><a href="/presupuesto" th:text="#{presupuesto.title}"></a></li>
<li class="breadcrumb-item active" aria-current="page" th:if="${appMode == 'add'}" th:text="#{presupuesto.add}">
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
<li class="breadcrumb-item"><a href="/presupuesto" th:text="#{presupuesto.title}"></a></li>
<li class="breadcrumb-item active" aria-current="page" th:if="${appMode == 'add'}"
th:text="#{presupuesto.add}">
Nuevo presupuesto
</li>
<li class="breadcrumb-item active" aria-current="page" th:text="#{presupuesto.editar.title}" th:if="${appMode == 'edit'}">
<li class="breadcrumb-item active" aria-current="page" th:text="#{presupuesto.editar.title}"
th:if="${appMode == 'edit'}">
Editar presupuesto
</li>
</ol>
</nav>
</div>
<div class="container-fluid">
<!-- alert info -->
<div th:if="${appMode} == 'view'" class="alert alert-warning fade show" role="alert">
<i class="ri-information-fill me-1 align-middle"></i>
<span th:text="#{presupuesto.info.presupuestos-anonimos-view}"></span>
</ol>
</nav>
</div>
<div th:insert="~{imprimelibros/presupuestos/presupuestador :: presupuestador}"></div>
</div>
</div>
</th:block>
<div class="container-fluid">
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<!-- alert info -->
<div th:if="${appMode} == 'view'" class="alert alert-warning fade show" role="alert">
<i class="ri-information-fill me-1 align-middle"></i>
<span th:text="#{presupuesto.info.presupuestos-anonimos-view}"></span>
</div>
<!-- JS de Buttons y dependencias -->
<div th:if="${appMode} == 'view'">
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos.js}"></script>
</div>
<div th:if="${appMode} == 'edit'">
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js}"></script>
</div>
<div th:if="${appMode} == 'add'">
<div th:if="${mode} == 'public'">
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos-add.js}"></script>
<div th:insert="~{imprimelibros/presupuestos/presupuestador :: presupuestador}"></div>
</div>
</div>
<div th:if="${mode} != 'public'">
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js}"></script>
</th:block>
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
<th:block layout:fragment="pagejs">
<script th:inline="javascript">
window.languageBundle = /*[[${languageBundle}]]*/ {};
</script>
<!-- JS de Buttons y dependencias -->
<div th:if="${appMode} == 'view'">
<script type="module"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos.js}"></script>
</div>
</div>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-maquetacion.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-marcapaginas.js}"></script>
</th:block>
<div th:if="${appMode} == 'edit'">
<script type="module"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js}"></script>
</div>
<div th:if="${appMode} == 'add'">
<div th:if="${mode} == 'public'">
<script type="module"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-publicos-add.js}"></script>
</div>
<div th:if="${mode} != 'public'">
<script type="module"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/wizard-privado.js}"></script>
</div>
</div>
<script type="module"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-maquetacion.js}"></script>
<script type="module"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-marcapaginas.js}"></script>
<script sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
th:src="@{/assets/libs/quill/quill.min.js}"></script>
<script sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
th:src="@{/assets/js/pages/imprimelibros/presupuestador/text-editor.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestos/duplicate-reprint.js}"></script>
</th:block>
</body>
</html>

View File

@ -12,7 +12,7 @@
<th scope="col" th:text="#{presupuesto.tabla.estado}">Estado</th>
<th scope="col" th:text="#{presupuesto.tabla.total-iva}">Total con IVA</th>
<th scope="col" th:text="#{presupuesto.tabla.updated-at}">Actualizado el</th>
<th scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
<th style="min-width: 100px;" scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th>

View File

@ -13,7 +13,7 @@
<th scope="col" th:text="#{presupuesto.tabla.estado}">Estado</th>
<th scope="col" th:text="#{presupuesto.tabla.total-iva}">Total con IVA</th>
<th scope="col" th:text="#{presupuesto.tabla.updated-at}">Actualizado el</th>
<th scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
<th style="min-width: 100px;" scope="col" th:text="#{presupuesto.tabla.acciones}">Acciones</th>
</tr>
<tr>
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th>

View File

@ -18,7 +18,7 @@ public class envioCarroTest {
void addPedido(){
Locale locale = Locale.forLanguageTag("es-ES");
cartService.crearPedido(carritoId, locale);
//cartService.crearPedido(carritoId, null, locale);
}

View File

@ -17,10 +17,10 @@ public class savePresupuestosTest {
@Test
void testGuardarPresupuesto() {
Locale locale = new Locale("es", "ES");
Long resultado = cartService.crearPedido(9L, locale);
//Long resultado = cartService.crearPedido(9L, null, locale);
System.out.println("📦 Presupuesto guardado:");
System.out.println(resultado);
//System.out.println(resultado);
// Aquí irían las aserciones para verificar que el presupuesto se guardó correctamente
}