1534 lines
40 KiB
Go
1534 lines
40 KiB
Go
// ===================== PART 1 (Satır 1-300) =====================
|
||
// =======================================================
|
||
// order_write.go — v4.3 FINAL (Insert + Update + Delete)
|
||
// - PCTCode her zaman "%0" olarak yazılır (cdPCT FK uyumlu)
|
||
// - INSERT/UPDATE öncesi ItemVariant Guard + Duplicate Guard (payload içi)
|
||
// - UpdateOrder: DELETE öncesi child tablolar (trOrderLineCurrency) temizlenir
|
||
// =======================================================
|
||
|
||
package queries
|
||
|
||
import (
|
||
"bssapp-backend/db"
|
||
"bssapp-backend/models"
|
||
"database/sql"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
func nf0(v models.NullFloat64) float64 {
|
||
if !v.Valid {
|
||
return 0
|
||
}
|
||
return v.Float64
|
||
}
|
||
|
||
// =======================================================
|
||
// ✅ trOrderLineCurrency UPSERT (Doc Price / Amount yaz)
|
||
// =======================================================
|
||
func upsertLineCurrency(
|
||
tx *sql.Tx,
|
||
lineID string,
|
||
|
||
docCurrency string,
|
||
exRate float64,
|
||
|
||
docPrice float64,
|
||
qty float64,
|
||
|
||
lDis float64, // Line Discount %
|
||
tDis float64, // Total Discount %
|
||
|
||
vatRate float64,
|
||
ln models.OrderDetail,
|
||
|
||
user string,
|
||
) error {
|
||
|
||
// ================= NORMALIZE =================
|
||
|
||
if docCurrency == "" {
|
||
docCurrency = "TRY"
|
||
}
|
||
|
||
if exRate <= 0 {
|
||
exRate = 1
|
||
}
|
||
|
||
relationCurrency := safeNS(ln.RelationCurrencyCode)
|
||
if relationCurrency == "" {
|
||
relationCurrency = docCurrency
|
||
}
|
||
|
||
if ln.LineDiscount.Valid {
|
||
lDis = ln.LineDiscount.Float64
|
||
}
|
||
if ln.TotalDiscount.Valid {
|
||
tDis = ln.TotalDiscount.Float64
|
||
}
|
||
|
||
basePrice := docPrice // Price (KDV hariç)
|
||
if docCurrency == "TRY" && ln.LocalPrice.Valid && ln.LocalPrice.Float64 > 0 {
|
||
basePrice = ln.LocalPrice.Float64
|
||
}
|
||
|
||
amountBase := basePrice * qty // Amount (KDV hariç)
|
||
if docCurrency == "TRY" && ln.LocalAmount.Valid && ln.LocalAmount.Float64 > 0 {
|
||
amountBase = ln.LocalAmount.Float64
|
||
}
|
||
|
||
priceVI := basePrice // PriceVI (KDV dahil)
|
||
if ln.DocPrice.Valid && ln.DocPrice.Float64 > 0 {
|
||
priceVI = ln.DocPrice.Float64
|
||
}
|
||
|
||
amountVI := priceVI * qty // AmountVI (KDV dahil)
|
||
if ln.DocAmount.Valid && ln.DocAmount.Float64 > 0 {
|
||
amountVI = ln.DocAmount.Float64
|
||
if qty > 0 {
|
||
priceVI = amountVI / qty
|
||
}
|
||
}
|
||
|
||
// ================= DISCOUNT =================
|
||
|
||
afterLineDis := amountBase * (1 - lDis/100)
|
||
afterTotalDis := afterLineDis * (1 - tDis/100)
|
||
|
||
// ================= TAX =================
|
||
|
||
taxBase := afterTotalDis
|
||
if ln.TaxBase.Valid {
|
||
taxBase = ln.TaxBase.Float64
|
||
}
|
||
|
||
vat := taxBase * vatRate / 100
|
||
if ln.VatAmount.Valid {
|
||
vat = ln.VatAmount.Float64
|
||
}
|
||
|
||
vatDeducation := 0.0
|
||
if ln.VatDeducation.Valid {
|
||
vatDeducation = ln.VatDeducation.Float64
|
||
}
|
||
|
||
net := taxBase + vat - vatDeducation
|
||
if ln.NetAmount.Valid {
|
||
net = ln.NetAmount.Float64
|
||
}
|
||
|
||
pct := 0.0
|
||
if ln.Pct.Valid {
|
||
pct = ln.Pct.Float64
|
||
}
|
||
|
||
// payload yoksa doc tutarı net ile hizala
|
||
if !ln.DocAmount.Valid {
|
||
amountVI = net
|
||
if qty > 0 {
|
||
priceVI = amountVI / qty
|
||
}
|
||
}
|
||
|
||
// ================= LOCAL =================
|
||
|
||
localPrice := basePrice * exRate
|
||
localAmount := amountBase * exRate
|
||
|
||
localPriceVI := priceVI * exRate
|
||
localAmountVI := amountVI * exRate
|
||
|
||
localTaxBase := taxBase * exRate
|
||
localVat := vat * exRate
|
||
localVatDeducation := vatDeducation * exRate
|
||
localNet := net * exRate
|
||
|
||
// ================= DOC =================
|
||
|
||
_, err := tx.Exec(`
|
||
MERGE BAGGI_V3.dbo.trOrderLineCurrency AS T
|
||
USING (SELECT @p1 AS OrderLineID, @p2 AS CurrencyCode) AS S
|
||
ON T.OrderLineID=S.OrderLineID AND T.CurrencyCode=S.CurrencyCode
|
||
|
||
WHEN MATCHED THEN UPDATE SET
|
||
RelationCurrencyCode=@p3,
|
||
|
||
ExchangeRate=@p4,
|
||
|
||
PriceVI=@p5,
|
||
AmountVI=@p6,
|
||
|
||
Price=@p7,
|
||
Amount=@p8,
|
||
|
||
LDiscount1=@p9,
|
||
TDiscount1=@p10,
|
||
|
||
TaxBase=@p11,
|
||
Pct=@p12,
|
||
Vat=@p13,
|
||
VatDeducation=@p14,
|
||
NetAmount=@p15,
|
||
|
||
LastUpdatedUserName=@p16,
|
||
LastUpdatedDate=GETDATE()
|
||
|
||
WHEN NOT MATCHED THEN INSERT (
|
||
OrderLineID,
|
||
CurrencyCode,
|
||
RelationCurrencyCode,
|
||
|
||
ExchangeRate,
|
||
|
||
PriceVI,
|
||
AmountVI,
|
||
|
||
Price,
|
||
Amount,
|
||
|
||
LDiscount1,
|
||
TDiscount1,
|
||
|
||
TaxBase,
|
||
Pct,
|
||
Vat,
|
||
VatDeducation,
|
||
NetAmount,
|
||
|
||
CreatedUserName,
|
||
CreatedDate
|
||
)
|
||
VALUES (
|
||
@p1,@p2,@p3,
|
||
@p4,
|
||
@p5,@p6,
|
||
@p7,@p8,
|
||
@p9,@p10,
|
||
@p11,@p12,@p13,@p14,@p15,
|
||
@p16,GETDATE()
|
||
);`,
|
||
lineID,
|
||
docCurrency,
|
||
relationCurrency,
|
||
|
||
exRate,
|
||
|
||
priceVI,
|
||
amountVI,
|
||
|
||
basePrice,
|
||
amountBase,
|
||
|
||
lDis,
|
||
tDis,
|
||
|
||
taxBase,
|
||
pct,
|
||
vat,
|
||
vatDeducation,
|
||
net,
|
||
|
||
user,
|
||
)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if docCurrency != "TRY" {
|
||
_, err = tx.Exec(`
|
||
MERGE BAGGI_V3.dbo.trOrderLineCurrency AS T
|
||
USING (SELECT @p1 AS OrderLineID,'TRY' AS CurrencyCode) AS S
|
||
ON T.OrderLineID=S.OrderLineID AND T.CurrencyCode=S.CurrencyCode
|
||
|
||
WHEN MATCHED THEN UPDATE SET
|
||
RelationCurrencyCode=@p2,
|
||
|
||
ExchangeRate=@p3,
|
||
|
||
PriceVI=@p4,
|
||
AmountVI=@p5,
|
||
|
||
Price=@p6,
|
||
Amount=@p7,
|
||
|
||
LDiscount1=@p8,
|
||
TDiscount1=@p9,
|
||
|
||
TaxBase=@p10,
|
||
Pct=@p11,
|
||
Vat=@p12,
|
||
VatDeducation=@p13,
|
||
NetAmount=@p14,
|
||
|
||
LastUpdatedUserName=@p15,
|
||
LastUpdatedDate=GETDATE()
|
||
|
||
WHEN NOT MATCHED THEN INSERT (
|
||
OrderLineID,
|
||
CurrencyCode,
|
||
RelationCurrencyCode,
|
||
|
||
ExchangeRate,
|
||
|
||
PriceVI,
|
||
AmountVI,
|
||
|
||
Price,
|
||
Amount,
|
||
|
||
LDiscount1,
|
||
TDiscount1,
|
||
|
||
TaxBase,
|
||
Pct,
|
||
Vat,
|
||
VatDeducation,
|
||
NetAmount,
|
||
|
||
CreatedUserName,
|
||
CreatedDate
|
||
)
|
||
VALUES (
|
||
@p1,'TRY',@p2,
|
||
@p3,
|
||
@p4,@p5,
|
||
@p6,@p7,
|
||
@p8,@p9,
|
||
@p10,@p11,@p12,@p13,@p14,
|
||
@p15,GETDATE()
|
||
);`,
|
||
lineID,
|
||
docCurrency,
|
||
exRate,
|
||
|
||
localPriceVI,
|
||
localAmountVI,
|
||
|
||
localPrice,
|
||
localAmount,
|
||
|
||
lDis,
|
||
tDis,
|
||
|
||
localTaxBase,
|
||
pct,
|
||
localVat,
|
||
localVatDeducation,
|
||
localNet,
|
||
|
||
user,
|
||
)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// =======================================================
|
||
// COMBO KEY & STRING HELPERS
|
||
// =======================================================
|
||
func normalizeComboKey(s string) string {
|
||
return strings.ToUpper(strings.TrimSpace(s))
|
||
}
|
||
|
||
// makeComboKey: frontend tarafında NullString olmayan struct’lar için
|
||
func makeComboKey(ln models.OrderDetail) string {
|
||
model := safeNS(ln.ItemCode)
|
||
renk := safeNS(ln.ColorCode)
|
||
renk2 := safeNS(ln.ItemDim2Code)
|
||
beden := safeNS(ln.ItemDim1Code)
|
||
|
||
return normalizeComboKey(
|
||
fmt.Sprintf("%s||%s||%s||%s", model, renk, beden, renk2),
|
||
)
|
||
}
|
||
|
||
// qtyValue → NullFloat64 güvenli float64
|
||
func qtyValue(q models.NullFloat64) float64 {
|
||
if !q.Valid {
|
||
return 0
|
||
}
|
||
return q.Float64
|
||
}
|
||
|
||
func buildV3AuditUser(user *models.User) string {
|
||
if user == nil {
|
||
return "V3-0"
|
||
}
|
||
|
||
v3Name := strings.ToUpper(strings.TrimSpace(user.V3Username))
|
||
v3Group := user.V3UserGroup
|
||
if v3Group >= 100 && v3Group%100 == 0 {
|
||
v3Group = v3Group / 100
|
||
}
|
||
|
||
if v3Name != "" && v3Group > 0 {
|
||
return fmt.Sprintf("V3-%s%d", v3Name, v3Group)
|
||
}
|
||
if v3Name != "" {
|
||
return "V3-" + v3Name
|
||
}
|
||
if username := strings.TrimSpace(user.Username); username != "" {
|
||
return username
|
||
}
|
||
if v3Group > 0 {
|
||
return fmt.Sprintf("V3-%d", v3Group)
|
||
}
|
||
return "V3-0"
|
||
}
|
||
|
||
// VatCode: NullString → string
|
||
// - NULL → ""
|
||
// - "0" → "" (FK patlamasın, sadece anlamlı kodlar gönderiyoruz)
|
||
// - "%0", "%10" vb → trimlenmiş hali
|
||
func normalizeVatCode(ns models.NullString) string {
|
||
if !ns.Valid {
|
||
return ""
|
||
}
|
||
s := strings.TrimSpace(ns.String)
|
||
if s == "0" {
|
||
return ""
|
||
}
|
||
return s
|
||
}
|
||
|
||
// PCTCode: NullString → string
|
||
// - Artık her durumda "%0" döndürür (tek PCT tipi kullanımı)
|
||
// NOT: SQL tarafında da "%0" cdPCT içinde tanımlı olmalı
|
||
func normalizePCTCode(ns models.NullString) string {
|
||
return "%0"
|
||
}
|
||
|
||
// models.NullString → trimlenmiş string (NULL ise "")
|
||
func safeNS(ns models.NullString) string {
|
||
if !ns.Valid {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(ns.String)
|
||
}
|
||
|
||
// =======================================================
|
||
// COMBO KEY HELPERS (DB alanlarından)
|
||
// =======================================================
|
||
|
||
// makeComboKeyParts: düz string alanlardan comboKey üret
|
||
// comboKey = model || renk || beden || renk2
|
||
func makeComboKeyParts(item, color, dim1, dim2 string) string {
|
||
item = strings.TrimSpace(item)
|
||
color = strings.TrimSpace(color)
|
||
dim1 = strings.TrimSpace(dim1)
|
||
dim2 = strings.TrimSpace(dim2)
|
||
|
||
if item == "" && color == "" && dim1 == "" && dim2 == "" {
|
||
return ""
|
||
}
|
||
|
||
return normalizeComboKey(item + "||" + color + "||" + dim1 + "||" + dim2)
|
||
}
|
||
|
||
// comboFromNulls: NullString alanlardan comboKey üret
|
||
func comboFromNulls(item, color, dim1, dim2 models.NullString) string {
|
||
return makeComboKeyParts(
|
||
safeNS(item),
|
||
safeNS(color),
|
||
safeNS(dim1),
|
||
safeNS(dim2),
|
||
)
|
||
}
|
||
|
||
// =======================================================
|
||
// ✅ ItemVariant Guard — INSERT / UPDATE öncesi
|
||
// =======================================================
|
||
|
||
// normalizeKeyPart: NullString → trim + UPPER
|
||
func normalizeKeyPart(ns models.NullString) string {
|
||
s := strings.TrimSpace(safeNS(ns))
|
||
return strings.ToUpper(s)
|
||
}
|
||
|
||
// =======================================================
|
||
// AKSBIR DETECTION
|
||
// =======================================================
|
||
|
||
// =======================================================
|
||
// COMBO KEY BUILDER (AKSBIR AWARE)
|
||
// =======================================================
|
||
|
||
// Variant check: ItemCode + ColorCode + Dim1 + Dim2
|
||
func ValidateItemVariant(tx *sql.Tx, ln models.OrderDetail) error {
|
||
fmt.Printf(
|
||
"🧪 VARIANT GUARD INPUT | ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
|
||
safeNS(ln.ClientKey),
|
||
safeNS(ln.ItemCode),
|
||
safeNS(ln.ColorCode),
|
||
safeNS(ln.ItemDim1Code),
|
||
safeNS(ln.ItemDim2Code),
|
||
safeNS(ln.ItemDim3Code),
|
||
nf0(ln.Qty1),
|
||
)
|
||
|
||
item := normalizeKeyPart(ln.ItemCode)
|
||
color := normalizeKeyPart(ln.ColorCode)
|
||
dim1 := normalizeKeyPart(ln.ItemDim1Code)
|
||
dim2 := normalizeKeyPart(ln.ItemDim2Code)
|
||
|
||
// ✅ Placeholder/boş standardizasyon (SENDE "_" geliyor)
|
||
normalizeEmpty := func(s string) string {
|
||
s = strings.TrimSpace(strings.ToUpper(s))
|
||
if s == "_" || s == "-" {
|
||
return ""
|
||
}
|
||
return s
|
||
}
|
||
|
||
item = normalizeEmpty(item)
|
||
color = normalizeEmpty(color)
|
||
dim1 = normalizeEmpty(dim1)
|
||
dim2 = normalizeEmpty(dim2)
|
||
|
||
if item == "" {
|
||
return fmt.Errorf(
|
||
"ItemCode boş olamaz (ClientKey=%s)",
|
||
safeNS(ln.ClientKey),
|
||
)
|
||
fmt.Printf(
|
||
"🧪 VARIANT NORMALIZED | Item=%q Color=%q Dim1=%q Dim2=%q\n",
|
||
item, color, dim1, dim2,
|
||
)
|
||
|
||
}
|
||
|
||
var exists int
|
||
err := tx.QueryRow(`
|
||
SELECT CASE WHEN EXISTS (
|
||
SELECT 1
|
||
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK)
|
||
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
|
||
AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
|
||
AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3
|
||
AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4
|
||
) THEN 1 ELSE 0 END
|
||
`, item, color, dim1, dim2).Scan(&exists)
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("ItemVariant kontrol query hatası: %w", err)
|
||
}
|
||
|
||
if exists != 1 {
|
||
return &models.ValidationError{
|
||
Code: "INVALID_ITEM_VARIANT",
|
||
Message: "Tanımsız ürün kombinasyonu",
|
||
ClientKey: safeNS(ln.ClientKey),
|
||
ItemCode: item,
|
||
ColorCode: color,
|
||
Dim1: dim1,
|
||
Dim2: dim2,
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ValidateOrderVariants: save/update öncesi payload satırlarını prItemVariant'a göre doğrular.
|
||
// invalid döner; error sadece DB/prepare/query hatalarında döner.
|
||
func ValidateOrderVariants(db *sql.DB, lines []models.OrderDetail) ([]models.InvalidVariant, error) {
|
||
|
||
normalizeEmpty := func(s string) string {
|
||
s = strings.TrimSpace(strings.ToUpper(s))
|
||
if s == "_" || s == "-" {
|
||
return ""
|
||
}
|
||
return s
|
||
}
|
||
|
||
stmt, err := db.Prepare(`
|
||
SELECT CASE WHEN EXISTS (
|
||
SELECT 1
|
||
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK)
|
||
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
|
||
AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
|
||
AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3
|
||
AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4
|
||
) THEN 1 ELSE 0 END
|
||
`)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("validate prepare hatası: %w", err)
|
||
}
|
||
defer stmt.Close()
|
||
|
||
invalid := make([]models.InvalidVariant, 0)
|
||
|
||
for i, ln := range lines {
|
||
qty := qtyValue(ln.Qty1)
|
||
if qty <= 0 {
|
||
continue
|
||
}
|
||
|
||
item := normalizeEmpty(normalizeKeyPart(ln.ItemCode))
|
||
color := normalizeEmpty(normalizeKeyPart(ln.ColorCode))
|
||
dim1 := normalizeEmpty(normalizeKeyPart(ln.ItemDim1Code))
|
||
dim2 := normalizeEmpty(normalizeKeyPart(ln.ItemDim2Code))
|
||
|
||
// ItemCode boş ise invalid
|
||
if strings.TrimSpace(item) == "" {
|
||
invalid = append(invalid, models.InvalidVariant{
|
||
Index: i,
|
||
ClientKey: safeNS(ln.ClientKey),
|
||
ItemCode: item,
|
||
ColorCode: color,
|
||
Dim1: dim1,
|
||
Dim2: dim2,
|
||
Qty1: qty,
|
||
ComboKey: safeNS(ln.ComboKey),
|
||
Reason: "ItemCode boş",
|
||
})
|
||
continue
|
||
}
|
||
|
||
var exists int
|
||
if err := stmt.QueryRow(item, color, dim1, dim2).Scan(&exists); err != nil {
|
||
return nil, fmt.Errorf("validate query hatası (i=%d): %w", i, err)
|
||
}
|
||
|
||
if exists != 1 {
|
||
invalid = append(invalid, models.InvalidVariant{
|
||
Index: i,
|
||
ClientKey: safeNS(ln.ClientKey),
|
||
ItemCode: item,
|
||
ColorCode: color,
|
||
Dim1: dim1,
|
||
Dim2: dim2,
|
||
Qty1: qty,
|
||
ComboKey: safeNS(ln.ComboKey),
|
||
Reason: "prItemVariant’ta yok",
|
||
})
|
||
}
|
||
}
|
||
|
||
return invalid, nil
|
||
}
|
||
|
||
// =======================================================
|
||
// LineResult → frontend senkronu için
|
||
// =======================================================
|
||
|
||
type OrderLineResult struct {
|
||
ClientKey string `json:"clientKey"`
|
||
OrderLineID string `json:"orderLineID"`
|
||
}
|
||
|
||
// =======================================================
|
||
// PART 1 — InsertOrder (header + lines insert) — FINAL v5.1
|
||
// =======================================================
|
||
|
||
func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) (string, []OrderLineResult, error) {
|
||
conn := db.GetDB()
|
||
|
||
fmt.Println("🟦 InsertOrder() BAŞLADI -----------------------------")
|
||
|
||
tx, err := conn.Begin()
|
||
if err != nil {
|
||
return "", nil, fmt.Errorf("tx baslatilamadi: %w", err)
|
||
}
|
||
defer tx.Rollback()
|
||
|
||
now := time.Now()
|
||
v3User := buildV3AuditUser(user)
|
||
|
||
// =======================================================
|
||
// 1) BACKEND — OrderHeaderID üretimi (HER ZAMAN)
|
||
// =======================================================
|
||
realHeaderID := uuid.New().String()
|
||
|
||
fmt.Println("🟩 Backend yeni OrderHeaderID üretti:", realHeaderID)
|
||
|
||
header.OrderHeaderID = realHeaderID
|
||
|
||
// =======================================================
|
||
// 2) OrderNumber üretimi (LOCAL-* ise gerçek WS numarası)
|
||
// =======================================================
|
||
if !header.OrderNumber.Valid ||
|
||
strings.HasPrefix(header.OrderNumber.String, "LOCAL-") ||
|
||
len(strings.TrimSpace(header.OrderNumber.String)) == 0 {
|
||
|
||
fmt.Println("🟨 LOCAL numara geldi → gerçek WS numarası üretilecek...")
|
||
|
||
var realNumber string
|
||
err := tx.QueryRow(`
|
||
SELECT CONCAT(
|
||
'1-WS-3-',
|
||
RIGHT('00000' + CAST(NEXT VALUE FOR BAGGI_V3.dbo.Seq_OrderNumber_WS AS VARCHAR(10)), 5)
|
||
)
|
||
`).Scan(&realNumber)
|
||
|
||
if err != nil {
|
||
return "", nil, fmt.Errorf("Gerçek sipariş numarası üretilemedi: %w", err)
|
||
}
|
||
|
||
fmt.Println("🟩 Üretilen gerçek WS numarası:", realNumber)
|
||
|
||
header.OrderNumber.String = realNumber
|
||
header.OrderNumber.Valid = true
|
||
}
|
||
|
||
newID := realHeaderID // artık DB’ye bu yazılacak
|
||
|
||
// =======================================================
|
||
// 3) Döviz kuru çözümü
|
||
// =======================================================
|
||
exRate := 1.0
|
||
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
|
||
if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil {
|
||
if c.Rate > 0 {
|
||
exRate = c.Rate
|
||
}
|
||
}
|
||
}
|
||
// =======================================================
|
||
// 4) HEADER INSERT
|
||
// =======================================================
|
||
|
||
queryHeader := `
|
||
INSERT INTO BAGGI_V3.dbo.trOrderHeader (
|
||
OrderHeaderID, OrderTypeCode, ProcessCode, OrderNumber, IsCancelOrder,
|
||
OrderDate, OrderTime, DocumentNumber, PaymentTerm,
|
||
AverageDueDate, Description, InternalDescription,
|
||
CurrAccTypeCode, CurrAccCode, SubCurrAccID, ContactID,
|
||
ShipmentMethodCode, ShippingPostalAddressID, BillingPostalAddressID,
|
||
GuarantorContactID, GuarantorContactID2, RoundsmanCode,
|
||
DeliveryCompanyCode, TaxTypeCode, WithHoldingTaxTypeCode, DOVCode,
|
||
TaxExemptionCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode,
|
||
POSTerminalID, WarehouseCode, ToWarehouseCode,
|
||
OrdererCompanyCode, OrdererOfficeCode, OrdererStoreCode,
|
||
GLTypeCode, DocCurrencyCode, LocalCurrencyCode, ExchangeRate,
|
||
TDisRate1, TDisRate2, TDisRate3, TDisRate4, TDisRate5,
|
||
DiscountReasonCode, SurplusOrderQtyToleranceRate,
|
||
ImportFileNumber, ExportFileNumber,
|
||
IncotermCode1, IncotermCode2, LettersOfCreditNumber,
|
||
PaymentMethodCode, IsInclutedVat, IsCreditSale, IsCreditableConfirmed,
|
||
CreditableConfirmedUser, CreditableConfirmedDate,
|
||
IsSalesViaInternet, IsSuspended, IsCompleted, IsPrinted,
|
||
IsLocked, UserLocked, IsClosed,
|
||
ApplicationCode, ApplicationID,
|
||
CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate,
|
||
IsProposalBased
|
||
)
|
||
VALUES (
|
||
@p1,@p2,@p3,@p4,
|
||
@p5,@p6,@p7,@p8,
|
||
@p9,@p10,@p11,
|
||
@p12,@p13,@p14,@p15,
|
||
@p16,@p17,@p18,
|
||
@p19,@p20,@p21,
|
||
@p22,@p23,@p24,@p25,
|
||
@p26,@p27,@p28,@p29,@p30,
|
||
@p31,@p32,@p33,
|
||
@p34,@p35,@p36,
|
||
@p37,@p38,@p39,@p40,
|
||
@p41,@p42,@p43,@p44,@p45,
|
||
@p46,@p47,
|
||
@p48,@p49,
|
||
@p50,@p51,@p52,
|
||
@p53,@p54,@p55,@p56,@p57,
|
||
@p58,@p59,@p60,@p61,@p62,
|
||
@p63,@p64,@p65,
|
||
@p66,@p67,@p68,@p69,@p70,@p71,@p72,@p73
|
||
);
|
||
`
|
||
|
||
fmt.Println("🟪 HEADER INSERT ÇALIŞIYOR...")
|
||
|
||
// ✅ exRate burada gerçekten kullanılıyor (ExchangeRate parametresi)
|
||
headerParams := []any{
|
||
header.OrderHeaderID,
|
||
nullableInt16(header.OrderTypeCode, 1),
|
||
nullableString(header.ProcessCode, "WS"),
|
||
nullableString(header.OrderNumber, ""),
|
||
nullableBool(header.IsCancelOrder, false),
|
||
|
||
nullableDateString(header.OrderDate),
|
||
nullableTimeString(header.OrderTime),
|
||
nullableString(header.DocumentNumber, ""),
|
||
nullableInt16(header.PaymentTerm, 0),
|
||
|
||
nullableDateString(header.AverageDueDate),
|
||
nullableString(header.Description, ""),
|
||
nullableString(header.InternalDescription, ""),
|
||
|
||
nullableInt16(header.CurrAccTypeCode, 0),
|
||
nullableString(header.CurrAccCode, ""),
|
||
nullableUUID(header.SubCurrAccID),
|
||
nullableUUID(header.ContactID),
|
||
|
||
nullableString(header.ShipmentMethodCode, ""),
|
||
nullableUUID(header.ShippingPostalAddressID),
|
||
nullableUUID(header.BillingPostalAddressID),
|
||
|
||
nullableUUID(header.GuarantorContactID),
|
||
nullableUUID(header.GuarantorContactID2),
|
||
nullableString(header.RoundsmanCode, ""),
|
||
|
||
nullableString(header.DeliveryCompanyCode, ""),
|
||
nullableInt16(header.TaxTypeCode, 0),
|
||
nullableString(header.WithHoldingTaxTypeCode, ""),
|
||
nullableString(header.DOVCode, ""),
|
||
|
||
nullableInt16(header.TaxExemptionCode, 0),
|
||
nullableInt32ToInt16(header.CompanyCode, 1),
|
||
nullableString(header.OfficeCode, "101"),
|
||
nullableInt16(header.StoreTypeCode, 5),
|
||
nullableString(header.StoreCode, ""),
|
||
|
||
nullableInt16(header.POSTerminalID, 0),
|
||
nullableString(header.WarehouseCode, "1-0-12"),
|
||
nullableString(header.ToWarehouseCode, ""),
|
||
|
||
nullableInt32ToInt16(header.OrdererCompanyCode, 1),
|
||
nullableString(header.OrdererOfficeCode, "101"),
|
||
nullableString(header.OrdererStoreCode, ""),
|
||
|
||
nullableString(header.GLTypeCode, ""),
|
||
nullableString(header.DocCurrencyCode, "TRY"),
|
||
nullableString(header.LocalCurrencyCode, "TRY"),
|
||
nullableFloat64(header.ExchangeRate, exRate), // ✅ exRate kullanıldı
|
||
|
||
nullableFloat64(header.TDisRate1, 0),
|
||
nullableFloat64(header.TDisRate2, 0),
|
||
nullableFloat64(header.TDisRate3, 0),
|
||
nullableFloat64(header.TDisRate4, 0),
|
||
nullableFloat64(header.TDisRate5, 0),
|
||
|
||
nullableInt16(header.DiscountReasonCode, 0),
|
||
nullableFloat64(header.SurplusOrderQtyToleranceRate, 0),
|
||
|
||
nullableString(header.ImportFileNumber, ""),
|
||
nullableString(header.ExportFileNumber, ""),
|
||
|
||
nullableString(header.IncotermCode1, ""),
|
||
nullableString(header.IncotermCode2, ""),
|
||
nullableString(header.LettersOfCreditNumber, ""),
|
||
|
||
nullableString(header.PaymentMethodCode, ""),
|
||
nullableBool(header.IsInclutedVat, false),
|
||
nullableBool(header.IsCreditSale, true),
|
||
nullableBool(header.IsCreditableConfirmed, true),
|
||
|
||
nullableString(header.CreditableConfirmedUser, v3User),
|
||
nullableDateTime(header.CreditableConfirmedDate, now),
|
||
|
||
nullableBool(header.IsSalesViaInternet, false),
|
||
nullableBool(header.IsSuspended, false),
|
||
nullableBool(header.IsCompleted, false),
|
||
nullableBool(header.IsPrinted, false),
|
||
|
||
nullableBool(header.IsLocked, false),
|
||
nullableBool(header.UserLocked, false),
|
||
nullableBool(header.IsClosed, false),
|
||
|
||
nullableString(header.ApplicationCode, "Order"),
|
||
nullableUUID(header.ApplicationID),
|
||
|
||
nullableString(header.CreatedUserName, v3User),
|
||
nullableDateTimeString(header.CreatedDate, now),
|
||
nullableString(header.LastUpdatedUserName, v3User),
|
||
nullableDateTimeString(header.LastUpdatedDate, now),
|
||
|
||
nullableBool(header.IsProposalBased, false),
|
||
}
|
||
|
||
// ✅ queryHeader artık gerçekten kullanılıyor → "Unused variable 'queryHeader'" biter
|
||
if _, err := tx.Exec(queryHeader, headerParams...); err != nil {
|
||
fmt.Println("❌ HEADER INSERT ERROR:", err)
|
||
return "", nil, fmt.Errorf("header insert hatasi: %w", err)
|
||
}
|
||
|
||
fmt.Println("🟩 HEADER INSERT OK — ID:", newID)
|
||
|
||
// headerParams ... (senin mevcut hali aynen)
|
||
// =======================================================
|
||
// 5) LINE INSERT
|
||
// =======================================================
|
||
|
||
insStmt, err := tx.Prepare(`
|
||
INSERT INTO BAGGI_V3.dbo.trOrderLine (
|
||
OrderLineID,
|
||
SortOrder, ItemTypeCode, ItemCode, ColorCode,
|
||
ItemDim1Code, ItemDim2Code, ItemDim3Code,
|
||
Qty1, Qty2,
|
||
CancelQty1, CancelQty2,
|
||
OrderCancelReasonCode,
|
||
DeliveryDate, PlannedDateOfLading,
|
||
LineDescription,
|
||
UsedBarcode, CostCenterCode,
|
||
VatCode, VatRate, PCTCode, PCTRate,
|
||
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
|
||
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
|
||
Price,
|
||
BaseProcessCode, BaseOrderNumber,
|
||
BaseCustomerTypeCode, BaseCustomerCode,
|
||
BaseSubCurrAccID, BaseStoreCode,
|
||
OrderHeaderID,
|
||
CreatedUserName, CreatedDate,
|
||
LastUpdatedUserName, LastUpdatedDate,
|
||
SurplusOrderQtyToleranceRate,
|
||
WithHoldingTaxTypeCode,
|
||
DOVCode
|
||
)
|
||
VALUES (
|
||
@p1,@p2,@p3,@p4,
|
||
@p5,@p6,@p7,@p8,
|
||
@p9,@p10,
|
||
@p11,@p12,
|
||
@p13,
|
||
@p14,@p15,
|
||
@p16,
|
||
@p17,@p18,
|
||
@p19,@p20,@p21,@p22,
|
||
@p23,@p24,@p25,@p26,@p27,
|
||
@p28,@p29,@p30,
|
||
@p31,
|
||
@p32,@p33,
|
||
@p34,@p35,
|
||
@p36,@p37,
|
||
@p38,
|
||
@p39,@p40,
|
||
@p41,@p42,
|
||
@p43,
|
||
@p44,
|
||
@p45
|
||
)`)
|
||
if err != nil {
|
||
return "", nil, fmt.Errorf("line insert stmt hazirlanamadi: %w", err)
|
||
}
|
||
defer insStmt.Close()
|
||
|
||
lineResults := make([]OrderLineResult, 0, len(lines))
|
||
|
||
// ✅ Duplicate Guard (payload içi)
|
||
seenCombo := make(map[string]bool)
|
||
|
||
for i, ln := range lines {
|
||
fmt.Println("────────────────────────────────────")
|
||
fmt.Printf("🟨 [INSERT] LINE %d — gelen OrderLineID=%s\n", i+1, ln.OrderLineID)
|
||
|
||
// Her satır için yeni GUID
|
||
if ln.OrderLineID == "" {
|
||
newLineID := uuid.New().String()
|
||
fmt.Println("🆕 Yeni LineID üretildi:", newLineID)
|
||
ln.OrderLineID = newLineID
|
||
}
|
||
|
||
// ✅ Duplicate Guard (comboKey)
|
||
comboKey := normalizeComboKey(safeNS(ln.ComboKey))
|
||
if comboKey == "" {
|
||
comboKey = makeComboKey(ln)
|
||
}
|
||
|
||
if qtyValue(ln.Qty1) > 0 && comboKey != "" {
|
||
if seenCombo[comboKey] {
|
||
return "", nil, fmt.Errorf(
|
||
"Duplicate satır (comboKey=%s, ClientKey=%s)",
|
||
comboKey,
|
||
safeNS(ln.ClientKey),
|
||
)
|
||
}
|
||
seenCombo[comboKey] = true
|
||
}
|
||
|
||
// V2 Logic → %0 PCT
|
||
vatCode := normalizeVatCode(ln.VatCode)
|
||
pctCode := normalizePCTCode(ln.PCTCode)
|
||
|
||
var pctParam any
|
||
if pctCode == "" {
|
||
pctParam = nil
|
||
} else {
|
||
pctParam = pctCode
|
||
}
|
||
|
||
planned := nullableDateString(ln.PlannedDateOfLading)
|
||
|
||
// ✅ INSERT ÖNCESİ ItemVariant GUARD
|
||
if qtyValue(ln.Qty1) > 0 {
|
||
if err := ValidateItemVariant(tx, ln); err != nil {
|
||
fmt.Println("❌ VARIANT GUARD (INSERT):", err)
|
||
return "", nil, err
|
||
}
|
||
}
|
||
|
||
fmt.Printf(
|
||
"🚨 INSERT LINE[%d] | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
|
||
i+1,
|
||
ln.OrderLineID,
|
||
safeNS(ln.ClientKey),
|
||
safeNS(ln.ItemCode),
|
||
safeNS(ln.ColorCode),
|
||
safeNS(ln.ItemDim1Code),
|
||
safeNS(ln.ItemDim2Code),
|
||
safeNS(ln.ItemDim3Code),
|
||
nf0(ln.Qty1),
|
||
)
|
||
|
||
_, err := insStmt.Exec(
|
||
ln.OrderLineID,
|
||
ln.SortOrder,
|
||
ln.ItemTypeCode,
|
||
nullableString(ln.ItemCode, ""),
|
||
safeNS(ln.ColorCode),
|
||
safeNS(ln.ItemDim1Code),
|
||
safeNS(ln.ItemDim2Code),
|
||
nullableString(ln.ItemDim3Code, ""),
|
||
ln.Qty1, ln.Qty2,
|
||
ln.CancelQty1, ln.CancelQty2,
|
||
nullableString(ln.OrderCancelReasonCode, ""),
|
||
nullableTime(ln.DeliveryDate, now),
|
||
planned,
|
||
nullableString(ln.LineDescription, ""),
|
||
nullableString(ln.UsedBarcode, ""),
|
||
nullableString(ln.CostCenterCode, ""),
|
||
vatCode, nf0(ln.VatRate),
|
||
pctParam, nf0(ln.PCTRate),
|
||
nf0(ln.LDisRate1),
|
||
nf0(ln.LDisRate2),
|
||
nf0(ln.LDisRate3),
|
||
nf0(ln.LDisRate4),
|
||
nf0(ln.LDisRate5),
|
||
nullableString(ln.DocCurrencyCode, "TRY"),
|
||
nullableString(ln.PriceCurrencyCode, "TRY"),
|
||
nf0(ln.PriceExchangeRate),
|
||
nf0(ln.Price),
|
||
nullableString(ln.BaseProcessCode, ""),
|
||
nullableString(ln.BaseOrderNumber, ""),
|
||
ln.BaseCustomerTypeCode,
|
||
nullableString(ln.BaseCustomerCode, ""),
|
||
nullableUUID(ln.BaseSubCurrAccID),
|
||
nullableString(ln.BaseStoreCode, ""),
|
||
header.OrderHeaderID,
|
||
v3User, now,
|
||
v3User, now,
|
||
nf0(ln.SurplusOrderQtyToleranceRate),
|
||
nullableString(ln.WithHoldingTaxTypeCode, ""),
|
||
nullableString(ln.DOVCode, ""),
|
||
)
|
||
|
||
if err != nil {
|
||
fmt.Println("❌ INSERT LINE ERROR")
|
||
fmt.Printf(
|
||
"💥 FAILED LINE | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q\n",
|
||
ln.OrderLineID,
|
||
safeNS(ln.ClientKey),
|
||
safeNS(ln.ItemCode),
|
||
safeNS(ln.ColorCode),
|
||
safeNS(ln.ItemDim1Code),
|
||
safeNS(ln.ItemDim2Code),
|
||
safeNS(ln.ItemDim3Code),
|
||
)
|
||
fmt.Println("SQL ERROR:", err)
|
||
return "", nil, fmt.Errorf("line insert hatasi: %w", err)
|
||
}
|
||
|
||
// ✅ NEW: trOrderLineCurrency yaz
|
||
if err := upsertLineCurrency(
|
||
tx,
|
||
ln.OrderLineID,
|
||
|
||
safeNS(ln.DocCurrencyCode),
|
||
nf0(ln.PriceExchangeRate),
|
||
|
||
nf0(ln.Price),
|
||
nf0(ln.Qty1),
|
||
|
||
nf0(ln.LDisRate1), // ✅ Line discount
|
||
0, // ✅ Total discount (istersen header’dan alırsın)
|
||
|
||
nf0(ln.VatRate), // ✅ Vat rate
|
||
ln,
|
||
|
||
v3User,
|
||
); err != nil {
|
||
return "", nil, fmt.Errorf("currency insert hatası: %w", err)
|
||
}
|
||
|
||
if ln.ClientKey.Valid && ln.ClientKey.String != "" {
|
||
lineResults = append(lineResults, OrderLineResult{
|
||
ClientKey: ln.ClientKey.String,
|
||
OrderLineID: ln.OrderLineID,
|
||
})
|
||
}
|
||
}
|
||
|
||
// =======================================================
|
||
// 6) COMMIT
|
||
// =======================================================
|
||
fmt.Println("🟨 COMMIT EDİLİYOR...")
|
||
|
||
if err := tx.Commit(); err != nil {
|
||
fmt.Println("❌ COMMIT ERROR:", err)
|
||
return "", nil, err
|
||
}
|
||
|
||
fmt.Println("✅ COMMIT OK — INSERT ORDER BİTTİ")
|
||
fmt.Println("────────────────────────────────────")
|
||
|
||
return newID, lineResults, nil
|
||
}
|
||
|
||
// =======================================================
|
||
// PART 2 — UpdateOrder FULL DEBUG (v4.3)
|
||
// =======================================================
|
||
|
||
func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) ([]OrderLineResult, error) {
|
||
conn := db.GetDB()
|
||
|
||
fmt.Println("══════════════════════════════════════")
|
||
fmt.Println("🔍 [DEBUG] UpdateOrder çağrıldı")
|
||
fmt.Printf("🔍 HeaderID: %v\n", header.OrderHeaderID)
|
||
fmt.Printf("🔍 Line sayısı: %v\n", len(lines))
|
||
fmt.Printf("🔍 User: %v (V3: %s/%d)\n", user.Username, user.V3Username, user.V3UserGroup)
|
||
fmt.Println("══════════════════════════════════════")
|
||
|
||
tx, err := conn.Begin()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("tx baslatilamadi: %w", err)
|
||
}
|
||
defer tx.Rollback()
|
||
|
||
now := time.Now()
|
||
v3User := buildV3AuditUser(user)
|
||
|
||
// Hard delete bazen FK nedeniyle patlayabiliyor.
|
||
// Bu durumda satırı soft-close ederek akışı güvenli şekilde tamamlarız.
|
||
deleteOrSoftCloseLine := func(lineID string) error {
|
||
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, lineID); err != nil {
|
||
return fmt.Errorf("line currency delete failed line_id=%s: %w", lineID, err)
|
||
}
|
||
|
||
if _, err := tx.Exec(`
|
||
DELETE FROM BAGGI_V3.dbo.trOrderLine
|
||
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
|
||
`, header.OrderHeaderID, lineID); err != nil {
|
||
fmt.Printf("[ORDER_UPDATE] hard delete failed, trying soft-close line_id=%s err=%v\n", lineID, err)
|
||
|
||
if _, err2 := tx.Exec(`
|
||
UPDATE BAGGI_V3.dbo.trOrderLine
|
||
SET
|
||
Qty1 = 0,
|
||
Qty2 = 0,
|
||
CancelQty1 = 0,
|
||
CancelQty2 = 0,
|
||
IsClosed = 1,
|
||
LastUpdatedUserName = @p1,
|
||
LastUpdatedDate = @p2
|
||
WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
|
||
`, v3User, now, header.OrderHeaderID, lineID); err2 != nil {
|
||
return fmt.Errorf("line delete failed line_id=%s: %v; soft-close failed: %w", lineID, err, err2)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Döviz kuru (Header ExchangeRate fallback)
|
||
exRate := 1.0
|
||
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
|
||
if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil && c.Rate > 0 {
|
||
exRate = c.Rate
|
||
}
|
||
}
|
||
|
||
// =======================================================
|
||
// 0) Mevcut satırları oku (GUID STRING olarak!)
|
||
// =======================================================
|
||
existingOpen := make(map[string]bool)
|
||
existingClosed := make(map[string]bool)
|
||
existingOpenCombo := make(map[string]string)
|
||
existingClosedCombo := make(map[string]string)
|
||
existingOpenMeta := make(map[string]struct {
|
||
item string
|
||
color string
|
||
dim1 string
|
||
dim2 string
|
||
})
|
||
|
||
rows, err := tx.Query(`
|
||
SELECT
|
||
CONVERT(varchar(36), OrderLineID),
|
||
ISNULL(IsClosed, 0),
|
||
ISNULL(ItemCode,''),
|
||
ISNULL(ColorCode,''),
|
||
ISNULL(ItemDim1Code,''),
|
||
ISNULL(ItemDim2Code,'')
|
||
FROM BAGGI_V3.dbo.trOrderLine
|
||
WHERE OrderHeaderID=@p1
|
||
`, header.OrderHeaderID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("mevcut satirlar okunamadi: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
for rows.Next() {
|
||
var id, item, color, dim1, dim2 string
|
||
var closed bool
|
||
|
||
if err := rows.Scan(&id, &closed, &item, &color, &dim1, &dim2); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
combo := makeComboKeyParts(item, color, dim1, dim2)
|
||
|
||
if closed {
|
||
existingClosed[id] = true
|
||
if combo != "" {
|
||
existingClosedCombo[combo] = id
|
||
}
|
||
} else {
|
||
existingOpen[id] = true
|
||
existingOpenMeta[id] = struct {
|
||
item string
|
||
color string
|
||
dim1 string
|
||
dim2 string
|
||
}{
|
||
item: strings.TrimSpace(item),
|
||
color: strings.TrimSpace(color),
|
||
dim1: strings.TrimSpace(dim1),
|
||
dim2: strings.TrimSpace(dim2),
|
||
}
|
||
if combo != "" {
|
||
existingOpenCombo[combo] = id
|
||
}
|
||
}
|
||
}
|
||
|
||
isLineInvoiced := func(lineID string) (bool, error) {
|
||
var exists int
|
||
err := tx.QueryRow(`
|
||
SELECT CASE WHEN EXISTS (
|
||
SELECT 1
|
||
FROM BAGGI_V3.dbo.trInvoiceLine WITH (NOLOCK)
|
||
WHERE OrderLineID=@p1
|
||
) THEN 1 ELSE 0 END
|
||
`, lineID).Scan(&exists)
|
||
if err != nil {
|
||
return false, fmt.Errorf("invoice line kontrol query hatasi: %w", err)
|
||
}
|
||
return exists == 1, nil
|
||
}
|
||
|
||
// ======================================================
|
||
// HEADER UPDATE
|
||
// ======================================================
|
||
_, err = tx.Exec(`
|
||
UPDATE BAGGI_V3.dbo.trOrderHeader SET
|
||
OrderDate=@p1,
|
||
OrderTime=@p2,
|
||
AverageDueDate=@p3,
|
||
Description=@p4,
|
||
InternalDescription=@p5,
|
||
DocCurrencyCode=@p6,
|
||
LocalCurrencyCode=@p7,
|
||
ExchangeRate=@p8,
|
||
LastUpdatedUserName=@p9,
|
||
LastUpdatedDate=@p10
|
||
WHERE OrderHeaderID=@p11
|
||
`,
|
||
nullableDateString(header.OrderDate),
|
||
nullableTimeString(header.OrderTime),
|
||
nullableDateString(header.AverageDueDate),
|
||
nullableString(header.Description, ""),
|
||
nullableString(header.InternalDescription, ""),
|
||
nullableString(header.DocCurrencyCode, "TRY"),
|
||
nullableString(header.LocalCurrencyCode, "TRY"),
|
||
nullableFloat64(header.ExchangeRate, exRate),
|
||
v3User,
|
||
now,
|
||
header.OrderHeaderID,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// ======================================================
|
||
// PREPARE STATEMENTS
|
||
// ======================================================
|
||
insStmt, err := tx.Prepare(`
|
||
INSERT INTO BAGGI_V3.dbo.trOrderLine (
|
||
OrderLineID, SortOrder, ItemTypeCode, ItemCode, ColorCode,
|
||
ItemDim1Code, ItemDim2Code, ItemDim3Code,
|
||
Qty1, Qty2, CancelQty1, CancelQty2, OrderCancelReasonCode,
|
||
DeliveryDate, PlannedDateOfLading, LineDescription,
|
||
UsedBarcode, CostCenterCode,
|
||
VatCode, VatRate, PCTCode, PCTRate,
|
||
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
|
||
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
|
||
Price, BaseProcessCode, BaseOrderNumber,
|
||
BaseCustomerTypeCode, BaseCustomerCode,
|
||
BaseSubCurrAccID, BaseStoreCode,
|
||
OrderHeaderID, CreatedUserName, CreatedDate,
|
||
LastUpdatedUserName, LastUpdatedDate,
|
||
SurplusOrderQtyToleranceRate,
|
||
WithHoldingTaxTypeCode, DOVCode
|
||
)
|
||
VALUES (
|
||
@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,
|
||
@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,
|
||
@p19,@p20,@p21,@p22,@p23,@p24,@p25,@p26,@p27,
|
||
@p28,@p29,@p30,@p31,@p32,@p33,@p34,@p35,
|
||
@p36,@p37,@p38,@p39,@p40,@p41,@p42,@p43,
|
||
@p44,@p45
|
||
)`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer insStmt.Close()
|
||
|
||
updStmt, err := tx.Prepare(`
|
||
UPDATE BAGGI_V3.dbo.trOrderLine SET
|
||
SortOrder=@p1, ItemTypeCode=@p2, ItemCode=@p3, ColorCode=@p4,
|
||
ItemDim1Code=@p5, ItemDim2Code=@p6, ItemDim3Code=@p7,
|
||
Qty1=@p8, Qty2=@p9, CancelQty1=@p10, CancelQty2=@p11,
|
||
OrderCancelReasonCode=@p12,
|
||
DeliveryDate=@p13, PlannedDateOfLading=@p14,
|
||
LineDescription=@p15, UsedBarcode=@p16, CostCenterCode=@p17,
|
||
VatCode=@p18, VatRate=@p19, PCTCode=@p20, PCTRate=@p21,
|
||
LDisRate1=@p22, LDisRate2=@p23, LDisRate3=@p24,
|
||
LDisRate4=@p25, LDisRate5=@p26,
|
||
DocCurrencyCode=@p27, PriceCurrencyCode=@p28,
|
||
PriceExchangeRate=@p29, Price=@p30,
|
||
BaseProcessCode=@p31, BaseOrderNumber=@p32,
|
||
BaseCustomerTypeCode=@p33, BaseCustomerCode=@p34,
|
||
BaseSubCurrAccID=@p35, BaseStoreCode=@p36,
|
||
LastUpdatedUserName=@p37, LastUpdatedDate=@p38,
|
||
SurplusOrderQtyToleranceRate=@p39,
|
||
WithHoldingTaxTypeCode=@p40, DOVCode=@p41
|
||
WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer updStmt.Close()
|
||
|
||
// ======================================================
|
||
// LOOP
|
||
// ======================================================
|
||
lineResults := make([]OrderLineResult, 0)
|
||
seenCombo := make(map[string]bool)
|
||
|
||
for _, ln := range lines {
|
||
comboKey := normalizeComboKey(safeNS(ln.ComboKey))
|
||
if comboKey == "" {
|
||
comboKey = makeComboKey(ln)
|
||
}
|
||
|
||
// Duplicate guard (SADECE aktif)
|
||
if qtyValue(ln.Qty1) > 0 && comboKey != "" {
|
||
if seenCombo[comboKey] {
|
||
return nil, fmt.Errorf("Duplicate satır: %s", comboKey)
|
||
}
|
||
seenCombo[comboKey] = true
|
||
}
|
||
|
||
// Kapalı satır guard
|
||
if ln.OrderLineID != "" && existingClosed[ln.OrderLineID] {
|
||
continue
|
||
}
|
||
if comboKey != "" {
|
||
if _, ok := existingClosedCombo[comboKey]; ok {
|
||
continue
|
||
}
|
||
}
|
||
|
||
// DELETE SIGNAL
|
||
if ln.OrderLineID != "" && qtyValue(ln.Qty1) <= 0 {
|
||
invoiced, err := isLineInvoiced(ln.OrderLineID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if invoiced {
|
||
return nil, &models.ValidationError{
|
||
Code: "ORDER_LINE_INVOICED",
|
||
Message: fmt.Sprintf("Faturalanmis satir silinemez (OrderLineID=%s)", ln.OrderLineID),
|
||
ClientKey: safeNS(ln.ClientKey),
|
||
ItemCode: strings.TrimSpace(safeNS(ln.ItemCode)),
|
||
ColorCode: strings.TrimSpace(safeNS(ln.ColorCode)),
|
||
Dim1: strings.TrimSpace(safeNS(ln.ItemDim1Code)),
|
||
Dim2: strings.TrimSpace(safeNS(ln.ItemDim2Code)),
|
||
}
|
||
}
|
||
if err := deleteOrSoftCloseLine(ln.OrderLineID); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
delete(existingOpen, ln.OrderLineID)
|
||
delete(existingOpenCombo, comboKey)
|
||
continue
|
||
}
|
||
|
||
isNew := false
|
||
|
||
// ID resolve: boşsa combo'dan yakala, yoksa yeni üret
|
||
if ln.OrderLineID == "" {
|
||
if dbID, ok := existingOpenCombo[comboKey]; ok {
|
||
ln.OrderLineID = dbID
|
||
} else {
|
||
ln.OrderLineID = uuid.New().String()
|
||
isNew = true
|
||
}
|
||
}
|
||
|
||
// Edit akışında bazen stale OrderLineID gelebiliyor (silinmiş/eski satır ID'si).
|
||
// Böyle bir ID ile UPDATE denemek yerine yeni satır olarak ele al.
|
||
if !isNew && ln.OrderLineID != "" {
|
||
if _, stillOpen := existingOpen[ln.OrderLineID]; !stillOpen {
|
||
ln.OrderLineID = uuid.New().String()
|
||
isNew = true
|
||
}
|
||
}
|
||
|
||
// Variant guard
|
||
if qtyValue(ln.Qty1) > 0 {
|
||
if err := ValidateItemVariant(tx, ln); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if isNew {
|
||
_, err := insStmt.Exec(
|
||
ln.OrderLineID, ln.SortOrder, ln.ItemTypeCode,
|
||
nullableString(ln.ItemCode, ""), safeNS(ln.ColorCode),
|
||
safeNS(ln.ItemDim1Code), safeNS(ln.ItemDim2Code),
|
||
nullableString(ln.ItemDim3Code, ""),
|
||
ln.Qty1, ln.Qty2, ln.CancelQty1, ln.CancelQty2,
|
||
nullableString(ln.OrderCancelReasonCode, ""),
|
||
nullableTime(ln.DeliveryDate, now),
|
||
nullableDateString(ln.PlannedDateOfLading),
|
||
nullableString(ln.LineDescription, ""),
|
||
nullableString(ln.UsedBarcode, ""),
|
||
nullableString(ln.CostCenterCode, ""),
|
||
normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
|
||
normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
|
||
nf0(ln.LDisRate1), nf0(ln.LDisRate2),
|
||
nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
|
||
nullableString(ln.DocCurrencyCode, "TRY"),
|
||
nullableString(ln.PriceCurrencyCode, "TRY"),
|
||
nf0(ln.PriceExchangeRate), nf0(ln.Price),
|
||
nullableString(ln.BaseProcessCode, ""),
|
||
nullableString(ln.BaseOrderNumber, ""),
|
||
ln.BaseCustomerTypeCode,
|
||
nullableString(ln.BaseCustomerCode, ""),
|
||
nullableUUID(ln.BaseSubCurrAccID),
|
||
nullableString(ln.BaseStoreCode, ""),
|
||
header.OrderHeaderID,
|
||
v3User, now, v3User, now,
|
||
nf0(ln.SurplusOrderQtyToleranceRate),
|
||
nullableString(ln.WithHoldingTaxTypeCode, ""),
|
||
nullableString(ln.DOVCode, ""),
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
} else {
|
||
_, err := updStmt.Exec(
|
||
ln.SortOrder, ln.ItemTypeCode,
|
||
nullableString(ln.ItemCode, ""),
|
||
safeNS(ln.ColorCode),
|
||
safeNS(ln.ItemDim1Code),
|
||
safeNS(ln.ItemDim2Code),
|
||
nullableString(ln.ItemDim3Code, ""),
|
||
nf0(ln.Qty1), nf0(ln.Qty2),
|
||
nf0(ln.CancelQty1), nf0(ln.CancelQty2),
|
||
nullableString(ln.OrderCancelReasonCode, ""),
|
||
nullableTime(ln.DeliveryDate, now),
|
||
nullableDateString(ln.PlannedDateOfLading),
|
||
nullableString(ln.LineDescription, ""),
|
||
nullableString(ln.UsedBarcode, ""),
|
||
nullableString(ln.CostCenterCode, ""),
|
||
normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
|
||
normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
|
||
nf0(ln.LDisRate1), nf0(ln.LDisRate2),
|
||
nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
|
||
nullableString(ln.DocCurrencyCode, "TRY"),
|
||
nullableString(ln.PriceCurrencyCode, "TRY"),
|
||
nf0(ln.PriceExchangeRate), nf0(ln.Price),
|
||
nullableString(ln.BaseProcessCode, ""),
|
||
nullableString(ln.BaseOrderNumber, ""),
|
||
ln.BaseCustomerTypeCode,
|
||
nullableString(ln.BaseCustomerCode, ""),
|
||
nullableUUID(ln.BaseSubCurrAccID),
|
||
nullableString(ln.BaseStoreCode, ""),
|
||
v3User, now,
|
||
nf0(ln.SurplusOrderQtyToleranceRate),
|
||
nullableString(ln.WithHoldingTaxTypeCode, ""),
|
||
nullableString(ln.DOVCode, ""),
|
||
ln.OrderLineID,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// ✅ Currency UPSERT (insert/update sonrası ortak)
|
||
if err := upsertLineCurrency(
|
||
tx,
|
||
ln.OrderLineID,
|
||
safeNS(ln.DocCurrencyCode),
|
||
nf0(ln.PriceExchangeRate),
|
||
nf0(ln.Price),
|
||
nf0(ln.Qty1),
|
||
nf0(ln.LDisRate1),
|
||
0, // TODO: header TDisRate toplamını istersen buraya bağlarız
|
||
nf0(ln.VatRate), // satır vat oranı
|
||
ln,
|
||
v3User,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Bu satır işlendi -> existingOpen setinden düş
|
||
delete(existingOpen, ln.OrderLineID)
|
||
delete(existingOpenCombo, comboKey)
|
||
|
||
// Sonuç mapping
|
||
if ln.ClientKey.Valid {
|
||
lineResults = append(lineResults, OrderLineResult{
|
||
ClientKey: ln.ClientKey.String,
|
||
OrderLineID: ln.OrderLineID,
|
||
})
|
||
}
|
||
}
|
||
|
||
// =======================================================
|
||
// Grid dışı kalan açık satırlar (payload'da yok -> sil)
|
||
// =======================================================
|
||
for id := range existingOpen {
|
||
invoiced, err := isLineInvoiced(id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if invoiced {
|
||
meta := existingOpenMeta[id]
|
||
fmt.Printf("[ORDER_UPDATE] skip delete invoiced line id=%s item=%s color=%s dim1=%s dim2=%s\n",
|
||
id, meta.item, meta.color, meta.dim1, meta.dim2)
|
||
continue
|
||
}
|
||
if err := deleteOrSoftCloseLine(id); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// =======================================================
|
||
// COMMIT + RETURN
|
||
// =======================================================
|
||
if err := tx.Commit(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return lineResults, nil
|
||
}
|