Tela para análise de Vendas x Estoque

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:

Exemplo da Tela

Exemplo da 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.

Dan (Daniel Atilio)
Cristão de ramificação protestante. Especialista em Engenharia de Software pela FIB, graduado em Banco de Dados pela FATEC Bauru e técnico em informática pelo CTI da Unesp. Entusiasta de soluções Open Source e blogueiro nas horas vagas. Autor e mantenedor do portal Terminal de Informação.

Deixe uma resposta

Terminal de Informação