No artigo de hoje, vamos mostrar uma tela feita em TLPP que faz o confronto do que tem em estoque com o que tem nos pedidos de venda.
Imagina você ter que analisar, se o saldo em estoque vai atender às necessidades de venda da empresa, tendo que confrontar SB2 com SC6.
Pensando nisso, o grande João Pedro Baldasso ( LinkedIn ), preparou um exemplo que facilita isso para o pessoal que trabalha com essa análise.
Abaixo um print de exemplo de como ficou a tela:
E abaixo o código fonte que foi gentilmente cedido por ele:
#INCLUDE 'TLPP-CORE.TH'
Namespace custom.sales
Static cTitle := 'Produtos x Pedidos de Venda - Plásticos MB' As Character
Static oFont := TFont():New( 'Arial',, -12, .T. ) As Object
/***************************************************************************
{Protheus.doc} productsXsales
(Programa Desenvolvido Para Visualização de Produtos x Pedidos de Venda)
@author João Pedro Baldasso.
@since 16/10/2025
@version 1.0
@chamada function : custom.sales.U_productsXsales()
****************************************************************************/
User Function productsXsales()
Local aArea := FWGetArea() As Array
Local aCoors := FWGetDialogSize( oMainWnd ) As Array
// Dialog, camadas e paineis
Local oDialogBrowse := NIL As Object
Local oLayer := NIL As Object
Local oPanelTopLeft := NIL As Object
Local oPanelTopRight := NIL As Object
Local oPanelBottomSC5 := NIL As Object
Local oPanelBottomSC6 := NIL As Object
Local oPanelBottomTotal := NIL As Object
// Browses
Local oBrowseTopSB1 := NIL As Object
Local oBrowseTopSB2 := NIL As Object
Local oBrowseBottomSC6 := NIL As Object
Local oBrowseBottomSC5 := NIL As Object
// Relacionamentos
Local oRelationSB1xSC6 := NIL As Object
Local oRelationSB2xSB1 := NIL As Object
Local oRelationSC5xSC6 := NIL As Object
// Outros objetos
Private oSayOrdersQty := NIL As Object
Private oSayItemsQty := NIL As Object
Private oSayTotalValue := NIL As Object
Private oSayPercentage := NIL As Object
// Criando à Dialog Principal
oDialogBrowse := MSDialog():New( aCoors[1], aCoors[2], aCoors[3], aCoors[4], cTitle,,,.F.,,,,,,.T.,,,.T. )
oLayer := FWLayer():New()
oLayer:Init( oDialogBrowse, .F., .T. )
// Linha Superior - 40% da Tela.
oLayer:AddLine( 'SUPERIOR', 40, .F. )
oLayer:AddCollumn( 'COL_SUPESQ' , 60 , .T. , 'SUPERIOR' )
oLayer:AddCollumn( 'SEPARADOR' , 0.5, .T. , 'SUPERIOR' )
oLayer:AddCollumn( 'COL_SUPDIR' , 39 , .T. , 'SUPERIOR' )
oPanelTopLeft := oLayer:GetColPanel( 'COL_SUPESQ', 'SUPERIOR' )
oPanelTopRight := oLayer:GetColPanel( 'COL_SUPDIR', 'SUPERIOR' )
// Linha que Separará as Linhas Superior e Inferior.
oLayer:AddLine( 'SEPARADOR', 04, .F. )
oPainelSeparador := oLayer:GetLinePanel( 'SEPARADOR' )
oGroup1 := TGroup():New( 1, 1, 4, ( oPainelSeparador:nWidth / 2 ), '', oPainelSeparador,,, .T. )
// Linha Inferior - 56% da Tela.
oLayer:AddLine( 'INFERIOR', 56, .F. )
oLayer:AddCollumn( 'COL_INF_ESQ' , 40 , .T., 'INFERIOR' )
oLayer:AddCollumn( 'SEPARADOR1' , 0.5 , .T., 'INFERIOR' )
oLayer:AddCollumn( 'COL_INF_CENT', 45.5 , .T., 'INFERIOR' )
oLayer:AddCollumn( 'COL_INF_DIR' , 14 , .T., 'INFERIOR' )
oPanelBottomSC5 := oLayer:GetColPanel( 'COL_INF_ESQ' , 'INFERIOR' )
oPanelBottomSC6 := oLayer:GetColPanel( 'COL_INF_CENT', 'INFERIOR' )
oPanelBottomTotal := oLayer:GetColPanel( 'COL_INF_DIR' , 'INFERIOR' )
// Browse 4 - Inferior Central (Itens do Pedido).
oBrowseBottomSC6 := FWMBrowse():New()
oBrowseBottomSC6:SetOwner( oPanelBottomSC6 )
oBrowseBottomSC6:SetAlias( 'SC6' )
oBrowseBottomSC6:SetDescription( 'Itens do Pedido de Venda' )
oBrowseBottomSC6:DisableDetails()
oBrowseBottomSC6:DisableReport()
oBrowseBottomSC6:SetMenuDef( '' )
oBrowseBottomSC6:SetSeek( .F. )
oBrowseBottomSC6:Activate()
// Browse 1 - Superior Esquerdo (Produtos).
oBrowseTopSB1 := FWMBrowse():New()
oBrowseTopSB1:SetOwner( oPanelTopLeft )
oBrowseTopSB1:SetAlias( 'SB1' )
oBrowseTopSB1:SetDescription( 'Produtos' )
oBrowseTopSB1:DisableDetails()
oBrowseTopSB1:DisableReport()
oBrowseTopSB1:SetMenuDef( '' )
oBrowseTopSB1:SetSeek( .F. )
oBrowseTopSB1:SetChange( { || refreshTotal() } )
oRelationSB1xSC6 := FWBrwRelation():New()
oRelationSB1xSC6:AddRelation( oBrowseBottomSC6, oBrowseTopSB1, { { 'B1_COD', 'C6_PRODUTO' } } )
oBrowseTopSB1:Activate()
oRelationSB1xSC6:Activate()
// Browse 2 - Superior Direito (Estoque).
oBrowseTopSB2 := FWMBrowse():New()
oBrowseTopSB2:SetOwner( oPanelTopRight )
oBrowseTopSB2:SetAlias( 'SB2' )
oBrowseTopSB2:SetDescription( 'Estoque' )
oBrowseTopSB2:DisableDetails()
oBrowseTopSB2:DisableReport()
oBrowseTopSB2:SetMenuDef( '' )
oBrowseTopSB2:SetSeek( .F. )
oBrowseTopSB2:SetFilterDefault( 'SB2->B2_LOCAL == "10" ' )
// Adiciona coluna customizada com status
oBrowseTopSB2:AddMarkColumns( { || getStatus() }, { || '' }, { || '' } )
oRelationSB2xSB1 := FWBrwRelation():New()
oRelationSB2xSB1:AddRelation( oBrowseTopSB1, oBrowseTopSB2, { { 'B2_COD', 'B1_COD' } } )
oBrowseTopSB2:Activate()
oRelationSB2xSB1:Activate()
// Browse 3 - Inferior Esquerdo (Pedidos).
oBrowseBottomSC5 := FWMBrowse():New()
oBrowseBottomSC5:SetOwner( oPanelBottomSC5 )
oBrowseBottomSC5:SetAlias( 'SC5' )
oBrowseBottomSC5:SetDescription( 'Pedidos de Venda' )
oBrowseBottomSC5:DisableDetails()
oBrowseBottomSC5:DisableReport()
oBrowseBottomSC5:SetMenuDef( '' )
oBrowseBottomSC5:SetSeek( .F. )
oBrowseBottomSC5:SetFilterDefault( 'SC5->D_E_L_E_T_ = " " .AND. SC5->C5_NOTA = " " ' )
oRelationSC5xSC6 := FWBrwRelation():New()
oRelationSC5xSC6:AddRelation( oBrowseBottomSC5, oBrowseBottomSC6, { { 'C6_FILIAL', 'C5_FILIAL' }, { 'C6_NUM', 'C5_NUM' } } )
oBrowseBottomSC5:Activate()
oRelationSC5xSC6:Activate()
// Browse 5 - Inferior Direito (Totalizadores).
oBrwInferiorSomaQtdItensPedidoVenda := TGroup():New( 005, 005, ( oPanelBottomTotal:nHeight / 2 ), ( oPanelBottomTotal:nWidth / 2 ) -06, 'Totalizadores', oPanelBottomTotal,,, .T. )
// Quantidade Total de Pedidos.
TSay():New( 020, 015, { || 'Quantidade Pedidos : ' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 080, 010 )
oSayOrdersQty := TSay():New( 035, 015, { || '0' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 120, 020 )
// Quantidade Total de Itens.
TSay():New( 060, 015, { || 'Quantidade Itens : ' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 060, 010 )
oSayItemsQty := TSay():New( 070, 015, { || '0' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 120, 020 )
// Valor Total dos Pedidos.
TSay():New( 100, 015, { || 'Valor Total Itens : ' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 080, 010 )
oSayTotalValue := TSay():New( 110, 015, { || 'R$ 0,00' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 120, 020 )
// % de Atingimento de Saldo (Produtos).
TSay():New( 140, 015, { || 'Atingimento : ' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 080, 010 )
oSayPercentage := TSay():New( 150, 015, { || '0,00 %' }, oPanelBottomTotal,, oFont ,,,, .T.,,, 120, 020 )
// Atualiza totalizadores na inicialização
refreshTotal()
// Ativa Dialog.
oDialogBrowse:Activate( ,,, .T. )
FWRestArea(aArea)
Return()
/***************************************************************************
{Protheus.doc} getStatus
Retorna o ícone/cor conforme o atendimento do estoque
@author João Pedro Baldasso.
@since 20/10/2025
@version 1.0
@return cStatus - Status visual do estoque
****************************************************************************/
Static Function getStatus()
Local cStatus := 'BR_VERMELHO' As Character
Local cProduct := SB2->B2_COD As Character
Local nProductQty := SB2->B2_QATU As Numeric
Local cQuery := '' As Character
Local cAliasTemp := GetNextAlias() As Character
Local nTotalQty := 0 As Numeric
// Query para buscar a quantidade total do produto nos pedidos em aberto
cQuery := " SELECT SUM(C6_QTDVEN) AS QTD_TOTAL "
cQuery += " FROM " + RetSqlName('SC6') + " SC6 "
cQuery += " INNER JOIN " + RetSqlName('SC5') + " SC5 "
cQuery += " ON SC5.C5_FILIAL = SC6.C6_FILIAL "
cQuery += " AND SC5.C5_NUM = SC6.C6_NUM "
cQuery += " AND SC5.D_E_L_E_T_ = ' ' "
cQuery += " AND SC5.C5_NOTA = ' ' "
cQuery += " WHERE SC6.C6_FILIAL = '" + FWxFilial('SC6') + "' "
cQuery += " AND SC6.C6_PRODUTO = '" + cProduct + "' "
cQuery += " AND SC6.D_E_L_E_T_ = ' ' "
cQuery := ChangeQuery(cQuery)
If Select(cAliasTemp) > 0
(cAliasTemp)->(DbCloseArea())
EndIf
DbUseArea(.T., "TOPCONN", TCGenQry(,,cQuery), cAliasTemp, .F., .T.)
If (cAliasTemp)->(!Eof())
nTotalQty := (cAliasTemp)->QTD_TOTAL
EndIf
(cAliasTemp)->(DbCloseArea())
// Define a cor do status
If nTotalQty == 0
cStatus := 'BR_VERDE' // Verde - Sem pedidos pendentes
ElseIf nProductQty >= nTotalQty
cStatus := 'BR_VERDE' // Verde - Atende totalmente
ElseIf nProductQty > 0 .And. nProductQty < nTotalQty
cStatus := 'BR_AMARELO' // Amarelo - Atende parcialmente
Else
cStatus := 'BR_VERMELHO' // Vermelho - Não atende
EndIf
Return(cStatus)
/***************************************************************************
{Protheus.doc} refreshTotal
Atualiza os totalizadores com base no produto selecionado
@author João Pedro Baldasso.
@since 20/10/2025
@version 1.0
****************************************************************************/
Static Function refreshTotal()
Local cProduct := SB1->B1_COD As Character
Local cQuery := '' As Character
Local cAliasTemp := GetNextAlias() As Character
Local nSalesQty := 0 As Numeric
Local nTotalQty := 0 As Numeric
Local nTotalValue := 0 As Numeric
Local nProductQty := 0 As Numeric
Local nPercentage := 0 As Numeric
// Busca o saldo em estoque do produto no armazém 10
SB2->( DbSetOrder(1) ) // B2_FILIAL + B2_COD + B2_LOCAL
If SB2->( DbSeek( FWxFilial('SB2') + cProduct + '10' ) )
nProductQty := SB2->B2_QATU
EndIf
// Query para buscar os pedidos em aberto do produto
cQuery := " SELECT COUNT(DISTINCT C6_NUM) AS QTD_PEDIDOS, "
cQuery += " SUM(C6_QTDVEN) AS QTD_TOTAL, "
cQuery += " SUM(C6_VALOR) AS VLR_TOTAL "
cQuery += " FROM " + RetSqlName('SC6') + " SC6 "
cQuery += " INNER JOIN " + RetSqlName('SC5') + " SC5 "
cQuery += " ON SC5.C5_FILIAL = SC6.C6_FILIAL "
cQuery += " AND SC5.C5_NUM = SC6.C6_NUM "
cQuery += " AND SC5.D_E_L_E_T_ = ' ' "
cQuery += " AND SC5.C5_NOTA = ' ' "
cQuery += " WHERE SC6.C6_FILIAL = '" + FWxFilial('SC6') + "' "
cQuery += " AND SC6.C6_PRODUTO = '" + cProduct + "' "
cQuery += " AND SC6.D_E_L_E_T_ = ' ' "
cQuery := ChangeQuery(cQuery)
If Select(cAliasTemp) > 0
(cAliasTemp)->(DbCloseArea())
EndIf
DbUseArea(.T., "TOPCONN", TCGenQry(,,cQuery), cAliasTemp, .F., .T.)
If (cAliasTemp)->(!Eof())
nSalesQty := (cAliasTemp)->QTD_PEDIDOS
nTotalQty := (cAliasTemp)->QTD_TOTAL
nTotalValue := (cAliasTemp)->VLR_TOTAL
EndIf
(cAliasTemp)->(DbCloseArea())
// Calcula o percentual de atingimento
If nTotalQty > 0
nPercentage := (nProductQty / nTotalQty) * 100
Else
nPercentage := 0
EndIf
// Atualiza os objetos TSay
If ValType(oSayOrdersQty) == 'O'
oSayOrdersQty:SetText( AllTrim(Transform(nSalesQty, '@E 999,999')) )
oSayOrdersQty:CtrlRefresh()
EndIf
If ValType(oSayItemsQty) == 'O'
oSayItemsQty:SetText( AllTrim(Transform(nTotalQty, '@E 999,999,999.99')) )
oSayItemsQty:CtrlRefresh()
EndIf
If ValType(oSayTotalValue) == 'O'
oSayTotalValue:SetText( 'R$ ' + AllTrim(Transform(nTotalValue, '@E 999,999,999.99')) )
oSayTotalValue:CtrlRefresh()
EndIf
If ValType(oSayPercentage) == 'O'
oSayPercentage:SetText( AllTrim(Transform(nPercentage, '@E 9999.99')) + ' %' )
oSayPercentage:CtrlRefresh()
EndIf
Return()
Bom pessoal, por hoje é só.
Abraços e até a próxima.
