Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-13 07:27:57 +03:00
parent d571fe2fd5
commit 7f56bb40c5
38 changed files with 1709 additions and 457 deletions

View File

@@ -13,9 +13,10 @@ import (
"bssapp-backend/models"
"database/sql"
"fmt"
"github.com/google/uuid"
"strings"
"time"
"github.com/google/uuid"
)
func nf0(v models.NullFloat64) float64 {
@@ -25,6 +26,311 @@ func nf0(v models.NullFloat64) float64 {
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
// =======================================================
@@ -52,6 +358,32 @@ func qtyValue(q models.NullFloat64) float64 {
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)
@@ -173,9 +505,6 @@ func ValidateItemVariant(tx *sql.Tx, ln models.OrderDetail) error {
}
// İstersen debug:
// fmt.Printf("🧪 VARIANT CHECK item=%q color=%q dim1=%q dim2=%q clientKey=%s\n", item, color, dim1, dim2, safeNS(ln.ClientKey))
var exists int
err := tx.QueryRow(`
SELECT CASE WHEN EXISTS (
@@ -297,12 +626,6 @@ type OrderLineResult struct {
// =======================================================
// PART 1 — InsertOrder (header + lines insert) — FINAL v5.1
// ✔ OrderHeaderID backend üretir
// ✔ LOCAL-... numara gelirse gerçek WS numarası üretir
// ✔ Full debug
// ✔ Tüm satırlar INSERT edilir
// ✔ INSERT öncesi ItemVariant Guard
// ✔ Payload içi Duplicate Guard (comboKey)
// =======================================================
func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) (string, []OrderLineResult, error) {
@@ -317,7 +640,7 @@ func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
defer tx.Rollback()
now := time.Now()
v3User := fmt.Sprintf("V3U%d-%s", user.V3UserGroup, user.V3Username)
v3User := buildV3AuditUser(user)
// =======================================================
// 1) BACKEND — OrderHeaderID üretimi (HER ZAMAN)
@@ -368,7 +691,6 @@ func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
}
}
}
// =======================================================
// 4) HEADER INSERT
// =======================================================
@@ -423,6 +745,7 @@ VALUES (
fmt.Println("🟪 HEADER INSERT ÇALIŞIYOR...")
// ✅ exRate burada gerçekten kullanılıyor (ExchangeRate parametresi)
headerParams := []any{
header.OrderHeaderID,
nullableInt16(header.OrderTypeCode, 1),
@@ -474,7 +797,7 @@ VALUES (
nullableString(header.GLTypeCode, ""),
nullableString(header.DocCurrencyCode, "TRY"),
nullableString(header.LocalCurrencyCode, "TRY"),
nullableFloat64(header.ExchangeRate, exRate),
nullableFloat64(header.ExchangeRate, exRate), // ✅ exRate kullanıldı
nullableFloat64(header.TDisRate1, 0),
nullableFloat64(header.TDisRate2, 0),
@@ -520,6 +843,7 @@ VALUES (
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)
@@ -527,6 +851,7 @@ VALUES (
fmt.Println("🟩 HEADER INSERT OK — ID:", newID)
// headerParams ... (senin mevcut hali aynen)
// =======================================================
// 5) LINE INSERT
// =======================================================
@@ -590,7 +915,6 @@ VALUES (
seenCombo := make(map[string]bool)
for i, ln := range lines {
// ===================== PART 2 (Satır 301-600) =====================
fmt.Println("────────────────────────────────────")
fmt.Printf("🟨 [INSERT] LINE %d — gelen OrderLineID=%s\n", i+1, ln.OrderLineID)
@@ -630,6 +954,7 @@ VALUES (
}
planned := nullableDateString(ln.PlannedDateOfLading)
// ✅ INSERT ÖNCESİ ItemVariant GUARD
if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil {
@@ -637,6 +962,7 @@ VALUES (
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,
@@ -708,6 +1034,28 @@ VALUES (
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 headerdan 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,
@@ -734,25 +1082,16 @@ VALUES (
// =======================================================
// PART 2 — UpdateOrder FULL DEBUG (v4.3)
// ✔ ComboKey ile açık satır eşleştirme
// ✔ Kapalı satırları korur
// ✔ Payload içi Duplicate Guard
// ✔ INSERT/UPDATE öncesi ItemVariant Guard (tek noktada)
// ✔ Gridde olmayan açık satırları siler (önce child)
// =======================================================
func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) ([]OrderLineResult, error) {
conn := db.GetDB()
// ======================================================
// 🔍 SCAN DEBUG — HEADER bilgisi
// ======================================================
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.Printf("🔍 User: %v (V3: %s/%d)\n", user.Username, user.V3Username, user.V3UserGroup)
fmt.Println("══════════════════════════════════════")
tx, err := conn.Begin()
@@ -762,9 +1101,9 @@ func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
defer tx.Rollback()
now := time.Now()
v3User := fmt.Sprintf("V3U%d-%s", user.V3UserGroup, user.V3Username)
v3User := buildV3AuditUser(user)
// Döviz kuru
// 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 {
@@ -775,11 +1114,16 @@ func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *mo
// =======================================================
// 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
@@ -814,16 +1158,41 @@ WHERE OrderHeaderID=@p1
}
} 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,
@@ -853,11 +1222,12 @@ WHERE OrderHeaderID=@p11
if err != nil {
return nil, err
}
// ======================================================
// PREPARE STATEMENTS
// ======================================================
insStmt, err := tx.Prepare(`INSERT INTO BAGGI_V3.dbo.trOrderLine (
insStmt, err := tx.Prepare(`
INSERT INTO BAGGI_V3.dbo.trOrderLine (
OrderLineID, SortOrder, ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, Qty2, CancelQty1, CancelQty2, OrderCancelReasonCode,
@@ -872,21 +1242,23 @@ BaseSubCurrAccID, BaseStoreCode,
OrderHeaderID, CreatedUserName, CreatedDate,
LastUpdatedUserName, LastUpdatedDate,
SurplusOrderQtyToleranceRate,
WithHoldingTaxTypeCode, DOVCode)
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)`)
@p44,@p45
)`)
if err != nil {
return nil, err
}
defer insStmt.Close()
updStmt, err := tx.Prepare(`UPDATE BAGGI_V3.dbo.trOrderLine SET
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,
@@ -905,17 +1277,18 @@ 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)
@@ -929,7 +1302,7 @@ WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
seenCombo[comboKey] = true
}
// Kapalı satır
// Kapalı satır guard
if ln.OrderLineID != "" && existingClosed[ln.OrderLineID] {
continue
}
@@ -941,23 +1314,39 @@ WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
// DELETE SIGNAL
if ln.OrderLineID != "" && qtyValue(ln.Qty1) <= 0 {
_, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, ln.OrderLineID)
invoiced, err := isLineInvoiced(ln.OrderLineID)
if err != nil {
return nil, err
}
_, err = tx.Exec(`
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 := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, ln.OrderLineID); err != nil {
return nil, err
}
if _, err := tx.Exec(`
DELETE FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
`, header.OrderHeaderID, ln.OrderLineID)
if err != nil {
`, header.OrderHeaderID, 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
@@ -967,6 +1356,7 @@ WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
}
}
// Variant guard
if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil {
return nil, err
@@ -1048,9 +1438,28 @@ WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
}
}
// ✅ 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,
@@ -1059,18 +1468,31 @@ WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
}
}
// Grid dışı kalan açık satırlar
// =======================================================
// Grid dışı kalan açık satırlar (payload'da yok -> sil)
// =======================================================
for id := range existingOpen {
_, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, id)
invoiced, err := isLineInvoiced(id)
if err != nil {
return nil, err
}
_, err = tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLine WHERE OrderLineID=@p1 AND ISNULL(IsClosed,0)=0`, id)
if err != nil {
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 := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, id); err != nil {
return nil, err
}
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLine WHERE OrderLineID=@p1 AND ISNULL(IsClosed,0)=0`, id); err != nil {
return nil, err
}
}
// =======================================================
// COMMIT + RETURN
// =======================================================
if err := tx.Commit(); err != nil {
return nil, err
}