Compare commits

...
149 Commits
Author SHA1 Message Date
M_Kececi db13574e9d Fix product performance grouped JSON build 2026-08-19 01:10:20 +03:00
M_Kececi ba9a744aef Fix product performance grouped JSON build 2026-08-19 01:06:55 +03:00
M_Kececi 82ad3783b3 Fix product performance grouped JSON build 2026-08-19 01:00:03 +03:00
M_Kececi 322bed753f Fix product performance grouped JSON build 2026-08-19 00:53:51 +03:00
M_Kececi bdc9459330 Fix product performance grouped JSON build 2026-08-19 00:50:40 +03:00
M_Kececi 2d85a33623 Fix product performance grouped JSON build 2026-08-19 00:45:58 +03:00
M_Kececi 5f1c37859b Fix product performance grouped JSON build 2026-08-19 00:08:19 +03:00
M_Kececi 31a71ab566 Fix product performance grouped JSON build 2026-08-18 23:38:24 +03:00
M_Kececi 25ef023df5 Fix product performance grouped JSON build 2026-08-18 23:32:39 +03:00
M_Kececi 0c51729f7a Fix product performance grouped JSON build 2026-08-18 22:41:08 +03:00
M_Kececi 20f5fea2e8 Fix product performance grouped JSON build 2026-08-18 22:35:20 +03:00
M_Kececi 65e8aa2b62 Fix product performance grouped JSON build 2026-08-18 22:23:41 +03:00
M_Kececi 1f8355b77c Fix product performance grouped JSON build 2026-08-18 22:17:28 +03:00
M_Kececi 11048f1555 Fix product performance grouped JSON build 2026-08-18 22:11:13 +03:00
M_Kececi b83a3d22ce Fix product performance grouped JSON build 2026-08-18 22:07:33 +03:00
M_Kececi 77af9ef533 Fix product performance grouped JSON build 2026-08-18 22:02:44 +03:00
M_Kececi 4395d6bae2 Fix product performance grouped JSON build 2026-08-18 21:57:46 +03:00
M_Kececi 6bc19e7a43 Fix product performance grouped JSON build 2026-08-18 21:54:16 +03:00
M_Kececi 797d069e59 Fix product performance grouped JSON build 2026-08-18 21:47:27 +03:00
M_Kececi 36e0ec9ff5 Fix product performance grouped JSON build 2026-08-18 21:39:53 +03:00
M_Kececi f14a96a024 Fix product performance grouped JSON build 2026-08-18 21:35:10 +03:00
M_Kececi f877b611b7 Fix product performance grouped JSON build 2026-08-18 21:13:38 +03:00
M_Kececi dbb3f3f380 Fix product performance grouped JSON build 2026-08-18 21:07:52 +03:00
M_Kececi 03287d4850 Fix product performance grouped JSON build 2026-08-18 20:58:29 +03:00
M_Kececi 12ce3a3eca Fix product performance grouped JSON build 2026-08-18 20:54:45 +03:00
M_Kececi 52420bc5ff Fix product performance grouped JSON build 2026-08-18 20:40:39 +03:00
M_Kececi d5aa43ccb5 Fix product performance grouped JSON build 2026-08-18 20:30:44 +03:00
M_Kececi 04461acb58 Fix product performance grouped JSON build 2026-08-18 20:23:55 +03:00
M_Kececi cd101811c4 Fix product performance grouped JSON build 2026-08-18 20:19:24 +03:00
M_Kececi c4028010c0 Fix product performance grouped JSON build 2026-08-18 20:13:21 +03:00
M_Kececi c2d010751f Fix product performance grouped JSON build 2026-08-18 20:08:09 +03:00
M_Kececi a9ab9fd9b0 Fix product performance grouped JSON build 2026-08-18 20:03:12 +03:00
M_Kececi 1945fd4456 Fix product performance grouped JSON build 2026-08-18 19:57:24 +03:00
M_Kececi 7b8f8905e5 Fix product performance grouped JSON build 2026-08-18 19:44:36 +03:00
M_Kececi 89df3d2d3d Fix product performance grouped JSON build 2026-08-18 18:44:26 +03:00
M_Kececi 474904481f Fix product performance grouped JSON build 2026-08-18 13:38:32 +03:00
M_Kececi 6eaf0e3ed3 Fix product performance grouped JSON build 2026-08-18 13:23:10 +03:00
M_Kececi 8249761415 Fix product performance grouped JSON build 2026-08-18 13:10:11 +03:00
M_Kececi dbd84f7399 Fix product performance grouped JSON build 2026-08-18 13:04:50 +03:00
M_Kececi 9edc806345 Fix product performance grouped JSON build 2026-08-18 12:45:11 +03:00
M_Kececi 7a7008cc1b Fix product performance grouped JSON build 2026-07-14 00:18:23 +03:00
M_Kececi 3e0253f6be Fix product performance grouped JSON build 2026-07-13 22:43:51 +03:00
M_Kececi b80b5024f4 Fix product performance grouped JSON build 2026-07-13 21:55:17 +03:00
M_Kececi 5eaf8be9e6 Fix product performance grouped JSON build 2026-07-12 16:15:41 +03:00
M_Kececi cc2930caf2 Fix product performance grouped JSON build 2026-07-09 16:41:41 +03:00
M_Kececi 26b4a63c45 Fix product performance grouped JSON build 2026-07-07 23:51:29 +03:00
M_Kececi c6a3d1552e Fix product performance grouped JSON build 2026-07-07 12:59:19 +03:00
M_Kececi 005d2eafac Fix product performance grouped JSON build 2026-07-07 01:21:07 +03:00
M_Kececi 289f204752 Fix product performance grouped JSON build 2026-07-06 18:01:34 +03:00
M_Kececi 1448dab9d7 Fix product performance grouped JSON build 2026-07-06 17:01:39 +03:00
M_Kececi 5d4216d42e Fix product performance grouped JSON build 2026-07-06 16:54:36 +03:00
M_Kececi 3993c363cb Fix product performance grouped JSON build 2026-07-06 01:28:44 +03:00
M_Kececi a0da3f2bed Fix product performance grouped JSON build 2026-07-06 00:53:30 +03:00
M_Kececi 19a42e0551 Merge remote-tracking branch 'origin/master' 2026-07-05 23:43:20 +03:00
M_Kececi 53cdd1a4a1 Fix product performance grouped JSON build 2026-07-05 23:43:17 +03:00
M_Kececi fe29e763c5 Fix product performance grouped JSON build 2026-07-05 23:41:26 +03:00
M_Kececi 9e1ecb9b3f Fix product performance grouped JSON build 2026-07-05 22:06:43 +03:00
M_Kececi fbdde54eee Fix product performance grouped JSON build 2026-07-05 20:31:27 +03:00
M_Kececi b2113a8d49 Fix product performance grouped JSON build 2026-07-05 18:31:40 +03:00
M_Kececi 28e33d5e58 Fix product performance grouped JSON build 2026-07-05 17:02:28 +03:00
M_Kececi fbdc4934ff Merge remote-tracking branch 'origin/master' 2026-07-05 15:30:29 +03:00
M_Kececi 5b512fd30a Fix product performance grouped JSON build 2026-07-05 15:30:25 +03:00
M_Kececi 7879172c54 Fix product performance grouped JSON build 2026-07-05 15:29:59 +03:00
M_Kececi c602e1e119 Fix product performance grouped JSON build 2026-07-05 13:54:52 +03:00
M_Kececi 27934bccdf Fix product performance grouped JSON build 2026-07-05 05:16:31 +03:00
M_Kececi 48bde09c2c Fix product performance grouped JSON build 2026-07-05 02:58:06 +03:00
M_Kececi 480cad0d6f Fix product performance grouped JSON build 2026-07-05 01:02:04 +03:00
M_Kececi 9f6e288d04 Fix product performance grouped JSON build 2026-07-05 00:38:44 +03:00
M_Kececi d0717900a6 Fix product performance grouped JSON build 2026-07-04 23:31:45 +03:00
M_Kececi 25bc9b2d02 Fix product performance grouped JSON build 2026-07-04 14:11:19 +03:00
M_Kececi 4c56062ef4 Fix product performance grouped JSON build 2026-07-04 13:45:43 +03:00
M_Kececi 60b2f76537 Fix product performance grouped JSON build 2026-07-04 13:26:10 +03:00
M_Kececi 81384426ff Fix product performance grouped JSON build 2026-07-04 13:10:34 +03:00
M_Kececi 3697e35064 Fix product performance grouped JSON build 2026-07-03 21:35:14 +03:00
M_Kececi 4ae6475875 Merge remote-tracking branch 'origin/master' 2026-07-03 20:53:53 +03:00
M_Kececi 5f006b8be0 Fix product performance grouped JSON build 2026-07-03 20:53:48 +03:00
M_Kececi 38c168f7bd Fix product performance grouped JSON build 2026-07-03 20:52:48 +03:00
M_Kececi be125a6880 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	svc/queries/product_performance_sql.go
#	ui/src/pages/ProductPerformanceProfitability.vue
2026-07-03 20:46:31 +03:00
M_Kececi 3b2bcda462 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	svc/queries/product_performance_sql.go
#	ui/src/pages/ProductPerformanceProfitability.vue
2026-07-03 20:40:35 +03:00
M_Kececi 9e22cc5ec5 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	svc/queries/product_performance_sql.go
#	ui/src/pages/ProductPerformanceProfitability.vue
2026-07-03 20:10:05 +03:00
M_Kececi 5e2a85b98e product performance grouped kpi improvements 2026-07-03 20:09:39 +03:00
M_Kececi a45478d15f product performance grouped kpi improvements 2026-07-03 19:53:16 +03:00
M_Kececi b133d43ebe product performance grouped kpi improvements 2026-07-03 17:43:13 +03:00
M_Kececi 9fee8c8ab2 product performance grouped kpi improvements 2026-07-03 17:12:30 +03:00
M_Kececi 95d72ec74b product performance grouped kpi improvements 2026-07-03 15:39:39 +03:00
M_Kececi 4cd1b118d0 product performance grouped kpi improvements 2026-07-03 14:49:50 +03:00
M_Kececi 2f885a4841 product performance grouped kpi improvements 2026-07-03 14:34:30 +03:00
M_Kececi 4aebb36ff2 product performance grouped kpi improvements 2026-07-03 06:01:35 +03:00
M_Kececi fd086d668f product performance grouped kpi improvements 2026-07-03 05:25:05 +03:00
M_Kececi 5288f19976 product performance grouped kpi improvements 2026-07-03 04:27:42 +03:00
M_Kececi 60eef45769 product performance grouped kpi improvements 2026-07-03 03:58:15 +03:00
M_Kececi a05e6bf1e2 product performance grouped kpi improvements 2026-07-03 03:43:17 +03:00
M_Kececi 94c83c3825 product performance grouped kpi improvements 2026-07-03 03:34:55 +03:00
M_Kececi a0dd832d2b product performance grouped kpi improvements 2026-07-03 03:29:31 +03:00
M_Kececi c2d27b2d77 product performance grouped kpi improvements 2026-07-03 03:14:52 +03:00
M_Kececi 9f45160648 product performance grouped kpi improvements 2026-07-03 02:55:58 +03:00
M_Kececi c7ff2328fb product performance grouped kpi improvements 2026-07-03 02:37:58 +03:00
M_Kececi e7a4c58934 product performance grouped kpi improvements 2026-07-03 02:24:24 +03:00
M_Kececi 5d5a38647d product performance grouped kpi improvements 2026-07-03 02:12:55 +03:00
M_Kececi 75e5136ab7 fix sales breakdown ambiguous product fields 2026-07-03 01:56:16 +03:00
M_Kececi 64105815a0 Merge remote-tracking branch 'origin/master' 2026-07-03 01:53:23 +03:00
M_Kececi 98e3587608 product performance grouped kpi improvements 2026-07-03 01:53:19 +03:00
M_Kececi a0d06b5fcf product performance grouped kpi improvements 2026-07-03 01:53:00 +03:00
M_Kececi be6feb8aa8 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-03 00:30:54 +03:00
M_Kececi c536b85a4c ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-03 00:01:42 +03:00
M_Kececi d419f52e3c ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 23:08:02 +03:00
M_Kececi f3d4661cff ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:45:53 +03:00
M_Kececi f07ea2359d Merge remote-tracking branch 'origin/master' 2026-07-02 21:42:14 +03:00
M_Kececi 1ff7a03a81 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:42:10 +03:00
M_Kececi 05e192e30b ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:38:08 +03:00
M_Kececi c00ce200d7 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:34:36 +03:00
M_Kececi b5098fb94f ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:18:36 +03:00
M_Kececi d4e853806a ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 21:01:55 +03:00
M_Kececi 8d0f3c302a ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 20:07:14 +03:00
M_Kececi 2fc9699ff1 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 20:02:19 +03:00
M_Kececi 593a7d4816 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 18:11:54 +03:00
M_Kececi d98ce372c7 ui: update ProductPerformanceProfitability page to enhance table views and tabs management 2026-07-02 17:39:22 +03:00
M_Kececi 1bf51b9e82 ui: add B2B olmayan stok (orphans) page 2026-07-02 16:08:27 +03:00
M_Kececi cbb853d433 ui: add B2B olmayan stok (orphans) page 2026-07-02 15:33:49 +03:00
M_Kececi 810c0e5eff ui: add B2B olmayan stok (orphans) page 2026-06-25 17:24:56 +03:00
M_Kececi dfad548963 ui: add B2B olmayan stok (orphans) page 2026-06-25 14:14:23 +03:00
M_Kececi 52b39725ec ui: add B2B olmayan stok (orphans) page 2026-06-25 13:11:46 +03:00
M_Kececi 3095c06cbf ui: add B2B olmayan stok (orphans) page 2026-06-24 23:27:42 +03:00
M_Kececi aa100973b3 ui: add B2B olmayan stok (orphans) page 2026-06-24 23:14:58 +03:00
M_Kececi 701cc5e439 ui: add B2B olmayan stok (orphans) page 2026-06-24 23:08:28 +03:00
M_Kececi 6b9cd6fe5a ui: add B2B olmayan stok (orphans) page 2026-06-24 22:59:25 +03:00
M_Kececi 583af0230a Merge remote-tracking branch 'origin/master' 2026-06-24 22:54:53 +03:00
M_Kececi be7ccdc466 Merge remote-tracking branch 'origin/master' 2026-06-24 22:11:36 +03:00
M_Kececi e67fad9f90 Merge remote-tracking branch 'origin/master' 2026-06-24 21:49:48 +03:00
M_Kececi 215b7fd321 Merge remote-tracking branch 'origin/master' 2026-06-24 20:42:40 +03:00
M_Kececi ec21dfac70 Merge remote-tracking branch 'origin/master' 2026-06-24 20:36:18 +03:00
M_Kececi 10fd77bece Merge remote-tracking branch 'origin/master' 2026-06-24 19:47:09 +03:00
M_Kececi 62bf272f17 Merge remote-tracking branch 'origin/master' 2026-06-24 19:05:40 +03:00
M_Kececi aca2cf5d02 Merge remote-tracking branch 'origin/master' 2026-06-24 17:39:38 +03:00
M_Kececi a56a0a8b21 Merge remote-tracking branch 'origin/master' 2026-06-24 17:30:33 +03:00
M_Kececi a972565859 Merge remote-tracking branch 'origin/master' 2026-06-24 13:36:16 +03:00
M_Kececi ccf34559c2 Merge remote-tracking branch 'origin/master' 2026-06-24 12:45:00 +03:00
M_Kececi 79ffe7862c Merge remote-tracking branch 'origin/master' 2026-06-24 12:04:33 +03:00
M_Kececi 01e457630c Merge remote-tracking branch 'origin/master' 2026-06-24 11:25:23 +03:00
M_Kececi 8866361a65 Merge remote-tracking branch 'origin/master' 2026-06-23 22:21:05 +03:00
M_Kececi 9526dfe645 Merge remote-tracking branch 'origin/master' 2026-06-23 21:32:59 +03:00
M_Kececi b8296d2da9 Merge remote-tracking branch 'origin/master' 2026-06-23 21:32:54 +03:00
M_Kececi feb7dc4f05 Merge remote-tracking branch 'origin/master' 2026-06-23 21:12:14 +03:00
M_Kececi 8b97e26d70 Merge remote-tracking branch 'origin/master' 2026-06-23 21:00:22 +03:00
M_Kececi 057f52a1f9 Merge remote-tracking branch 'origin/master' 2026-06-23 20:47:52 +03:00
M_Kececi 136c1e74fb Merge remote-tracking branch 'origin/master' 2026-06-23 20:36:41 +03:00
M_Kececi f6cf6bb4cc Merge remote-tracking branch 'origin/master' 2026-06-23 18:12:52 +03:00
M_Kececi 750444b9c8 Merge remote-tracking branch 'origin/master' 2026-06-23 18:05:21 +03:00
M_Kececi 6b96e6b52b Merge remote-tracking branch 'origin/master' 2026-06-23 18:05:17 +03:00
87 changed files with 22724 additions and 701 deletions
+1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" /> <component name="Go" enabled="true" />
<component name="GoModuleSettings" enabled="true" />
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
+17
View File
@@ -33,6 +33,23 @@ PDF_FONT_DIR=D:\\baggitekstilas\\software projects\\bssapp\\bssapp\\svc\\fonts
API_HOST=0.0.0.0 API_HOST=0.0.0.0
API_PORT=8080 API_PORT=8080
# ===============================
# PRODUCT PERFORMANCE CACHE
# ===============================
# MSSQL yogun kullanildigi icin uygulama acilisinda cache isi calismaz.
# Ilk full refresh 19:00 sonrasi manuel calistirilmali.
PRODUCT_PERFORMANCE_ENABLED=true
PRODUCT_PERFORMANCE_RUN_ON_STARTUP=false
PRODUCT_PERFORMANCE_DELTA_DAYS=45
PRODUCT_PERFORMANCE_DELTA_HHMM=02:00
PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS=6
PRODUCT_PERFORMANCE_FULL_ENABLED=true
PRODUCT_PERFORMANCE_FULL_WEEKDAY=saturday
PRODUCT_PERFORMANCE_FULL_HHMM=16:00
PRODUCT_PERFORMANCE_FULL_START_DATE=2022-01-01
PRODUCT_PERFORMANCE_FULL_TIMEOUT_HOURS=18
PRODUCT_PERFORMANCE_RESUME_ON_STARTUP=true
AZURE_TRANSLATOR_KEY=d055c693-a84e-4594-8aef-a6c05c42623a AZURE_TRANSLATOR_KEY=d055c693-a84e-4594-8aef-a6c05c42623a
AZURE_TRANSLATOR_ENDPOINT=https://api.cognitive.microsofttranslator.com AZURE_TRANSLATOR_ENDPOINT=https://api.cognitive.microsofttranslator.com
+2
View File
@@ -3,6 +3,8 @@ bssapp
# Env # Env
.env .env
.env.local
.env.argebaggi.local
mail.env mail.env
# Runtime fonts # Runtime fonts
@@ -0,0 +1,83 @@
package main
import (
"bssapp-backend/db"
"bssapp-backend/queries"
"context"
"flag"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
)
func main() {
mode := flag.String("mode", "delta", "refresh mode: full or delta")
stage := flag.String("stage", "all", "start stage: sales, stock, price, kpi, all")
startRaw := flag.String("start", "", "start date YYYY-MM-DD")
endRaw := flag.String("end", "", "end date YYYY-MM-DD")
prefix := flag.String("prefix", "", "optional product code prefix")
mssqlTimeoutSec := flag.Int("mssql-timeout-sec", 0, "override MSSQL connection/dial timeout seconds")
skipDelete := flag.Bool("skip-delete", false, "skip deleting existing cache rows")
resumeAfter := flag.Int("resume-after", 0, "skip this many MSSQL rows before inserting")
flag.Parse()
_ = godotenv.Load(".env", "mail.env", ".env.local")
if *mssqlTimeoutSec > 0 {
timeout := strconv.Itoa(*mssqlTimeoutSec)
_ = os.Setenv("MSSQL_CONNECTION_TIMEOUT_SEC", timeout)
_ = os.Setenv("MSSQL_DIAL_TIMEOUT_SEC", timeout)
}
if err := db.ConnectMSSQL(); err != nil {
log.Fatalf("mssql connect failed: %v", err)
}
pgDB, err := db.ConnectPostgres()
if err != nil {
log.Fatalf("postgres connect failed: %v", err)
}
defer pgDB.Close()
startDate, err := parseDateFlag(*startRaw)
if err != nil {
log.Fatalf("invalid -start: %v", err)
}
endDate, err := parseDateFlag(*endRaw)
if err != nil {
log.Fatalf("invalid -end: %v", err)
}
ctx := context.Background()
result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{
Mode: strings.TrimSpace(*mode),
Stage: strings.TrimSpace(*stage),
StartDate: startDate,
EndDate: endDate,
ProductPrefix: strings.TrimSpace(*prefix),
SkipDelete: *skipDelete,
ResumeAfter: *resumeAfter,
})
if err != nil {
log.Fatalf("refresh failed: %v", err)
}
fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d snapshots=%d\n",
result.SalesRows,
result.StockRows,
result.KpiRows,
result.SnapshotRows,
)
}
func parseDateFlag(raw string) (time.Time, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return time.Time{}, nil
}
return time.ParseInLocation("2006-01-02", raw, time.Local)
}
+39
View File
@@ -14,6 +14,7 @@ import (
var MssqlDB *sql.DB var MssqlDB *sql.DB
var UretimDB *sql.DB var UretimDB *sql.DB
var ArgeBaggiDB *sql.DB
func envInt(name string, fallback int) int { func envInt(name string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(name)) raw := strings.TrimSpace(os.Getenv(name))
@@ -158,3 +159,41 @@ func ConnectMSSQLUretim() error {
func GetUretimDB() *sql.DB { func GetUretimDB() *sql.DB {
return UretimDB return UretimDB
} }
// ConnectMSSQLArgeBaggi initializes the optional ArgeBAGGI connection.
// Credentials must be supplied through ARGEBAGGI_MSSQL_CONN; never keep them
// in source control.
func ConnectMSSQLArgeBaggi() error {
connString := strings.TrimSpace(os.Getenv("ARGEBAGGI_MSSQL_CONN"))
if connString == "" {
return fmt.Errorf("ARGEBAGGI_MSSQL_CONN tanimli degil")
}
connectionTimeoutSec := envInt("ARGEBAGGI_MSSQL_CONNECTION_TIMEOUT_SEC", 10)
dialTimeoutSec := envInt("ARGEBAGGI_MSSQL_DIAL_TIMEOUT_SEC", connectionTimeoutSec)
connString = ensureMSSQLTimeouts(connString, connectionTimeoutSec, dialTimeoutSec)
var err error
ArgeBaggiDB, err = sql.Open("sqlserver", connString)
if err != nil {
return fmt.Errorf("ArgeBAGGI MSSQL baglanti hatasi: %w", err)
}
ArgeBaggiDB.SetMaxOpenConns(envInt("ARGEBAGGI_MSSQL_MAX_OPEN_CONNS", 10))
ArgeBaggiDB.SetMaxIdleConns(envInt("ARGEBAGGI_MSSQL_MAX_IDLE_CONNS", 5))
ArgeBaggiDB.SetConnMaxLifetime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_LIFETIME_MIN", 30)) * time.Minute)
ArgeBaggiDB.SetConnMaxIdleTime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_IDLE_MIN", 10)) * time.Minute)
if err = ArgeBaggiDB.Ping(); err != nil {
_ = ArgeBaggiDB.Close()
ArgeBaggiDB = nil
return fmt.Errorf("ArgeBAGGI MSSQL erisilemiyor: %w", err)
}
fmt.Printf("ArgeBAGGI MSSQL baglantisi basarili (connection timeout=%ds, dial timeout=%ds)\n", connectionTimeoutSec, dialTimeoutSec)
return nil
}
func GetArgeBaggiDB() *sql.DB {
return ArgeBaggiDB
}
+38 -11
View File
@@ -13,25 +13,29 @@ import (
var PgDB *sql.DB var PgDB *sql.DB
// ConnectPostgres PostgreSQL veritabanına bağlanır. // ConnectPostgres PostgreSQL veritabanına baÄŸlanır.
func ConnectPostgres() (*sql.DB, error) { func ConnectPostgres() (*sql.DB, error) {
connStr := strings.TrimSpace(os.Getenv("POSTGRES_CONN")) connStr := strings.TrimSpace(os.Getenv("POSTGRES_CONN"))
if connStr == "" { if connStr == "" {
return nil, fmt.Errorf("POSTGRES_CONN tanımlı değil") return nil, fmt.Errorf("POSTGRES_CONN tanımlı deÄŸil")
} }
// Make DB writes attributable in Postgres audit triggers via current_setting('application_name').
// Prefer setting it in POSTGRES_CONN, but default it here if missing.
connStr = ensurePGApplicationName(connStr, strings.TrimSpace(os.Getenv("POSTGRES_APPLICATION_NAME")))
db, err := sql.Open("postgres", connStr) db, err := sql.Open("postgres", connStr)
if err != nil { if err != nil {
return nil, fmt.Errorf("PostgreSQL bağlantı hatası: %w", err) return nil, fmt.Errorf("PostgreSQL baÄŸlantı hatası: %w", err)
} }
// Bağlantı havuzu ayarları (audit log uyumlu). // BaÄŸlantı havuzu ayarları (audit log uyumlu).
db.SetMaxOpenConns(30) db.SetMaxOpenConns(30)
db.SetMaxIdleConns(10) db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute)
// Bağlantıyı test et. // BaÄŸlantıyı test et.
if err = db.Ping(); err != nil { if err = db.Ping(); err != nil {
// Some managed PostgreSQL servers require TLS. If the current DSN uses // Some managed PostgreSQL servers require TLS. If the current DSN uses
// sslmode=disable and server rejects with "no encryption", retry once // sslmode=disable and server rejects with "no encryption", retry once
@@ -40,6 +44,7 @@ func ConnectPostgres() (*sql.DB, error) {
strings.Contains(err.Error(), "no encryption") && strings.Contains(err.Error(), "no encryption") &&
strings.Contains(strings.ToLower(connStr), "sslmode=disable") { strings.Contains(strings.ToLower(connStr), "sslmode=disable") {
secureConnStr := strings.Replace(connStr, "sslmode=disable", "sslmode=require", 1) secureConnStr := strings.Replace(connStr, "sslmode=disable", "sslmode=require", 1)
secureConnStr = ensurePGApplicationName(secureConnStr, strings.TrimSpace(os.Getenv("POSTGRES_APPLICATION_NAME")))
log.Println("PostgreSQL TLS gerektiriyor, sslmode=require ile tekrar deneniyor") log.Println("PostgreSQL TLS gerektiriyor, sslmode=require ile tekrar deneniyor")
_ = db.Close() _ = db.Close()
@@ -54,28 +59,50 @@ func ConnectPostgres() (*sql.DB, error) {
db.SetConnMaxIdleTime(5 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute)
if err = db.Ping(); err != nil { if err = db.Ping(); err != nil {
return nil, fmt.Errorf("PostgreSQL erişilemiyor (TLS retry): %w", err) return nil, fmt.Errorf("PostgreSQL eriÅŸilemiyor (TLS retry): %w", err)
} }
} else { } else {
return nil, fmt.Errorf("PostgreSQL erişilemiyor: %w", err) return nil, fmt.Errorf("PostgreSQL eriÅŸilemiyor: %w", err)
} }
} }
log.Println("PostgreSQL bağlantısı başarılı") log.Println("PostgreSQL baÄŸlantısı baÅŸarılı")
PgDB = db PgDB = db
return db, nil return db, nil
} }
// GetPostgresUsers test amaçlı ilk 5 kullanıcıyı listeler. func ensurePGApplicationName(connStr string, appName string) string {
if strings.TrimSpace(appName) == "" {
appName = "bssapp-backend"
}
low := strings.ToLower(connStr)
if strings.Contains(low, "application_name=") {
return connStr
}
// URL style DSN: postgres://...?... (lib/pq supports it)
if strings.HasPrefix(low, "postgres://") || strings.HasPrefix(low, "postgresql://") {
sep := "?"
if strings.Contains(connStr, "?") {
sep = "&"
}
return connStr + sep + "application_name=" + appName
}
// Keyword DSN: "host=... user=... dbname=... sslmode=..."
return strings.TrimSpace(connStr) + " application_name=" + appName
}
// GetPostgresUsers test amaçlı ilk 5 kullanıcıyı listeler.
func GetPostgresUsers(db *sql.DB) error { func GetPostgresUsers(db *sql.DB) error {
query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5` query := `SELECT id, code, email FROM mk_dfusr ORDER BY id LIMIT 5`
rows, err := db.Query(query) rows, err := db.Query(query)
if err != nil { if err != nil {
return fmt.Errorf("PostgreSQL sorgu hatası: %w", err) return fmt.Errorf("PostgreSQL sorgu hatası: %w", err)
} }
defer rows.Close() defer rows.Close()
fmt.Println("İlk 5 PostgreSQL kullanıcısı:") fmt.Println("İlk 5 PostgreSQL kullanıcısı:")
for rows.Next() { for rows.Next() {
var id int var id int
var code, email string var code, email string
+1
View File
@@ -0,0 +1 @@
exit status 0xffffffff
+299
View File
@@ -0,0 +1,299 @@
time=2026-06-29T15:41:11.450+03:00 level=INFO msg="backend start" app=bssapp-backend scope=main
time=2026-06-29T15:41:11.491+03:00 level=INFO msg="🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥" app=bssapp-backend
time=2026-06-29T15:41:11.523+03:00 level=INFO msg="🔐 JWT_SECRET yüklendi" app=bssapp-backend
MSSQL baglantisi basarili (connection timeout=120s, dial timeout=120s)
URETIM MSSQL baglantisi basarili (connection timeout=120s, dial timeout=120s)
time=2026-06-29T15:41:11.988+03:00 level=INFO msg="PostgreSQL bağlantısı başarılı" app=bssapp-backend
time=2026-06-29T15:41:12.338+03:00 level=INFO msg="✅ Admin dept permissions seeded" app=bssapp-backend
time=2026-06-29T15:41:12.338+03:00 level=INFO msg="🟢 auditlog Init called, buffer: 1000" app=bssapp-backend
time=2026-06-29T15:41:12.338+03:00 level=INFO msg="🕵️ AuditLog sistemi başlatıldı (buffer=1000)" app=bssapp-backend
time=2026-06-29T15:41:12.338+03:00 level=INFO msg="🟢 auditlog worker STARTED" app=bssapp-backend
time=2026-06-29T15:41:12.447+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE EXTENSION IF NOT EXISTS pg_trgm\"" app=bssapp-backend
time=2026-06-29T15:41:12.553+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE INDEX IF NOT EXISTS idx_mk_translator_t_key_lang ON mk_translator (t_key, lang_code)\"" app=bssapp-backend
time=2026-06-29T15:41:12.662+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE INDEX IF NOT EXISTS idx_mk_translator_status_lang_updated ON mk_translator (status, lang_code...\"" app=bssapp-backend
time=2026-06-29T15:41:12.767+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE INDEX IF NOT EXISTS idx_mk_translator_manual_status ON mk_translator (is_manual, status)\"" app=bssapp-backend
time=2026-06-29T15:41:12.871+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE INDEX IF NOT EXISTS idx_mk_translator_source_type_expr ON mk_translator ((COALESCE(NULLIF(pro...\"" app=bssapp-backend
time=2026-06-29T15:41:12.973+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE INDEX IF NOT EXISTS idx_mk_translator_source_text_trgm ON mk_translator USING gin (source_tex...\"" app=bssapp-backend
time=2026-06-29T15:41:13.083+03:00 level=INFO msg="[TranslationPerf] index_ready sql=\"CREATE INDEX IF NOT EXISTS idx_mk_translator_translated_text_trgm ON mk_translator USING gin (transl...\"" app=bssapp-backend
time=2026-06-29T15:41:28.657+03:00 level=INFO msg="✉️ Graph Mailer hazır (App-only token) | from=baggiss@baggi.com.tr" app=bssapp-backend
time=2026-06-29T15:41:28.657+03:00 level=INFO msg="✉️ Graph Mailer hazır" app=bssapp-backend
📋 [DEBUG] İlk 10 kullanıcı:
- 1 : ctengiz
- 2 : ali.kale
- 5 : mehmet.keçeci
- 6 : mert.keçeci
- 7 : samet.keçeci
- 9 : orhan.caliskan
- 10 : nilgun.sara
- 14 : rustem.kurbanov
- 15 : caner.akyol
- 16 : kemal.matyakupov
time=2026-06-29T15:41:29.664+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/auth/login [auth:login]" app=bssapp-backend
time=2026-06-29T15:41:30.592+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/auth/refresh [auth:refresh]" app=bssapp-backend
time=2026-06-29T15:41:31.591+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/password/forgot [auth:update]" app=bssapp-backend
time=2026-06-29T15:41:32.507+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/password/reset/validate/{token} [auth:view]" app=bssapp-backend
time=2026-06-29T15:41:33.468+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/password/reset [auth:update]" app=bssapp-backend
time=2026-06-29T15:41:34.431+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/password/change [auth:update]" app=bssapp-backend
time=2026-06-29T15:41:35.355+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/activity-logs [system:read]" app=bssapp-backend
time=2026-06-29T15:41:36.316+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/test-mail [system:update]" app=bssapp-backend
time=2026-06-29T15:41:37.229+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/market-mail-mappings/lookups [system:update]" app=bssapp-backend
time=2026-06-29T15:41:38.104+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/market-mail-mappings [system:update]" app=bssapp-backend
time=2026-06-29T15:41:39.006+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/system/market-mail-mappings/{marketId} [system:update]" app=bssapp-backend
time=2026-06-29T15:41:39.955+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/costing-mail-mappings/lookups [system:update]" app=bssapp-backend
time=2026-06-29T15:41:40.895+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/costing-mail-mappings [system:update]" app=bssapp-backend
time=2026-06-29T15:41:41.853+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/system/costing-mail-mappings/{group} [system:update]" app=bssapp-backend
time=2026-06-29T15:41:42.755+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/pricing-mail-mappings/lookups [system:update]" app=bssapp-backend
time=2026-06-29T15:41:43.666+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/pricing-mail-mappings [system:update]" app=bssapp-backend
time=2026-06-29T15:41:44.571+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/system/pricing-mail-mappings/{group} [system:update]" app=bssapp-backend
time=2026-06-29T15:41:45.491+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/order-price-list-mail-mappings/lookups [system:update]" app=bssapp-backend
time=2026-06-29T15:41:46.403+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/order-price-list-mail-mappings [system:update]" app=bssapp-backend
time=2026-06-29T15:41:47.355+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/system/order-price-list-mail-mappings/{group} [system:update]" app=bssapp-backend
time=2026-06-29T15:41:48.332+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/order-price-list-user-price-groups/lookups [system:update]" app=bssapp-backend
time=2026-06-29T15:41:49.253+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/system/order-price-list-user-price-groups [system:update]" app=bssapp-backend
time=2026-06-29T15:41:50.165+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/system/order-price-list-user-price-groups/{id} [system:update]" app=bssapp-backend
time=2026-06-29T15:41:51.073+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/language/translations [language:update]" app=bssapp-backend
time=2026-06-29T15:41:51.973+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/language/translations/{id} [language:update]" app=bssapp-backend
time=2026-06-29T15:41:52.967+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/language/translations/upsert-missing [language:update]" app=bssapp-backend
time=2026-06-29T15:41:53.910+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/language/translations/sync-sources [language:update]" app=bssapp-backend
time=2026-06-29T15:41:54.837+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/language/translations/translate-selected [language:update]" app=bssapp-backend
time=2026-06-29T15:41:55.808+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/language/translations/bulk-approve [language:update]" app=bssapp-backend
time=2026-06-29T15:41:56.725+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/language/translations/bulk-update [language:update]" app=bssapp-backend
time=2026-06-29T15:41:57.669+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/roles/{id}/permissions [system:update]" app=bssapp-backend
time=2026-06-29T15:41:58.664+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/roles/{id}/permissions [system:update]" app=bssapp-backend
time=2026-06-29T15:41:59.554+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/users/{id}/permissions [system:update]" app=bssapp-backend
time=2026-06-29T15:42:00.500+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/users/{id}/permissions [system:update]" app=bssapp-backend
time=2026-06-29T15:42:01.430+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/users/{id}/order-price-list-price-groups [user:update]" app=bssapp-backend
time=2026-06-29T15:42:02.375+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/users/order-price-list-price-groups/lookups [user:update]" app=bssapp-backend
time=2026-06-29T15:42:03.355+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/users/{id}/order-price-list-price-groups [user:update]" app=bssapp-backend
time=2026-06-29T15:42:04.295+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/permissions/routes [system:view]" app=bssapp-backend
time=2026-06-29T15:42:05.231+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/permissions/effective [system:view]" app=bssapp-backend
time=2026-06-29T15:42:06.173+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/permissions/matrix [system:view]" app=bssapp-backend
time=2026-06-29T15:42:07.109+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/role-dept-permissions/list [system:update]" app=bssapp-backend
time=2026-06-29T15:42:08.011+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/roles/{roleId}/departments/{deptCode}/permissions [system:update]" app=bssapp-backend
time=2026-06-29T15:42:08.934+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/roles/{roleId}/departments/{deptCode}/permissions [system:update]" app=bssapp-backend
time=2026-06-29T15:42:09.893+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/roles/{roleId}/departments/{deptCode}/members [system:update]" app=bssapp-backend
time=2026-06-29T15:42:10.775+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/roles/{roleId}/departments/{deptCode}/members [system:update]" app=bssapp-backend
time=2026-06-29T15:42:11.780+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/users/list [user:view]" app=bssapp-backend
time=2026-06-29T15:42:12.713+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/users [user:insert]" app=bssapp-backend
time=2026-06-29T15:42:13.664+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/users/{id} [user:update]" app=bssapp-backend
time=2026-06-29T15:42:14.607+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/users/{id} [user:update]" app=bssapp-backend
time=2026-06-29T15:42:15.547+03:00 level=INFO msg="✅ Route+Perm registered → DELETE /api/users/{id} [user:delete]" app=bssapp-backend
time=2026-06-29T15:42:16.518+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/users/{id}/admin-reset-password [user:update]" app=bssapp-backend
time=2026-06-29T15:42:17.416+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/users/{id}/send-password-mail [user:update]" app=bssapp-backend
time=2026-06-29T15:42:18.324+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/users/create [user:insert]" app=bssapp-backend
time=2026-06-29T15:42:19.277+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/departments [user:view]" app=bssapp-backend
time=2026-06-29T15:42:20.282+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/nebim-users [user:view]" app=bssapp-backend
time=2026-06-29T15:42:21.217+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/piyasalar [user:view]" app=bssapp-backend
time=2026-06-29T15:42:22.158+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/users-perm [user:view]" app=bssapp-backend
time=2026-06-29T15:42:23.136+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/roles-perm [user:view]" app=bssapp-backend
time=2026-06-29T15:42:24.117+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/departments-perm [user:view]" app=bssapp-backend
time=2026-06-29T15:42:25.118+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/modules [user:view]" app=bssapp-backend
time=2026-06-29T15:42:26.015+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/lookups/roles [user:view]" app=bssapp-backend
time=2026-06-29T15:42:26.927+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/accounts [customer:view]" app=bssapp-backend
time=2026-06-29T15:42:27.875+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/customer-list [customer:view]" app=bssapp-backend
time=2026-06-29T15:42:28.763+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/today-currency [finance:view]" app=bssapp-backend
time=2026-06-29T15:42:29.651+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/export-pdf [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:30.569+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/export-pdf [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:32.534+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/exportstamentheaderreport-pdf [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:33.448+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/customer-balances [finance:view]" app=bssapp-backend
time=2026-06-29T15:42:34.348+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/customer-balances/export-pdf [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:35.245+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/customer-balances/export-excel [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:36.185+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/account-aging-statement [finance:view]" app=bssapp-backend
time=2026-06-29T15:42:37.086+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/account-aging-statement/export-pdf [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:37.983+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/account-aging-statement/export-screen-pdf [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:38.918+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/account-aging-statement/export-excel [finance:export]" app=bssapp-backend
time=2026-06-29T15:42:39.840+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/finance/aged-customer-balance-list [finance:view]" app=bssapp-backend
time=2026-06-29T15:42:40.767+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/statements [finance:view]" app=bssapp-backend
time=2026-06-29T15:42:41.670+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/statements/{id}/details [finance:view]" app=bssapp-backend
time=2026-06-29T15:42:42.592+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/create [order:insert]" app=bssapp-backend
time=2026-06-29T15:42:43.557+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/update [order:update]" app=bssapp-backend
time=2026-06-29T15:42:44.465+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/{id}/bulk-due-date [order:update]" app=bssapp-backend
time=2026-06-29T15:42:45.376+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/get/{id} [order:view]" app=bssapp-backend
time=2026-06-29T15:42:46.342+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orders/list [order:view]" app=bssapp-backend
time=2026-06-29T15:42:47.266+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orders/production-list [order:update]" app=bssapp-backend
time=2026-06-29T15:42:48.177+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orders/production-items/cditem-lookups [order:view]" app=bssapp-backend
time=2026-06-29T15:42:49.084+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orders/production-items/{id} [order:view]" app=bssapp-backend
time=2026-06-29T15:42:50.038+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/orders/production-items/{id}/insert-missing [order:update]" app=bssapp-backend
time=2026-06-29T15:42:50.995+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/orders/production-items/{id}/validate [order:update]" app=bssapp-backend
time=2026-06-29T15:42:51.949+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/orders/production-items/{id}/apply [order:update]" app=bssapp-backend
time=2026-06-29T15:42:52.868+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orders/close-ready [order:update]" app=bssapp-backend
time=2026-06-29T15:42:53.773+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/orders/bulk-close [order:update]" app=bssapp-backend
time=2026-06-29T15:42:54.664+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orders/export [order:export]" app=bssapp-backend
time=2026-06-29T15:42:55.565+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/check/{id} [order:view]" app=bssapp-backend
time=2026-06-29T15:42:56.469+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/validate [order:insert]" app=bssapp-backend
time=2026-06-29T15:42:57.370+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/pdf/{id} [order:export]" app=bssapp-backend
time=2026-06-29T15:42:58.297+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/send-market-mail [order:read]" app=bssapp-backend
time=2026-06-29T15:42:59.232+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order-inventory [order:view]" app=bssapp-backend
time=2026-06-29T15:43:00.117+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/orderpricelistb2b [order:view]" app=bssapp-backend
time=2026-06-29T15:43:01.035+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/min-price [order:view]" app=bssapp-backend
time=2026-06-29T15:43:01.945+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/products [order:view]" app=bssapp-backend
time=2026-06-29T15:43:02.855+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-detail [order:view]" app=bssapp-backend
time=2026-06-29T15:43:03.830+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-cditem [order:view]" app=bssapp-backend
time=2026-06-29T15:43:04.818+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-colors [order:view]" app=bssapp-backend
time=2026-06-29T15:43:05.732+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-newcolors [order:view]" app=bssapp-backend
time=2026-06-29T15:43:06.688+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-colorsize [order:view]" app=bssapp-backend
time=2026-06-29T15:43:07.614+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-secondcolor [order:view]" app=bssapp-backend
time=2026-06-29T15:43:08.550+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-newsecondcolor [order:view]" app=bssapp-backend
time=2026-06-29T15:43:09.459+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-attributes [order:view]" app=bssapp-backend
time=2026-06-29T15:43:10.377+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-item-attributes [order:view]" app=bssapp-backend
time=2026-06-29T15:43:11.293+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-stock-query [order:view]" app=bssapp-backend
time=2026-06-29T15:43:12.205+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-stock-attribute-options [order:view]" app=bssapp-backend
time=2026-06-29T15:43:13.156+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-stock-query-by-attributes [order:view]" app=bssapp-backend
time=2026-06-29T15:43:14.084+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-images [order:view]" app=bssapp-backend
time=2026-06-29T15:43:15.031+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-images/{id}/content [order:view]" app=bssapp-backend
time=2026-06-29T15:43:15.892+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/price-list/products [order:view]" app=bssapp-backend
time=2026-06-29T15:43:16.795+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/price-list/options [order:view]" app=bssapp-backend
time=2026-06-29T15:43:17.755+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/price-list/my-price-groups [order:view]" app=bssapp-backend
time=2026-06-29T15:43:18.648+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/price-list/campaigns [order:view]" app=bssapp-backend
time=2026-06-29T15:43:19.564+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/order/price-list/variant-rows [order:view]" app=bssapp-backend
time=2026-06-29T15:43:20.499+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/price-list/export-excel [order:view]" app=bssapp-backend
time=2026-06-29T15:43:21.398+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/price-list/export-pdf [order:view]" app=bssapp-backend
time=2026-06-29T15:43:22.375+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/order/price-list/export-notify [order:view]" app=bssapp-backend
time=2026-06-29T15:43:23.291+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/product-size-match/rules [order:view]" app=bssapp-backend
time=2026-06-29T15:43:24.221+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/products [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:25.123+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/products/options [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:26.103+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/products/export-all [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:27.014+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/products/price-list/export-excel [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:27.922+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/products/price-list/export-pdf [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:28.826+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/products/calculate-snapshots [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:29.795+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/products/{code}/price-history [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:30.748+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/products/{code}/price-history/delete-latest [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:31.725+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/products/{code}/price-history/delete-selected [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:32.615+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/products/save [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:33.535+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-performance [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:34.467+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-performance/summary [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:35.370+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-performance/markets [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:36.287+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-performance/countries [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:37.181+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-performance/customers [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:39.332+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/product-performance/refresh [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:40.243+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-series/definitions [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:41.188+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/product-series/definitions [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:42.100+03:00 level=INFO msg="✅ Route+Perm registered → PUT /api/pricing/product-series/definitions/{id} [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:43.038+03:00 level=INFO msg="✅ Route+Perm registered → DELETE /api/pricing/product-series/definitions/{id} [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:43.957+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-series/mappings [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:44.899+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/product-series/mappings/orphans [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:45.835+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/product-series/mappings/save [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:46.738+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/wholesale-campaigns [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:47.659+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/wholesale-campaigns/assignments [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:48.603+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/wholesale-campaigns/variants [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:49.594+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/wholesale-campaigns/variant-rows [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:50.549+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/wholesale-campaigns/save [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:51.451+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/wholesale-campaigns/{code}/campaign-history [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:52.372+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/wholesale-campaigns/{code}/campaign-history/delete-selected [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:53.287+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/brand-classification/lookups [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:54.164+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/brand-classification/brands [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:55.120+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/brand-classification/brands/sync [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:56.023+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/brand-classification/brand/{code}/group [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:56.949+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/brand-classification/brands/group-bulk [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:57.868+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/brand-group-currency [pricing:view]" app=bssapp-backend
time=2026-06-29T15:43:58.833+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/brand-group-currency/bulk-save [pricing:update]" app=bssapp-backend
time=2026-06-29T15:43:59.763+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/pricing-rules [pricing:view]" app=bssapp-backend
time=2026-06-29T15:44:00.680+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/pricing-rules/bulk-save [pricing:update]" app=bssapp-backend
time=2026-06-29T15:44:01.570+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/pricing-rules/import [pricing:update]" app=bssapp-backend
time=2026-06-29T15:44:02.472+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/pricing-rules/options [pricing:view]" app=bssapp-backend
time=2026-06-29T15:44:03.423+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/pricing-rules/parameters [pricing:view]" app=bssapp-backend
time=2026-06-29T15:44:04.293+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/pricing-rules/export-all [pricing:view]" app=bssapp-backend
time=2026-06-29T15:44:05.228+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/no-cost-products [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:06.168+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-products [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:07.108+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-history [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:08.025+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-detail-groups [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:08.893+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-detail-header [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:09.837+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/production-types [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:10.850+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/detail-editor-options [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:11.791+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-detail-exchange-rates [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:12.719+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-detail-line-history [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:13.671+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/has-cost-detail-similar-history [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:14.645+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/has-cost-detail-bulk-prices [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:15.528+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/has-cost-detail/last-detail [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:16.433+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/options/hammadde-by-nos [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:17.368+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/onml/save [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:18.358+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/onml/pdf [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:19.331+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/onml/delete [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:20.246+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/default-quantities [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:21.151+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/default-quantities/upsert [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:22.099+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/default-quantities/update-bulk [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:23.019+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/default-quantities/calc-avg [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:23.954+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/default-quantities/lookup [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:24.948+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/default-quantities/refresh [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:25.896+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/tbstok/exists-bulk [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:26.851+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/last10-warnings [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:27.756+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/options/urun-ana-grup [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:28.694+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/options/urun-alt-grup [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:29.598+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/options/urun-ana-alt-combos [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:30.600+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/options/mtbolum [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:31.588+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/pricing/production-product-costing/maliyet-parca-eslestirme [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:32.613+03:00 level=INFO msg="✅ Route+Perm registered → DELETE /api/pricing/production-product-costing/maliyet-parca-eslestirme [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:33.550+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/maliyet-parca-eslestirme/upsert [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:34.514+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/pricing/production-product-costing/maliyet-parca-eslestirme/set-active [costing:view]" app=bssapp-backend
time=2026-06-29T15:44:35.402+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/roles [user:view]" app=bssapp-backend
time=2026-06-29T15:44:36.301+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/departments [user:view]" app=bssapp-backend
time=2026-06-29T15:44:37.224+03:00 level=INFO msg="✅ Route+Perm registered → GET /api/piyasalar [user:view]" app=bssapp-backend
time=2026-06-29T15:44:38.110+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/roles/{id}/departments [user:update]" app=bssapp-backend
time=2026-06-29T15:44:39.073+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/roles/{id}/piyasalar [user:update]" app=bssapp-backend
time=2026-06-29T15:44:40.025+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/users/{id}/roles [user:update]" app=bssapp-backend
time=2026-06-29T15:44:41.004+03:00 level=INFO msg="✅ Route+Perm registered → POST /api/admin/users/{id}/piyasa-sync [admin:user.update]" app=bssapp-backend
time=2026-06-29T15:44:41.005+03:00 level=INFO msg="🌍 CORS Allowed Origin: http://ss.baggi.com.tr/app" app=bssapp-backend
time=2026-06-29T15:44:41.006+03:00 level=INFO msg="🚀 Server running at: 0.0.0.0:8080" app=bssapp-backend
time=2026-06-29T15:44:41.006+03:00 level=INFO msg="🕓 Translation sync next run at 2026-06-30T04:00:00+03:00 (in 12h15m19s)" app=bssapp-backend
time=2026-06-29T15:44:43.006+03:00 level=INFO msg="[PricingFxFull] scheduled next_at=2026-07-06T06:00:00+03:00 in=158h15m17s" app=bssapp-backend
time=2026-06-29T15:44:43.673+03:00 level=INFO msg="[PricingFxDelta] ok (startup): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:44:46.006+03:00 level=INFO msg="[ProductSeriesFull] scheduled next_at=2026-06-30T05:00:00+03:00 in=13h15m14s" app=bssapp-backend
time=2026-06-29T15:45:16.284+03:00 level=INFO msg="✅ Brand sync ok (startup): total=197 upserted=197 deleted=0" app=bssapp-backend
time=2026-06-29T15:45:23.771+03:00 level=INFO msg="Pricing parameter sync source loaded: rows=1003 duration=40.7644195s" app=bssapp-backend
time=2026-06-29T15:45:24.388+03:00 level=INFO msg="Pricing parameter sync copy loaded: rows=1003 duration=41.3818724s" app=bssapp-backend
time=2026-06-29T15:45:24.796+03:00 level=INFO msg="Pricing parameter sync committed: rows=0 duration=41.7900213s" app=bssapp-backend
time=2026-06-29T15:45:24.797+03:00 level=INFO msg="Pricing parameter sync ok (startup): total=1003 upserted=0 deactivated=0" app=bssapp-backend
time=2026-06-29T15:45:43.997+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:46:43.997+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:47:44.011+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:48:44.034+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:49:44.010+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:50:44.005+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:51:44.007+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:52:43.996+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:53:44.001+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:54:44.001+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:55:43.999+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:56:44.002+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:57:44.001+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:58:44.090+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T15:59:43.999+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:00:44.002+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:01:43.999+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:02:44.005+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:03:44.000+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:04:43.999+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:05:44.055+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:06:43.998+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:07:44.001+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:08:44.009+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:09:44.000+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:10:43.997+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:10:44.115+03:00 level=INFO msg="[ProductPricingCalcJob] ok (startup): requested=15178 calculated=8893 skipped=6285 fx_date=2026-06-29 snapshot_publish=false products=0 sdprc_written=0 publish_skipped=0 interval_hours=24 duration=25m58s" app=bssapp-backend
time=2026-06-29T16:11:44.046+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:12:44.012+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:13:44.026+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:14:44.366+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:15:44.347+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:15:50.731+03:00 level=INFO msg="✅ Brand sync ok (scheduled): total=197 upserted=197 deleted=0" app=bssapp-backend
time=2026-06-29T16:16:02.711+03:00 level=INFO msg="Pricing parameter sync source loaded: rows=1003 duration=37.9122008s" app=bssapp-backend
time=2026-06-29T16:16:03.306+03:00 level=INFO msg="Pricing parameter sync copy loaded: rows=1003 duration=38.5077364s" app=bssapp-backend
time=2026-06-29T16:16:03.679+03:00 level=INFO msg="Pricing parameter sync committed: rows=0 duration=38.8807898s" app=bssapp-backend
time=2026-06-29T16:16:03.683+03:00 level=INFO msg="Pricing parameter sync ok (scheduled): total=1003 upserted=0 deactivated=0" app=bssapp-backend
time=2026-06-29T16:16:44.025+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:17:44.018+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:18:44.028+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:19:44.107+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:20:44.026+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:21:44.024+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:22:44.204+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:23:44.029+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:24:44.044+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:25:44.029+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:26:44.028+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:27:44.011+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:28:44.033+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:29:44.037+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:30:44.022+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:31:44.002+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
time=2026-06-29T16:32:44.022+03:00 level=INFO msg="[PricingFxDelta] ok (scheduled): claimed=0 sdprc_written=0 interval_min=1 batch_size=200" app=bssapp-backend
+134
View File
@@ -227,6 +227,7 @@ InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router { func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
r := mux.NewRouter() r := mux.NewRouter()
mountUploads(r)
mountSPA(r) mountSPA(r)
/* /*
@@ -395,6 +396,10 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"language", "update", "language", "update",
wrapV3(routes.GetTranslationRowsHandler(pgDB)), wrapV3(routes.GetTranslationRowsHandler(pgDB)),
) )
r.Handle(
"/api/language/translations/runtime",
http.HandlerFunc(routes.GetRuntimeTranslationsHandler(pgDB)),
).Methods("GET", "OPTIONS")
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/language/translations/{id}", "PUT", "/api/language/translations/{id}", "PUT",
"language", "update", "language", "update",
@@ -825,6 +830,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"order", "view", "order", "view",
wrapV3(http.HandlerFunc(routes.GetProductImagesHandler(pgDB))), wrapV3(http.HandlerFunc(routes.GetProductImagesHandler(pgDB))),
) )
bindV3(r, pgDB,
"/api/product-images/batch", "POST",
"order", "view",
wrapV3(http.HandlerFunc(routes.PostProductImagesBatchHandler(pgDB))),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/product-images/{id}/content", "GET", "/api/product-images/{id}/content", "GET",
"order", "view", "order", "view",
@@ -925,6 +935,91 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "update", "pricing", "update",
wrapV3(routes.PostProductPricingSaveHandler(pgDB, ml)), wrapV3(routes.PostProductPricingSaveHandler(pgDB, ml)),
) )
bindV3(r, pgDB,
"/api/pricing/product-performance", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/export-excel", "POST",
"pricing", "view",
wrapV3(routes.ExportProductPerformanceExcelHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/summary", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceSummaryHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/general", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGeneralHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/orders", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceOrdersHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/orders/groups", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceOrderGroupsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/orders/product-customers", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceOrderProductCustomersHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/orders/market-details", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceOrderMarketDetailsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/markets", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceMarketsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/countries", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceCountriesHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/customers", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceCustomersHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/sales-breakdown", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceSalesBreakdownHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped", "POST",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped/filter-options", "POST",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedFilterOptionsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/sales-details", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceSalesDetailsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/stock-sizes", "GET",
"pricing", "view",
wrapV3(routes.GetProductPerformanceStockSizesHandler()),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/product-series/definitions", "GET", "/api/pricing/product-series/definitions", "GET",
"pricing", "view", "pricing", "view",
@@ -950,6 +1045,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view", "pricing", "view",
wrapV3(http.HandlerFunc(routes.GetProductSeriesMappingsHandler(pgDB))), wrapV3(http.HandlerFunc(routes.GetProductSeriesMappingsHandler(pgDB))),
) )
bindV3(r, pgDB,
"/api/pricing/product-series/mappings/orphans", "GET",
"pricing", "view",
wrapV3(http.HandlerFunc(routes.GetProductSeriesOrphanMappingsHandler(pgDB))),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/product-series/mappings/save", "POST", "/api/pricing/product-series/mappings/save", "POST",
"pricing", "update", "pricing", "update",
@@ -1290,6 +1390,7 @@ func main() {
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432). // (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
_ = godotenv.Overload(".env.local") _ = godotenv.Overload(".env.local")
_ = godotenv.Overload(".env.argebaggi.local")
} }
jwtSecret := os.Getenv("JWT_SECRET") jwtSecret := os.Getenv("JWT_SECRET")
@@ -1317,6 +1418,15 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := db.ConnectMSSQLArgeBaggi(); err != nil {
if strings.Contains(err.Error(), "ARGEBAGGI_MSSQL_CONN tanimli degil") {
log.Println("ArgeBAGGI DB baglantisi atlandi: ARGEBAGGI_MSSQL_CONN tanimli degil")
} else {
// This source enriches one screen; a temporary outage must not stop
// the whole application from starting.
log.Println("ArgeBAGGI DB baglantisi basarisiz:", err)
}
}
pgDB, err := db.ConnectPostgres() pgDB, err := db.ConnectPostgres()
if err != nil { if err != nil {
@@ -1363,6 +1473,9 @@ func main() {
if err := queries.EnsureProductSeriesAutoInfraTables(pgDB); err != nil { if err := queries.EnsureProductSeriesAutoInfraTables(pgDB); err != nil {
log.Println("product series auto infra bootstrap failed:", err) log.Println("product series auto infra bootstrap failed:", err)
} }
if err := queries.EnsureProductPerformanceTables(pgDB); err != nil {
log.Println("product performance infra bootstrap failed:", err)
}
// ------------------------------------------------------- // -------------------------------------------------------
// ✉️ MAILER INIT // ✉️ MAILER INIT
@@ -1389,6 +1502,7 @@ func main() {
startProductPricingFxDeltaScheduler(pgDB) startProductPricingFxDeltaScheduler(pgDB)
startProductPricingFxFullScheduler(pgDB) startProductPricingFxFullScheduler(pgDB)
startProductSeriesAutoSchedulers(pgDB) startProductSeriesAutoSchedulers(pgDB)
startProductPerformanceScheduler(pgDB)
handler := enableCORS( handler := enableCORS(
middlewares.GlobalAuthMiddleware( middlewares.GlobalAuthMiddleware(
@@ -1414,6 +1528,26 @@ func main() {
} }
func mountUploads(r *mux.Router) {
root := strings.TrimSpace(os.Getenv("BLOB_ROOT"))
if root == "" {
return
}
uploadsRoot := filepath.Join(root, "uploads")
if fi, err := os.Stat(uploadsRoot); err != nil || !fi.IsDir() {
return
}
fileServer := http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadsRoot)))
r.PathPrefix("/uploads/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Cache-Control", "public, max-age=3600")
fileServer.ServeHTTP(w, r)
})).Methods(http.MethodGet, http.MethodHead, http.MethodOptions)
}
func mountSPA(r *mux.Router) { func mountSPA(r *mux.Router) {
r.NotFoundHandler = http.HandlerFunc(spaIndex) r.NotFoundHandler = http.HandlerFunc(spaIndex)
r.HandleFunc("/", spaIndex).Methods(http.MethodGet) r.HandleFunc("/", spaIndex).Methods(http.MethodGet)
+1
View File
@@ -66,6 +66,7 @@ var routeMetaCache sync.Map
var routeMetaFallback = map[string]routeMeta{ var routeMetaFallback = map[string]routeMeta{
"GET /api/product-images": {module: "order", action: "view"}, "GET /api/product-images": {module: "order", action: "view"},
"POST /api/product-images/batch": {module: "order", action: "view"},
"GET /api/product-images/{id}/content": {module: "order", action: "view"}, "GET /api/product-images/{id}/content": {module: "order", action: "view"},
"GET /api/product-stock-query-by-attributes": {module: "order", action: "view"}, "GET /api/product-stock-query-by-attributes": {module: "order", action: "view"},
} }
+1
View File
@@ -12,6 +12,7 @@ var publicPaths = []string{
"/api/auth/refresh", "/api/auth/refresh",
"/api/password/forgot", "/api/password/forgot",
"/api/password/reset", "/api/password/reset",
"/api/language/translations/runtime",
"/api/product-images/", "/api/product-images/",
"/uploads/", "/uploads/",
} }
+441
View File
@@ -0,0 +1,441 @@
package models
type ProductPerformanceRow struct {
KpiDate string `json:"kpi_date"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
StockQty float64 `json:"stock_qty"`
SalesQty30 float64 `json:"sales_qty_30d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesQty730 float64 `json:"sales_qty_730d"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSD30 float64 `json:"sales_usd_30d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgDailySales90 float64 `json:"avg_daily_sales_90d"`
AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
AvgDailySales365 float64 `json:"avg_daily_sales_365d"`
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
AvgStock90 float64 `json:"avg_stock_90d"`
AvgStock180 float64 `json:"avg_stock_180d"`
AvgStock365 float64 `json:"avg_stock_365d"`
AvgStockTotal float64 `json:"avg_stock_total"`
StockDays90 float64 `json:"stock_days_90d"`
StockDays180 float64 `json:"stock_days_180d"`
StockDays365 float64 `json:"stock_days_365d"`
StockDaysTotal float64 `json:"stock_days_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
BasePriceTRY float64 `json:"base_price_try"`
GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"`
GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"`
GrossMargin90 float64 `json:"gross_margin_90d"`
GrossMargin180 float64 `json:"gross_margin_180d"`
UnitProfitCost90 float64 `json:"unit_profit_cost_90d"`
UnitProfitCost180 float64 `json:"unit_profit_cost_180d"`
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
MarketCount90 int `json:"market_count_90d"`
MarketCount180 int `json:"market_count_180d"`
MarketCount365 int `json:"market_count_365d"`
MarketCountTotal int `json:"market_count_total"`
CustomerCount90 int `json:"customer_count_90d"`
CustomerCount180 int `json:"customer_count_180d"`
CustomerCount365 int `json:"customer_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
SalesIndex90 float64 `json:"sales_index_90d"`
SalesIndex180 float64 `json:"sales_index_180d"`
SalesIndex365 float64 `json:"sales_index_365d"`
SalesIndexTotal float64 `json:"sales_index_total"`
PriceIndex90 float64 `json:"price_index_90d"`
MarginIndex90 float64 `json:"margin_index_90d"`
PerformanceScore90 float64 `json:"performance_score_90d"`
PerformanceScore180 float64 `json:"performance_score_180d"`
PerformanceScore365 float64 `json:"performance_score_365d"`
PerformanceScoreTotal float64 `json:"performance_score_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
LastRefNumber string `json:"last_ref_number"`
UpdatedAt string `json:"updated_at"`
}
type ProductPerformanceSummary struct {
KpiDate string `json:"kpi_date"`
TotalRows int `json:"total_rows"`
StarCount int `json:"star_count"`
StockRisk int `json:"stock_risk"`
NoStockDemand int `json:"no_stock_demand"`
TotalStock float64 `json:"total_stock"`
StockCostUSD float64 `json:"stock_cost_usd"`
RiskCostUSD float64 `json:"risk_cost_usd"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
GrossProfit90 float64 `json:"gross_profit_usd_90d"`
MarketCount90 int `json:"market_count_90d"`
CustomerCount90 int `json:"customer_count_90d"`
UpdatedAt string `json:"updated_at"`
}
type ProductPerformanceGeneralRow struct {
PeriodStart string `json:"period_start"`
PeriodEnd string `json:"period_end"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
StockQty float64 `json:"stock_qty"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
StockDaysTotal float64 `json:"stock_days_total"`
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
GrossProfitUSDTotal float64 `json:"gross_profit_usd_total"`
GrossMarginTotal float64 `json:"gross_margin_total"`
UnitProfitCostTotal float64 `json:"unit_profit_cost_total"`
UnitProfitBaseTotal float64 `json:"unit_profit_base_total"`
MarketCountTotal int `json:"market_count_total"`
CustomerCountTotal int `json:"customer_count_total"`
InvoiceCountTotal int `json:"invoice_count_total"`
SalesIndexTotal float64 `json:"sales_index_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
FirstSaleDate string `json:"first_sale_date"`
LastSaleDate string `json:"last_sale_date"`
LastRefNumber string `json:"last_ref_number"`
}
type ProductPerformanceOrderAnalysisRow struct {
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
OrderQty float64 `json:"order_qty"`
OrderUSD float64 `json:"order_usd"`
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
StockQty float64 `json:"stock_qty"`
NetStockAfterOrder float64 `json:"net_stock_after_order"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
UnitProfitCostUSD float64 `json:"unit_profit_cost_usd"`
UnitProfitBaseUSD float64 `json:"unit_profit_base_usd"`
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
ExpectedMarginCost float64 `json:"expected_margin_cost"`
MarketCount int `json:"market_count"`
CustomerCount int `json:"customer_count"`
OrderCount int `json:"order_count"`
LineCount int `json:"line_count"`
FirstOrderDate string `json:"first_order_date"`
LastOrderDate string `json:"last_order_date"`
EarliestDueDate string `json:"earliest_due_date"`
OverdueQty float64 `json:"overdue_qty"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
}
type ProductPerformanceOrderGroupRow struct {
Breakdown string `json:"breakdown"`
GroupKey string `json:"group_key"`
MarketKey string `json:"market_key"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
ProductCount int `json:"product_count"`
OrderQty float64 `json:"order_qty"`
OrderUSD float64 `json:"order_usd"`
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
BaseCostUSD float64 `json:"base_cost_usd"`
CostAmountUSD float64 `json:"cost_amount_usd"`
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
ExpectedMarginBase float64 `json:"expected_margin_base"`
ExpectedMarginCost float64 `json:"expected_margin_cost"`
StockQty float64 `json:"stock_qty"`
NetStockAfterOrder float64 `json:"net_stock_after_order"`
MarketCount int `json:"market_count"`
CustomerCount int `json:"customer_count"`
OrderCount int `json:"order_count"`
LineCount int `json:"line_count"`
OverdueQty float64 `json:"overdue_qty"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
}
type ProductPerformanceOrderProductCustomerRow struct {
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
OrderQty float64 `json:"order_qty"`
OrderUSD float64 `json:"order_usd"`
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
StockQty float64 `json:"stock_qty"`
NetStockAfterOrder float64 `json:"net_stock_after_order"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
ExpectedMarginBase float64 `json:"expected_margin_base"`
ExpectedMarginCost float64 `json:"expected_margin_cost"`
OrderCount int `json:"order_count"`
LineCount int `json:"line_count"`
OverdueQty float64 `json:"overdue_qty"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
}
type ProductPerformanceOrderMarketDetailRow struct {
MarketKey string `json:"market_key"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
OrderNumber string `json:"order_number"`
OrderDate string `json:"order_date"`
DueDate string `json:"due_date"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
OrderQty float64 `json:"order_qty"`
OrderUSD float64 `json:"order_usd"`
AvgOrderPriceUSD float64 `json:"avg_order_price_usd"`
StockQty float64 `json:"stock_qty"`
NetStockAfterOrder float64 `json:"net_stock_after_order"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
ExpectedProfitBaseUSD float64 `json:"expected_profit_base_usd"`
ExpectedProfitCostUSD float64 `json:"expected_profit_cost_usd"`
ExpectedMarginBase float64 `json:"expected_margin_base"`
ExpectedMarginCost float64 `json:"expected_margin_cost"`
IsOverdue bool `json:"is_overdue"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
}
type ProductPerformanceMarketRow struct {
MarketKey string `json:"market_key"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
ProductCount int `json:"product_count"`
StarCount int `json:"star_count"`
StockRiskCount int `json:"stock_risk_count"`
StockQty float64 `json:"stock_qty"`
StockCostValueUSD float64 `json:"stock_cost_value_usd"`
RiskStockCostValueUSD float64 `json:"risk_stock_cost_value_usd"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"`
AvgGrossMargin90 float64 `json:"avg_gross_margin_90d"`
AvgStockDays90 float64 `json:"avg_stock_days_90d"`
}
type ProductPerformanceCountryRow struct {
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
MarketKey string `json:"market_key"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
ProductCount int `json:"product_count"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
CustomerCount90 int `json:"customer_count_90d"`
InvoiceCount90 int `json:"invoice_count_90d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
}
type ProductPerformanceCustomerRow struct {
Breakdown string `json:"breakdown"`
MarketKey string `json:"market_key"`
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
ProductCount int `json:"product_count"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
InvoiceCount90 int `json:"invoice_count_90d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
LastSaleDate string `json:"last_sale_date"`
}
type ProductPerformanceSalesBreakdownRow struct {
Breakdown string `json:"breakdown"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
ProductCount int `json:"product_count"`
ProductGroupCount90 int `json:"product_group_count_90d"`
ProductGroupCount180 int `json:"product_group_count_180d"`
ProductGroupCount365 int `json:"product_group_count_365d"`
ProductGroupCountTotal int `json:"product_group_count_total"`
MarketCount90 int `json:"market_count_90d"`
MarketCount180 int `json:"market_count_180d"`
MarketCount365 int `json:"market_count_365d"`
MarketCountTotal int `json:"market_count_total"`
CustomerCount90 int `json:"customer_count_90d"`
InvoiceCount90 int `json:"invoice_count_90d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
BasePriceUSD90 float64 `json:"base_price_usd_90d"`
CostPriceUSD90 float64 `json:"cost_price_usd_90d"`
GrossProfitBase90 float64 `json:"gross_profit_base_usd_90d"`
GrossProfitCost90 float64 `json:"gross_profit_cost_usd_90d"`
GrossMarginBase90 float64 `json:"gross_margin_base_90d"`
GrossMarginCost90 float64 `json:"gross_margin_cost_90d"`
CustomerCount180 int `json:"customer_count_180d"`
InvoiceCount180 int `json:"invoice_count_180d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
BasePriceUSD180 float64 `json:"base_price_usd_180d"`
CostPriceUSD180 float64 `json:"cost_price_usd_180d"`
GrossProfitBase180 float64 `json:"gross_profit_base_usd_180d"`
GrossProfitCost180 float64 `json:"gross_profit_cost_usd_180d"`
GrossMarginBase180 float64 `json:"gross_margin_base_180d"`
GrossMarginCost180 float64 `json:"gross_margin_cost_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
AvgPriceUSD365 float64 `json:"avg_price_usd_365d"`
BasePriceUSD365 float64 `json:"base_price_usd_365d"`
CostPriceUSD365 float64 `json:"cost_price_usd_365d"`
GrossProfitBase365 float64 `json:"gross_profit_base_usd_365d"`
GrossProfitCost365 float64 `json:"gross_profit_cost_usd_365d"`
GrossMarginBase365 float64 `json:"gross_margin_base_365d"`
GrossMarginCost365 float64 `json:"gross_margin_cost_365d"`
CustomerCount365 int `json:"customer_count_365d"`
InvoiceCount365 int `json:"invoice_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
InvoiceCountTotal int `json:"invoice_count_total"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
BasePriceUSDTotal float64 `json:"base_price_usd_total"`
CostPriceUSDTotal float64 `json:"cost_price_usd_total"`
GrossProfitBaseTotal float64 `json:"gross_profit_base_usd_total"`
GrossProfitCostTotal float64 `json:"gross_profit_cost_usd_total"`
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
StockQty float64 `json:"stock_qty"`
AvgStock90 float64 `json:"avg_stock_90d"`
AvgStock180 float64 `json:"avg_stock_180d"`
AvgStock365 float64 `json:"avg_stock_365d"`
AvgStockTotal float64 `json:"avg_stock_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
HasCost bool `json:"has_cost"`
SalesIndex90 float64 `json:"sales_index_90d"`
SalesIndex180 float64 `json:"sales_index_180d"`
SalesIndex365 float64 `json:"sales_index_365d"`
SalesIndexTotal float64 `json:"sales_index_total"`
CustomerScore90 float64 `json:"customer_score_90d"`
CustomerScore180 float64 `json:"customer_score_180d"`
CustomerScore365 float64 `json:"customer_score_365d"`
CustomerScoreTotal float64 `json:"customer_score_total"`
PerformanceScore90 float64 `json:"performance_score_90d"`
PerformanceScore180 float64 `json:"performance_score_180d"`
PerformanceScore365 float64 `json:"performance_score_365d"`
PerformanceScoreTotal float64 `json:"performance_score_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
}
type ProductPerformanceSalesDetailRow struct {
SalesDate string `json:"sales_date"`
RefNumber string `json:"ref_number"`
MarketKey string `json:"market_key"`
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
SalesQty float64 `json:"sales_qty"`
SalesUSD float64 `json:"sales_usd"`
AvgPriceUSD float64 `json:"avg_price_usd"`
}
type ProductPerformanceStockSizeRow struct {
SizeCode string `json:"size_code"`
StockQty float64 `json:"stock_qty"`
}
+19 -12
View File
@@ -1,22 +1,29 @@
package models package models
type ProductionNoCostProductRow struct { type ProductionNoCostProductRow struct {
UretimSekli string `json:"UretimSekli"` UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"` UrtSiparisNo string `json:"nUrtSiparisNo"`
IslemTarihi string `json:"dteIslemTarihi"` IslemTarihi string `json:"dteIslemTarihi"`
FirmaKodu string `json:"FirmaKodu"` FirmaKodu string `json:"FirmaKodu"`
FirmaAdi string `json:"FirmaAdi"` FirmaAdi string `json:"FirmaAdi"`
SonIsEmriVeren string `json:"SonIsEmriVeren"` SonIsEmriVeren string `json:"SonIsEmriVeren"`
ModelAdi string `json:"sAdi"` ModelAdi string `json:"sAdi"`
Kodu string `json:"sKodu"` Kodu string `json:"sKodu"`
SKullaniciAdi string `json:"sKullaniciAdi"` SKullaniciAdi string `json:"sKullaniciAdi"`
SKullaniciGunc string `json:"sKullaniciAdiGunc"` SKullaniciGunc string `json:"sKullaniciAdiGunc"`
MMiktarG float64 `json:"lMMiktar_G"` MMiktarG float64 `json:"lMMiktar_G"`
MModelKodu string `json:"sMModelKodu"` CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
MModelKodu string `json:"sMModelKodu"`
} }
type ProductionHasCostProductRow struct { type ProductionHasCostProductRow struct {
UretimSekli string `json:"UretimSekli"` UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
NOnMLNo string `json:"nOnMLNo"` NOnMLNo string `json:"nOnMLNo"`
UrunKodu string `json:"UrunKodu"` UrunKodu string `json:"UrunKodu"`
UrunAdi string `json:"UrunAdi"` UrunAdi string `json:"UrunAdi"`
+214
View File
@@ -0,0 +1,214 @@
package main
import (
"bssapp-backend/queries"
"context"
"database/sql"
"log"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
func startProductPerformanceScheduler(pgDB *sql.DB) {
enabled := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_PERFORMANCE_ENABLED")))
if enabled == "0" || enabled == "false" || enabled == "off" {
log.Println("Product performance scheduler disabled")
return
}
if pgDB == nil {
return
}
deltaDays := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_DAYS", 45, 1, 365)
deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00")
deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24)
runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false)
fullEnabled := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_ENABLED", false)
fullWeekday := productPerformanceEnvWeekday("PRODUCT_PERFORMANCE_FULL_WEEKDAY", time.Saturday)
fullHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_FULL_HHMM", "16:00")
fullStartDate := productPerformanceEnvDate("PRODUCT_PERFORMANCE_FULL_START_DATE", time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local))
fullTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_FULL_TIMEOUT_HOURS", 18, 1, 72)
fullRunOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_RUN_ON_STARTUP", false)
var running int32
runDelta := func(reason string) {
if !atomic.CompareAndSwapInt32(&running, 0, 1) {
log.Printf("[ProductPerformanceJob] skip (%s): already running", reason)
return
}
defer atomic.StoreInt32(&running, 0)
endDate := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deltaTimeoutHours)*time.Hour)
defer cancel()
result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{
Mode: "delta",
Stage: "all",
StartDate: endDate.AddDate(0, 0, -deltaDays),
EndDate: endDate,
})
if err != nil {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return
}
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
}
runFull := func(reason string) {
if !atomic.CompareAndSwapInt32(&running, 0, 1) {
log.Printf("[ProductPerformanceJob] skip (%s): already running", reason)
return
}
defer atomic.StoreInt32(&running, 0)
endDate := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(fullTimeoutHours)*time.Hour)
defer cancel()
result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{
Mode: "full",
Stage: "all",
StartDate: fullStartDate,
EndDate: endDate,
})
if err != nil {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return
}
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
}
log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t",
deltaHHMM, deltaDays, runOnStartup)
if fullEnabled {
log.Printf("[ProductPerformanceJob] scheduled weekly_full=%s %s start_date=%s timeout_hours=%d run_full_on_startup=%t",
fullWeekday.String(), fullHHMM, fullStartDate.Format("2006-01-02"), fullTimeoutHours, fullRunOnStartup)
}
go func() {
if runOnStartup {
time.Sleep(20 * time.Second)
runDelta("startup-delta")
}
for {
next := productPerformanceNextDaily(time.Now(), deltaHHMM)
log.Printf("[ProductPerformanceJob] daily delta next_at=%s in=%s", next.Format(time.RFC3339), time.Until(next).Round(time.Second))
time.Sleep(time.Until(next))
runDelta("daily-delta")
}
}()
if fullEnabled {
go func() {
if fullRunOnStartup {
time.Sleep(30 * time.Second)
runFull("startup-full")
}
for {
next := productPerformanceNextWeekly(time.Now(), fullWeekday, fullHHMM)
log.Printf("[ProductPerformanceJob] weekly full next_at=%s in=%s", next.Format(time.RFC3339), time.Until(next).Round(time.Second))
time.Sleep(time.Until(next))
runFull("weekly-full")
}
}()
}
}
func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
hour, minute := productPerformanceParseHHMM(hhmm, 2, 0)
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
if !next.After(now) {
next = next.AddDate(0, 0, 1)
}
return next
}
func productPerformanceNextWeekly(now time.Time, weekday time.Weekday, hhmm string) time.Time {
hour, minute := productPerformanceParseHHMM(hhmm, 16, 0)
daysUntil := (int(weekday) - int(now.Weekday()) + 7) % 7
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()).AddDate(0, 0, daysUntil)
if !next.After(now) {
next = next.AddDate(0, 0, 7)
}
return next
}
func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) {
parts := strings.Split(strings.TrimSpace(raw), ":")
if len(parts) != 2 {
return fallbackHour, fallbackMinute
}
h, herr := strconv.Atoi(parts[0])
m, merr := strconv.Atoi(parts[1])
if herr != nil || merr != nil || h < 0 || h > 23 || m < 0 || m > 59 {
return fallbackHour, fallbackMinute
}
return h, m
}
func productPerformanceEnvString(name, fallback string) string {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
return raw
}
func productPerformanceEnvInt(name string, fallback, min, max int) int {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
n, err := strconv.Atoi(raw)
if err != nil || n < min || n > max {
return fallback
}
return n
}
func productPerformanceEnvBool(name string, fallback bool) bool {
raw := strings.TrimSpace(strings.ToLower(os.Getenv(name)))
if raw == "" {
return fallback
}
return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}
func productPerformanceEnvDate(name string, fallback time.Time) time.Time {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := time.ParseInLocation("2006-01-02", raw, time.Local)
if err != nil {
return fallback
}
return parsed
}
func productPerformanceEnvWeekday(name string, fallback time.Weekday) time.Weekday {
switch strings.ToLower(strings.TrimSpace(os.Getenv(name))) {
case "sunday", "sun", "pazar", "0":
return time.Sunday
case "monday", "mon", "pazartesi", "1":
return time.Monday
case "tuesday", "tue", "sali", "salı", "2":
return time.Tuesday
case "wednesday", "wed", "carsamba", "çarşamba", "3":
return time.Wednesday
case "thursday", "thu", "persembe", "perşembe", "4":
return time.Thursday
case "friday", "fri", "cuma", "5":
return time.Friday
case "saturday", "sat", "cumartesi", "6":
return time.Saturday
default:
return fallback
}
}
+508 -60
View File
@@ -101,10 +101,60 @@ func productSeriesFallbackAutoCreateEnabled() bool {
return raw == "1" || raw == "true" || raw == "on" || raw == "yes" return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
} }
func productSeriesFallbackLogEnabled() bool {
raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_SERIES_FALLBACK_LOG")))
if raw == "" {
return false
}
return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}
func productSeriesFallbackOverrideExisting() bool {
// Default off: do not override existing (possibly manual) assignments.
raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_SERIES_FALLBACK_OVERRIDE_EXISTING")))
if raw == "" {
return false
}
return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}
func productSeriesDebugMatch(v productSeriesAutoVariant) bool {
p := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_DEBUG_PRODUCT"))
if p != "" && !strings.EqualFold(strings.TrimSpace(v.ProductCode), p) {
return false
}
c := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_DEBUG_COLOR"))
if c != "" && !strings.EqualFold(strings.TrimSpace(v.ColorCode), c) {
return false
}
d := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_DEBUG_DIM3"))
if d != "" && !strings.EqualFold(strings.TrimSpace(v.Dim3Code), d) {
return false
}
return p != "" || c != "" || d != ""
}
func productSeriesFormatSizeQty(m map[string]float64) string {
if len(m) == 0 {
return "{}"
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%.0f", k, m[k]))
}
return "{" + strings.Join(parts, ",") + "}"
}
func productSeriesFallbackCode() string { func productSeriesFallbackCode() string {
code := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_FALLBACK_SERIES_CODE")) code := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_FALLBACK_SERIES_CODE"))
if code == "" { if code == "" {
code = "1-" // In this installation, the default series code is "1" (not "1-").
code = "1"
} }
return code return code
} }
@@ -242,9 +292,12 @@ func startProductSeriesAutoSchedulers(pgDB *sql.DB) {
apply := envBool("PRODUCT_SERIES_AUTO_APPLY", false) apply := envBool("PRODUCT_SERIES_AUTO_APPLY", false)
deltaIntervalMin := envIntRange("PRODUCT_SERIES_DELTA_INTERVAL_MIN", 60, 5, 1440) deltaIntervalMin := envIntRange("PRODUCT_SERIES_DELTA_INTERVAL_MIN", 60, 5, 1440)
queueBatch := envIntRange("PRODUCT_SERIES_DELTA_BATCH_SIZE", 300, 10, 2000) queueBatch := envIntRange("PRODUCT_SERIES_DELTA_BATCH_SIZE", 300, 10, 2000)
fullHH, fullMM := envHHMM("PRODUCT_SERIES_FULL_HHMM", 5, 30) fullHH, fullMM := envHHMM("PRODUCT_SERIES_FULL_HHMM", 5, 0)
runFullOnStartup := envBool("PRODUCT_SERIES_FULL_RUN_ON_STARTUP", false) // Deploys should not trigger heavy jobs. Startup runs are gated behind an extra allow flag so
runDeltaOnStartup := envBool("PRODUCT_SERIES_DELTA_RUN_ON_STARTUP", false) // a stale/old env var value can't accidentally bring back "runs on every deploy" behavior.
allowStartupRuns := envBool("PRODUCT_SERIES_ALLOW_STARTUP_RUNS", false)
runFullOnStartup := allowStartupRuns && envBool("PRODUCT_SERIES_FULL_RUN_ON_STARTUP", false)
runDeltaOnStartup := allowStartupRuns && envBool("PRODUCT_SERIES_DELTA_RUN_ON_STARTUP", false)
var deltaRunning int32 var deltaRunning int32
var fullRunning int32 var fullRunning int32
@@ -313,14 +366,14 @@ func startProductSeriesAutoSchedulers(pgDB *sql.DB) {
runFull("startup_manual") runFull("startup_manual")
} }
for { for {
next := nextProductSeriesWeeklyRun(time.Now(), fullHH, fullMM) next := nextProductSeriesDailyRun(time.Now(), fullHH, fullMM)
wait := time.Until(next) wait := time.Until(next)
if wait < 0 { if wait < 0 {
wait = time.Minute wait = time.Minute
} }
log.Printf("[ProductSeriesFull] scheduled next_at=%s in=%s", next.Format(time.RFC3339), wait.Round(time.Second)) log.Printf("[ProductSeriesFull] scheduled next_at=%s in=%s", next.Format(time.RFC3339), wait.Round(time.Second))
time.Sleep(wait) time.Sleep(wait)
runFull("weekly") runFull("daily")
} }
}() }()
} }
@@ -483,36 +536,170 @@ func productSeriesApplyVariant(ctx context.Context, pg *sql.DB, v productSeriesA
if err != nil { if err != nil {
return 0, 1, err return 0, 1, err
} }
appliedFallback := false
if !ready { if !ready {
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] skip_not_ready product=%s color=%s dim3=%s", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code))
}
return 0, 1, nil return 0, 1, nil
} }
if productSeriesDebugMatch(v) {
log.Printf("[ProductSeriesDebug] variant product=%s color=%s dim3=%s mmitem_id=%d dim1=%d dim3_id=%v group=%s total=%.0f sizes=%s",
strings.TrimSpace(v.ProductCode),
strings.TrimSpace(v.ColorCode),
strings.TrimSpace(v.Dim3Code),
mmitemID,
dim1ID,
func() any {
if dim3ID.Valid {
return dim3ID.Int64
}
return nil
}(),
strings.TrimSpace(v.GroupKey),
math.Round(v.TotalQty),
productSeriesFormatSizeQty(v.SizeQty),
)
}
selected := productSeriesSelectRules(v, rules) selected := productSeriesSelectRules(v, rules)
if productSeriesDebugMatch(v) {
if len(selected) == 0 {
log.Printf("[ProductSeriesDebug] selected=0 product=%s color=%s dim3=%s", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code))
} else {
ids := make([]string, 0, len(selected))
for _, r := range selected {
ids = append(ids, fmt.Sprintf("%d", r.SeriesID))
}
log.Printf("[ProductSeriesDebug] selected=%d product=%s color=%s dim3=%s series_ids=%s",
len(selected),
strings.TrimSpace(v.ProductCode),
strings.TrimSpace(v.ColorCode),
strings.TrimSpace(v.Dim3Code),
strings.Join(ids, ","),
)
}
}
if len(selected) == 0 { if len(selected) == 0 {
fallbackID, _, err := productSeriesResolveFallbackSeries(ctx, pg) // Apply fallback based on per-size availability, not total.
// If none of the sizes has qty >= 1 (default), do nothing.
minSizeQty := envFloat64("PRODUCT_SERIES_FALLBACK_MIN_SIZE_QTY", 1)
if !productSeriesHasAnySizeAtLeast(v.SizeQty, minSizeQty) {
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] skip_insufficient_stock product=%s color=%s dim3=%s min_size_qty=%.2f sizes=%s",
strings.TrimSpace(v.ProductCode),
strings.TrimSpace(v.ColorCode),
strings.TrimSpace(v.Dim3Code),
minSizeQty,
productSeriesFormatSizeQty(v.SizeQty),
)
}
return 0, 1, nil
}
fallbackID, fallbackCode, err := productSeriesResolveFallbackSeries(ctx, pg)
if err != nil { if err != nil {
return 0, 1, err return 0, 1, err
} }
if fallbackID > 0 { if fallbackID > 0 {
// Only apply fallback when the variant has no assignments yet. if !productSeriesFallbackOverrideExisting() {
var exists int // By default we don't override existing assignments. However, if the existing assignment(s)
checkErr := pg.QueryRowContext(ctx, ` // are no longer compatible with current stock (e.g., stock sold out), keeping them causes
SELECT 1 // stale/wrong series to stick forever (already_exists). In that case, we should replace
FROM zbggseri // them with fallback=1.
WHERE mmitem_id=$1 existingIDs, err := productSeriesLoadExistingSeriesIDs(ctx, pg, mmitemID, dim1ID, dim3ID)
AND dim1=$2 if err != nil {
AND (($3::bigint IS NULL AND dim3 IS NULL) OR dim3=$3::bigint) return 0, 1, err
LIMIT 1 }
`, mmitemID, dim1ID, nullableInt64ForAuto(dim3ID)).Scan(&exists) if len(existingIDs) > 0 {
if checkErr == nil { // Optional grace window: if we recently applied non-fallback rules for this variant,
// keep existing manual/previous assignment; nothing to do // don't immediately flap back to fallback due to a transient/partial stock snapshot.
return 0, 0, nil graceMin := envIntRange("PRODUCT_SERIES_FALLBACK_GRACE_MINUTES", 2, 0, 60)
if graceMin > 0 {
rowKey := productSeriesAutoKey(v.ProductCode, v.ColorCode, v.Dim3Code)
kind, at, err := productSeriesLoadLastApplyState(ctx, pg, rowKey)
if err != nil {
return 0, 1, err
}
if kind == "rules" && time.Since(at) < time.Duration(graceMin)*time.Minute {
// keep existing mapping as-is for now
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] grace_skip product=%s color=%s dim3=%s grace_min=%d last_rules_at=%s",
strings.TrimSpace(v.ProductCode),
strings.TrimSpace(v.ColorCode),
strings.TrimSpace(v.Dim3Code),
graceMin,
at.Format(time.RFC3339),
)
}
return 0, 0, nil
}
}
anyStillValid := false
for _, sid := range existingIDs {
// If we can't find the rule definition in memory, assume it's a manual/unknown mapping and keep it.
rule, ok := productSeriesFindRuleByID(rules, sid)
if !ok {
anyStillValid = true
break
}
if productSeriesCanConsume(v.SizeQty, rule) {
anyStillValid = true
break
}
}
if anyStillValid {
_ = productSeriesResetInvalidStreak(ctx, pg, productSeriesAutoKey(v.ProductCode, v.ColorCode, v.Dim3Code))
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] already_exists product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID)
}
// keep existing manual/previous assignment; nothing to do
return 0, 0, nil
}
// Guardrail: if stock snapshots are partial/transient, "existing invalid" can flap.
// Require N consecutive invalid observations before overwriting existing mapping.
confirmations := envIntRange("PRODUCT_SERIES_EXISTING_INVALID_CONFIRMATIONS", 3, 1, 20)
rowKey := productSeriesAutoKey(v.ProductCode, v.ColorCode, v.Dim3Code)
streak, err := productSeriesBumpInvalidStreak(ctx, pg, rowKey)
if err != nil {
return 0, 1, err
}
if streak < confirmations {
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] existing_invalid_defer product=%s color=%s dim3=%s streak=%d/%d fallback=%s(%d)",
strings.TrimSpace(v.ProductCode),
strings.TrimSpace(v.ColorCode),
strings.TrimSpace(v.Dim3Code),
streak,
confirmations,
strings.TrimSpace(fallbackCode),
fallbackID,
)
}
// keep existing mapping for now
return 0, 0, nil
}
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] existing_invalid_apply product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID)
}
}
} }
if checkErr != nil && checkErr != sql.ErrNoRows { if productSeriesFallbackLogEnabled() {
return 0, 1, checkErr if productSeriesFallbackOverrideExisting() {
log.Printf("[ProductSeriesFallback] override_apply product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID)
} else {
log.Printf("[ProductSeriesFallback] apply product=%s color=%s dim3=%s fallback=%s(%d)", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode), fallbackID)
}
} }
// Use the fallback series as the single selected rule. // Use the fallback series as the single selected rule.
selected = []productSeriesAutoRule{{SeriesID: fallbackID}} selected = []productSeriesAutoRule{{SeriesID: fallbackID}}
appliedFallback = true
} else { } else {
if productSeriesFallbackLogEnabled() {
log.Printf("[ProductSeriesFallback] missing_fallback product=%s color=%s dim3=%s fallback_code=%s", strings.TrimSpace(v.ProductCode), strings.TrimSpace(v.ColorCode), strings.TrimSpace(v.Dim3Code), strings.TrimSpace(fallbackCode))
}
return 0, 1, nil return 0, 1, nil
} }
} }
@@ -525,6 +712,11 @@ LIMIT 1
return 0, 0, err return 0, 0, err
} }
defer tx.Rollback() defer tx.Rollback()
// Tag the DB session for audit attribution (scheduler vs http handler).
// This affects only statements in this transaction.
_, _ = tx.ExecContext(ctx, `SELECT set_config('application_name', current_setting('application_name') || '|series-auto', true)`)
if _, err := tx.ExecContext(ctx, ` if _, err := tx.ExecContext(ctx, `
DELETE FROM zbggseri DELETE FROM zbggseri
WHERE mmitem_id=$1 WHERE mmitem_id=$1
@@ -544,9 +736,120 @@ VALUES ($1, $2, $3, $4)
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return 0, 0, err return 0, 0, err
} }
// Track what we last applied for this variant to reduce flapping and help ops debugging.
rowKey := productSeriesAutoKey(v.ProductCode, v.ColorCode, v.Dim3Code)
kind := "rules"
if appliedFallback {
kind = "fallback"
}
ids := make([]string, 0, len(selected))
for _, r := range selected {
ids = append(ids, fmt.Sprintf("%d", r.SeriesID))
}
_, _ = pg.ExecContext(ctx, `
INSERT INTO mk_product_series_apply_state (row_key, last_apply_at, last_apply_kind, last_series_ids, updated_at)
VALUES ($1, now(), $2, $3, now())
ON CONFLICT (row_key) DO UPDATE
SET last_apply_at=EXCLUDED.last_apply_at,
last_apply_kind=EXCLUDED.last_apply_kind,
last_series_ids=EXCLUDED.last_series_ids,
invalid_streak=0,
last_invalid_at=NULL,
updated_at=now()
`, rowKey, kind, strings.Join(ids, ","))
if productSeriesDebugMatch(v) {
log.Printf("[ProductSeriesDebug] wrote=%d product=%s color=%s dim3=%s series_ids=%s",
len(selected),
strings.TrimSpace(v.ProductCode),
strings.TrimSpace(v.ColorCode),
strings.TrimSpace(v.Dim3Code),
strings.Join(ids, ","),
)
}
return len(selected), 0, nil return len(selected), 0, nil
} }
func productSeriesLoadLastApplyState(ctx context.Context, pg *sql.DB, rowKey string) (kind string, at time.Time, err error) {
var k string
var t time.Time
err = pg.QueryRowContext(ctx, `
SELECT COALESCE(last_apply_kind,''), COALESCE(last_apply_at, now())
FROM mk_product_series_apply_state
WHERE row_key=$1
`, rowKey).Scan(&k, &t)
if err == sql.ErrNoRows {
return "", time.Time{}, nil
}
if err != nil {
return "", time.Time{}, err
}
return k, t, nil
}
func productSeriesBumpInvalidStreak(ctx context.Context, pg *sql.DB, rowKey string) (int, error) {
var next int
err := pg.QueryRowContext(ctx, `
INSERT INTO mk_product_series_apply_state (row_key, last_apply_at, last_apply_kind, last_series_ids, invalid_streak, last_invalid_at, updated_at)
VALUES ($1, now(), '', '', 1, now(), now())
ON CONFLICT (row_key) DO UPDATE
SET invalid_streak=mk_product_series_apply_state.invalid_streak + 1,
last_invalid_at=now(),
updated_at=now()
RETURNING invalid_streak
`, rowKey).Scan(&next)
if err != nil {
return 0, err
}
return next, nil
}
func productSeriesResetInvalidStreak(ctx context.Context, pg *sql.DB, rowKey string) error {
_, err := pg.ExecContext(ctx, `
UPDATE mk_product_series_apply_state
SET invalid_streak=0,
last_invalid_at=NULL,
updated_at=now()
WHERE row_key=$1
`, rowKey)
return err
}
func productSeriesFindRuleByID(rules []productSeriesAutoRule, seriesID int64) (productSeriesAutoRule, bool) {
for _, r := range rules {
if r.SeriesID == seriesID {
return r, true
}
}
return productSeriesAutoRule{}, false
}
func productSeriesLoadExistingSeriesIDs(ctx context.Context, pg *sql.DB, mmitemID int64, dim1ID int64, dim3ID sql.NullInt64) ([]int64, error) {
rows, err := pg.QueryContext(ctx, `
SELECT seri_id
FROM zbggseri
WHERE mmitem_id=$1
AND dim1=$2
AND (($3::bigint IS NULL AND dim3 IS NULL) OR dim3=$3::bigint)
ORDER BY seri_id
`, mmitemID, dim1ID, nullableInt64ForAuto(dim3ID))
if err != nil {
return nil, err
}
defer rows.Close()
out := []int64{}
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
if id > 0 {
out = append(out, id)
}
}
return out, rows.Err()
}
func productSeriesLoadMSSQLStockVariants(ctx context.Context) (map[string]productSeriesAutoVariant, int, error) { func productSeriesLoadMSSQLStockVariants(ctx context.Context) (map[string]productSeriesAutoVariant, int, error) {
rows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery, rows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery,
"", "", "", "", "", "", "", "", "", "0", "", "", "", "", "", "", "", "", "", "", "0", "",
@@ -659,13 +962,19 @@ ORDER BY d.id, r.priority DESC, r.id
} }
out := make([]productSeriesAutoRule, 0, len(byID)) out := make([]productSeriesAutoRule, 0, len(byID))
for _, r := range byID { for _, r := range byID {
if len(r.Ratio) == 0 { // Always augment ratio map from the dfgrp.title tokens.
for _, part := range strings.Split(r.Title, "-") { // mk_product_series_rule can be partially populated (or manually edited), which would allow
size := normalizeProductSeriesSize(part) // a "subset" rule (e.g. only M+XL) to match stock and incorrectly assign a series like
if size != "" { // "XS-S-M-L-XL-2XL". Title tokens are the canonical definition of a series in this setup.
r.Ratio[size] = 1 for _, part := range strings.Split(r.Title, "-") {
} size := normalizeProductSeriesSize(part)
if size == "" {
continue
} }
if _, ok := r.Ratio[size]; ok {
continue
}
r.Ratio[size] = 1
} }
if len(r.Ratio) > 0 { if len(r.Ratio) > 0 {
out = append(out, *r) out = append(out, *r)
@@ -684,6 +993,11 @@ ORDER BY d.id, r.priority DESC, r.id
} }
func productSeriesSelectRules(v productSeriesAutoVariant, rules []productSeriesAutoRule) []productSeriesAutoRule { func productSeriesSelectRules(v productSeriesAutoVariant, rules []productSeriesAutoRule) []productSeriesAutoRule {
selectionMode := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_SERIES_SELECTION_MODE")))
if selectionMode == "" {
selectionMode = "dfs"
}
candidates := make([]productSeriesAutoRule, 0, len(rules)) candidates := make([]productSeriesAutoRule, 0, len(rules))
for _, rule := range rules { for _, rule := range rules {
if !productSeriesRuleFitsGroup(v.GroupKey, rule) { if !productSeriesRuleFitsGroup(v.GroupKey, rule) {
@@ -696,6 +1010,58 @@ func productSeriesSelectRules(v productSeriesAutoVariant, rules []productSeriesA
if len(candidates) == 0 { if len(candidates) == 0 {
return nil return nil
} }
if selectionMode == "greedy" {
// Greedy: start from the widest series (most sizes), then narrower, until nothing fits.
// This matches the operational expectation: cover as much of the stock-size space as possible
// using broad series first, then progressively narrower series; fallback=1 only if nothing matches.
sort.Slice(candidates, func(i, j int) bool {
li := len(candidates[i].Ratio)
lj := len(candidates[j].Ratio)
if li != lj {
return li > lj
}
if candidates[i].Priority != candidates[j].Priority {
return candidates[i].Priority > candidates[j].Priority
}
// Prefer numeric codes ascending when comparable, otherwise lexicographic.
ai, ei := strconv.Atoi(strings.TrimSpace(candidates[i].Code))
aj, ej := strconv.Atoi(strings.TrimSpace(candidates[j].Code))
if ei == nil && ej == nil && ai != aj {
return ai < aj
}
return candidates[i].Code < candidates[j].Code
})
stock := copyProductSeriesStock(v.SizeQty)
picked := make([]productSeriesAutoRule, 0, 8)
used := make([]bool, len(candidates))
for {
pickedOne := false
for i := range candidates {
if used[i] {
continue
}
rule := candidates[i]
if productSeriesCanConsume(stock, rule) {
used[i] = true
picked = append(picked, rule)
productSeriesConsume(stock, rule, -1)
pickedOne = true
break // restart from widest again
}
}
if !pickedOne {
break
}
}
if len(picked) == 0 {
return nil
}
sort.Slice(picked, func(i, j int) bool { return picked[i].Code < picked[j].Code })
return picked
}
if len(candidates) > 24 { if len(candidates) > 24 {
candidates = candidates[:24] candidates = candidates[:24]
} }
@@ -786,12 +1152,33 @@ func productSeriesResolvePGVariant(ctx context.Context, pg *sql.DB, productCode,
} }
return 0, 0, sql.NullInt64{}, false, err return 0, 0, sql.NullInt64{}, false, err
} }
dim1ID, ok, err := productSeriesResolveDimTokenID(ctx, pg, "dimval1", colorCode, mmitemID) // Authoritative dim1 resolver: for this installation, "color_code" tokens correspond to dfgrp.code on
if err != nil { // the dim values referenced by mmitem_dim.val1. Do NOT rely on mk_dim_token_map for dimval1 because
return 0, 0, sql.NullInt64{}, false, err // it can be polluted and conflate tokens.
} var dim1ID int64
if !ok || dim1ID <= 0 { {
return 0, 0, sql.NullInt64{}, false, nil tok := strings.ToUpper(strings.TrimSpace(colorCode))
if tok == "" || tok == "0" {
return 0, 0, sql.NullInt64{}, false, nil
}
if err := pg.QueryRowContext(ctx, `
SELECT md.val1
FROM mmitem_dim md
JOIN dfgrp d ON d.id = md.val1
WHERE md.mmitem_id = $1
AND md.is_active = TRUE
AND UPPER(BTRIM(d.code)) = $2
GROUP BY md.val1
LIMIT 1
`, mmitemID, tok).Scan(&dim1ID); err != nil {
if err == sql.ErrNoRows {
return 0, 0, sql.NullInt64{}, false, nil
}
return 0, 0, sql.NullInt64{}, false, err
}
if dim1ID <= 0 {
return 0, 0, sql.NullInt64{}, false, nil
}
} }
var dim3ID sql.NullInt64 var dim3ID sql.NullInt64
if strings.TrimSpace(dim3Code) != "" { if strings.TrimSpace(dim3Code) != "" {
@@ -804,6 +1191,28 @@ func productSeriesResolvePGVariant(ctx context.Context, pg *sql.DB, productCode,
} }
dim3ID = sql.NullInt64{Int64: id, Valid: true} dim3ID = sql.NullInt64{Int64: id, Valid: true}
} }
// Only treat variants as ready if they exist in authoritative item-dim combos (mmitem_dim).
// This prevents writing series assignments for stock variants that don't exist on the PG side.
dim3Key := int64(0)
if dim3ID.Valid && dim3ID.Int64 > 1000 {
dim3Key = dim3ID.Int64
}
var exists int
if err := pg.QueryRowContext(ctx, `
SELECT 1
FROM mmitem_dim
WHERE mmitem_id=$1
AND is_active=TRUE
AND val1=$2
AND COALESCE(CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END, 0) = $3
LIMIT 1
`, mmitemID, dim1ID, dim3Key).Scan(&exists); err != nil {
if err == sql.ErrNoRows {
return 0, 0, sql.NullInt64{}, false, nil
}
return 0, 0, sql.NullInt64{}, false, err
}
return mmitemID, dim1ID, dim3ID, true, nil return mmitemID, dim1ID, dim3ID, true, nil
} }
@@ -813,29 +1222,39 @@ func productSeriesResolveDimTokenID(ctx context.Context, pg *sql.DB, column stri
return 0, false, nil return 0, false, nil
} }
// dimval3 tokens like "001" can map to different dim ids per product in this installation.
// Prefer per-mmitem inference from dfblob (src_id filter) to avoid global mk_dim_token_map mismatches.
if column == "dimval3" && mmitemID > 0 {
if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok {
return inferred, true, nil
}
}
var id int64 var id int64
err := pg.QueryRowContext(ctx, `SELECT dim_id FROM mk_dim_token_map WHERE dim_column=$1 AND token=$2`, column, tok).Scan(&id) err := pg.QueryRowContext(ctx, `SELECT dim_id FROM mk_dim_token_map WHERE dim_column=$1 AND token=$2`, column, tok).Scan(&id)
if err == nil { if err == nil {
// mk_dim_token_map can become polluted by heuristic inference/persistence (especially for dimval1).
// If we can cross-check against item-scoped images, prefer the item-specific inference to avoid
// conflating different tokens onto the same dim_id.
if column == "dimval1" && mmitemID > 0 {
if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok && inferred > 0 && inferred != id {
log.Printf("[DimTokenConflict] column=dimval1 token=%s mmitem_id=%d mapped=%d inferred=%d", tok, mmitemID, id, inferred)
return inferred, true, nil
}
}
return id, id > 0, nil return id, id > 0, nil
} }
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
return 0, false, err return 0, false, err
} }
// Fallback: infer from dfblob filenames. For dimval3 do not persist globally. // Fallback: infer from dfblob filenames.
// Prefer item-scoped inference when we have mmitem_id. We intentionally avoid persisting these
// inferred rows into mk_dim_token_map to prevent global pollution.
if mmitemID > 0 { if mmitemID > 0 {
if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok { if inferred, ok := productSeriesInferDimIDFromImages(pg, mmitemID, column, tok); ok && inferred > 0 {
return inferred, true, nil return inferred, true, nil
} }
} }
// As a last resort, do a global inference. This is intentionally disabled for dimval1 because it is
// too easy to mis-infer and conflate tokens across the whole catalog.
if column == "dimval1" {
return 0, false, nil
}
v := productSeriesResolveDimvalFromFileNameToken(pg, column, tok, 0) v := productSeriesResolveDimvalFromFileNameToken(pg, column, tok, 0)
if v == "" { if v == "" {
return 0, false, nil return 0, false, nil
@@ -844,15 +1263,6 @@ func productSeriesResolveDimTokenID(ctx context.Context, pg *sql.DB, column stri
if perr != nil || parsed <= 0 { if perr != nil || parsed <= 0 {
return 0, false, nil return 0, false, nil
} }
if column == "dimval1" {
// Persist only for dimval1 where tokens are globally stable.
_, _ = pg.ExecContext(ctx, `
INSERT INTO mk_dim_token_map (dim_column, token, dim_id, updated_at)
VALUES ($1,$2,$3,now())
ON CONFLICT (dim_column, token)
DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
`, column, tok, parsed)
}
return parsed, true, nil return parsed, true, nil
} }
@@ -1134,7 +1544,26 @@ func productSeriesAutoKey(productCode, colorCode, dim3Code string) string {
} }
func normalizeProductSeriesSize(v string) string { func normalizeProductSeriesSize(v string) string {
return strings.ToUpper(strings.TrimSpace(v)) s := strings.ToUpper(strings.TrimSpace(v))
// Some installations store series size tokens with a "seriesCode/size" prefix in title/rules
// (e.g. "2/48-50-52"). Treat "2/48" as size "48".
if i := strings.IndexByte(s, '/'); i > 0 {
prefix := strings.TrimSpace(s[:i])
suffix := strings.TrimSpace(s[i+1:])
if suffix != "" {
allDigits := true
for _, r := range prefix {
if r < '0' || r > '9' {
allDigits = false
break
}
}
if allDigits {
s = suffix
}
}
}
return s
} }
func copyProductSeriesStock(in map[string]float64) map[string]float64 { func copyProductSeriesStock(in map[string]float64) map[string]float64 {
@@ -1152,15 +1581,13 @@ func nullableInt64ForAuto(v sql.NullInt64) any {
return v.Int64 return v.Int64
} }
func nextProductSeriesWeeklyRun(now time.Time, hh int, mm int) time.Time { func nextProductSeriesDailyRun(now time.Time, hh int, mm int) time.Time {
loc := now.Location() loc := now.Location()
base := time.Date(now.Year(), now.Month(), now.Day(), hh, mm, 0, 0, loc) base := time.Date(now.Year(), now.Month(), now.Day(), hh, mm, 0, 0, loc)
daysUntilMon := (int(time.Monday) - int(now.Weekday()) + 7) % 7 if base.After(now) {
candidate := base.AddDate(0, 0, daysUntilMon) return base
if !candidate.After(now) {
candidate = candidate.AddDate(0, 0, 7)
} }
return candidate return base.AddDate(0, 0, 1)
} }
func envBool(key string, fallback bool) bool { func envBool(key string, fallback bool) bool {
@@ -1183,6 +1610,27 @@ func envIntRange(key string, fallback int, min int, max int) int {
return n return n
} }
func envFloat64(key string, fallback float64) float64 {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
v, err := strconv.ParseFloat(raw, 64)
if err != nil {
return fallback
}
return v
}
func productSeriesHasAnySizeAtLeast(sizeQty map[string]float64, min float64) bool {
for _, q := range sizeQty {
if q >= min {
return true
}
}
return false
}
func envHHMM(key string, fallbackHH int, fallbackMM int) (int, int) { func envHHMM(key string, fallbackHH int, fallbackMM int) (int, int) {
raw := strings.TrimSpace(os.Getenv(key)) raw := strings.TrimSpace(os.Getenv(key))
if raw == "" { if raw == "" {
+2 -2
View File
@@ -41,8 +41,8 @@ func GetAccounts(ctx context.Context) ([]models.Account, error) {
c.CurrAccCode, c.CurrAccCode,
a.AttributeTypeCode, a.AttributeTypeCode,
a.AttributeCode a.AttributeCode
FROM cdCurrAcc c WITH (NOLOCK) FROM cdCurrAcc c
LEFT JOIN prCurrAccAttribute a WITH (NOLOCK) LEFT JOIN prCurrAccAttribute a
ON a.CurrAccTypeCode = c.CurrAccTypeCode ON a.CurrAccTypeCode = c.CurrAccTypeCode
AND a.CurrAccCode = c.CurrAccCode AND a.CurrAccCode = c.CurrAccCode
WHERE c.CurrAccTypeCode = 1 WHERE c.CurrAccTypeCode = 1
+2 -2
View File
@@ -32,8 +32,8 @@ SELECT DISTINCT
LTRIM(RTRIM(a.AttributeCode)) AS BrandCode, LTRIM(RTRIM(a.AttributeCode)) AS BrandCode,
COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) AS BrandName, COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) AS BrandName,
ISNULL(a.IsBlocked, 0) AS IsBlocked ISNULL(a.IsBlocked, 0) AS IsBlocked
FROM dbo.cdItemAttribute a WITH(NOLOCK) FROM dbo.cdItemAttribute a
LEFT JOIN dbo.cdItemAttributeDesc d WITH(NOLOCK) LEFT JOIN dbo.cdItemAttributeDesc d
ON d.ItemTypeCode = a.ItemTypeCode ON d.ItemTypeCode = a.ItemTypeCode
AND d.AttributeTypeCode = a.AttributeTypeCode AND d.AttributeTypeCode = a.AttributeTypeCode
AND d.AttributeCode = a.AttributeCode AND d.AttributeCode = a.AttributeCode
+8 -8
View File
@@ -237,7 +237,7 @@ WITH BaseCari AS
PARTITION BY LEFT(CB.CurrAccCode, 8) PARTITION BY LEFT(CB.CurrAccCode, 8)
ORDER BY CB.CurrAccCode ORDER BY CB.CurrAccCode
) )
FROM cdCurrAcc CB WITH (NOLOCK) FROM cdCurrAcc CB
WHERE CB.CurrAccTypeCode IN (1,3) WHERE CB.CurrAccTypeCode IN (1,3)
AND LEFT(CB.CurrAccCode, 8) IN (%s) AND LEFT(CB.CurrAccCode, 8) IN (%s)
), ),
@@ -264,7 +264,7 @@ SELECT
ILCE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt07Desc ELSE CDesc.CustomerAtt07Desc END, ''), ILCE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt07Desc ELSE CDesc.CustomerAtt07Desc END, ''),
Risk_Durumu = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt03Desc ELSE CDesc.CustomerAtt03Desc END, '') Risk_Durumu = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt03Desc ELSE CDesc.CustomerAtt03Desc END, '')
FROM FirstCari F FROM FirstCari F
LEFT JOIN cdCurrAccDesc cd WITH (NOLOCK) LEFT JOIN cdCurrAccDesc cd
ON cd.CurrAccTypeCode = F.CurrAccTypeCode ON cd.CurrAccTypeCode = F.CurrAccTypeCode
AND cd.CurrAccCode = F.CurrAccCode AND cd.CurrAccCode = F.CurrAccCode
AND cd.LangCode = 'TR' AND cd.LangCode = 'TR'
@@ -441,18 +441,18 @@ func loadCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]
ILCE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt07Desc ELSE cad.CustomerAtt07Desc END, ''), ILCE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt07Desc ELSE cad.CustomerAtt07Desc END, ''),
TC = ISNULL(c.IdentityNum, ''), TC = ISNULL(c.IdentityNum, ''),
Risk_Durumu = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt03Desc ELSE cad.CustomerAtt03Desc END, '') Risk_Durumu = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt03Desc ELSE cad.CustomerAtt03Desc END, '')
FROM cdCurrAcc c WITH(NOLOCK) FROM cdCurrAcc c
LEFT JOIN cdCurrAccDesc d WITH(NOLOCK) LEFT JOIN cdCurrAccDesc d
ON d.CurrAccTypeCode = c.CurrAccTypeCode ON d.CurrAccTypeCode = c.CurrAccTypeCode
AND d.CurrAccCode = c.CurrAccCode AND d.CurrAccCode = c.CurrAccCode
AND d.LangCode = 'TR' AND d.LangCode = 'TR'
LEFT JOIN VendorAttributes va WITH(NOLOCK) LEFT JOIN VendorAttributes va
ON va.CurrAccTypeCode = c.CurrAccTypeCode ON va.CurrAccTypeCode = c.CurrAccTypeCode
AND va.CurrAccCode = c.CurrAccCode AND va.CurrAccCode = c.CurrAccCode
LEFT JOIN VendorAttributeDescriptions('TR') vad LEFT JOIN VendorAttributeDescriptions('TR') vad
ON vad.CurrAccTypeCode = c.CurrAccTypeCode ON vad.CurrAccTypeCode = c.CurrAccTypeCode
AND vad.CurrAccCode = c.CurrAccCode AND vad.CurrAccCode = c.CurrAccCode
LEFT JOIN CustomerAttributes ca WITH(NOLOCK) LEFT JOIN CustomerAttributes ca
ON ca.CurrAccTypeCode = c.CurrAccTypeCode ON ca.CurrAccTypeCode = c.CurrAccTypeCode
AND ca.CurrAccCode = c.CurrAccCode AND ca.CurrAccCode = c.CurrAccCode
LEFT JOIN CustomerAttributeDescriptions('TR') cad LEFT JOIN CustomerAttributeDescriptions('TR') cad
@@ -530,7 +530,7 @@ func loadGLAccountMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT CurrAccTypeCode, CurrAccCode, CompanyCode, GLAccCode SELECT CurrAccTypeCode, CurrAccCode, CompanyCode, GLAccCode
FROM prCurrAccGLAccount WITH(NOLOCK) FROM prCurrAccGLAccount
WHERE PostAccTypeCode = 100 WHERE PostAccTypeCode = 100
AND CompanyCode IN (%s) AND CompanyCode IN (%s)
AND (%s) AND (%s)
@@ -561,7 +561,7 @@ func loadGLAccountMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string
} }
func loadCompanyMap(ctx context.Context) (map[int]string, error) { func loadCompanyMap(ctx context.Context) (map[int]string, error) {
rows, err := db.MssqlDB.QueryContext(ctx, `SELECT CompanyCode, CompanyName FROM cdCompany WITH(NOLOCK)`) rows, err := db.MssqlDB.QueryContext(ctx, `SELECT CompanyCode, CompanyName FROM cdCompany`)
if err != nil { if err != nil {
return nil, fmt.Errorf("company map query error: %w", err) return nil, fmt.Errorf("company map query error: %w", err)
} }
+5 -5
View File
@@ -25,7 +25,7 @@ func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) {
dbo.HG_Temizlik( dbo.HG_Temizlik(
ISNULL(( ISNULL((
SELECT AttributeDescription SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK) FROM cdCurrAccAttributeDesc
WHERE CurrAccTypeCode = 3 WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 8 AND AttributeTypeCode = 8
AND AttributeCode = f.CustomerAtt08 AND AttributeCode = f.CustomerAtt08
@@ -36,7 +36,7 @@ func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) {
dbo.HG_Temizlik( dbo.HG_Temizlik(
ISNULL(( ISNULL((
SELECT AttributeDescription SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK) FROM cdCurrAccAttributeDesc
WHERE CurrAccTypeCode = 3 WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 1 AND AttributeTypeCode = 1
AND AttributeCode = f.CustomerAtt01 AND AttributeCode = f.CustomerAtt01
@@ -47,7 +47,7 @@ func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) {
dbo.HG_Temizlik( dbo.HG_Temizlik(
ISNULL(( ISNULL((
SELECT AttributeDescription SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK) FROM cdCurrAccAttributeDesc
WHERE CurrAccTypeCode = 3 WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 2 AND AttributeTypeCode = 2
AND AttributeCode = f.CustomerAtt02 AND AttributeCode = f.CustomerAtt02
@@ -58,7 +58,7 @@ func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) {
dbo.HG_Temizlik( dbo.HG_Temizlik(
ISNULL(( ISNULL((
SELECT AttributeDescription SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK) FROM cdCurrAccAttributeDesc
WHERE CurrAccTypeCode = 3 WHERE CurrAccTypeCode = 3
AND AttributeTypeCode = 5 AND AttributeTypeCode = 5
AND AttributeCode = f.CustomerAtt05 AND AttributeCode = f.CustomerAtt05
@@ -69,7 +69,7 @@ func GetCustomerList(ctx context.Context) ([]models.CustomerList, error) {
ISNULL(c.CurrencyCode, ''), ISNULL(c.CurrencyCode, ''),
ISNULL(( ISNULL((
SELECT TOP 1 CAST(pa.PostalAddressID AS varchar(36)) SELECT TOP 1 CAST(pa.PostalAddressID AS varchar(36))
FROM prCurrAccPostalAddress pa WITH(NOLOCK) FROM prCurrAccPostalAddress pa
WHERE pa.CurrAccTypeCode = c.CurrAccTypeCode WHERE pa.CurrAccTypeCode = c.CurrAccTypeCode
AND pa.CurrAccCode = c.CurrAccCode AND pa.CurrAccCode = c.CurrAccCode
AND pa.AddressTypeCode = 2 AND pa.AddressTypeCode = 2
+1 -1
View File
@@ -154,7 +154,7 @@ LEFT JOIN (
LEFT JOIN ( LEFT JOIN (
SELECT DISTINCT OrderLineID SELECT DISTINCT OrderLineID
FROM BAGGI_V3.dbo.trInvoiceLine WITH (NOLOCK) FROM BAGGI_V3.dbo.trInvoiceLine
WHERE OrderLineID IS NOT NULL WHERE OrderLineID IS NOT NULL
) il ) il
ON il.OrderLineID = l.OrderLineID ON il.OrderLineID = l.OrderLineID
+11 -11
View File
@@ -26,8 +26,8 @@ SELECT
- (SUM(inv.ReserveQty1) + SUM(inv.DispOrderQty1) + SUM(inv.PickingQty1)), - (SUM(inv.ReserveQty1) + SUM(inv.DispOrderQty1) + SUM(inv.PickingQty1)),
cdUnitOfMeasure.RoundDigit cdUnitOfMeasure.RoundDigit
) AS Kullanilabilir_Envanter ) AS Kullanilabilir_Envanter
FROM cdItem WITH (NOLOCK) FROM cdItem
JOIN cdUnitOfMeasure WITH (NOLOCK) JOIN cdUnitOfMeasure
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
JOIN ( JOIN (
SELECT SELECT
@@ -40,19 +40,19 @@ JOIN (
FROM ( FROM (
SELECT 'PickingStates' AS SourceTable, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, SELECT 'PickingStates' AS SourceTable, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0 AS In_Qty1, 0 AS Out_Qty1 ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0 AS In_Qty1, 0 AS Out_Qty1
FROM PickingStates WITH (NOLOCK) FROM PickingStates
UNION ALL UNION ALL
SELECT 'ReserveStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, SELECT 'ReserveStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0 ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0
FROM ReserveStates WITH (NOLOCK) FROM ReserveStates
UNION ALL UNION ALL
SELECT 'DispOrderStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, SELECT 'DispOrderStates', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0 ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, Qty1, 0, 0
FROM DispOrderStates WITH (NOLOCK) FROM DispOrderStates
UNION ALL UNION ALL
SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, 0, SUM(In_Qty1), SUM(Out_Qty1) ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, 0, SUM(In_Qty1), SUM(Out_Qty1)
FROM trStock WITH (NOLOCK) FROM trStock
GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) AS src ) AS src
@@ -61,18 +61,18 @@ JOIN (
) AS inv ) AS inv
ON cdItem.ItemTypeCode = inv.ItemTypeCode ON cdItem.ItemTypeCode = inv.ItemTypeCode
AND cdItem.ItemCode = inv.ItemCode AND cdItem.ItemCode = inv.ItemCode
LEFT JOIN ProductAttributesFilter AS attr WITH (NOLOCK) LEFT JOIN ProductAttributesFilter AS attr
ON attr.ItemCode = inv.ItemCode ON attr.ItemCode = inv.ItemCode
LEFT JOIN bsItemTypeDesc WITH (NOLOCK) LEFT JOIN bsItemTypeDesc
ON bsItemTypeDesc.ItemTypeCode = inv.ItemTypeCode ON bsItemTypeDesc.ItemTypeCode = inv.ItemTypeCode
AND bsItemTypeDesc.LangCode = 'TR' AND bsItemTypeDesc.LangCode = 'TR'
LEFT JOIN cdWarehouseDesc AS wh WITH (NOLOCK) LEFT JOIN cdWarehouseDesc AS wh
ON wh.WarehouseCode = inv.WarehouseCode ON wh.WarehouseCode = inv.WarehouseCode
LEFT JOIN cdItemDesc AS descItem WITH (NOLOCK) LEFT JOIN cdItemDesc AS descItem
ON descItem.ItemTypeCode = inv.ItemTypeCode ON descItem.ItemTypeCode = inv.ItemTypeCode
AND descItem.ItemCode = inv.ItemCode AND descItem.ItemCode = inv.ItemCode
AND descItem.LangCode = 'TR' AND descItem.LangCode = 'TR'
LEFT JOIN cdColorDesc AS descColor WITH (NOLOCK) LEFT JOIN cdColorDesc AS descColor
ON descColor.ColorCode = inv.ColorCode ON descColor.ColorCode = inv.ColorCode
AND descColor.LangCode = 'TR' AND descColor.LangCode = 'TR'
WHERE WHERE
@@ -58,7 +58,7 @@ SELECT
T.avg_doc_price, T.avg_doc_price,
CONVERT(varchar(10), T.min_invoice_date, 23) AS min_invoice_date, CONVERT(varchar(10), T.min_invoice_date, 23) AS min_invoice_date,
CONVERT(varchar(10), T.max_invoice_date, 23) AS max_invoice_date CONVERT(varchar(10), T.max_invoice_date, 23) AS max_invoice_date
FROM dbo.cache_last10_avg_purchase_price T WITH (NOLOCK) FROM dbo.cache_last10_avg_purchase_price T
INNER JOIN C INNER JOIN C
ON C.ItemCode = T.ItemCode ON C.ItemCode = T.ItemCode
`, strings.Join(valParts, ",")) `, strings.Join(valParts, ","))
+3 -3
View File
@@ -220,7 +220,7 @@ SELECT
WHEN ISNULL(L.IsClosed, 0) = 1 WHEN ISNULL(L.IsClosed, 0) = 1
OR EXISTS ( OR EXISTS (
SELECT 1 SELECT 1
FROM BAGGI_V3.dbo.trInvoiceLine il WITH (NOLOCK) FROM BAGGI_V3.dbo.trInvoiceLine il
WHERE il.OrderLineID = L.OrderLineID WHERE il.OrderLineID = L.OrderLineID
) )
THEN CAST(1 AS bit) THEN CAST(1 AS bit)
@@ -237,10 +237,10 @@ SELECT
P.ProductAtt38Desc AS Fit1, P.ProductAtt38Desc AS Fit1,
P.ProductAtt39Desc AS Fit2 P.ProductAtt39Desc AS Fit2
FROM BAGGI_V3.dbo.trOrderLine AS L FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK) LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD
ON CD.OrderLineID = L.OrderLineID ON CD.OrderLineID = L.OrderLineID
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY') AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CL WITH (NOLOCK) LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CL
ON CL.OrderLineID = L.OrderLineID ON CL.OrderLineID = L.OrderLineID
AND CL.CurrencyCode = 'TRY' AND CL.CurrencyCode = 'TRY'
LEFT JOIN ProductFilterWithDescription('TR') AS P LEFT JOIN ProductFilterWithDescription('TR') AS P
+1 -1
View File
@@ -115,7 +115,7 @@ SELECT
L.VatCode, L.VatCode,
L.VatRate L.VatRate
FROM BAGGI_V3.dbo.trOrderLine AS L FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK) LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD
ON CD.OrderLineID = L.OrderLineID ON CD.OrderLineID = L.OrderLineID
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY') AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
LEFT JOIN ProductFilterWithDescription('TR') AS P LEFT JOIN ProductFilterWithDescription('TR') AS P
+3 -3
View File
@@ -492,7 +492,7 @@ func variantCacheKey(item, color, dim2 string) string {
func loadVariantDim1SetTx(tx *sql.Tx, item, color, dim2 string) (map[string]struct{}, error) { func loadVariantDim1SetTx(tx *sql.Tx, item, color, dim2 string) (map[string]struct{}, error) {
rows, err := tx.Query(` rows, err := tx.Query(`
SELECT ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') AS ItemDim1Code SELECT ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') AS ItemDim1Code
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK) FROM BAGGI_V3.dbo.prItemVariant V
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1 WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
AND ( AND (
( (
@@ -538,7 +538,7 @@ func loadVariantDim1SetTx(tx *sql.Tx, item, color, dim2 string) (map[string]stru
func loadVariantDim1SetDB(conn *sql.DB, item, color, dim2 string) (map[string]struct{}, error) { func loadVariantDim1SetDB(conn *sql.DB, item, color, dim2 string) (map[string]struct{}, error) {
rows, err := conn.Query(` rows, err := conn.Query(`
SELECT ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') AS ItemDim1Code SELECT ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') AS ItemDim1Code
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK) FROM BAGGI_V3.dbo.prItemVariant V
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1 WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
AND ( AND (
( (
@@ -1396,7 +1396,7 @@ WHERE OrderHeaderID=@p1
err := tx.QueryRow(` err := tx.QueryRow(`
SELECT CASE WHEN EXISTS ( SELECT CASE WHEN EXISTS (
SELECT 1 SELECT 1
FROM BAGGI_V3.dbo.trInvoiceLine WITH (NOLOCK) FROM BAGGI_V3.dbo.trInvoiceLine
WHERE OrderLineID=@p1 WHERE OrderLineID=@p1
) THEN 1 ELSE 0 END ) THEN 1 ELSE 0 END
`, lineID).Scan(&exists) `, lineID).Scan(&exists)
+4 -4
View File
@@ -29,7 +29,7 @@ SELECT
Inventory.ColorCode AS Renk_Kodu, Inventory.ColorCode AS Renk_Kodu,
ISNULL(( ISNULL((
SELECT TOP 1 ColorDescription SELECT TOP 1 ColorDescription
FROM cdColorDesc WITH(NOLOCK) FROM cdColorDesc
WHERE cdColorDesc.ColorCode = Inventory.ColorCode WHERE cdColorDesc.ColorCode = Inventory.ColorCode
AND cdColorDesc.LangCode = N'TR' AND cdColorDesc.LangCode = N'TR'
), '') AS Renk_Aciklamasi, ), '') AS Renk_Aciklamasi,
@@ -44,8 +44,8 @@ SELECT
cdUnitOfMeasure.RoundDigit cdUnitOfMeasure.RoundDigit
) AS Kullanilabilir_Envanter ) AS Kullanilabilir_Envanter
FROM cdItem WITH (NOLOCK) FROM cdItem
JOIN cdUnitOfMeasure WITH (NOLOCK) JOIN cdUnitOfMeasure
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
JOIN ( JOIN (
SELECT SELECT
@@ -74,7 +74,7 @@ JOIN (
SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, SELECT 'trStock', CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code,
0, SUM(In_Qty1), SUM(Out_Qty1) 0, SUM(In_Qty1), SUM(Out_Qty1)
FROM trStock WITH (NOLOCK) FROM trStock
GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, GROUP BY CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code ItemTypeCode, ItemCode, ColorCode, ItemDim1Code, ItemDim2Code, ItemDim3Code
) AS SourceData ) AS SourceData
+1 -1
View File
@@ -164,7 +164,7 @@ JOIN (
ON h2.OrderHeaderID = l.OrderHeaderID ON h2.OrderHeaderID = l.OrderHeaderID
LEFT JOIN ( LEFT JOIN (
SELECT DISTINCT OrderLineID SELECT DISTINCT OrderLineID
FROM BAGGI_V3.dbo.trInvoiceLine WITH (NOLOCK) FROM BAGGI_V3.dbo.trInvoiceLine
WHERE OrderLineID IS NOT NULL WHERE OrderLineID IS NOT NULL
) il ) il
ON il.OrderLineID = l.OrderLineID ON il.OrderLineID = l.OrderLineID
+23 -23
View File
@@ -28,7 +28,7 @@ SELECT
ISNULL(l.ColorCode,'') AS OldColor, ISNULL(l.ColorCode,'') AS OldColor,
ISNULL(( ISNULL((
SELECT TOP 1 LTRIM(RTRIM(cd.ColorDescription)) SELECT TOP 1 LTRIM(RTRIM(cd.ColorDescription))
FROM dbo.cdColorDesc cd WITH (NOLOCK) FROM dbo.cdColorDesc cd
WHERE cd.ColorCode = l.ColorCode WHERE cd.ColorCode = l.ColorCode
AND cd.LangCode = N'TR' AND cd.LangCode = N'TR'
), '') AS OldColorDescription, ), '') AS OldColorDescription,
@@ -68,7 +68,7 @@ SELECT
FROM dbo.trOrderLine l FROM dbo.trOrderLine l
JOIN dbo.trOrderHeader h JOIN dbo.trOrderHeader h
ON h.OrderHeaderID = l.OrderHeaderID ON h.OrderHeaderID = l.OrderHeaderID
LEFT JOIN dbo.trOrderLineCurrency c WITH (NOLOCK) LEFT JOIN dbo.trOrderLineCurrency c
ON c.OrderLineID = l.OrderLineID ON c.OrderLineID = l.OrderLineID
AND c.CurrencyCode = ISNULL(h.DocCurrencyCode,'TRY') AND c.CurrencyCode = ISNULL(h.DocCurrencyCode,'TRY')
@@ -206,7 +206,7 @@ SELECT
ISNULL(ItemDim1Code,'') AS ItemDim1Code, ISNULL(ItemDim1Code,'') AS ItemDim1Code,
ISNULL(ItemDim2Code,'') AS ItemDim2Code, ISNULL(ItemDim2Code,'') AS ItemDim2Code,
ISNULL(ItemDim3Code,'') AS ItemDim3Code ISNULL(ItemDim3Code,'') AS ItemDim3Code
FROM dbo.trOrderLine WITH(NOLOCK) FROM dbo.trOrderLine
WHERE OrderHeaderID = @p1 WHERE OrderHeaderID = @p1
`, orderHeaderID) `, orderHeaderID)
if err != nil { if err != nil {
@@ -519,7 +519,7 @@ SET
WHEN NULLIF(@p8,'') IS NULL THEN ItemTaxGrCode WHEN NULLIF(@p8,'') IS NULL THEN ItemTaxGrCode
WHEN EXISTS ( WHEN EXISTS (
SELECT 1 SELECT 1
FROM dbo.cdItemTaxGr g WITH(NOLOCK) FROM dbo.cdItemTaxGr g
WHERE LTRIM(RTRIM(g.ItemTaxGrCode)) = LTRIM(RTRIM(@p8)) WHERE LTRIM(RTRIM(g.ItemTaxGrCode)) = LTRIM(RTRIM(@p8))
) THEN @p8 ) THEN @p8
ELSE ItemTaxGrCode ELSE ItemTaxGrCode
@@ -1670,13 +1670,13 @@ func GetOrderProductionLookupOptions(mssql *sql.DB) (models.OrderProductionCdIte
{"ItemDimTypeCodes", `SELECT {"ItemDimTypeCodes", `SELECT
CAST(t.ItemDimTypeCode AS NVARCHAR(50)) AS Code, CAST(t.ItemDimTypeCode AS NVARCHAR(50)) AS Code,
ISNULL(d.ItemDimTypeDescription, CAST(t.ItemDimTypeCode AS NVARCHAR(50))) AS [Description] ISNULL(d.ItemDimTypeDescription, CAST(t.ItemDimTypeCode AS NVARCHAR(50))) AS [Description]
FROM dbo.bsItemDimType t WITH(NOLOCK) FROM dbo.bsItemDimType t
LEFT JOIN dbo.bsItemDimTypeDesc d WITH(NOLOCK) LEFT JOIN dbo.bsItemDimTypeDesc d
ON d.ItemDimTypeCode = t.ItemDimTypeCode ON d.ItemDimTypeCode = t.ItemDimTypeCode
AND d.LangCode = 'TR' AND d.LangCode = 'TR'
WHERE ISNULL(t.IsBlocked, 0) = 0 WHERE ISNULL(t.IsBlocked, 0) = 0
ORDER BY t.ItemDimTypeCode`, &out.ItemDimTypeCodes}, ORDER BY t.ItemDimTypeCode`, &out.ItemDimTypeCodes},
{"ProductTypeCodes", `SELECT DISTINCT CAST(ProductTypeCode AS NVARCHAR(50)) AS Code, CAST(ProductTypeCode AS NVARCHAR(50)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE ProductTypeCode IS NOT NULL ORDER BY Code`, &out.ProductTypeCodes}, {"ProductTypeCodes", `SELECT DISTINCT CAST(ProductTypeCode AS NVARCHAR(50)) AS Code, CAST(ProductTypeCode AS NVARCHAR(50)) AS [Description] FROM dbo.cdItem WHERE ProductTypeCode IS NOT NULL ORDER BY Code`, &out.ProductTypeCodes},
{"ProductHierarchyIDs", `SELECT {"ProductHierarchyIDs", `SELECT
CAST(h.ProductHierarchyID AS NVARCHAR(50)) AS Code, CAST(h.ProductHierarchyID AS NVARCHAR(50)) AS Code,
LTRIM(RTRIM( LTRIM(RTRIM(
@@ -1688,25 +1688,25 @@ func GetOrderProductionLookupOptions(mssql *sql.DB) (models.OrderProductionCdIte
END END
) )
)) AS [Description] )) AS [Description]
FROM dbo.dfProductHierarchy h WITH(NOLOCK) FROM dbo.dfProductHierarchy h
LEFT JOIN dbo.cdProductHierarchyLevelDesc d WITH(NOLOCK) LEFT JOIN dbo.cdProductHierarchyLevelDesc d
ON d.ProductHierarchyLevelCode = h.ProductHierarchyLevelCode01 ON d.ProductHierarchyLevelCode = h.ProductHierarchyLevelCode01
AND d.LangCode = 'TR' AND d.LangCode = 'TR'
ORDER BY h.ProductHierarchyID`, &out.ProductHierarchyIDs}, ORDER BY h.ProductHierarchyID`, &out.ProductHierarchyIDs},
{"UnitOfMeasureCode1List", `SELECT DISTINCT CAST(UnitOfMeasureCode1 AS NVARCHAR(50)) AS Code, CAST(UnitOfMeasureCode1 AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(UnitOfMeasureCode1 AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.UnitOfMeasureCode1List}, {"UnitOfMeasureCode1List", `SELECT DISTINCT CAST(UnitOfMeasureCode1 AS NVARCHAR(50)) AS Code, CAST(UnitOfMeasureCode1 AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(UnitOfMeasureCode1 AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.UnitOfMeasureCode1List},
{"ItemAccountGrCodes", `SELECT DISTINCT CAST(ItemAccountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemAccountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(ItemAccountGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemAccountGrCodes}, {"ItemAccountGrCodes", `SELECT DISTINCT CAST(ItemAccountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemAccountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(ItemAccountGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemAccountGrCodes},
{"ItemTaxGrCodes", `SELECT DISTINCT CAST(ItemTaxGrCode AS NVARCHAR(50)) AS Code, CAST(ItemTaxGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(ItemTaxGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemTaxGrCodes}, {"ItemTaxGrCodes", `SELECT DISTINCT CAST(ItemTaxGrCode AS NVARCHAR(50)) AS Code, CAST(ItemTaxGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(ItemTaxGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemTaxGrCodes},
{"ItemPaymentPlanGrCodes", `SELECT DISTINCT CAST(ItemPaymentPlanGrCode AS NVARCHAR(50)) AS Code, CAST(ItemPaymentPlanGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(ItemPaymentPlanGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemPaymentPlanGrCodes}, {"ItemPaymentPlanGrCodes", `SELECT DISTINCT CAST(ItemPaymentPlanGrCode AS NVARCHAR(50)) AS Code, CAST(ItemPaymentPlanGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(ItemPaymentPlanGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemPaymentPlanGrCodes},
{"ItemDiscountGrCodes", `SELECT DISTINCT CAST(ItemDiscountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemDiscountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(ItemDiscountGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemDiscountGrCodes}, {"ItemDiscountGrCodes", `SELECT DISTINCT CAST(ItemDiscountGrCode AS NVARCHAR(50)) AS Code, CAST(ItemDiscountGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(ItemDiscountGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemDiscountGrCodes},
{"ItemVendorGrCodes", `SELECT DISTINCT CAST(ItemVendorGrCode AS NVARCHAR(50)) AS Code, CAST(ItemVendorGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(ItemVendorGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemVendorGrCodes}, {"ItemVendorGrCodes", `SELECT DISTINCT CAST(ItemVendorGrCode AS NVARCHAR(50)) AS Code, CAST(ItemVendorGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(ItemVendorGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ItemVendorGrCodes},
{"PromotionGroupCodes", `SELECT DISTINCT CAST(PromotionGroupCode AS NVARCHAR(50)) AS Code, CAST(PromotionGroupCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(PromotionGroupCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.PromotionGroupCodes}, {"PromotionGroupCodes", `SELECT DISTINCT CAST(PromotionGroupCode AS NVARCHAR(50)) AS Code, CAST(PromotionGroupCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(PromotionGroupCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.PromotionGroupCodes},
{"ProductCollectionGrCodes", `SELECT DISTINCT CAST(ProductCollectionGrCode AS NVARCHAR(50)) AS Code, CAST(ProductCollectionGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(ProductCollectionGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ProductCollectionGrCodes}, {"ProductCollectionGrCodes", `SELECT DISTINCT CAST(ProductCollectionGrCode AS NVARCHAR(50)) AS Code, CAST(ProductCollectionGrCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(ProductCollectionGrCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.ProductCollectionGrCodes},
{"StorePriceLevelCodes", `SELECT DISTINCT CAST(StorePriceLevelCode AS NVARCHAR(50)) AS Code, CAST(StorePriceLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(StorePriceLevelCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.StorePriceLevelCodes}, {"StorePriceLevelCodes", `SELECT DISTINCT CAST(StorePriceLevelCode AS NVARCHAR(50)) AS Code, CAST(StorePriceLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(StorePriceLevelCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.StorePriceLevelCodes},
{"PerceptionOfFashionCodes", `SELECT DISTINCT CAST(PerceptionOfFashionCode AS NVARCHAR(50)) AS Code, CAST(PerceptionOfFashionCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(PerceptionOfFashionCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.PerceptionOfFashionCodes}, {"PerceptionOfFashionCodes", `SELECT DISTINCT CAST(PerceptionOfFashionCode AS NVARCHAR(50)) AS Code, CAST(PerceptionOfFashionCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(PerceptionOfFashionCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.PerceptionOfFashionCodes},
{"CommercialRoleCodes", `SELECT DISTINCT CAST(CommercialRoleCode AS NVARCHAR(50)) AS Code, CAST(CommercialRoleCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(CommercialRoleCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.CommercialRoleCodes}, {"CommercialRoleCodes", `SELECT DISTINCT CAST(CommercialRoleCode AS NVARCHAR(50)) AS Code, CAST(CommercialRoleCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(CommercialRoleCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.CommercialRoleCodes},
{"StoreCapacityLevelCodes", `SELECT DISTINCT CAST(StoreCapacityLevelCode AS NVARCHAR(50)) AS Code, CAST(StoreCapacityLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(StoreCapacityLevelCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.StoreCapacityLevelCodes}, {"StoreCapacityLevelCodes", `SELECT DISTINCT CAST(StoreCapacityLevelCode AS NVARCHAR(50)) AS Code, CAST(StoreCapacityLevelCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(StoreCapacityLevelCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.StoreCapacityLevelCodes},
{"CustomsTariffNumbers", `SELECT DISTINCT CAST(CustomsTariffNumberCode AS NVARCHAR(50)) AS Code, CAST(CustomsTariffNumberCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(CustomsTariffNumberCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.CustomsTariffNumbers}, {"CustomsTariffNumbers", `SELECT DISTINCT CAST(CustomsTariffNumberCode AS NVARCHAR(50)) AS Code, CAST(CustomsTariffNumberCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(CustomsTariffNumberCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.CustomsTariffNumbers},
{"CompanyCodes", `SELECT DISTINCT CAST(CompanyCode AS NVARCHAR(50)) AS Code, CAST(CompanyCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WITH(NOLOCK) WHERE NULLIF(LTRIM(RTRIM(CAST(CompanyCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.CompanyCodes}, {"CompanyCodes", `SELECT DISTINCT CAST(CompanyCode AS NVARCHAR(50)) AS Code, CAST(CompanyCode AS NVARCHAR(200)) AS [Description] FROM dbo.cdItem WHERE NULLIF(LTRIM(RTRIM(CAST(CompanyCode AS NVARCHAR(200)))), '') IS NOT NULL ORDER BY Code`, &out.CompanyCodes},
} }
for _, pair := range queryPairs { for _, pair := range queryPairs {
+1 -1
View File
@@ -34,7 +34,7 @@ func buildPiyasaExistsForCariCode(ctx context.Context, cariCodeExpr string) (str
return fmt.Sprintf(` return fmt.Sprintf(`
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM CustomerAttributesFilter PF WITH(NOLOCK) FROM CustomerAttributesFilter PF
WHERE (PF.CurrAccCode = %s OR LEFT(PF.CurrAccCode, 8) = LEFT(%s, 8)) WHERE (PF.CurrAccCode = %s OR LEFT(PF.CurrAccCode, 8) = LEFT(%s, 8))
AND %s AND %s
)`, cariCodeExpr, cariCodeExpr, inClause), nil )`, cariCodeExpr, cariCodeExpr, inClause), nil
File diff suppressed because it is too large Load Diff
+760
View File
@@ -0,0 +1,760 @@
package queries
func productPerformanceSalesSQL() string {
return `
;WITH SalesLines AS (
SELECT
I.InvoiceLineID,
SalesDate = CAST(I.InvoiceDate AS date),
I.InvoiceHeaderID,
I.InvoiceNumber,
ProductCode = UPPER(LTRIM(RTRIM(I.ItemCode))),
ColorCode = UPPER(LTRIM(RTRIM(ISNULL(I.ColorCode, '')))),
YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(I.ItemDim2Code, '')))),
ItemDescription = dbo.HG_Temizlik(ISNULL((
SELECT ItemDescription
FROM cdItemDesc WITH(NOLOCK)
WHERE cdItemDesc.ItemTypeCode = I.ItemTypeCode
AND cdItemDesc.ItemCode = I.ItemCode
AND cdItemDesc.LangCode = 'TR'
), SPACE(0))),
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt01, SPACE(0))),
UrunAltGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt02, SPACE(0))),
Kategori = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt44, SPACE(0))),
UrunIlkGrubu = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt42, SPACE(0))),
AskiliYan = dbo.HG_Temizlik(ISNULL(PAF.ProductAtt45, SPACE(0))),
ChannelCode = CASE
WHEN I.ProcessCode IN ('R') THEN dbo.HG_Temizlik(ISNULL((
SELECT OfficeDescription
FROM cdOfficeDesc WITH(NOLOCK)
WHERE cdOfficeDesc.OfficeCode = I.OfficeCode
AND cdOfficeDesc.LangCode = 'TR'
), SPACE(0)))
ELSE dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
AND AttributeTypeCode = 1
AND CAF.CustomerAtt01 = AttributeCode
AND cdCurrAccAttributeDesc.LangCode = 'TR'
), SPACE(0)))
END,
CustomerCountry = dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
AND AttributeTypeCode = 5
AND CAF.CustomerAtt05 = AttributeCode
AND cdCurrAccAttributeDesc.LangCode = 'TR'
), SPACE(0))),
CustomerSegment = dbo.HG_Temizlik(ISNULL((
SELECT AttributeDescription
FROM cdCurrAccAttributeDesc WITH(NOLOCK)
WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
AND AttributeTypeCode = 11
AND CAF.CustomerAtt11 = AttributeCode
AND cdCurrAccAttributeDesc.LangCode = 'TR'
), SPACE(0))),
CustomerCode = CASE
WHEN I.ProcessCode = 'R' THEN dbo.HG_Temizlik(ISNULL(I.StoreCode, ''))
ELSE dbo.HG_Temizlik(ISNULL(I.CurrAccCode, ''))
END,
CustomerDescription = CASE
WHEN I.ProcessCode = 'R' THEN dbo.HG_Temizlik(ISNULL((
SELECT CurrAccDescription
FROM cdCurrAccDesc WITH(NOLOCK)
WHERE cdCurrAccDesc.CurrAccTypeCode = 5
AND cdCurrAccDesc.CurrAccCode = I.StoreCode
AND cdCurrAccDesc.LangCode = 'TR'
), SPACE(0)))
ELSE dbo.HG_Temizlik(ISNULL((
SELECT CurrAccDescription
FROM cdCurrAccDesc WITH(NOLOCK)
WHERE cdCurrAccDesc.CurrAccTypeCode = I.CurrAccTypeCode
AND cdCurrAccDesc.CurrAccCode = I.CurrAccCode
AND cdCurrAccDesc.LangCode = 'TR'
), SPACE(0)))
END,
Qty1 = ISNULL(I.Qty1, 0),
TLAmount = CASE
WHEN Amount.DocCurrencyCode = 'TRY' THEN Amount.DocNetAmount
WHEN CurRate.Rate > 0 THEN Amount.DocNetAmount * CurRate.Rate
WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 THEN Amount.DocNetAmount * I.Loc_ExchangeRate
ELSE 0
END,
USDAmount = CASE
WHEN Amount.DocCurrencyCode = 'USD' THEN Amount.DocNetAmount
WHEN Amount.DocCurrencyCode = 'TRY' AND UsdRate.Rate > 0 THEN Amount.DocNetAmount / UsdRate.Rate
WHEN CurRate.Rate > 0 AND UsdRate.Rate > 0 THEN (Amount.DocNetAmount * CurRate.Rate) / UsdRate.Rate
WHEN ISNULL(I.Loc_ExchangeRate, 0) > 0 AND UsdRate.Rate > 0 THEN (Amount.DocNetAmount * I.Loc_ExchangeRate) / UsdRate.Rate
ELSE 0
END,
RN = ROW_NUMBER() OVER (
PARTITION BY I.InvoiceLineID
ORDER BY I.InvoiceDate DESC, I.InvoiceNumber
)
FROM AllInvoicesWithAttributes I WITH(NOLOCK)
LEFT OUTER JOIN CustomerAttributesFilter CAF WITH(NOLOCK)
ON CAF.CurrAccTypeCode = I.CurrAccTypeCode
AND CAF.CurrAccCode = I.CurrAccCode
LEFT OUTER JOIN ProductAttributesFilter PAF WITH(NOLOCK)
ON PAF.ItemCode = I.ItemCode
LEFT OUTER JOIN dbo.trInvoiceLineCurrency ILC WITH(NOLOCK)
ON ILC.InvoiceLineID = I.InvoiceLineID
AND UPPER(LTRIM(RTRIM(ILC.CurrencyCode))) = UPPER(LTRIM(RTRIM(ISNULL(NULLIF(LTRIM(RTRIM(I.Doc_CurrencyCode)), ''), 'TRY'))))
OUTER APPLY (
SELECT
DocCurrencyCode = UPPER(LTRIM(RTRIM(ISNULL(NULLIF(LTRIM(RTRIM(I.Doc_CurrencyCode)), ''), ISNULL(NULLIF(LTRIM(RTRIM(ILC.CurrencyCode)), ''), 'TRY'))))),
DocNetAmount = COALESCE(
NULLIF(ISNULL(ILC.NetAmount, 0), 0),
NULLIF(ISNULL(I.Doc_Amount, 0), 0),
NULLIF(ISNULL(I.Doc_Price, 0) * ISNULL(I.Qty1, 0), 0),
0
)
) Amount
OUTER APPLY (
SELECT TOP 1 Rate
FROM AllExchangeRates WITH(NOLOCK)
WHERE CurrencyCode = 'USD'
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(I.InvoiceDate AS date)
ORDER BY Date DESC
) UsdRate
OUTER APPLY (
SELECT TOP 1 Rate
FROM AllExchangeRates WITH(NOLOCK)
WHERE CurrencyCode = Amount.DocCurrencyCode
AND RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND Date <= CAST(I.InvoiceDate AS date)
ORDER BY Date DESC
) CurRate
WHERE I.InvoiceDate >= @p1
AND I.InvoiceDate < DATEADD(DAY, 1, @p2)
AND I.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(I.ItemCode))) = 13
AND (@p3 = '' OR UPPER(LTRIM(RTRIM(I.ItemCode))) LIKE @p3 + '%')
AND (
(I.CompanyCode = 1 AND I.ATAtt01 IN (1,2) AND I.ProcessCode IN ('WS','R'))
OR (I.CompanyCode = 4 AND I.ProcessCode = 'R')
)
AND EXISTS (
SELECT 1
FROM trDebitHeader DH WITH(NOLOCK)
INNER JOIN trDebitLine DL WITH(NOLOCK)
ON DL.DebitHeaderID = DH.DebitHeaderID
WHERE DH.ApplicationCode = 'Invoi'
AND DH.ApplicationID = I.InvoiceHeaderID
)
)
SELECT
SalesDate,
ProductCode,
ColorCode,
YakaKodu,
ItemDescription = MAX(ItemDescription),
Kategori = MAX(Kategori),
Seri = '',
YasGrubu = '',
AskiliYan = ISNULL(NULLIF(MAX(AskiliYan), '-'), ''),
UrunIlkGrubu = MAX(UrunIlkGrubu),
UrunAnaGrubu = MAX(UrunAnaGrubu),
UrunAltGrubu = MAX(UrunAltGrubu),
MarketKey = CONCAT(MAX(Kategori), '|', MAX(UrunIlkGrubu), '|', ISNULL(NULLIF(MAX(AskiliYan), '-'), ''), '|', MAX(UrunAnaGrubu), '|', MAX(UrunAltGrubu), '|', ChannelCode),
ChannelCode,
CustomerCountry = ISNULL(NULLIF(CustomerCountry, ''), '-'),
CustomerSegment = ISNULL(NULLIF(CustomerSegment, ''), '-'),
CustomerCode = ISNULL(NULLIF(CustomerCode, ''), '-'),
CustomerDescription = MAX(ISNULL(NULLIF(CustomerDescription, ''), '-')),
SalesQty = ISNULL(SUM(Qty1), 0),
SalesTL = ISNULL(SUM(TLAmount), 0),
SalesUSD = ISNULL(SUM(USDAmount), 0),
AvgPriceUSD = ISNULL(SUM(USDAmount) / NULLIF(SUM(Qty1), 0), 0),
InvoiceLineCount = COUNT(DISTINCT InvoiceLineID),
InvoiceCount = COUNT(DISTINCT InvoiceHeaderID),
CustomerCount = COUNT(DISTINCT CustomerCode),
LastRefNumber = MAX(InvoiceNumber)
FROM SalesLines
WHERE RN = 1
GROUP BY SalesDate, ProductCode, ColorCode, YakaKodu, ChannelCode, CustomerCountry, CustomerSegment, CustomerCode
`
}
func productPerformanceStockSQLUnused() string {
return `
;WITH ActiveWarehouses AS (
SELECT WarehouseCode
FROM (VALUES
('1-0-12'),('1.01.2014'),('1.02.2005'),('1.02.2004'),('1-0-43'),
('4.02.2001'),('1-0-55'),('1.01.2003'),('1-0-21'),('1-0-2'),
('1.01.2004'),('1-0-49'),('1-0-37'),('1-0-29'),('1-0-28'),
('1-0-10'),('100'),('1.02.2006'),('1-0-42'),('1.01.2002'),
('1-0-52')
) W(WarehouseCode)
),
Raw AS (
SELECT
ProductCode = UPPER(LTRIM(RTRIM(S.ItemCode))),
ColorCode = UPPER(LTRIM(RTRIM(ISNULL(S.ColorCode, '')))),
YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim2Code, '')))),
MovementDate = CAST(S.DocumentDate AS date),
ProcessCode = CASE
WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
ELSE LTRIM(RTRIM(ISNULL(S.InnerProcessCode, '')))
END,
InQty = SUM(S.In_Qty1),
OutQty = SUM(S.Out_Qty1),
NetQty = SUM(S.InventoryQty1)
FROM StockWithCost S WITH(NOLOCK)
INNER JOIN ActiveWarehouses W
ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
WHERE S.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
AND (@p3 = '' OR UPPER(LTRIM(RTRIM(S.ItemCode))) LIKE @p3 + '%')
AND S.DocumentDate < DATEADD(DAY, 1, @p2)
GROUP BY
S.ItemCode,
S.ColorCode,
S.ItemDim2Code,
CAST(S.DocumentDate AS date),
CASE
WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
ELSE LTRIM(RTRIM(ISNULL(S.InnerProcessCode, '')))
END
),
Opening AS (
SELECT
StockDate = @p1,
ProductCode,
ColorCode,
YakaKodu,
NetQty = SUM(NetQty),
InQty = CAST(0 AS decimal(18,4)),
OutQty = CAST(0 AS decimal(18,4)),
KpiInQty = CAST(0 AS decimal(18,4)),
KpiOutQty = CAST(0 AS decimal(18,4)),
SalesMovementQty = CAST(0 AS decimal(18,4)),
ProductionInQty = CAST(0 AS decimal(18,4)),
PurchaseInQty = CAST(0 AS decimal(18,4)),
ConsumptionOutQty = CAST(0 AS decimal(18,4)),
CountDiffQty = CAST(0 AS decimal(18,4))
FROM Raw
WHERE MovementDate < @p1
GROUP BY ProductCode, ColorCode, YakaKodu
),
Daily AS (
SELECT
StockDate = MovementDate,
ProductCode,
ColorCode,
YakaKodu,
NetQty = SUM(NetQty),
InQty = SUM(InQty),
OutQty = SUM(OutQty),
KpiInQty = SUM(CASE WHEN ProcessCode IN ('OP','BP','CI') THEN InQty ELSE 0 END),
KpiOutQty = SUM(CASE WHEN ProcessCode IN ('R','WS','OC','CO') THEN OutQty ELSE 0 END),
SalesMovementQty = SUM(CASE WHEN ProcessCode IN ('R','WS') THEN OutQty ELSE 0 END),
ProductionInQty = SUM(CASE WHEN ProcessCode = 'OP' THEN InQty ELSE 0 END),
PurchaseInQty = SUM(CASE WHEN ProcessCode = 'BP' THEN InQty ELSE 0 END),
ConsumptionOutQty = SUM(CASE WHEN ProcessCode = 'OC' THEN OutQty ELSE 0 END),
CountDiffQty = SUM(CASE WHEN ProcessCode IN ('CO','CI') THEN NetQty ELSE 0 END)
FROM Raw
WHERE MovementDate BETWEEN @p1 AND @p2
GROUP BY MovementDate, ProductCode, ColorCode, YakaKodu
),
Series AS (
SELECT * FROM Opening
UNION ALL
SELECT * FROM Daily
),
Collapsed AS (
SELECT
StockDate,
ProductCode,
ColorCode,
YakaKodu,
NetQty = SUM(NetQty),
InQty = SUM(InQty),
OutQty = SUM(OutQty),
KpiInQty = SUM(KpiInQty),
KpiOutQty = SUM(KpiOutQty),
SalesMovementQty = SUM(SalesMovementQty),
ProductionInQty = SUM(ProductionInQty),
PurchaseInQty = SUM(PurchaseInQty),
ConsumptionOutQty = SUM(ConsumptionOutQty),
CountDiffQty = SUM(CountDiffQty)
FROM Series
GROUP BY StockDate, ProductCode, ColorCode, YakaKodu
),
Running AS (
SELECT
StockDate,
ProductCode,
ColorCode,
YakaKodu,
StockQty = SUM(NetQty) OVER (
PARTITION BY ProductCode, ColorCode, YakaKodu
ORDER BY StockDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
),
InQty,
OutQty,
KpiInQty,
KpiOutQty,
SalesMovementQty,
ProductionInQty,
PurchaseInQty,
ConsumptionOutQty,
CountDiffQty
FROM Collapsed
)
SELECT
StockDate,
ProductCode,
ColorCode,
YakaKodu,
StockQty,
InQty,
OutQty,
KpiInQty,
KpiOutQty,
SalesMovementQty,
ProductionInQty,
PurchaseInQty,
ConsumptionOutQty,
CountDiffQty
FROM Running
WHERE StockDate BETWEEN @p1 AND @p2
ORDER BY StockDate, ProductCode, ColorCode, YakaKodu
`
}
func productPerformancePriceSQL() string {
return `
;WITH ProductCodes AS (
SELECT DISTINCT ItemCode
FROM cdItem WITH(NOLOCK)
WHERE ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
UNION
SELECT DISTINCT ItemCode
FROM trPriceListLine WITH(NOLOCK)
WHERE ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
UNION
SELECT DISTINCT ItemCode
FROM prItemBasePrice WITH(NOLOCK)
WHERE ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND (@p1 = '' OR UPPER(LTRIM(RTRIM(ItemCode))) LIKE @p1 + '%')
),
LatestPrice AS (
SELECT
p.ItemCode,
DocCurrencyCode = LTRIM(RTRIM(p.DocCurrencyCode)),
p.Price,
rn = ROW_NUMBER() OVER (
PARTITION BY p.ItemCode, LTRIM(RTRIM(p.DocCurrencyCode))
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC
)
FROM trPriceListLine p WITH(NOLOCK)
INNER JOIN ProductCodes pc ON pc.ItemCode = p.ItemCode
WHERE p.ItemTypeCode = 1
AND ISNULL(p.IsDisabled, 0) = 0
AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY')
AND (
(LTRIM(RTRIM(p.DocCurrencyCode)) = 'USD' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-USD')
OR (LTRIM(RTRIM(p.DocCurrencyCode)) = 'TRY' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-TRY')
)
AND p.Price > 0
),
BasePrice AS (
SELECT
ItemCode,
BasePriceUsd = MAX(CASE WHEN DocCurrencyCode = 'USD' THEN Price END),
BasePriceTry = MAX(CASE WHEN DocCurrencyCode = 'TRY' THEN Price END)
FROM LatestPrice
WHERE rn = 1
GROUP BY ItemCode
),
Cost AS (
SELECT
b.ItemCode,
CostPriceUsd = CAST(b.Price AS decimal(18,2)),
LastPricingDate = CAST(b.PriceDate AS date),
rn = ROW_NUMBER() OVER (
PARTITION BY b.ItemCode
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
)
FROM prItemBasePrice b WITH(NOLOCK)
INNER JOIN ProductCodes pc ON pc.ItemCode = b.ItemCode
WHERE b.ItemTypeCode = 1
AND b.BasePriceCode = 1
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
)
SELECT
UPPER(LTRIM(RTRIM(pc.ItemCode))) AS ItemCode,
ItemDescription = dbo.HG_Temizlik(ISNULL((
SELECT ItemDescription
FROM cdItemDesc WITH(NOLOCK)
WHERE cdItemDesc.ItemTypeCode = 1
AND cdItemDesc.ItemCode = pc.ItemCode
AND cdItemDesc.LangCode = 'TR'
), SPACE(0))),
Kategori = dbo.HG_Temizlik(ISNULL(paf.ProductAtt44, SPACE(0))),
AskiliYan = dbo.HG_Temizlik(ISNULL(paf.ProductAtt45, SPACE(0))),
UrunIlkGrubu = dbo.HG_Temizlik(ISNULL(paf.ProductAtt42, SPACE(0))),
UrunAnaGrubu = dbo.HG_Temizlik(ISNULL(paf.ProductAtt01, SPACE(0))),
UrunAltGrubu = dbo.HG_Temizlik(ISNULL(paf.ProductAtt02, SPACE(0))),
CostPriceUsd = ISNULL(c.CostPriceUsd, 0),
BasePriceUsd = ISNULL(bp.BasePriceUsd, 0),
BasePriceTry = ISNULL(bp.BasePriceTry, 0),
c.LastPricingDate
FROM ProductCodes pc
LEFT JOIN ProductAttributesFilter paf WITH(NOLOCK)
ON paf.ItemCode = pc.ItemCode
LEFT JOIN BasePrice bp ON bp.ItemCode = pc.ItemCode
LEFT JOIN Cost c ON c.ItemCode = pc.ItemCode AND c.rn = 1
`
}
func productPerformanceKPISQL() string {
return `
WITH LatestStock AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock90Start AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1::date - INTERVAL '89 days'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock180Start AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1::date - INTERVAL '179 days'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock365Start AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= $1::date - INTERVAL '359 days'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
StockTotalStart AS (
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
product_code, color_code, yaka_kodu, stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date <= DATE '2022-01-01'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
SalesAgg AS (
SELECT
product_code,
color_code,
yaka_kodu,
market_key,
MAX(item_description) AS item_description,
MAX(kategori) AS kategori,
MAX(seri) AS seri,
MAX(yas_grubu) AS yas_grubu,
COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan,
MAX(urun_ilk_grubu) AS urun_ilk_grubu,
MAX(urun_ana_grubu) AS urun_ana_grubu,
MAX(urun_alt_grubu) AS urun_alt_grubu,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '29 days') AS sales_qty_30d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS sales_qty_90d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_qty_180d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_qty_365d,
SUM(sales_qty) FILTER (WHERE sales_date >= $1::date - INTERVAL '729 days') AS sales_qty_730d,
SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_qty_total,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '29 days') AS sales_usd_30d,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS sales_usd_90d,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS sales_usd_180d,
SUM(sales_usd) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS sales_usd_365d,
SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01') AS sales_usd_total,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_90d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_180d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days' AND COALESCE(sales_usd, 0) > 0) AS customer_count_365d,
COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd, 0) > 0) AS customer_count_total,
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '89 days') AS first_sale_date_90d,
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '179 days') AS first_sale_date_180d,
MIN(sales_date) FILTER (WHERE sales_date >= $1::date - INTERVAL '359 days') AS first_sale_date_365d,
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total,
MAX(sales_date) AS last_sale_date,
MAX(last_ref_number) AS last_ref_number
FROM mk_product_performance_sales_daily
WHERE sales_date >= DATE '2022-01-01'
AND sales_date <= $1::date
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
GROUP BY product_code, color_code, yaka_kodu, market_key
),
Scope AS (
SELECT
product_code, color_code, yaka_kodu, market_key,
item_description, kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu,
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total,
customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
first_sale_date_90d, first_sale_date_180d, first_sale_date_365d, first_sale_date_total,
last_sale_date, last_ref_number
FROM SalesAgg
UNION ALL
SELECT
ls.product_code,
ls.color_code,
ls.yaka_kodu,
'STOK' AS market_key,
'' AS item_description,
'' AS kategori,
'' AS seri,
'' AS yas_grubu,
'' AS askili_yan,
COALESCE(pd.urun_ilk_grubu, '') AS urun_ilk_grubu,
'' AS urun_ana_grubu,
'' AS urun_alt_grubu,
0 AS sales_qty_30d,
0 AS sales_qty_90d,
0 AS sales_qty_180d,
0 AS sales_qty_365d,
0 AS sales_qty_730d,
0 AS sales_qty_total,
0 AS sales_usd_30d,
0 AS sales_usd_90d,
0 AS sales_usd_180d,
0 AS sales_usd_365d,
0 AS sales_usd_total,
0 AS customer_count_90d,
0 AS customer_count_180d,
0 AS customer_count_365d,
0 AS customer_count_total,
NULL::date AS first_sale_date_90d,
NULL::date AS first_sale_date_180d,
NULL::date AS first_sale_date_365d,
NULL::date AS first_sale_date_total,
NULL::date AS last_sale_date,
'' AS last_ref_number
FROM LatestStock ls
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=ls.product_code
WHERE COALESCE(ls.stock_qty, 0) <> 0
AND upper(translate(btrim(COALESCE(pd.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
AND NOT EXISTS (
SELECT 1
FROM SalesAgg sa
WHERE sa.product_code = ls.product_code
AND sa.color_code = ls.color_code
AND sa.yaka_kodu = ls.yaka_kodu
)
),
PriceDim AS (
SELECT
product_code,
COALESCE(urun_ilk_grubu, '') AS urun_ilk_grubu,
COALESCE(urun_ana_grubu, '') AS urun_ana_grubu,
COALESCE(urun_alt_grubu, '') AS urun_alt_grubu,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd > base_price_usd THEN base_price_usd
ELSE cost_price_usd
END AS cost_price_usd,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd > base_price_usd THEN cost_price_usd
ELSE base_price_usd
END AS base_price_usd,
base_price_try
FROM mk_product_performance_price_dim
),
Base AS (
SELECT
s.*,
COALESCE(ls.stock_qty, 0) AS stock_qty,
COALESCE(pd.cost_price_usd, 0) AS cost_price_usd,
COALESCE(pd.base_price_usd, 0) AS base_price_usd,
COALESCE(pd.base_price_try, 0) AS base_price_try,
COALESCE(s.sales_qty_90d, 0) / 90.0 AS avg_daily_sales_90d,
COALESCE(s.sales_qty_180d, 0) / 180.0 AS avg_daily_sales_180d,
COALESCE(s.sales_qty_365d, 0) / 360.0 AS avg_daily_sales_365d,
COALESCE(s.sales_qty_total, 0) / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) AS avg_daily_sales_total,
(COALESCE(s90.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_90d,
(COALESCE(s180.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_180d,
(COALESCE(s365.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_365d,
(COALESCE(stotal.stock_qty, ls.stock_qty, 0) + COALESCE(ls.stock_qty, 0)) / 2.0 AS avg_stock_total,
CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0) END AS avg_price_usd_90d,
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0) END AS avg_price_usd_180d,
COALESCE(s.sales_usd_90d, 0) - (COALESCE(s.sales_qty_90d, 0) * COALESCE(pd.cost_price_usd, 0)) AS gross_profit_usd_90d,
COALESCE(s.sales_usd_180d, 0) - (COALESCE(s.sales_qty_180d, 0) * COALESCE(pd.cost_price_usd, 0)) AS gross_profit_usd_180d,
CASE WHEN COALESCE(s.sales_usd_90d, 0) = 0 THEN 0
ELSE (COALESCE(s.sales_usd_90d, 0) - (COALESCE(s.sales_qty_90d, 0) * COALESCE(pd.cost_price_usd, 0))) / NULLIF(s.sales_usd_90d, 0)
END AS gross_margin_90d,
CASE WHEN COALESCE(s.sales_usd_180d, 0) = 0 THEN 0
ELSE (COALESCE(s.sales_usd_180d, 0) - (COALESCE(s.sales_qty_180d, 0) * COALESCE(pd.cost_price_usd, 0))) / NULLIF(s.sales_usd_180d, 0)
END AS gross_margin_180d,
CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0)) - COALESCE(pd.cost_price_usd, 0) END AS unit_profit_cost_90d,
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0)) - COALESCE(pd.cost_price_usd, 0) END AS unit_profit_cost_180d,
CASE WHEN COALESCE(s.sales_qty_90d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_90d, 0) / NULLIF(s.sales_qty_90d, 0)) - COALESCE(pd.base_price_usd, 0) END AS unit_profit_base_90d,
CASE WHEN COALESCE(s.sales_qty_180d, 0) = 0 THEN 0 ELSE (COALESCE(s.sales_usd_180d, 0) / NULLIF(s.sales_qty_180d, 0)) - COALESCE(pd.base_price_usd, 0) END AS unit_profit_base_180d
FROM Scope s
LEFT JOIN LatestStock ls ON ls.product_code=s.product_code AND ls.color_code=s.color_code AND ls.yaka_kodu=s.yaka_kodu
LEFT JOIN PriceDim pd ON pd.product_code=s.product_code
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_90d, $1::date - INTERVAL '89 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s90 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_180d, $1::date - INTERVAL '179 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s180 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_365d, $1::date - INTERVAL '359 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s365 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code=s.product_code
AND x.color_code=s.color_code
AND x.yaka_kodu=s.yaka_kodu
AND x.stock_date <= COALESCE(s.first_sale_date_total, DATE '2022-01-01')
ORDER BY x.stock_date DESC
LIMIT 1
) stotal ON TRUE
WHERE upper(translate(btrim(COALESCE(NULLIF(s.urun_ilk_grubu,''), pd.urun_ilk_grubu, '')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
),
Spread AS (
SELECT
product_code,
color_code,
yaka_kodu,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_90d, 0) > 0) AS market_count_90d,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_180d, 0) > 0) AS market_count_180d,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_365d, 0) > 0) AS market_count_365d,
COUNT(DISTINCT NULLIF(NULLIF(market_key, 'STOK'), '')) FILTER (WHERE COALESCE(sales_usd_total, 0) > 0) AS market_count_total
FROM SalesAgg
GROUP BY product_code, color_code, yaka_kodu
),
Market AS (
SELECT
market_key,
AVG(NULLIF(sales_qty_90d, 0)) AS market_avg_sales_qty_90d,
AVG(NULLIF(sales_qty_180d, 0)) AS market_avg_sales_qty_180d,
AVG(NULLIF(sales_qty_365d, 0)) AS market_avg_sales_qty_365d,
AVG(NULLIF(sales_qty_total, 0)) AS market_avg_sales_qty_total,
AVG(NULLIF(avg_price_usd_90d, 0)) AS market_avg_price_usd_90d,
AVG(NULLIF(gross_margin_90d, 0)) AS market_avg_margin_90d
FROM Base
GROUP BY market_key
),
Scored AS (
SELECT
b.*,
COALESCE(sp.market_count_90d, 0) AS market_count_90d,
COALESCE(sp.market_count_180d, 0) AS market_count_180d,
COALESCE(sp.market_count_365d, 0) AS market_count_365d,
COALESCE(sp.market_count_total, 0) AS market_count_total,
CASE WHEN b.avg_daily_sales_90d = 0 THEN 0 ELSE b.avg_stock_90d / NULLIF(b.avg_daily_sales_90d, 0) END AS stock_days_90d,
CASE WHEN b.avg_daily_sales_180d = 0 THEN 0 ELSE b.avg_stock_180d / NULLIF(b.avg_daily_sales_180d, 0) END AS stock_days_180d,
CASE WHEN b.avg_daily_sales_365d = 0 THEN 0 ELSE b.avg_stock_365d / NULLIF(b.avg_daily_sales_365d, 0) END AS stock_days_365d,
CASE WHEN b.avg_daily_sales_total = 0 THEN 0 ELSE b.avg_stock_total / NULLIF(b.avg_daily_sales_total, 0) END AS stock_days_total,
CASE WHEN b.avg_stock_90d = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(b.avg_stock_90d, 0) * 4.0 END AS stock_turnover_90d,
CASE WHEN b.avg_stock_180d = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(b.avg_stock_180d, 0) * 2.0 END AS stock_turnover_180d,
CASE WHEN b.avg_stock_365d = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(b.avg_stock_365d, 0) END AS stock_turnover_365d,
CASE WHEN b.avg_stock_total = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(b.avg_stock_total, 0) * 360.0 / GREATEST(1, ($1::date - DATE '2022-01-01') + 1) END AS stock_turnover_total,
CASE WHEN COALESCE(m.market_avg_sales_qty_90d, 0) = 0 THEN 0 ELSE b.sales_qty_90d / NULLIF(m.market_avg_sales_qty_90d, 0) END AS sales_index_90d,
CASE WHEN COALESCE(m.market_avg_sales_qty_180d, 0) = 0 THEN 0 ELSE b.sales_qty_180d / NULLIF(m.market_avg_sales_qty_180d, 0) END AS sales_index_180d,
CASE WHEN COALESCE(m.market_avg_sales_qty_365d, 0) = 0 THEN 0 ELSE b.sales_qty_365d / NULLIF(m.market_avg_sales_qty_365d, 0) END AS sales_index_365d,
CASE WHEN COALESCE(m.market_avg_sales_qty_total, 0) = 0 THEN 0 ELSE b.sales_qty_total / NULLIF(m.market_avg_sales_qty_total, 0) END AS sales_index_total,
CASE WHEN COALESCE(m.market_avg_price_usd_90d, 0) = 0 THEN 0 ELSE b.avg_price_usd_90d / NULLIF(m.market_avg_price_usd_90d, 0) END AS price_index_90d,
CASE WHEN COALESCE(m.market_avg_margin_90d, 0) = 0 THEN 0 ELSE b.gross_margin_90d / NULLIF(m.market_avg_margin_90d, 0) END AS margin_index_90d
FROM Base b
LEFT JOIN Market m ON m.market_key=b.market_key
LEFT JOIN Spread sp ON sp.product_code=b.product_code AND sp.color_code=b.color_code AND sp.yaka_kodu=b.yaka_kodu
)
INSERT INTO mk_product_performance_kpi_daily (
kpi_date, product_code, color_code, yaka_kodu, item_description,
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total, avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total,
avg_stock_90d, avg_stock_180d, avg_stock_365d, avg_stock_total,
stock_days_90d, stock_days_180d, stock_days_365d, stock_days_total, stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_total,
avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d,
market_count_90d, market_count_180d, market_count_365d, market_count_total,
customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total, price_index_90d, margin_index_90d, performance_score,
performance_bucket, recommendation, last_sale_date, last_ref_number, updated_at
)
SELECT
$1::date,
product_code, color_code, yaka_kodu, item_description,
kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
COALESCE(sales_qty_30d, 0), COALESCE(sales_qty_90d, 0), COALESCE(sales_qty_180d, 0), COALESCE(sales_qty_365d, 0), COALESCE(sales_qty_730d, 0), COALESCE(sales_qty_total, 0),
COALESCE(sales_usd_30d, 0), COALESCE(sales_usd_90d, 0), COALESCE(sales_usd_180d, 0), COALESCE(sales_usd_365d, 0), COALESCE(sales_usd_total, 0), COALESCE(avg_daily_sales_90d, 0), COALESCE(avg_daily_sales_180d, 0), COALESCE(avg_daily_sales_365d, 0), COALESCE(avg_daily_sales_total, 0),
COALESCE(avg_stock_90d, 0), COALESCE(avg_stock_180d, 0), COALESCE(avg_stock_365d, 0), COALESCE(avg_stock_total, 0),
COALESCE(stock_days_90d, 0), COALESCE(stock_days_180d, 0), COALESCE(stock_days_365d, 0), COALESCE(stock_days_total, 0), COALESCE(stock_turnover_90d, 0), COALESCE(stock_turnover_180d, 0), COALESCE(stock_turnover_365d, 0), COALESCE(stock_turnover_total, 0),
COALESCE(avg_price_usd_90d, 0), COALESCE(avg_price_usd_180d, 0), COALESCE(cost_price_usd, 0), COALESCE(base_price_usd, 0), COALESCE(base_price_try, 0),
COALESCE(gross_profit_usd_90d, 0), COALESCE(gross_profit_usd_180d, 0), COALESCE(gross_margin_90d, 0), COALESCE(gross_margin_180d, 0),
COALESCE(unit_profit_cost_90d, 0), COALESCE(unit_profit_cost_180d, 0), COALESCE(unit_profit_base_90d, 0), COALESCE(unit_profit_base_180d, 0),
COALESCE(market_count_90d, 0), COALESCE(market_count_180d, 0), COALESCE(market_count_365d, 0), COALESCE(market_count_total, 0),
COALESCE(customer_count_90d, 0), COALESCE(customer_count_180d, 0), COALESCE(customer_count_365d, 0), COALESCE(customer_count_total, 0),
COALESCE(sales_index_90d, 0), COALESCE(sales_index_180d, 0), COALESCE(sales_index_365d, 0), COALESCE(sales_index_total, 0), COALESCE(price_index_90d, 0), COALESCE(margin_index_90d, 0),
CASE WHEN COALESCE(sales_usd_90d, 0) <= 0 THEN 1 ELSE
ROUND(
LEAST(30, GREATEST(COALESCE(gross_margin_90d, 0), 0) / 0.45 * 30)
+ LEAST(20, COALESCE(stock_turnover_90d, 0) / 4.0 * 20)
+ LEAST(20, COALESCE(sales_usd_90d, 0) / 25000.0 * 20)
+ LEAST(5, COALESCE(market_count_90d, 0) / 3.0 * 5)
+ LEAST(25, COALESCE(customer_count_90d, 0) / 8.0 * 25)
, 4)
END AS performance_score,
CASE
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.45 THEN 'YILDIZ_URUN'
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'STOKSUZ_TALEP'
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'STOK_RISKI'
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'FIYAT_BASKISI'
WHEN COALESCE(price_index_90d,0) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'FIYAT_FIRSATI'
ELSE 'TAKIP'
END AS performance_bucket,
CASE
WHEN COALESCE(sales_index_90d,0) >= 1.25 AND COALESCE(gross_margin_90d,0) >= 0.45 THEN 'Piyasa ustu satis ve iyi marj: stok ve fiyat gucu takip edilmeli.'
WHEN COALESCE(stock_qty,0) <= 0 AND COALESCE(sales_qty_90d,0) > 0 THEN 'Talep var, stok yok: uretim/satin alma planina alinmali.'
WHEN COALESCE(stock_days_90d,0) > 180 AND COALESCE(sales_index_90d,0) < 0.75 THEN 'Stok yuksek, satis piyasa alti: kampanya veya fiyat kontrolu gerekli.'
WHEN COALESCE(price_index_90d,0) > 1.10 AND COALESCE(sales_index_90d,0) < 0.80 THEN 'Fiyat piyasa ustunde ve satis zayif: fiyat revizyonu degerlendirilmeli.'
WHEN COALESCE(price_index_90d,0) < 0.90 AND COALESCE(sales_index_90d,0) > 1.00 THEN 'Satis guclu, fiyat piyasa alti: taban fiyat artisi degerlendirilebilir.'
ELSE 'Duzenli takip.'
END AS recommendation,
last_sale_date,
COALESCE(last_ref_number, ''),
now()
FROM Scored
`
}
+73 -73
View File
@@ -162,50 +162,50 @@ func GetAllProductPricingRows(ctx context.Context, chunkSize int, filters Produc
CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode); CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode);
SELECT SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode, s.ItemCode AS ItemCode,
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
INTO #stock_base INTO #stock_base
FROM trStock s WITH(NOLOCK) FROM trStock s
INNER JOIN #req_codes rc INNER JOIN #req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) ON rc.ProductCode = s.ItemCode
WHERE s.ItemTypeCode = 1 WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 AND LEN(s.ItemCode) = 13
GROUP BY LTRIM(RTRIM(s.ItemCode)); GROUP BY s.ItemCode;
CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode); CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode);
SELECT SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode, p.ItemCode AS ItemCode,
SUM(p.Qty1) AS PickingQty1 SUM(p.Qty1) AS PickingQty1
INTO #pick_base INTO #pick_base
FROM PickingStates p FROM PickingStates p
INNER JOIN #req_codes rc INNER JOIN #req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) ON rc.ProductCode = p.ItemCode
WHERE p.ItemTypeCode = 1 WHERE p.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 AND LEN(p.ItemCode) = 13
GROUP BY LTRIM(RTRIM(p.ItemCode)); GROUP BY p.ItemCode;
SELECT SELECT
LTRIM(RTRIM(r.ItemCode)) AS ItemCode, r.ItemCode AS ItemCode,
SUM(r.Qty1) AS ReserveQty1 SUM(r.Qty1) AS ReserveQty1
INTO #reserve_base INTO #reserve_base
FROM ReserveStates r FROM ReserveStates r
INNER JOIN #req_codes rc INNER JOIN #req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode)) ON rc.ProductCode = r.ItemCode
WHERE r.ItemTypeCode = 1 WHERE r.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 AND LEN(r.ItemCode) = 13
GROUP BY LTRIM(RTRIM(r.ItemCode)); GROUP BY r.ItemCode;
SELECT SELECT
LTRIM(RTRIM(d.ItemCode)) AS ItemCode, d.ItemCode AS ItemCode,
SUM(d.Qty1) AS DispOrderQty1 SUM(d.Qty1) AS DispOrderQty1
INTO #disp_base INTO #disp_base
FROM DispOrderStates d FROM DispOrderStates d
INNER JOIN #req_codes rc INNER JOIN #req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode)) ON rc.ProductCode = d.ItemCode
WHERE d.ItemTypeCode = 1 WHERE d.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 AND LEN(d.ItemCode) = 13
GROUP BY LTRIM(RTRIM(d.ItemCode)); GROUP BY d.ItemCode;
SELECT SELECT
rc.ProductCode, rc.ProductCode,
@@ -315,16 +315,16 @@ func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricin
), ),
latest_pricelist_line AS ( latest_pricelist_line AS (
SELECT SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode, p.ItemCode AS ItemCode,
LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode, LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode,
CAST(p.Price AS DECIMAL(18, 2)) AS Price, CAST(p.Price AS DECIMAL(18, 2)) AS Price,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode)) PARTITION BY p.ItemCode, LTRIM(RTRIM(p.DocCurrencyCode))
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC
) AS rn ) AS rn
FROM dbo.trPriceListLine p WITH(NOLOCK) FROM dbo.trPriceListLine p
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) ON rc.ProductCode = p.ItemCode
WHERE p.ItemTypeCode = 1 WHERE p.ItemTypeCode = 1
AND ISNULL(p.IsDisabled, 0) = 0 AND ISNULL(p.IsDisabled, 0) = 0
AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY') AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY')
@@ -346,29 +346,29 @@ func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricin
), ),
latest_base_price AS ( latest_base_price AS (
SELECT SELECT
LTRIM(RTRIM(b.ItemCode)) AS ItemCode, b.ItemCode AS ItemCode,
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(b.ItemCode)) PARTITION BY b.ItemCode
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
) AS rn ) AS rn
FROM prItemBasePrice b FROM prItemBasePrice b
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode)) ON rc.ProductCode = b.ItemCode
WHERE b.ItemTypeCode = 1 WHERE b.ItemTypeCode = 1
AND b.BasePriceCode = 1 AND b.BasePriceCode = 1
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
), ),
stock_entry_dates AS ( stock_entry_dates AS (
SELECT SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode, s.ItemCode AS ItemCode,
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
FROM trStock s WITH(NOLOCK) FROM trStock s
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) ON rc.ProductCode = s.ItemCode
WHERE s.ItemTypeCode = 1 WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 AND LEN(s.ItemCode) = 13
AND s.In_Qty1 > 0 AND s.In_Qty1 > 0
AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP' AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP'
AND LTRIM(RTRIM(s.WarehouseCode)) IN ( AND LTRIM(RTRIM(s.WarehouseCode)) IN (
@@ -376,7 +376,7 @@ func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricin
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36'
) )
GROUP BY LTRIM(RTRIM(s.ItemCode)) GROUP BY s.ItemCode
) )
SELECT SELECT
rc.ProductCode, rc.ProductCode,
@@ -431,7 +431,7 @@ func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricin
SELECT SELECT
LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu, LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu,
CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate
FROM dbo.spUrtOnMLMas m WITH(NOLOCK) FROM dbo.spUrtOnMLMas m
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu)) ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu))
GROUP BY LTRIM(RTRIM(m.UrunKodu)); GROUP BY LTRIM(RTRIM(m.UrunKodu));
@@ -878,15 +878,15 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode); CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode);
SELECT SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode, s.ItemCode AS ItemCode,
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
INTO #stock_base INTO #stock_base
FROM trStock s WITH(NOLOCK) FROM trStock s
INNER JOIN #req_codes rc INNER JOIN #req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) ON rc.ProductCode = s.ItemCode
WHERE s.ItemTypeCode = 1 WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 AND LEN(s.ItemCode) = 13
GROUP BY LTRIM(RTRIM(s.ItemCode)); GROUP BY s.ItemCode;
CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode); CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode);
@@ -927,7 +927,7 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
), ),
attr AS ( attr AS (
SELECT SELECT
LTRIM(RTRIM(a.ItemCode)) AS ProductCode, a.ItemCode AS ProductCode,
MAX(CASE WHEN a.AttributeTypeCode = 45 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS AskiliYan, MAX(CASE WHEN a.AttributeTypeCode = 45 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS AskiliYan,
MAX(CASE WHEN a.AttributeTypeCode = 44 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Kategori, MAX(CASE WHEN a.AttributeTypeCode = 44 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Kategori,
MAX(CASE WHEN a.AttributeTypeCode = 42 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS UrunIlkGrubuCode, MAX(CASE WHEN a.AttributeTypeCode = 42 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS UrunIlkGrubuCode,
@@ -938,17 +938,17 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
MAX(CASE WHEN a.AttributeTypeCode = 29 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Karisim, MAX(CASE WHEN a.AttributeTypeCode = 29 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Karisim,
MAX(CASE WHEN a.AttributeTypeCode = 10 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Marka, MAX(CASE WHEN a.AttributeTypeCode = 10 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Marka,
MAX(CASE WHEN a.AttributeTypeCode = 10 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS BrandCode MAX(CASE WHEN a.AttributeTypeCode = 10 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS BrandCode
FROM dbo.prItemAttribute a WITH(NOLOCK) FROM dbo.prItemAttribute a
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(a.ItemCode)) ON rc.ProductCode = a.ItemCode
LEFT JOIN dbo.cdItemAttributeDesc d WITH(NOLOCK) LEFT JOIN dbo.cdItemAttributeDesc d
ON d.ItemTypeCode = a.ItemTypeCode ON d.ItemTypeCode = a.ItemTypeCode
AND d.AttributeTypeCode = a.AttributeTypeCode AND d.AttributeTypeCode = a.AttributeTypeCode
AND d.AttributeCode = a.AttributeCode AND d.AttributeCode = a.AttributeCode
AND d.LangCode = 'TR' AND d.LangCode = 'TR'
WHERE a.ItemTypeCode = 1 WHERE a.ItemTypeCode = 1
AND a.AttributeTypeCode IN (1,2,10,29,41,42,44,45) AND a.AttributeTypeCode IN (1,2,10,29,41,42,44,45)
GROUP BY LTRIM(RTRIM(a.ItemCode)) GROUP BY a.ItemCode
) )
SELECT SELECT
rc.ProductCode, rc.ProductCode,
@@ -963,13 +963,13 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
COALESCE(attr.Marka, '') AS Marka, COALESCE(attr.Marka, '') AS Marka,
COALESCE(attr.BrandCode, '') AS BrandCode COALESCE(attr.BrandCode, '') AS BrandCode
FROM req_codes rc FROM req_codes rc
INNER JOIN dbo.cdItem ci WITH(NOLOCK) INNER JOIN dbo.cdItem ci
ON ci.ItemTypeCode = 1 ON ci.ItemTypeCode = 1
AND LTRIM(RTRIM(ci.ItemCode)) = rc.ProductCode AND ci.ItemCode = rc.ProductCode
LEFT JOIN attr LEFT JOIN attr
ON attr.ProductCode = rc.ProductCode ON attr.ProductCode = rc.ProductCode
WHERE ISNULL(ci.IsBlocked, 0) = 0 WHERE ISNULL(ci.IsBlocked, 0) = 0
AND LEN(LTRIM(RTRIM(ci.ItemCode))) = 13 AND LEN(ci.ItemCode) = 13
AND COALESCE(attr.UrunIlkGrubuCode, '') IN ('SERI', 'AKSESUAR') AND COALESCE(attr.UrunIlkGrubuCode, '') IN ('SERI', 'AKSESUAR')
ORDER BY rc.ProductCode ASC ORDER BY rc.ProductCode ASC
OFFSET ` + strconv.Itoa(offset) + ` ROWS OFFSET ` + strconv.Itoa(offset) + ` ROWS
@@ -1109,16 +1109,16 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
-- Base prices from Nebim V3 price lists (trPriceListLine). -- Base prices from Nebim V3 price lists (trPriceListLine).
-- Pick the latest record per (ItemCode, Currency) using ValidDate/ValidTime, then LastUpdatedDate. -- Pick the latest record per (ItemCode, Currency) using ValidDate/ValidTime, then LastUpdatedDate.
SELECT SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode, p.ItemCode AS ItemCode,
LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode, LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode,
CAST(p.Price AS DECIMAL(18, 2)) AS Price, CAST(p.Price AS DECIMAL(18, 2)) AS Price,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode)) PARTITION BY p.ItemCode, LTRIM(RTRIM(p.DocCurrencyCode))
ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC
) AS rn ) AS rn
FROM dbo.trPriceListLine p WITH(NOLOCK) FROM dbo.trPriceListLine p
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) ON rc.ProductCode = p.ItemCode
WHERE p.ItemTypeCode = 1 WHERE p.ItemTypeCode = 1
AND ISNULL(p.IsDisabled, 0) = 0 AND ISNULL(p.IsDisabled, 0) = 0
AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY') AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY')
@@ -1140,29 +1140,29 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
), ),
latest_base_price AS ( latest_base_price AS (
SELECT SELECT
LTRIM(RTRIM(b.ItemCode)) AS ItemCode, b.ItemCode AS ItemCode,
CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice,
CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(b.ItemCode)) PARTITION BY b.ItemCode
ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC
) AS rn ) AS rn
FROM prItemBasePrice b FROM prItemBasePrice b
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode)) ON rc.ProductCode = b.ItemCode
WHERE b.ItemTypeCode = 1 WHERE b.ItemTypeCode = 1
AND b.BasePriceCode = 1 AND b.BasePriceCode = 1
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
), ),
stock_entry_dates AS ( stock_entry_dates AS (
SELECT SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode, s.ItemCode AS ItemCode,
CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate
FROM trStock s WITH(NOLOCK) FROM trStock s
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) ON rc.ProductCode = s.ItemCode
WHERE s.ItemTypeCode = 1 WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 AND LEN(s.ItemCode) = 13
AND s.In_Qty1 > 0 AND s.In_Qty1 > 0
AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP' AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP'
AND LTRIM(RTRIM(s.WarehouseCode)) IN ( AND LTRIM(RTRIM(s.WarehouseCode)) IN (
@@ -1170,51 +1170,51 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36'
) )
GROUP BY LTRIM(RTRIM(s.ItemCode)) GROUP BY s.ItemCode
), ),
stock_base AS ( stock_base AS (
SELECT SELECT
LTRIM(RTRIM(s.ItemCode)) AS ItemCode, s.ItemCode AS ItemCode,
SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1
FROM trStock s WITH(NOLOCK) FROM trStock s
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) ON rc.ProductCode = s.ItemCode
WHERE s.ItemTypeCode = 1 WHERE s.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 AND LEN(s.ItemCode) = 13
GROUP BY LTRIM(RTRIM(s.ItemCode)) GROUP BY s.ItemCode
), ),
pick_base AS ( pick_base AS (
SELECT SELECT
LTRIM(RTRIM(p.ItemCode)) AS ItemCode, p.ItemCode AS ItemCode,
SUM(p.Qty1) AS PickingQty1 SUM(p.Qty1) AS PickingQty1
FROM PickingStates p FROM PickingStates p
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) ON rc.ProductCode = p.ItemCode
WHERE p.ItemTypeCode = 1 WHERE p.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 AND LEN(p.ItemCode) = 13
GROUP BY LTRIM(RTRIM(p.ItemCode)) GROUP BY p.ItemCode
), ),
reserve_base AS ( reserve_base AS (
SELECT SELECT
LTRIM(RTRIM(r.ItemCode)) AS ItemCode, r.ItemCode AS ItemCode,
SUM(r.Qty1) AS ReserveQty1 SUM(r.Qty1) AS ReserveQty1
FROM ReserveStates r FROM ReserveStates r
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode)) ON rc.ProductCode = r.ItemCode
WHERE r.ItemTypeCode = 1 WHERE r.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 AND LEN(r.ItemCode) = 13
GROUP BY LTRIM(RTRIM(r.ItemCode)) GROUP BY r.ItemCode
), ),
disp_base AS ( disp_base AS (
SELECT SELECT
LTRIM(RTRIM(d.ItemCode)) AS ItemCode, d.ItemCode AS ItemCode,
SUM(d.Qty1) AS DispOrderQty1 SUM(d.Qty1) AS DispOrderQty1
FROM DispOrderStates d FROM DispOrderStates d
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode)) ON rc.ProductCode = d.ItemCode
WHERE d.ItemTypeCode = 1 WHERE d.ItemTypeCode = 1
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 AND LEN(d.ItemCode) = 13
GROUP BY LTRIM(RTRIM(d.ItemCode)) GROUP BY d.ItemCode
) )
SELECT SELECT
rc.ProductCode, rc.ProductCode,
@@ -1311,7 +1311,7 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
SELECT SELECT
LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu, LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu,
CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate
FROM dbo.spUrtOnMLMas m WITH(NOLOCK) FROM dbo.spUrtOnMLMas m
INNER JOIN req_codes rc INNER JOIN req_codes rc
ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu)) ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu))
GROUP BY LTRIM(RTRIM(m.UrunKodu)); GROUP BY LTRIM(RTRIM(m.UrunKodu));
+1 -1
View File
@@ -14,7 +14,7 @@ SELECT DISTINCT
LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode, LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode,
LTRIM(RTRIM(ISNULL(V.ItemDim1Code,''))) AS ItemDim1Code, LTRIM(RTRIM(ISNULL(V.ItemDim1Code,''))) AS ItemDim1Code,
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code
FROM prItemVariant V WITH(NOLOCK) FROM prItemVariant V
WHERE V.ItemTypeCode = 1 WHERE V.ItemTypeCode = 1
AND V.ItemCode = @ProductCode AND V.ItemCode = @ProductCode
AND LEN(V.ItemCode) = 13 AND LEN(V.ItemCode) = 13
+3 -3
View File
@@ -101,7 +101,7 @@ CREATE CLUSTERED INDEX IX_AttrFiltered_ProductCode ON #AttrFiltered(ProductCode)
( (
SELECT T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, SELECT T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code,
SUM(T.In_Qty1 - T.Out_Qty1) AS InventoryQty1, 0 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1 SUM(T.In_Qty1 - T.Out_Qty1) AS InventoryQty1, 0 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1
FROM trStock T WITH (NOLOCK) FROM trStock T
INNER JOIN #AttrFiltered AF ON AF.ProductCode = T.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = T.ItemCode
WHERE T.ItemTypeCode = 1 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 AND LEN(T.ItemCode) = 13
@@ -144,7 +144,7 @@ Avail AS
I.ItemDim2Code, I.ItemDim2Code,
CAST(ROUND(I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1, 2) AS DECIMAL(18,2)) AS Qty CAST(ROUND(I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1, 2) AS DECIMAL(18,2)) AS Qty
FROM Inv I FROM Inv I
JOIN cdItem CI WITH (NOLOCK) JOIN cdItem CI
ON CI.ItemTypeCode = 1 ON CI.ItemTypeCode = 1
AND CI.ItemCode = I.ItemCode AND CI.ItemCode = I.ItemCode
WHERE CI.IsBlocked = 0 WHERE CI.IsBlocked = 0
@@ -190,7 +190,7 @@ JOIN Avail A
AND A.ColorCode = G.ColorCode AND A.ColorCode = G.ColorCode
AND ISNULL(A.ItemDim2Code, '') = ISNULL(G.ItemDim2Code, '') AND ISNULL(A.ItemDim2Code, '') = ISNULL(G.ItemDim2Code, '')
JOIN #AttrFiltered AF ON AF.ProductCode = G.ItemCode JOIN #AttrFiltered AF ON AF.ProductCode = G.ItemCode
LEFT JOIN cdColorDesc C WITH (NOLOCK) LEFT JOIN cdColorDesc C
ON C.ColorCode = G.ColorCode ON C.ColorCode = G.ColorCode
AND C.LangCode = 'TR' AND C.LangCode = 'TR'
ORDER BY G.ItemCode, G.ColorCode, G.ItemDim2Code, A.ItemDim1Code ORDER BY G.ItemCode, G.ColorCode, G.ItemDim2Code, A.ItemDim1Code
+27
View File
@@ -70,18 +70,45 @@ CREATE TABLE IF NOT EXISTS mk_product_series_job_log (
error_text TEXT NOT NULL DEFAULT '' error_text TEXT NOT NULL DEFAULT ''
)`, )`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_series_job_log_started ON mk_product_series_job_log (started_at DESC)`, `CREATE INDEX IF NOT EXISTS ix_mk_product_series_job_log_started ON mk_product_series_job_log (started_at DESC)`,
`
CREATE TABLE IF NOT EXISTS mk_product_series_apply_state (
row_key TEXT PRIMARY KEY,
last_apply_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_apply_kind TEXT NOT NULL DEFAULT '',
last_series_ids TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT ck_mk_product_series_apply_state_kind CHECK (last_apply_kind IN ('', 'rules', 'fallback'))
)`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_series_apply_state_updated ON mk_product_series_apply_state (updated_at DESC)`,
// Backward compatible schema upgrades.
`ALTER TABLE mk_product_series_apply_state ADD COLUMN IF NOT EXISTS invalid_streak INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_series_apply_state ADD COLUMN IF NOT EXISTS last_invalid_at TIMESTAMPTZ`,
} }
for _, stmt := range stmts { for _, stmt := range stmts {
if _, err := pg.Exec(stmt); err != nil { if _, err := pg.Exec(stmt); err != nil {
return err return err
} }
} }
// Ensure a default fallback series exists for "stok var ama seri secilemedi" cases.
// The scheduler may apply it when no other rule matches. We create only the definition;
// we intentionally do NOT auto-seed mk_product_series_rule from its title.
// Default code for this installation is "1".
_, _ = pg.Exec(`
INSERT INTO dfgrp (code, title, is_active, typ, master, parent_filter, sort_order, is_required, notes)
SELECT '1', '', TRUE, 'opt', 'zbggseri', '', 0, FALSE, 'auto-created fallback series'
WHERE NOT EXISTS (
SELECT 1 FROM dfgrp WHERE master='zbggseri' AND code='1'
)
`)
_, err := pg.Exec(` _, err := pg.Exec(`
INSERT INTO mk_product_series_rule (series_id, size_group, size_code, ratio_qty, priority, source, notes) INSERT INTO mk_product_series_rule (series_id, size_group, size_code, ratio_qty, priority, source, notes)
SELECT d.id, '', BTRIM(x.size_code), 1, 0, 'dfgrp_title', 'auto-seeded from dfgrp.title' SELECT d.id, '', BTRIM(x.size_code), 1, 0, 'dfgrp_title', 'auto-seeded from dfgrp.title'
FROM dfgrp d FROM dfgrp d
CROSS JOIN LATERAL regexp_split_to_table(COALESCE(d.title, ''), '-') AS x(size_code) CROSS JOIN LATERAL regexp_split_to_table(COALESCE(d.title, ''), '-') AS x(size_code)
WHERE d.master='zbggseri' WHERE d.master='zbggseri'
AND COALESCE(d.code,'') <> '1'
AND BTRIM(x.size_code) <> '' AND BTRIM(x.size_code) <> ''
ON CONFLICT (series_id, size_group, size_code) DO NOTHING ON CONFLICT (series_id, size_group, size_code) DO NOTHING
`) `)
+5 -5
View File
@@ -6,8 +6,8 @@ const GetProductAttributes = `
t.ItemTypeCode, t.ItemTypeCode,
t.AttributeTypeCode, t.AttributeTypeCode,
ISNULL(NULLIF(td.AttributeTypeDescription, ''), CAST(t.AttributeTypeCode AS NVARCHAR(30))) AS AttributeTypeDescription ISNULL(NULLIF(td.AttributeTypeDescription, ''), CAST(t.AttributeTypeCode AS NVARCHAR(30))) AS AttributeTypeDescription
FROM dbo.cdItemAttributeType AS t WITH(NOLOCK) FROM dbo.cdItemAttributeType AS t
LEFT JOIN dbo.cdItemAttributeTypeDesc AS td WITH(NOLOCK) LEFT JOIN dbo.cdItemAttributeTypeDesc AS td
ON td.ItemTypeCode = t.ItemTypeCode ON td.ItemTypeCode = t.ItemTypeCode
AND td.AttributeTypeCode = t.AttributeTypeCode AND td.AttributeTypeCode = t.AttributeTypeCode
AND td.LangCode = 'TR' AND td.LangCode = 'TR'
@@ -21,8 +21,8 @@ Attr AS (
a.AttributeTypeCode, a.AttributeTypeCode,
ISNULL(a.AttributeCode, '') AS AttributeCode, ISNULL(a.AttributeCode, '') AS AttributeCode,
ISNULL(d.AttributeDescription, ISNULL(a.AttributeCode, '')) AS AttributeDescription ISNULL(d.AttributeDescription, ISNULL(a.AttributeCode, '')) AS AttributeDescription
FROM dbo.cdItemAttribute AS a WITH(NOLOCK) FROM dbo.cdItemAttribute AS a
LEFT JOIN dbo.cdItemAttributeDesc AS d WITH(NOLOCK) LEFT JOIN dbo.cdItemAttributeDesc AS d
ON d.ItemTypeCode = a.ItemTypeCode ON d.ItemTypeCode = a.ItemTypeCode
AND d.AttributeTypeCode = a.AttributeTypeCode AND d.AttributeTypeCode = a.AttributeTypeCode
AND d.AttributeCode = a.AttributeCode AND d.AttributeCode = a.AttributeCode
@@ -53,7 +53,7 @@ SELECT
a.ItemTypeCode, a.ItemTypeCode,
a.AttributeTypeCode, a.AttributeTypeCode,
ISNULL(a.AttributeCode, '') AS AttributeCode ISNULL(a.AttributeCode, '') AS AttributeCode
FROM dbo.prItemAttribute AS a WITH(NOLOCK) FROM dbo.prItemAttribute AS a
WHERE a.ItemTypeCode = @p1 WHERE a.ItemTypeCode = @p1
AND ISNULL(LTRIM(RTRIM(a.ItemCode)), '') = ISNULL(LTRIM(RTRIM(@p2)), '') AND ISNULL(LTRIM(RTRIM(a.ItemCode)), '') = ISNULL(LTRIM(RTRIM(@p2)), '')
` `
+2 -2
View File
@@ -8,9 +8,9 @@ SELECT DISTINCT
v.ColorCode, v.ColorCode,
ISNULL(cd.ColorDescription, '') AS ColorDescription ISNULL(cd.ColorDescription, '') AS ColorDescription
FROM ProductFilterWithDescription('TR') AS p FROM ProductFilterWithDescription('TR') AS p
INNER JOIN prItemVariant AS v WITH(NOLOCK) INNER JOIN prItemVariant AS v
ON v.ItemCode = p.ProductCode ON v.ItemCode = p.ProductCode
LEFT JOIN cdColorDesc AS cd WITH(NOLOCK) LEFT JOIN cdColorDesc AS cd
ON cd.ColorCode = v.ColorCode ON cd.ColorCode = v.ColorCode
AND cd.LangCode = 'TR' AND cd.LangCode = 'TR'
WHERE WHERE
+3 -3
View File
@@ -39,14 +39,14 @@ SELECT
ISNULL(prItemVariant.ItemDim1Code, '') AS ItemDim1Code, ISNULL(prItemVariant.ItemDim1Code, '') AS ItemDim1Code,
ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code
FROM ProductFilterWithDescription('TR') AS Product FROM ProductFilterWithDescription('TR') AS Product
INNER JOIN cdItem WITH(NOLOCK) INNER JOIN cdItem
ON cdItem.ItemTypeCode = 1 ON cdItem.ItemTypeCode = 1
AND cdItem.ItemCode = Product.ProductCode AND cdItem.ItemCode = Product.ProductCode
LEFT JOIN prProductLot WITH(NOLOCK) LEFT JOIN prProductLot
ON prProductLot.ItemTypeCode = cdItem.ItemTypeCode ON prProductLot.ItemTypeCode = cdItem.ItemTypeCode
AND prProductLot.ItemCode = cdItem.ItemCode AND prProductLot.ItemCode = cdItem.ItemCode
AND prProductLot.IsDefault = 1 AND prProductLot.IsDefault = 1
LEFT JOIN prItemVariant WITH(NOLOCK) LEFT JOIN prItemVariant
ON prItemVariant.ItemCode = Product.ProductCode ON prItemVariant.ItemCode = Product.ProductCode
WHERE WHERE
ProductAtt42 IN ('SERI','AKSESUAR') ProductAtt42 IN ('SERI','AKSESUAR')
+116 -29
View File
@@ -59,7 +59,7 @@ func LookupFirmaIDByKodu(ctx context.Context, uretimDB *sql.DB, firmaKodu string
} }
sqlText := ` sqlText := `
SELECT TOP 1 ISNULL(F.nFirmaID, 0) AS nFirmaID SELECT TOP 1 ISNULL(F.nFirmaID, 0) AS nFirmaID
FROM dbo.tbFirma F WITH (NOLOCK) FROM dbo.tbFirma F
WHERE LTRIM(RTRIM(ISNULL(F.sKodu, ''))) = @p1 WHERE LTRIM(RTRIM(ISNULL(F.sKodu, ''))) = @p1
ORDER BY F.nFirmaID ORDER BY F.nFirmaID
` `
@@ -234,10 +234,10 @@ func UpsertV3ItemBasePriceUSD(
var targetCountry string var targetCountry string
err := mssqlDB.QueryRowContext(ctx, ` err := mssqlDB.QueryRowContext(ctx, `
SELECT TOP 1 C.CountryCode SELECT TOP 1 C.CountryCode
FROM dbo.cdCountry C WITH (NOLOCK) FROM dbo.cdCountry C
WHERE C.CountryCode <> 'TR' WHERE C.CountryCode <> 'TR'
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM dbo.prItemBasePrice P WITH (NOLOCK) SELECT 1 FROM dbo.prItemBasePrice P
WHERE P.ItemTypeCode = 1 WHERE P.ItemTypeCode = 1
AND LTRIM(RTRIM(P.ItemCode)) = LTRIM(RTRIM(@p1)) AND LTRIM(RTRIM(P.ItemCode)) = LTRIM(RTRIM(@p1))
AND P.BasePriceCode = 1 AND P.BasePriceCode = 1
@@ -448,7 +448,7 @@ func GetProductionProductCostingMTBolumOptions(ctx context.Context, uretimDB *sq
SELECT TOP (@p2) SELECT TOP (@p2)
ISNULL(B.nUrtMTBolumID, 0) AS nUrtMTBolumID, ISNULL(B.nUrtMTBolumID, 0) AS nUrtMTBolumID,
ISNULL(B.sAdi, '') AS sAdi ISNULL(B.sAdi, '') AS sAdi
FROM dbo.spUrtMTBolum B WITH (NOLOCK) FROM dbo.spUrtMTBolum B
WHERE ISNULL(B.bAktif, 0) = 1 WHERE ISNULL(B.bAktif, 0) = 1
AND ISNULL(B.nUrtTipiID, 0) = 1 AND ISNULL(B.nUrtTipiID, 0) = 1
AND (@p1 = '' OR ISNULL(B.sAdi, '') LIKE @p3 OR CONVERT(VARCHAR(32), ISNULL(B.nUrtMTBolumID, 0)) LIKE @p3) AND (@p1 = '' OR ISNULL(B.sAdi, '') LIKE @p3 OR CONVERT(VARCHAR(32), ISNULL(B.nUrtMTBolumID, 0)) LIKE @p3)
@@ -477,15 +477,15 @@ SELECT
CAST(CASE WHEN ISNULL(M.bAktif, 0) = 1 THEN 1 ELSE 0 END AS bit) AS bAktif, CAST(CASE WHEN ISNULL(M.bAktif, 0) = 1 THEN 1 ELSE 0 END AS bit) AS bAktif,
CONVERT(VARCHAR(16), M.dteIslemTarihi, 120) AS dteIslemTarihi, CONVERT(VARCHAR(16), M.dteIslemTarihi, 120) AS dteIslemTarihi,
ISNULL(M.sKullaniciAdi, '') AS sKullaniciAdi ISNULL(M.sKullaniciAdi, '') AS sKullaniciAdi
FROM dbo.mk_MaliyetParcaEslestirme M WITH (NOLOCK) FROM dbo.mk_MaliyetParcaEslestirme M
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK) LEFT JOIN dbo.spUrtMTBolum B
ON B.nUrtMTBolumID = M.nUrtMTBolumID ON B.nUrtMTBolumID = M.nUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1 AND ISNULL(B.nUrtTipiID, 0) = 1
OUTER APPLY ( OUTER APPLY (
SELECT SELECT
STUFF(( STUFF((
SELECT ',' + LTRIM(RTRIM(CONVERT(VARCHAR(32), X.nHammaddeTuruNo))) SELECT ',' + LTRIM(RTRIM(CONVERT(VARCHAR(32), X.nHammaddeTuruNo)))
FROM dbo.mk_MaliyetParcaEslestirme_HammaddeTuru X WITH (NOLOCK) FROM dbo.mk_MaliyetParcaEslestirme_HammaddeTuru X
WHERE X.mapping_id = M.id WHERE X.mapping_id = M.id
ORDER BY X.nHammaddeTuruNo ORDER BY X.nHammaddeTuruNo
FOR XML PATH(''), TYPE FOR XML PATH(''), TYPE
@@ -630,7 +630,7 @@ SELECT
ISNULL(H.nHammaddeTuruNo, 0) AS nHammaddeTuruNo, ISNULL(H.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
ISNULL(H.sAciklama, '') AS sAciklama, ISNULL(H.sAciklama, '') AS sAciklama,
ISNULL(H.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID ISNULL(H.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK) FROM dbo.spUrtOnMLHammaddeTuru H
WHERE H.nHammaddeTuruNo IN (%s) WHERE H.nHammaddeTuruNo IN (%s)
`, strings.Join(ph, ",")) `, strings.Join(ph, ","))
@@ -793,6 +793,82 @@ ORDER BY SonIsEmri.dteIslemTarihi DESC, LTRIM(RTRIM(R.sMModelKodu)) ASC
return uretimDB.QueryContext(ctx, sqlText, fromDate, search) return uretimDB.QueryContext(ctx, sqlText, fromDate, search)
} }
type ArgeBaggiDepotEntryTotals struct {
Ceket float64
Pantolon float64
Yelek float64
}
// GetArgeBaggiDepotEntryTotals returns operation 985/986/987 quantities keyed
// by production work order. The requested work orders are queried in one batch
// to avoid an ArgeBAGGI round trip for every list row.
func GetArgeBaggiDepotEntryTotals(ctx context.Context, argeDB *sql.DB, workOrders []string) (map[string]ArgeBaggiDepotEntryTotals, error) {
out := make(map[string]ArgeBaggiDepotEntryTotals)
if argeDB == nil || len(workOrders) == 0 {
return out, nil
}
seen := make(map[string]struct{}, len(workOrders))
values := make([]string, 0, len(workOrders))
for _, raw := range workOrders {
value := strings.TrimSpace(raw)
if value == "" || value == "0" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
values = append(values, value)
}
if len(values) == 0 {
return out, nil
}
placeholders := make([]string, 0, len(values))
args := make([]any, 0, len(values))
for i, value := range values {
placeholders = append(placeholders, fmt.Sprintf("@p%d", i+1))
args = append(args, value)
}
sqlText := fmt.Sprintf(`
SELECT
LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi))) AS IsEmriNo,
SUM(CASE WHEN O.oper_num = 985 THEN CONVERT(float, B.adet) ELSE 0 END) AS CeketDepoGiris,
SUM(CASE WHEN O.oper_num = 986 THEN CONVERT(float, B.adet) ELSE 0 END) AS PantolonDepoGiris,
SUM(CASE WHEN O.oper_num = 987 THEN CONVERT(float, B.adet) ELSE 0 END) AS YelekDepoGiris
FROM dbo.is_acma_bar B
INNER JOIN dbo.is_acma I ON I.is_num = B.is_num
INNER JOIN dbo.model_def MD ON I.imdef_num = MD.mdef_num
INNER JOIN dbo.oper_urun_no O ON B.oper_num = O.oper_num
INNER JOIN dbo.model_num MN ON I.model_num = MN.model_num
INNER JOIN dbo.bant_no BN ON I.bant_num = BN.bant_no
INNER JOIN dbo.model_oper MO ON O.oper_num = MO.oper_num AND MD.mdef_num = MO.model_num
INNER JOIN dbo.model_oper_tkp_def TD ON MO.tkp_cik = TD.tkp_cik
WHERE B.tarih >= '2025-01-01'
AND TD.tkp_cik IN (1,2,3,4,5)
AND O.oper_num IN (985,986,987)
AND LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi))) IN (%s)
GROUP BY LTRIM(RTRIM(CONVERT(VARCHAR(64), MN.model_adi)))
`, strings.Join(placeholders, ","))
rows, err := argeDB.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var workOrder string
var totals ArgeBaggiDepotEntryTotals
if err := rows.Scan(&workOrder, &totals.Ceket, &totals.Pantolon, &totals.Yelek); err != nil {
return nil, err
}
out[strings.TrimSpace(workOrder)] = totals
}
return out, rows.Err()
}
func GetProductionHasCostProducts( func GetProductionHasCostProducts(
ctx context.Context, ctx context.Context,
uretimDB *sql.DB, uretimDB *sql.DB,
@@ -816,12 +892,22 @@ WITH SonOnMaliyet AS (
FROM dbo.spUrtOnMLMas M FROM dbo.spUrtOnMLMas M
WHERE LTRIM(RTRIM(M.UrunKodu)) <> '' WHERE LTRIM(RTRIM(M.UrunKodu)) <> ''
), ),
SonSiparis AS ( SonSiparisSirali AS (
SELECT SELECT
LTRIM(RTRIM(sMModelKodu)) AS UrunKodu, LTRIM(RTRIM(SD.sMModelKodu)) AS UrunKodu,
MAX(dteIslemTarihi) AS SonSiparisTarihi SD.dteIslemTarihi AS SonSiparisTarihi,
FROM dbo.spUrtSiparisDet SM.nUrtSiparisNo,
GROUP BY LTRIM(RTRIM(sMModelKodu)) ROW_NUMBER() OVER (
PARTITION BY LTRIM(RTRIM(SD.sMModelKodu))
ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) AS rn
FROM dbo.spUrtSiparisDet SD
INNER JOIN dbo.spUrtSiparis SM ON SM.nUrtSiparisID = SD.nUrtSiparisID
),
SonSiparis AS (
SELECT UrunKodu, SonSiparisTarihi, nUrtSiparisNo
FROM SonSiparisSirali
WHERE rn = 1
) )
SELECT SELECT
CASE CASE
@@ -831,6 +917,7 @@ SELECT
THEN N'FASON ICIN URETILEN' THEN N'FASON ICIN URETILEN'
ELSE N'Tanimsiz' ELSE N'Tanimsiz'
END AS UretimSekli, END AS UretimSekli,
RTRIM(CONVERT(VARCHAR(32), ISNULL(SS.nUrtSiparisNo, 0))) AS nUrtSiparisNo,
RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo, RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo,
LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu, LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu,
ISNULL(OM.UrunAdi, '') AS UrunAdi, ISNULL(OM.UrunAdi, '') AS UrunAdi,
@@ -954,7 +1041,7 @@ func GetProductionHasCostDetailRowsByOnMLNo(
ON T.nHammaddeTuruNo = D.nHammaddeTuruNo ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
LEFT JOIN dbo.spUrtMTBolum B LEFT JOIN dbo.spUrtMTBolum B
ON B.nUrtMTBolumID = D.nUrtMTBolumID ON B.nUrtMTBolumID = D.nUrtMTBolumID
LEFT JOIN dbo.tbStok SX WITH (NOLOCK) LEFT JOIN dbo.tbStok SX
ON (SX.IsBlocked = 0 OR SX.IsBlocked IS NULL) ON (SX.IsBlocked = 0 OR SX.IsBlocked IS NULL)
AND ISNULL(SX.sKodu,'') = ISNULL(D.sKodu,'') AND ISNULL(SX.sKodu,'') = ISNULL(D.sKodu,'')
WHERE D.nOnMLNo = @p1 WHERE D.nOnMLNo = @p1
@@ -1168,7 +1255,7 @@ HammaddeTekil AS (
CASE WHEN H.MTnUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END, CASE WHEN H.MTnUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
H.nHammaddeTuruNo H.nHammaddeTuruNo
) HT ) HT
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK) LEFT JOIN dbo.spUrtMTBolum B
ON B.nUrtMTBolumID = HT.MTnUrtMTBolumID ON B.nUrtMTBolumID = HT.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1 AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE HT.nHammaddeTuruNo IS NOT NULL WHERE HT.nHammaddeTuruNo IS NOT NULL
@@ -1246,8 +1333,8 @@ func GetProductionHasCostDetailHammaddeTypeOptions(
COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3, COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID, ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK) FROM dbo.spUrtOnMLHammaddeTuru T
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK) LEFT JOIN dbo.spUrtMTBolum B
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1 AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE WHERE
@@ -1320,10 +1407,10 @@ SELECT
ISNULL(T.sAciklama, '') AS sAciklama, ISNULL(T.sAciklama, '') AS sAciklama,
ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID, ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
ISNULL(B.sAdi, '') AS sParcaAdi ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T WITH (NOLOCK) FROM dbo.spUrtOnMLHammaddeTuru T
INNER JOIN req R INNER JOIN req R
ON R.nHammaddeTuruNo = T.nHammaddeTuruNo ON R.nHammaddeTuruNo = T.nHammaddeTuruNo
LEFT JOIN dbo.spUrtMTBolum B WITH (NOLOCK) LEFT JOIN dbo.spUrtMTBolum B
ON B.nUrtMTBolumID = T.MTnUrtMTBolumID ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
AND ISNULL(B.nUrtTipiID, 0) = 1 AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE ISNULL(T.bAktif, 0) = 1 WHERE ISNULL(T.bAktif, 0) = 1
@@ -1373,8 +1460,8 @@ SELECT TOP (@p3)
-- We intentionally don't depend on mk_MaliyetParcaEslestirme_vmiktarlar.bAktif -- We intentionally don't depend on mk_MaliyetParcaEslestirme_vmiktarlar.bAktif
-- to keep this feature robust across schema changes; only active hammadde types are listed. -- to keep this feature robust across schema changes; only active hammadde types are listed.
CAST(1 AS bit) AS bAktif CAST(1 AS bit) AS bAktif
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK) FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK) LEFT JOIN dbo.spUrtOnMLHammaddeTuru H
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
WHERE WHERE
(@p1 = '' OR CONVERT(VARCHAR(32), ISNULL(V.nHammaddeTuruNo, 0)) LIKE @p2) (@p1 = '' OR CONVERT(VARCHAR(32), ISNULL(V.nHammaddeTuruNo, 0)) LIKE @p2)
@@ -1427,8 +1514,8 @@ func RefreshProductionProductCostingDefaultQty(ctx context.Context, uretimDB *sq
D.dteIslemTarihi DESC, D.dteIslemTarihi DESC,
D.nOnMLDetNo DESC D.nOnMLDetNo DESC
) AS rn ) AS rn
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK) FROM dbo.spUrtOnMLMasDet D
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK) INNER JOIN dbo.spUrtOnMLMas M
ON M.nOnMLNo = D.nOnMLNo ON M.nOnMLNo = D.nOnMLNo
WHERE ISNULL(D.lMiktar, 0) > 0 WHERE ISNULL(D.lMiktar, 0) > 0
AND ISNULL(M.bIptal, 0) = 0 AND ISNULL(M.bIptal, 0) = 0
@@ -1470,8 +1557,8 @@ func CalcProductionProductCostingDefaultQtyFromLastOnML(ctx context.Context, ure
;WITH ranked AS ( ;WITH ranked AS (
SELECT TOP (@p2) SELECT TOP (@p2)
ISNULL(D.lMiktar, 0) AS lMiktar ISNULL(D.lMiktar, 0) AS lMiktar
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK) FROM dbo.spUrtOnMLMasDet D
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK) INNER JOIN dbo.spUrtOnMLMas M
ON M.nOnMLNo = D.nOnMLNo ON M.nOnMLNo = D.nOnMLNo
WHERE D.nHammaddeTuruNo = @p1 WHERE D.nHammaddeTuruNo = @p1
AND ISNULL(D.lMiktar, 0) > 0 AND ISNULL(D.lMiktar, 0) > 0
@@ -1535,9 +1622,9 @@ WITH req(nHammaddeTuruNo) AS (
ISNULL(H.sAciklama, '') AS sAciklama, ISNULL(H.sAciklama, '') AS sAciklama,
CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
FROM req R FROM req R
INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V WITH (NOLOCK) INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V
ON V.nHammaddeTuruNo = R.nHammaddeTuruNo ON V.nHammaddeTuruNo = R.nHammaddeTuruNo
INNER JOIN dbo.spUrtOnMLHammaddeTuru H WITH (NOLOCK) INNER JOIN dbo.spUrtOnMLHammaddeTuru H
ON H.nHammaddeTuruNo = V.nHammaddeTuruNo ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
WHERE ISNULL(H.bAktif, 0) = 1; WHERE ISNULL(H.bAktif, 0) = 1;
` `
@@ -1653,8 +1740,8 @@ ranked AS (
M.nOnMLNo DESC, M.nOnMLNo DESC,
D.nOnMLDetNo DESC D.nOnMLDetNo DESC
) AS rn ) AS rn
FROM dbo.spUrtOnMLMasDet D WITH (NOLOCK) FROM dbo.spUrtOnMLMasDet D
INNER JOIN dbo.spUrtOnMLMas M WITH (NOLOCK) INNER JOIN dbo.spUrtOnMLMas M
ON M.nOnMLNo = D.nOnMLNo ON M.nOnMLNo = D.nOnMLNo
INNER JOIN req R INNER JOIN req R
ON R.nHammaddeTuruNo = D.nHammaddeTuruNo ON R.nHammaddeTuruNo = D.nHammaddeTuruNo
+2 -2
View File
@@ -5,8 +5,8 @@ SELECT
CAST(@p1 AS NVARCHAR(30)) AS ProductCode, CAST(@p1 AS NVARCHAR(30)) AS ProductCode,
LTRIM(RTRIM(c.ColorCode)) AS ColorCode, LTRIM(RTRIM(c.ColorCode)) AS ColorCode,
ISNULL(NULLIF(LTRIM(RTRIM(cd.ColorDescription)), ''), ISNULL(NULLIF(LTRIM(RTRIM(c.ColorHex)), ''), LTRIM(RTRIM(c.ColorCode)))) AS ColorDescription ISNULL(NULLIF(LTRIM(RTRIM(cd.ColorDescription)), ''), ISNULL(NULLIF(LTRIM(RTRIM(c.ColorHex)), ''), LTRIM(RTRIM(c.ColorCode)))) AS ColorDescription
FROM dbo.cdColor AS c WITH(NOLOCK) FROM dbo.cdColor AS c
LEFT JOIN dbo.cdColorDesc AS cd WITH(NOLOCK) LEFT JOIN dbo.cdColorDesc AS cd
ON cd.ColorCode = c.ColorCode ON cd.ColorCode = c.ColorCode
AND cd.LangCode = 'TR' AND cd.LangCode = 'TR'
WHERE ISNULL(c.IsBlocked, 0) = 0 WHERE ISNULL(c.IsBlocked, 0) = 0
+2 -2
View File
@@ -6,8 +6,8 @@ SELECT
LTRIM(RTRIM(ISNULL(@ColorCode, ''))) AS ColorCode, LTRIM(RTRIM(ISNULL(@ColorCode, ''))) AS ColorCode,
LTRIM(RTRIM(d2.ItemDim2Code)) AS ItemDim2Code, LTRIM(RTRIM(d2.ItemDim2Code)) AS ItemDim2Code,
ISNULL(NULLIF(LTRIM(RTRIM(cd.ColorDescription)), ''), LTRIM(RTRIM(d2.ItemDim2Code))) AS ColorDescription ISNULL(NULLIF(LTRIM(RTRIM(cd.ColorDescription)), ''), LTRIM(RTRIM(d2.ItemDim2Code))) AS ColorDescription
FROM dbo.cdItemDim2 AS d2 WITH(NOLOCK) FROM dbo.cdItemDim2 AS d2
LEFT JOIN dbo.cdColorDesc AS cd WITH(NOLOCK) LEFT JOIN dbo.cdColorDesc AS cd
ON cd.ColorCode = d2.ItemDim2Code ON cd.ColorCode = d2.ItemDim2Code
AND cd.LangCode = 'TR' AND cd.LangCode = 'TR'
WHERE ISNULL(d2.IsBlocked, 0) = 0 WHERE ISNULL(d2.IsBlocked, 0) = 0
+2 -2
View File
@@ -6,10 +6,10 @@ SELECT
ISNULL(prItemVariant.ColorCode, '') AS ColorCode, ISNULL(prItemVariant.ColorCode, '') AS ColorCode,
ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code, ISNULL(prItemVariant.ItemDim2Code, '') AS ItemDim2Code,
ISNULL(ColorDesc.ColorDescription, '') AS ColorDescription ISNULL(ColorDesc.ColorDescription, '') AS ColorDescription
FROM prItemVariant WITH(NOLOCK) FROM prItemVariant
INNER JOIN ProductFilterWithDescription('TR') AS Product INNER JOIN ProductFilterWithDescription('TR') AS Product
ON prItemVariant.ItemCode = Product.ProductCode ON prItemVariant.ItemCode = Product.ProductCode
LEFT JOIN cdColorDesc AS ColorDesc WITH(NOLOCK) LEFT JOIN cdColorDesc AS ColorDesc
ON ColorDesc.ColorCode = prItemVariant.ItemDim2Code ON ColorDesc.ColorCode = prItemVariant.ItemDim2Code
AND ColorDesc.LangCode = 'TR' AND ColorDesc.LangCode = 'TR'
WHERE Product.ProductCode = @ProductCode WHERE Product.ProductCode = @ProductCode
+30 -30
View File
@@ -3,7 +3,7 @@ package queries
// GetProductStockQuery: // GetProductStockQuery:
// Urun kodu bazli, STOCK/PICK/RESERVE/DISP ayrik CTE ile optimize sorgu. // Urun kodu bazli, STOCK/PICK/RESERVE/DISP ayrik CTE ile optimize sorgu.
const GetProductStockQuery = ` const GetProductStockQuery = `
DECLARE @ProductCode NVARCHAR(50) = @p1; DECLARE @ProductCode NVARCHAR(50) = UPPER(LTRIM(RTRIM(@p1)));
;WITH STOCK AS ;WITH STOCK AS
( (
@@ -14,20 +14,20 @@ DECLARE @ProductCode NVARCHAR(50) = @p1;
StoreCode, StoreCode,
WarehouseCode, WarehouseCode,
ItemTypeCode, ItemTypeCode,
ItemCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode,
ColorCode, ColorCode,
ItemDim1Code, ItemDim1Code,
ItemDim2Code, ItemDim2Code,
ItemDim3Code, ItemDim3Code,
SUM(In_Qty1 - Out_Qty1) AS InventoryQty1 SUM(In_Qty1 - Out_Qty1) AS InventoryQty1
FROM trStock WITH(NOLOCK) FROM trStock
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
), ),
@@ -35,17 +35,17 @@ PICK AS
( (
SELECT SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS PickingQty1 SUM(Qty1) AS PickingQty1
FROM PickingStates FROM PickingStates
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
), ),
@@ -53,17 +53,17 @@ RESERVE AS
( (
SELECT SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS ReserveQty1 SUM(Qty1) AS ReserveQty1
FROM ReserveStates FROM ReserveStates
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
), ),
@@ -71,17 +71,17 @@ DISP AS
( (
SELECT SELECT
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))) AS ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code, ItemDim1Code, ItemDim2Code, ItemDim3Code,
SUM(Qty1) AS DispOrderQty1 SUM(Qty1) AS DispOrderQty1
FROM DispOrderStates FROM DispOrderStates
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(ItemCode))) = @ProductCode
AND LEN(ItemCode) = 13 AND LEN(LTRIM(RTRIM(ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
GROUP BY GROUP BY
CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode, WarehouseCode,
ItemTypeCode, ItemCode, ColorCode, ItemTypeCode, UPPER(LTRIM(RTRIM(ItemCode))), ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code ItemDim1Code, ItemDim2Code, ItemDim3Code
) )
@@ -184,25 +184,25 @@ AND DP.ItemDim1Code=S.ItemDim1Code
AND DP.ItemDim2Code=S.ItemDim2Code AND DP.ItemDim2Code=S.ItemDim2Code
AND DP.ItemDim3Code=S.ItemDim3Code AND DP.ItemDim3Code=S.ItemDim3Code
JOIN cdItem WITH(NOLOCK) JOIN cdItem
ON S.ItemCode = cdItem.ItemCode ON UPPER(LTRIM(RTRIM(S.ItemCode))) = UPPER(LTRIM(RTRIM(cdItem.ItemCode)))
AND S.ItemTypeCode = cdItem.ItemTypeCode AND S.ItemTypeCode = cdItem.ItemTypeCode
LEFT JOIN cdUnitOfMeasure WITH(NOLOCK) LEFT JOIN cdUnitOfMeasure
ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode ON cdItem.UnitOfMeasureCode1 = cdUnitOfMeasure.UnitOfMeasureCode
LEFT JOIN ProductFilterWithDescription('TR') P LEFT JOIN ProductFilterWithDescription('TR') P
ON P.ProductCode = S.ItemCode ON UPPER(LTRIM(RTRIM(P.ProductCode))) = UPPER(LTRIM(RTRIM(S.ItemCode)))
LEFT JOIN bsItemTypeDesc WITH(NOLOCK) LEFT JOIN bsItemTypeDesc
ON bsItemTypeDesc.ItemTypeCode = S.ItemTypeCode ON bsItemTypeDesc.ItemTypeCode = S.ItemTypeCode
AND bsItemTypeDesc.LangCode='TR' AND bsItemTypeDesc.LangCode='TR'
LEFT JOIN cdWarehouseDesc W WITH(NOLOCK) LEFT JOIN cdWarehouseDesc W
ON W.WarehouseCode = S.WarehouseCode ON W.WarehouseCode = S.WarehouseCode
AND W.LangCode='TR' AND W.LangCode='TR'
LEFT JOIN cdColorDesc C WITH(NOLOCK) LEFT JOIN cdColorDesc C
ON C.ColorCode = S.ColorCode ON C.ColorCode = S.ColorCode
AND C.LangCode='TR' AND C.LangCode='TR'
@@ -212,16 +212,16 @@ LEFT JOIN (
ItemTypeCode, ItemTypeCode,
Price, Price,
ROW_NUMBER() OVER (PARTITION BY ItemCode, ItemTypeCode ORDER BY PriceDate DESC) AS RowNum ROW_NUMBER() OVER (PARTITION BY ItemCode, ItemTypeCode ORDER BY PriceDate DESC) AS RowNum
FROM prItemBasePrice WITH(NOLOCK) FROM prItemBasePrice
) prFilteredBasePrice ) prFilteredBasePrice
ON prFilteredBasePrice.ItemCode = S.ItemCode ON UPPER(LTRIM(RTRIM(prFilteredBasePrice.ItemCode))) = UPPER(LTRIM(RTRIM(S.ItemCode)))
AND prFilteredBasePrice.ItemTypeCode = S.ItemTypeCode AND prFilteredBasePrice.ItemTypeCode = S.ItemTypeCode
AND prFilteredBasePrice.RowNum = 1 AND prFilteredBasePrice.RowNum = 1
WHERE WHERE
S.ItemTypeCode IN (1) S.ItemTypeCode IN (1)
AND S.ItemCode = @ProductCode AND UPPER(LTRIM(RTRIM(S.ItemCode))) = @ProductCode
AND LEN(S.ItemCode) = 13 AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
AND LEN(@ProductCode) = 13 AND LEN(@ProductCode) = 13
AND ( AND (
S.InventoryQty1 S.InventoryQty1
@@ -234,6 +234,6 @@ WHERE
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
); );
` `
+57 -57
View File
@@ -28,7 +28,7 @@ IF OBJECT_ID('dbo.ProductFilterTRCache','U') IS NOT NULL
BEGIN BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT SELECT
ProductCode, UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)), Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)),
@@ -48,7 +48,7 @@ ELSE
BEGIN BEGIN
INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal) INSERT INTO #AttrBase (ProductCode, Kategori, UrunAnaGrubu, UrunAltGrubu, UrunIcerigi, Fit, DropVal)
SELECT SELECT
ProductCode, UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
Kategori = LTRIM(RTRIM(ProductAtt44Desc)), Kategori = LTRIM(RTRIM(ProductAtt44Desc)),
UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)), UrunAnaGrubu = LTRIM(RTRIM(ProductAtt01Desc)),
UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)), UrunAltGrubu = LTRIM(RTRIM(ProductAtt02Desc)),
@@ -101,40 +101,40 @@ CREATE NONCLUSTERED INDEX IX_AttrBase_Filter ON #AttrBase(Kategori, UrunAnaGrubu
FROM FROM
( (
SELECT SELECT
P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, UPPER(LTRIM(RTRIM(P.ItemCode))) AS ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code,
P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P FROM PickingStates P
INNER JOIN #AttrBase AB ON AB.ProductCode = P.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(P.ItemCode)))
WHERE P.ItemTypeCode = 1 WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13 AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
UNION ALL UNION ALL
SELECT SELECT
R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, UPPER(LTRIM(RTRIM(R.ItemCode))) AS ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code,
0, R.Qty1, 0, 0 0, R.Qty1, 0, 0
FROM ReserveStates R FROM ReserveStates R
INNER JOIN #AttrBase AB ON AB.ProductCode = R.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(R.ItemCode)))
WHERE R.ItemTypeCode = 1 WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13 AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
UNION ALL UNION ALL
SELECT SELECT
D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, UPPER(LTRIM(RTRIM(D.ItemCode))) AS ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code,
0, 0, D.Qty1, 0 0, 0, D.Qty1, 0
FROM DispOrderStates D FROM DispOrderStates D
INNER JOIN #AttrBase AB ON AB.ProductCode = D.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(D.ItemCode)))
WHERE D.ItemTypeCode = 1 WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13 AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
UNION ALL UNION ALL
SELECT SELECT
T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, UPPER(LTRIM(RTRIM(T.ItemCode))) AS ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T WITH (NOLOCK) FROM trStock T
INNER JOIN #AttrBase AB ON AB.ProductCode = T.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(T.ItemCode)))
WHERE T.ItemTypeCode = 1 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 AND LEN(LTRIM(RTRIM(T.ItemCode))) = 13
GROUP BY T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code GROUP BY UPPER(LTRIM(RTRIM(T.ItemCode))), T.ColorCode, T.ItemDim1Code, T.ItemDim2Code
) X ) X
GROUP BY X.ItemCode, X.ColorCode, X.ItemDim1Code, X.ItemDim2Code GROUP BY X.ItemCode, X.ColorCode, X.ItemDim1Code, X.ItemDim2Code
), ),
@@ -148,7 +148,7 @@ Avail AS
Beden = LTRIM(RTRIM(I.ItemDim1Code)), Beden = LTRIM(RTRIM(I.ItemDim1Code)),
Kullanilabilir = (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) Kullanilabilir = (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1)
FROM INV I FROM INV I
LEFT JOIN cdColorDesc C WITH (NOLOCK) LEFT JOIN cdColorDesc C
ON C.ColorCode = I.ColorCode ON C.ColorCode = I.ColorCode
AND C.LangCode = 'TR' AND C.LangCode = 'TR'
WHERE (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0 WHERE (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0
@@ -180,7 +180,7 @@ SELECT 'renk', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END SELECT DISTINCT FieldValue = CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END
FROM Avail A FROM Avail A
INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) <> '' WHERE (CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.Renk END) <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -196,7 +196,7 @@ SELECT 'renk2', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = A.Renk2 SELECT DISTINCT FieldValue = A.Renk2
FROM Avail A FROM Avail A
INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE A.Renk2 <> '' WHERE A.Renk2 <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -245,7 +245,7 @@ SELECT 'beden', X.FieldValue
FROM ( FROM (
SELECT DISTINCT FieldValue = A.Beden SELECT DISTINCT FieldValue = A.Beden
FROM Avail A FROM Avail A
INNER JOIN #AttrBase AB ON AB.ProductCode = A.ItemCode INNER JOIN #AttrBase AB ON AB.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE A.Beden <> '' WHERE A.Beden <> ''
AND (@Kategori IS NULL OR AB.Kategori = @Kategori) AND (@Kategori IS NULL OR AB.Kategori = @Kategori)
AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR AB.UrunAnaGrubu = @UrunAnaGrubu)
@@ -346,7 +346,7 @@ BEGIN
ProductAtt46Desc ProductAtt46Desc
) )
SELECT SELECT
C.ProductCode, UPPER(LTRIM(RTRIM(C.ProductCode))) AS ProductCode,
C.ProductDescription, C.ProductDescription,
C.ProductAtt01Desc, C.ProductAtt01Desc,
C.ProductAtt02Desc, C.ProductAtt02Desc,
@@ -379,7 +379,7 @@ BEGIN
ISNULL(PF.ProductAtt45Desc, '') AS ProductAtt45Desc, ISNULL(PF.ProductAtt45Desc, '') AS ProductAtt45Desc,
ISNULL(PF.ProductAtt46Desc, '') AS ProductAtt46Desc ISNULL(PF.ProductAtt46Desc, '') AS ProductAtt46Desc
FROM dbo.ProductFilterTRCache C FROM dbo.ProductFilterTRCache C
LEFT JOIN ProductFilterWithDescription('TR') PF ON PF.ProductCode = C.ProductCode LEFT JOIN ProductFilterWithDescription('TR') PF ON UPPER(LTRIM(RTRIM(PF.ProductCode))) = UPPER(LTRIM(RTRIM(C.ProductCode)))
WHERE LEN(C.ProductCode) = 13 WHERE LEN(C.ProductCode) = 13
AND (@Kategori IS NULL OR C.ProductAtt44Desc = @Kategori) AND (@Kategori IS NULL OR C.ProductAtt44Desc = @Kategori)
AND (@UrunAnaGrubu IS NULL OR C.ProductAtt01Desc = @UrunAnaGrubu) AND (@UrunAnaGrubu IS NULL OR C.ProductAtt01Desc = @UrunAnaGrubu)
@@ -390,9 +390,9 @@ BEGIN
END END
ELSE ELSE
BEGIN BEGIN
INSERT INTO #AttrFiltered INSERT INTO #AttrFiltered
( (
ProductCode, ProductCode,
ProductDescription, ProductDescription,
ProductAtt01Desc, ProductAtt01Desc,
ProductAtt02Desc, ProductAtt02Desc,
@@ -426,7 +426,7 @@ BEGIN
ProductAtt46Desc ProductAtt46Desc
) )
SELECT SELECT
ProductCode, UPPER(LTRIM(RTRIM(ProductCode))) AS ProductCode,
ProductDescription, ProductDescription,
ProductAtt01Desc, ProductAtt01Desc,
ProductAtt02Desc, ProductAtt02Desc,
@@ -493,69 +493,69 @@ INV AS
( (
SELECT SELECT
P.CompanyCode, P.OfficeCode, P.StoreTypeCode, P.StoreCode, P.WarehouseCode, P.CompanyCode, P.OfficeCode, P.StoreTypeCode, P.StoreCode, P.WarehouseCode,
P.ItemTypeCode, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code, P.ItemTypeCode, UPPER(LTRIM(RTRIM(P.ItemCode))) AS ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code,
P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1 P.Qty1 AS PickingQty1, 0 AS ReserveQty1, 0 AS DispOrderQty1, 0 AS InventoryQty1
FROM PickingStates P FROM PickingStates P
INNER JOIN #AttrFiltered AF ON AF.ProductCode = P.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(P.ItemCode)))
WHERE P.ItemTypeCode = 1 WHERE P.ItemTypeCode = 1
AND LEN(P.ItemCode) = 13 AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
AND P.WarehouseCode IN AND P.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
UNION ALL UNION ALL
SELECT SELECT
R.CompanyCode, R.OfficeCode, R.StoreTypeCode, R.StoreCode, R.WarehouseCode, R.CompanyCode, R.OfficeCode, R.StoreTypeCode, R.StoreCode, R.WarehouseCode,
R.ItemTypeCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code, R.ItemTypeCode, UPPER(LTRIM(RTRIM(R.ItemCode))) AS ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code,
0, R.Qty1, 0, 0 0, R.Qty1, 0, 0
FROM ReserveStates R FROM ReserveStates R
INNER JOIN #AttrFiltered AF ON AF.ProductCode = R.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(R.ItemCode)))
WHERE R.ItemTypeCode = 1 WHERE R.ItemTypeCode = 1
AND LEN(R.ItemCode) = 13 AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
AND R.WarehouseCode IN AND R.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
UNION ALL UNION ALL
SELECT SELECT
D.CompanyCode, D.OfficeCode, D.StoreTypeCode, D.StoreCode, D.WarehouseCode, D.CompanyCode, D.OfficeCode, D.StoreTypeCode, D.StoreCode, D.WarehouseCode,
D.ItemTypeCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code, D.ItemTypeCode, UPPER(LTRIM(RTRIM(D.ItemCode))) AS ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code,
0, 0, D.Qty1, 0 0, 0, D.Qty1, 0
FROM DispOrderStates D FROM DispOrderStates D
INNER JOIN #AttrFiltered AF ON AF.ProductCode = D.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(D.ItemCode)))
WHERE D.ItemTypeCode = 1 WHERE D.ItemTypeCode = 1
AND LEN(D.ItemCode) = 13 AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
AND D.WarehouseCode IN AND D.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
UNION ALL UNION ALL
SELECT SELECT
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode, T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code, T.ItemTypeCode, UPPER(LTRIM(RTRIM(T.ItemCode))) AS ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code,
0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1) 0, 0, 0, SUM(T.In_Qty1 - T.Out_Qty1)
FROM trStock T WITH (NOLOCK) FROM trStock T
INNER JOIN #AttrFiltered AF ON AF.ProductCode = T.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(T.ItemCode)))
WHERE T.ItemTypeCode = 1 WHERE T.ItemTypeCode = 1
AND LEN(T.ItemCode) = 13 AND LEN(LTRIM(RTRIM(T.ItemCode))) = 13
AND T.WarehouseCode IN AND T.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
GROUP BY GROUP BY
T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode, T.CompanyCode, T.OfficeCode, T.StoreTypeCode, T.StoreCode, T.WarehouseCode,
T.ItemTypeCode, T.ItemCode, T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code T.ItemTypeCode, UPPER(LTRIM(RTRIM(T.ItemCode))), T.ColorCode, T.ItemDim1Code, T.ItemDim2Code, T.ItemDim3Code
) X ) X
GROUP BY GROUP BY
X.CompanyCode, X.OfficeCode, X.StoreTypeCode, X.StoreCode, X.WarehouseCode, X.CompanyCode, X.OfficeCode, X.StoreTypeCode, X.StoreCode, X.WarehouseCode,
@@ -578,23 +578,23 @@ Avail AS
Kullanilabilir = ROUND(I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1, U.RoundDigit), Kullanilabilir = ROUND(I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1, U.RoundDigit),
RenkAciklama = LTRIM(RTRIM(C.ColorDescription)) RenkAciklama = LTRIM(RTRIM(C.ColorDescription))
FROM INV I FROM INV I
JOIN cdItem CI WITH (NOLOCK) JOIN cdItem CI
ON CI.ItemTypeCode = I.ItemTypeCode ON CI.ItemTypeCode = I.ItemTypeCode
AND CI.ItemCode = I.ItemCode AND UPPER(LTRIM(RTRIM(CI.ItemCode))) = I.ItemCode
LEFT JOIN cdUnitOfMeasure U WITH (NOLOCK) LEFT JOIN cdUnitOfMeasure U
ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1 ON U.UnitOfMeasureCode = CI.UnitOfMeasureCode1
LEFT JOIN cdColorDesc C WITH (NOLOCK) LEFT JOIN cdColorDesc C
ON C.ColorCode = I.ColorCode ON C.ColorCode = I.ColorCode
AND C.LangCode = 'TR' AND C.LangCode = 'TR'
WHERE I.ItemTypeCode = 1 WHERE I.ItemTypeCode = 1
AND LEN(I.ItemCode) = 13 AND LEN(LTRIM(RTRIM(I.ItemCode))) = 13
AND (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0 AND (I.InventoryQty1 - I.PickingQty1 - I.ReserveQty1 - I.DispOrderQty1) > 0
AND CI.IsBlocked = 0 AND CI.IsBlocked = 0
AND I.WarehouseCode IN AND I.WarehouseCode IN
( (
'1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28',
'1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3',
'1-0-33','101','1-014','1-0-49','1-0-36' '1-0-33','101','1-014','1-0-49','1-0-36','1-0-4','1-0-29'
) )
), ),
Grouped AS Grouped AS
@@ -604,7 +604,7 @@ Grouped AS
A.ColorCode, A.ColorCode,
A.ItemDim2Code A.ItemDim2Code
FROM Avail A FROM Avail A
INNER JOIN #AttrFiltered AF ON AF.ProductCode = A.ItemCode INNER JOIN #AttrFiltered AF ON AF.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
WHERE (@RenkList IS NULL OR CHARINDEX(NCHAR(31) + ISNULL((CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.ColorCode END),'') + NCHAR(31), NCHAR(31) + @RenkList + NCHAR(31)) > 0) WHERE (@RenkList IS NULL OR CHARINDEX(NCHAR(31) + ISNULL((CASE WHEN A.RenkAciklama <> '' THEN A.RenkAciklama ELSE A.ColorCode END),'') + NCHAR(31), NCHAR(31) + @RenkList + NCHAR(31)) > 0)
AND (@Renk2List IS NULL OR CHARINDEX(NCHAR(31) + ISNULL(A.ItemDim2Code,'') + NCHAR(31), NCHAR(31) + @Renk2List + NCHAR(31)) > 0) AND (@Renk2List IS NULL OR CHARINDEX(NCHAR(31) + ISNULL(A.ItemDim2Code,'') + NCHAR(31), NCHAR(31) + @Renk2List + NCHAR(31)) > 0)
AND ( AND (
@@ -669,19 +669,19 @@ INNER JOIN Grouped G
AND G.ColorCode = A.ColorCode AND G.ColorCode = A.ColorCode
AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '') AND ISNULL(G.ItemDim2Code, '') = ISNULL(A.ItemDim2Code, '')
INNER JOIN #AttrFiltered AF INNER JOIN #AttrFiltered AF
ON AF.ProductCode = A.ItemCode ON AF.ProductCode = UPPER(LTRIM(RTRIM(A.ItemCode)))
LEFT JOIN cdWarehouseDesc W WITH (NOLOCK) LEFT JOIN cdWarehouseDesc W
ON W.WarehouseCode = A.WarehouseCode ON W.WarehouseCode = A.WarehouseCode
AND W.LangCode = 'TR' AND W.LangCode = 'TR'
LEFT JOIN bsItemTypeDesc IT WITH (NOLOCK) LEFT JOIN bsItemTypeDesc IT
ON IT.ItemTypeCode = A.ItemTypeCode ON IT.ItemTypeCode = A.ItemTypeCode
AND IT.LangCode = 'TR' AND IT.LangCode = 'TR'
OUTER APPLY ( OUTER APPLY (
SELECT TOP 1 Price SELECT TOP 1 Price
FROM prItemBasePrice PB WITH (NOLOCK) FROM prItemBasePrice PB
WHERE PB.ItemTypeCode = 1 WHERE PB.ItemTypeCode = 1
AND PB.ItemCode = A.ItemCode AND UPPER(LTRIM(RTRIM(PB.ItemCode))) = UPPER(LTRIM(RTRIM(A.ItemCode)))
AND LEN(PB.ItemCode) = 13 AND LEN(LTRIM(RTRIM(PB.ItemCode))) = 13
ORDER BY PB.PriceDate DESC ORDER BY PB.PriceDate DESC
) P ) P
OPTION (RECOMPILE); OPTION (RECOMPILE);
+2 -2
View File
@@ -159,7 +159,7 @@ WITH BaseCari AS (
PARTITION BY LEFT(CurrAccCode, 8) PARTITION BY LEFT(CurrAccCode, 8)
ORDER BY CurrAccCode ORDER BY CurrAccCode
) )
FROM cdCurrAcc WITH (NOLOCK) FROM cdCurrAcc
WHERE CurrAccTypeCode IN (1,3) WHERE CurrAccTypeCode IN (1,3)
AND LEFT(CurrAccCode, 8) IN (%s) AND LEFT(CurrAccCode, 8) IN (%s)
) )
@@ -167,7 +167,7 @@ SELECT
b.MasterCari, b.MasterCari,
CariDetay = ISNULL(d.CurrAccDescription, '') CariDetay = ISNULL(d.CurrAccDescription, '')
FROM BaseCari b FROM BaseCari b
LEFT JOIN cdCurrAccDesc d WITH (NOLOCK) LEFT JOIN cdCurrAccDesc d
ON d.CurrAccTypeCode = b.CurrAccTypeCode ON d.CurrAccTypeCode = b.CurrAccTypeCode
AND d.CurrAccCode = b.CurrAccCode AND d.CurrAccCode = b.CurrAccCode
AND d.LangCode = 'TR' AND d.LangCode = 'TR'
+2 -2
View File
@@ -46,7 +46,7 @@ func GetStatements(ctx context.Context, params models.StatementParams) ([]models
( (
(b.CurrAccTypeCode = 3 AND EXISTS ( (b.CurrAccTypeCode = 3 AND EXISTS (
SELECT 1 SELECT 1
FROM CustomerAttributesFilter PF WITH (NOLOCK) FROM CustomerAttributesFilter PF
WHERE (PF.CurrAccCode = b.CurrAccCode OR LEFT(PF.CurrAccCode, 8) = LEFT(b.CurrAccCode, 8)) WHERE (PF.CurrAccCode = b.CurrAccCode OR LEFT(PF.CurrAccCode, 8) = LEFT(b.CurrAccCode, 8))
AND %s AND %s
)) ))
@@ -57,7 +57,7 @@ func GetStatements(ctx context.Context, params models.StatementParams) ([]models
SELECT SELECT
CurrAccCode, CurrAccCode,
VendorAtt01 = MAX(CASE WHEN AttributeTypeCode = 1 THEN AttributeCode END) VendorAtt01 = MAX(CASE WHEN AttributeTypeCode = 1 THEN AttributeCode END)
FROM prCurrAccAttribute WITH (NOLOCK) FROM prCurrAccAttribute
WHERE CurrAccTypeCode = 1 WHERE CurrAccTypeCode = 1
GROUP BY CurrAccCode GROUP BY CurrAccCode
) VF ) VF
+2 -2
View File
@@ -64,13 +64,13 @@ WITH C AS (
HIT AS ( HIT AS (
SELECT DISTINCT C.code SELECT DISTINCT C.code
FROM C FROM C
JOIN dbo.tbStok S WITH (NOLOCK) JOIN dbo.tbStok S
ON S.sKodu = C.code ON S.sKodu = C.code
OR S.sKodu = C.code_nospace OR S.sKodu = C.code_nospace
UNION UNION
SELECT DISTINCT C.code SELECT DISTINCT C.code
FROM C FROM C
JOIN dbo.tbStok S WITH (NOLOCK) JOIN dbo.tbStok S
ON S.sModel = C.code ON S.sModel = C.code
) )
SELECT SELECT
@@ -23,7 +23,7 @@ VARIANT_MASTER AS (
LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode, LTRIM(RTRIM(ISNULL(V.ColorCode,''))) AS ColorCode,
LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code, LTRIM(RTRIM(ISNULL(V.ItemDim2Code,''))) AS ItemDim3Code,
MAX(LTRIM(RTRIM(ISNULL(V.ItemDim1Code,'')))) AS ItemDim1Code MAX(LTRIM(RTRIM(ISNULL(V.ItemDim1Code,'')))) AS ItemDim1Code
FROM prItemVariant V WITH(NOLOCK) FROM prItemVariant V
JOIN INP ON INP.ItemCode = V.ItemCode JOIN INP ON INP.ItemCode = V.ItemCode
WHERE V.ItemTypeCode = 1 WHERE V.ItemTypeCode = 1
AND LEN(V.ItemCode) = 13 AND LEN(V.ItemCode) = 13
@@ -36,7 +36,7 @@ VARIANT_STOCK AS (
LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode, LTRIM(RTRIM(ISNULL(S.ColorCode,''))) AS ColorCode,
LTRIM(RTRIM(ISNULL(S.ItemDim2Code,''))) AS ItemDim3Code, LTRIM(RTRIM(ISNULL(S.ItemDim2Code,''))) AS ItemDim3Code,
MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code
FROM trStock S WITH(NOLOCK) FROM trStock S
JOIN INP ON INP.ItemCode = S.ItemCode JOIN INP ON INP.ItemCode = S.ItemCode
WHERE S.ItemTypeCode = 1 WHERE S.ItemTypeCode = 1
AND LEN(S.ItemCode) = 13 AND LEN(S.ItemCode) = 13
@@ -63,7 +63,7 @@ STOCK AS (
LTRIM(RTRIM(ISNULL(S.ItemDim2Code,''))) AS ItemDim3Code, LTRIM(RTRIM(ISNULL(S.ItemDim2Code,''))) AS ItemDim3Code,
MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code, MAX(LTRIM(RTRIM(ISNULL(S.ItemDim1Code,'')))) AS ItemDim1Code,
SUM(S.In_Qty1 - S.Out_Qty1) AS InventoryQty1 SUM(S.In_Qty1 - S.Out_Qty1) AS InventoryQty1
FROM trStock S WITH(NOLOCK) FROM trStock S
JOIN INP ON INP.ItemCode = S.ItemCode JOIN INP ON INP.ItemCode = S.ItemCode
WHERE S.ItemTypeCode = 1 WHERE S.ItemTypeCode = 1
AND LEN(S.ItemCode) = 13 AND LEN(S.ItemCode) = 13
+3 -3
View File
@@ -215,10 +215,10 @@ SELECT TOP (1)
ISNULL(h.CurrAccCode, ''), ISNULL(h.CurrAccCode, ''),
ISNULL(LTRIM(RTRIM(f.CustomerAtt01)), '') AS MarketCode, ISNULL(LTRIM(RTRIM(f.CustomerAtt01)), '') AS MarketCode,
ISNULL(py.AttributeDescription, '') AS MarketTitle ISNULL(py.AttributeDescription, '') AS MarketTitle
FROM BAGGI_V3.dbo.trOrderHeader h WITH (NOLOCK) FROM BAGGI_V3.dbo.trOrderHeader h
LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter f WITH (NOLOCK) LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter f
ON f.CurrAccCode = h.CurrAccCode ON f.CurrAccCode = h.CurrAccCode
LEFT JOIN BAGGI_V3.dbo.cdCurrAccAttributeDesc py WITH (NOLOCK) LEFT JOIN BAGGI_V3.dbo.cdCurrAccAttributeDesc py
ON py.CurrAccTypeCode = h.CurrAccTypeCode ON py.CurrAccTypeCode = h.CurrAccTypeCode
AND py.AttributeTypeCode = 1 AND py.AttributeTypeCode = 1
AND py.AttributeCode = f.CustomerAtt01 AND py.AttributeCode = f.CustomerAtt01
+3 -3
View File
@@ -691,7 +691,7 @@ func getOrderHeaderFromDB(db *sql.DB, orderID string) (*OrderHeader, error) {
h.CreatedUserName, h.CreatedUserName,
ISNULL(( ISNULL((
SELECT TOP (1) ca.AttributeDescription SELECT TOP (1) ca.AttributeDescription
FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca WITH (NOLOCK) FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca
WHERE ca.CurrAccTypeCode IN (1,3) WHERE ca.CurrAccTypeCode IN (1,3)
AND ca.AttributeTypeCode = 2 -- 🟡 Müşteri Temsilcisi AND ca.AttributeTypeCode = 2 -- 🟡 Müşteri Temsilcisi
AND ca.AttributeCode = f.CustomerAtt02 AND ca.AttributeCode = f.CustomerAtt02
@@ -764,7 +764,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
WHEN ISNULL(L.IsClosed, 0) = 1 WHEN ISNULL(L.IsClosed, 0) = 1
OR EXISTS ( OR EXISTS (
SELECT 1 SELECT 1
FROM BAGGI_V3.dbo.trInvoiceLine il WITH (NOLOCK) FROM BAGGI_V3.dbo.trInvoiceLine il
WHERE il.OrderLineID = L.OrderLineID WHERE il.OrderLineID = L.OrderLineID
) )
THEN CAST(1 AS bit) THEN CAST(1 AS bit)
@@ -777,7 +777,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
L.VatCode, L.VatCode,
L.VatRate L.VatRate
FROM BAGGI_V3.dbo.trOrderLine AS L FROM BAGGI_V3.dbo.trOrderLine AS L
LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD WITH (NOLOCK) LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD
ON CD.OrderLineID = L.OrderLineID ON CD.OrderLineID = L.OrderLineID
AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY') AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
LEFT JOIN ProductFilterWithDescription('TR') AS P LEFT JOIN ProductFilterWithDescription('TR') AS P
+1 -1
View File
@@ -43,7 +43,7 @@ SELECT
StoreCapacityLevelCode, StoreCapacityLevelCode,
CustomsTariffNumberCode, CustomsTariffNumberCode,
CompanyCode CompanyCode
FROM dbo.cdItem WITH(NOLOCK) FROM dbo.cdItem
WHERE ItemCode = @p1; WHERE ItemCode = @p1;
` `
row := db.MssqlDB.QueryRow(query, code) row := db.MssqlDB.QueryRow(query, code)
+277 -17
View File
@@ -17,14 +17,33 @@ import (
) )
type ProductImageItem struct { type ProductImageItem struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FileName string `json:"file_name"` FileName string `json:"file_name"`
FileSize int64 `json:"file_size"` FileSize int64 `json:"file_size"`
Storage string `json:"storage_path"` Storage string `json:"storage_path"`
ContentURL string `json:"content_url"` ContentURL string `json:"content_url"`
UUID string `json:"uuid,omitempty"` UUID string `json:"uuid,omitempty"`
ThumbURL string `json:"thumb_url,omitempty"` ThumbURL string `json:"thumb_url,omitempty"`
FullURL string `json:"full_url,omitempty"` FullURL string `json:"full_url,omitempty"`
StoredInDB bool `json:"stored_in_db,omitempty"`
HasDBContent bool `json:"-"`
}
type ProductImageBatchItem struct {
Key string `json:"key"`
Code string `json:"code"`
Dim1 string `json:"dim1"`
Dim3 string `json:"dim3"`
Limit int `json:"limit,omitempty"`
}
type ProductImageBatchRequest struct {
Items []ProductImageBatchItem `json:"items"`
}
type ProductImageBatchResponseItem struct {
Key string `json:"key"`
Images []ProductImageItem `json:"images"`
} }
var uuidPattern = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) var uuidPattern = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
@@ -119,6 +138,206 @@ func extractImageUUID(storagePath, fileName string) string {
return "" return ""
} }
func enrichProductImageItem(it *ProductImageItem) {
if it == nil {
return
}
if u := extractImageUUID(it.Storage, it.FileName); u != "" {
it.UUID = u
it.ThumbURL = productImagePublicURLIfExists("uploads/image/t300/" + u + ".jpg")
it.FullURL = productImagePublicURLIfExists("uploads/image/" + u + ".jpg")
}
if it.FullURL == "" {
it.FullURL = productImagePublicURLIfExists(it.Storage)
}
if it.FullURL == "" {
it.FullURL = productImagePublicURLIfExists(it.FileName)
}
if it.ID > 0 && (it.HasDBContent || productImageStoredFileExists(it.Storage, it.FileName)) {
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID)
}
}
func productImageItemHasURL(it *ProductImageItem) bool {
return it != nil && (it.ThumbURL != "" || it.FullURL != "" || it.ContentURL != "")
}
func productImageStoredFileExists(storagePath, fileName string) bool {
if resolved, _ := resolveStoragePath(storagePath); resolved != "" {
return true
}
if resolved, _ := resolveStoragePath(fileName); resolved != "" {
return true
}
return false
}
func productImagePublicURLIfExists(storagePath string) string {
raw := strings.TrimSpace(storagePath)
if raw == "" {
return ""
}
resolved, _ := resolveStoragePath(raw)
if resolved == "" {
return ""
}
normalized := strings.ReplaceAll(raw, "\\", "/")
lower := strings.ToLower(normalized)
if idx := strings.Index(lower, "/uploads/"); idx >= 0 {
return normalized[idx:]
}
if strings.HasPrefix(lower, "uploads/") {
return "/" + normalized
}
root := strings.TrimSpace(os.Getenv("BLOB_ROOT"))
if root == "" {
return ""
}
rel, err := filepath.Rel(root, resolved)
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
return ""
}
rel = filepath.ToSlash(rel)
if strings.HasPrefix(strings.ToLower(rel), "uploads/") {
return "/" + rel
}
return ""
}
// POST /api/product-images/batch
func PostProductImagesBatchHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqID := strings.TrimSpace(r.Header.Get("X-Request-ID"))
if reqID == "" {
reqID = uuid.NewString()
}
w.Header().Set("X-Request-ID", reqID)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var req ProductImageBatchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Gecersiz gorsel batch istegi: "+err.Error(), http.StatusBadRequest)
return
}
if len(req.Items) > 300 {
req.Items = req.Items[:300]
}
cleanItems := make([]ProductImageBatchItem, 0, len(req.Items))
seen := map[string]bool{}
for _, item := range req.Items {
item.Key = strings.TrimSpace(item.Key)
item.Code = strings.TrimSpace(item.Code)
item.Dim1 = normalizeDimParam(item.Dim1)
item.Dim3 = normalizeDimParam(item.Dim3)
if item.Key == "" {
item.Key = item.Code + "|" + item.Dim1 + "|" + item.Dim3
}
if item.Code == "" || seen[item.Key] {
continue
}
seen[item.Key] = true
cleanItems = append(cleanItems, item)
}
if len(cleanItems) == 0 {
_ = json.NewEncoder(w).Encode([]ProductImageBatchResponseItem{})
return
}
payload, err := json.Marshal(cleanItems)
if err != nil {
http.Error(w, "Gorsel batch hazirlanamadi: "+err.Error(), http.StatusInternalServerError)
return
}
rows, err := pg.Query(`
WITH req AS (
SELECT *
FROM jsonb_to_recordset($1::jsonb) AS x(key text, code text, dim1 text, dim3 text)
),
matched_item AS (
SELECT DISTINCT ON (r.key)
r.key,
r.code,
r.dim1,
r.dim3,
m.id AS mmitem_id
FROM req r
JOIN mmitem m ON
UPPER(REPLACE(COALESCE(m.code,''), ' ', '')) = UPPER(REPLACE(COALESCE(r.code,''), ' ', ''))
OR UPPER(REPLACE(REGEXP_REPLACE(COALESCE(m.code,''), '^.*-', ''), ' ', '')) =
UPPER(REPLACE(REGEXP_REPLACE(COALESCE(r.code,''), '^.*-', ''), ' ', ''))
ORDER BY r.key, m.id
),
ranked AS (
SELECT
mi.key,
b.id,
COALESCE(b.file_name,'') AS file_name,
COALESCE(b.file_size,0) AS file_size,
COALESCE(b.storage_path,'') AS storage_path,
COALESCE(b.stored_in_db,false) AS stored_in_db,
(COALESCE(octet_length(b.bin),0) > 0) AS has_db_content,
ROW_NUMBER() OVER (
PARTITION BY mi.key
ORDER BY
CASE
WHEN mi.dim1 <> '' AND mi.dim3 <> '' AND COALESCE(b.dimval1::text,'') = mi.dim1 AND COALESCE(b.dimval3::text,'') = mi.dim3 THEN 0
WHEN mi.dim1 <> '' AND COALESCE(b.dimval1::text,'') = mi.dim1 AND COALESCE(b.dimval3::text,'') = '' THEN 1
WHEN COALESCE(b.dimval1::text,'') = '' AND COALESCE(b.dimval3::text,'') = '' THEN 2
ELSE 3
END,
COALESCE(b.sort_order,999999),
b.id
) AS rn
FROM matched_item mi
JOIN dfblob b ON b.src_table='mmitem'
AND b.typ='img'
AND b.src_id=mi.mmitem_id
)
SELECT key, id, file_name, file_size, storage_path, stored_in_db, has_db_content
FROM ranked
WHERE rn <= 5
ORDER BY key, rn
`, string(payload))
if err != nil {
slog.Error("product_images.batch.query_failed", "req_id", reqID, "err", err.Error())
http.Error(w, "Gorsel batch sorgu hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
grouped := map[string][]ProductImageItem{}
for rows.Next() {
var key string
var it ProductImageItem
if err := rows.Scan(&key, &it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB, &it.HasDBContent); err != nil {
continue
}
enrichProductImageItem(&it)
if !productImageItemHasURL(&it) {
continue
}
grouped[key] = append(grouped[key], it)
}
if err := rows.Err(); err != nil {
http.Error(w, "Gorsel batch okuma hatasi: "+err.Error(), http.StatusInternalServerError)
return
}
out := make([]ProductImageBatchResponseItem, 0, len(cleanItems))
for _, item := range cleanItems {
out = append(out, ProductImageBatchResponseItem{
Key: item.Key,
Images: grouped[item.Key],
})
}
_ = json.NewEncoder(w).Encode(out)
}
}
// GET /api/product-images?code=...&dim1=...&dim3=... // GET /api/product-images?code=...&dim1=...&dim3=...
func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc { func GetProductImagesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@@ -190,7 +409,9 @@ SELECT
id, id,
COALESCE(file_name,'') AS file_name, COALESCE(file_name,'') AS file_name,
COALESCE(file_size,0) AS file_size, COALESCE(file_size,0) AS file_size,
COALESCE(storage_path,'') AS storage_path COALESCE(storage_path,'') AS storage_path,
COALESCE(stored_in_db,false) AS stored_in_db,
(COALESCE(octet_length(bin),0) > 0) AS has_db_content
FROM dfblob FROM dfblob
WHERE typ='img' WHERE typ='img'
AND src_table='mmitem' AND src_table='mmitem'
@@ -221,16 +442,13 @@ ORDER BY
items := make([]ProductImageItem, 0, 16) items := make([]ProductImageItem, 0, 16)
for rows.Next() { for rows.Next() {
var it ProductImageItem var it ProductImageItem
if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage); err != nil { if err := rows.Scan(&it.ID, &it.FileName, &it.FileSize, &it.Storage, &it.StoredInDB, &it.HasDBContent); err != nil {
continue continue
} }
it.ContentURL = fmt.Sprintf("/api/product-images/%d/content", it.ID) enrichProductImageItem(&it)
if u := extractImageUUID(it.Storage, it.FileName); u != "" { if productImageItemHasURL(&it) {
it.UUID = u items = append(items, it)
it.ThumbURL = "/uploads/image/t300/" + u + ".jpg"
it.FullURL = "/uploads/image/" + u + ".jpg"
} }
items = append(items, it)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, err return nil, err
@@ -372,7 +590,8 @@ WHERE id = $1
return return
} }
if storedInDB && len(binData) > 0 { _ = storedInDB
if len(binData) > 0 {
w.Header().Set("Content-Type", http.DetectContentType(binData)) w.Header().Set("Content-Type", http.DetectContentType(binData))
w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("Cache-Control", "public, max-age=3600")
_, _ = w.Write(binData) _, _ = w.Write(binData)
@@ -380,6 +599,9 @@ WHERE id = $1
} }
resolved, _ := resolveStoragePath(storagePath) resolved, _ := resolveStoragePath(storagePath)
if resolved == "" {
resolved, _ = resolveStoragePath(fileName)
}
if resolved == "" { if resolved == "" {
http.NotFound(w, r) http.NotFound(w, r)
return return
@@ -413,14 +635,42 @@ func resolveStoragePath(storagePath string) (string, []string) {
raw = filepath.ToSlash(filepath.Clean(raw)) raw = filepath.ToSlash(filepath.Clean(raw))
relUploads := filepath.FromSlash(filepath.Join("uploads", raw)) relUploads := filepath.FromSlash(filepath.Join("uploads", raw))
baseName := filepath.Base(raw)
relImage := filepath.FromSlash(filepath.Join("uploads", "image", baseName))
relImageThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", baseName))
candidates := []string{ candidates := []string{
filepath.Clean(storagePath), filepath.Clean(storagePath),
filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))), filepath.FromSlash(filepath.Clean(strings.TrimPrefix(storagePath, "/"))),
filepath.FromSlash(filepath.Clean(raw)), filepath.FromSlash(filepath.Clean(raw)),
relUploads, relUploads,
relImage,
relImageThumb,
filepath.Join(".", relUploads), filepath.Join(".", relUploads),
filepath.Join(".", relImage),
filepath.Join(".", relImageThumb),
filepath.Join("..", relUploads), filepath.Join("..", relUploads),
filepath.Join("..", relImage),
filepath.Join("..", relImageThumb),
filepath.Join("..", "..", relUploads), filepath.Join("..", "..", relUploads),
filepath.Join("..", "..", relImage),
filepath.Join("..", "..", relImageThumb),
}
if u := extractImageUUID(raw, baseName); u != "" {
uFull := filepath.FromSlash(filepath.Join("uploads", "image", u+".jpg"))
uThumb := filepath.FromSlash(filepath.Join("uploads", "image", "t300", u+".jpg"))
candidates = append(candidates,
filepath.FromSlash(filepath.Join("image", u+".jpg")),
filepath.FromSlash(filepath.Join("image", "t300", u+".jpg")),
uFull,
uThumb,
filepath.Join(".", uFull),
filepath.Join(".", uThumb),
filepath.Join("..", uFull),
filepath.Join("..", uThumb),
filepath.Join("..", "..", uFull),
filepath.Join("..", "..", uThumb),
)
} }
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" { if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
@@ -428,7 +678,17 @@ func resolveStoragePath(storagePath string) (string, []string) {
filepath.Join(root, raw), filepath.Join(root, raw),
filepath.Join(root, relUploads), filepath.Join(root, relUploads),
filepath.Join(root, "uploads", raw), filepath.Join(root, "uploads", raw),
filepath.Join(root, relImage),
filepath.Join(root, relImageThumb),
) )
if u := extractImageUUID(raw, baseName); u != "" {
candidates = append(candidates,
filepath.Join(root, "uploads", "image", u+".jpg"),
filepath.Join(root, "uploads", "image", "t300", u+".jpg"),
filepath.Join(root, "image", u+".jpg"),
filepath.Join(root, "image", "t300", u+".jpg"),
)
}
} }
for _, p := range candidates { for _, p := range candidates {
+450
View File
@@ -0,0 +1,450 @@
package routes
import (
"bssapp-backend/queries"
"bssapp-backend/utils"
"context"
"database/sql"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
)
func GetProductPerformanceHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
f := queries.ProductPerformanceFilters{
Search: strings.TrimSpace(r.URL.Query().Get("q")),
ProductCode: strings.TrimSpace(r.URL.Query().Get("product_code")),
MarketKey: strings.TrimSpace(r.URL.Query().Get("market_key")),
Kategori: strings.TrimSpace(r.URL.Query().Get("kategori")),
Seri: strings.TrimSpace(r.URL.Query().Get("seri")),
Bucket: strings.TrimSpace(r.URL.Query().Get("bucket")),
SortBy: strings.TrimSpace(r.URL.Query().Get("sort_by")),
Descending: boolQuery(r, "descending", true),
Limit: intQuery(r, "limit", 50000),
Page: intQuery(r, "page", 1),
}
rows, total, err := queries.ListProductPerformance(ctx, pg, f)
if err != nil {
http.Error(w, "urun performans listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("X-Total-Count", strconv.Itoa(total))
_ = json.NewEncoder(w).Encode(map[string]any{
"rows": rows,
"total_count": total,
"page": f.Page,
"limit": f.Limit,
})
}
}
func GetProductPerformanceSalesDetailsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceSalesDetails(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("product_code")),
strings.TrimSpace(r.URL.Query().Get("color_code")),
strings.TrimSpace(r.URL.Query().Get("yaka_kodu")),
intQuery(r, "limit", 100),
)
if err != nil {
http.Error(w, "urun satis detayi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceStockSizesHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceStockSizes(
ctx,
strings.TrimSpace(r.URL.Query().Get("product_code")),
strings.TrimSpace(r.URL.Query().Get("color_code")),
strings.TrimSpace(r.URL.Query().Get("yaka_kodu")),
)
if err != nil {
http.Error(w, "urun beden stok detayi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceSummaryHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
summary, err := queries.GetProductPerformanceSummary(ctx, pg)
if err != nil {
http.Error(w, "urun performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(summary)
}
}
func GetProductPerformanceGeneralHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceGeneral(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "genel urun performans KPI alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceOrdersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderAnalysis(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceOrderGroupsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderGroups(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("breakdown")),
intQuery(r, "limit", 50000),
)
if err != nil {
http.Error(w, "siparis grup analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceOrderProductCustomersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderProductCustomers(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "urun musteri siparis analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceOrderMarketDetailsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceOrderMarketDetails(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "piyasa siparis detay analiz verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceMarketsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceMarkets(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "piyasa performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceCountriesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceCountries(ctx, pg, intQuery(r, "limit", 50000))
if err != nil {
http.Error(w, "ulke performans ozeti alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceCustomersHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceCustomers(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("breakdown")),
intQuery(r, "limit", 50000),
)
if err != nil {
http.Error(w, "musteri performans kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceSalesBreakdownHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
rows, err := queries.ListProductPerformanceSalesBreakdown(
ctx,
pg,
strings.TrimSpace(r.URL.Query().Get("mode")),
intQuery(r, "limit", 50000),
)
if err != nil {
http.Error(w, "satis kpi kirilimi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceGroupedHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by"))
descending := boolQuery(r, "descending", true)
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
limit := intQuery(r, "limit", 50000)
expanded := map[string]bool{}
filters := map[string][]string{}
expandThroughLevel := -1
if rawExpandLevel := strings.TrimSpace(r.URL.Query().Get("expand_through_level")); rawExpandLevel != "" {
if parsed, err := strconv.Atoi(rawExpandLevel); err == nil {
expandThroughLevel = parsed
}
}
const maxExpandedGroupKeys = 1000
addExpandedKey := func(key string) {
key = strings.TrimSpace(key)
if key != "" && len(expanded) < maxExpandedGroupKeys {
expanded[key] = true
}
}
if r.Method == http.MethodPost {
var body struct {
Mode string `json:"mode"`
GroupLevels []string `json:"group_levels"`
ExpandedKeys []string `json:"expanded_keys"`
ExpandThroughLevel *int `json:"expand_through_level"`
Limit int `json:"limit"`
MainGroup string `json:"urun_ana_grubu"`
Filters map[string][]string `json:"filters"`
SortBy string `json:"sort_by"`
Descending *bool `json:"descending"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "gecersiz grup istegi: "+err.Error(), http.StatusBadRequest)
return
}
if strings.TrimSpace(body.Mode) != "" {
mode = strings.TrimSpace(body.Mode)
}
if strings.TrimSpace(body.MainGroup) != "" {
mainGroup = strings.TrimSpace(body.MainGroup)
}
if len(body.GroupLevels) > 0 {
groupLevels = body.GroupLevels
}
if body.Limit > 0 {
limit = body.Limit
}
for _, key := range body.ExpandedKeys {
addExpandedKey(key)
}
if body.ExpandThroughLevel != nil {
expandThroughLevel = *body.ExpandThroughLevel
}
if len(body.Filters) > 0 {
filters = body.Filters
}
if strings.TrimSpace(body.SortBy) != "" {
sortBy = strings.TrimSpace(body.SortBy)
}
if body.Descending != nil {
descending = *body.Descending
}
} else {
for _, key := range strings.Split(r.URL.Query().Get("expanded_keys"), ",") {
addExpandedKey(key)
}
}
rows, err := queries.ListProductPerformanceGrouped(ctx, pg, queries.ProductPerformanceGroupedRequest{
Mode: mode,
GroupLevels: groupLevels,
ExpandedKeys: expanded,
ExpandThroughLevel: expandThroughLevel,
Limit: limit,
MainGroup: mainGroup,
Filters: filters,
SortBy: sortBy,
Descending: descending,
})
if err != nil {
log.Printf("[ProductPerformance][grouped] failed trace_id=%s mode=%s levels=%v expand_through_level=%d filters=%d: %v", traceID, mode, groupLevels, expandThroughLevel, len(filters), err)
http.Error(w, "urun performans grup verisi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func GetProductPerformanceGroupedFilterOptionsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 60*time.Second)
defer cancel()
mode := strings.TrimSpace(r.URL.Query().Get("mode"))
mainGroup := strings.TrimSpace(r.URL.Query().Get("urun_ana_grubu"))
groupLevels := splitCSVQuery(r.URL.Query().Get("group_levels"))
fields := splitCSVQuery(r.URL.Query().Get("fields"))
limit := intQuery(r, "limit", 50000)
if r.Method == http.MethodPost {
var body struct {
Mode string `json:"mode"`
GroupLevels []string `json:"group_levels"`
MainGroup string `json:"urun_ana_grubu"`
Fields []string `json:"fields"`
Limit int `json:"limit"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "gecersiz filtre secenek istegi: "+err.Error(), http.StatusBadRequest)
return
}
if strings.TrimSpace(body.Mode) != "" {
mode = strings.TrimSpace(body.Mode)
}
if strings.TrimSpace(body.MainGroup) != "" {
mainGroup = strings.TrimSpace(body.MainGroup)
}
if len(body.GroupLevels) > 0 {
groupLevels = body.GroupLevels
}
if len(body.Fields) > 0 {
fields = body.Fields
}
if body.Limit > 0 {
limit = body.Limit
}
}
options, err := queries.ListProductPerformanceGroupedFilterOptions(ctx, pg, queries.ProductPerformanceGroupedFilterOptionsRequest{
Mode: mode,
GroupLevels: groupLevels,
MainGroup: mainGroup,
Fields: fields,
Limit: limit,
})
if err != nil {
log.Printf("[ProductPerformance][grouped-filter-options] failed trace_id=%s mode=%s levels=%v fields=%d: %v", traceID, mode, groupLevels, len(fields), err)
http.Error(w, "urun performans filtre secenekleri alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(options)
}
}
func splitCSVQuery(value string) []string {
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}
func intQuery(r *http.Request, key string, fallback int) int {
raw := strings.TrimSpace(r.URL.Query().Get(key))
if raw == "" {
return fallback
}
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
return fallback
}
return n
}
func boolQuery(r *http.Request, key string, fallback bool) bool {
raw := strings.TrimSpace(strings.ToLower(r.URL.Query().Get(key)))
if raw == "" {
return fallback
}
return raw == "1" || raw == "true" || raw == "yes" || raw == "on"
}
+1140
View File
@@ -0,0 +1,1140 @@
package routes
import (
"bssapp-backend/models"
"bssapp-backend/queries"
"bssapp-backend/utils"
"context"
"database/sql"
"encoding/json"
"fmt"
"math"
"net/http"
"sort"
"strings"
"time"
"github.com/xuri/excelize/v2"
)
type productPerformanceExcelExportRequest struct {
Filters map[string][]string `json:"filters"`
SortBy string `json:"sort_by"`
Descending *bool `json:"descending"`
}
type productPerformanceExcelColumn struct {
Header string
Kind string
Value func(*productPerformanceExcelVariant, productPerformanceExcelStats) any
}
type productPerformanceExcelStats struct {
AvgSalesUSD90 float64
AvgSalesUSD180 float64
AvgSalesUSD365 float64
AvgSalesUSDTotal float64
}
type productPerformanceExcelVariant struct {
KpiDate string
ProductCode string
ColorCode string
ColorDescription string
YakaKodu string
ItemDescription string
Kategori string
Seri string
YasGrubu string
AskiliYan string
UrunIlkGrubu string
UrunAnaGrubu string
UrunAltGrubu string
StockQty float64
AvgStock90 float64
AvgStock180 float64
AvgStock365 float64
AvgStockTotal float64
BasePriceUSD float64
CostPriceUSD float64
SalesQty90 float64
SalesQty180 float64
SalesQty365 float64
SalesUSD90 float64
SalesUSD180 float64
SalesUSD365 float64
SalesQtyTotalProduct float64
SalesUSDTotalProduct float64
SalesQtyTotalGeneral float64
SalesUSDTotalGeneral float64
HasGeneralTotal bool
MarketCount90 int
CustomerCount90 int
MarketCountTotal int
CustomerCountTotal int
Markets90 map[string]bool
MarketsTotal map[string]bool
MarketFiltered bool
PerformanceBucket string
Recommendation string
LastSaleDate string
LastRefNumber string
UpdatedAt string
}
func ExportProductPerformanceExcelHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
traceID := utils.TraceIDFromRequest(r)
ctx, cancel := context.WithTimeout(utils.ContextWithTraceID(r.Context(), traceID), 180*time.Second)
defer cancel()
var req productPerformanceExcelExportRequest
if r.Method == http.MethodPost {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "gecersiz excel istegi: "+err.Error(), http.StatusBadRequest)
return
}
}
productRows, _, err := queries.ListProductPerformance(ctx, pg, queries.ProductPerformanceFilters{
Limit: 50000,
Page: 1,
SortBy: "product_code",
Descending: false,
})
if err != nil {
http.Error(w, "urun performans excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
generalRows, err := queries.ListProductPerformanceGeneral(ctx, pg, 50000)
if err != nil {
http.Error(w, "urun performans genel excel listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
rows := buildProductPerformanceExcelVariants(productRows, generalRows, req.Filters)
sortProductPerformanceExcelVariants(rows, req.SortBy, req.Descending == nil || *req.Descending)
stats := productPerformanceExcelStatsFor(rows)
file, err := buildProductPerformanceExcelFile(rows, stats)
if err != nil {
http.Error(w, "urun performans excel dosyasi hazirlanamadi: "+err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = file.Close() }()
buf, err := file.WriteToBuffer()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filename := fmt.Sprintf("product_performance_renk_yaka_%s.xlsx", time.Now().Format("20060102_150405"))
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes())))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
}
}
func buildProductPerformanceExcelVariants(productRows []models.ProductPerformanceRow, generalRows []models.ProductPerformanceGeneralRow, filters map[string][]string) []*productPerformanceExcelVariant {
byKey := map[string]*productPerformanceExcelVariant{}
marketFiltered := productPerformanceExcelHasFilter(filters, "market_key")
for _, row := range productRows {
if !productPerformanceExcelProductRowMatchesFilters(row, filters) {
continue
}
key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)
v := byKey[key]
if v == nil {
v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered}
byKey[key] = v
}
v.addProductRow(row)
}
for _, row := range generalRows {
if !productPerformanceExcelGeneralRowMatchesFilters(row, filters) {
continue
}
key := productPerformanceExcelVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)
v := byKey[key]
if v == nil {
v = &productPerformanceExcelVariant{Markets90: map[string]bool{}, MarketsTotal: map[string]bool{}, MarketFiltered: marketFiltered}
byKey[key] = v
}
v.addGeneralRow(row)
}
out := make([]*productPerformanceExcelVariant, 0, len(byKey))
for _, row := range byKey {
if productPerformanceExcelVariantMatchesPostFilters(row, filters) {
out = append(out, row)
}
}
return out
}
func (v *productPerformanceExcelVariant) addProductRow(row models.ProductPerformanceRow) {
v.fillDimensions(
row.KpiDate, row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription,
row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu,
row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, row.UpdatedAt,
)
v.SalesQty90 += row.SalesQty90
v.SalesQty180 += row.SalesQty180
v.SalesQty365 += row.SalesQty365
v.SalesUSD90 += row.SalesUSD90
v.SalesUSD180 += row.SalesUSD180
v.SalesUSD365 += row.SalesUSD365
v.AvgStock90 = productPerformanceExcelFirstNonZero(v.AvgStock90, row.AvgStock90)
v.AvgStock180 = productPerformanceExcelFirstNonZero(v.AvgStock180, row.AvgStock180)
v.AvgStock365 = productPerformanceExcelFirstNonZero(v.AvgStock365, row.AvgStock365)
v.AvgStockTotal = productPerformanceExcelFirstNonZero(v.AvgStockTotal, row.AvgStockTotal)
v.SalesQtyTotalProduct += row.SalesQtyTotal
v.SalesUSDTotalProduct += row.SalesUSDTotal
v.MarketCount90 = productPerformanceExcelMaxInt(v.MarketCount90, row.MarketCount90)
if row.SalesUSD90 > 0 {
v.CustomerCount90 += row.CustomerCount90
}
v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket)
v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation)
v.addMarket(row.MarketKey, row.SalesQty90, row.SalesUSD90, row.SalesQtyTotal, row.SalesUSDTotal)
}
func (v *productPerformanceExcelVariant) addGeneralRow(row models.ProductPerformanceGeneralRow) {
v.fillDimensions(
productPerformanceExcelFirstNonEmpty(row.PeriodEnd, row.PeriodStart), row.ProductCode, row.ColorCode, row.ColorDescription, row.YakaKodu, row.ItemDescription,
row.Kategori, row.Seri, row.YasGrubu, row.AskiliYan, row.UrunIlkGrubu, row.UrunAnaGrubu, row.UrunAltGrubu,
row.BasePriceUSD, row.CostPriceUSD, row.StockQty, row.LastSaleDate, row.LastRefNumber, "",
)
v.SalesQtyTotalGeneral += row.SalesQtyTotal
v.SalesUSDTotalGeneral += row.SalesUSDTotal
v.HasGeneralTotal = true
v.MarketCountTotal = productPerformanceExcelMaxInt(v.MarketCountTotal, row.MarketCountTotal)
if row.SalesUSDTotal > 0 {
v.CustomerCountTotal += row.CustomerCountTotal
}
v.PerformanceBucket = productPerformanceExcelFirstNonEmpty(v.PerformanceBucket, row.PerformanceBucket)
v.Recommendation = productPerformanceExcelFirstNonEmpty(v.Recommendation, row.Recommendation)
v.addMarket(row.MarketKey, 0, 0, row.SalesQtyTotal, row.SalesUSDTotal)
}
func (v *productPerformanceExcelVariant) fillDimensions(kpiDate, productCode, colorCode, colorDescription, yakaKodu, itemDescription, kategori, seri, yasGrubu, askiliYan, urunIlkGrubu, urunAnaGrubu, urunAltGrubu string, basePrice, costPrice, stockQty float64, lastSaleDate, lastRefNumber, updatedAt string) {
v.KpiDate = productPerformanceExcelFirstNonEmpty(v.KpiDate, kpiDate)
v.ProductCode = productPerformanceExcelFirstNonEmpty(v.ProductCode, productCode)
v.ColorCode = productPerformanceExcelFirstNonEmpty(v.ColorCode, colorCode)
v.ColorDescription = productPerformanceExcelFirstNonEmpty(v.ColorDescription, colorDescription)
v.YakaKodu = productPerformanceExcelFirstNonEmpty(v.YakaKodu, yakaKodu)
v.ItemDescription = productPerformanceExcelFirstNonEmpty(v.ItemDescription, itemDescription)
v.Kategori = productPerformanceExcelFirstNonEmpty(v.Kategori, kategori)
v.Seri = productPerformanceExcelFirstNonEmpty(v.Seri, seri)
v.YasGrubu = productPerformanceExcelFirstNonEmpty(v.YasGrubu, yasGrubu)
v.AskiliYan = productPerformanceExcelFirstNonEmpty(v.AskiliYan, productPerformanceExcelCleanOptional(askiliYan))
v.UrunIlkGrubu = productPerformanceExcelFirstNonEmpty(v.UrunIlkGrubu, productPerformanceExcelCleanOptional(urunIlkGrubu))
v.UrunAnaGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAnaGrubu, urunAnaGrubu)
v.UrunAltGrubu = productPerformanceExcelFirstNonEmpty(v.UrunAltGrubu, urunAltGrubu)
v.BasePriceUSD = productPerformanceExcelFirstNonZero(v.BasePriceUSD, basePrice)
v.CostPriceUSD = productPerformanceExcelFirstNonZero(v.CostPriceUSD, costPrice)
v.CostPriceUSD, v.BasePriceUSD = productPerformanceExcelNormalizeCostPair(v.CostPriceUSD, v.BasePriceUSD)
if stockQty > v.StockQty {
v.StockQty = stockQty
}
if lastSaleDate > v.LastSaleDate {
v.LastSaleDate = lastSaleDate
v.LastRefNumber = lastRefNumber
}
if updatedAt > v.UpdatedAt {
v.UpdatedAt = updatedAt
}
}
func (v *productPerformanceExcelVariant) addMarket(marketKey string, salesQty90, salesUSD90, salesQtyTotal, salesUSDTotal float64) {
market := productPerformanceExcelMarketName(marketKey)
if market == "" || market == "STOK" {
return
}
if salesUSD90 > 0 {
v.Markets90[market] = true
}
if salesUSDTotal > 0 {
v.MarketsTotal[market] = true
}
}
func (v *productPerformanceExcelVariant) totalQty() float64 {
if v.HasGeneralTotal {
return v.SalesQtyTotalGeneral
}
return v.SalesQtyTotalProduct
}
func (v *productPerformanceExcelVariant) totalUSD() float64 {
if v.HasGeneralTotal {
return v.SalesUSDTotalGeneral
}
return v.SalesUSDTotalProduct
}
func (v *productPerformanceExcelVariant) marketCount90() int {
if v.MarketFiltered {
return len(v.Markets90)
}
return productPerformanceExcelMaxInt(v.MarketCount90, len(v.Markets90))
}
func (v *productPerformanceExcelVariant) marketCountTotal() int {
if v.MarketFiltered {
return len(v.MarketsTotal)
}
return productPerformanceExcelMaxInt(v.MarketCountTotal, len(v.MarketsTotal))
}
func (v *productPerformanceExcelVariant) marketsLabel() string {
return productPerformanceExcelSortedSetLabel(v.MarketsTotal)
}
func buildProductPerformanceExcelFile(rows []*productPerformanceExcelVariant, stats productPerformanceExcelStats) (*excelize.File, error) {
f := excelize.NewFile()
sheet := "RenkYaka"
f.SetSheetName("Sheet1", sheet)
columns := productPerformanceExcelColumns()
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"1F4E78"}},
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
})
textStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 49})
intStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 3})
decimalStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 4})
percentStyle, _ := f.NewStyle(&excelize.Style{NumFmt: 10})
for i, col := range columns {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
_ = f.SetCellStr(sheet, cell, col.Header)
}
if len(columns) > 0 {
lastHeader, _ := excelize.CoordinatesToCellName(len(columns), 1)
_ = f.SetCellStyle(sheet, "A1", lastHeader, headerStyle)
}
for rowIndex, row := range rows {
excelRow := rowIndex + 2
for colIndex, col := range columns {
cell, _ := excelize.CoordinatesToCellName(colIndex+1, excelRow)
value := col.Value(row, stats)
if col.Kind == "text" {
_ = f.SetCellStr(sheet, cell, strings.TrimSpace(fmt.Sprint(value)))
} else {
_ = f.SetCellValue(sheet, cell, productPerformanceExcelFloat(value))
}
}
}
lastRow := productPerformanceExcelMaxInt(len(rows)+1, 2)
if len(columns) > 0 {
lastFilterCell, _ := excelize.CoordinatesToCellName(len(columns), lastRow)
_ = f.AutoFilter(sheet, "A1:"+lastFilterCell, []excelize.AutoFilterOptions{})
}
for i, col := range columns {
colName, _ := excelize.ColumnNumberToName(i + 1)
_ = f.SetColWidth(sheet, colName, colName, productPerformanceExcelColumnWidth(col.Header, col.Kind))
style := decimalStyle
switch col.Kind {
case "text":
style = textStyle
case "int":
style = intStyle
case "percent":
style = percentStyle
}
_ = f.SetCellStyle(sheet, fmt.Sprintf("%s2", colName), fmt.Sprintf("%s%d", colName, lastRow), style)
}
_ = f.SetPanes(sheet, &excelize.Panes{Freeze: true, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft"})
return f, nil
}
func productPerformanceExcelColumns() []productPerformanceExcelColumn {
base := []productPerformanceExcelColumn{
textExcelColumn("KPI Tarihi", func(v *productPerformanceExcelVariant) any { return v.KpiDate }),
textExcelColumn("Ürün İlk Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunIlkGrubu }),
textExcelColumn("Askılı/Yan", func(v *productPerformanceExcelVariant) any { return v.AskiliYan }),
textExcelColumn("Kategori", func(v *productPerformanceExcelVariant) any { return v.Kategori }),
textExcelColumn("Ürün Ana Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAnaGrubu }),
textExcelColumn("Ürün Alt Grubu", func(v *productPerformanceExcelVariant) any { return v.UrunAltGrubu }),
textExcelColumn("Ürün", func(v *productPerformanceExcelVariant) any { return v.ProductCode }),
textExcelColumn("Açıklama", func(v *productPerformanceExcelVariant) any { return v.ItemDescription }),
textExcelColumn("Renk", func(v *productPerformanceExcelVariant) any { return v.ColorCode }),
textExcelColumn("Renk Açıklama", func(v *productPerformanceExcelVariant) any { return v.ColorDescription }),
textExcelColumn("Yaka", func(v *productPerformanceExcelVariant) any { return v.YakaKodu }),
textExcelColumn("Renk/Yaka", func(v *productPerformanceExcelVariant) any {
return productPerformanceExcelColorYaka(v.ColorCode, v.ColorDescription, v.YakaKodu)
}),
textExcelColumn("Piyasalar", func(v *productPerformanceExcelVariant) any { return v.marketsLabel() }),
numberExcelColumn("Toplam Stok", "int", func(v *productPerformanceExcelVariant) any { return v.StockQty }),
}
base = append(base, productPerformanceExcelPeriodColumns("90G", "90d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.SalesQty90, v.SalesUSD90, v.marketCount90(), v.CustomerCount90
})...)
base = append(base, productPerformanceExcelPeriodColumns("180G", "180d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.SalesQty180, v.SalesUSD180, v.marketCount90(), v.CustomerCount90
})...)
base = append(base, productPerformanceExcelPeriodColumns("360G", "365d", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.SalesQty365, v.SalesUSD365, v.marketCount90(), v.CustomerCount90
})...)
base = append(base, productPerformanceExcelPeriodColumns("Genel", "total", func(v *productPerformanceExcelVariant) (float64, float64, int, int) {
return v.totalQty(), v.totalUSD(), v.marketCountTotal(), v.CustomerCountTotal
})...)
base = append(base,
textExcelColumn("Durum", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelStatus(v) }),
textExcelColumn("Öneri", func(v *productPerformanceExcelVariant) any { return productPerformanceExcelRecommendation(v) }),
textExcelColumn("Son Satış", func(v *productPerformanceExcelVariant) any { return v.LastSaleDate }),
textExcelColumn("Son Ref", func(v *productPerformanceExcelVariant) any { return v.LastRefNumber }),
textExcelColumn("Güncelleme", func(v *productPerformanceExcelVariant) any { return v.UpdatedAt }),
)
return base
}
func productPerformanceExcelPeriodColumns(label, suffix string, values func(*productPerformanceExcelVariant) (float64, float64, int, int)) []productPerformanceExcelColumn {
return []productPerformanceExcelColumn{
numberExcelColumn(label+" Toplam Adet", "int", func(v *productPerformanceExcelVariant) any {
qty, _, _, _ := values(v)
return qty
}),
numberExcelColumn(label+" Toplam Ciro USD", "number", func(v *productPerformanceExcelVariant) any {
_, usd, _, _ := values(v)
return usd
}),
numberExcelColumn(label+" Ort. Satış Fiyatı USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelAvgPrice(usd, qty)
}),
numberExcelColumn(label+" Yıllık Stok Devir", "number", func(v *productPerformanceExcelVariant) any {
qty, _, _, _ := values(v)
return productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), productPerformanceExcelPeriodDays(v, suffix))
}),
numberExcelColumn(label+" Taban Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.BasePriceUSD }),
numberExcelColumn(label+" Çıplak Maliyet USD", "number", func(v *productPerformanceExcelVariant) any { return v.CostPriceUSD }),
numberExcelColumn(label+" Taban Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelGrossProfit(usd, qty, v.BasePriceUSD)
}),
numberExcelColumn(label+" Çıplak Toplam K/Z USD", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelGrossProfit(usd, qty, v.CostPriceUSD)
}),
numberExcelColumn(label+" Taban Marj", "percent", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelMargin(usd, qty, v.BasePriceUSD)
}),
numberExcelColumn(label+" Çıplak Marj", "percent", func(v *productPerformanceExcelVariant) any {
qty, usd, _, _ := values(v)
return productPerformanceExcelMargin(usd, qty, v.CostPriceUSD)
}),
numberExcelColumn(label+" Tekil Piyasa", "int", func(v *productPerformanceExcelVariant) any {
_, _, markets, _ := values(v)
return markets
}),
numberExcelColumn(label+" Tekil Müşteri", "int", func(v *productPerformanceExcelVariant) any {
_, _, _, customers := values(v)
return customers
}),
numberExcelColumn(label+" Ürün Skor", "number", func(v *productPerformanceExcelVariant) any {
qty, usd, markets, customers := values(v)
days := productPerformanceExcelPeriodDays(v, suffix)
return productPerformanceExcelProductScore(suffix, usd, productPerformanceExcelStockTurnover(qty, productPerformanceExcelAvgStock(v, suffix), days), float64(markets), float64(customers), productPerformanceExcelMargin(usd, qty, v.CostPriceUSD), days)
}),
}
}
func textExcelColumn(header string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn {
return productPerformanceExcelColumn{Header: header, Kind: "text", Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }}
}
func numberExcelColumn(header, kind string, value func(*productPerformanceExcelVariant) any) productPerformanceExcelColumn {
return productPerformanceExcelColumn{Header: header, Kind: kind, Value: func(v *productPerformanceExcelVariant, _ productPerformanceExcelStats) any { return value(v) }}
}
func productPerformanceExcelProductRowMatchesFilters(row models.ProductPerformanceRow, filters map[string][]string) bool {
for field, selected := range productPerformanceExcelFilterSets(filters) {
if field == "performance_bucket" {
continue
}
if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelProductFilterCandidates(row, field)) {
return false
}
}
return true
}
func productPerformanceExcelGeneralRowMatchesFilters(row models.ProductPerformanceGeneralRow, filters map[string][]string) bool {
for field, selected := range productPerformanceExcelFilterSets(filters) {
if field == "performance_bucket" {
continue
}
if !productPerformanceExcelMatchesAny(selected, productPerformanceExcelGeneralFilterCandidates(row, field)) {
return false
}
}
return true
}
func productPerformanceExcelVariantMatchesPostFilters(row *productPerformanceExcelVariant, filters map[string][]string) bool {
selected := productPerformanceExcelFilterSets(filters)["performance_bucket"]
if len(selected) == 0 {
return true
}
return productPerformanceExcelMatchesAny(selected, []string{row.PerformanceBucket, productPerformanceExcelStatus(row)})
}
func productPerformanceExcelFilterSets(filters map[string][]string) map[string]map[string]bool {
out := map[string]map[string]bool{}
for field, values := range filters {
field = strings.TrimSpace(field)
for _, value := range values {
value = productPerformanceExcelNormalize(value)
if value == "" {
continue
}
if out[field] == nil {
out[field] = map[string]bool{}
}
out[field][value] = true
}
}
return out
}
func productPerformanceExcelHasFilter(filters map[string][]string, field string) bool {
for _, value := range filters[strings.TrimSpace(field)] {
if strings.TrimSpace(value) != "" {
return true
}
}
return false
}
func productPerformanceExcelProductFilterCandidates(row models.ProductPerformanceRow, field string) []string {
switch strings.TrimSpace(field) {
case "product_code":
return []string{row.ProductCode}
case "item_description":
return []string{row.ItemDescription}
case "color_code":
return []string{row.ColorCode}
case "yaka_kodu":
return []string{row.YakaKodu}
case "color_yaka":
return []string{productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu), row.ColorCode + "/" + row.YakaKodu}
case "kategori":
return []string{row.Kategori}
case "askili_yan":
return []string{productPerformanceExcelCleanOptional(row.AskiliYan)}
case "urun_ilk_grubu":
return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)}
case "urun_ana_grubu":
return []string{row.UrunAnaGrubu}
case "urun_alt_grubu":
return []string{row.UrunAltGrubu}
case "market_key":
return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)}
default:
return nil
}
}
func productPerformanceExcelGeneralFilterCandidates(row models.ProductPerformanceGeneralRow, field string) []string {
switch strings.TrimSpace(field) {
case "product_code":
return []string{row.ProductCode}
case "item_description":
return []string{row.ItemDescription}
case "color_code":
return []string{row.ColorCode}
case "yaka_kodu":
return []string{row.YakaKodu}
case "color_yaka":
return []string{productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu), row.ColorCode + "/" + row.YakaKodu}
case "kategori":
return []string{row.Kategori}
case "askili_yan":
return []string{productPerformanceExcelCleanOptional(row.AskiliYan)}
case "urun_ilk_grubu":
return []string{productPerformanceExcelCleanOptional(row.UrunIlkGrubu)}
case "urun_ana_grubu":
return []string{row.UrunAnaGrubu}
case "urun_alt_grubu":
return []string{row.UrunAltGrubu}
case "market_key":
return []string{row.MarketKey, productPerformanceExcelMarketName(row.MarketKey)}
default:
return nil
}
}
func productPerformanceExcelMatchesAny(selected map[string]bool, candidates []string) bool {
if len(selected) == 0 {
return true
}
for _, candidate := range candidates {
if selected[productPerformanceExcelNormalize(candidate)] {
return true
}
}
return false
}
func sortProductPerformanceExcelVariants(rows []*productPerformanceExcelVariant, sortBy string, desc bool) {
sortBy = strings.TrimSpace(sortBy)
if sortBy == "" || sortBy == "image" {
sortBy = "product_code"
desc = false
}
sort.SliceStable(rows, func(i, j int) bool {
cmp := productPerformanceExcelCompare(rows[i], rows[j], sortBy)
if cmp == 0 {
cmp = strings.Compare(productPerformanceExcelHierarchySortKey(rows[i]), productPerformanceExcelHierarchySortKey(rows[j]))
}
if desc {
return cmp > 0
}
return cmp < 0
})
}
func productPerformanceExcelCompare(a, b *productPerformanceExcelVariant, sortBy string) int {
if productPerformanceExcelIsNumericSort(sortBy) {
av := productPerformanceExcelSortNumber(a, sortBy)
bv := productPerformanceExcelSortNumber(b, sortBy)
if av < bv {
return -1
}
if av > bv {
return 1
}
return 0
}
return strings.Compare(productPerformanceExcelNormalize(productPerformanceExcelSortText(a, sortBy)), productPerformanceExcelNormalize(productPerformanceExcelSortText(b, sortBy)))
}
func productPerformanceExcelIsNumericSort(sortBy string) bool {
switch sortBy {
case "stock_qty", "sales_qty_90d", "sales_qty_180d", "sales_qty_365d", "sales_qty_total",
"sales_usd_90d", "sales_usd_180d", "sales_usd_365d", "sales_usd_total",
"avg_price_usd_90d", "avg_price_usd_180d", "avg_price_usd_365d", "avg_price_usd_total",
"stock_turnover_90d", "stock_turnover_180d", "stock_turnover_365d", "stock_turnover_total",
"base_price_usd", "cost_price_usd", "market_count_90d", "customer_count_90d", "market_count_total", "customer_count_total",
"performance_score", "performance_score_90d", "performance_score_total":
return true
default:
return false
}
}
func productPerformanceExcelSortNumber(row *productPerformanceExcelVariant, sortBy string) float64 {
switch sortBy {
case "stock_qty":
return row.StockQty
case "sales_qty_90d":
return row.SalesQty90
case "sales_qty_180d":
return row.SalesQty180
case "sales_qty_365d":
return row.SalesQty365
case "sales_qty_total":
return row.totalQty()
case "sales_usd_90d":
return row.SalesUSD90
case "sales_usd_180d":
return row.SalesUSD180
case "sales_usd_365d":
return row.SalesUSD365
case "sales_usd_total":
return row.totalUSD()
case "avg_price_usd_90d":
return productPerformanceExcelAvgPrice(row.SalesUSD90, row.SalesQty90)
case "avg_price_usd_180d":
return productPerformanceExcelAvgPrice(row.SalesUSD180, row.SalesQty180)
case "avg_price_usd_365d":
return productPerformanceExcelAvgPrice(row.SalesUSD365, row.SalesQty365)
case "avg_price_usd_total":
return productPerformanceExcelAvgPrice(row.totalUSD(), row.totalQty())
case "stock_turnover_90d":
return productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), productPerformanceExcelPeriodDays(row, "90d"))
case "stock_turnover_180d":
return productPerformanceExcelStockTurnover(row.SalesQty180, productPerformanceExcelAvgStock(row, "180d"), productPerformanceExcelPeriodDays(row, "180d"))
case "stock_turnover_365d":
return productPerformanceExcelStockTurnover(row.SalesQty365, productPerformanceExcelAvgStock(row, "365d"), productPerformanceExcelPeriodDays(row, "365d"))
case "stock_turnover_total":
return productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total"))
case "base_price_usd":
return row.BasePriceUSD
case "cost_price_usd":
return row.CostPriceUSD
case "market_count_90d":
return float64(row.marketCount90())
case "customer_count_90d":
return float64(row.CustomerCount90)
case "market_count_total":
return float64(row.marketCountTotal())
case "customer_count_total":
return float64(row.CustomerCountTotal)
case "performance_score", "performance_score_90d":
days := productPerformanceExcelPeriodDays(row, "90d")
return productPerformanceExcelProductScore("90d", row.SalesUSD90, productPerformanceExcelStockTurnover(row.SalesQty90, productPerformanceExcelAvgStock(row, "90d"), days), float64(row.marketCount90()), float64(row.CustomerCount90), productPerformanceExcelMargin(row.SalesUSD90, row.SalesQty90, row.CostPriceUSD), days)
case "performance_score_total":
days := productPerformanceExcelPeriodDays(row, "total")
return productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), days), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD), days)
default:
return 0
}
}
func productPerformanceExcelSortText(row *productPerformanceExcelVariant, sortBy string) string {
switch sortBy {
case "product_code":
return row.ProductCode
case "color_yaka":
return productPerformanceExcelColorYaka(row.ColorCode, row.ColorDescription, row.YakaKodu)
case "color_code":
return row.ColorCode
case "yaka_kodu":
return row.YakaKodu
case "item_description":
return row.ItemDescription
case "kategori":
return row.Kategori
case "askili_yan":
return row.AskiliYan
case "urun_ilk_grubu":
return row.UrunIlkGrubu
case "urun_ana_grubu":
return row.UrunAnaGrubu
case "urun_alt_grubu":
return row.UrunAltGrubu
case "market_key":
return row.marketsLabel()
case "performance_bucket":
return productPerformanceExcelStatus(row)
default:
return productPerformanceExcelHierarchySortKey(row)
}
}
func productPerformanceExcelStatsFor(rows []*productPerformanceExcelVariant) productPerformanceExcelStats {
var stats productPerformanceExcelStats
var c90, c180, c365, cTotal float64
for _, row := range rows {
if row.SalesUSD90 > 0 {
stats.AvgSalesUSD90 += row.SalesUSD90
c90++
}
if row.SalesUSD180 > 0 {
stats.AvgSalesUSD180 += row.SalesUSD180
c180++
}
if row.SalesUSD365 > 0 {
stats.AvgSalesUSD365 += row.SalesUSD365
c365++
}
if row.totalUSD() > 0 {
stats.AvgSalesUSDTotal += row.totalUSD()
cTotal++
}
}
if c90 > 0 {
stats.AvgSalesUSD90 /= c90
}
if c180 > 0 {
stats.AvgSalesUSD180 /= c180
}
if c365 > 0 {
stats.AvgSalesUSD365 /= c365
}
if cTotal > 0 {
stats.AvgSalesUSDTotal /= cTotal
}
return stats
}
func productPerformanceExcelVariantKey(productCode, colorCode, yakaKodu string) string {
return strings.ToUpper(strings.TrimSpace(productCode)) + "|" + strings.ToUpper(strings.TrimSpace(colorCode)) + "|" + strings.ToUpper(strings.TrimSpace(yakaKodu))
}
func productPerformanceExcelHierarchySortKey(row *productPerformanceExcelVariant) string {
return strings.Join([]string{row.UrunIlkGrubu, row.AskiliYan, row.Kategori, row.UrunAnaGrubu, row.UrunAltGrubu, row.ProductCode, row.ColorCode, row.YakaKodu}, "|")
}
func productPerformanceExcelColorYaka(colorCode, colorDescription, yakaKodu string) string {
color := strings.TrimSpace(colorCode)
if desc := strings.TrimSpace(colorDescription); color != "" && desc != "" {
color = color + "-" + strings.ToUpper(desc)
}
yaka := strings.TrimSpace(yakaKodu)
parts := make([]string, 0, 2)
if color != "" && color != "-" {
parts = append(parts, color)
}
if yaka != "" && yaka != "-" {
parts = append(parts, yaka)
}
return strings.Join(parts, "/")
}
func productPerformanceExcelMarketName(value string) string {
parts := strings.Split(value, "|")
for i := len(parts) - 1; i >= 0; i-- {
part := strings.TrimSpace(parts[i])
if part != "" {
return part
}
}
return ""
}
func productPerformanceExcelStatus(row *productPerformanceExcelVariant) string {
switch {
case row.totalQty() > 0 && row.totalUSD() <= 0:
return "Ciro/Fiyat Kontrol"
case row.totalQty() <= 0 && row.StockQty > 0:
return "Stok Riski"
case productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD) < 0:
return "Fiyat Baskısı"
case productPerformanceExcelProductScore("total", row.totalUSD(), productPerformanceExcelStockTurnover(row.totalQty(), productPerformanceExcelAvgStock(row, "total"), productPerformanceExcelPeriodDays(row, "total")), float64(row.marketCountTotal()), float64(row.CustomerCountTotal), productPerformanceExcelMargin(row.totalUSD(), row.totalQty(), row.CostPriceUSD), productPerformanceExcelPeriodDays(row, "total")) >= 70:
return "Yıldız Ürün"
default:
return productPerformanceExcelBucketLabel(row.PerformanceBucket)
}
}
func productPerformanceExcelRecommendation(row *productPerformanceExcelVariant) string {
switch {
case row.totalQty() > 0 && row.totalUSD() <= 0:
return "Satış adedi var ama USD ciro/fiyat yok. Satış tutarı aktarımı ve döviz dönüşümü kontrol edilmeli."
case row.totalQty() <= 0 && row.StockQty > 0:
return "Satış yok, stok duruyor. Piyasa/fiyat aksiyonu gerekli."
case row.Recommendation != "":
return row.Recommendation
default:
return "Düzenli takip."
}
}
func productPerformanceExcelBucketLabel(value string) string {
switch strings.TrimSpace(value) {
case "YILDIZ_URUN":
return "Yıldız Ürün"
case "STOKSUZ_TALEP":
return "Stoksuz Talep"
case "STOK_RISKI":
return "Stok Riski"
case "FIYAT_BASKISI":
return "Fiyat Baskısı"
case "FIYAT_FIRSATI":
return "Fiyat Fırsatı"
case "MALIYET_YOK":
return "Maliyet Yok"
case "TAKIP":
return "Takip"
default:
return strings.TrimSpace(value)
}
}
func productPerformanceExcelCleanOptional(value string) string {
value = strings.TrimSpace(value)
if value == "-" {
return ""
}
return value
}
func productPerformanceExcelNormalize(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func productPerformanceExcelFirstNonEmpty(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
return value
}
}
return ""
}
func productPerformanceExcelFirstNonZero(values ...float64) float64 {
for _, value := range values {
if value != 0 && !math.IsNaN(value) && !math.IsInf(value, 0) {
return value
}
}
return 0
}
func productPerformanceExcelNormalizeCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD {
return basePriceUSD, costPriceUSD
}
return costPriceUSD, basePriceUSD
}
func productPerformanceExcelAvgPrice(salesUSD, qty float64) float64 {
if qty <= 0 {
return 0
}
return salesUSD / qty
}
const productPerformanceExcelStockTurnoverYearDays = 360.0
func productPerformanceExcelAvgStock(row *productPerformanceExcelVariant, suffix string) float64 {
switch suffix {
case "90d":
return productPerformanceExcelFirstNonZero(row.AvgStock90, row.StockQty)
case "180d":
return productPerformanceExcelFirstNonZero(row.AvgStock180, row.StockQty)
case "365d":
return productPerformanceExcelFirstNonZero(row.AvgStock365, row.StockQty)
case "total":
return productPerformanceExcelFirstNonZero(row.AvgStockTotal, row.StockQty)
default:
return row.StockQty
}
}
func productPerformanceExcelPeriodDays(row *productPerformanceExcelVariant, suffix string) float64 {
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 360
case "total":
end, err := time.Parse("2006-01-02", strings.TrimSpace(row.KpiDate))
if err != nil {
return 0
}
start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
if end.Before(start) {
return 0
}
return end.Sub(start).Hours()/24 + 1
default:
return 0
}
}
func productPerformanceExcelStockTurnover(salesQty, avgStock, periodDays float64) float64 {
if salesQty <= 0 || avgStock <= 0 {
return 0
}
raw := salesQty / avgStock
if periodDays <= 0 {
return raw
}
return raw * productPerformanceExcelStockTurnoverYearDays / periodDays
}
func productPerformanceExcelGrossProfit(salesUSD, qty, unitCost float64) float64 {
if qty <= 0 {
return 0
}
return salesUSD - qty*unitCost
}
func productPerformanceExcelMargin(salesUSD, qty, unitCost float64) float64 {
if salesUSD <= 0 {
return 0
}
return productPerformanceExcelGrossProfit(salesUSD, qty, unitCost) / salesUSD
}
func productPerformanceExcelProductScore(suffix string, salesUSD, stockTurnover, marketCount, customerCount, margin float64, periodDays ...float64) float64 {
if salesUSD <= 0 {
return 1
}
days := productPerformanceExcelScorePeriodDays(suffix, periodDays...)
revenueScore := productPerformanceExcelRatioScore(salesUSD, productPerformanceExcelProductRevenueTargetForDays(suffix, days))
score := 0.30*productPerformanceExcelMarginScore(margin) +
0.20*productPerformanceExcelRatioScore(stockTurnover, 4) +
0.20*revenueScore +
0.05*productPerformanceExcelRatioScore(marketCount, productPerformanceExcelMarketSpreadTarget(suffix)) +
0.25*productPerformanceExcelRatioScore(customerCount, productPerformanceExcelCustomerSpreadTargetForDays(suffix, days))
return productPerformanceExcelRoundScore(score)
}
func productPerformanceExcelMarketSpreadTarget(suffix string) float64 {
switch suffix {
case "90d":
return 3
case "180d":
return 3
case "365d":
return 6
default:
return 6
}
}
func productPerformanceExcelCustomerSpreadTarget(suffix string) float64 {
return productPerformanceExcelCustomerSpreadTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix))
}
func productPerformanceExcelCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 {
switch suffix {
case "90d":
return 8
case "180d":
return 20
case "365d":
return 50
default:
return 20 * productPerformanceExcelTotalPeriodMultiplier(periodDays)
}
}
func productPerformanceExcelProductRevenueTarget(suffix string) float64 {
return productPerformanceExcelProductRevenueTargetForDays(suffix, productPerformanceExcelScorePeriodDays(suffix))
}
func productPerformanceExcelProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
switch suffix {
case "180d":
return 50000
case "365d":
return 100000
case "total":
return 50000 * productPerformanceExcelTotalPeriodMultiplier(periodDays)
default:
return 25000
}
}
func productPerformanceExcelScorePeriodDays(suffix string, periodDays ...float64) float64 {
if len(periodDays) > 0 && periodDays[0] > 0 {
return periodDays[0]
}
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 360
case "total":
start := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
now := time.Now()
if now.Before(start) {
return 0
}
return now.Sub(start).Hours()/24 + 1
default:
return 0
}
}
func productPerformanceExcelTotalPeriodMultiplier(periodDays float64) float64 {
if periodDays <= 0 {
periodDays = 180
}
return math.Max(1, periodDays/180.0)
}
func productPerformanceExcelMarginScore(margin float64) float64 {
if margin < 0 {
margin = 0
}
return productPerformanceExcelRatioScore(margin, 0.45)
}
func productPerformanceExcelRatioScore(value, target float64) float64 {
if target <= 0 || value <= 0 {
return 0
}
score := value * 100 / target
if score > 100 {
return 100
}
if score < 0 {
return 0
}
return score
}
func productPerformanceExcelRoundScore(score float64) float64 {
if score < 0 {
score = 0
}
if score > 100 {
score = 100
}
return math.Round(score*10000) / 10000
}
func productPerformanceExcelFloat(value any) float64 {
switch v := value.(type) {
case float64:
if math.IsNaN(v) || math.IsInf(v, 0) {
return 0
}
return v
case float32:
return productPerformanceExcelFloat(float64(v))
case int:
return float64(v)
case int64:
return float64(v)
case int32:
return float64(v)
case uint:
return float64(v)
case uint64:
return float64(v)
case uint32:
return float64(v)
default:
return 0
}
}
func productPerformanceExcelSortedSetLabel(values map[string]bool) string {
out := make([]string, 0, len(values))
for value := range values {
if strings.TrimSpace(value) != "" {
out = append(out, value)
}
}
sort.Strings(out)
return strings.Join(out, ", ")
}
func productPerformanceExcelColumnWidth(header, kind string) float64 {
if kind == "text" {
if strings.Contains(header, "Piyasalar") || strings.Contains(header, "Öneri") {
return 34
}
if len([]rune(header)) > 14 {
return 24
}
return 16
}
if strings.Contains(header, "Marj") || strings.Contains(header, "Skor") {
return 14
}
return 16
}
func productPerformanceExcelMaxInt(a, b int) int {
if a > b {
return a
}
return b
}
+1 -1
View File
@@ -160,7 +160,7 @@ SELECT TOP (400)
CONVERT(VARCHAR(8), p.ValidTime, 108) AS ValidTime, CONVERT(VARCHAR(8), p.ValidTime, 108) AS ValidTime,
CONVERT(VARCHAR(19), p.LastUpdatedDate, 120) AS LastUpdatedDate, CONVERT(VARCHAR(19), p.LastUpdatedDate, 120) AS LastUpdatedDate,
CAST(ISNULL(p.IsDisabled, 0) AS BIT) AS IsDisabled CAST(ISNULL(p.IsDisabled, 0) AS BIT) AS IsDisabled
FROM dbo.trPriceListLine p WITH(NOLOCK) FROM dbo.trPriceListLine p
WHERE p.ItemTypeCode = 1 WHERE p.ItemTypeCode = 1
AND LTRIM(RTRIM(p.ItemCode)) = @p1 AND LTRIM(RTRIM(p.ItemCode)) = @p1
AND ` + wherePG + ` AND ` + wherePG + `
+4 -4
View File
@@ -213,7 +213,7 @@ VALUES (
// Re-read header id. // Re-read header id.
err = tx.QueryRowContext(ctx, ` err = tx.QueryRowContext(ctx, `
SELECT TOP (1) CONVERT(NVARCHAR(36), PriceListHeaderID) SELECT TOP (1) CONVERT(NVARCHAR(36), PriceListHeaderID)
FROM dbo.trPriceListHeader WITH (NOLOCK) FROM dbo.trPriceListHeader
WHERE CompanyCode = 1 WHERE CompanyCode = 1
AND LTRIM(RTRIM(PriceGroupCode)) = @pg AND LTRIM(RTRIM(PriceGroupCode)) = @pg
AND LTRIM(RTRIM(DocCurrencyCode)) = @cur AND LTRIM(RTRIM(DocCurrencyCode)) = @cur
@@ -900,7 +900,7 @@ FROM (
PARTITION BY LTRIM(RTRIM(PriceGroupCode)), LTRIM(RTRIM(DocCurrencyCode)) PARTITION BY LTRIM(RTRIM(PriceGroupCode)), LTRIM(RTRIM(DocCurrencyCode))
ORDER BY ValidDate DESC, ValidTime DESC, LastUpdatedDate DESC ORDER BY ValidDate DESC, ValidTime DESC, LastUpdatedDate DESC
) AS rn ) AS rn
FROM dbo.trPriceListLine WITH(NOLOCK) FROM dbo.trPriceListLine
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND LTRIM(RTRIM(ItemCode)) = @ItemCode AND LTRIM(RTRIM(ItemCode)) = @ItemCode
AND ISNULL(IsDisabled, 0) = 0 AND ISNULL(IsDisabled, 0) = 0
@@ -970,7 +970,7 @@ WHERE rn = 1;
var latestPrice sql.NullFloat64 var latestPrice sql.NullFloat64
_ = msTx.QueryRowContext(ctx, ` _ = msTx.QueryRowContext(ctx, `
SELECT TOP (1) CAST(Price AS FLOAT) SELECT TOP (1) CAST(Price AS FLOAT)
FROM dbo.trPriceListLine WITH(NOLOCK) FROM dbo.trPriceListLine
WHERE ItemTypeCode = 1 WHERE ItemTypeCode = 1
AND LTRIM(RTRIM(ItemCode)) = @p1 AND LTRIM(RTRIM(ItemCode)) = @p1
AND LTRIM(RTRIM(DocCurrencyCode)) = @p2 AND LTRIM(RTRIM(DocCurrencyCode)) = @p2
@@ -988,7 +988,7 @@ ORDER BY ValidDate DESC, ValidTime DESC, LastUpdatedDate DESC;
if nextSort <= 0 { if nextSort <= 0 {
_ = msTx.QueryRowContext(ctx, ` _ = msTx.QueryRowContext(ctx, `
SELECT ISNULL(MAX(SortOrder), 0) + 1 SELECT ISNULL(MAX(SortOrder), 0) + 1
FROM dbo.trPriceListLine WITH(NOLOCK) FROM dbo.trPriceListLine
WHERE PriceListHeaderID = CONVERT(UNIQUEIDENTIFIER, @p1); WHERE PriceListHeaderID = CONVERT(UNIQUEIDENTIFIER, @p1);
`, sql.Named("p1", headerID)).Scan(&nextSort) `, sql.Named("p1", headerID)).Scan(&nextSort)
if nextSort <= 0 { if nextSort <= 0 {
+523 -32
View File
@@ -7,6 +7,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
@@ -106,11 +107,43 @@ func PostProductSeriesDefinitionHandler(pg *sql.DB) http.HandlerFunc {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel() defer cancel()
err := pg.QueryRowContext(ctx, ` err := pg.QueryRowContext(ctx, `
INSERT INTO dfgrp (code, title, is_active, typ, master, parent_filter, sort_order, is_required, notes) WITH updated AS (
VALUES ($1, $2, COALESCE($3, TRUE), 'opt', 'zbggseri', $4, $5, FALSE, $6) UPDATE dfgrp
RETURNING id SET title=$2,
is_active=COALESCE($3, TRUE),
parent_filter=$4,
sort_order=$5,
notes=$6
WHERE master='zbggseri' AND code=$1
RETURNING id
),
inserted AS (
INSERT INTO dfgrp (code, title, is_active, typ, master, parent_filter, sort_order, is_required, notes)
SELECT $1, $2, COALESCE($3, TRUE), 'opt', 'zbggseri', $4, $5, FALSE, $6
WHERE NOT EXISTS (SELECT 1 FROM updated)
RETURNING id
)
SELECT id FROM updated
UNION ALL
SELECT id FROM inserted
LIMIT 1
`, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes).Scan(&req.ID) `, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes).Scan(&req.ID)
if err != nil { if err != nil {
if isPostgresUniqueViolation(err, "ndx_dfgrp_uq4") {
if selErr := pg.QueryRowContext(ctx, `
UPDATE dfgrp
SET title=$2,
is_active=COALESCE($3, TRUE),
parent_filter=$4,
sort_order=$5,
notes=$6
WHERE master='zbggseri' AND code=$1
RETURNING id
`, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes).Scan(&req.ID); selErr == nil {
writeJSON(w, req)
return
}
}
http.Error(w, "Seri tanimi eklenemedi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Seri tanimi eklenemedi: "+err.Error(), http.StatusInternalServerError)
return return
} }
@@ -150,6 +183,10 @@ SET code=$2,
WHERE id=$1 AND master='zbggseri' WHERE id=$1 AND master='zbggseri'
`, id, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes) `, id, req.Code, req.Title, req.IsActive, req.ParentFilter, req.SortOrder, req.Notes)
if err != nil { if err != nil {
if isPostgresUniqueViolation(err, "ndx_dfgrp_uq4") {
http.Error(w, "Bu seri kodu zaten var. Mevcut seri satirini duzenleyin veya farkli kod kullanin.", http.StatusConflict)
return
}
http.Error(w, "Seri tanimi guncellenemedi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Seri tanimi guncellenemedi: "+err.Error(), http.StatusInternalServerError)
return return
} }
@@ -291,10 +328,50 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc {
defByID[d.ID] = d defByID[d.ID] = d
} }
// Strict token->dim_id resolver (no inference/persistence). UI should not invent dim mappings.
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
out := map[string]int64{}
if len(tokens) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT token, dim_id
FROM mk_dim_token_map
WHERE dim_column=$1 AND token = ANY($2)
`, column, pq.Array(tokens))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var tok string
var id int64
if err := rows.Scan(&tok, &id); err != nil {
return out, err
}
out[strings.TrimSpace(tok)] = id
}
return out, rows.Err()
}
// Authoritative dims/combos are resolved via mmitem_dim + dfgrp.code (see helpers below).
codes := setToSortedSlice(codeSet) codes := setToSortedSlice(codeSet)
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes) mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
dim1ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval1", setToSortedSlice(colorSet)) dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set)) mmitemIDs := make([]int64, 0, len(mmitemByCode))
for _, c := range codes {
if id := mmitemByCode[c]; id > 0 {
mmitemIDs = append(mmitemIDs, id)
}
}
// Normalize color tokens to UPPER/TRIM to match dfgrp.code.
colorTokens := setToSortedSlice(colorSet)
for i := range colorTokens {
colorTokens[i] = strings.ToUpper(strings.TrimSpace(colorTokens[i]))
}
dim1ByMmitemToken, _ := loadDim1ByMmitemAndToken(ctx, pg, mmitemIDs, colorTokens)
combos, _ := loadProductDimCombosFromMmitemDim(ctx, pg, mmitemIDs)
existing, _ := loadProductSeriesAssignments(ctx, pg, codes) existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
// Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans. // Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans.
@@ -309,7 +386,6 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc {
if v, ok := inferCache[key]; ok { if v, ok := inferCache[key]; ok {
return v return v
} }
// Use the same pattern approach as resolveDimvalFromFileNameToken, but scoped to src_id.
patterns := buildNameLikePatterns(tok) patterns := buildNameLikePatterns(tok)
if len(patterns) == 0 { if len(patterns) == 0 {
inferCache[key] = 0 inferCache[key] = 0
@@ -359,18 +435,29 @@ LIMIT 1
out := make([]productSeriesMappingRow, 0, len(grouped)) out := make([]productSeriesMappingRow, 0, len(grouped))
for _, row := range grouped { for _, row := range grouped {
row.MmitemID = mmitemByCode[row.ProductCode] row.MmitemID = mmitemByCode[row.ProductCode]
row.Dim1ID = dim1ByToken[row.ColorCode] row.Dim1ID = dim1ByMmitemToken[fmt.Sprintf("%d|%s", row.MmitemID, strings.ToUpper(strings.TrimSpace(row.ColorCode)))]
if row.Dim3Code != "" { if row.Dim3Code != "" {
// dimval3 tokens can be ambiguous globally; prefer per-mmitem inference. // Prefer token map; fallback to per-item inference (do not persist).
if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 { if v := dim3ByToken[row.Dim3Code]; v > 0 {
row.Dim3ID = v
} else if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 {
row.Dim3ID = inferred row.Dim3ID = inferred
} else {
row.Dim3ID = dim3ByToken[row.Dim3Code]
} }
} }
// Only show variants that are also known/valid on the PG side (product_code + dim1 + dim3_key).
// This prevents UI from listing stock variants that don't exist in PG's authoritative item-dim combos.
row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0) row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
dim3Key := int64(0)
if row.Dim3Code != "" {
dim3Key = row.Dim3ID
}
if row.MappingReady {
_, ok := combos[fmt.Sprintf("%d|%d|%d", row.MmitemID, row.Dim1ID, dim3Key)]
row.MappingReady = ok
}
if !row.MappingReady { if !row.MappingReady {
row.MappingWarning = "PG urun veya varyant token eslesmesi bulunamadi" // Skip: do not surface "orphan" variants in the UI.
continue
} }
assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID) assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID)
appendSeries := func(key string) { appendSeries := func(key string) {
@@ -382,11 +469,6 @@ LIMIT 1
} }
} }
appendSeries(assignKey) appendSeries(assignKey)
// Fallback: if we couldn't match dim3-specific assignments (dim3 token ambiguity / missing),
// show the default (dim3 NULL) assignments for the same product+color.
if len(row.SeriesIDs) == 0 && row.Dim3Code != "" && row.Dim1ID > 0 {
appendSeries(assignmentKey(row.ProductCode, row.Dim1ID, 0))
}
sort.Slice(row.Series, func(i, j int) bool { return row.Series[i].Code < row.Series[j].Code }) sort.Slice(row.Series, func(i, j int) bool { return row.Series[i].Code < row.Series[j].Code })
sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] }) sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] })
out = append(out, *row) out = append(out, *row)
@@ -415,6 +497,282 @@ LIMIT 1
} }
} }
func GetProductSeriesOrphanMappingsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
f := readStockAttrFilters(r)
limit := 0
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
limit = n
}
}
search := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("code"), r.URL.Query().Get("product_code"))
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
defer cancel()
msRows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery,
f.kategori,
f.urunAnaGrubu,
joinFilterValues(f.urunAltGrubu),
joinFilterValues(f.renk),
joinFilterValues(f.renk2),
joinFilterValues(f.urunIcerigi),
joinFilterValues(f.fit),
joinFilterValues(f.drop),
joinFilterValues(f.beden),
strconv.Itoa(limit),
search,
)
if err != nil {
http.Error(w, "Stok seri listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
defer msRows.Close()
grouped := map[string]*productSeriesMappingRow{}
codeSet := map[string]struct{}{}
colorSet := map[string]struct{}{}
dim3Set := map[string]struct{}{}
sizeSet := map[string]struct{}{}
for msRows.Next() {
var raw productSeriesStockRawRow
if err := msRows.Scan(
&raw.ProductCode,
&raw.ProductDescription,
&raw.ColorCode,
&raw.ColorTitle,
&raw.Dim3Code,
&raw.SizeCode,
&raw.Qty,
&raw.UrunAnaGrubu,
&raw.UrunAltGrubu,
&raw.Marka,
&raw.DropVal,
&raw.Fit,
&raw.UrunIcerigi,
&raw.Kategori,
); err != nil {
http.Error(w, "Stok satiri okunamadi: "+err.Error(), http.StatusInternalServerError)
return
}
raw.ProductCode = strings.TrimSpace(raw.ProductCode)
raw.ColorCode = strings.TrimSpace(raw.ColorCode)
raw.Dim3Code = strings.TrimSpace(raw.Dim3Code)
raw.SizeCode = strings.TrimSpace(raw.SizeCode)
key := productSeriesKey(raw.ProductCode, raw.ColorCode, raw.Dim3Code)
row := grouped[key]
if row == nil {
row = &productSeriesMappingRow{
RowKey: key,
ProductCode: raw.ProductCode,
ProductDescription: strings.TrimSpace(raw.ProductDescription),
ColorCode: raw.ColorCode,
ColorTitle: strings.TrimSpace(raw.ColorTitle),
Dim3Code: raw.Dim3Code,
UrunAnaGrubu: strings.TrimSpace(raw.UrunAnaGrubu),
UrunAltGrubu: strings.TrimSpace(raw.UrunAltGrubu),
Marka: strings.TrimSpace(raw.Marka),
DropVal: strings.TrimSpace(raw.DropVal),
Fit: strings.TrimSpace(raw.Fit),
UrunIcerigi: strings.TrimSpace(raw.UrunIcerigi),
Kategori: strings.TrimSpace(raw.Kategori),
SizeQty: map[string]float64{},
SeriesIDs: []int64{},
Series: []productSeriesDefinition{},
}
grouped[key] = row
codeSet[raw.ProductCode] = struct{}{}
colorSet[raw.ColorCode] = struct{}{}
if raw.Dim3Code != "" {
dim3Set[raw.Dim3Code] = struct{}{}
}
}
if raw.SizeCode != "" {
row.SizeQty[raw.SizeCode] = raw.Qty
sizeSet[raw.SizeCode] = struct{}{}
}
row.TotalQty += raw.Qty
}
if err := msRows.Err(); err != nil {
http.Error(w, "Stok satirlari okunamadi: "+err.Error(), http.StatusInternalServerError)
return
}
defs, err := listProductSeriesDefinitions(ctx, pg, true)
if err != nil {
http.Error(w, "Seri tanimlari alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
// Strict token map lookups (no inference).
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
out := map[string]int64{}
if len(tokens) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT token, dim_id
FROM mk_dim_token_map
WHERE dim_column=$1 AND token = ANY($2)
`, column, pq.Array(tokens))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var tok string
var id int64
if err := rows.Scan(&tok, &id); err != nil {
return out, err
}
out[strings.TrimSpace(tok)] = id
}
return out, rows.Err()
}
codes := setToSortedSlice(codeSet)
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
mmitemIDs := make([]int64, 0, len(mmitemByCode))
for _, c := range codes {
if id := mmitemByCode[c]; id > 0 {
mmitemIDs = append(mmitemIDs, id)
}
}
colorTokens := setToSortedSlice(colorSet)
for i := range colorTokens {
colorTokens[i] = strings.ToUpper(strings.TrimSpace(colorTokens[i]))
}
dim1ByMmitemToken, _ := loadDim1ByMmitemAndToken(ctx, pg, mmitemIDs, colorTokens)
combos, _ := loadProductDimCombosFromMmitemDim(ctx, pg, mmitemIDs)
// Per-request cache for per-mmitem dimval3 inference (dfblob scan).
inferCache := map[string]int64{}
inferDim3ForMmitem := func(mmitemID int64, token string) int64 {
mmitemID = int64(mmitemID)
tok := strings.ToUpper(normalizeDimParam(token))
if mmitemID <= 0 || tok == "" {
return 0
}
key := fmt.Sprintf("%d|%s", mmitemID, tok)
if v, ok := inferCache[key]; ok {
return v
}
patterns := buildNameLikePatterns(tok)
if len(patterns) == 0 {
inferCache[key] = 0
return 0
}
var dimv string
err := pg.QueryRowContext(ctx, `
SELECT x.dimv
FROM (
SELECT COALESCE(dimval3::text, '') AS dimv, COUNT(*) AS cnt
FROM dfblob
WHERE src_table='mmitem'
AND typ='img'
AND src_id=$7
AND COALESCE(dimval3::text, '') <> ''
AND (
UPPER(COALESCE(file_name,'')) LIKE $1 OR
UPPER(COALESCE(file_name,'')) LIKE $2 OR
UPPER(COALESCE(file_name,'')) LIKE $3 OR
UPPER(COALESCE(file_name,'')) LIKE $4 OR
UPPER(COALESCE(file_name,'')) LIKE $5 OR
UPPER(COALESCE(file_name,'')) LIKE $6
)
GROUP BY COALESCE(dimval3::text, '')
) x
ORDER BY x.cnt DESC, x.dimv
LIMIT 1
`, patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5], mmitemID).Scan(&dimv)
if err != nil {
inferCache[key] = 0
return 0
}
dimv = strings.TrimSpace(dimv)
if dimv == "" || dimv == "0" {
inferCache[key] = 0
return 0
}
id, err := strconv.ParseInt(dimv, 10, 64)
if err != nil || id <= 0 {
inferCache[key] = 0
return 0
}
inferCache[key] = id
return id
}
out := make([]productSeriesMappingRow, 0, len(grouped))
for _, row := range grouped {
if row.TotalQty <= 0 {
continue
}
row.MmitemID = mmitemByCode[row.ProductCode]
row.Dim1ID = dim1ByMmitemToken[fmt.Sprintf("%d|%s", row.MmitemID, strings.ToUpper(strings.TrimSpace(row.ColorCode)))]
if row.Dim3Code != "" {
if v := dim3ByToken[row.Dim3Code]; v > 0 {
row.Dim3ID = v
} else if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 {
row.Dim3ID = inferred
}
}
// Determine orphan reason.
baseReady := row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
dim3Key := int64(0)
if row.Dim3Code != "" {
dim3Key = row.Dim3ID
}
_, comboOK := combos[fmt.Sprintf("%d|%d|%d", row.MmitemID, row.Dim1ID, dim3Key)]
row.MappingReady = false
switch {
case row.MmitemID <= 0:
row.MappingWarning = "B2B'de urun yok"
case row.Dim1ID <= 0:
row.MappingWarning = "B2B'de bu urun icin renk yok"
case row.Dim3Code != "" && row.Dim3ID <= 0:
row.MappingWarning = "B2B'de ikinci renk eslesmesi yok"
case !comboOK:
row.MappingWarning = "B2B'de bu urun/renk/ikinci renk kombinasyonu yok"
default:
// Not an orphan; skip.
if baseReady && comboOK {
continue
}
row.MappingWarning = "B2B varyant eslesmesi bulunamadi"
}
out = append(out, *row)
}
productTotals := productSeriesTotalQtyByCode(out)
sort.Slice(out, func(i, j int) bool {
totalI := productTotals[out[i].ProductCode]
totalJ := productTotals[out[j].ProductCode]
if totalI != totalJ {
return totalI > totalJ
}
if out[i].ProductCode != out[j].ProductCode {
return out[i].ProductCode < out[j].ProductCode
}
if out[i].ColorCode != out[j].ColorCode {
return out[i].ColorCode < out[j].ColorCode
}
return out[i].Dim3Code < out[j].Dim3Code
})
writeJSON(w, productSeriesMappingsResponse{
Rows: out,
SizeColumns: sortSizeColumns(setToSortedSlice(sizeSet)),
Definitions: defs,
})
}
}
func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 { func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 {
out := make(map[string]float64, len(rows)) out := make(map[string]float64, len(rows))
for _, row := range rows { for _, row := range rows {
@@ -423,6 +781,68 @@ func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]floa
return out return out
} }
func loadDim1ByMmitemAndToken(ctx context.Context, pg *sql.DB, mmitemIDs []int64, colorTokens []string) (map[string]int64, error) {
out := map[string]int64{}
if len(mmitemIDs) == 0 || len(colorTokens) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT md.mmitem_id, UPPER(BTRIM(d.code)) AS code, md.val1
FROM mmitem_dim md
JOIN dfgrp d ON d.id = md.val1
WHERE md.mmitem_id = ANY($1)
AND md.is_active = TRUE
AND UPPER(BTRIM(d.code)) = ANY($2)
GROUP BY md.mmitem_id, UPPER(BTRIM(d.code)), md.val1
`, pq.Array(mmitemIDs), pq.Array(colorTokens))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var mmid, dim1 int64
var code string
if err := rows.Scan(&mmid, &code, &dim1); err != nil {
return out, err
}
key := fmt.Sprintf("%d|%s", mmid, strings.TrimSpace(code))
if _, ok := out[key]; !ok && dim1 > 0 {
out[key] = dim1
}
}
return out, rows.Err()
}
func loadProductDimCombosFromMmitemDim(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) {
out := map[string]struct{}{}
if len(mmitemIDs) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT
mmitem_id,
val1 AS dim1,
CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key
FROM mmitem_dim
WHERE mmitem_id = ANY($1)
AND is_active = TRUE
AND val1 IS NOT NULL
GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END
`, pq.Array(mmitemIDs))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var mmitemID, dim1, dim3Key int64
if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil {
return out, err
}
out[fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key)] = struct{}{}
}
return out, rows.Err()
}
type saveProductSeriesMappingsRequest struct { type saveProductSeriesMappingsRequest struct {
Items []struct { Items []struct {
ProductCode string `json:"product_code"` ProductCode string `json:"product_code"`
@@ -449,23 +869,58 @@ func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc {
} }
defer tx.Rollback() defer tx.Rollback()
// Tag the DB session for audit attribution (http handler vs scheduler).
// This affects only statements in this transaction.
_, _ = tx.ExecContext(ctx, `SELECT set_config('application_name', current_setting('application_name') || '|series-save', true)`)
// Safety defaults:
// - Don't allow accidental clearing when UI sends empty series_ids.
// - Don't allow saving fallback code=1 from UI unless explicitly enabled.
allowClear := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("clear"))) == "true"
allowFallback := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("allow_fallback"))) == "true"
log.Printf("[ProductSeriesMappingsSave] items=%d allow_clear=%t allow_fallback=%t remote=%s ua=%s",
len(req.Items), allowClear, allowFallback, strings.TrimSpace(r.RemoteAddr), strings.TrimSpace(r.UserAgent()))
var fallbackSeriesID int64 = 0
_ = tx.QueryRowContext(ctx, `SELECT id FROM dfgrp WHERE master='zbggseri' AND code='1'`).Scan(&fallbackSeriesID)
saved := 0 saved := 0
skipped := 0
for _, item := range req.Items { for _, item := range req.Items {
code := strings.TrimSpace(item.ProductCode) code := strings.TrimSpace(item.ProductCode)
color := strings.TrimSpace(item.ColorCode) color := strings.TrimSpace(item.ColorCode)
dim3Token := strings.TrimSpace(item.Dim3Code) dim3Token := strings.TrimSpace(item.Dim3Code)
if code == "" || color == "" { if code == "" || color == "" {
skipped++
continue continue
} }
mmitemID, err := resolveMmitemIDTx(ctx, tx, code) mmitemID, err := resolveMmitemIDTx(ctx, tx, code)
if err != nil || mmitemID <= 0 { if err != nil || mmitemID <= 0 {
http.Error(w, "PG urun bulunamadi: "+code, http.StatusBadRequest) http.Error(w, "B2B urun bulunamadi: "+code, http.StatusBadRequest)
return return
} }
dim1ID, err := resolveDimTokenIDTx(ctx, tx, "dimval1", color) // Authoritative dim1 resolver: only allow saving against a color that exists for this product in mmitem_dim.
if err != nil || dim1ID <= 0 { var dim1ID int64
http.Error(w, "Renk token eslesmesi bulunamadi: "+color, http.StatusBadRequest) {
return tok := strings.ToUpper(strings.TrimSpace(color))
if tok == "" || tok == "0" {
http.Error(w, "Renk token eslesmesi bulunamadi: "+color, http.StatusBadRequest)
return
}
if err := tx.QueryRowContext(ctx, `
SELECT md.val1
FROM mmitem_dim md
JOIN dfgrp d ON d.id = md.val1
WHERE md.mmitem_id = $1
AND md.is_active = TRUE
AND UPPER(BTRIM(d.code)) = $2
GROUP BY md.val1
LIMIT 1
`, mmitemID, tok).Scan(&dim1ID); err != nil || dim1ID <= 0 {
http.Error(w, "Renk bu urunde yok: "+color, http.StatusBadRequest)
return
}
} }
var dim3ID sql.NullInt64 var dim3ID sql.NullInt64
if dim3Token != "" { if dim3Token != "" {
@@ -477,6 +932,39 @@ func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc {
dim3ID = sql.NullInt64{Int64: id, Valid: true} dim3ID = sql.NullInt64{Int64: id, Valid: true}
} }
// Normalize/validate series ids.
seen := map[int64]struct{}{}
seriesIDs := make([]int64, 0, len(item.SeriesIDs))
for _, seriesID := range item.SeriesIDs {
if seriesID <= 0 {
continue
}
if fallbackSeriesID > 0 && seriesID == fallbackSeriesID && !allowFallback {
continue
}
if _, ok := seen[seriesID]; ok {
continue
}
seen[seriesID] = struct{}{}
seriesIDs = append(seriesIDs, seriesID)
}
// Prevent accidental destructive writes from UI.
if len(seriesIDs) == 0 && !allowClear {
log.Printf("[ProductSeriesMappingsSave] skip_empty_series_ids product=%s color=%s dim3=%s", code, color, dim3Token)
skipped++
continue
}
{
out := make([]string, 0, len(seriesIDs))
for _, id := range seriesIDs {
out = append(out, fmt.Sprintf("%d", id))
}
log.Printf("[ProductSeriesMappingsSave] write product=%s color=%s dim3=%s series_ids=%s",
code, color, dim3Token, strings.Join(out, ","))
}
if _, err := tx.ExecContext(ctx, ` if _, err := tx.ExecContext(ctx, `
DELETE FROM zbggseri DELETE FROM zbggseri
WHERE mmitem_id=$1 WHERE mmitem_id=$1
@@ -486,15 +974,7 @@ WHERE mmitem_id=$1
http.Error(w, "Seri eslesmesi temizlenemedi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Seri eslesmesi temizlenemedi: "+err.Error(), http.StatusInternalServerError)
return return
} }
seen := map[int64]struct{}{} for _, seriesID := range seriesIDs {
for _, seriesID := range item.SeriesIDs {
if seriesID <= 0 {
continue
}
if _, ok := seen[seriesID]; ok {
continue
}
seen[seriesID] = struct{}{}
if _, err := tx.ExecContext(ctx, ` if _, err := tx.ExecContext(ctx, `
INSERT INTO zbggseri (mmitem_id, dim1, seri_id, dim3) INSERT INTO zbggseri (mmitem_id, dim1, seri_id, dim3)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
@@ -510,7 +990,7 @@ VALUES ($1, $2, $3, $4)
http.Error(w, "Seri eslesmeleri kaydedilemedi: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Seri eslesmeleri kaydedilemedi: "+err.Error(), http.StatusInternalServerError)
return return
} }
writeJSON(w, map[string]any{"saved": saved}) writeJSON(w, map[string]any{"saved": saved, "skipped": skipped})
} }
} }
@@ -711,3 +1191,14 @@ func writeJSON(w http.ResponseWriter, payload any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(payload) _ = json.NewEncoder(w).Encode(payload)
} }
func isPostgresUniqueViolation(err error, constraint string) bool {
if err == nil {
return false
}
pqErr, ok := err.(*pq.Error)
if !ok {
return false
}
return string(pqErr.Code) == "23505" && (constraint == "" || pqErr.Constraint == constraint)
}
+55 -8
View File
@@ -158,6 +158,26 @@ func GetProductionNoCostProductsHandler(w http.ResponseWriter, r *http.Request)
return return
} }
if argeDB := db.GetArgeBaggiDB(); argeDB != nil && len(list) > 0 {
workOrders := make([]string, 0, len(list))
for i := range list {
workOrders = append(workOrders, list[i].UrtSiparisNo)
}
argeCtx, cancelArge := context.WithTimeout(r.Context(), 15*time.Second)
totalsByWorkOrder, argeErr := queries.GetArgeBaggiDepotEntryTotals(argeCtx, argeDB, workOrders)
cancelArge()
if argeErr != nil {
log.Printf("[ProductionNoCost] ArgeBAGGI enrichment error: %v", argeErr)
} else {
for i := range list {
totals := totalsByWorkOrder[strings.TrimSpace(list[i].UrtSiparisNo)]
list[i].CeketDepoGiris = totals.Ceket
list[i].PantolonDepoGiris = totals.Pantolon
list[i].YelekDepoGiris = totals.Yelek
}
}
}
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
} }
@@ -191,6 +211,7 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
var item models.ProductionHasCostProductRow var item models.ProductionHasCostProductRow
if err := rows.Scan( if err := rows.Scan(
&item.UretimSekli, &item.UretimSekli,
&item.UrtSiparisNo,
&item.NOnMLNo, &item.NOnMLNo,
&item.UrunKodu, &item.UrunKodu,
&item.UrunAdi, &item.UrunAdi,
@@ -219,6 +240,26 @@ func GetProductionHasCostProductsHandler(w http.ResponseWriter, r *http.Request)
return return
} }
if argeDB := db.GetArgeBaggiDB(); argeDB != nil && len(list) > 0 {
workOrders := make([]string, 0, len(list))
for i := range list {
workOrders = append(workOrders, list[i].UrtSiparisNo)
}
argeCtx, cancelArge := context.WithTimeout(r.Context(), 15*time.Second)
totalsByWorkOrder, argeErr := queries.GetArgeBaggiDepotEntryTotals(argeCtx, argeDB, workOrders)
cancelArge()
if argeErr != nil {
log.Printf("[ProductionHasCost] ArgeBAGGI enrichment error: %v", argeErr)
} else {
for i := range list {
totals := totalsByWorkOrder[strings.TrimSpace(list[i].UrtSiparisNo)]
list[i].CeketDepoGiris = totals.Ceket
list[i].PantolonDepoGiris = totals.Pantolon
list[i].YelekDepoGiris = totals.Yelek
}
}
}
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
} }
@@ -1427,6 +1468,12 @@ func postProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
totalUSD := 0.0 totalUSD := 0.0
totalEUR := 0.0 totalEUR := 0.0
for _, r := range req.Detail.Upserts { for _, r := range req.Detail.Upserts {
// The top header in the detail screen only includes rows marked
// "Maliyete Dahil". Persist that exact business total to the master
// record as well; excluded detail rows must not inflate the cost.
if r.MaliyeteDahil != 1 {
continue
}
qty := r.LMiktar qty := r.LMiktar
if qty < 0 { if qty < 0 {
qty = 0 qty = 0
@@ -1494,7 +1541,7 @@ func postProductionProductCostingOnMLSaveHandler(w http.ResponseWriter, r *http.
isUpdate := false isUpdate := false
{ {
var flag int var flag int
_ = tx.QueryRowContext(ctx, `SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtOnMLMas WITH (NOLOCK) WHERE nOnMLNo=@p1) THEN 1 ELSE 0 END`, nOnMLNo).Scan(&flag) _ = tx.QueryRowContext(ctx, `SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtOnMLMas WHERE nOnMLNo=@p1) THEN 1 ELSE 0 END`, nOnMLNo).Scan(&flag)
isUpdate = flag == 1 isUpdate = flag == 1
} }
@@ -1644,7 +1691,7 @@ SELECT
C.code, C.code,
ISNULL(( ISNULL((
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
FROM dbo.tbStok S WITH (NOLOCK) FROM dbo.tbStok S
WHERE ISNULL(S.IsBlocked, 0) = 0 WHERE ISNULL(S.IsBlocked, 0) = 0
AND ( AND (
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(C.code, ' ', '') REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(C.code, ' ', '')
@@ -1739,7 +1786,7 @@ FROM C
var mtID int var mtID int
err := tx.QueryRowContext(ctx, ` err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID SELECT TOP 1 ISNULL(MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru WITH (NOLOCK) FROM dbo.spUrtOnMLHammaddeTuru
WHERE nHammaddeTuruNo = @p1 WHERE nHammaddeTuruNo = @p1
`, row.NHammaddeTuruNo).Scan(&mtID) `, row.NHammaddeTuruNo).Scan(&mtID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
@@ -1834,7 +1881,7 @@ WHERE nHammaddeTuruNo = @p1
perRowCtx, cancel := context.WithTimeout(ctx, 2*time.Second) perRowCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
err := lookupDB.QueryRowContext(perRowCtx, ` err := lookupDB.QueryRowContext(perRowCtx, `
SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID SELECT TOP 1 ISNULL(CONVERT(int, ISNULL(S.nStokTipi, 0)), 0) AS nStokTipiID
FROM dbo.tbStok S WITH (NOLOCK) FROM dbo.tbStok S
WHERE ISNULL(S.IsBlocked, 0) = 0 WHERE ISNULL(S.IsBlocked, 0) = 0
AND ( AND (
REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '') REPLACE(LTRIM(RTRIM(ISNULL(S.sKodu,''))), ' ', '') = REPLACE(@p1, ' ', '')
@@ -2009,7 +2056,7 @@ WHEN NOT MATCHED THEN
nUrtUBolumID := 13 nUrtUBolumID := 13
_ = tx.QueryRowContext(ctx, ` _ = tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(CONVERT(int, nUrtUBolumID), 0) AS nUrtUBolumID SELECT TOP 1 ISNULL(CONVERT(int, nUrtUBolumID), 0) AS nUrtUBolumID
FROM dbo.spUrtRecMBolum WITH (NOLOCK) FROM dbo.spUrtRecMBolum
WHERE nUrtReceteID = @p1 WHERE nUrtReceteID = @p1
ORDER BY nUrtRecMBolumID ASC ORDER BY nUrtRecMBolumID ASC
`, req.Header.NUrtReceteID).Scan(&nUrtUBolumID) `, req.Header.NUrtReceteID).Scan(&nUrtUBolumID)
@@ -2024,7 +2071,7 @@ SELECT
ISNULL(CONVERT(int, nUrtMBolumID), 0) AS nUrtMBolumID, ISNULL(CONVERT(int, nUrtMBolumID), 0) AS nUrtMBolumID,
LTRIM(RTRIM(ISNULL(nHStokID_G,''))) AS nHStokID_G, LTRIM(RTRIM(ISNULL(nHStokID_G,''))) AS nHStokID_G,
ISNULL(CONVERT(float, lHMiktar_G), 0) AS lHMiktar_G ISNULL(CONVERT(float, lHMiktar_G), 0) AS lHMiktar_G
FROM dbo.spUrtRecMBolum WITH (NOLOCK) FROM dbo.spUrtRecMBolum
WHERE nUrtReceteID = @p1 WHERE nUrtReceteID = @p1
`, req.Header.NUrtReceteID); err == nil { `, req.Header.NUrtReceteID); err == nil {
for rows.Next() { for rows.Next() {
@@ -2090,7 +2137,7 @@ FROM dbo.spUrtRecMBolum WITH (UPDLOCK, HOLDLOCK)
// FK guard: only insert if nUrtMBolumID exists in spUrtMBolum. // FK guard: only insert if nUrtMBolumID exists in spUrtMBolum.
var bolumExists int var bolumExists int
if err := tx.QueryRowContext(ctx, ` if err := tx.QueryRowContext(ctx, `
SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtMBolum WITH (NOLOCK) WHERE nUrtMBolumID = @p1) THEN 1 ELSE 0 END SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.spUrtMBolum WHERE nUrtMBolumID = @p1) THEN 1 ELSE 0 END
`, k.nUrtMBolumID).Scan(&bolumExists); err != nil || bolumExists != 1 { `, k.nUrtMBolumID).Scan(&bolumExists); err != nil || bolumExists != 1 {
logger.Error("recipe insert blocked (missing spUrtMBolum FK)", "trace_id", traceID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err) logger.Error("recipe insert blocked (missing spUrtMBolum FK)", "trace_id", traceID, "n_urt_m_bolum_id", k.nUrtMBolumID, "s_kodu", k.sKodu, "err", err)
http.Error(w, "Recete insert engellendi (bolum FK yok)", http.StatusBadRequest) http.Error(w, "Recete insert engellendi (bolum FK yok)", http.StatusBadRequest)
@@ -2468,7 +2515,7 @@ func PostProductionProductCostingOnMLDeleteHandler(w http.ResponseWriter, r *htt
SELECT TOP 1 SELECT TOP 1
LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu, LTRIM(RTRIM(ISNULL(UrunKodu,''))) AS UrunKodu,
COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi COALESCE(Tarihi, dteKayitTarihi, GETDATE()) AS Tarihi
FROM dbo.spUrtOnMLMas WITH (NOLOCK) FROM dbo.spUrtOnMLMas
WHERE nOnMLNo = @p1 WHERE nOnMLNo = @p1
`, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi) `, req.NOnMLNo).Scan(&urunKodu, &maliyetTarihi)
if err != nil { if err != nil {
@@ -298,7 +298,7 @@ func sendCostingSummaryMail(
WITH X AS (SELECT CONVERT(int, V.id) AS id FROM (VALUES %s) AS V(id)) WITH X AS (SELECT CONVERT(int, V.id) AS id FROM (VALUES %s) AS V(id))
SELECT X.id, LTRIM(RTRIM(ISNULL(M.sAdi,''))) AS title SELECT X.id, LTRIM(RTRIM(ISNULL(M.sAdi,''))) AS title
FROM X FROM X
LEFT JOIN dbo.spUrtMTBolum M WITH (NOLOCK) LEFT JOIN dbo.spUrtMTBolum M
ON M.nUrtMTBolumID = X.id ON M.nUrtMTBolumID = X.id
`, strings.Join(vals, ",")) `, strings.Join(vals, ","))
rows, err := uretim.QueryContext(ctx, q, args...) rows, err := uretim.QueryContext(ctx, q, args...)
+122 -4
View File
@@ -134,6 +134,12 @@ type TranslateSelectedPayload struct {
Limit int `json:"limit"` Limit int `json:"limit"`
} }
type RuntimeTranslationsResponse struct {
Lang string `json:"lang"`
ByKey map[string]string `json:"by_key"`
ByText map[string]string `json:"by_text"`
}
type BulkUpdateItem struct { type BulkUpdateItem struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SourceTextTR *string `json:"source_text_tr"` SourceTextTR *string `json:"source_text_tr"`
@@ -296,6 +302,92 @@ ORDER BY t_key, lang_code
} }
} }
func GetRuntimeTranslationsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
lang := normalizeRuntimeTranslationLang(firstNonEmpty(
r.URL.Query().Get("lang"),
r.Header.Get("Accept-Language"),
))
if lang == "" {
lang = "tr"
}
resp := RuntimeTranslationsResponse{
Lang: lang,
ByKey: map[string]string{},
ByText: map[string]string{},
}
rows, err := db.Query(`
WITH base AS (
SELECT DISTINCT ON (t_key)
t_key,
COALESCE(NULLIF(source_text_tr, ''), translated_text, '') AS source_text_tr
FROM mk_translator
WHERE COALESCE(NULLIF(source_text_tr, ''), translated_text, '') <> ''
AND status = 'approved'
AND (
COALESCE(NULLIF(provider_meta->>'source_type', ''), 'dummy') <> 'dummy'
OR is_manual = true
)
AND char_length(COALESCE(NULLIF(source_text_tr, ''), translated_text, '')) <= 180
AND COALESCE(NULLIF(source_text_tr, ''), translated_text, '') !~ '[<>{}\[\]]'
AND position(chr(96) in COALESCE(NULLIF(source_text_tr, ''), translated_text, '')) = 0
AND COALESCE(NULLIF(source_text_tr, ''), translated_text, '') !~ '(=>|\|\||&&|\?\?|/\*|\*/|\.trim\(|\.replace\(|\.map\(|\.filter\()'
ORDER BY t_key, CASE WHEN lang_code='tr' THEN 0 ELSE 1 END, updated_at DESC
),
target AS (
SELECT DISTINCT ON (t_key)
t_key,
COALESCE(translated_text, '') AS translated_text
FROM mk_translator
WHERE lang_code=$1
AND (status = 'approved' OR is_manual = true)
ORDER BY t_key, is_manual DESC, updated_at DESC
)
SELECT
base.t_key,
base.source_text_tr,
CASE
WHEN $1 = 'tr' THEN base.source_text_tr
ELSE COALESCE(NULLIF(target.translated_text, ''), base.source_text_tr)
END AS display_text
FROM base
LEFT JOIN target ON target.t_key = base.t_key
`, lang)
if err != nil {
http.Error(w, "runtime translations query error", http.StatusInternalServerError)
return
}
defer rows.Close()
for rows.Next() {
var key, sourceText, displayText string
if err := rows.Scan(&key, &sourceText, &displayText); err != nil {
http.Error(w, "runtime translations scan error", http.StatusInternalServerError)
return
}
key = strings.TrimSpace(key)
sourceText = normalizeRuntimeTranslationText(sourceText)
displayText = normalizeRuntimeTranslationText(displayText)
if key != "" && displayText != "" {
resp.ByKey[key] = displayText
}
if sourceText != "" && displayText != "" {
resp.ByText[sourceText] = displayText
}
}
if err := rows.Err(); err != nil {
http.Error(w, "runtime translations rows error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(resp)
}
}
func UpdateTranslationRowHandler(db *sql.DB) http.HandlerFunc { func UpdateTranslationRowHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -911,10 +1003,12 @@ func collectSourceSeeds(pgDB *sql.DB, mssqlDB *sql.DB, limit int) []sourceSeed {
return out return out
} }
} }
for _, row := range collectDummySeeds(limit - len(out)) { if strings.EqualFold(strings.TrimSpace(os.Getenv("TRANSLATION_DUMMY_SEEDS_ENABLED")), "true") {
appendSeed(row) for _, row := range collectDummySeeds(limit - len(out)) {
if len(out) >= limit { appendSeed(row)
return out if len(out) >= limit {
return out
}
} }
} }
@@ -1667,6 +1761,30 @@ func normalizeTranslationLang(v string) string {
return "" return ""
} }
func normalizeRuntimeTranslationLang(v string) string {
raw := strings.ToLower(strings.TrimSpace(v))
if raw == "" {
return ""
}
if strings.Contains(raw, ",") {
raw = strings.TrimSpace(strings.Split(raw, ",")[0])
}
if strings.Contains(raw, ";") {
raw = strings.TrimSpace(strings.Split(raw, ";")[0])
}
if strings.Contains(raw, "-") {
raw = strings.TrimSpace(strings.Split(raw, "-")[0])
}
if strings.Contains(raw, "_") {
raw = strings.TrimSpace(strings.Split(raw, "_")[0])
}
return normalizeTranslationLang(raw)
}
func normalizeRuntimeTranslationText(v string) string {
return strings.Join(strings.Fields(strings.TrimSpace(v)), " ")
}
func normalizeTranslationStatus(v string) string { func normalizeTranslationStatus(v string) string {
status := strings.ToLower(strings.TrimSpace(v)) status := strings.ToLower(strings.TrimSpace(v))
if _, ok := translationStatusSet[status]; ok { if _, ok := translationStatusSet[status]; ok {
+15 -1
View File
@@ -1,8 +1,20 @@
// quasar.config.js // quasar.config.js
import { defineConfig } from '#q-app/wrappers' import { defineConfig } from '#q-app/wrappers'
import { execSync } from 'node:child_process'
function readGitBuildInfo () {
try {
const commitCount = execSync('git rev-list --count HEAD', { encoding: 'utf8' }).trim()
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()
return { version: `v1.0.${commitCount}`, commit: commitHash }
} catch {
return { version: 'v1.0.0', commit: 'local' }
}
}
export default defineConfig(() => { export default defineConfig(() => {
const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim() const apiBaseUrl = (process.env.VITE_API_BASE_URL || '/api').trim()
const gitBuildInfo = readGitBuildInfo()
return { return {
@@ -36,7 +48,9 @@ export default defineConfig(() => {
build: { build: {
vueRouterMode: 'hash', vueRouterMode: 'hash',
env: { env: {
VITE_API_BASE_URL: apiBaseUrl VITE_API_BASE_URL: apiBaseUrl,
APP_VERSION: gitBuildInfo.version,
APP_COMMIT: gitBuildInfo.commit
}, },
esbuildTarget: { esbuildTarget: {
+4 -1
View File
@@ -1,7 +1,10 @@
import { boot } from 'quasar/wrappers' import { boot } from 'quasar/wrappers'
import { useLocaleStore } from 'src/stores/localeStore' import { useLocaleStore } from 'src/stores/localeStore'
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'
export default boot(() => { export default boot(async () => {
const localeStore = useLocaleStore() const localeStore = useLocaleStore()
localeStore.setLocale(localeStore.locale) localeStore.setLocale(localeStore.locale)
const runtimeTranslations = useRuntimeTranslationStore()
await runtimeTranslations.loadLocale(localeStore.locale)
}) })
+7
View File
@@ -3,6 +3,7 @@ import { computed } from 'vue'
import { messages } from 'src/i18n/messages' import { messages } from 'src/i18n/messages'
import { DEFAULT_LOCALE } from 'src/i18n/languages' import { DEFAULT_LOCALE } from 'src/i18n/languages'
import { useLocaleStore } from 'src/stores/localeStore' import { useLocaleStore } from 'src/stores/localeStore'
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'
function lookup(obj, path) { function lookup(obj, path) {
return String(path || '') return String(path || '')
@@ -13,6 +14,7 @@ function lookup(obj, path) {
export function useI18n() { export function useI18n() {
const localeStore = useLocaleStore() const localeStore = useLocaleStore()
const runtimeTranslations = useRuntimeTranslationStore()
const currentLocale = computed(() => localeStore.locale) const currentLocale = computed(() => localeStore.locale)
@@ -24,6 +26,11 @@ export function useI18n() {
} }
function t(key) { function t(key) {
const runtimeValue = runtimeTranslations.translateKey(key)
if (runtimeValue && runtimeValue !== key) {
return runtimeValue
}
for (const locale of fallbackLocales(currentLocale.value)) { for (const locale of fallbackLocales(currentLocale.value)) {
const val = lookup(messages[locale] || {}, key) const val = lookup(messages[locale] || {}, key)
if (val != null) return val if (val != null) return val
@@ -0,0 +1,157 @@
import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { DEFAULT_LOCALE } from 'src/i18n/languages'
import { useLocaleStore } from 'src/stores/localeStore'
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'
const textOriginals = new WeakMap()
const attrOriginals = new WeakMap()
const TRANSLATABLE_ATTRS = ['placeholder', 'title', 'aria-label']
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'])
function normalizeRuntimeText (value) {
return String(value || '').trim().replace(/\s+/g, ' ')
}
function preserveOuterWhitespace (original, translated) {
const text = String(original || '')
const leading = text.match(/^\s*/)?.[0] || ''
const trailing = text.match(/\s*$/)?.[0] || ''
return `${leading}${translated}${trailing}`
}
function shouldSkipNode (node) {
const parent = node?.parentElement
if (!parent) return true
if (SKIP_TAGS.has(parent.tagName)) return true
if (parent.closest?.('[data-no-runtime-i18n="true"]')) return true
return false
}
export function useRuntimeDomTranslations () {
const route = useRoute()
const localeStore = useLocaleStore()
const runtimeTranslations = useRuntimeTranslationStore()
let observer = null
let applyTimer = null
function translateTextNode (node) {
if (shouldSkipNode(node)) return
const current = String(node.nodeValue || '')
if (!normalizeRuntimeText(current)) return
if (!textOriginals.has(node)) {
textOriginals.set(node, current)
}
const original = textOriginals.get(node)
const key = normalizeRuntimeText(original)
const translated = localeStore.locale === DEFAULT_LOCALE
? key
: runtimeTranslations.byText[key]
const nextValue = preserveOuterWhitespace(original, translated || key)
if (node.nodeValue !== nextValue) {
node.nodeValue = nextValue
}
}
function translateElementAttrs (el) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) return
if (el.closest?.('[data-no-runtime-i18n="true"]')) return
let originals = attrOriginals.get(el)
if (!originals) {
originals = {}
attrOriginals.set(el, originals)
}
for (const attr of TRANSLATABLE_ATTRS) {
if (!el.hasAttribute(attr)) continue
const current = el.getAttribute(attr)
if (!normalizeRuntimeText(current)) continue
if (!Object.prototype.hasOwnProperty.call(originals, attr)) {
originals[attr] = current
}
const key = normalizeRuntimeText(originals[attr])
const translated = localeStore.locale === DEFAULT_LOCALE
? key
: runtimeTranslations.byText[key]
const nextValue = translated || key
if (el.getAttribute(attr) !== nextValue) {
el.setAttribute(attr, nextValue)
}
}
}
function translateRoot (root = document.body) {
if (typeof document === 'undefined' || !root) return
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
{
acceptNode (node) {
if (node.nodeType === Node.TEXT_NODE) {
return normalizeRuntimeText(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
}
return NodeFilter.FILTER_ACCEPT
}
}
)
let node = walker.currentNode
while (node) {
if (node.nodeType === Node.TEXT_NODE) {
translateTextNode(node)
} else if (node.nodeType === Node.ELEMENT_NODE) {
translateElementAttrs(node)
}
node = walker.nextNode()
}
}
function scheduleApply () {
if (applyTimer) clearTimeout(applyTimer)
applyTimer = setTimeout(async () => {
applyTimer = null
await nextTick()
translateRoot(document.body)
}, 30)
}
onMounted(async () => {
await runtimeTranslations.loadLocale(localeStore.locale)
scheduleApply()
observer = new MutationObserver(() => scheduleApply())
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: TRANSLATABLE_ATTRS
})
})
watch(
() => localeStore.locale,
async (locale) => {
await runtimeTranslations.loadLocale(locale)
scheduleApply()
}
)
watch(
() => route.fullPath,
() => scheduleApply()
)
watch(
() => runtimeTranslations.version,
() => scheduleApply()
)
onBeforeUnmount(() => {
if (observer) observer.disconnect()
if (applyTimer) clearTimeout(applyTimer)
})
}
+13 -1
View File
@@ -171,6 +171,7 @@ import { Dialog, useQuasar } from 'quasar'
import { useAuthStore } from 'stores/authStore' import { useAuthStore } from 'stores/authStore'
import { usePermissionStore } from 'stores/permissionStore' import { usePermissionStore } from 'stores/permissionStore'
import { useI18n } from 'src/composables/useI18n' import { useI18n } from 'src/composables/useI18n'
import { useRuntimeDomTranslations } from 'src/composables/useRuntimeDomTranslations'
import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages' import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages'
import { useLocaleStore } from 'src/stores/localeStore' import { useLocaleStore } from 'src/stores/localeStore'
import { activityLogsMenuItem, getAuthUserId } from 'src/modules/activityLogs' import { activityLogsMenuItem, getAuthUserId } from 'src/modules/activityLogs'
@@ -185,6 +186,7 @@ const auth = useAuthStore()
const perm = usePermissionStore() const perm = usePermissionStore()
const localeStore = useLocaleStore() const localeStore = useLocaleStore()
const { t } = useI18n() const { t } = useI18n()
useRuntimeDomTranslations()
const languageOptions = UI_LANGUAGE_OPTIONS const languageOptions = UI_LANGUAGE_OPTIONS
const selectedLocale = computed({ const selectedLocale = computed({
@@ -354,7 +356,7 @@ const menuItems = [
}, },
{ {
label: 'Ürün Fiyatlandırma', label: 'Ürün Fiyatlandırma ve Analiz',
icon: 'sell', icon: 'sell',
children: [ children: [
@@ -363,6 +365,11 @@ const menuItems = [
to: '/app/pricing/product-pricing', to: '/app/pricing/product-pricing',
permission: 'pricing:view' permission: 'pricing:view'
}, },
{
label: 'Ürün Performans ve Karlılık Analizi',
to: '/app/pricing/product-performance-profitability',
permission: 'pricing:view'
},
{ {
label: 'Toptan Kampanya Yönetimi', label: 'Toptan Kampanya Yönetimi',
to: '/app/pricing/wholesale-campaigns', to: '/app/pricing/wholesale-campaigns',
@@ -373,6 +380,11 @@ const menuItems = [
to: '/app/pricing/product-series-mappings', to: '/app/pricing/product-series-mappings',
permission: 'pricing:view' permission: 'pricing:view'
}, },
{
label: 'B2B Olmayan Stok',
to: '/app/pricing/product-series-mappings-orphans',
permission: 'pricing:view'
},
{ {
label: 'Ürün Seri Tanımlamaları', label: 'Ürün Seri Tanımlamaları',
to: '/app/pricing/product-series-definitions', to: '/app/pricing/product-series-definitions',
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="col-12 col-md"> <div class="col-12 col-md">
<div class="text-h6">Marka Sınıflandırma</div> <div class="text-h6">Marka Sınıflandırma</div>
<div class="text-caption text-grey-7"> <div class="text-caption text-grey-7">
Kaynak: BAGGI_V3 `cdItemAttribute` (ItemTypeCode=1, AttributeTypeCode=10) Kaynak: NEBIM_V3 marka bilgileri
</div> </div>
</div> </div>
+21
View File
@@ -74,6 +74,10 @@
</q-form> </q-form>
</q-card> </q-card>
<div class="login-version" :title="`Commit: ${appCommit}`">
Baggi BSS {{ appVersion }}
</div>
<!-- 🔐 FORGOT PASSWORD --> <!-- 🔐 FORGOT PASSWORD -->
<q-dialog v-model="forgotOpen" persistent> <q-dialog v-model="forgotOpen" persistent>
<q-card style="width:420px; max-width:90vw"> <q-card style="width:420px; max-width:90vw">
@@ -129,6 +133,8 @@ import api from 'src/services/api'
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const $q = useQuasar() const $q = useQuasar()
const appVersion = process.env.APP_VERSION || 'v1.0.0'
const appCommit = process.env.APP_COMMIT || 'local'
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
@@ -266,6 +272,21 @@ async function sendResetMail () {
.full-width { .full-width {
width: 100%; width: 100%;
} }
.login-version {
position: absolute;
left: 16px;
bottom: 12px;
z-index: 2;
padding: 4px 8px;
border-radius: 6px;
color: rgba(45, 45, 45, 0.75);
background: rgba(255, 255, 255, 0.68);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.2px;
user-select: text;
}
/* =============================== /* ===============================
LOGIN INPUT TEXT COLOR (GOLD) LOGIN INPUT TEXT COLOR (GOLD)
=============================== */ =============================== */
+42 -35
View File
@@ -1222,49 +1222,58 @@ async function reloadData ({ page = 1 } = {}) {
} }
async function loadImagesForRows (list) { async function loadImagesForRows (list) {
const targets = [] const items = []
const rowByKey = new Map()
const seen = new Set() const seen = new Set()
for (const row of list) { for (const row of list) {
const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}` const key = `${row.productCode}|${row.dim1 || 0}|${row.dim3 || 0}`
if (!row.productCode || seen.has(key)) continue if (!row.productCode || seen.has(key)) continue
seen.add(key) seen.add(key)
targets.push({ row, key })
}
const concurrency = 12
let cursor = 0
let loaded = 0
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
for (;;) {
const target = targets[cursor]
cursor += 1
if (!target) return
const { row, key } = target
if (imageCache.has(key)) { if (imageCache.has(key)) {
row.imageUrl = imageCache.get(key) row.imageUrl = imageCache.get(key)
continue continue
} }
rowByKey.set(key, row)
items.push({
key,
code: row.productCode,
dim1: row.dim1 || '',
dim3: row.dim3 || ''
})
}
if (!items.length) {
rows.value = [...rows.value]
return
}
const chunks = []
for (let i = 0; i < items.length; i += 250) chunks.push(items.slice(i, i + 250))
for (const chunk of chunks) {
try { try {
const res = await api.get('/product-images', { const res = await api.post('/product-images/batch', { items: chunk }, { timeout: 30000 })
params: { const returned = new Set()
code: row.productCode, for (const item of Array.isArray(res?.data) ? res.data : []) {
dim1_id: row.dim1 || '', const key = toText(item?.key)
dim3_id: row.dim3 || '' if (!key) continue
}, returned.add(key)
timeout: 15000 const list = Array.isArray(item.images) ? item.images : []
}) const url = resolveProductImageUrl(list[0])
const first = Array.isArray(res?.data) ? res.data[0] : null imageCache.set(key, url)
const url = resolveProductImageUrl(first) imageListCache.set(key, list)
imageCache.set(key, url) const row = rowByKey.get(key)
row.imageUrl = url if (row) row.imageUrl = url
imageListCache.set(key, Array.isArray(res?.data) ? res.data : []) }
for (const item of chunk) {
if (!returned.has(item.key)) {
imageCache.set(item.key, '')
imageListCache.set(item.key, [])
}
}
} catch { } catch {
imageCache.set(key, '') for (const item of chunk) imageCache.set(item.key, '')
} }
loaded += 1 rows.value = [...rows.value]
if (loaded % 12 === 0) rows.value = [...rows.value] }
}
})
await Promise.all(workers)
rows.value = [...rows.value] rows.value = [...rows.value]
} }
@@ -1280,14 +1289,12 @@ function normalizeUploadsPath (storagePath) {
function resolveProductImageUrl (item) { function resolveProductImageUrl (item) {
if (!item || typeof item !== 'object') return '' if (!item || typeof item !== 'object') return ''
const imageId = Number(item.id || item.ID || 0) const contentUrl = toText(item.content_url || item.ContentURL)
if (Number.isFinite(imageId) && imageId > 0) return `/api/product-images/${imageId}/content` if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
const thumbUrl = toText(item.thumb_url || item.thumbUrl) const thumbUrl = toText(item.thumb_url || item.thumbUrl)
if (thumbUrl) return thumbUrl if (thumbUrl) return thumbUrl
const fullUrl = toText(item.full_url || item.fullUrl) const fullUrl = toText(item.full_url || item.fullUrl)
if (fullUrl) return fullUrl if (fullUrl) return fullUrl
const contentUrl = toText(item.content_url || item.ContentURL)
if (contentUrl) return contentUrl.startsWith('/api/') ? contentUrl : contentUrl
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage) const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage)
if (uploadsPath) return uploadsPath if (uploadsPath) return uploadsPath
const fileName = toText(item.file_name || item.FileName) const fileName = toText(item.file_name || item.FileName)
+2 -2
View File
@@ -371,7 +371,7 @@ const importKeyFieldLabels = [
const importFieldMap = { const importFieldMap = {
AKTIF: 'is_active', AKTIF: 'is_active',
'HESAP AKTIF': 'calc_enabled', 'HESAP AKTIF': 'calc_enabled',
'PG YAYIN': 'publish_postgres', 'B2B YAYIN': 'publish_postgres',
'NEBIM YAYIN': 'publish_nebim', 'NEBIM YAYIN': 'publish_nebim',
'TRY TOPTAN YUVARLAMA': 'try_wholesale_step', 'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
'TRY PERAKENDE MODU': 'try_retail_mode', 'TRY PERAKENDE MODU': 'try_retail_mode',
@@ -454,7 +454,7 @@ const columns = [
col('brand_group', 'MARKA GRUBU', 'brand_group', 76, { classes: 'ps-col', headerClasses: 'ps-col' }), col('brand_group', 'MARKA GRUBU', 'brand_group', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
col('anchor_mode', 'ANCHOR MODE', 'anchor_mode', 66, { classes: 'ps-col', headerClasses: 'ps-col' }), col('anchor_mode', 'ANCHOR MODE', 'anchor_mode', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
col('calc_enabled', 'HESAP AKTIF', 'calc_enabled', 66, { classes: 'ps-col', headerClasses: 'ps-col' }), col('calc_enabled', 'HESAP AKTIF', 'calc_enabled', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
col('publish_postgres', 'PG YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }), col('publish_postgres', 'B2B YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
col('publish_nebim', 'NEBIM YAYIN', 'publish_nebim', 66, { classes: 'ps-col', headerClasses: 'ps-col' }), col('publish_nebim', 'NEBIM YAYIN', 'publish_nebim', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }), col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
File diff suppressed because it is too large Load Diff
+66 -11
View File
@@ -71,6 +71,31 @@
</q-btn> </q-btn>
</div> </div>
</div> </div>
<div class="col-fixed kategori">
<div class="filter-head">
<span>KATEGORI</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('kategori') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('kategori')" floating color="primary">{{ activeFilterCount('kategori') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.kategori" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('kategori')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('kategori')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.kategori"
:options="filteredFilterOptions('kategori')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed renk"> <div class="col-fixed renk">
<div class="filter-head"> <div class="filter-head">
<span>RENK</span> <span>RENK</span>
@@ -214,6 +239,7 @@
> >
<div class="sub-col model">{{ row.product_code || '-' }}</div> <div class="sub-col model">{{ row.product_code || '-' }}</div>
<div class="sub-col desc">{{ row.product_description || '-' }}</div> <div class="sub-col desc">{{ row.product_description || '-' }}</div>
<div class="sub-col kategori">{{ row.kategori || '-' }}</div>
<div class="sub-col renk"> <div class="sub-col renk">
<div class="renk-kodu">{{ variantCode(row) }}</div> <div class="renk-kodu">{{ variantCode(row) }}</div>
<div class="renk-aciklama">{{ row.color_title || '-' }}</div> <div class="renk-aciklama">{{ row.color_title || '-' }}</div>
@@ -287,6 +313,7 @@ const errorMessage = ref('')
const columnFilters = ref({ const columnFilters = ref({
model: [], model: [],
desc: [], desc: [],
kategori: [],
renk: [], renk: [],
ana: [], ana: [],
alt: [], alt: [],
@@ -295,6 +322,7 @@ const columnFilters = ref({
const filterSearch = ref({ const filterSearch = ref({
model: '', model: '',
desc: '', desc: '',
kategori: '',
renk: '', renk: '',
ana: '', ana: '',
alt: '', alt: '',
@@ -341,6 +369,7 @@ const productGroups = computed(() => displayRows.value)
const filterOptions = computed(() => ({ const filterOptions = computed(() => ({
model: uniqueOptions(rows.value.map(row => row.product_code)), model: uniqueOptions(rows.value.map(row => row.product_code)),
desc: uniqueOptions(rows.value.map(row => row.product_description)), desc: uniqueOptions(rows.value.map(row => row.product_description)),
kategori: uniqueOptions(rows.value.map(row => row.kategori)),
renk: uniqueOptions(rows.value.map(row => variantCode(row))), renk: uniqueOptions(rows.value.map(row => variantCode(row))),
ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)), ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)),
alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)), alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)),
@@ -386,6 +415,7 @@ function clearFilter (key) {
function rowPassesFilters (row) { function rowPassesFilters (row) {
return filterMatch('model', row.product_code) && return filterMatch('model', row.product_code) &&
filterMatch('desc', row.product_description) && filterMatch('desc', row.product_description) &&
filterMatch('kategori', row.kategori) &&
filterMatch('renk', variantCode(row)) && filterMatch('renk', variantCode(row)) &&
filterMatch('ana', row.urun_ana_grubu) && filterMatch('ana', row.urun_ana_grubu) &&
filterMatch('alt', row.urun_alt_grubu) && filterMatch('alt', row.urun_alt_grubu) &&
@@ -548,7 +578,7 @@ function mapRowSizesToSchema (row) {
const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize)) const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize))
const schemaMap = getSchemaMap() const schemaMap = getSchemaMap()
const grpKey = detectRowGroupKey(row, rawSizeLabels) const grpKey = detectRowGroupKey(row, rawSizeLabels)
const schema = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null const schema = pickSchemaForRow(schemaMap, grpKey, rawSizeLabels)
const schemaLabelMap = normalizedSchemaLabelMap(schema) const schemaLabelMap = normalizedSchemaLabelMap(schema)
const mapped = {} const mapped = {}
@@ -569,6 +599,26 @@ function mapRowSizesToSchema (row) {
return { grpKey, schema, mapped } return { grpKey, schema, mapped }
} }
function pickSchemaForRow (schemaMap, grpKey, rawSizeLabels) {
const primary = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null
const rawSet = new Set((rawSizeLabels || []).map(v => normalizeBedenLabel(v)).filter(Boolean))
if (!rawSet.size) return primary
const primarySet = new Set((primary?.values || []).map(v => normalizeBedenLabel(v)))
if ([...rawSet].some(v => primarySet.has(v))) return primary
let best = primary
let bestScore = 0
for (const schema of Object.values(schemaMap || {})) {
const schemaSet = new Set((schema?.values || []).map(v => normalizeBedenLabel(v)))
const score = [...rawSet].reduce((sum, v) => sum + (schemaSet.has(v) ? 1 : 0), 0)
if (score > bestScore) {
best = schema
bestScore = score
}
}
return best
}
function formatQty (value) { function formatQty (value) {
const n = Number(value || 0) const n = Number(value || 0)
if (!Number.isFinite(n) || n === 0) return '' if (!Number.isFinite(n) || n === 0) return ''
@@ -605,7 +655,7 @@ function escapeHtml (value) {
function exportVisibleExcel () { function exportVisibleExcel () {
const groups = schemaRows.value const groups = schemaRows.value
const rowSpan = Math.max(groups.length, 1) const rowSpan = Math.max(groups.length, 1)
const leftHeaders = ['MODEL', 'DESC', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA'] const leftHeaders = ['MODEL', 'DESC', 'KATEGORI', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA']
const headerRows = (groups.length ? groups : [{ key: 'tak', title: 'TAKIM ELBISE', values: [] }]).map((grp, index) => { const headerRows = (groups.length ? groups : [{ key: 'tak', title: 'TAKIM ELBISE', values: [] }]).map((grp, index) => {
const left = index === 0 const left = index === 0
? leftHeaders.map(h => `<th rowspan="${rowSpan}">${escapeHtml(h)}</th>`).join('') ? leftHeaders.map(h => `<th rowspan="${rowSpan}">${escapeHtml(h)}</th>`).join('')
@@ -631,6 +681,7 @@ function exportVisibleExcel () {
return `<tr> return `<tr>
<td>${escapeHtml(row.product_code || '')}</td> <td>${escapeHtml(row.product_code || '')}</td>
<td>${escapeHtml(row.product_description || '')}</td> <td>${escapeHtml(row.product_description || '')}</td>
<td>${escapeHtml(row.kategori || '')}</td>
<td>${escapeHtml(variantCode(row))}</td> <td>${escapeHtml(variantCode(row))}</td>
<td>${escapeHtml(row.urun_ana_grubu || '')}</td> <td>${escapeHtml(row.urun_ana_grubu || '')}</td>
<td>${escapeHtml(row.urun_alt_grubu || '')}</td> <td>${escapeHtml(row.urun_alt_grubu || '')}</td>
@@ -665,6 +716,7 @@ onMounted(async () => {
--psq-sticky-offset: 12px; --psq-sticky-offset: 12px;
--grp-title-w: 90px; --grp-title-w: 90px;
--col-desc: var(--col-aciklama); --col-desc: var(--col-aciklama);
--col-kategori-series: 86px;
--col-marka-series: var(--col-marka, 90px); --col-marka-series: var(--col-marka, 90px);
--psq-header-h: var(--grid-header-h); --psq-header-h: var(--grid-header-h);
--series-total-w: 76px; --series-total-w: 76px;
@@ -681,6 +733,7 @@ onMounted(async () => {
grid-template-columns: grid-template-columns:
var(--col-model) var(--col-model)
var(--col-desc) var(--col-desc)
var(--col-kategori-series)
var(--col-renk) var(--col-renk)
var(--col-ana) var(--col-ana)
var(--col-alt) var(--col-alt)
@@ -774,11 +827,11 @@ onMounted(async () => {
} }
.total-header-cell { .total-header-cell {
grid-column: 8 / 9; grid-column: 9 / 10;
} }
.series-header-cell { .series-header-cell {
grid-column: 9 / 10; grid-column: 10 / 11;
} }
.series-flat-row { .series-flat-row {
@@ -816,12 +869,14 @@ onMounted(async () => {
.series-flat-row .sub-col.model { grid-column: 1; } .series-flat-row .sub-col.model { grid-column: 1; }
.series-flat-row .sub-col.desc { grid-column: 2; } .series-flat-row .sub-col.desc { grid-column: 2; }
.series-flat-row .sub-col.renk { grid-column: 3; } .series-flat-row .sub-col.kategori { grid-column: 3; }
.series-flat-row .sub-col.ana { grid-column: 4; } .series-flat-row .sub-col.renk { grid-column: 4; }
.series-flat-row .sub-col.alt { grid-column: 5; } .series-flat-row .sub-col.ana { grid-column: 5; }
.series-flat-row .sub-col.marka { grid-column: 6; } .series-flat-row .sub-col.alt { grid-column: 6; }
.series-flat-row .sub-col.marka { grid-column: 7; }
.series-flat-row .sub-col.model, .series-flat-row .sub-col.model,
.series-flat-row .sub-col.kategori,
.series-flat-row .sub-col.renk, .series-flat-row .sub-col.renk,
.series-flat-row .sub-col.ana, .series-flat-row .sub-col.ana,
.series-flat-row .sub-col.alt, .series-flat-row .sub-col.alt,
@@ -851,7 +906,7 @@ onMounted(async () => {
} }
.flat-size-cells { .flat-size-cells {
grid-column: 7; grid-column: 8;
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
grid-auto-columns: var(--beden-w); grid-auto-columns: var(--beden-w);
@@ -893,7 +948,7 @@ onMounted(async () => {
} }
.total-cell { .total-cell {
grid-column: 8; grid-column: 9;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
@@ -906,7 +961,7 @@ onMounted(async () => {
} }
.series-select-cell { .series-select-cell {
grid-column: 9 / 10; grid-column: 10 / 11;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@@ -0,0 +1,919 @@
<template>
<q-page
class="order-page product-series-page q-pa-md"
:style="{
'--grid-header-h': showGridHeader ? `${schemaRows.length * 28}px` : '0px',
'--beden-count': 16
}"
>
<div class="sticky-stack">
<div class="save-toolbar row items-center q-gutter-sm">
<div>
<div class="text-subtitle2 text-weight-bold">B2B'de Olmayan Stoklu Varyantlar</div>
<div class="text-caption text-grey-8">
Stogu olan ancak B2B tarafinda urun/renk/dim3 kombosu bulunmayan varyantlar listelenir. Bu ekrandan kaydetme yapilmaz.
</div>
</div>
<q-space />
<q-btn color="primary" outline icon="grid_on" label="Excel" :disable="!displayRows.length" @click="exportVisibleExcel" />
<q-btn color="secondary" outline icon="refresh" label="Yenile" :loading="loading" @click="reload" />
</div>
<div v-if="showGridHeader" class="order-grid-header series-grid-header">
<div class="col-fixed model">
<div class="filter-head">
<span>MODEL</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('model') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('model')" floating color="primary">{{ activeFilterCount('model') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.model" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('model')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('model')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.model"
:options="filteredFilterOptions('model')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed desc-col">
<div class="filter-head">
<span>DESC</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('desc') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('desc')" floating color="primary">{{ activeFilterCount('desc') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.desc" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('desc')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('desc')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.desc"
:options="filteredFilterOptions('desc')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed kategori">
<div class="filter-head">
<span>KATEGORI</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('kategori') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('kategori')" floating color="primary">{{ activeFilterCount('kategori') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.kategori" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('kategori')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('kategori')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.kategori"
:options="filteredFilterOptions('kategori')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed renk">
<div class="filter-head">
<span>RENK</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('renk') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('renk')" floating color="primary">{{ activeFilterCount('renk') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.renk" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('renk')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('renk')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.renk"
:options="filteredFilterOptions('renk')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed ana">
<div class="filter-head">
<span>URUN ANA GRUBU</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('ana') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('ana')" floating color="primary">{{ activeFilterCount('ana') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.ana" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('ana')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('ana')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.ana"
:options="filteredFilterOptions('ana')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed alt">
<div class="filter-head">
<span>URUN ALT GRUBU</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('alt') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('alt')" floating color="primary">{{ activeFilterCount('alt') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.alt" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('alt')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('alt')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.alt"
:options="filteredFilterOptions('alt')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="col-fixed marka">
<div class="filter-head">
<span>MARKA</span>
<q-btn dense flat round size="sm" icon="filter_list" :color="activeFilterCount('marka') ? 'primary' : 'grey-8'">
<q-badge v-if="activeFilterCount('marka')" floating color="primary">{{ activeFilterCount('marka') }}</q-badge>
<q-menu class="column-filter-menu" anchor="bottom left" self="top left">
<div class="q-pa-sm">
<q-input v-model="filterSearch.marka" dense outlined clearable placeholder="Ara" autofocus />
<div class="row q-gutter-xs q-mt-sm">
<q-btn dense flat size="sm" label="Tumu" @click="selectAllFilter('marka')" />
<q-btn dense flat size="sm" label="Temizle" @click="clearFilter('marka')" />
</div>
<q-separator class="q-my-sm" />
<q-option-group
v-model="columnFilters.marka"
:options="filteredFilterOptions('marka')"
type="checkbox"
dense
class="column-filter-options"
/>
</div>
</q-menu>
</q-btn>
</div>
</div>
<div class="beden-block">
<div
v-for="grp in schemaRows"
:key="'series-hdr-' + grp.key"
class="grp-row"
>
<div class="grp-title">{{ grp.title }}</div>
<div class="grp-body">
<div v-for="v in paddedSchemaValues(grp)" :key="'hdr-' + grp.key + '-' + v.key" class="grp-cell hdr" :class="{ ghost: v.ghost }">
{{ v.ghost ? '' : v.value }}
</div>
</div>
</div>
</div>
<div class="total-header-cell">TOPLAM</div>
<div class="series-header-cell">SERI</div>
</div>
</div>
<q-banner v-if="errorMessage" class="bg-red-1 text-negative q-my-sm rounded-borders" dense>
{{ errorMessage }}
</q-banner>
<q-banner v-else-if="!loading && !displayRows.length" class="bg-blue-1 text-primary q-my-sm rounded-borders" dense>
B2B'de olmayan stoklu varyant bulunamadi.
</q-banner>
<q-inner-loading :showing="loading">
<q-spinner color="primary" size="42px" />
</q-inner-loading>
<div class="order-scroll-y series-scroll">
<div v-if="displayRows.length" class="order-grid-body series-grid-body">
<div
v-for="row in displayRows"
:key="row.row_key"
class="series-flat-row warning"
>
<div class="sub-col model">{{ row.product_code || '-' }}</div>
<div class="sub-col desc">{{ row.product_description || '-' }}</div>
<div class="sub-col kategori">{{ row.kategori || '-' }}</div>
<div class="sub-col renk">
<div class="renk-kodu">{{ variantCode(row) }}</div>
<div class="renk-aciklama">{{ row.color_title || '-' }}</div>
</div>
<div class="sub-col ana">{{ row.urun_ana_grubu || '-' }}</div>
<div class="sub-col alt">{{ row.urun_alt_grubu || '-' }}</div>
<div class="sub-col marka">{{ row.marka || '-' }}</div>
<div class="flat-size-cells">
<div v-for="sz in rowSizeCells(row)" :key="`${row.row_key}-${sz.key}`" class="beden-cell" :class="{ ghost: sz.ghost }">
{{ formatQty(row._mapped_size_qty?.[sz.value]) }}
</div>
</div>
<div class="total-cell">{{ formatQty(row.total_qty) }}</div>
<div class="series-select-cell" @click.stop>
<q-select
v-model="row._series_ids"
dense
outlined
multiple
emit-value
map-options
use-chips
:options="seriesOptions"
option-value="id"
option-label="label"
disable
/>
<div class="series-meta">
<q-badge color="grey-7">Salt okunur</q-badge>
<span v-if="row.mapping_warning" class="mapping-warning">{{ row.mapping_warning }}</span>
</div>
</div>
</div>
</div>
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { Notify } from 'quasar'
import api from 'src/services/api'
import {
detectBedenGroup,
normalizeBedenLabel,
schemaByKey as fallbackSchemaByKey,
useOrderEntryStore
} from 'src/stores/orderentryStore'
const orderStore = useOrderEntryStore()
const loading = ref(false)
const rows = ref([])
const definitions = ref([])
const errorMessage = ref('')
const columnFilters = ref({
model: [],
desc: [],
kategori: [],
renk: [],
ana: [],
alt: [],
marka: []
})
const filterSearch = ref({
model: '',
desc: '',
kategori: '',
renk: '',
ana: '',
alt: '',
marka: ''
})
const showGridHeader = computed(() => !loading.value && productGroups.value.length > 0)
const seriesOptions = computed(() => definitions.value.map(item => ({
...item,
label: formatSeries(item)
})))
const displayRows = computed(() => {
const list = rows.value.filter(rowPassesFilters)
const productTotals = productTotalQtyMap(list)
return list.sort((a, b) => {
const groupDiff = Number(productTotals.get(b.product_code) || 0) - Number(productTotals.get(a.product_code) || 0)
if (groupDiff !== 0) return groupDiff
const code = String(a.product_code || '').localeCompare(String(b.product_code || ''), 'tr')
if (code !== 0) return code
const color = String(a.color_code || '').localeCompare(String(b.color_code || ''), 'tr')
if (color !== 0) return color
const dim3 = String(a.dim3_code || '').localeCompare(String(b.dim3_code || ''), 'tr')
if (dim3 !== 0) return dim3
return Number(b.total_qty || 0) - Number(a.total_qty || 0)
})
})
function productTotalQtyMap (list) {
const totals = new Map()
for (const row of list || []) {
const code = String(row?.product_code || '').trim()
if (!code) continue
totals.set(code, Number(totals.get(code) || 0) + Number(row?.total_qty || 0))
}
return totals
}
const productGroups = computed(() => displayRows.value)
const filterOptions = computed(() => ({
model: uniqueOptions(rows.value.map(row => row.product_code)),
desc: uniqueOptions(rows.value.map(row => row.product_description)),
kategori: uniqueOptions(rows.value.map(row => row.kategori)),
renk: uniqueOptions(rows.value.map(row => variantCode(row))),
ana: uniqueOptions(rows.value.map(row => row.urun_ana_grubu)),
alt: uniqueOptions(rows.value.map(row => row.urun_alt_grubu)),
marka: uniqueOptions(rows.value.map(row => row.marka))
}))
const schemaRows = computed(() => {
const map = getSchemaMap()
const preferred = ['tak', 'ayk', 'ayk_garson', 'yas', 'pan', 'gom', 'aksbir']
const ordered = preferred
.map(key => map?.[key])
.filter(Boolean)
const extras = Object.values(map || {})
.filter(grp => grp?.key && !preferred.includes(grp.key))
return [...ordered, ...extras]
})
function uniqueOptions (values) {
return [...new Set(values.map(v => String(v || '').trim()).filter(Boolean))]
.sort((a, b) => a.localeCompare(b, 'tr'))
.map(value => ({ label: value, value }))
}
function filteredFilterOptions (key) {
const needle = normalizeFilterText(filterSearch.value[key])
const opts = filterOptions.value[key] || []
if (!needle) return opts
return opts.filter(opt => normalizeFilterText(opt.label).includes(needle))
}
function activeFilterCount (key) {
return Array.isArray(columnFilters.value[key]) ? columnFilters.value[key].length : 0
}
function selectAllFilter (key) {
columnFilters.value[key] = filteredFilterOptions(key).map(opt => opt.value)
}
function clearFilter (key) {
columnFilters.value[key] = []
}
function rowPassesFilters (row) {
return filterMatch('model', row.product_code) &&
filterMatch('desc', row.product_description) &&
filterMatch('kategori', row.kategori) &&
filterMatch('renk', variantCode(row)) &&
filterMatch('ana', row.urun_ana_grubu) &&
filterMatch('alt', row.urun_alt_grubu) &&
filterMatch('marka', row.marka)
}
function filterMatch (key, value) {
const selected = columnFilters.value[key] || []
if (!selected.length) return true
return selected.includes(String(value || '').trim())
}
function normalizeFilterText (value) {
return String(value || '')
.trim()
.toLocaleLowerCase('tr-TR')
}
function normalizeRow (row) {
const ids = Array.isArray(row.series_ids) ? row.series_ids.map(Number).filter(Boolean) : []
const mappedInfo = mapRowSizesToSchema(row)
return {
...row,
series_ids: ids,
_series_ids: [...ids],
_grp_key: mappedInfo.grpKey,
_schema: mappedInfo.schema,
_mapped_size_qty: mappedInfo.mapped
}
}
async function reload () {
loading.value = true
errorMessage.value = ''
try {
if (!orderStore.schemaMap || !Object.keys(orderStore.schemaMap).length) {
orderStore.initSchemaMap()
}
await orderStore.ensureProductSizeMatchRules()
const res = await api.get('/pricing/product-series/mappings/orphans', { timeout: 180000 })
rows.value = (res.data?.rows || []).map(normalizeRow)
definitions.value = res.data?.definitions || []
} catch (err) {
errorMessage.value = err?.response?.data || err?.message || 'B2B olmayan stoklu varyantlar alinamadi'
Notify.create({ type: 'negative', message: errorMessage.value })
} finally {
loading.value = false
}
}
function paddedSchemaValues (grp) {
const values = Array.isArray(grp?.values) ? grp.values.map(v => normalizeBedenLabel(v)) : []
const out = values.slice(0, 16).map((value, index) => ({ key: `${value}-${index}`, value, ghost: false, toString: () => value }))
while (out.length < 16) {
const index = out.length
out.push({ key: `ghost-${index}`, value: `__ghost_${index}`, ghost: true, toString: () => '' })
}
return out
}
function rowSizeCells (row) {
const schema = row?._schema || getSchemaMap()?.[row?._grp_key] || fallbackSchemaByKey.tak
return paddedSchemaValues(schema)
}
function getSchemaMap () {
return Object.keys(orderStore.schemaMap || {}).length
? orderStore.schemaMap
: fallbackSchemaByKey
}
function normalizedSchemaLabelMap (schema) {
const out = new Map()
for (const label of schema?.values || []) {
const normalized = normalizeBedenLabel(label)
if (!out.has(normalized)) out.set(normalized, label)
}
return out
}
function detectRowGroupKey (row, rawSizeLabels) {
const fallback = detectRowGroupKeyFallback(row, rawSizeLabels)
if (fallback) return fallback
return detectBedenGroup(
rawSizeLabels,
row.urun_ana_grubu || '',
row.kategori || '',
row.kategori || '',
row.urun_alt_grubu || ''
)
}
function normalizeMatchText (value) {
return String(value || '')
.trim()
.toLocaleUpperCase('tr-TR')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
function detectRowGroupKeyFallback (row, rawSizeLabels) {
const ana = normalizeMatchText(row?.urun_ana_grubu)
const alt = normalizeMatchText(row?.urun_alt_grubu)
const marka = normalizeMatchText(row?.marka)
const text = `${ana} ${alt} ${marka}`
const sizes = (rawSizeLabels || []).map(v => normalizeBedenLabel(v))
const hasLetterSize = sizes.some(v => ['XS', 'S', 'M', 'L', 'XL', '2XL', '3XL', '4XL', '5XL', '6XL', '7XL'].includes(v))
if (['KRAVAT', 'PAPYON', 'KEMER', 'CORAP', 'FULAR', 'MENDIL', 'KASKOL', 'ASKI', 'AKSESUAR'].some(key => text.includes(key))) return 'aksbir'
if (text.includes('AYAKKABI')) return text.includes('GARSON') ? 'ayk_garson' : 'ayk'
if (text.includes('PANTOLON')) return 'pan'
if (text.includes('GOMLEK') || hasLetterSize) return 'gom'
if (text.includes('TAKIM') || text.includes('DAMATLIK') || text.includes('CEKET') || text.includes('KABAN') || text.includes('MONT') || text.includes('YELEK')) return 'tak'
return ''
}
function mapRowSizesToSchema (row) {
const sizeEntries = Object.entries(row?.size_qty || {})
const rawSizeLabels = sizeEntries.map(([rawSize]) => normalizeBedenLabel(rawSize))
const schemaMap = getSchemaMap()
const grpKey = detectRowGroupKey(row, rawSizeLabels)
const schema = pickSchemaForRow(schemaMap, grpKey, rawSizeLabels)
const schemaLabelMap = normalizedSchemaLabelMap(schema)
const mapped = {}
for (const [rawSize, rawQty] of sizeEntries) {
const normalized = normalizeBedenLabel(rawSize)
const target = normalizeBedenLabel(schemaLabelMap.get(normalized) || normalized)
mapped[target] = Number(mapped[target] || 0) + Number(rawQty || 0)
}
if (grpKey === 'aksbir' && !Object.keys(mapped).length && Number(row?.total_qty || 0) > 0) {
mapped[' '] = Number(row.total_qty || 0)
}
if (grpKey !== 'aksbir' && Object.keys(mapped).some(k => String(k).trim() !== '')) {
delete mapped[' ']
}
return { grpKey, schema, mapped }
}
function pickSchemaForRow (schemaMap, grpKey, rawSizeLabels) {
const primary = grpKey && schemaMap?.[grpKey] ? schemaMap[grpKey] : null
const rawSet = new Set((rawSizeLabels || []).map(v => normalizeBedenLabel(v)).filter(Boolean))
if (!rawSet.size) return primary
const primarySet = new Set((primary?.values || []).map(v => normalizeBedenLabel(v)))
if ([...rawSet].some(v => primarySet.has(v))) return primary
let best = primary
let bestScore = 0
for (const schema of Object.values(schemaMap || {})) {
const schemaSet = new Set((schema?.values || []).map(v => normalizeBedenLabel(v)))
const score = [...rawSet].reduce((sum, v) => sum + (schemaSet.has(v) ? 1 : 0), 0)
if (score > bestScore) {
best = schema
bestScore = score
}
}
return best
}
function formatQty (value) {
const n = Number(value || 0)
if (!Number.isFinite(n) || n === 0) return ''
return n.toLocaleString('tr-TR', { maximumFractionDigits: 2 })
}
function formatSeries (item) {
const code = String(item?.code || '').trim()
const title = String(item?.title || '').trim()
return title ? `${code}/${title}` : code
}
function variantCode (row) {
const c = String(row?.color_code || '').trim()
const d = String(row?.dim3_code || '').trim()
return d ? `${c}-${d}` : (c || '-')
}
function exportFileStamp () {
const now = new Date()
const pad = v => String(v).padStart(2, '0')
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}`
}
function csvEscape (v) {
const s = String(v ?? '')
if (!s.includes(',') && !s.includes('"') && !s.includes('\n')) return s
return `"${s.replace(/"/g, '""')}"`
}
function exportVisibleExcel () {
const cols = ['MODEL', 'DESC', 'KATEGORI', 'RENK', 'URUN ANA GRUBU', 'URUN ALT GRUBU', 'MARKA']
const sizeCols = schemaRows.value.flatMap(grp => paddedSchemaValues(grp).map(v => (v.ghost ? '' : v.value))).filter(Boolean)
cols.push(...sizeCols)
cols.push('TOPLAM', 'SERI', 'UYARI')
const lines = [cols.map(csvEscape).join(',')]
for (const row of displayRows.value) {
const sizeValues = []
const cells = rowSizeCells(row)
for (const cell of cells) {
sizeValues.push(cell.ghost ? '' : formatQty(row._mapped_size_qty?.[cell.value]))
}
lines.push([
row.product_code || '',
row.product_description || '',
row.kategori || '',
variantCode(row),
row.urun_ana_grubu || '',
row.urun_alt_grubu || '',
row.marka || '',
...sizeValues,
formatQty(row.total_qty),
'',
row.mapping_warning || ''
].map(csvEscape).join(','))
}
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `b2b-olmayan-stoklu-varyantlar_${exportFileStamp()}.csv`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
onMounted(() => reload())
</script>
<style scoped>
.product-series-page {
--filter-h: 0px;
--psq-sticky-offset: 12px;
--grp-title-w: 90px;
--col-desc: var(--col-aciklama);
--col-kategori-series: 86px;
--col-marka-series: var(--col-marka, 90px);
--psq-header-h: var(--grid-header-h);
--series-total-w: 76px;
--series-col-w: minmax(220px, 1fr);
background: #f5f1da;
}
.series-scroll {
overflow: auto;
}
.series-grid-header,
.series-flat-row {
grid-template-columns:
var(--col-model)
var(--col-desc)
var(--col-kategori-series)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-marka-series)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
var(--series-total-w)
var(--series-col-w) !important;
width: 100%;
min-width: min-content;
}
.series-grid-header {
top: calc(var(--header-h) + var(--filter-h) + var(--save-h) + var(--psq-sticky-offset)) !important;
}
.series-grid-header .col-fixed,
.total-header-cell,
.series-header-cell {
writing-mode: horizontal-tb !important;
transform: none !important;
height: var(--psq-header-h) !important;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px !important;
line-height: 1 !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px !important;
}
.filter-head {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
width: 100%;
min-width: 0;
}
.filter-head span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.column-filter-menu {
width: 280px;
max-width: 86vw;
}
.column-filter-options {
max-height: 300px;
overflow: auto;
}
.series-grid-header .beden-block {
height: var(--psq-header-h) !important;
}
.series-grid-header .grp-row {
height: var(--beden-h) !important;
align-items: center;
}
.series-grid-header .grp-title {
width: var(--grp-title-w) !important;
text-align: center !important;
padding-right: 0 !important;
font-size: 10px !important;
}
.series-grid-header .grp-cell.hdr {
height: var(--beden-h) !important;
font-size: 10px !important;
}
.series-grid-header .grp-cell.hdr.ghost {
color: transparent;
}
.total-header-cell,
.series-header-cell {
display: flex;
align-items: center;
justify-content: center;
background: #fff8d1;
border-left: 1px solid #d4c79f;
font-weight: 700;
}
.total-header-cell {
grid-column: 9 / 10;
}
.series-header-cell {
grid-column: 10 / 11;
}
.series-flat-row {
display: grid;
min-height: 72px;
align-items: center;
background: #fff9c4 !important;
border-top: 1px solid #d4c79f !important;
border-bottom: 1px solid #d4c79f !important;
}
.series-flat-row.dirty {
background: #fff0cf !important;
}
.series-flat-row.warning {
box-shadow: inset 3px 0 0 #f59e0b;
}
.series-flat-row .sub-col {
display: flex;
align-items: center;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
color: #111;
min-width: 0;
border-right: 1px solid #d4c79f;
white-space: normal;
overflow: visible;
line-height: 1.2;
word-break: break-word;
overflow-wrap: anywhere;
}
.series-flat-row .sub-col.model { grid-column: 1; }
.series-flat-row .sub-col.desc { grid-column: 2; }
.series-flat-row .sub-col.kategori { grid-column: 3; }
.series-flat-row .sub-col.renk { grid-column: 4; }
.series-flat-row .sub-col.ana { grid-column: 5; }
.series-flat-row .sub-col.alt { grid-column: 6; }
.series-flat-row .sub-col.marka { grid-column: 7; }
.series-flat-row .sub-col.model,
.series-flat-row .sub-col.kategori,
.series-flat-row .sub-col.renk,
.series-flat-row .sub-col.ana,
.series-flat-row .sub-col.alt,
.series-flat-row .sub-col.marka {
justify-content: center;
text-align: center;
}
.series-flat-row .sub-col.desc {
justify-content: flex-start;
text-align: left;
}
.series-flat-row .sub-col.renk {
flex-direction: column;
gap: 2px;
line-height: 1.1;
}
.series-flat-row .sub-col.renk .renk-kodu {
font-weight: 700;
}
.series-flat-row .sub-col.renk .renk-aciklama {
font-size: 11px;
opacity: 0.9;
}
.flat-size-cells {
grid-column: 8;
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
justify-content: start;
align-items: stretch;
width: calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)));
padding-left: calc(var(--grp-title-w) + var(--grp-title-gap));
margin-left: 0;
height: 100%;
box-sizing: border-box;
}
.flat-size-cells::before,
.flat-size-cells::after {
content: none;
}
.flat-size-cells .beden-cell {
display: flex;
align-items: center;
justify-content: center;
width: var(--beden-w);
height: 100%;
box-sizing: border-box;
border: 1px solid #d4c79f;
border-right: none;
background: #fffef6;
color: #1f1f1f;
font-size: 13px;
font-weight: 700;
}
.flat-size-cells .beden-cell:last-child {
border-right: 1px solid #d4c79f;
}
.flat-size-cells .beden-cell.ghost {
color: transparent;
}
.total-cell {
grid-column: 9;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 10px;
border-left: 1px solid #d4c79f;
border-right: 1px solid #d4c79f;
color: var(--q-primary, #1976d2);
font-size: 14px;
font-weight: 800;
}
.series-select-cell {
grid-column: 10 / 11;
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
padding: 8px 10px;
border-left: 1px solid #d4c79f;
background: #fffdf0;
}
.series-select-cell :deep(.q-field__control) {
min-height: 36px;
}
.series-chip {
max-width: 170px;
font-size: 11px;
}
.series-meta {
display: flex;
align-items: center;
gap: 6px;
min-height: 18px;
font-size: 11px;
}
.mapping-warning {
color: #b45309;
}
</style>
+75 -35
View File
@@ -423,7 +423,6 @@ const productImageListByCode = ref({})
const productImageListLoading = ref({}) const productImageListLoading = ref({})
const productImageFallbackByKey = ref({}) const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({}) const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0) const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false) const productCardDialog = ref(false)
const productCardData = ref({}) const productCardData = ref({})
@@ -444,6 +443,8 @@ const IMAGE_LIST_CONCURRENCY = 8
const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000 const FILTER_OPTIONS_CACHE_TTL_MS = 60 * 1000
const FILTER_OPTIONS_DEBOUNCE_MS = 250 const FILTER_OPTIONS_DEBOUNCE_MS = 250
let imageListActiveRequests = 0 let imageListActiveRequests = 0
let productImageBatchTimer = null
const productImageBatchQueue = new Map()
let filterOptionsDebounceTimer = null let filterOptionsDebounceTimer = null
let filterOptionsRequestSeq = 0 let filterOptionsRequestSeq = 0
const imageListWaitQueue = [] const imageListWaitQueue = []
@@ -635,14 +636,9 @@ function resolveProductImageUrl(item) {
} }
let contentUrl = '' let contentUrl = ''
const imageId = Number(item.id || item.ID || 0) const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (Number.isFinite(imageId) && imageId > 0) { if (contentURL.startsWith('/api/')) contentUrl = contentURL
contentUrl = `/api/product-images/${imageId}/content` else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
} else {
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '') const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = '' let publicUrl = ''
@@ -706,34 +702,82 @@ function clearGalleryQueryIndex() {
async function resolveProductImageUrlForCarousel(item) { async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item) const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim() const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) { if (contentUrl) return contentUrl
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' }) const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
const blob = blobRes?.data if (directUrl) return directUrl
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob) return ''
productImageBlobUrls.value.push(objectUrl)
return objectUrl
}
} catch {
// fall through to public url
}
}
const fullUrl = String(resolved.fullUrl || '').trim()
if (fullUrl) return fullUrl
const publicUrl = String(resolved.publicUrl || '').trim()
return String(publicUrl || fullUrl || contentUrl || '').trim()
} }
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') { function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key] const existing = productImageCache.value[key]
if (existing !== undefined) return existing || '' if (existing !== undefined) return existing || ''
queueProductImageBatch(code, color, secondColor, dim1Id, dim3Id)
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id)
return '' return ''
} }
function queueProductImageBatch(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const codeTrim = String(code || '').trim().toUpperCase()
if (!codeTrim || productImageCache.value[key] !== undefined || productImageLoading.value[key]) return
productImageLoading.value[key] = true
productImageBatchQueue.set(key, {
key,
code: codeTrim,
dim1: String(dim1Id || color || '').trim().toUpperCase(),
dim3: String(dim3Id || secondColor || '').trim().toUpperCase(),
listKey: buildImageKey(
codeTrim,
String(color || '').trim().toUpperCase(),
String(secondColor || '').trim().toUpperCase(),
String(dim1Id || '').trim().toUpperCase(),
String(dim3Id || '').trim().toUpperCase()
)
})
if (productImageBatchTimer) window.clearTimeout(productImageBatchTimer)
productImageBatchTimer = window.setTimeout(flushProductImageBatch, 80)
}
async function flushProductImageBatch() {
const batch = Array.from(productImageBatchQueue.values()).slice(0, 250)
for (const item of batch) productImageBatchQueue.delete(item.key)
if (!batch.length) return
try {
const res = await api.post('/product-images/batch', {
items: batch.map(item => ({ key: item.key, code: item.code, dim1: item.dim1, dim3: item.dim3 }))
}, { timeout: 30000 })
const returned = new Set()
for (const item of Array.isArray(res?.data) ? res.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const queued = batch.find(entry => entry.key === key)
const list = Array.isArray(item.images) ? item.images : []
const first = list[0] || null
const resolved = resolveProductImageUrl(first)
const url = resolved.contentUrl || resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
productImageCache.value[key] = String(url || '').trim()
productImageFallbackByKey.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
if (queued) productImageListByCode.value[queued.listKey] = list
delete productImageLoading.value[key]
}
for (const item of batch) {
if (!returned.has(item.key)) {
productImageCache.value[item.key] = ''
productImageListByCode.value[item.listKey] = []
delete productImageLoading.value[item.key]
}
}
} catch {
for (const item of batch) {
delete productImageLoading.value[item.key]
void ensureProductImage(item.code, item.dim1, item.dim3)
}
}
}
async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') { async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim() productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
@@ -804,11 +848,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
productImageCache.value[key] = String( productImageCache.value[key] = String(
preferredCardUrl || preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl || primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl || secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
'' ''
).trim() ).trim()
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || '' productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || ''
} catch (err) { } catch (err) {
console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err }) console.warn('[ProductStockByAttributes] product image fetch failed', { code, color, err })
productImageCache.value[key] = '' productImageCache.value[key] = ''
@@ -1520,10 +1564,6 @@ onUnmounted(() => {
clearTimeout(filterOptionsDebounceTimer) clearTimeout(filterOptionsDebounceTimer)
filterOptionsDebounceTimer = null filterOptionsDebounceTimer = null
} }
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
}) })
</script> </script>
+75 -34
View File
@@ -390,7 +390,6 @@ const productImageListByCode = ref({})
const productImageListLoading = ref({}) const productImageListLoading = ref({})
const productImageFallbackByKey = ref({}) const productImageFallbackByKey = ref({})
const productImageContentLoading = ref({}) const productImageContentLoading = ref({})
const productImageBlobUrls = ref([])
const productImageListBlockedUntil = ref(0) const productImageListBlockedUntil = ref(0)
const productCardDialog = ref(false) const productCardDialog = ref(false)
const productCardData = ref({}) const productCardData = ref({})
@@ -409,6 +408,8 @@ const productImageFullscreenDragOriginX = ref(0)
const productImageFullscreenDragOriginY = ref(0) const productImageFullscreenDragOriginY = ref(0)
const IMAGE_LIST_CONCURRENCY = 8 const IMAGE_LIST_CONCURRENCY = 8
let imageListActiveRequests = 0 let imageListActiveRequests = 0
let productImageBatchTimer = null
const productImageBatchQueue = new Map()
const imageListWaitQueue = [] const imageListWaitQueue = []
const activeSchema = ref(storeSchemaByKey.tak) const activeSchema = ref(storeSchemaByKey.tak)
const activeGrpKey = ref('tak') const activeGrpKey = ref('tak')
@@ -616,14 +617,9 @@ function resolveProductImageUrl(item) {
} }
let contentUrl = '' let contentUrl = ''
const imageId = Number(item.id || item.ID || 0) const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (Number.isFinite(imageId) && imageId > 0) { if (contentURL.startsWith('/api/')) contentUrl = contentURL
contentUrl = `/api/product-images/${imageId}/content` else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
} else {
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) contentUrl = contentURL
else if (contentURL.startsWith('/')) contentUrl = `/api${contentURL}`
}
const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '') const uploadsPath = normalizeUploadsPath(item.storage_path || item.storage || '')
let publicUrl = '' let publicUrl = ''
@@ -685,33 +681,82 @@ function clearGalleryQueryIndex() {
async function resolveProductImageUrlForCarousel(item) { async function resolveProductImageUrlForCarousel(item) {
const resolved = resolveProductImageUrl(item) const resolved = resolveProductImageUrl(item)
const contentUrl = String(resolved.contentUrl || '').trim() const contentUrl = String(resolved.contentUrl || '').trim()
if (contentUrl) { if (contentUrl) return contentUrl
try {
const blobRes = await api.get(contentUrl, { baseURL: '', responseType: 'blob' }) const directUrl = String(resolved.fullUrl || resolved.thumbUrl || resolved.publicUrl || '').trim()
const blob = blobRes?.data if (directUrl) return directUrl
if (blob instanceof Blob) {
const objectUrl = URL.createObjectURL(blob) return ''
productImageBlobUrls.value.push(objectUrl)
return objectUrl
}
} catch {
// fall through to public url
}
}
const fullUrl = String(resolved.fullUrl || '').trim()
if (fullUrl) return fullUrl
const publicUrl = String(resolved.publicUrl || '').trim()
return String(publicUrl || fullUrl || contentUrl || '').trim()
} }
function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') { function getProductImageUrl(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const existing = productImageCache.value[key] const existing = productImageCache.value[key]
if (existing !== undefined) return existing || '' if (existing !== undefined) return existing || ''
void ensureProductImage(code, color, secondColor, dim1Id, dim3Id) queueProductImageBatch(code, color, secondColor, dim1Id, dim3Id)
return '' return ''
} }
function queueProductImageBatch(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
const codeTrim = String(code || '').trim().toUpperCase()
if (!codeTrim || productImageCache.value[key] !== undefined || productImageLoading.value[key]) return
productImageLoading.value[key] = true
productImageBatchQueue.set(key, {
key,
code: codeTrim,
dim1: String(dim1Id || color || '').trim().toUpperCase(),
dim3: String(dim3Id || secondColor || '').trim().toUpperCase(),
listKey: buildImageKey(
codeTrim,
String(color || '').trim().toUpperCase(),
String(secondColor || '').trim().toUpperCase(),
String(dim1Id || '').trim().toUpperCase(),
String(dim3Id || '').trim().toUpperCase()
)
})
if (productImageBatchTimer) window.clearTimeout(productImageBatchTimer)
productImageBatchTimer = window.setTimeout(flushProductImageBatch, 80)
}
async function flushProductImageBatch() {
const batch = Array.from(productImageBatchQueue.values()).slice(0, 250)
for (const item of batch) productImageBatchQueue.delete(item.key)
if (!batch.length) return
try {
const res = await api.post('/product-images/batch', {
items: batch.map(item => ({ key: item.key, code: item.code, dim1: item.dim1, dim3: item.dim3 }))
}, { timeout: 30000 })
const returned = new Set()
for (const item of Array.isArray(res?.data) ? res.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const queued = batch.find(entry => entry.key === key)
const list = Array.isArray(item.images) ? item.images : []
const first = list[0] || null
const resolved = resolveProductImageUrl(first)
const url = resolved.contentUrl || resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
productImageCache.value[key] = String(url || '').trim()
productImageFallbackByKey.value[key] = resolved.thumbUrl || resolved.fullUrl || resolved.publicUrl || ''
if (queued) productImageListByCode.value[queued.listKey] = list
delete productImageLoading.value[key]
}
for (const item of batch) {
if (!returned.has(item.key)) {
productImageCache.value[item.key] = ''
productImageListByCode.value[item.listKey] = []
delete productImageLoading.value[item.key]
}
}
} catch {
for (const item of batch) {
delete productImageLoading.value[item.key]
void ensureProductImage(item.code, item.dim1, item.dim3)
}
}
}
async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') { async function onProductImageError(code, color, secondColor = '', dim1Id = '', dim3Id = '') {
const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id) const key = buildImageKey(code, color, secondColor, dim1Id, dim3Id)
productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim() productImageCache.value[key] = String(productImageFallbackByKey.value[key] || '').trim()
@@ -779,11 +824,11 @@ async function ensureProductImage(code, color, secondColor = '', dim1Id = '', di
const secondaryResolved = resolveProductImageUrl(secondaryItem) const secondaryResolved = resolveProductImageUrl(secondaryItem)
productImageCache.value[key] = String( productImageCache.value[key] = String(
preferredCardUrl || preferredCardUrl ||
primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || primaryResolved.contentUrl || primaryResolved.contentUrl || primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl ||
secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || secondaryResolved.contentUrl || secondaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl ||
'' ''
).trim() ).trim()
productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.contentUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.contentUrl || '' productImageFallbackByKey.value[key] = primaryResolved.fullUrl || primaryResolved.publicUrl || primaryResolved.thumbUrl || secondaryResolved.fullUrl || secondaryResolved.publicUrl || secondaryResolved.thumbUrl || ''
} catch (err) { } catch (err) {
console.warn('[ProductStockQuery] product image fetch failed', { code, color, err }) console.warn('[ProductStockQuery] product image fetch failed', { code, color, err })
productImageCache.value[key] = '' productImageCache.value[key] = ''
@@ -1317,10 +1362,6 @@ function resetForm() {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('mousemove', onFullscreenMouseMove) window.removeEventListener('mousemove', onFullscreenMouseMove)
window.removeEventListener('mouseup', onFullscreenMouseUp) window.removeEventListener('mouseup', onFullscreenMouseUp)
for (const url of productImageBlobUrls.value) {
try { URL.revokeObjectURL(url) } catch {}
}
productImageBlobUrls.value = []
}) })
onMounted(async () => { onMounted(async () => {
@@ -40,6 +40,14 @@
:loading="loading" :loading="loading"
@click="fetchRows" @click="fetchRows"
/> />
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="loading || rows.length === 0"
@click="exportVisibleRows"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -141,7 +149,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
import { get, extractApiErrorDetail } from 'src/services/api' import { get, extractApiErrorDetail } from 'src/services/api'
@@ -168,6 +176,10 @@ const dateColumns = new Set(['Tarihi', 'dteKayitTarihi', 'dteGuncellemeTarihi',
const columns = [ const columns = [
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' }, { name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
{ name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' }, { name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
{ name: 'nUrtSiparisNo', label: 'Uretim Siparis No', field: 'nUrtSiparisNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'ceketDepoGiris', label: 'C-Ceket Depo Giris', field: 'ceketDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'pantolonDepoGiris', label: 'P-Pantolon Depo Giris', field: 'pantolonDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'yelekDepoGiris', label: 'Y-Yelek Depo Giris', field: 'yelekDepoGiris', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' }, { name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
{ name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' }, { name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' }, { name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
@@ -186,6 +198,30 @@ const columns = [
] ]
const columnFilters = reactive({}) const columnFilters = reactive({})
const LIST_STATE_STORAGE_KEY = 'bssapp:production-product-costing:has-cost-list-state:v1'
function persistListState () {
try {
sessionStorage.setItem(LIST_STATE_STORAGE_KEY, JSON.stringify({
filters: { search: String(filters.search || '') },
columnFilters: JSON.parse(JSON.stringify(columnFilters)),
pagination: { ...tablePagination.value }
}))
} catch {}
}
function restoreListState () {
try {
const raw = sessionStorage.getItem(LIST_STATE_STORAGE_KEY)
if (!raw) return
const saved = JSON.parse(raw)
filters.search = String(saved?.filters?.search || '')
Object.assign(columnFilters, saved?.columnFilters && typeof saved.columnFilters === 'object' ? saved.columnFilters : {})
if (saved?.pagination && typeof saved.pagination === 'object') {
tablePagination.value = { ...tablePagination.value, ...saved.pagination }
}
} catch {}
}
function getColumnFilter (name) { function getColumnFilter (name) {
if (!columnFilters[name]) { if (!columnFilters[name]) {
@@ -293,9 +329,11 @@ function clearAllColumnFilters () {
} }
let searchTimer = null let searchTimer = null
let filterWatchEnabled = false
watch( watch(
() => filters.search, () => filters.search,
() => { () => {
if (!filterWatchEnabled) return
clearTimeout(searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => { searchTimer = setTimeout(() => {
fetchRows() fetchRows()
@@ -348,19 +386,66 @@ function clearFilters () {
fetchRows() fetchRows()
} }
function escapeExcelCsvCell (value) {
let text = String(value ?? '')
// Prevent spreadsheet applications from evaluating data as a formula.
if (/^[=+\-@]/.test(text)) text = `'${text}`
return `"${text.replace(/"/g, '""')}"`
}
const excelNumericColumns = new Set(['ceketDepoGiris', 'pantolonDepoGiris', 'yelekDepoGiris', 'lTutarTL', 'lTutarUSD', 'lTutarEURO'])
function formatExcelCsvCell (col, rawValue, displayValue) {
if (excelNumericColumns.has(col?.name)) {
const numericValue = Number(rawValue)
if (Number.isFinite(numericValue)) return String(numericValue).replace('.', ',')
}
return escapeExcelCsvCell(displayValue)
}
function exportVisibleRows () {
const visibleRows = Array.isArray(rows.value) ? rows.value : []
if (visibleRows.length === 0) return
const exportColumns = columns.filter(col => col.name !== 'open')
const csvLines = [
exportColumns.map(col => escapeExcelCsvCell(col.label)).join(';'),
...visibleRows.map(row => exportColumns.map(col => {
const rawValue = typeof col.field === 'function' ? col.field(row) : row?.[col.field]
const displayValue = typeof col.format === 'function' ? col.format(rawValue, row) : rawValue
return formatExcelCsvCell(col, rawValue, displayValue)
}).join(';'))
]
const blob = new Blob([`\uFEFF${csvLines.join('\r\n')}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `mevcut_maliyetli_urunler_${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
}
function openRow (row) { function openRow (row) {
const urunKodu = String(row?.UrunKodu || '').trim() const urunKodu = String(row?.UrunKodu || '').trim()
if (!urunKodu) return if (!urunKodu) return
persistListState()
router.push({ router.push({
name: 'production-product-costing-has-cost-history', name: 'production-product-costing-has-cost-history',
query: { urun_kodu: urunKodu } query: { urun_kodu: urunKodu }
}) })
} }
onMounted(() => { onMounted(async () => {
if (!canReadOrder.value) return if (!canReadOrder.value) return
fetchRows() restoreListState()
await nextTick()
filterWatchEnabled = true
await fetchRows()
}) })
</script> </script>
@@ -457,7 +457,7 @@
<div v-else class="column q-gutter-md pcd-content-body"> <div v-else class="column q-gutter-md pcd-content-body">
<div <div
v-for="(grp, gi) in detailGroups" v-for="(grp, gi) in detailGroupsWithTotals"
:key="groupKey(grp, gi)" :key="groupKey(grp, gi)"
class="pcd-group-card" class="pcd-group-card"
> >
@@ -469,7 +469,11 @@
</span> </span>
</div> </div>
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)"> <div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }} <span class="pcd-group-total-text" data-no-runtime-i18n="true">
Grup Toplamı TRY: {{ formatBarMoney(grp.tryTotal) }}
<span class="pcd-group-total-divider">|</span>
USD: {{ formatBarMoney(grp.usdTotal) }}
</span>
<q-icon <q-icon
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'" :name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
size="18px" size="18px"
@@ -840,7 +844,7 @@
<div> <div>
<div class="text-subtitle1 text-weight-bold">Fiyat Kontrolu (Satinalma Ortalama)</div> <div class="text-subtitle1 text-weight-bold">Fiyat Kontrolu (Satinalma Ortalama)</div>
<div class="text-caption text-grey-7"> <div class="text-caption text-grey-7">
BAGGI_V3 satinalma gecmisindeki son 10 USD ortalamasindan %10'dan fazla sapan tum satirlar. NEBIM_V3 satinalma gecmisindeki son 10 USD ortalamasindan %10'dan fazla sapan tum satirlar.
</div> </div>
</div> </div>
<q-badge color="primary" outline>{{ priceDeviationRows.length }} satir</q-badge> <q-badge color="primary" outline>{{ priceDeviationRows.length }} satir</q-badge>
@@ -1105,6 +1109,7 @@ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
import { get, post, download, extractApiErrorDetail } from 'src/services/api' import { get, post, download, extractApiErrorDetail } from 'src/services/api'
import { createTraceId, slog } from 'src/utils/slog' import { createTraceId, slog } from 'src/utils/slog'
import { formatDataSourceLabel } from 'src/utils/formatters'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -1556,8 +1561,14 @@ function ensureBeforeUnloadGuard (enabled) {
return '' return ''
} }
} }
function shouldIncludeRowInCostingTotal (row) {
// The explicit "Maliyete Dahil" selection is authoritative for the top
// header and the amount persisted to spUrtOnMLMas, including CM rows.
return normalizeBooleanFlag(row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil)
}
const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => { const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => {
if (!row?.maliyeteDahil) return acc if (!shouldIncludeRowInCostingTotal(row)) return acc
acc.tryTotal += resolveRowTRYTutar(row) acc.tryTotal += resolveRowTRYTutar(row)
acc.usdTotal += resolveRowUSDTutar(row) acc.usdTotal += resolveRowUSDTutar(row)
acc.eurTotal += resolveRowEURTutar(row) acc.eurTotal += resolveRowEURTutar(row)
@@ -2334,8 +2345,8 @@ function resolveHistorySourceType (item, forcedType = '') {
} }
function resolveHistorySourceLabel (sourceType) { function resolveHistorySourceLabel (sourceType) {
if (sourceType === 'purchase') return 'BAGGI_V3' if (sourceType === 'purchase') return formatDataSourceLabel('BAGGI_V3')
if (sourceType === 'recipe') return 'URETIM' if (sourceType === 'recipe') return formatDataSourceLabel('URETIM')
return 'HISTORY' return 'HISTORY'
} }
@@ -2693,7 +2704,9 @@ function normalizeDetailRows (items, groupName = '') {
// Use fiyat_girilen which is the unit price in original currency. // Use fiyat_girilen which is the unit price in original currency.
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen), inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
inputPricePrBr: originalCurrency, inputPricePrBr: originalCurrency,
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil), maliyeteDahil: normalizeBooleanFlag(x?.maliyeteDahil ?? x?.maliyete_dahil ?? x?.Maliyete_dahil),
__lastValidTRYAmount: parseMoneyInput(x?.lTutar ?? x?.lTutarTL),
__lastValidUSDAmount: parseMoneyInput(x?.usdTutar ?? x?.lTutarUSD),
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3), cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
draftChanged: Boolean(x?.draftChanged), draftChanged: Boolean(x?.draftChanged),
priceUpdateState: String(x?.priceUpdateState || '').trim() priceUpdateState: String(x?.priceUpdateState || '').trim()
@@ -2795,6 +2808,13 @@ function recalculateDetailRow (row, options = {}) {
row.lTutarGBP = row.gbpTutar row.lTutarGBP = row.gbpTutar
row.lTutarTL = row.lTutar row.lTutarTL = row.lTutar
if (row.lTutar !== 0 || options.markChanged) {
row.__lastValidTRYAmount = row.lTutar
}
if (row.usdTutar !== 0 || options.markChanged) {
row.__lastValidUSDAmount = row.usdTutar
}
if (!options.preserveInputs) { if (!options.preserveInputs) {
row.miktarInput = normalizeQuantityInput(quantity) row.miktarInput = normalizeQuantityInput(quantity)
row.inputPrice = normalizeInputPrice(inputPrice) row.inputPrice = normalizeInputPrice(inputPrice)
@@ -2816,58 +2836,124 @@ function recalculateDetailRow (row, options = {}) {
} }
function recalculateAllDetailRows () { function recalculateAllDetailRows () {
detailGroups.value = detailGroups.value.map(grp => ({ // Preserve row identity. QTable editors and the computed group totals must
...grp, // observe the exact same reactive row objects. Replacing rows with
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true })) // `{ ...row }` after the async exchange-rate request left QTable editing an
})) // older object while totals watched the replacement object.
detailGroups.value.forEach(grp => {
const items = Array.isArray(grp?.items) ? grp.items : []
items.forEach(row => {
recalculateDetailRow(row, { preserveInputs: true })
})
})
} }
function resolveRowTRYTutar (row) { function resolveRowTRYTutar (row) {
const tryAmount = Number(row?.lTutar || 0) const quantity = resolveNumericRowQuantity(row)
return Number.isFinite(tryAmount) ? tryAmount : 0 const inputPrice = resolveNumericRowInputPrice(row)
const inputCurrency = resolveInputCurrency(row)
const liveAmount = resolveTRYUnitPriceByInput(
inputPrice,
inputCurrency,
resolveExchangeRateValue('USD'),
resolveExchangeRateValue('EUR'),
resolveExchangeRateValue('GBP')
) * quantity
if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
return parseMoneyInput(row?.lTutar)
} }
function resolveRowEURTutar (row) { function resolveRowEURTutar (row) {
const eurAmount = Number(row?.eurTutar || 0) return parseMoneyInput(row?.eurTutar)
return Number.isFinite(eurAmount) ? eurAmount : 0
} }
function resolveRowGBPTutar (row) { function resolveRowGBPTutar (row) {
const gbpAmount = Number(row?.gbpTutar || 0) return parseMoneyInput(row?.gbpTutar)
return Number.isFinite(gbpAmount) ? gbpAmount : 0
} }
function resolveRowUSDTutar (row) { function resolveRowUSDTutar (row) {
const usdAmount = Number(row?.usdTutar || 0) const quantity = resolveNumericRowQuantity(row)
if (Number.isFinite(usdAmount)) return usdAmount const inputPrice = resolveNumericRowInputPrice(row)
const inputCurrency = resolveInputCurrency(row)
const liveAmount = resolveUSDUnitPriceByInput(
inputPrice,
inputCurrency,
resolveExchangeRateValue('USD'),
resolveExchangeRateValue('EUR'),
resolveExchangeRateValue('GBP')
) * quantity
const miktar = resolveNumericRowQuantity(row) if (Number.isFinite(liveAmount) && inputPrice > 0) return liveAmount
const dovizFiyati = Number(row?.lDovizFiyati || 0)
const calc = miktar * dovizFiyati const rawUSDAmount = row?.usdTutar
if (rawUSDAmount !== undefined && rawUSDAmount !== null && String(rawUSDAmount).trim() !== '') {
return parseMoneyInput(rawUSDAmount)
}
const dovizFiyati = parseMoneyInput(row?.lDovizFiyati)
const calc = quantity * dovizFiyati
return Number.isFinite(calc) ? calc : 0 return Number.isFinite(calc) ? calc : 0
} }
function shouldIgnoreGroupMaliyeteDahil (grp) {
return isCMGroupName(grp?.sAciklama3)
}
function shouldIncludeRowInGroupTotal (grp, row) { function shouldIncludeRowInGroupTotal (grp, row) {
return shouldIgnoreGroupMaliyeteDahil(grp) || normalizeBooleanFlag(row?.maliyeteDahil) if (!row) return false
// CM1/CM2 sub-headers always show their full group value. For every other
// group, the explicit "Maliyete Dahil" selection controls the subtotal.
if (isCMGroupName(grp?.sAciklama3 || row?.sAciklama3)) return true
const inclusionValue = row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil
if (inclusionValue === false || inclusionValue === 0) return false
const normalizedValue = String(inclusionValue ?? '').trim().toLowerCase()
return normalizedValue !== '0' && normalizedValue !== 'false' && normalizedValue !== 'hayir'
} }
function resolveGroupTRYTutar (grp) { function resolveGroupRowTRYAmount (row) {
const items = Array.isArray(grp?.items) ? grp.items : [] const columnAmount = parseMoneyInput(row?.lTutar)
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowTRYTutar(row) : 0), 0) if (columnAmount !== 0) return columnAmount
const liveAmount = resolveRowTRYTutar(row)
if (liveAmount !== 0) return liveAmount
return parseMoneyInput(row?.__lastValidTRYAmount ?? row?.lTutarTL)
} }
function resolveGroupUSDTutar (grp) { function resolveGroupRowUSDAmount (row) {
const items = Array.isArray(grp?.items) ? grp.items : [] const columnAmount = parseMoneyInput(row?.usdTutar ?? row?.lTutarUSD)
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0) if (columnAmount !== 0) return columnAmount
const liveAmount = resolveRowUSDTutar(row)
if (liveAmount !== 0) return liveAmount
return parseMoneyInput(row?.__lastValidUSDAmount)
} }
// OrderEntry groupedRows ile ayni desen: sub-header degerleri ayri bir
// state/cache icinde tutulmaz; iki gorunen tutar kolonu satirlardan reaktif
// olarak her degisiklikte yeniden toplanir.
const detailGroupsWithTotals = computed(() => (
detailGroups.value.map(group => {
const items = Array.isArray(group?.items) ? group.items : []
const totals = items.reduce((acc, row) => {
if (!shouldIncludeRowInGroupTotal(group, row)) return acc
acc.tryTotal += resolveGroupRowTRYAmount(row)
acc.usdTotal += resolveGroupRowUSDAmount(row)
return acc
}, { tryTotal: 0, usdTotal: 0 })
return {
...group,
tryTotal: totals.tryTotal,
usdTotal: totals.usdTotal
}
})
))
function resolveGroupQuantity (grp) { function resolveGroupQuantity (grp) {
const items = Array.isArray(grp?.items) ? grp.items : [] return (Array.isArray(grp?.items) ? grp.items : []).reduce((total, row) => (
return items.reduce((acc, row) => acc + (Number(row?.lMiktar || 0) || 0), 0) total + resolveNumericRowQuantity(row)
), 0)
} }
function groupKey (grp, gi) { function groupKey (grp, gi) {
@@ -3243,7 +3329,7 @@ function onRowQuantityInput (row, value) {
} }
function triggerUIUpdate () { function triggerUIUpdate () {
detailGroups.value = [...detailGroups.value] // detailGroupsWithTotals is computed from the reactive row fields.
} }
function normalizeRowQuantityDisplay (row) { function normalizeRowQuantityDisplay (row) {
@@ -3341,25 +3427,26 @@ async function fetchBulkItemPrices () {
} }
let appliedCount = 0 let appliedCount = 0
detailGroups.value = detailGroups.value.map(grp => ({ detailGroups.value.forEach(grp => {
...grp, const items = Array.isArray(grp?.items) ? grp.items : []
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => { items.forEach(row => {
const match = updates.find(update => rowMatchesBulkUpdate(row, update)) const match = updates.find(update => rowMatchesBulkUpdate(row, update))
if (!match) return row if (!match) return
appliedCount += 1 appliedCount += 1
return recalculateDetailRow({ row.inputPrice = match.inputPrice
...row, row.fiyat_girilen = match.fiyat_girilen
inputPrice: match.inputPrice, row.inputPricePrBr = match.inputPricePrBr
fiyat_girilen: match.fiyat_girilen, row.fiyat_doviz = match.fiyat_doviz
inputPricePrBr: match.inputPricePrBr, recalculateDetailRow(row, {
fiyat_doviz: match.fiyat_doviz
}, {
priceType: match.priceType || 'SAF', priceType: match.priceType || 'SAF',
updateState: 'bulk', updateState: 'bulk',
markChanged: true markChanged: true
}) })
}) })
})) })
// Update sub-headers immediately after the bulk response is applied. Do
// not wait for the optional previous-cost autofill request below.
triggerUIUpdate()
$q.notify({ $q.notify({
type: appliedCount > 0 ? 'positive' : 'warning', type: appliedCount > 0 ? 'positive' : 'warning',
@@ -3399,14 +3486,14 @@ async function fetchBulkItemPrices () {
}) })
let filled = 0 let filled = 0
detailGroups.value = detailGroups.value.map(grp => ({ detailGroups.value.forEach(grp => {
...grp, const items = Array.isArray(grp?.items) ? grp.items : []
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => { items.forEach(row => {
if (!row?.requiredPlaceholder) return row if (!row?.requiredPlaceholder) return
if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return row if (isCMGroupName(row?.sAciklama3) || normalizeGroupName(row?.sAciklama3) === 'FABRIC') return
const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0 const no = parseInt(String(row?.nHammaddeTuruNo || '').trim() || '0', 10) || 0
const hit = byNo[no] const hit = byNo[no]
if (!hit) return row if (!hit) return
const next = { ...row } const next = { ...row }
const hasCode = String(next.sKodu || '').trim() !== '' const hasCode = String(next.sKodu || '').trim() !== ''
@@ -3426,20 +3513,20 @@ async function fetchBulkItemPrices () {
// Only mark if something changed // Only mark if something changed
const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr) const changed = (next.sKodu !== row.sKodu) || (next.sAciklama !== row.sAciklama) || (next.inputPrice !== row.inputPrice) || (next.inputPricePrBr !== row.inputPricePrBr)
if (!changed) return row if (!changed) return
filled += 1 filled += 1
return recalculateDetailRow({ Object.assign(row, next, {
...next, __autoFilledFromPrev: true,
__autoFilledFromPrev: true
,
__autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma) __autoFilledFromPrevSameFirma: Boolean(hit?.is_same_firma || hit?.isSameFirma)
}, { })
recalculateDetailRow(row, {
priceType: 'PREV', priceType: 'PREV',
updateState: 'autofill', updateState: 'autofill',
markChanged: true markChanged: true
}) })
}) })
})) })
triggerUIUpdate()
if (filled > 0) { if (filled > 0) {
$q.notify({ $q.notify({
@@ -3456,6 +3543,11 @@ async function fetchBulkItemPrices () {
error: String(e?.message || e || '') error: String(e?.message || e || '')
}) })
} }
// Bulk price and previous-cost autofill paths can update many rows at once.
// Recalculate once more as a whole so group sub-header totals react immediately.
recalculateAllDetailRows()
schedulePersistLocalDraft()
triggerUIUpdate()
} catch (err) { } catch (err) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
@@ -3927,10 +4019,14 @@ async function ensureNoCostRequiredRowsFromMappings (mappings) {
const groupName = normalizeGroupName(meta?.groupName || '') const groupName = normalizeGroupName(meta?.groupName || '')
const hammaddeAdi = String(meta?.hammaddeAdi || '').trim() const hammaddeAdi = String(meta?.hammaddeAdi || '').trim()
const effectiveGroupName = groupName || 'TANIMSIZ' const effectiveGroupName = groupName || 'TANIMSIZ'
const mtBolumID = (meta?.mtBolumID > 0 ? meta.mtBolumID : mappingMtBolumID) || 0 // The product-type mapping is authoritative for the required part. A raw
const metaParcaCandidate = normalizeGroupName((meta?.parcaAdi || '').trim()) // material type can be required for multiple parts, while its master
// Defensive: if backend/lookup accidentally returns group label (DT/TP/...) as part name, ignore it. // record only carries one default MT section. Using that default here
const desiredParcaAdi = normalizeGroupName((metaParcaCandidate && !isKnownGroupName(metaParcaCandidate)) ? metaParcaCandidate : mappingParcaAdi) // incorrectly made an existing recipe row satisfy every mapped part.
const mtBolumID = mappingMtBolumID > 0 ? mappingMtBolumID : (meta?.mtBolumID || 0)
const desiredParcaAdi = mappingMtBolumID > 0
? mappingParcaAdi
: normalizeGroupName(meta?.parcaAdi || mappingParcaAdi)
const anyMatch = flatDetailRows.value.find(r => { const anyMatch = flatDetailRows.value.find(r => {
if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false if (normalizeHammaddeNo(r?.nHammaddeTuruNo) !== hNo) return false
@@ -4770,7 +4866,7 @@ async function confirmBrPriceDeviationIfNeeded () {
html: true, html: true,
message: ` message: `
<div style="margin-bottom:10px;"> <div style="margin-bottom:10px;">
Bazı satırlarda girilen fiyat, BAGGI_V3 satınalma geçmişindeki <b>son 10</b> kaydın USD ortalamasından <b>%10</b>'dan fazla sapıyor. Bazı satırlarda girilen fiyat, NEBIM_V3 satınalma geçmişindeki <b>son 10</b> kaydın USD ortalamasından <b>%10</b>'dan fazla sapıyor.
</div> </div>
<div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;"> <div style="max-height: 360px; overflow:auto; border:1px solid #e0e0e0; border-radius:6px;">
<table style="width:100%; border-collapse:collapse; font-size:13px;"> <table style="width:100%; border-collapse:collapse; font-size:13px;">
@@ -5061,13 +5157,31 @@ async function saveChanges () {
// If we created a new OnML (no-cost), switch to has-cost detail mode. // If we created a new OnML (no-cost), switch to has-cost detail mode.
if (isNoCostDetail.value && newOnMLNo > 0) { if (isNoCostDetail.value && newOnMLNo > 0) {
router.replace({ await router.replace({
name: 'production-product-costing-has-cost-detail', name: 'production-product-costing-has-cost-detail',
query: { query: {
n_onml_no: String(newOnMLNo), n_onml_no: String(newOnMLNo),
urun_kodu: String(header?.UrunKodu || productCode.value || '').trim() urun_kodu: String(header?.UrunKodu || productCode.value || '').trim()
} }
}) })
$q.dialog({
title: 'Maliyet Kaydedildi',
message: 'Maliyeti olmayan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
ok: {
label: 'Listeye Don',
color: 'primary',
icon: 'arrow_back'
},
cancel: {
label: 'Bu Sayfada Devam Et',
color: 'grey-7',
flat: true
},
persistent: true
}).onOk(() => {
router.push({ name: 'production-product-costing-no-cost' })
})
return return
} }
@@ -5077,6 +5191,24 @@ async function saveChanges () {
window.setTimeout(() => { window.setTimeout(() => {
try { refreshLast10Warnings() } catch {} try { refreshLast10Warnings() } catch {}
}, 1200) }, 1200)
$q.dialog({
title: 'Maliyet Guncellendi',
message: 'Mevcut maliyeti olan urunler listesine donup kaldiginiz yerden devam etmek ister misiniz?',
ok: {
label: 'Listeye Don',
color: 'primary',
icon: 'arrow_back'
},
cancel: {
label: 'Bu Sayfada Devam Et',
color: 'grey-7',
flat: true
},
persistent: true
}).onOk(() => {
router.push({ name: 'production-product-costing-has-cost' })
})
} catch (e) { } catch (e) {
// Surface backend message (http.Error text) when available. // Surface backend message (http.Error text) when available.
const msg = String( const msg = String(
@@ -5525,8 +5657,8 @@ watch(
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
padding: 8px 10px; padding: 8px 10px;
min-height: 42px; min-height: 46px;
height: 42px; height: auto;
border-top: 1px solid #d6c06a; border-top: 1px solid #d6c06a;
border-bottom: 1px solid #d6c06a; border-bottom: 1px solid #d6c06a;
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%); background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
@@ -5548,7 +5680,9 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: nowrap; flex-wrap: wrap;
row-gap: 4px;
max-width: 70%;
} }
.pcd-sub-header .sub-left { .pcd-sub-header .sub-left {
display: flex; display: flex;
@@ -5565,8 +5699,31 @@ watch(
user-select: none; user-select: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px 10px;
flex-wrap: nowrap; flex-wrap: wrap;
}
.pcd-group-total-text {
color: #2f2408;
font-size: 13px;
font-weight: 900;
white-space: nowrap;
}
.pcd-group-total-divider {
margin: 0 4px;
color: #8d6d13;
}
.pcd-group-debug {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 7px;
border: 1px solid rgba(180, 35, 24, 0.35);
border-radius: 5px;
color: #8f1d14;
background: rgba(255, 255, 255, 0.78);
font-size: 11px;
font-weight: 800;
text-transform: none;
white-space: nowrap; white-space: nowrap;
} }
.pcd-sub-mt-qty { .pcd-sub-mt-qty {
@@ -5576,6 +5733,18 @@ watch(
font-weight: 900; font-weight: 900;
} }
@media (max-width: 900px) {
.pcd-sub-header {
align-items: flex-start;
}
.pcd-sub-header .sub-right {
max-width: 78%;
}
.pcd-group-debug {
flex-basis: auto;
}
}
.pcd-detail-table :deep(.q-table__middle) { .pcd-detail-table :deep(.q-table__middle) {
overflow: visible !important; overflow: visible !important;
} }
@@ -5716,12 +5885,69 @@ watch(
} }
.pcd-history-table :deep(.q-table thead th) { .pcd-history-table :deep(.q-table thead th) {
font-size: 12px; font-size: 9.5px;
line-height: 1.1;
white-space: normal;
padding: 3px 2px;
} }
.pcd-history-table :deep(.q-table tbody td) { .pcd-history-table :deep(.q-table tbody td) {
font-size: 12px; font-size: 9.5px;
vertical-align: top; vertical-align: top;
padding: 2px;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.pcd-history-table :deep(.q-table__middle) {
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
.pcd-history-table :deep(.q-table) {
table-layout: fixed;
width: 100%;
min-width: 0;
}
.pcd-history-table :deep(.q-table th),
.pcd-history-table :deep(.q-table td) {
min-width: 0 !important;
}
.pcd-history-table :deep(.q-table th:nth-child(1)), .pcd-history-table :deep(.q-table td:nth-child(1)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(2)), .pcd-history-table :deep(.q-table td:nth-child(2)) { width: 6% !important; }
.pcd-history-table :deep(.q-table th:nth-child(3)), .pcd-history-table :deep(.q-table td:nth-child(3)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(4)), .pcd-history-table :deep(.q-table td:nth-child(4)) { width: 6% !important; }
.pcd-history-table :deep(.q-table th:nth-child(5)), .pcd-history-table :deep(.q-table td:nth-child(5)) { width: 8% !important; }
.pcd-history-table :deep(.q-table th:nth-child(6)), .pcd-history-table :deep(.q-table td:nth-child(6)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(7)), .pcd-history-table :deep(.q-table td:nth-child(7)) { width: 10% !important; }
.pcd-history-table :deep(.q-table th:nth-child(8)), .pcd-history-table :deep(.q-table td:nth-child(8)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(9)), .pcd-history-table :deep(.q-table td:nth-child(9)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(10)), .pcd-history-table :deep(.q-table td:nth-child(10)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(11)), .pcd-history-table :deep(.q-table td:nth-child(11)) { width: 7% !important; }
.pcd-history-table :deep(.q-table th:nth-child(12)), .pcd-history-table :deep(.q-table td:nth-child(12)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(13)), .pcd-history-table :deep(.q-table td:nth-child(13)) { width: 3% !important; }
.pcd-history-table :deep(.q-table th:nth-child(14)), .pcd-history-table :deep(.q-table td:nth-child(14)) { width: 5% !important; }
.pcd-history-table :deep(.q-table th:nth-child(15)), .pcd-history-table :deep(.q-table td:nth-child(15)) { width: 6% !important; }
.pcd-history-table :deep(.q-table th:nth-child(16)), .pcd-history-table :deep(.q-table td:nth-child(16)) { width: 4% !important; }
.pcd-history-table :deep(.q-table th:nth-child(17)), .pcd-history-table :deep(.q-table td:nth-child(17)) { width: 4% !important; }
.pcd-history-table :deep(.pcd-history-source-chip) {
min-width: 0;
max-width: 100%;
padding: 2px 4px;
font-size: 9px;
}
.pcd-history-table :deep(.q-btn) {
min-width: 0;
padding-left: 5px;
padding-right: 5px;
font-size: 9px;
} }
.pcd-history-table :deep(.pcd-history-row-purchase td) { .pcd-history-table :deep(.pcd-history-row-purchase td) {
@@ -5,7 +5,7 @@
<div class="col"> <div class="col">
<div class="text-h6">Maliyet Parca Eslestirme</div> <div class="text-h6">Maliyet Parca Eslestirme</div>
<div class="text-caption text-grey-7"> <div class="text-caption text-grey-7">
V3 Urun Ilk Grubu (42. ozellik) + Urun Ana/Alt Grup + URETIM Parca Bolum + Hammadde Turleri eslestirmesi (URETIM mk_ tablolarinda tutulur) NEBIM_V3 urun gruplari + TEDARIK URETIM PROGRAMI parca bolum + hammadde turleri eslestirmesi
</div> </div>
</div> </div>
+181 -26
View File
@@ -49,6 +49,14 @@
:loading="loading" :loading="loading"
@click="fetchRows" @click="fetchRows"
/> />
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="loading || rows.length === 0"
@click="exportVisibleRows"
/>
<div class="npc-missing-count text-caption text-grey-7"> <div class="npc-missing-count text-caption text-grey-7">
Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b> Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b>
</div> </div>
@@ -87,26 +95,52 @@
<q-menu class="npc-filter-menu" fit> <q-menu class="npc-filter-menu" fit>
<div class="npc-filter-menu-content"> <div class="npc-filter-menu-content">
<div class="text-caption text-weight-bold q-mb-sm">{{ props.col.label }}</div> <div class="text-caption text-weight-bold q-mb-sm">{{ props.col.label }}</div>
<q-input <template v-if="isNumericColumn(props.col.name)">
v-model="getColumnFilter(props.col.name).text" <div class="row q-col-gutter-sm">
dense <div class="col-6">
outlined <q-input
clearable v-model="getColumnFilter(props.col.name).min"
label="Icerir" dense
/> outlined
<q-select clearable
v-model="getColumnFilter(props.col.name).selected" inputmode="decimal"
class="q-mt-sm" label="Min"
dense />
outlined </div>
multiple <div class="col-6">
use-chips <q-input
use-input v-model="getColumnFilter(props.col.name).max"
emit-value dense
map-options outlined
:options="getColumnDistinctOptions(props.col.name)" clearable
label="Deger Sec" inputmode="decimal"
/> label="Max"
/>
</div>
</div>
</template>
<template v-else>
<q-input
v-model="getColumnFilter(props.col.name).text"
dense
outlined
clearable
label="Icerir"
/>
<q-select
v-model="getColumnFilter(props.col.name).selected"
class="q-mt-sm"
dense
outlined
multiple
use-chips
use-input
emit-value
map-options
:options="getColumnDistinctOptions(props.col.name)"
label="Deger Sec"
/>
</template>
<div class="row justify-end q-gutter-sm q-mt-sm"> <div class="row justify-end q-gutter-sm q-mt-sm">
<q-btn dense flat color="grey-7" label="Temizle" @click="clearColumnFilter(props.col.name)" /> <q-btn dense flat color="grey-7" label="Temizle" @click="clearColumnFilter(props.col.name)" />
</div> </div>
@@ -150,7 +184,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
@@ -218,6 +252,9 @@ const columns = [
style: 'width:7%', style: 'width:7%',
headerStyle: 'width:7%' headerStyle: 'width:7%'
}, },
{ name: 'ceketDepoGiris', label: 'C-Ceket Depo Giris', field: 'ceketDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'pantolonDepoGiris', label: 'P-Pantolon Depo Giris', field: 'pantolonDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
{ name: 'yelekDepoGiris', label: 'Y-Yelek Depo Giris', field: 'yelekDepoGiris', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width:7%', headerStyle: 'width:7%' },
{ {
name: 'sMModelKodu', name: 'sMModelKodu',
label: 'Model Kodu', label: 'Model Kodu',
@@ -236,17 +273,66 @@ const columns = [
] ]
const columnFilters = reactive({}) const columnFilters = reactive({})
const LIST_STATE_STORAGE_KEY = 'bssapp:production-product-costing:no-cost-list-state:v1'
function persistListState () {
try {
sessionStorage.setItem(LIST_STATE_STORAGE_KEY, JSON.stringify({
filters: {
search: String(filters.search || ''),
fromDate: String(filters.fromDate || '')
},
columnFilters: JSON.parse(JSON.stringify(columnFilters)),
scrollY: Number(window.scrollY || 0)
}))
} catch {}
}
function restoreListState () {
try {
const raw = sessionStorage.getItem(LIST_STATE_STORAGE_KEY)
if (!raw) return 0
const saved = JSON.parse(raw)
filters.search = String(saved?.filters?.search || '')
filters.fromDate = String(saved?.filters?.fromDate || '2025-06-01')
Object.assign(columnFilters, saved?.columnFilters && typeof saved.columnFilters === 'object' ? saved.columnFilters : {})
return Number(saved?.scrollY || 0)
} catch {
return 0
}
}
function getColumnFilter (name) { function getColumnFilter (name) {
if (!columnFilters[name]) { if (!columnFilters[name]) {
columnFilters[name] = { columnFilters[name] = {
text: '', text: '',
selected: [] selected: [],
min: '',
max: ''
} }
} }
return columnFilters[name] return columnFilters[name]
} }
function isNumericColumn (name) {
return ['lMMiktar_G', 'ceketDepoGiris', 'pantolonDepoGiris', 'yelekDepoGiris'].includes(name)
}
function formatQuantity (value) {
return Number(value || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function parseNumberFilter (value) {
const raw = String(value ?? '').trim()
if (!raw) return null
const normalized = raw
.replace(/\s/g, '')
.replace(/\./g, '')
.replace(',', '.')
const n = Number(normalized)
return Number.isFinite(n) ? n : null
}
function formatDateTR (value) { function formatDateTR (value) {
const s = String(value || '').trim() const s = String(value || '').trim()
if (!s) return '' if (!s) return ''
@@ -264,10 +350,19 @@ const rows = computed(() => {
const cf = getColumnFilter(col.name) const cf = getColumnFilter(col.name)
const text = String(cf.text || '').trim().toLowerCase() const text = String(cf.text || '').trim().toLowerCase()
const selected = Array.isArray(cf.selected) ? cf.selected : [] const selected = Array.isArray(cf.selected) ? cf.selected : []
const min = parseNumberFilter(cf.min)
const max = parseNumberFilter(cf.max)
if (!text && selected.length === 0) continue if (!text && selected.length === 0 && min === null && max === null) continue
result = result.filter((row) => { result = result.filter((row) => {
if (isNumericColumn(col.name)) {
const numericValue = Number(row?.[col.name] || 0)
if (min !== null && numericValue < min) return false
if (max !== null && numericValue > max) return false
return true
}
const value = getColumnComparableValue(row, col.name) const value = getColumnComparableValue(row, col.name)
const valueLC = value.toLowerCase() const valueLC = value.toLowerCase()
@@ -310,13 +405,18 @@ function getColumnDistinctOptions (colName) {
function isColumnFilterActive (name) { function isColumnFilterActive (name) {
const cf = getColumnFilter(name) const cf = getColumnFilter(name)
return !!String(cf.text || '').trim() || (Array.isArray(cf.selected) && cf.selected.length > 0) return !!String(cf.text || '').trim() ||
(Array.isArray(cf.selected) && cf.selected.length > 0) ||
parseNumberFilter(cf.min) !== null ||
parseNumberFilter(cf.max) !== null
} }
function clearColumnFilter (name) { function clearColumnFilter (name) {
const cf = getColumnFilter(name) const cf = getColumnFilter(name)
cf.text = '' cf.text = ''
cf.selected = [] cf.selected = []
cf.min = ''
cf.max = ''
} }
function clearAllColumnFilters () { function clearAllColumnFilters () {
@@ -327,9 +427,11 @@ function clearAllColumnFilters () {
} }
let searchTimer = null let searchTimer = null
let filterWatchEnabled = false
watch( watch(
() => filters.search, () => filters.search,
() => { () => {
if (!filterWatchEnabled) return
clearTimeout(searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => { searchTimer = setTimeout(() => {
fetchRows() fetchRows()
@@ -340,6 +442,7 @@ watch(
watch( watch(
() => filters.fromDate, () => filters.fromDate,
() => { () => {
if (!filterWatchEnabled) return
fetchRows() fetchRows()
} }
) )
@@ -373,6 +476,49 @@ function clearFilters () {
fetchRows() fetchRows()
} }
function escapeExcelCsvCell (value) {
let text = String(value ?? '')
// Prevent spreadsheet applications from evaluating data as a formula.
if (/^[=+\-@]/.test(text)) text = `'${text}`
return `"${text.replace(/"/g, '""')}"`
}
function formatExcelCsvCell (col, rawValue, displayValue) {
if (isNumericColumn(col?.name)) {
const numericValue = Number(rawValue)
if (Number.isFinite(numericValue)) return String(numericValue).replace('.', ',')
}
return escapeExcelCsvCell(displayValue)
}
function exportVisibleRows () {
const visibleRows = Array.isArray(rows.value) ? rows.value : []
if (visibleRows.length === 0) {
$q.notify({ type: 'warning', message: 'Excel aktarimi icin ekranda satir bulunamadi.', position: 'top-right' })
return
}
const exportColumns = columns.filter(col => col.name !== 'open')
const csvLines = [
exportColumns.map(col => escapeExcelCsvCell(col.label)).join(';'),
...visibleRows.map(row => exportColumns.map(col => {
const rawValue = typeof col.field === 'function' ? col.field(row) : row?.[col.field]
const displayValue = typeof col.format === 'function' ? col.format(rawValue, row) : rawValue
return formatExcelCsvCell(col, rawValue, displayValue)
}).join(';'))
]
const blob = new Blob([`\uFEFF${csvLines.join('\r\n')}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `maliyeti_olmayan_urunler_${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(link)
link.click()
link.remove()
URL.revokeObjectURL(url)
}
function openRow (row) { function openRow (row) {
const productCode = String(row?.sMModelKodu || '').trim() const productCode = String(row?.sMModelKodu || '').trim()
const recipeCode = String(row?.sKodu || '').trim() const recipeCode = String(row?.sKodu || '').trim()
@@ -392,6 +538,8 @@ function openRow (row) {
recipe_code: recipeCode recipe_code: recipeCode
}) })
persistListState()
router.push({ router.push({
name: 'production-product-costing-has-cost-detail', name: 'production-product-costing-has-cost-detail',
query: { query: {
@@ -403,9 +551,16 @@ function openRow (row) {
}) })
} }
onMounted(() => { onMounted(async () => {
if (!canReadOrder.value) return if (!canReadOrder.value) return
fetchRows() const savedScrollY = restoreListState()
await nextTick()
filterWatchEnabled = true
await fetchRows()
if (savedScrollY > 0) {
await nextTick()
window.scrollTo({ top: savedScrollY, behavior: 'auto' })
}
}) })
</script> </script>
+5 -9
View File
@@ -207,6 +207,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
import { useTranslationStore } from 'src/stores/translationStore' import { useTranslationStore } from 'src/stores/translationStore'
import { formatDataSourceLabel } from 'src/utils/formatters'
const $q = useQuasar() const $q = useQuasar()
const store = useTranslationStore() const store = useTranslationStore()
@@ -231,15 +232,10 @@ const tablePagination = ref({
let filterReloadTimer = null let filterReloadTimer = null
const sourceTypeOptions = [ const sourceTypeOptions = [
{ label: 'dummy', value: 'dummy' }, { label: formatDataSourceLabel('dummy'), value: 'dummy' },
{ label: 'postgre', value: 'postgre' }, { label: formatDataSourceLabel('postgre'), value: 'postgre' },
{ label: 'mssql', value: 'mssql' } { label: formatDataSourceLabel('mssql'), value: 'mssql' }
] ]
const sourceTypeLabelMap = {
dummy: 'UI',
postgre: 'PostgreSQL',
mssql: 'MSSQL'
}
const columns = [ const columns = [
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' }, { name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
@@ -413,7 +409,7 @@ function cellClass (key, field) {
function sourceTypeLabel (key) { function sourceTypeLabel (key) {
const val = String(rowDraft(key).source_type || 'dummy').toLowerCase() const val = String(rowDraft(key).source_type || 'dummy').toLowerCase()
return sourceTypeLabelMap[val] || val || '-' return formatDataSourceLabel(val)
} }
function toggleSelected (key, checked) { function toggleSelected (key, checked) {
+24 -24
View File
@@ -3,9 +3,8 @@
v-if="canReadUser" v-if="canReadUser"
class="q-pa-md user-sync-page" class="q-pa-md user-sync-page"
> >
<div class="row items-center justify-between q-mb-md"> <div class="row items-center justify-between q-mb-md">
<div class="text-h6 text-primary">👤 Kullanıcı Yönetimi</div> <div class="text-h6 text-primary">Kullanici Yonetimi</div>
<q-btn <q-btn
v-if="canUpdateUser" v-if="canUpdateUser"
color="primary" color="primary"
@@ -20,11 +19,10 @@
<q-separator /> <q-separator />
<div class="row q-col-gutter-md q-mt-md"> <div class="row q-col-gutter-md q-mt-md">
<!-- 🔹 PostgreSQL Kullanıcıları -->
<div class="col-6"> <div class="col-6">
<q-card flat bordered> <q-card flat bordered>
<q-card-section class="bg-primary text-white text-subtitle1"> <q-card-section class="bg-primary text-white text-subtitle1">
PostgreSQL Kullanıcıları B2B Kullanicilari
</q-card-section> </q-card-section>
<q-table <q-table
@@ -36,7 +34,7 @@
bordered bordered
separator="cell" separator="cell"
> >
<template v-slot:body-cell-sync_status="props"> <template #body-cell-sync_status="props">
<q-td :props="props"> <q-td :props="props">
<q-chip <q-chip
:color="statusColor(props.row.sync_status)" :color="statusColor(props.row.sync_status)"
@@ -48,23 +46,27 @@
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-actions="props"> <template #body-cell-actions="props">
<q-td :props="props"> <q-td :props="props">
<q-btn <q-btn
v-if="canUpdateUser" v-if="canUpdateUser"
dense flat icon="link" dense
flat
icon="link"
color="primary" color="primary"
size="sm" size="sm"
@click="openMapDialog(props.row)"
:disable="store.loading || !canUpdateUser" :disable="store.loading || !canUpdateUser"
@click="openMapDialog(props.row)"
/> />
<q-btn <q-btn
v-if="canUpdateUser" v-if="canUpdateUser"
dense flat icon="link_off" dense
flat
icon="link_off"
color="negative" color="negative"
size="sm" size="sm"
@click="store.unmap(props.row.id)"
:disable="store.loading || !props.row.mssql_username || !canUpdateUser" :disable="store.loading || !props.row.mssql_username || !canUpdateUser"
@click="store.unmap(props.row.id)"
/> />
</q-td> </q-td>
</template> </template>
@@ -72,11 +74,10 @@
</q-card> </q-card>
</div> </div>
<!-- 🔸 MSSQL Kullanıcıları -->
<div class="col-6"> <div class="col-6">
<q-card flat bordered> <q-card flat bordered>
<q-card-section class="bg-secondary text-white text-subtitle1"> <q-card-section class="bg-secondary text-white text-subtitle1">
MSSQL Kullanıcıları NEBIM_V3 Kullanicilari
</q-card-section> </q-card-section>
<q-table <q-table
@@ -88,7 +89,7 @@
bordered bordered
separator="cell" separator="cell"
> >
<template v-slot:body-cell-is_blocked="props"> <template #body-cell-is_blocked="props">
<q-td :props="props"> <q-td :props="props">
<q-chip <q-chip
:color="props.row.is_blocked ? 'negative' : 'positive'" :color="props.row.is_blocked ? 'negative' : 'positive'"
@@ -117,9 +118,9 @@
<script setup> <script setup>
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useUserSyncStore } from 'src/stores/userSyncStore'
import { Dialog } from 'quasar' import { Dialog } from 'quasar'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
import { useUserSyncStore } from 'src/stores/userSyncStore'
const { canRead, canUpdate } = usePermission() const { canRead, canUpdate } = usePermission()
const canReadUser = canRead('user') const canReadUser = canRead('user')
@@ -132,20 +133,20 @@ const pgColumns = [
{ name: 'code', label: 'Kodu', field: 'code', align: 'left' }, { name: 'code', label: 'Kodu', field: 'code', align: 'left' },
{ name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left' }, { name: 'full_name', label: 'Ad Soyad', field: 'full_name', align: 'left' },
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' }, { name: 'email', label: 'E-posta', field: 'email', align: 'left' },
{ name: 'mssql_username', label: 'MSSQL Kullanıcı', field: 'mssql_username', align: 'left' }, { name: 'mssql_username', label: 'NEBIM_V3 Kullanicisi', field: 'mssql_username', align: 'left' },
{ name: 'sync_status', label: 'Durum', field: 'sync_status', align: 'center' }, { name: 'sync_status', label: 'Durum', field: 'sync_status', align: 'center' },
{ name: 'actions', label: 'İşlemler', field: 'actions', align: 'center' } { name: 'actions', label: 'Islemler', field: 'actions', align: 'center' }
] ]
const msColumns = [ const msColumns = [
{ name: 'username', label: 'Kullanıcı Adı', field: 'username', align: 'left' }, { name: 'username', label: 'Kullanici Adi', field: 'username', align: 'left' },
{ name: 'first_name', label: 'Ad', field: 'first_name', align: 'left' }, { name: 'first_name', label: 'Ad', field: 'first_name', align: 'left' },
{ name: 'last_name', label: 'Soyad', field: 'last_name', align: 'left' }, { name: 'last_name', label: 'Soyad', field: 'last_name', align: 'left' },
{ name: 'email', label: 'E-posta', field: 'email', align: 'left' }, { name: 'email', label: 'E-posta', field: 'email', align: 'left' },
{ name: 'is_blocked', label: 'Durum', field: 'is_blocked', align: 'center' } { name: 'is_blocked', label: 'Durum', field: 'is_blocked', align: 'center' }
] ]
function statusColor(status) { function statusColor (status) {
switch (status) { switch (status) {
case 'synced': return 'positive' case 'synced': return 'positive'
case 'manual': return 'primary' case 'manual': return 'primary'
@@ -155,14 +156,12 @@ function statusColor(status) {
} }
} }
function openMapDialog(pgUser) { function openMapDialog (pgUser) {
if (!canUpdateUser.value) { if (!canUpdateUser.value) return
return
}
Dialog.create({ Dialog.create({
title: 'Kullanıcı Eşleme', title: 'Kullanici Esleme',
message: 'Bu PostgreSQL kullanıcısını hangi MSSQL kullanıcısına bağlamak istiyorsunuz?', message: 'Bu B2B kullanicisini hangi NEBIM_V3 kullanicisina baglamak istiyorsunuz?',
options: { options: {
type: 'radio', type: 'radio',
model: '', model: '',
@@ -186,6 +185,7 @@ onMounted(() => {
.user-sync-page { .user-sync-page {
background: #fafafa; background: #fafafa;
} }
.q-card-section { .q-card-section {
font-weight: 600; font-weight: 600;
} }
+12
View File
@@ -394,6 +394,12 @@ const routes = [
component: () => import('pages/ProductPricing.vue'), component: () => import('pages/ProductPricing.vue'),
meta: { permission: 'pricing:view' } meta: { permission: 'pricing:view' }
}, },
{
path: 'pricing/product-performance-profitability',
name: 'product-performance-profitability',
component: () => import('pages/ProductPerformanceProfitability.vue'),
meta: { permission: 'pricing:view' }
},
{ {
path: 'pricing/brand-classification', path: 'pricing/brand-classification',
name: 'brand-classification', name: 'brand-classification',
@@ -425,6 +431,12 @@ const routes = [
component: () => import('pages/ProductSeriesMappings.vue'), component: () => import('pages/ProductSeriesMappings.vue'),
meta: { permission: 'pricing:view' } meta: { permission: 'pricing:view' }
}, },
{
path: 'pricing/product-series-mappings-orphans',
name: 'product-series-mappings-orphans',
component: () => import('pages/ProductSeriesMappingsOrphans.vue'),
meta: { permission: 'pricing:view' }
},
{ {
path: 'pricing/product-series-definitions', path: 'pricing/product-series-definitions',
name: 'product-series-definitions', name: 'product-series-definitions',
+31 -1
View File
@@ -3,6 +3,7 @@ import qs from 'qs'
import { useAuthStore } from 'stores/authStore' import { useAuthStore } from 'stores/authStore'
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages' import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
import { slog } from 'src/utils/slog' import { slog } from 'src/utils/slog'
import { formatDataSourceLabel } from 'src/utils/formatters'
const rawBaseUrl = const rawBaseUrl =
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api' (typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
@@ -63,7 +64,7 @@ function sanitizeApiErrorDetail(detail, status) {
return `Unexpected HTML error response (${status || '-'})` return `Unexpected HTML error response (${status || '-'})`
} }
const compact = normalized.replace(/\s+/g, ' ').trim() const compact = normalizeTechnicalSourceLabels(normalized.replace(/\s+/g, ' ').trim())
if (compact.length > 320) { if (compact.length > 320) {
return `${compact.slice(0, 320)}...` return `${compact.slice(0, 320)}...`
} }
@@ -71,6 +72,17 @@ function sanitizeApiErrorDetail(detail, status) {
return compact return compact
} }
function normalizeTechnicalSourceLabels(text) {
return String(text || '')
.replace(/\bPostgreSQL\b/gi, formatDataSourceLabel('postgresql'))
.replace(/\bPostgres\b/gi, formatDataSourceLabel('postgres'))
.replace(/\bpostgre\b/gi, formatDataSourceLabel('postgre'))
.replace(/\bPG\b/g, formatDataSourceLabel('pg'))
.replace(/\bMSSQL\b/gi, formatDataSourceLabel('mssql'))
.replace(/\bBAGGI_V3\b/gi, formatDataSourceLabel('BAGGI_V3'))
.replace(/\bURETIM\b/g, formatDataSourceLabel('URETIM'))
}
function redirectToLogin() { function redirectToLogin() {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
if (window.location.hash === '#/login') return if (window.location.hash === '#/login') return
@@ -203,6 +215,8 @@ api.interceptors.response.use(
requestUrl.startsWith('/me/password') requestUrl.startsWith('/me/password')
const isPublicRequest = isPublicPath(requestUrl) const isPublicRequest = isPublicPath(requestUrl)
normalizeErrorResponsePayload(error)
if ((status >= 500 || hasBlob) && error) { if ((status >= 500 || hasBlob) && error) {
const method = String(requestConfig.method || 'GET').toUpperCase() const method = String(requestConfig.method || 'GET').toUpperCase()
const detail = sanitizeApiErrorDetail( const detail = sanitizeApiErrorDetail(
@@ -262,6 +276,22 @@ api.interceptors.response.use(
} }
) )
function normalizeErrorResponsePayload(error) {
const data = error?.response?.data
if (!data || typeof Blob !== 'undefined' && data instanceof Blob) return
if (typeof data === 'string') {
error.response.data = normalizeTechnicalSourceLabels(data)
return
}
if (typeof data === 'object') {
for (const key of ['detail', 'message', 'error']) {
if (typeof data[key] === 'string') {
data[key] = normalizeTechnicalSourceLabels(data[key])
}
}
}
}
export const get = (u, p = {}, c = {}) => export const get = (u, p = {}, c = {}) =>
api.get(u, { params: p, ...c }).then(r => r.data) api.get(u, { params: p, ...c }).then(r => r.data)
+7 -1
View File
@@ -53,7 +53,13 @@ const SIZE_GROUP_TITLES = {
} }
const FALLBACK_SCHEMA_MAP = { const FALLBACK_SCHEMA_MAP = {
tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] } tak: { key: 'tak', title: 'TAKIM ELBISE', values: ['44', '46', '48', '50', '52', '54', '56', '58', '60', '62', '64', '66', '68', '70', '72', '74'] },
ayk: { key: 'ayk', title: 'AYAKKABI', values: ['39', '40', '41', '42', '43', '44', '45'] },
ayk_garson: { key: 'ayk_garson', title: 'AYAKKABI GARSON', values: ['31', '32', '33', '34', '35', '36', '37', '38'] },
yas: { key: 'yas', title: 'YAS', values: ['2', '4', '6', '8', '10', '12', '14'] },
pan: { key: 'pan', title: 'PANTOLON', values: ['38', '40', '42', '44', '46', '48', '50', '52', '54', '56', '58', '60'] },
gom: { key: 'gom', title: 'GOMLEK', values: ['XS', 'S', 'M', 'L', 'XL', '2XL', '3XL', '4XL', '5XL', '6XL', '7XL'] },
aksbir: { key: 'aksbir', title: 'AKSESUAR', values: [' '] }
} }
export const schemaByKey = { ...FALLBACK_SCHEMA_MAP } export const schemaByKey = { ...FALLBACK_SCHEMA_MAP }
+264
View File
@@ -0,0 +1,264 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
const GROUPED_TTL_MS = 2 * 60 * 1000
const IMAGE_TTL_MS = 15 * 60 * 1000
function resolveProductImageUrl (item) {
return resolveProductImageUrls(item)[0] || ''
}
function resolveProductImageUrls (item) {
if (!item || typeof item !== 'object') return []
const urls = []
const addURL = value => {
const url = String(value || '').trim()
if (url && !urls.includes(url)) urls.push(url)
}
const thumbURL = String(item.thumb_url || item.thumbUrl || '').trim()
addURL(thumbURL)
const fullURL = String(item.full_url || item.fullUrl || '').trim()
addURL(fullURL)
const contentURL = String(item.content_url || item.ContentURL || '').trim()
if (contentURL.startsWith('/api/')) addURL(contentURL)
else if (contentURL.startsWith('/')) addURL(`/api${contentURL}`)
return urls
}
function normalizedFilterMap (filters = {}) {
return Object.fromEntries(Object.entries(filters || {})
.map(([key, values]) => [
String(key || '').trim(),
normalizedList(values, true)
])
.filter(([key, values]) => key && values.length)
.sort((a, b) => a[0].localeCompare(b[0], 'tr')))
}
function normalizedList (value, sort = false) {
if (!Array.isArray(value)) return []
const out = value.map(x => String(x || '').trim()).filter(Boolean)
return sort ? out.sort() : out
}
export const useProductPerformanceStore = defineStore('product-performance-store', {
state: () => ({
groupedRowsByKey: {},
groupedLoadedAtByKey: {},
groupedLoadingByKey: {},
groupedInFlightByKey: {},
groupedFilterOptionsByKey: {},
groupedFilterOptionsLoadedAtByKey: {},
groupedFilterOptionsInFlightByKey: {},
imageUrlByKey: {},
imageListByKey: {},
imageLoadedAtByKey: {}
}),
getters: {
groupedRows: state => key => state.groupedRowsByKey[key] || [],
hasImageKey: state => key => Object.prototype.hasOwnProperty.call(state.imageListByKey, key),
imageUrl: state => key => state.imageUrlByKey[key] || '',
imageList: state => key => state.imageListByKey[key] || []
},
actions: {
groupedCacheKey (params = {}) {
return JSON.stringify({
mode: String(params.mode || ''),
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
groupLevels: normalizedList(params.groupLevels),
expandedKeys: normalizedList(params.expandedKeys, true),
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
filters: normalizedFilterMap(params.filters),
sortBy: String(params.sortBy || params.sort_by || ''),
descending: params.descending !== false,
limit: Number(params.limit || 0)
})
},
groupedFilterOptionsCacheKey (params = {}) {
return JSON.stringify({
mode: String(params.mode || ''),
mainGroup: String(params.urunAnaGrubu || params.urun_ana_grubu || ''),
groupLevels: normalizedList(params.groupLevels),
fields: normalizedList(params.fields, true),
limit: Number(params.limit || 0)
})
},
async fetchGroupedRows (params = {}, options = {}) {
const key = this.groupedCacheKey(params)
const now = Date.now()
const loadedAt = Number(this.groupedLoadedAtByKey[key] || 0)
const cached = this.groupedRowsByKey[key]
const logPrefix = '[ProductPerformance][grouped]'
if (!options.force && Array.isArray(cached) && now - loadedAt < GROUPED_TTL_MS) {
console.info(`${logPrefix} cache hit`, {
mode: params.mode,
rows: cached.length,
ageMs: now - loadedAt
})
return cached
}
if (this.groupedInFlightByKey[key]) {
console.info(`${logPrefix} in-flight reuse`, {
mode: params.mode,
groupLevels: normalizedList(params.groupLevels),
expandedKeys: normalizedList(params.expandedKeys, true).length,
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1)
})
return this.groupedInFlightByKey[key]
}
this.groupedLoadingByKey = { ...this.groupedLoadingByKey, [key]: true }
const startedAt = performance.now()
console.info(`${logPrefix} request start`, {
mode: params.mode,
groupLevels: normalizedList(params.groupLevels),
expandedKeys: normalizedList(params.expandedKeys, true).length,
expandThroughLevel: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
limit: params.limit
})
const request = api.post('/pricing/product-performance/grouped', {
mode: params.mode,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
expanded_keys: Array.isArray(params.expandedKeys) ? params.expandedKeys : String(params.expandedKeys || '').split(',').filter(Boolean),
expand_through_level: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
filters: normalizedFilterMap(params.filters),
sort_by: String(params.sortBy || params.sort_by || ''),
descending: params.descending !== false,
limit: params.limit
}, {
params: {
mode: params.mode,
limit: params.limit,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
expand_through_level: Number(params.expandThroughLevel ?? params.expand_through_level ?? -1),
sort_by: String(params.sortBy || params.sort_by || ''),
descending: params.descending !== false
},
timeout: 90000
}).then(resp => {
const rows = Array.isArray(resp?.data) ? resp.data : []
this.groupedRowsByKey = { ...this.groupedRowsByKey, [key]: rows }
this.groupedLoadedAtByKey = { ...this.groupedLoadedAtByKey, [key]: Date.now() }
console.info(`${logPrefix} request done`, {
mode: params.mode,
rows: rows.length,
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2))
})
return rows
}).catch(err => {
const message = err?.response?.data || err?.message || String(err || '')
console.warn(`${logPrefix} request failed: ${message}`, {
mode: params.mode,
elapsedSec: Number(((performance.now() - startedAt) / 1000).toFixed(2)),
message
})
throw err
}).finally(() => {
const nextLoading = { ...this.groupedLoadingByKey }
const nextInFlight = { ...this.groupedInFlightByKey }
delete nextLoading[key]
delete nextInFlight[key]
this.groupedLoadingByKey = nextLoading
this.groupedInFlightByKey = nextInFlight
})
this.groupedInFlightByKey = { ...this.groupedInFlightByKey, [key]: request }
return request
},
async fetchGroupedFilterOptions (params = {}, options = {}) {
const key = this.groupedFilterOptionsCacheKey(params)
const now = Date.now()
const loadedAt = Number(this.groupedFilterOptionsLoadedAtByKey[key] || 0)
const cached = this.groupedFilterOptionsByKey[key]
if (!options.force && cached && now - loadedAt < GROUPED_TTL_MS) return cached
if (this.groupedFilterOptionsInFlightByKey[key]) return this.groupedFilterOptionsInFlightByKey[key]
const request = api.post('/pricing/product-performance/grouped/filter-options', {
mode: params.mode,
urun_ana_grubu: params.urunAnaGrubu || params.urun_ana_grubu || '',
group_levels: Array.isArray(params.groupLevels) ? params.groupLevels : String(params.groupLevels || '').split(',').filter(Boolean),
fields: Array.isArray(params.fields) ? params.fields : String(params.fields || '').split(',').filter(Boolean),
limit: params.limit || 50000
}, { timeout: 60000 }).then(resp => {
const data = resp?.data && typeof resp.data === 'object' ? resp.data : {}
this.groupedFilterOptionsByKey = { ...this.groupedFilterOptionsByKey, [key]: data }
this.groupedFilterOptionsLoadedAtByKey = { ...this.groupedFilterOptionsLoadedAtByKey, [key]: Date.now() }
return data
}).finally(() => {
const nextInFlight = { ...this.groupedFilterOptionsInFlightByKey }
delete nextInFlight[key]
this.groupedFilterOptionsInFlightByKey = nextInFlight
})
this.groupedFilterOptionsInFlightByKey = { ...this.groupedFilterOptionsInFlightByKey, [key]: request }
return request
},
setImageCache (key, urls = []) {
const normalizedKey = String(key || '').trim()
if (!normalizedKey) return
const list = Array.isArray(urls) ? urls.filter(Boolean) : []
this.imageListByKey = { ...this.imageListByKey, [normalizedKey]: list }
this.imageUrlByKey = { ...this.imageUrlByKey, [normalizedKey]: list[0] || '' }
this.imageLoadedAtByKey = { ...this.imageLoadedAtByKey, [normalizedKey]: Date.now() }
},
async fetchImageBatch (items = [], options = {}) {
const now = Date.now()
const pending = []
const seen = new Set()
for (const item of items) {
const key = String(item?.key || '').trim()
if (!key || seen.has(key)) continue
seen.add(key)
const loadedAt = Number(this.imageLoadedAtByKey[key] || 0)
if (!options.force && Object.prototype.hasOwnProperty.call(this.imageListByKey, key) && now - loadedAt < IMAGE_TTL_MS) continue
pending.push({
key,
code: String(item?.code || '').trim(),
dim1: String(item?.dim1 || '').trim(),
dim3: String(item?.dim3 || '').trim()
})
}
if (!pending.length) {
const keys = Array.from(seen.values())
return {
lists: Object.fromEntries(keys.map(key => [key, this.imageListByKey[key] || []])),
urls: Object.fromEntries(keys.map(key => [key, this.imageUrlByKey[key] || '']))
}
}
const resp = await api.post('/product-images/batch', { items: pending }, { timeout: 60000 })
const returned = new Set()
const lists = {}
const urlsByKey = {}
for (const item of Array.isArray(resp?.data) ? resp.data : []) {
const key = String(item?.key || '').trim()
if (!key) continue
returned.add(key)
const urls = (Array.isArray(item.images) ? item.images : []).flatMap(resolveProductImageUrls).filter(Boolean)
this.setImageCache(key, urls)
lists[key] = urls
urlsByKey[key] = urls[0] || ''
}
for (const item of pending) {
if (returned.has(item.key)) continue
this.setImageCache(item.key, [])
lists[item.key] = []
urlsByKey[item.key] = ''
}
return { lists, urls: urlsByKey }
}
}
})
+91
View File
@@ -0,0 +1,91 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import api from 'src/services/api'
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
function normalizeRuntimeText (value) {
return String(value || '').trim().replace(/\s+/g, ' ')
}
export const useRuntimeTranslationStore = defineStore('runtimeTranslation', () => {
const locale = ref(DEFAULT_LOCALE)
const byKey = ref({})
const byText = ref({})
const loading = ref(false)
const loadedLocales = ref({})
const version = ref(0)
async function loadLocale (nextLocale, options = {}) {
const normalized = normalizeLocale(nextLocale)
const force = Boolean(options?.force)
locale.value = normalized
if (!force && loadedLocales.value[normalized]) {
byKey.value = loadedLocales.value[normalized].byKey
byText.value = loadedLocales.value[normalized].byText
version.value += 1
return true
}
loading.value = true
try {
const res = await api.get('/language/translations/runtime', {
params: { lang: normalized },
timeout: 180000
})
const payload = res?.data || {}
const nextByKey = payload?.by_key && typeof payload.by_key === 'object' ? payload.by_key : {}
const nextByTextRaw = payload?.by_text && typeof payload.by_text === 'object' ? payload.by_text : {}
const nextByText = {}
for (const [source, translated] of Object.entries(nextByTextRaw)) {
const sourceKey = normalizeRuntimeText(source)
const display = normalizeRuntimeText(translated)
if (sourceKey && display) nextByText[sourceKey] = display
}
loadedLocales.value = {
...loadedLocales.value,
[normalized]: {
byKey: nextByKey,
byText: nextByText
}
}
byKey.value = nextByKey
byText.value = nextByText
version.value += 1
return true
} catch (err) {
console.warn('[runtime-i18n] dictionary load failed', normalized, err)
byKey.value = {}
byText.value = {}
version.value += 1
return false
} finally {
loading.value = false
}
}
function translateKey (key) {
const raw = String(key || '').trim()
if (!raw) return ''
return byKey.value[raw] || raw
}
function translateText (text) {
const normalized = normalizeRuntimeText(text)
if (!normalized) return ''
return byText.value[normalized] || normalized
}
return {
locale,
byKey,
byText,
loading,
version,
loadLocale,
translateKey,
translateText
}
})
+10
View File
@@ -84,3 +84,13 @@ export function formatPercent(value) {
const n = Number(value || 0) const n = Number(value || 0)
return `${(n * 100).toFixed(2)}%` return `${(n * 100).toFixed(2)}%`
} }
export function formatDataSourceLabel(value) {
const raw = String(value || '').trim()
const normalized = raw.toLowerCase().replace(/[\s-]+/g, '_')
if (['pg', 'postgre', 'postgres', 'postgresql'].includes(normalized)) return 'B2B'
if (['mssql', 'baggi_v3', 'baggi'].includes(normalized)) return 'NEBIM_V3'
if (normalized === 'uretim') return 'TEDARIK URETIM PROGRAMI'
if (normalized === 'dummy' || normalized === 'ui') return 'UI'
return raw || '-'
}