401 lines
11 KiB
Go
401 lines
11 KiB
Go
package routes
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"bssapp-backend/db"
|
|
"bssapp-backend/models"
|
|
"bssapp-backend/queries"
|
|
"bssapp-backend/utils"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
// GET /api/pricing/production-product-costing/onml/pdf?n_onml_no=100001
|
|
// Generates a PDF export for the costing detail screen (has-cost).
|
|
func GetProductionProductCostingOnMLPDFHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/pdf")
|
|
|
|
uretimDB := db.GetUretimDB()
|
|
if uretimDB == nil {
|
|
http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
nOnMLNo := parsePositiveIntOrDefault(r.URL.Query().Get("n_onml_no"), 0)
|
|
if nOnMLNo <= 0 {
|
|
http.Error(w, "n_onml_no zorunlu", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
|
logger := utils.SlogFromContext(ctx).With("handler", "production-product-costing.onml.pdf", "n_onml_no", nOnMLNo)
|
|
logger.Info("request start")
|
|
|
|
// Header
|
|
hRow, err := queries.GetProductionHasCostDetailHeaderByOnMLNo(ctx, uretimDB, nOnMLNo)
|
|
if err != nil {
|
|
logger.Error("header query prepare error", "err", err)
|
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var header models.ProductionHasCostDetailHeader
|
|
if err := hRow.Scan(
|
|
&header.UretimiYapanFirma,
|
|
&header.SonIsEmriVeren,
|
|
&header.FirmaKodu,
|
|
&header.NFirmaID,
|
|
&header.NOnMLNo,
|
|
&header.UrunKodu,
|
|
&header.UrunAdi,
|
|
&header.UretimSekliID,
|
|
&header.UretimSekli,
|
|
&header.DteKayitTarihi,
|
|
&header.SKullaniciAdi,
|
|
&header.LTutarTL,
|
|
&header.LTutarUSD,
|
|
&header.LTutarEURO,
|
|
&header.LTutarGBP,
|
|
&header.SDovizCinsi,
|
|
&header.LTutarDoviz,
|
|
&header.DteGuncellemeTarihi,
|
|
&header.SGuncellemeKullaniciAdi,
|
|
&header.NUrtReceteID,
|
|
); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "Kayit bulunamadi", http.StatusNotFound)
|
|
return
|
|
}
|
|
logger.Error("header scan error", "err", err)
|
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Detail groups
|
|
groups, err := loadHasCostDetailGroups(ctx, uretimDB, nOnMLNo)
|
|
if err != nil {
|
|
logger.Error("groups load error", "err", err)
|
|
http.Error(w, "Veritabani hatasi", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
pdf := gofpdf.New("L", "mm", "A4", "")
|
|
pdf.SetMargins(8, 8, 8)
|
|
pdf.SetAutoPageBreak(false, 10)
|
|
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
|
|
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
export := &costingPDF{
|
|
pdf: pdf,
|
|
header: header,
|
|
groups: groups,
|
|
}
|
|
export.draw()
|
|
|
|
if err := pdf.Error(); err != nil {
|
|
http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := pdf.Output(&buf); err != nil {
|
|
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
filename := fmt.Sprintf("onml-%d.pdf", nOnMLNo)
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, filename))
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(buf.Bytes())
|
|
logger.Info("request done")
|
|
}
|
|
|
|
func loadHasCostDetailGroups(ctx context.Context, uretimDB *sql.DB, nOnMLNo int) ([]models.ProductionHasCostDetailGroup, error) {
|
|
rows, err := queries.GetProductionHasCostDetailRowsByOnMLNo(ctx, uretimDB, nOnMLNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
groups := make([]models.ProductionHasCostDetailGroup, 0, 16)
|
|
groupIndexByName := map[string]int{}
|
|
|
|
for rows.Next() {
|
|
var (
|
|
groupName string
|
|
groupTotal float64
|
|
groupTotalUSD float64
|
|
nOnMLNoStr string
|
|
nOnMLDetNoStr string
|
|
hNoStr string
|
|
fiyatGirilen sql.NullFloat64
|
|
fiyatDoviz sql.NullString
|
|
maliyeteDahil sql.NullBool
|
|
cmPriceTypeID sql.NullInt64
|
|
item models.ProductionHasCostDetailGroupItem
|
|
)
|
|
|
|
if err := rows.Scan(
|
|
&groupName,
|
|
&groupTotal,
|
|
&groupTotalUSD,
|
|
&nOnMLNoStr,
|
|
&nOnMLDetNoStr,
|
|
&hNoStr,
|
|
&item.SKodu,
|
|
&item.SAciklama,
|
|
&item.SRenk,
|
|
&item.SBeden,
|
|
&item.SAciklama2,
|
|
&item.LMiktar,
|
|
&item.LFiyat,
|
|
&item.LTutar,
|
|
&item.SFiyatTipi,
|
|
&item.SDovizCinsi,
|
|
&item.LDovizKuru,
|
|
&item.LDovizFiyati,
|
|
&fiyatGirilen,
|
|
&fiyatDoviz,
|
|
&maliyeteDahil,
|
|
&cmPriceTypeID,
|
|
&item.USDTutar,
|
|
&item.EURTutar,
|
|
&item.GBPTutar,
|
|
&item.SBirim,
|
|
&item.SHammaddeTuruAdi,
|
|
&item.SParcaAdi,
|
|
); err != nil {
|
|
continue
|
|
}
|
|
|
|
item.NOnMLNo = strings.TrimSpace(nOnMLNoStr)
|
|
item.NOnMLDetNo = strings.TrimSpace(nOnMLDetNoStr)
|
|
item.NHammaddeTuruNo = strings.TrimSpace(hNoStr)
|
|
if fiyatGirilen.Valid {
|
|
item.FiyatGirilen = new(float64)
|
|
*item.FiyatGirilen = fiyatGirilen.Float64
|
|
}
|
|
if fiyatDoviz.Valid {
|
|
item.FiyatDoviz = strings.TrimSpace(fiyatDoviz.String)
|
|
}
|
|
item.MaliyeteDahil = maliyeteDahil.Valid && maliyeteDahil.Bool
|
|
if cmPriceTypeID.Valid {
|
|
v := int(cmPriceTypeID.Int64)
|
|
item.CMPriceTypeID = &v
|
|
}
|
|
|
|
idx, ok := groupIndexByName[groupName]
|
|
if !ok {
|
|
groups = append(groups, models.ProductionHasCostDetailGroup{
|
|
SAciklama3: groupName,
|
|
TotalTutar: groupTotal,
|
|
TotalUSDTutar: groupTotalUSD,
|
|
Items: make([]models.ProductionHasCostDetailGroupItem, 0, 24),
|
|
})
|
|
idx = len(groups) - 1
|
|
groupIndexByName[groupName] = idx
|
|
}
|
|
groups[idx].Items = append(groups[idx].Items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// --- PDF drawing ---
|
|
|
|
type costingPDF struct {
|
|
pdf *gofpdf.Fpdf
|
|
header models.ProductionHasCostDetailHeader
|
|
groups []models.ProductionHasCostDetailGroup
|
|
}
|
|
|
|
func (c *costingPDF) draw() {
|
|
c.addPage(true)
|
|
for gi, g := range c.groups {
|
|
c.drawGroup(g, gi == 0)
|
|
}
|
|
}
|
|
|
|
func (c *costingPDF) addPage(fullHeader bool) {
|
|
c.pdf.AddPage()
|
|
if fullHeader {
|
|
c.drawHeaderFull()
|
|
} else {
|
|
c.drawHeaderCompact()
|
|
}
|
|
}
|
|
|
|
func (c *costingPDF) drawHeaderFull() {
|
|
pdf := c.pdf
|
|
pdf.SetFont("dejavu", "B", 14)
|
|
pdf.CellFormat(0, 7, "Maliyet Detay", "", 1, "L", false, 0, "")
|
|
|
|
pdf.SetFont("dejavu", "", 9)
|
|
pdf.SetTextColor(60, 60, 60)
|
|
line1 := fmt.Sprintf("OnML No: %s | Tarih: %s | Uretim Sekli: %s", c.header.NOnMLNo, c.header.DteKayitTarihi, strings.TrimSpace(c.header.UretimSekli))
|
|
pdf.CellFormat(0, 5, line1, "", 1, "L", false, 0, "")
|
|
|
|
line2 := fmt.Sprintf("Urun: %s - %s", strings.TrimSpace(c.header.UrunKodu), strings.TrimSpace(c.header.UrunAdi))
|
|
pdf.CellFormat(0, 5, line2, "", 1, "L", false, 0, "")
|
|
|
|
line3 := fmt.Sprintf("Firma: %s | Kaydeden: %s | Guncelleme: %s (%s)", strings.TrimSpace(c.header.FirmaKodu), strings.TrimSpace(c.header.SKullaniciAdi), strings.TrimSpace(c.header.DteGuncellemeTarihi), strings.TrimSpace(c.header.SGuncellemeKullaniciAdi))
|
|
pdf.CellFormat(0, 5, line3, "", 1, "L", false, 0, "")
|
|
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.Ln(2)
|
|
}
|
|
|
|
func (c *costingPDF) drawHeaderCompact() {
|
|
pdf := c.pdf
|
|
pdf.SetFont("dejavu", "B", 10.5)
|
|
title := fmt.Sprintf("OnML %s | %s - %s | %s", c.header.NOnMLNo, strings.TrimSpace(c.header.UrunKodu), strings.TrimSpace(c.header.UrunAdi), c.header.DteKayitTarihi)
|
|
pdf.CellFormat(0, 6, title, "", 1, "L", false, 0, "")
|
|
pdf.Ln(1)
|
|
}
|
|
|
|
func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup bool) {
|
|
pdf := c.pdf
|
|
|
|
// Group bar
|
|
c.drawGroupBar(g, false)
|
|
|
|
// Columns
|
|
cols := []string{"No", "Parca", "Hammadde", "Kod", "Aciklama", "Renk", "Miktar", "Br", "Fiyat", "Pr.Br", "Tutar(TRY)"}
|
|
wn := []float64{10, 24, 24, 40, 90, 18, 18, 12, 20, 14, 24} // sum ~294 (A4 landscape width minus margins)
|
|
|
|
c.drawTableHeader(cols, wn)
|
|
for _, it := range g.Items {
|
|
c.drawRowWithGroup(it, wn, cols, g)
|
|
}
|
|
pdf.Ln(2)
|
|
_ = firstGroup
|
|
}
|
|
|
|
func (c *costingPDF) drawGroupBar(g models.ProductionHasCostDetailGroup, continued bool) {
|
|
pdf := c.pdf
|
|
pdf.SetFont("dejavu", "B", 10)
|
|
pdf.SetFillColor(245, 245, 245)
|
|
name := strings.TrimSpace(g.SAciklama3)
|
|
if continued {
|
|
name = name + " (devam)"
|
|
}
|
|
pdf.CellFormat(0, 6, fmt.Sprintf("%s | Toplam TRY: %s | Toplam USD: %s", name, pdfMoney(g.TotalTutar), pdfMoney(g.TotalUSDTutar)), "1", 1, "L", true, 0, "")
|
|
}
|
|
|
|
func (c *costingPDF) drawTableHeader(cols []string, wn []float64) {
|
|
pdf := c.pdf
|
|
pdf.SetFont("dejavu", "B", 8)
|
|
pdf.SetFillColor(30, 30, 30)
|
|
pdf.SetTextColor(255, 255, 255)
|
|
for i, col := range cols {
|
|
pdf.CellFormat(wn[i], 5.5, col, "1", 0, "C", true, 0, "")
|
|
}
|
|
pdf.Ln(5.5)
|
|
pdf.SetTextColor(0, 0, 0)
|
|
}
|
|
|
|
func (c *costingPDF) ensureSpace(h float64) (newPage bool) {
|
|
pdf := c.pdf
|
|
_, pageH := pdf.GetPageSize()
|
|
_, _, _, mb := pdf.GetMargins()
|
|
if pdf.GetY()+h > pageH-mb {
|
|
c.addPage(false)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *costingPDF) drawRowWithGroup(it models.ProductionHasCostDetailGroupItem, wn []float64, cols []string, g models.ProductionHasCostDetailGroup) {
|
|
pdf := c.pdf
|
|
pdf.SetFont("dejavu", "", 7.2)
|
|
|
|
// Compute row height based on description wrapping.
|
|
descLines := pdf.SplitLines([]byte(strings.TrimSpace(it.SAciklama)), wn[4]-2)
|
|
rowH := float64(len(descLines)) * 3.5
|
|
if rowH < 5.0 {
|
|
rowH = 5.0
|
|
}
|
|
if c.ensureSpace(rowH + 12.0) {
|
|
// Redraw group bar + table header on new page.
|
|
c.drawGroupBar(g, true)
|
|
c.drawTableHeader(cols, wn)
|
|
}
|
|
|
|
x0 := pdf.GetX()
|
|
y0 := pdf.GetY()
|
|
|
|
c.drawCell(x0, y0, wn[0], rowH, it.NOnMLDetNo, "R")
|
|
x := x0 + wn[0]
|
|
c.drawCell(x, y0, wn[1], rowH, strings.TrimSpace(it.SParcaAdi), "L")
|
|
x += wn[1]
|
|
hLabel := strings.TrimSpace(it.NHammaddeTuruNo)
|
|
if strings.TrimSpace(it.SHammaddeTuruAdi) != "" {
|
|
hLabel = hLabel + " " + strings.TrimSpace(it.SHammaddeTuruAdi)
|
|
}
|
|
c.drawCell(x, y0, wn[2], rowH, hLabel, "L")
|
|
x += wn[2]
|
|
c.drawCell(x, y0, wn[3], rowH, strings.TrimSpace(it.SKodu), "L")
|
|
x += wn[3]
|
|
c.drawCellWrap(x, y0, wn[4], rowH, strings.TrimSpace(it.SAciklama), "L")
|
|
x += wn[4]
|
|
c.drawCell(x, y0, wn[5], rowH, strings.TrimSpace(it.SRenk), "L")
|
|
x += wn[5]
|
|
c.drawCell(x, y0, wn[6], rowH, pdfQty(it.LMiktar), "R")
|
|
x += wn[6]
|
|
c.drawCell(x, y0, wn[7], rowH, strings.TrimSpace(it.SBirim), "C")
|
|
x += wn[7]
|
|
|
|
// Prefer input price if present; otherwise lFiyat.
|
|
price := it.LFiyat
|
|
cur := strings.TrimSpace(it.SDovizCinsi)
|
|
if it.FiyatGirilen != nil && *it.FiyatGirilen > 0 {
|
|
price = *it.FiyatGirilen
|
|
if strings.TrimSpace(it.FiyatDoviz) != "" {
|
|
cur = strings.TrimSpace(it.FiyatDoviz)
|
|
}
|
|
}
|
|
c.drawCell(x, y0, wn[8], rowH, pdfMoney(price), "R")
|
|
x += wn[8]
|
|
c.drawCell(x, y0, wn[9], rowH, cur, "C")
|
|
x += wn[9]
|
|
c.drawCell(x, y0, wn[10], rowH, pdfMoney(it.LTutar), "R")
|
|
|
|
pdf.SetXY(x0, y0+rowH)
|
|
}
|
|
|
|
func (c *costingPDF) drawCell(x, y, w, h float64, txt, align string) {
|
|
pdf := c.pdf
|
|
pdf.Rect(x, y, w, h, "")
|
|
pdf.SetXY(x+0.8, y+(h-3.5)/2)
|
|
pdf.CellFormat(w-1.6, 3.5, txt, "", 0, align, false, 0, "")
|
|
}
|
|
|
|
func (c *costingPDF) drawCellWrap(x, y, w, h float64, txt, align string) {
|
|
pdf := c.pdf
|
|
pdf.Rect(x, y, w, h, "")
|
|
pdf.SetXY(x+0.8, y+0.6)
|
|
pdf.MultiCell(w-1.6, 3.5, txt, "", align, false)
|
|
// restore cursor (MultiCell moves Y)
|
|
pdf.SetXY(x+w, y)
|
|
}
|
|
|
|
func pdfMoney(v float64) string {
|
|
// 2 decimals, dot
|
|
return fmt.Sprintf("%.2f", v)
|
|
}
|
|
|
|
func pdfQty(v float64) string {
|
|
// 4 decimals (screen-like)
|
|
return fmt.Sprintf("%.4f", v)
|
|
}
|