package models import ( "fmt" "odoo-go/pkg/orm" ) // initStockReport registers stock.report — transient model for stock quantity reporting. // Mirrors: odoo/addons/stock/report/stock_report_views.py func initStockReport() { m := orm.NewModel("stock.report", orm.ModelOpts{ Description: "Stock Report", Type: orm.ModelTransient, }) m.AddFields( orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product"}), orm.Many2one("location_id", "stock.location", orm.FieldOpts{String: "Location"}), orm.Date("date_from", orm.FieldOpts{String: "From"}), orm.Date("date_to", orm.FieldOpts{String: "To"}), ) // get_stock_data: Aggregate on-hand / reserved / available per product+location. // Mirrors: stock.report logic from Odoo stock views. m.RegisterMethod("get_stock_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() query := ` SELECT p.id, pt.name as product_name, l.name as location_name, COALESCE(SUM(q.quantity), 0) as on_hand, COALESCE(SUM(q.reserved_quantity), 0) as reserved, COALESCE(SUM(q.quantity - q.reserved_quantity), 0) as available FROM stock_quant q JOIN product_product p ON p.id = q.product_id JOIN product_template pt ON pt.id = p.product_tmpl_id JOIN stock_location l ON l.id = q.location_id WHERE l.usage = 'internal' GROUP BY p.id, pt.name, l.name ORDER BY pt.name, l.name` rows, err := env.Tx().Query(env.Ctx(), query) if err != nil { return nil, fmt.Errorf("stock.report: query stock data: %w", err) } defer rows.Close() var lines []map[string]interface{} for rows.Next() { var prodID int64 var prodName, locName string var onHand, reserved, available float64 if err := rows.Scan(&prodID, &prodName, &locName, &onHand, &reserved, &available); err != nil { return nil, fmt.Errorf("stock.report: scan row: %w", err) } lines = append(lines, map[string]interface{}{ "product_id": prodID, "product": prodName, "location": locName, "on_hand": onHand, "reserved": reserved, "available": available, }) } return map[string]interface{}{"lines": lines}, nil }) // get_stock_data_by_product: Aggregate stock for a specific product across all internal locations. // Mirrors: stock.report filtered by product m.RegisterMethod("get_stock_data_by_product", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.report.get_stock_data_by_product requires product_id") } productID, _ := args[0].(int64) if productID == 0 { return nil, fmt.Errorf("stock.report: invalid product_id") } env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), `SELECT l.id, l.complete_name, COALESCE(SUM(q.quantity), 0) as on_hand, COALESCE(SUM(q.reserved_quantity), 0) as reserved, COALESCE(SUM(q.quantity - q.reserved_quantity), 0) as available FROM stock_quant q JOIN stock_location l ON l.id = q.location_id WHERE q.product_id = $1 AND l.usage = 'internal' GROUP BY l.id, l.complete_name ORDER BY l.complete_name`, productID, ) if err != nil { return nil, fmt.Errorf("stock.report: query by product: %w", err) } defer rows.Close() var lines []map[string]interface{} var totalOnHand, totalReserved, totalAvailable float64 for rows.Next() { var locID int64 var locName string var onHand, reserved, available float64 if err := rows.Scan(&locID, &locName, &onHand, &reserved, &available); err != nil { return nil, fmt.Errorf("stock.report: scan by product row: %w", err) } lines = append(lines, map[string]interface{}{ "location_id": locID, "location": locName, "on_hand": onHand, "reserved": reserved, "available": available, }) totalOnHand += onHand totalReserved += reserved totalAvailable += available } return map[string]interface{}{ "product_id": productID, "lines": lines, "total_on_hand": totalOnHand, "total_reserved": totalReserved, "total_available": totalAvailable, }, nil }) // get_stock_data_by_location: Aggregate stock for a specific location across all products. // Mirrors: stock.report filtered by location m.RegisterMethod("get_stock_data_by_location", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.report.get_stock_data_by_location requires location_id") } locationID, _ := args[0].(int64) if locationID == 0 { return nil, fmt.Errorf("stock.report: invalid location_id") } env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), `SELECT p.id, pt.name as product_name, COALESCE(SUM(q.quantity), 0) as on_hand, COALESCE(SUM(q.reserved_quantity), 0) as reserved, COALESCE(SUM(q.quantity - q.reserved_quantity), 0) as available FROM stock_quant q JOIN product_product p ON p.id = q.product_id JOIN product_template pt ON pt.id = p.product_tmpl_id WHERE q.location_id = $1 GROUP BY p.id, pt.name ORDER BY pt.name`, locationID, ) if err != nil { return nil, fmt.Errorf("stock.report: query by location: %w", err) } defer rows.Close() var lines []map[string]interface{} var totalOnHand, totalReserved, totalAvailable float64 for rows.Next() { var prodID int64 var prodName string var onHand, reserved, available float64 if err := rows.Scan(&prodID, &prodName, &onHand, &reserved, &available); err != nil { return nil, fmt.Errorf("stock.report: scan by location row: %w", err) } lines = append(lines, map[string]interface{}{ "product_id": prodID, "product": prodName, "on_hand": onHand, "reserved": reserved, "available": available, }) totalOnHand += onHand totalReserved += reserved totalAvailable += available } return map[string]interface{}{ "location_id": locationID, "lines": lines, "total_on_hand": totalOnHand, "total_reserved": totalReserved, "total_available": totalAvailable, }, nil }) // get_move_history: Return stock move history with filters. // Mirrors: stock.move.line reporting / traceability m.RegisterMethod("get_move_history", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() query := ` SELECT sm.id, sm.name, sm.product_id, pt.name as product_name, sm.product_uom_qty, sm.state, sl_src.name as source_location, sl_dst.name as dest_location, sm.date, sm.origin FROM stock_move sm JOIN product_product pp ON pp.id = sm.product_id JOIN product_template pt ON pt.id = pp.product_tmpl_id JOIN stock_location sl_src ON sl_src.id = sm.location_id JOIN stock_location sl_dst ON sl_dst.id = sm.location_dest_id WHERE sm.state = 'done' ORDER BY sm.date DESC LIMIT 100` rows, err := env.Tx().Query(env.Ctx(), query) if err != nil { return nil, fmt.Errorf("stock.report: query move history: %w", err) } defer rows.Close() var moves []map[string]interface{} for rows.Next() { var moveID, productID int64 var name, productName, state, srcLoc, dstLoc string var qty float64 var date, origin *string if err := rows.Scan(&moveID, &name, &productID, &productName, &qty, &state, &srcLoc, &dstLoc, &date, &origin); err != nil { return nil, fmt.Errorf("stock.report: scan move history row: %w", err) } dateStr := "" if date != nil { dateStr = *date } originStr := "" if origin != nil { originStr = *origin } moves = append(moves, map[string]interface{}{ "id": moveID, "name": name, "product_id": productID, "product": productName, "quantity": qty, "state": state, "source_location": srcLoc, "dest_location": dstLoc, "date": dateStr, "origin": originStr, }) } return map[string]interface{}{"moves": moves}, nil }) // get_inventory_valuation: Return total inventory valuation by product. // Mirrors: stock report valuation views m.RegisterMethod("get_inventory_valuation", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), `SELECT p.id, pt.name as product_name, COALESCE(SUM(q.quantity), 0) as total_qty, COALESCE(SUM(q.value), 0) as total_value FROM stock_quant q JOIN product_product p ON p.id = q.product_id JOIN product_template pt ON pt.id = p.product_tmpl_id JOIN stock_location l ON l.id = q.location_id WHERE l.usage = 'internal' GROUP BY p.id, pt.name HAVING SUM(q.quantity) > 0 ORDER BY pt.name`, ) if err != nil { return nil, fmt.Errorf("stock.report: query valuation: %w", err) } defer rows.Close() var lines []map[string]interface{} var grandTotalQty, grandTotalValue float64 for rows.Next() { var prodID int64 var prodName string var totalQty, totalValue float64 if err := rows.Scan(&prodID, &prodName, &totalQty, &totalValue); err != nil { return nil, fmt.Errorf("stock.report: scan valuation row: %w", err) } avgCost := float64(0) if totalQty > 0 { avgCost = totalValue / totalQty } lines = append(lines, map[string]interface{}{ "product_id": prodID, "product": prodName, "quantity": totalQty, "value": totalValue, "average_cost": avgCost, }) grandTotalQty += totalQty grandTotalValue += totalValue } return map[string]interface{}{ "lines": lines, "total_qty": grandTotalQty, "total_value": grandTotalValue, }, nil }) } // initStockForecast registers stock.forecasted.product — transient model for forecast computation. // Mirrors: odoo/addons/stock/models/stock_forecasted.py func initStockForecast() { m := orm.NewModel("stock.forecasted.product", orm.ModelOpts{ Description: "Forecasted Stock", Type: orm.ModelTransient, }) m.AddFields( orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product"}), ) // get_forecast: Compute on-hand, incoming, outgoing and forecast for a product. // Mirrors: stock.forecasted.product_product._get_report_data() m.RegisterMethod("get_forecast", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() productID := int64(0) if len(args) > 0 { if p, ok := args[0].(float64); ok { productID = int64(p) } } // On hand var onHand float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(SUM(quantity - reserved_quantity), 0) FROM stock_quant WHERE product_id = $1 AND location_id IN (SELECT id FROM stock_location WHERE usage = 'internal')`, productID).Scan(&onHand) // Incoming (confirmed moves TO internal locations) var incoming float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(SUM(product_uom_qty), 0) FROM stock_move WHERE product_id = $1 AND state IN ('confirmed','assigned','waiting') AND location_dest_id IN (SELECT id FROM stock_location WHERE usage = 'internal')`, productID).Scan(&incoming) // Outgoing (confirmed moves FROM internal locations) var outgoing float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(SUM(product_uom_qty), 0) FROM stock_move WHERE product_id = $1 AND state IN ('confirmed','assigned','waiting') AND location_id IN (SELECT id FROM stock_location WHERE usage = 'internal')`, productID).Scan(&outgoing) return map[string]interface{}{ "on_hand": onHand, "incoming": incoming, "outgoing": outgoing, "forecast": onHand + incoming - outgoing, }, nil }) // get_forecast_details: Detailed forecast with move-level breakdown. // Mirrors: stock.forecasted.product_product._get_report_lines() m.RegisterMethod("get_forecast_details", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() productID := int64(0) if len(args) > 0 { if p, ok := args[0].(float64); ok { productID = int64(p) } } if productID == 0 { return nil, fmt.Errorf("stock.forecasted.product: product_id required") } // On hand var onHand float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(SUM(quantity - reserved_quantity), 0) FROM stock_quant WHERE product_id = $1 AND location_id IN (SELECT id FROM stock_location WHERE usage = 'internal')`, productID).Scan(&onHand) // Incoming moves inRows, err := env.Tx().Query(env.Ctx(), `SELECT sm.id, sm.name, sm.product_uom_qty, sm.date, sm.state, sl.name as source_location, sld.name as dest_location, sp.name as picking_name FROM stock_move sm JOIN stock_location sl ON sl.id = sm.location_id JOIN stock_location sld ON sld.id = sm.location_dest_id LEFT JOIN stock_picking sp ON sp.id = sm.picking_id WHERE sm.product_id = $1 AND sm.state IN ('confirmed','assigned','waiting') AND sm.location_dest_id IN (SELECT id FROM stock_location WHERE usage = 'internal') ORDER BY sm.date`, productID, ) if err != nil { return nil, fmt.Errorf("stock.forecasted: query incoming moves: %w", err) } defer inRows.Close() var incomingMoves []map[string]interface{} var totalIncoming float64 for inRows.Next() { var moveID int64 var name, state, srcLoc, dstLoc string var qty float64 var date, pickingName *string if err := inRows.Scan(&moveID, &name, &qty, &date, &state, &srcLoc, &dstLoc, &pickingName); err != nil { return nil, fmt.Errorf("stock.forecasted: scan incoming move: %w", err) } dateStr := "" if date != nil { dateStr = *date } pickStr := "" if pickingName != nil { pickStr = *pickingName } incomingMoves = append(incomingMoves, map[string]interface{}{ "id": moveID, "name": name, "quantity": qty, "date": dateStr, "state": state, "source": srcLoc, "destination": dstLoc, "picking": pickStr, }) totalIncoming += qty } // Outgoing moves outRows, err := env.Tx().Query(env.Ctx(), `SELECT sm.id, sm.name, sm.product_uom_qty, sm.date, sm.state, sl.name as source_location, sld.name as dest_location, sp.name as picking_name FROM stock_move sm JOIN stock_location sl ON sl.id = sm.location_id JOIN stock_location sld ON sld.id = sm.location_dest_id LEFT JOIN stock_picking sp ON sp.id = sm.picking_id WHERE sm.product_id = $1 AND sm.state IN ('confirmed','assigned','waiting') AND sm.location_id IN (SELECT id FROM stock_location WHERE usage = 'internal') ORDER BY sm.date`, productID, ) if err != nil { return nil, fmt.Errorf("stock.forecasted: query outgoing moves: %w", err) } defer outRows.Close() var outgoingMoves []map[string]interface{} var totalOutgoing float64 for outRows.Next() { var moveID int64 var name, state, srcLoc, dstLoc string var qty float64 var date, pickingName *string if err := outRows.Scan(&moveID, &name, &qty, &date, &state, &srcLoc, &dstLoc, &pickingName); err != nil { return nil, fmt.Errorf("stock.forecasted: scan outgoing move: %w", err) } dateStr := "" if date != nil { dateStr = *date } pickStr := "" if pickingName != nil { pickStr = *pickingName } outgoingMoves = append(outgoingMoves, map[string]interface{}{ "id": moveID, "name": name, "quantity": qty, "date": dateStr, "state": state, "source": srcLoc, "destination": dstLoc, "picking": pickStr, }) totalOutgoing += qty } forecast := onHand + totalIncoming - totalOutgoing return map[string]interface{}{ "product_id": productID, "on_hand": onHand, "incoming": totalIncoming, "outgoing": totalOutgoing, "forecast": forecast, "incoming_moves": incomingMoves, "outgoing_moves": outgoingMoves, }, nil }) // get_forecast_all: Compute forecast for all products with stock or pending moves. // Mirrors: stock.forecasted overview m.RegisterMethod("get_forecast_all", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), `SELECT p.id, pt.name as product_name, COALESCE(oh.on_hand, 0) as on_hand, COALESCE(inc.incoming, 0) as incoming, COALESCE(outg.outgoing, 0) as outgoing FROM product_product p JOIN product_template pt ON pt.id = p.product_tmpl_id LEFT JOIN ( SELECT product_id, SUM(quantity - reserved_quantity) as on_hand FROM stock_quant WHERE location_id IN (SELECT id FROM stock_location WHERE usage = 'internal') GROUP BY product_id ) oh ON oh.product_id = p.id LEFT JOIN ( SELECT product_id, SUM(product_uom_qty) as incoming FROM stock_move WHERE state IN ('confirmed','assigned','waiting') AND location_dest_id IN (SELECT id FROM stock_location WHERE usage = 'internal') GROUP BY product_id ) inc ON inc.product_id = p.id LEFT JOIN ( SELECT product_id, SUM(product_uom_qty) as outgoing FROM stock_move WHERE state IN ('confirmed','assigned','waiting') AND location_id IN (SELECT id FROM stock_location WHERE usage = 'internal') GROUP BY product_id ) outg ON outg.product_id = p.id WHERE COALESCE(oh.on_hand, 0) != 0 OR COALESCE(inc.incoming, 0) != 0 OR COALESCE(outg.outgoing, 0) != 0 ORDER BY pt.name`, ) if err != nil { return nil, fmt.Errorf("stock.forecasted: query all forecasts: %w", err) } defer rows.Close() var products []map[string]interface{} for rows.Next() { var prodID int64 var prodName string var onHand, incoming, outgoing float64 if err := rows.Scan(&prodID, &prodName, &onHand, &incoming, &outgoing); err != nil { return nil, fmt.Errorf("stock.forecasted: scan forecast row: %w", err) } products = append(products, map[string]interface{}{ "product_id": prodID, "product": prodName, "on_hand": onHand, "incoming": incoming, "outgoing": outgoing, "forecast": onHand + incoming - outgoing, }) } return map[string]interface{}{"products": products}, nil }) } // initStockIntrastat registers stock.intrastat.line — Intrastat reporting model for // EU cross-border trade declarations. Tracks move-level trade data. // Mirrors: odoo/addons/stock_intrastat/models/stock_intrastat.py func initStockIntrastat() { m := orm.NewModel("stock.intrastat.line", orm.ModelOpts{ Description: "Intrastat Line", Order: "id desc", }) m.AddFields( orm.Many2one("move_id", "stock.move", orm.FieldOpts{ String: "Stock Move", Required: true, Index: true, OnDelete: orm.OnDeleteCascade, }), orm.Many2one("product_id", "product.product", orm.FieldOpts{ String: "Product", Required: true, Index: true, }), orm.Many2one("country_id", "res.country", orm.FieldOpts{ String: "Country", Required: true, Index: true, }), orm.Float("weight", orm.FieldOpts{String: "Weight (kg)", Required: true}), orm.Monetary("value", orm.FieldOpts{String: "Fiscal Value", CurrencyField: "currency_id", Required: true}), orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}), orm.Selection("transaction_type", []orm.SelectionItem{ {Value: "arrival", Label: "Arrival"}, {Value: "dispatch", Label: "Dispatch"}, }, orm.FieldOpts{String: "Transaction Type", Required: true, Index: true}), orm.Char("intrastat_code", orm.FieldOpts{String: "Commodity Code"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Index: true}), orm.Date("date", orm.FieldOpts{String: "Date", Index: true}), orm.Char("transport_mode", orm.FieldOpts{String: "Transport Mode"}), ) // generate_lines: Auto-generate Intrastat lines from done stock moves in a date range. // Args: date_from (string), date_to (string), optional company_id (int64) // Mirrors: stock.intrastat.report generation m.RegisterMethod("generate_lines", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("stock.intrastat.line.generate_lines requires date_from, date_to") } dateFrom, _ := args[0].(string) dateTo, _ := args[1].(string) if dateFrom == "" || dateTo == "" { return nil, fmt.Errorf("stock.intrastat.line: invalid date range") } companyID := int64(1) if len(args) >= 3 { if cid, ok := args[2].(int64); ok && cid > 0 { companyID = cid } } env := rs.Env() // Find done moves crossing borders (source or dest is in a different country) // For simplicity, look for moves between locations belonging to different warehouses // or between internal and non-internal locations. rows, err := env.Tx().Query(env.Ctx(), `SELECT sm.id, sm.product_id, sm.product_uom_qty, sm.price_unit, sm.date, sl_src.usage as src_usage, sl_dst.usage as dst_usage, COALESCE(rp.country_id, 0) as partner_country_id FROM stock_move sm JOIN stock_location sl_src ON sl_src.id = sm.location_id JOIN stock_location sl_dst ON sl_dst.id = sm.location_dest_id LEFT JOIN stock_picking sp ON sp.id = sm.picking_id LEFT JOIN res_partner rp ON rp.id = sp.partner_id WHERE sm.state = 'done' AND sm.date >= $1 AND sm.date <= $2 AND sm.company_id = $3 AND ( (sl_src.usage = 'supplier' AND sl_dst.usage = 'internal') OR (sl_src.usage = 'internal' AND sl_dst.usage = 'customer') ) ORDER BY sm.date`, dateFrom, dateTo, companyID, ) if err != nil { return nil, fmt.Errorf("stock.intrastat.line: query moves: %w", err) } type moveData struct { MoveID, ProductID int64 Qty, PriceUnit float64 Date *string SrcUsage string DstUsage string CountryID int64 } var moves []moveData for rows.Next() { var md moveData if err := rows.Scan(&md.MoveID, &md.ProductID, &md.Qty, &md.PriceUnit, &md.Date, &md.SrcUsage, &md.DstUsage, &md.CountryID); err != nil { rows.Close() return nil, fmt.Errorf("stock.intrastat.line: scan move: %w", err) } moves = append(moves, md) } rows.Close() var created int for _, md := range moves { // Determine transaction type txnType := "arrival" if md.SrcUsage == "internal" && md.DstUsage == "customer" { txnType = "dispatch" } // Use partner country; skip if no country (can't determine border crossing) countryID := md.CountryID if countryID == 0 { continue } // Compute value and weight value := md.Qty * md.PriceUnit weight := md.Qty // Simplified: weight = qty (would use product.weight in full impl) dateStr := "" if md.Date != nil { dateStr = *md.Date } // Check if line already exists for this move var existing int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM stock_intrastat_line WHERE move_id = $1 LIMIT 1`, md.MoveID, ).Scan(&existing) if existing > 0 { continue } _, err := env.Tx().Exec(env.Ctx(), `INSERT INTO stock_intrastat_line (move_id, product_id, country_id, weight, value, transaction_type, company_id, date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, md.MoveID, md.ProductID, countryID, weight, value, txnType, companyID, dateStr, ) if err != nil { return nil, fmt.Errorf("stock.intrastat.line: create line for move %d: %w", md.MoveID, err) } created++ } return map[string]interface{}{ "created": created, "date_from": dateFrom, "date_to": dateTo, }, nil }) // get_report: Return Intrastat report data for a period. // Args: date_from (string), date_to (string), optional transaction_type (string) // Mirrors: stock.intrastat.report views m.RegisterMethod("get_report", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("stock.intrastat.line.get_report requires date_from, date_to") } dateFrom, _ := args[0].(string) dateTo, _ := args[1].(string) var txnTypeFilter string if len(args) >= 3 { txnTypeFilter, _ = args[2].(string) } env := rs.Env() query := `SELECT sil.id, sil.move_id, sil.product_id, pt.name as product_name, sil.country_id, COALESCE(rc.name, '') as country_name, sil.weight, sil.value, sil.transaction_type, COALESCE(sil.intrastat_code, '') as commodity_code, sil.date FROM stock_intrastat_line sil JOIN product_product pp ON pp.id = sil.product_id JOIN product_template pt ON pt.id = pp.product_tmpl_id LEFT JOIN res_country rc ON rc.id = sil.country_id WHERE sil.date >= $1 AND sil.date <= $2` queryArgs := []interface{}{dateFrom, dateTo} if txnTypeFilter != "" { query += ` AND sil.transaction_type = $3` queryArgs = append(queryArgs, txnTypeFilter) } query += ` ORDER BY sil.date, sil.id` rows, err := env.Tx().Query(env.Ctx(), query, queryArgs...) if err != nil { return nil, fmt.Errorf("stock.intrastat.line: query report: %w", err) } defer rows.Close() var lines []map[string]interface{} var totalWeight, totalValue float64 for rows.Next() { var lineID, moveID, productID, countryID int64 var productName, countryName, txnType, commodityCode string var weight, value float64 var date *string if err := rows.Scan(&lineID, &moveID, &productID, &productName, &countryID, &countryName, &weight, &value, &txnType, &commodityCode, &date); err != nil { return nil, fmt.Errorf("stock.intrastat.line: scan report row: %w", err) } dateStr := "" if date != nil { dateStr = *date } lines = append(lines, map[string]interface{}{ "id": lineID, "move_id": moveID, "product_id": productID, "product": productName, "country_id": countryID, "country": countryName, "weight": weight, "value": value, "transaction_type": txnType, "commodity_code": commodityCode, "date": dateStr, }) totalWeight += weight totalValue += value } return map[string]interface{}{ "lines": lines, "total_weight": totalWeight, "total_value": totalValue, "date_from": dateFrom, "date_to": dateTo, }, nil }) }