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
This commit is contained in:
2025-12-28 11:25:51 +00: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-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-11-15 08:19:06 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev" 2025-12-28 11:38:17 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-12-28 11:38:22 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-12-28 11:38:22 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6ed6a1c8
2025-11-15 08:19:10 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. 2025-12-28 11:38:22 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-12-28 11:38:23 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-12-28 11:38:23 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-12-28 11:38:23 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - UPDATE SUMMARY 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Run: 0 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Previously run: 51 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Previously run: 62
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Filtered out: 0 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - ------------------------------- 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Total change sets: 51 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Total change sets: 62
2025-11-15 08:19:11 INFO [restartedMain] liquibase.util - Update summary generated 2025-12-28 11:38:23 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:19:11 INFO [restartedMain] liquibase.command - Command execution complete 2025-12-28 11:38:23 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-12-28 11:38:24 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-12-28 11:38:24 INFO [restartedMain] org.hibernate.Version - HHH000412: Hibernate ORM core version 6.6.39.Final
2025-11-15 08:19:12 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled 2025-12-28 11:38:24 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:24 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)'] Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown Database driver: undefined/unknown
Database version: 8.0.43 Database version: 8.0.43
@ -25,33 +25,29 @@
Isolation level: undefined/unknown Isolation level: undefined/unknown
Minimum pool size: undefined/unknown Minimum pool size: undefined/unknown
Maximum 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-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-11-15 08:19:19 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 13.772 seconds (process running for 14.889) 2025-12-28 11:38:31 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 14.361 seconds (process running for 15.893)
2025-11-15 08:24:35 WARN [http-nio-8080-exec-9] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000 2025-12-28 11:54:36 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
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-12-28 11:54:36 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
2025-11-15 08:25:30 WARN [http-nio-8080-exec-8] o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 1062, SQLState: 23000 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-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-12-28 11:54:36 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev"
2025-11-15 08:29:20 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated... 2025-12-28 11:54:38 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Starting...
2025-11-15 08:29:20 INFO [Thread-5] com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed. 2025-12-28 11:54:38 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-2 - Added connection com.mysql.cj.jdbc.ConnectionImpl@4a1a1522
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-12-28 11:54:38 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Start completed.
2025-11-15 08:29:20 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - The following 1 profile is active: "dev" 2025-12-28 11:54:38 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:29:21 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Starting... 2025-12-28 11:54:39 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute
2025-11-15 08:29:21 INFO [restartedMain] com.zaxxer.hikari.pool.HikariPool - HikariPool-2 - Added connection com.mysql.cj.jdbc.ConnectionImpl@5582ad4 2025-12-28 11:54:39 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG
2025-11-15 08:29:21 INFO [restartedMain] com.zaxxer.hikari.HikariDataSource - HikariPool-2 - Start completed. 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - UPDATE SUMMARY
2025-11-15 08:29:21 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Run: 0
2025-11-15 08:29:21 INFO [restartedMain] liquibase.ui - Database is up to date, no changesets to execute 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Previously run: 62
2025-11-15 08:29:21 INFO [restartedMain] liquibase.changelog - Reading from imprimelibros.DATABASECHANGELOG 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Filtered out: 0
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - UPDATE SUMMARY 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - -------------------------------
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Run: 0 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Total change sets: 62
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Previously run: 51 2025-12-28 11:54:39 INFO [restartedMain] liquibase.util - Update summary generated
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Filtered out: 0 2025-12-28 11:54:39 INFO [restartedMain] liquibase.command - Command execution complete
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - ------------------------------- 2025-12-28 11:54:39 INFO [restartedMain] o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Total change sets: 51 2025-12-28 11:54:39 INFO [restartedMain] o.h.c.i.RegionFactoryInitiator - HHH000026: Second-level cache disabled
2025-11-15 08:29:21 INFO [restartedMain] liquibase.util - Update summary generated 2025-12-28 11:54:39 INFO [restartedMain] o.hibernate.orm.connections.pooling - HHH10001005: Database info:
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:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-2)'] Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-2)']
Database driver: undefined/unknown Database driver: undefined/unknown
Database version: 8.0.43 Database version: 8.0.43
@ -59,673 +55,5 @@
Isolation level: undefined/unknown Isolation level: undefined/unknown
Minimum pool size: undefined/unknown Minimum pool size: undefined/unknown
Maximum 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-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-11-15 08:29:23 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 3.113 seconds (process running for 618.77) 2025-12-28 11:54:41 INFO [restartedMain] c.imprimelibros.erp.ErpApplication - Started ErpApplication in 5.124 seconds (process running for 986.129)
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.

View File

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

View File

