feat(api,web): peso total (kg) e cubagem (m3) do pedido na impressao

- vw_produtos passa a expor vol_m3 (coluna nova no FIM da view - CREATE
  OR REPLACE exige; aplicada no dev)
- PedidoDetail calcula pesoTotal/m3Total (qtd x peso_liquido/vol_m3),
  tanto para pedidos SAR quanto ERP
- Impressao mostra "Peso total: X kg" e "Volume: Y m3" ao lado dos
  totais (some quando zerado)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 19:59:27 +00:00
parent 8a5db57f35
commit 2ff1f9328a
4 changed files with 66 additions and 5 deletions

View File

@@ -1022,14 +1022,31 @@ export class OrdersService {
const grupoRows =
prisma && prodIds.length > 0
? await prisma.$queryRawUnsafe<
{ id_erp: number; cod_grupo: number | null; cod_subgrupo: number | null }[]
{
id_erp: number;
cod_grupo: number | null;
cod_subgrupo: number | null;
peso_liquido: string | null;
vol_m3: string | null;
}[]
>(
`SELECT id_erp, cod_grupo, cod_subgrupo FROM vw_produtos
`SELECT id_erp, cod_grupo, cod_subgrupo, peso_liquido::text, vol_m3::text
FROM vw_produtos
WHERE id_empresa = ${idMatriz} AND id_erp IN (${prodIds.join(',')})`,
)
: [];
const grupoMap = new Map(grupoRows.map((g) => [Number(g.id_erp), g]));
// Peso total (kg) e cubagem (m3) do pedido: soma de qtd x peso/vol do produto
let pesoTotal = 0;
let m3Total = 0;
for (const it of o.itens) {
const prod = grupoMap.get(it.idProduto);
const qtd = Number(it.qtd);
pesoTotal += qtd * Number(prod?.peso_liquido ?? 0);
m3Total += qtd * Number(prod?.vol_m3 ?? 0);
}
return {
id: o.id,
numPedSar: o.numPedSar,
@@ -1042,6 +1059,8 @@ export class OrdersService {
dtPedido: o.dtPedido.toISOString(),
idPauta: o.idPauta,
codFormapag: o.codFormapag,
pesoTotal: pesoTotal.toFixed(3),
m3Total: m3Total.toFixed(4),
total: decimalToString(o.total),
descontoPerc: decimalToString(o.descontoPerc),
obs: o.obs,
@@ -1132,6 +1151,8 @@ export class OrdersService {
preco_unitario: string;
desconto_perc: string;
total: string;
peso_liquido: string | null;
vol_m3: string | null;
}
const vendedorFilter =
@@ -1164,7 +1185,8 @@ export class OrdersService {
SELECT ei.ordem, ei.id_produto,
TRIM(p.codigo) AS codigo, TRIM(p.descricao) AS descricao,
ei.qtd::text, ei.preco_unitario::text,
ei.desconto_perc::text, ei.total::text
ei.desconto_perc::text, ei.total::text,
p.peso_liquido::text AS peso_liquido, p.vol_m3::text AS vol_m3
FROM vw_peditens_erp ei
LEFT JOIN vw_produtos p
ON p.id_erp = ei.id_produto
@@ -1177,6 +1199,15 @@ export class OrdersService {
if (!headerRows[0]) throw new NotFoundException(`Pedido ERP ${idPedido} não encontrado`);
const h = headerRows[0];
const pesoTotalErp = itemRows.reduce(
(acc, it) => acc + Number(it.qtd) * Number(it.peso_liquido ?? 0),
0,
);
const m3TotalErp = itemRows.reduce(
(acc, it) => acc + Number(it.qtd) * Number(it.vol_m3 ?? 0),
0,
);
return {
id: `erp-${h.id_pedido}`,
numPedSar: (h.num_ped_sar ?? '').trim(),
@@ -1207,6 +1238,8 @@ export class OrdersService {
aprovadoEm: null,
motivoRecusa: null,
idempotencyKey: null,
pesoTotal: pesoTotalErp.toFixed(3),
m3Total: m3TotalErp.toFixed(4),
itens: itemRows.map((it) => ({
id: `${idPedido}-${it.ordem}`,
idProduto: Number(it.id_produto),

View File

@@ -381,7 +381,30 @@ export function OrderPrintPage() {
</div>
{/* ── Totais ──────────────────────────────────────────────────────── */}
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '14px 28px 4px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-end',
gap: 16,
padding: '14px 28px 4px',
}}
>
{/* Peso e cubagem do pedido (logística/frete) */}
<div style={{ fontSize: 10.5, color: MUTED, lineHeight: 1.8 }}>
{Number(order.pesoTotal ?? 0) > 0 && (
<div>
<strong style={{ color: INK }}>Peso total:</strong>{' '}
{Number(order.pesoTotal).toLocaleString('pt-BR', { maximumFractionDigits: 2 })} kg
</div>
)}
{Number(order.m3Total ?? 0) > 0 && (
<div>
<strong style={{ color: INK }}>Volume:</strong>{' '}
{Number(order.m3Total).toLocaleString('pt-BR', { maximumFractionDigits: 3 })} m³
</div>
)}
</div>
<div style={{ width: 300 }}>
<TotRow k="Total dos produtos" v={money(order.totalProdutos)} />
{Number(order.totalIpi) > 0 && <TotRow k="IPI" v={money(order.totalIpi)} />}