@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; 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.DireccionShipment;
import com.imprimelibros.erp.cart.dto.UpdateCartRequest; import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.common.email.EmailService;
import com.imprimelibros.erp.direcciones.DireccionService; import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.pedidos.Pedido; import com.imprimelibros.erp.pedidos.PedidoRepository;
import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository; import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
@Service @Service
public class CartService { public class CartService {
private final EmailService emailService;
private final CartRepository cartRepo; private final CartRepository cartRepo;
private final CartDireccionRepository cartDireccionRepo; private final CartDireccionRepository cartDireccionRepo;
private final CartItemRepository itemRepo; private final CartItemRepository itemRepo;
private final MessageSource messageSource; private final MessageSource messageSource;
private final PresupuestoRepository presupuestoRepo; private final PresupuestoRepository presupuestoRepo;
private final Utils utils;
private final DireccionService direccionService; private final DireccionService direccionService;
private final skApiClient skApiClient; private final skApiClient skApiClient;
private final PedidoService pedidoService;
private final PresupuestoService presupuestoService; private final PresupuestoService presupuestoService;
private final PedidoRepository pedidoRepository;
public CartService(CartRepository cartRepo, CartItemRepository itemRepo, public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
CartDireccionRepository cartDireccionRepo, MessageSource messageSource, CartDireccionRepository cartDireccionRepo, MessageSource messageSource,
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PedidoRepository pedidoRepository,
Utils utils, DireccionService direccionService, skApiClient skApiClient, DireccionService direccionService, skApiClient skApiClient,PresupuestoService presupuestoService, EmailService emailService) {
PedidoService pedidoService, PresupuestoService presupuestoService) {
this.cartRepo = cartRepo; this.cartRepo = cartRepo;
this.itemRepo = itemRepo; this.itemRepo = itemRepo;
this.cartDireccionRepo = cartDireccionRepo; this.cartDireccionRepo = cartDireccionRepo;
this.messageSource = messageSource; this.messageSource = messageSource;
this.presupuestoRepo = presupuestoRepo; this.presupuestoRepo = presupuestoRepo;
this.utils = utils;
this.direccionService = direccionService; this.direccionService = direccionService;
this.skApiClient = skApiClient; this.skApiClient = skApiClient;
this.pedidoService = pedidoService;
this.presupuestoService = presupuestoService; this.presupuestoService = presupuestoService;
this.emailService = emailService;
this.pedidoRepository = pedidoRepository;
} }
public Cart findById(Long cartId) { public Cart findById(Long cartId) {
@ -89,7 +90,7 @@ public class CartService {
Presupuesto p = item.getPresupuesto(); Presupuesto p = item.getPresupuesto();
Map<String, Object> elemento = getElementoCart(p, locale); Map<String, Object> elemento = presupuestoService.getPresupuestoInfoForCard(p, locale);
elemento.put("cartItemId", item.getId()); elemento.put("cartItemId", item.getId());
resultados.add(elemento); resultados.add(elemento);
} }
@ -159,38 +160,6 @@ public class CartService {
return itemRepo.findByCartId(cart.getId()).size(); 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) { public Map<String, Object> getCartSummaryRaw(Cart cart, Locale locale) {
double base = 0.0; double base = 0.0;
@ -298,7 +267,7 @@ public class CartService {
} }
double totalBeforeDiscount = base + iva4 + iva21 + shipment; double totalBeforeDiscount = base + iva4 + iva21 + shipment;
int fidelizacion = pedidoService.getDescuentoFidelizacion(); int fidelizacion = this.getDescuentoFidelizacion(cart.getUserId());
double descuento = totalBeforeDiscount * fidelizacion / 100.0; double descuento = totalBeforeDiscount * fidelizacion / 100.0;
double total = totalBeforeDiscount - descuento; double total = totalBeforeDiscount - descuento;
@ -325,6 +294,27 @@ public class CartService {
return summary; 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) { public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
Map<String, Object> raw = getCartSummaryRaw(cart, locale); Map<String, Object> raw = getCartSummaryRaw(cart, locale);
@ -445,175 +435,6 @@ public class CartService {
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE); 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 * MÉTODOS PRIVADOS

View File

@ -4,7 +4,9 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.security.Principal; import java.security.Principal;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -12,6 +14,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
@ -357,4 +360,62 @@ public class Utils {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale);
return dateTime.format(formatter); 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(); Long id = ((Integer) responseBody.get("id")).longValue();
if (success != null && id != null && success) { if (success != null && id != null && success) {
return Map.of("data", id); return Map.of("data", id);
} else { } else {
// Tu lógica actual: si success es true u otra cosa → error 2 // 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"); 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 { try {
String jsonResponse = performWithRetry(() -> { String jsonResponse = performWithRetry(() -> {
String url = this.skApiUrl + "api/calcular-solapas"; String url = this.skApiUrl + "api/calcular-solapas";
@ -288,7 +289,11 @@ public class skApiClient {
messageSource.getMessage("presupuesto.errores.error-interior", new Object[] { 1 }, locale)); 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) { } catch (JsonProcessingException e) {
// Fallback al 80% del ancho // Fallback al 80% del ancho
@ -301,7 +306,9 @@ public class skApiClient {
throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody); throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody);
else { else {
int ancho = (int) tamanio.get("ancho"); 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 * 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( private static BigDecimal calcularMargen(
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax, BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
BigDecimal margenMax, BigDecimal margenMin) { 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.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody; 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.common.Utils;
import com.imprimelibros.erp.datatables.DataTable; import com.imprimelibros.erp.datatables.DataTable;
import com.imprimelibros.erp.datatables.DataTablesParser; import com.imprimelibros.erp.datatables.DataTablesParser;
@ -98,7 +100,7 @@ public class PaymentController {
Specification<PaymentTransaction> base = Specification.allOf( Specification<PaymentTransaction> base = Specification.allOf(
(root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded)); (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.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"); String clientSearch = dt.getColumnSearch("client");
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification // 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
@ -229,10 +231,12 @@ public class PaymentController {
}) })
.add("transfer_id", pago -> { .add("transfer_id", pago -> {
if (pago.getPayment() != null) { if (pago.getPayment() != null) {
return "TRANSF-" + pago.getPayment().getOrderId(); Long pedido = pago.getPayment().getOrderId();
} else { if (pedido != null) {
return ""; return "TRANSF-" + pedido;
}
} }
return "";
}) })
.add("order_id", pago -> { .add("order_id", pago -> {
if (pago.getStatus() != PaymentTransactionStatus.pending) { 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.payments.repo.WebhookEventRepository; 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.time.LocalDateTime;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
@Service @Service
@ -28,18 +32,61 @@ public class PaymentService {
private final WebhookEventRepository webhookEventRepo; private final WebhookEventRepository webhookEventRepo;
private final ObjectMapper om = new ObjectMapper(); private final ObjectMapper om = new ObjectMapper();
private final CartService cartService; private final CartService cartService;
private final PedidoService pedidoService;
public PaymentService(PaymentRepository payRepo, public PaymentService(PaymentRepository payRepo,
PaymentTransactionRepository txRepo, PaymentTransactionRepository txRepo,
RefundRepository refundRepo, RefundRepository refundRepo,
RedsysService redsysService, RedsysService redsysService,
WebhookEventRepository webhookEventRepo, CartService cartService) { WebhookEventRepository webhookEventRepo,
CartService cartService,
PedidoService pedidoService) {
this.payRepo = payRepo; this.payRepo = payRepo;
this.txRepo = txRepo; this.txRepo = txRepo;
this.refundRepo = refundRepo; this.refundRepo = refundRepo;
this.redsysService = redsysService; this.redsysService = redsysService;
this.webhookEventRepo = webhookEventRepo; this.webhookEventRepo = webhookEventRepo;
this.cartService = cartService; 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). * oficial (ApiMacSha256).
*/ */
@Transactional @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 { throws Exception {
Payment p = new Payment(); Payment p = new Payment();
p.setOrderId(null); p.setOrderId(orderId);
Cart cart = this.cartService.findById(cartId); Cart cart = this.cartService.findById(cartId);
if (cart != null && cart.getUserId() != null) { if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId()); p.setUserId(cart.getUserId());
this.cartService.lockCartById(cartId);
} }
p.setCurrency(currency); p.setCurrency(currency);
p.setAmountTotalCents(amountCents); p.setAmountTotalCents(amountCents);
@ -62,10 +110,6 @@ public class PaymentService {
p.setStatus(PaymentStatus.requires_payment_method); p.setStatus(PaymentStatus.requires_payment_method);
p = payRepo.saveAndFlush(p); p = payRepo.saveAndFlush(p);
// ANTES:
// String dsOrder = String.format("%012d", p.getId());
// AHORA: timestamp
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
String dsOrder = String.format("%012d", now % 1_000_000_000_000L); String dsOrder = String.format("%012d", now % 1_000_000_000_000L);
@ -73,7 +117,7 @@ public class PaymentService {
payRepo.save(p); payRepo.save(p);
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents, RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
"Compra en Imprimelibros", cartId); "Compra en Imprimelibros", cartId, dirFactId);
if ("bizum".equalsIgnoreCase(method)) { if ("bizum".equalsIgnoreCase(method)) {
return redsysService.buildRedirectFormBizum(req); return redsysService.buildRedirectFormBizum(req);
@ -207,13 +251,12 @@ public class PaymentService {
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents); p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
p.setAuthorizedAt(LocalDateTime.now()); p.setAuthorizedAt(LocalDateTime.now());
p.setCapturedAt(LocalDateTime.now()); p.setCapturedAt(LocalDateTime.now());
pedidoService.setOrderAsPaid(p.getOrderId());
} else { } else {
p.setStatus(PaymentStatus.failed); p.setStatus(PaymentStatus.failed);
p.setFailedAt(LocalDateTime.now()); p.setFailedAt(LocalDateTime.now());
} pedidoService.markPedidoAsPaymentDenied(p.getOrderId());
if (authorized) {
processOrder(notif.cartId, locale);
} }
payRepo.save(p); payRepo.save(p);
@ -308,15 +351,13 @@ public class PaymentService {
} }
@Transactional @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(); Payment p = new Payment();
p.setOrderId(null); p.setOrderId(null);
Cart cart = this.cartService.findById(cartId); Cart cart = this.cartService.findById(cartId);
if (cart != null && cart.getUserId() != null) { if (cart != null && cart.getUserId() != null) {
p.setUserId(cart.getUserId()); 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 // Se bloquea el carrito para evitar modificaciones mientras se procesa el pago
this.cartService.lockCartById(cartId); this.cartService.lockCartById(cartId);
} }
@ -325,6 +366,9 @@ public class PaymentService {
p.setAmountTotalCents(amountCents); p.setAmountTotalCents(amountCents);
p.setGateway("bank_transfer"); p.setGateway("bank_transfer");
p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso
if (orderId != null) {
p.setOrderId(orderId);
}
p = payRepo.save(p); p = payRepo.save(p);
// Crear transacción pendiente // Crear transacción pendiente
@ -334,6 +378,18 @@ public class PaymentService {
tx.setStatus(PaymentTransactionStatus.pending); tx.setStatus(PaymentTransactionStatus.pending);
tx.setAmountCents(amountCents); tx.setAmountCents(amountCents);
tx.setCurrency(currency); 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 // tx.setProcessedAt(null); // la dejas nula hasta que se confirme
txRepo.save(tx); txRepo.save(tx);
@ -374,12 +430,37 @@ public class PaymentService {
p.setAmountCapturedCents(p.getAmountTotalCents()); p.setAmountCapturedCents(p.getAmountTotalCents());
p.setCapturedAt(LocalDateTime.now()); p.setCapturedAt(LocalDateTime.now());
p.setStatus(PaymentStatus.captured); p.setStatus(PaymentStatus.captured);
payRepo.save(p);
// 4) Procesar el pedido asociado al carrito (si existe) Long cartId = null;
if (p.getOrderId() != null) { Long dirFactId = null;
processOrder(p.getOrderId(), locale); 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; 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; package com.imprimelibros.erp.payments.repo;
import com.imprimelibros.erp.payments.model.Payment; import com.imprimelibros.erp.payments.model.Payment;
import com.imprimelibros.erp.payments.model.PaymentStatus;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional; import java.util.Optional;
public interface PaymentRepository extends JpaRepository<Payment, Long> { public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByGatewayAndGatewayOrderId(String gateway, String gatewayOrderId); 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> { public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId); List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey); Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
Optional<PaymentTransaction> findByPaymentIdAndType(
Long paymentId,
PaymentTransactionType type
);
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc( Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
Long paymentId, Long paymentId,
PaymentTransactionType type, PaymentTransactionType type,

View File

@ -1,11 +1,15 @@
package com.imprimelibros.erp.pedidos; package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
@Entity @Entity
@Table(name = "pedidos") @Table(name = "pedidos")
public class Pedido { public class Pedido extends AbstractAuditedEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@ -37,27 +41,8 @@ public class Pedido {
@Column(name = "proveedor_ref", length = 100) @Column(name = "proveedor_ref", length = 100)
private String proveedorRef; private String proveedorRef;
// Auditoría básica (coincidiendo con las columnas que se ven en la captura) @OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = false)
@Column(name = "created_by") private List<PedidoLinea> lineas = new ArrayList<>();
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;
// --- Getters y setters --- // --- Getters y setters ---
@ -132,60 +117,4 @@ public class Pedido {
public void setProveedorRef(String proveedorRef) { public void setProveedorRef(String proveedorRef) {
this.proveedorRef = 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") @Table(name = "pedidos_lineas")
public class PedidoLinea { 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 @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ -21,6 +51,16 @@ public class PedidoLinea {
@JoinColumn(name = "presupuesto_id", nullable = false) @JoinColumn(name = "presupuesto_id", nullable = false)
private Presupuesto presupuesto; 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") @Column(name = "created_at")
private LocalDateTime createdAt; private LocalDateTime createdAt;
@ -53,6 +93,30 @@ public class PedidoLinea {
this.presupuesto = presupuesto; 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() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }

View File

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

View File

@ -1,10 +1,23 @@
package com.imprimelibros.erp.pedidos; package com.imprimelibros.erp.pedidos;
import java.time.Instant;
import org.springframework.data.jpa.repository.JpaRepository; 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; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface PedidoRepository extends JpaRepository<Pedido, Long> { public interface PedidoRepository extends JpaRepository<Pedido, Long>, JpaSpecificationExecutor<Pedido> {
// aquí podrás añadir métodos tipo:
// List<Pedido> findByDeletedFalse(); // 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; package com.imprimelibros.erp.pedidos;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; 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.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto; 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 @Service
public class PedidoService { public class PedidoService {
@ -16,50 +32,45 @@ public class PedidoService {
private final PedidoRepository pedidoRepository; private final PedidoRepository pedidoRepository;
private final PedidoLineaRepository pedidoLineaRepository; private final PedidoLineaRepository pedidoLineaRepository;
private final PresupuestoRepository presupuestoRepository; 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, 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.pedidoRepository = pedidoRepository;
this.pedidoLineaRepository = pedidoLineaRepository; this.pedidoLineaRepository = pedidoLineaRepository;
this.presupuestoRepository = presupuestoRepository; 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 @Transactional
public Pedido crearPedido(List<Long> presupuestoIds, public Pedido crearPedido(
Map<String, Object> cartSummaryRaw, Long cartId,
Long direccionFacturacionId,
String proveedor, String proveedor,
String proveedorRef, String proveedorRef) {
Long userId) {
Pedido pedido = new Pedido(); 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) // Datos económicos (ojo con las claves, son las del summaryRaw)
pedido.setBase((Double) cartSummaryRaw.getOrDefault("base", 0.0d)); pedido.setBase((Double) cartSummaryRaw.getOrDefault("base", 0.0d));
pedido.setEnvio((Double) cartSummaryRaw.getOrDefault("shipment", 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)); pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
// Proveedor // Proveedor
pedido.setProveedor(proveedor); if (proveedor != null && proveedorRef != null) {
pedido.setProveedorRef(proveedorRef); pedido.setProveedor(proveedor);
pedido.setProveedorRef(proveedorRef);
}
// Auditoría mínima // Auditoría mínima
pedido.setCreatedBy(userId); Long userId = cart.getUserId();
pedido.setCreatedAt(LocalDateTime.now()); pedido.setCreatedBy(userService.findById(userId));
pedido.setCreatedAt(Instant.now());
pedido.setDeleted(false); pedido.setDeleted(false);
pedido.setUpdatedAt(LocalDateTime.now()); pedido.setUpdatedAt(Instant.now());
pedido.setUpdatedBy(userId); pedido.setUpdatedBy(userService.findById(userId));
// Guardamos el pedido // Guardamos el pedido
Pedido saved = pedidoRepository.save(pedido); Pedido pedidoGuardado = pedidoRepository.save(pedido);
// Crear líneas del pedido List<CartItem> items = cart.getItems();
for (Long presupuestoId : presupuestoIds) {
Presupuesto presupuesto = presupuestoRepository.getReferenceById(presupuestoId); 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(); PedidoLinea linea = new PedidoLinea();
linea.setPedido(saved); linea.setPedido(pedidoGuardado);
linea.setPresupuesto(presupuesto); linea.setPresupuesto(p);
linea.setCreatedBy(userId); linea.setCreatedBy(userId);
linea.setCreatedAt(LocalDateTime.now()); 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); 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); return ResponseEntity.badRequest().body(errores);
} }
Map<String, Object> resultado = new HashMap<>(); 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)); resultado.putAll(presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale));
return ResponseEntity.ok(resultado); 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); return ResponseEntity.ok(resultado);
} }
@ -300,7 +305,10 @@ public class PresupuestoController {
presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción 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); return ResponseEntity.ok(resultado);
} }
@ -323,7 +331,10 @@ public class PresupuestoController {
} }
Map<String, Object> resultado = new HashMap<>(); 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); return ResponseEntity.ok(resultado);
} }
@ -492,7 +503,8 @@ public class PresupuestoController {
String sessionId = request.getSession(true).getId(); String sessionId = request.getSession(true).getId();
String ip = IpUtils.getClientIp(request); 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); return ResponseEntity.ok(resumen);
} }
@ -519,7 +531,27 @@ public class PresupuestoController {
"presupuesto.add.cancel", "presupuesto.add.cancel",
"presupuesto.add.select-client", "presupuesto.add.select-client",
"presupuesto.add.error.options", "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); Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations); model.addAttribute("languageBundle", translations);
@ -543,7 +575,26 @@ public class PresupuestoController {
"presupuesto.exito.guardado", "presupuesto.exito.guardado",
"presupuesto.add.error.save.title", "presupuesto.add.error.save.title",
"presupuesto.iva-reducido", "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); Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations); model.addAttribute("languageBundle", translations);
@ -562,14 +613,14 @@ public class PresupuestoController {
return "redirect:/presupuesto"; return "redirect:/presupuesto";
} }
if(presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado){ if (presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado) {
Map<String, Object> resumen = presupuestoService.getTextosResumen( Map<String, Object> resumen = presupuestoService.getTextosResumen(
presupuestoOpt.get(), presupuestoOpt.get(),
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()), Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()), Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()), Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
locale); locale);
model.addAttribute("resumen", resumen); model.addAttribute("resumen", resumen);
model.addAttribute("presupuesto", presupuestoOpt.get()); model.addAttribute("presupuesto", presupuestoOpt.get());
@ -595,6 +646,7 @@ public class PresupuestoController {
model.addAttribute("appMode", "edit"); model.addAttribute("appMode", "edit");
} }
model.addAttribute("id", presupuestoOpt.get().getId()); model.addAttribute("id", presupuestoOpt.get().getId());
model.addAttribute("presupuesto", presupuestoOpt.get());
return "imprimelibros/presupuestos/presupuesto-form"; 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) .addIf(publico, "ciudad", Presupuesto::getCiudad)
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale)) .add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "") .addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
.add("actions", this::generarBotones) .add("actions", p -> generarBotones(p, locale))
.where(base) .where(base)
.toJson(count); .toJson(count);
} }
@ -115,18 +115,27 @@ public class PresupuestoDatatableService {
return df.format(instant); return df.format(instant);
} }
private String generarBotones(Presupuesto p) { private String generarBotones(Presupuesto p, Locale locale) {
boolean borrador = p.getEstado() == Presupuesto.Estado.borrador; boolean borrador = p.getEstado() == Presupuesto.Estado.borrador;
String id = String.valueOf(p.getId()); String id = String.valueOf(p.getId());
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" + 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) ? "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 String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-danger btn-delete-" + "\" class=\"link-danger btn-delete-"
+ (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + (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( private static final Map<String, String> CABEZADA_COLOR_KEYS = Map.of(
"NOCABE", "presupuesto.cabezada-sin-cabezada",
"WHI", "presupuesto.cabezada-blanca", "WHI", "presupuesto.cabezada-blanca",
"GRE", "presupuesto.cabezada-verde", "GRE", "presupuesto.cabezada-verde",
"BLUE", "presupuesto.cabezada-azul", "BLUE", "presupuesto.cabezada-azul",

View File

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

View File

@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.common.web.IpUtils; import com.imprimelibros.erp.common.web.IpUtils;
import com.imprimelibros.erp.configurationERP.VariableService; import com.imprimelibros.erp.configurationERP.VariableService;
import com.imprimelibros.erp.presupuesto.GeoIpService; import com.imprimelibros.erp.presupuesto.GeoIpService;
@ -71,14 +72,16 @@ public class PresupuestoService {
private final skApiClient apiClient; private final skApiClient apiClient;
private final GeoIpService geoIpService; private final GeoIpService geoIpService;
private final UserDao userRepo; private final UserDao userRepo;
private final Utils utils;
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter, public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo) { skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo, Utils utils) {
this.presupuestadorItems = presupuestadorItems; this.presupuestadorItems = presupuestadorItems;
this.presupuestoFormatter = presupuestoFormatter; this.presupuestoFormatter = presupuestoFormatter;
this.apiClient = apiClient; this.apiClient = apiClient;
this.geoIpService = geoIpService; this.geoIpService = geoIpService;
this.userRepo = userRepo; this.userRepo = userRepo;
this.utils = utils;
} }
public boolean validateDatosGenerales(int[] tiradas) { public boolean validateDatosGenerales(int[] tiradas) {
@ -348,6 +351,11 @@ public class PresupuestoService {
body.put("interior", interior); body.put("interior", interior);
body.put("cubierta", cubierta); body.put("cubierta", cubierta);
body.put("guardas", null); 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()) { if (presupuesto.getSobrecubierta()) {
Map<String, Object> sobrecubierta = new HashMap<>(); Map<String, Object> sobrecubierta = new HashMap<>();
sobrecubierta.put("papel", presupuesto.getPapelSobrecubiertaId()); sobrecubierta.put("papel", presupuesto.getPapelSobrecubiertaId());
@ -1020,6 +1028,7 @@ public class PresupuestoService {
resumen.put("iva_importe_4", presupuesto.getIvaImporte4()); resumen.put("iva_importe_4", presupuesto.getIvaImporte4());
resumen.put("iva_importe_21", presupuesto.getIvaImporte21()); resumen.put("iva_importe_21", presupuesto.getIvaImporte21());
resumen.put("total_con_iva", presupuesto.getTotalConIva()); resumen.put("total_con_iva", presupuesto.getTotalConIva());
resumen.put("isReimpresion", presupuesto.getIsReimpresion());
return resumen; return resumen;
} }
@ -1143,6 +1152,7 @@ public class PresupuestoService {
if(presupuesto.getSelectedTirada() != null && presupuesto.getSelectedTirada().equals(tirada)) if(presupuesto.getSelectedTirada() != null && presupuesto.getSelectedTirada().equals(tirada))
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios)); presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
} catch (Exception ignore) { } 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<>(); HashMap<String, Object> result = new HashMap<>();
try { 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( presupuesto.setDatosMaquetacionJson(
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null); datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
presupuesto.setDatosMarcapaginasJson( presupuesto.setDatosMarcapaginasJson(
@ -1311,6 +1333,93 @@ public class PresupuestoService {
return true; 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 // Métodos privados
// ======================================================================= // =======================================================================

View File

@ -1,10 +1,15 @@
package com.imprimelibros.erp.redsys; package com.imprimelibros.erp.redsys;
import com.imprimelibros.erp.cart.Cart;
import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.common.Utils;
import com.imprimelibros.erp.payments.PaymentService; import com.imprimelibros.erp.payments.PaymentService;
import com.imprimelibros.erp.payments.model.Payment; 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 com.imprimelibros.erp.redsys.RedsysService.FormPayload;
import groovy.util.logging.Log;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -27,6 +32,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Controller @Controller
@ -37,26 +43,37 @@ public class RedsysController {
private final MessageSource messageSource; private final MessageSource messageSource;
private final SpringTemplateEngine templateEngine; private final SpringTemplateEngine templateEngine;
private final ServletContext servletContext; private final ServletContext servletContext;
private final PedidoService pedidoService;
public RedsysController(PaymentService paymentService, MessageSource messageSource, public RedsysController(PaymentService paymentService, MessageSource messageSource,
SpringTemplateEngine templateEngine, ServletContext servletContext) { SpringTemplateEngine templateEngine, ServletContext servletContext,
PedidoService pedidoService) {
this.paymentService = paymentService; this.paymentService = paymentService;
this.messageSource = messageSource; this.messageSource = messageSource;
this.templateEngine = templateEngine; this.templateEngine = templateEngine;
this.servletContext = servletContext; this.servletContext = servletContext;
this.pedidoService = pedidoService;
} }
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody @ResponseBody
public ResponseEntity<byte[]> crearPago(@RequestParam("amountCents") Long amountCents, public ResponseEntity<byte[]> crearPago(@RequestParam("amountCents") Long amountCents,
@RequestParam("method") String method, @RequestParam("cartId") Long cartId, @RequestParam("method") String method, @RequestParam("cartId") Long cartId,
@RequestParam(value = "dirFactId", required = false) Long dirFactId,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, Locale locale) HttpServletResponse response, Locale locale)
throws Exception { throws Exception {
// Creamos el pedido inteno
Pedido order = pedidoService.crearPedido(cartId, dirFactId, null, null);
if ("bank-transfer".equalsIgnoreCase(method)) { if ("bank-transfer".equalsIgnoreCase(method)) {
// 1) Creamos el Payment interno SIN orderId (null) // 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) // 1⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext); JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
@ -88,7 +105,104 @@ public class RedsysController {
} }
// Tarjeta o Bizum (Redsys) // 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 = """ String html = """
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head> <html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>

View File

@ -49,7 +49,7 @@ public class RedsysService {
// ---------- RECORDS ---------- // ---------- RECORDS ----------
// Pedido a Redsys // 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 // Payload para el formulario
@ -84,7 +84,10 @@ public class RedsysService {
// Si tu PaymentRequest no lo lleva todavía, puedes pasarlo en description o // Si tu PaymentRequest no lo lleva todavía, puedes pasarlo en description o
// crear otro campo. // crear otro campo.
JSONObject ctx = new JSONObject(); 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()); api.setParameter("DS_MERCHANT_MERCHANTDATA", ctx.toString());
if (req.description() != null && !req.description().isBlank()) { if (req.description() != null && !req.description().isBlank()) {
@ -195,6 +198,7 @@ public class RedsysService {
public final long amountCents; public final long amountCents;
public final String currency; public final String currency;
public final Long cartId; public final Long cartId;
public final Long dirFactId;
public final String processedPayMethod; // Ds_ProcessedPayMethod public final String processedPayMethod; // Ds_ProcessedPayMethod
public final String bizumIdOper; // Ds_Bizum_IdOper public final String bizumIdOper; // Ds_Bizum_IdOper
public final String authorisationCode; // Ds_AuthorisationCode public final String authorisationCode; // Ds_AuthorisationCode
@ -206,6 +210,7 @@ public class RedsysService {
this.currency = str(raw.get("Ds_Currency")); this.currency = str(raw.get("Ds_Currency"));
this.amountCents = parseLongSafe(raw.get("Ds_Amount")); this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
this.cartId = extractCartId(raw.get("Ds_MerchantData")); this.cartId = extractCartId(raw.get("Ds_MerchantData"));
this.dirFactId = extractDirFactId(raw.get("Ds_MerchantData"));
this.processedPayMethod = str(raw.get("Ds_ProcessedPayMethod")); this.processedPayMethod = str(raw.get("Ds_ProcessedPayMethod"));
this.bizumIdOper = str(raw.get("Ds_Bizum_IdOper")); this.bizumIdOper = str(raw.get("Ds_Bizum_IdOper"));
this.authorisationCode = str(raw.get("Ds_AuthorisationCode")); 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() { public boolean authorized() {
try { try {
int r = Integer.parseInt(response); int r = Integer.parseInt(response);

View File

@ -17,4 +17,5 @@ public interface UserService extends UserDetailsService {
* @return página de usuarios * @return página de usuarios
*/ */
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable); 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; if (query == null || query.isBlank()) query = null;
return userDao.searchUsers(role, query, pageable); 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

@ -25,3 +25,21 @@ databaseChangeLog:
file: db/changelog/changesets/0012--drop-unique-gateway-txid-2.yml file: db/changelog/changesets/0012--drop-unique-gateway-txid-2.yml
- include: - 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.back=Volver
app.eliminar=Eliminar app.eliminar=Eliminar
app.imprimir=Imprimir app.imprimir=Imprimir
app.view=Ver
app.pay=Pagar
app.acciones.siguiente=Siguiente app.acciones.siguiente=Siguiente
app.acciones.anterior=Anterior app.acciones.anterior=Anterior
@ -20,6 +22,7 @@ app.logout=Cerrar sesión
app.sidebar.inicio=Inicio app.sidebar.inicio=Inicio
app.sidebar.presupuestos=Presupuestos app.sidebar.presupuestos=Presupuestos
app.sidebar.pedidos=Pedidos
app.sidebar.configuracion=Configuración app.sidebar.configuracion=Configuración
app.sidebar.usuarios=Usuarios app.sidebar.usuarios=Usuarios
app.sidebar.direcciones=Mis Direcciones app.sidebar.direcciones=Mis Direcciones

View File

@ -35,6 +35,8 @@ direcciones.pasaporte=Pasaporte
direcciones.cif=C.I.F. direcciones.cif=C.I.F.
direcciones.vat_id=VAT ID direcciones.vat_id=VAT ID
direcciones.direccionFacturacion=Dirección de facturación
direcciones.delete.title=Eliminar dirección direcciones.delete.title=Eliminar dirección
direcciones.delete.button=Si, ELIMINAR 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. 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.datos-marcapaginas=Datos de marcapáginas:
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península. 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=Política de privacidad
pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574 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.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.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.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.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.calcular=Calcular
presupuesto.add=Añadir presupuesto presupuesto.add=Añadir presupuesto
presupuesto.guardar=Guardar 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.add-to-cart=Añadir a la cesta
presupuesto.nav.presupuestos-cliente=Presupuestos cliente presupuesto.nav.presupuestos-cliente=Presupuestos cliente
@ -37,6 +43,8 @@ presupuesto.tabla.region=Región
presupuesto.tabla.ciudad=Ciudad presupuesto.tabla.ciudad=Ciudad
presupuesto.tabla.acciones=Acciones presupuesto.tabla.acciones=Acciones
presupuesto.comentario-administrador=Comentarios
# Pestaña datos generales de presupuesto # Pestaña datos generales de presupuesto
presupuesto.informacion-libro=Información del libro presupuesto.informacion-libro=Información del libro
presupuesto.datos-generales-descripcion=Datos generales del presupuesto presupuesto.datos-generales-descripcion=Datos generales del presupuesto
@ -130,6 +138,7 @@ presupuesto.papel-guardas=Papel de guardas
presupuesto.guardas-impresas=Guardas impresas presupuesto.guardas-impresas=Guardas impresas
presupuesto.no=No presupuesto.no=No
presupuesto.cabezada=Cabezada presupuesto.cabezada=Cabezada
presupuesto.cabezada-sin-cabezada=Sin cabezada
presupuesto.cabezada-blanca=Blanca presupuesto.cabezada-blanca=Blanca
presupuesto.cabezada-verde=Verde presupuesto.cabezada-verde=Verde
presupuesto.cabezada-azul=Azul 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-found=No se puede eliminar: presupuesto no encontrado.
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador. 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 # Añadir presupuesto
presupuesto.add.tipo=Tipo de presupuesto presupuesto.add.tipo=Tipo de presupuesto
presupuesto.add.anonimo=Anónimo 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-id').attr('name', 'direcciones[' + i + '].id');
$(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp'); $(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp');
$(this).find('.direccion-pais-code3').attr('name', 'direcciones[' + i + '].paisCode3'); $(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 if ($(this).find('.presupuesto-id').length > 0 && $(this).find('.presupuesto-id').val() !== null
&& $(this).find('.presupuesto-id').val() !== "") && $(this).find('.presupuesto-id').val() !== "")
$(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId'); $(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId');

View File

@ -140,6 +140,7 @@ $(() => {
let uri = `/checkout/get-address/${direccionId}`; let uri = `/checkout/get-address/${direccionId}`;
const response = await fetch(uri); const response = await fetch(uri);
if (response.ok) { if (response.ok) {
$('#dirFactId').val(direccionId);
const html = await response.text(); const html = await response.text();
$('#direccion-div').append(html); $('#direccion-div').append(html);
$('#addBillingAddressBtn').addClass('d-none'); $('#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.#getInteriorData(),
...this.#getCubiertaData(), ...this.#getCubiertaData(),
selectedTirada: this.formData.selectedTirada selectedTirada: this.formData.selectedTirada
}; };
const sobrecubierta = data.sobrecubierta; const sobrecubierta = data.sobrecubierta;
@ -927,6 +925,7 @@ export default class PresupuestoWizard {
this.#changeTab('pills-general-data'); this.#changeTab('pills-general-data');
} else { } else {
const maxSolapas = data.solapas ?? 120; const maxSolapas = data.solapas ?? 120;
const lomo = data.lomo ?? 0;
$('.solapas-presupuesto').attr('max', maxSolapas); $('.solapas-presupuesto').attr('max', maxSolapas);
$('.max-solapa-text').text(function (_, textoActual) { $('.max-solapa-text').text(function (_, textoActual) {
return textoActual.replace(/\d+/, maxSolapas); return textoActual.replace(/\d+/, maxSolapas);
@ -951,6 +950,20 @@ export default class PresupuestoWizard {
this.acabadoSobrecubierta.val(this.formData.cubierta.sobrecubierta.acabado); this.acabadoSobrecubierta.val(this.formData.cubierta.sobrecubierta.acabado);
this.fajaSobrecubierta.val(this.formData.cubierta.faja.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.#loadCubiertaData();
this.summaryTableCubierta.removeClass('d-none'); this.summaryTableCubierta.removeClass('d-none');
if (this.sobrecubierta.hasClass('active')) { if (this.sobrecubierta.hasClass('active')) {
@ -1683,7 +1696,7 @@ export default class PresupuestoWizard {
const body = { const body = {
presupuesto: this.#getPresupuestoData(), presupuesto: this.#getPresupuestoData(),
save: !this.opts.canSave, save: this.opts.canSave,
mode: this.opts.mode, mode: this.opts.mode,
servicios: servicios, servicios: servicios,
datosMaquetacion: this.formData.servicios.datosMaquetacion, 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 // 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) { $('#presupuestos-clientes-datatable').on('click', '.btn-delete-privado', function (e) {
e.preventDefault(); e.preventDefault();

View File

@ -75,3 +75,171 @@ export async function preguntarTipoPresupuesto() {
} }
}).then((r) => (r.isConfirmed ? r.value : null)); }).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(); e.preventDefault();
// obtén el id de donde lo tengas (data-attr o variable global) // 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`; const url = `/api/pdf/presupuesto/${id}?mode=download`;
@ -47,7 +47,7 @@ $(() => {
$('.add-cart-btn').on('click', async () => { $('.add-cart-btn').on('click', async () => {
const presupuestoId = $('#presupuesto-id').val(); const presupuestoId = $('#presupuesto_id').val();
const res = await $.ajax({ const res = await $.ajax({
url: `/cart/add/${presupuestoId}`, url: `/cart/add/${presupuestoId}`,
method: 'POST', method: 'POST',

View File

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

View File

@ -7,7 +7,7 @@
<input type="hidden" class="direccion-cp" th:value="${direccion.cp}" /> <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="direccion-pais-code3" th:value="${direccion.pais.code3}" />
<input type="hidden" class="item-tirada" th:value="${unidades != null ? unidades : ''}" /> <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="row g-3 align-items-start flex-nowrap">
<div class="col"> <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> <i class="ri-file-paper-2-line"></i> <span th:text="#{app.sidebar.presupuestos}">Presupuestos</span>
</a> </a>
</li> </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"> <li class="nav-item">
<a class="nav-link menu-link" href="/direcciones"> <a class="nav-link menu-link" href="/direcciones">
<i class="ri-truck-line"></i> <i class="ri-truck-line"></i>

View File

@ -139,6 +139,7 @@
<div class="footer"> <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 <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> 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="privacy">
<div class="pv-title" th:text="#{pdf.politica-privacidad}">Política de privacidad</div> <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 - <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> <span th:text="#{app.imprimir}">Imprimir</span>
</button> </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" <button th:if="${appMode == 'add' or appMode == 'edit'}" type="button"
class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn"> class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn">
<i class="ri-shopping-cart-line me-2"></i> <i class="ri-shopping-cart-line me-2"></i>

View File

@ -151,6 +151,7 @@
<div class="col-auto mb-3"> <div class="col-auto mb-3">
<label for="cabezada" class="form-label" th:text="#{presupuesto.cabezada}">Cabezada</label> <label for="cabezada" class="form-label" th:text="#{presupuesto.cabezada}">Cabezada</label>
<select class="form-select select2 datos-cubierta tapa-cubierta-summary" id="cabezada"> <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="WHI" th:text="#{presupuesto.cabezada-blanca}" selected>Blanca</option>
<option value="GRE" th:text="#{presupuesto.cabezada-verde}">Verde</option> <option value="GRE" th:text="#{presupuesto.cabezada-verde}">Verde</option>
<option value="BLUE" th:text="#{presupuesto.cabezada-azul}">Azul</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"> <div class="col-9 mx-auto mt-4">
<h5 id="resumen-titulo" class="text-center"></h5> <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" <table id="resumen-tabla-final" class="table table-borderless table-striped mt-3"
th:data-currency="#{app.currency}"> th:data-currency="#{app.currency}">
<thead> <thead>

View File

@ -4,6 +4,8 @@
<div class="d-flex"> <div class="d-flex">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h5 th:text="#{presupuesto.resumen-presupuesto}">Resumen presupuesto</h5> <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> </div>
</div> </div>

View File

@ -42,18 +42,21 @@
<div class="container-fluid"> <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="row" id="card presupuesto-row animate-fadeInUpBounce">
<div class="card"> <div class="card">
<div class="card-header"> <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> presupuesto</h4>
</div> </div>
<div class="card-body"> <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"> <div class="card col-12 col-sm-9 mx-auto">
<h5 id="resumen-titulo" class="text-center"></h5> <h5 id="resumen-titulo" class="text-center"></h5>
@ -77,7 +80,9 @@
</thead> </thead>
<tbody> <tbody>
<tr th:if="${resumen['linea0']}"> <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}"> <td class="text-start" th:utext="${resumen['linea0'].descripcion}">
Descripción 1</td> Descripción 1</td>
<td class="text-end" th:text="${resumen['linea0'].cantidad}">1</td> <td class="text-end" th:text="${resumen['linea0'].cantidad}">1</td>
@ -159,12 +164,50 @@
</button> </button>
<button type="button" <button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn"> class="btn btn-secondary d-flex align-items-center mx-2 duplicar-btn">
<i class="ri-shopping-cart-line me-2"></i> <i class="ri-file-copy-2-line me-2"></i>
<span th:text="#{presupuesto.add-to-cart}">Añadir a la cesta</span> <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> </button>
</div> </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> </div>
</div> </div>
@ -189,6 +232,7 @@
</div> </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/resumen-view.js}"></script>
<script type="module" th:src="@{/assets/js/pages/imprimelibros/presupuestos/duplicate-reprint.js}"></script>
</th:block> </th:block>
</body> </body>

View File

@ -33,7 +33,8 @@
aria-controls="pills-general-data" aria-selected="true"> aria-controls="pills-general-data" aria-selected="true">
<i <i
class="ri-information-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></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> </button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@ -60,7 +61,8 @@
aria-controls="pills-seleccion-tirada" aria-selected="false"> aria-controls="pills-seleccion-tirada" aria-selected="false">
<i <i
class="ri-add-box-line fs-5 p-1 bg-soft-primary text-primary rounded-circle align-middle me-2"></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> </button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@ -137,6 +139,34 @@
</div> </div>
<!-- end tab content --> <!-- 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> </form>
</div> </div>
<!-- end card body --> <!-- end card body -->

View File

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

View File

@ -12,7 +12,7 @@
<th scope="col" th:text="#{presupuesto.tabla.estado}">Estado</th> <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.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.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>
<tr> <tr>
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th> <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.estado}">Estado</th>
<th scope="col" th:text="#{presupuesto.tabla.total-iva}">Total con IVA</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.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>
<tr> <tr>
<th><input type="text" class="form-control form-control-sm presupuesto-filter" data-col="id" /></th> <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(){ void addPedido(){
Locale locale = Locale.forLanguageTag("es-ES"); 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 @Test
void testGuardarPresupuesto() { void testGuardarPresupuesto() {
Locale locale = new Locale("es", "ES"); 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("📦 Presupuesto guardado:");
System.out.println(resultado); //System.out.println(resultado);
// Aquí irían las aserciones para verificar que el presupuesto se guardó correctamente // Aquí irían las aserciones para verificar que el presupuesto se guardó correctamente
} }