Tabela de Cores

Para nossa referência futura:

CLR_YELLOW    16   Amarelo
CLR_BLUE       2   Azul
CLR_HBLUE     11   Azul Claro
CLR_WHITE     17   Branco
CLR_CYAN       4   Ciano
CLR_HCYAN     13   Ciano Claro
CLR_GRAY      10   Cinza
CLR_HGRAY      8   Cinza Claro
CLR_MAGENTA    6   Magenta
CLR_HMAGENTA  15   Magenta Claro
CLR_BROWN      7   Marrom
CLR_BLACK      1   Preto
CLR_GREEN      3   Verde
CLR_HGREEN    12   Verde Claro
CLR_RED        5   Vermellho
CLR_HRED      14   Vermelho Claro

Limite de Memória no SQL Server

Adicionalmente ao post que eu fiz sobre Configuração de Balance no Protheus gostaria também de compartilhar uma configuração muito simples e que pode salvar servidores que estão à beira de um colapso, especialmente quando as aplicações do Protheus e o SQL Server rodam na mesma máquina. Essa situação (aplicações e banco no mesmo servidor) não é muito recomendada quando a empresa começa a exigir mais do sistema (a partir de 30 usuários simultâneos; ou quando se utiliza rotinas de processamento com certa frequência). Mas sabemos que nem todos possuem estrutura ($$$) suficiente para um dimensionamento ideal. Dessa forma, travamentos no sistema podem ser resolvidos com o Balance que expliquei anteriormente. Mas a lentidão ainda existirá. Resolvê-la completamente é outra história. Exige realmente um correto dimensionamento dos servidores, de preferência feito por um Analista de Infra e um bom DBA (eu não sou nem um nem outro!).

No entanto, podemos reduzir bastante a lentidão e praticamente nos livrar de travamentos no servidor por estouro de memória. Sabemos que o Windows Server não faz um gerenciamento de memória perfeito, portanto quando falei sobre o Balance expliquei que o ideal é que os appservers do Protheus não trabalhem com mais de 1 GB de memória cada. Acompanhando esse raciocínio, devemos então entender que o servidor precisa ter disponível esse 1 GB de memória RAM para cada appserver que configuramos. Quando temos uma demanda de 20 a 40 usuários, o SQL Server roda muito bem com 2 GB de RAM dedicados. Mas no padrão, o SQL Server utiliza TODA a memória disponível do sistema, e isso não é nada bom, pois o Windows Server frequentemente se enrola. Precisamos resolver isso! Então devemos definir no SQL Server um limite máximo de uso de memória.

(1) No Management Studio, clique com o botão direito no servidor conectado.

(2) Em seguida, clique em Propriedades ou Properties.

memsql_1

 

(3) Acesse a página Memória ou Memory.

(4) Edite o campo Maximum server memory ou Memória máxima do servidor.

memsql_2

Pronto. Ao confirmar essa tela, imediatamente você poderá ver no monitor de recursos ou gerenciador de tarefas, que o SQL Server agora ocupará no máximo o número que você determinou. Lembrando que você somente conseguirá editar essa configuração se for o administrador do SQL Server.

Assim, se configuramos um balance com 1 master e 3 slaves, por exemplo, ter um servidor com 8 GB de memória RAM é suficiente, pois deixamos 2 GB para o próprio Windows Server que trabalhará com uma folga + 4 GB para os appservers + 2 GB para o SQL Server.

Mais uma vez quero deixar claro de que não sou um expert em Infra tão menos um DBA. Minhas dicas são com base na minha experiência no mercado atendendo empresas de médio e grande porte como consultor de negócios, analista e programador, onde eventualmente me deparo com problemas no servidor e preciso dar solução imediata. Então, por favor, não considerem as minhas dicas como a última verdade!

Mas elas funcionam 😉

Tela básica com TCBrowse

O intuito deste post é oferecer um exemplo básico de tela em ADVPL contendo um Say, um Get, uma Enchoice e um TCBrowse (com cores) da maneira mais simples possível, sem definir fontes, tamanhos e posicionamentos relativos, etc. A idéia é servir para uma referência rápida. Segue:


Local oVerde    := LoadBitmap(GetResources(),'BR_VERDE')    
Local oAmarelo  := LoadBitmap(GetResources(),'BR_AMARELO') 
Local oVermelho := LoadBitmap(GetResources(),'BR_VERMELHO') 
Local oPreto    := LoadBitmap(GetResources(),'BR_PRETO') 
Local oAzul     := LoadBitmap(GetResources(),'BR_AZUL') 

Private _cIDCons := "ABC123"

oDlg := MSDialog():New(0,0,350,552,"Consulta",,,.F.,,,,,,.T.,,,.T. )
oDlg:bInit := {||EnchoiceBar(oDlg,{||oDlg:End()},{|| oDlg:End()},,)}

oSay := TSay():New(10,05,{||'Código da consulta:'},oDlg,,/*oFont*/,,,,.T.,/*CLR_BLACK*/,/*CLR_WHITE*/,50,10)
oGet := TGet():New(08,55,{||_cIDCons},oDlg,040,009,"@!",,0,,,.F.,,.T.,,.F.,,.F.,.F.,,.T.,.F.,,_cIDCons,,,, )

aBrowse := {{oAzul,"Nome","Antônio Gustavo"},;
            {oVerde,"Endereço","Rua A, 123 casa 4"},;
            {oVermelho,"Telefone","(21) 99583-1283"}}

oBrowse := TCBrowse():New( 25 , 5, 270, 135,,;
                          {'','Consulta','Resultado'},{20,150,50},;

oDlg,,,,,{||},,,,,,,.F.,,.T.,,.F.,,, )

oBrowse:SetArray(aBrowse)

oBrowse:bLine := {||{ aBrowse[oBrowse:nAt,01],;
                      aBrowse[oBrowse:nAt,02],;
                      aBrowse[oBrowse:nAt,03] } }

oDlg:Activate(,,,.T.)

Transferir registros via SQL

Frequentemente precisamos importar dados de uma tabela criando registros em outra tabela (ou na mesma). Acontece que quando temos formatos diferentes, pela PSDU fica impossível. Então é necessário criar uma rotina para isso, ou então utilizar um artifício pelo SQL Management mesmo.

Segue um exemplo de query que cria uma outra query para, por exemplo, copiar as Previsões de Venda (tabela SC4) para o Plano Mestre de Produção (tabela SHC) de uma determinado período. Esse exemplo é apenas para ilustrar, pois esse artifício pode ser usado para qualquer outra coisa e inclusive pode ser aperfeiçoado dependendo da necessidade:

SELECT
 'INSERT INTO SHC010 (HC_FILIAL,HC_PRODUTO,HC_DATA,HC_QUANT,HC_DOC,R_E_C_N_O_) SELECT ''01'','''+
 C4_PRODUTO+''','''+
 C4_DATA+''','+
 STR(C4_QUANT)+','''+
 C4_DOC+''','+
 '(SELECT CASE WHEN MAX(R_E_C_N_O_) IS NULL THEN 1 ELSE MAX(R_E_C_N_O_)+1 END FROM SHC010)' SQLSTR
FROM
 SC4010
WHERE
 D_E_L_E_T_ = ' '
 AND C4_DATA BETWEEN '20141027' AND '20141031'
ORDER BY
 C4_DATA,
 C4_PRODUTO,
 C4_LOCAL

Perceba que para o R_E_C_N_O_ utilizamos uma subquery que recalcula em cada inserção. Qual é o resultado dessa query? Configure no Management Studio no menu Query / Results to / Results to Text e terá o resultado como texto simples:

SQLSTR
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
INSERT INTO SHC010 (HC_FILIAL,HC_PRODUTO,HC_DATA,HC_QUANT,HC_DOC,R_E_C_N_O_) SELECT '01','05010 ','20141027', 100,'271014 ',(SELECT CASE WHEN MAX(R_E_C_N_O_) IS NULL THEN 1 ELSE MAX(R_E_C_N_O_)+1 END FROM SHC010)

...

INSERT INTO SHC010 (HC_FILIAL,HC_PRODUTO,HC_DATA,HC_QUANT,HC_DOC,R_E_C_N_O_) SELECT '01','05011 ','20141027', 420,'271014 ',(SELECT CASE WHEN MAX(R_E_C_N_O_) IS NULL THEN 1 ELSE MAX(R_E_C_N_O_)+1 END FROM SHC010)

...

INSERT INTO SHC010 (HC_FILIAL,HC_PRODUTO,HC_DATA,HC_QUANT,HC_DOC,R_E_C_N_O_) SELECT '01','05040 ','20141027', 130,'271014 ',(SELECT CASE WHEN MAX(R_E_C_N_O_) IS NULL THEN 1 ELSE MAX(R_E_C_N_O_)+1 END FROM SHC010)

Quebra de linha automática no TMSPrinter

Segue uma função que desenvolvi e pode ser útil aos programadores para agilizar impressão de grandes textos (ou apenas descrições de produtos que possam quebrar linha por exemplo).

A função tem ainda duas falhas que por falta de tempo ainda não trabalhei para solucionar.

1) ela não pula duas linhas consecutivas quando no texto memo a pessoa pulou linha de propósito para distanciar parágrafos. isso porque simplesmente pular linha em branco gera apenas Chr(13) ao invés de Chr(13)+Chr(10) mas se eu tratar apenas Chr(13) dá chabú na impressão pois a manipulação de string dá problemas se ficar sobrando Chr(10) na expressão. Mas isso dá pra resolver fácil, só estou sem tempo agora.

2) ela ainda não trata uma palavra maior do que o próprio espaço disponível gerando um loop infinito. isso também não é difícil de resolver.

Enfim, pro uso que estou fazendo agora, nenhum desses dois casos acontecerá por isso não estou priorizando solucionar isso hoje. Mas nos próximos dias sim. De qualquer forma, segue a função para os que estão precisando ou para aqueles que tenham tempo de ajudar no aperfeiçoamento dela.

/********************************************************************
fSayBox - Thiago Coimbra - 15/08/14
*********************************************************************
Parâmetros:
---------------------------------------------------------------------
@oPrn : objeto de impressão
-----------------------------------------------------------------
oFnt : objeto da fonte utilizada
-----------------------------------------------------------------
nY : linha
-----------------------------------------------------------------
nX : coluna
-----------------------------------------------------------------
nH : altura da caixa imaginária (se informar 0 então a altura
será subordinada ao tamanho do texto)
-----------------------------------------------------------------
nW : largura da caixa imaginária
-----------------------------------------------------------------
cTxt : texto completo
*********************************************************************
Retorno:
---------------------------------------------------------------------
Próxima nLin após o texto
-------------------------------------------------------------------*/
*********************************************************************
Static Function fSayBox(oPrn,oFnt,nY,nX,nH,nW,cTxt)
*********************************************************************
Local aTexto
Local aTexto2
Local cTexto := AllTrim(cTxt)
Local nLin := nY
Local nAltLin := oPrn:GetTextHeight("MMMM",oFnt)

aTexto := StrTokArr(cTexto,Chr(13)+Chr(10),.T.)

For nA := 1 to Len(aTexto)

aTexto2 := StrTokArr(aTexto[nA]," ",.T.)

cLinha := ""

For nB := 1 to Len(aTexto2)

If oPrn:GetTextWidth(cLinha+" "+aTexto2[nB],oFnt) > nW .And. !Empty(cLinha)

oPrn:Say( nLin, nX ,AllTrim(cLinha),oFnt,,,,0)
cLinha := ""
nLin += nAltLin

Else

cLinha += " "+aTexto2[nB]

EndIf

Next nB

If !Empty(cLinha)

oPrn:Say( nLin, nX ,AllTrim(cLinha),oFnt,,,,0)
nLin += nAltLin

EndIf

Next nA

Return nLin

Rateio Off-line

É um recurso gerencial que facilita o rateio entre contas, centros de custo, classes de valor, itens conta ou uma combinação entre eles, sem que os usuários precisem utilizar os rateios em cada operação, como por exemplo na entrada de nota fiscal.

A idéia é que o analista contábil faça o rateio ao fim do período e tenha melhores recursos de análise e possibilidade de refazer quantas vezes for necessário.

1) Acesse em Contabilidade Gerencial: Atualizações > Cadastros > Hist. Inteligente e cadastre um histórico inteligente para ser utilizado nos lançamentos de rateio:

2) Acesse Atualizações > Rateios > Rateio Off-line ou busque a rotina CTBA270 e crie um novo rateio. Não testei outras formas, segue um exemplo de uso:

Tipo: Movimento Mês
Perc. Base: 100%
Origem: critério para filtragem automática do rateio (deixar em branco para não considerar um ou mais critérios)
Grade: contrapartidas nos moldes do rateio (percentual)

3) Acesse Miscelânea > Processamentos > Rat. Off-line Comb.:

Data Inicial e Final: período que o sistema irá processar o rateio.

Numero do Lote, Sub-Lote e Documento: usados para os lançamentos de rateio que serão criados, o sistema irá incrementar a numeração automaticamente de acordo com a necessidade.

Cód. Hist. Padrão: código do histórico inteligente criado.

Do rateio/Até o rateio: rateio(s) off-line que será(ão) processados.

Os campos de Conta Contábil, Centro de Custo, Item Conta, Classe Valor normalmente são preenchidos com branco até ZZZZZZZZ porém, quando na configuração de “Origem” do rateio foi deixado um critério em branco, pode-se utilizar esses recursos para uma filtragem mais ampla.

4) Após processar, o sistema realizará movimentos de contrapartida nos lançamentos originais e novos lançamentos rateados nas contas, centros de custo, itens conta, classes de valor, dependendo das regras configuradas. Todos esses lançamentos serão criados em um novo documento, tornando mais fácil a exclusão caso necessário.

Exemplo Completo MSNewGetDados

Segue um exemplo de uma tela modelo 2, ou atualmente chamada de tela múltipla, utilizando o objeto MSNewGetDados, que nos oferece os mais importantes recursos de comportamento e validação em um grid. Vemos nesse exemplo como é criado o cabeçalho (aheader), o vetor de conteúdo (acols) e as principais validações necessárias: validação de campo, validação de linha e validação total.

Estruturei esse fonte de maneira que uma única função seja utilizada nos 3 tipos de validação. Então pode parecer um pouco confuso de início, mas leia o código com atenção e verá que, após entender a lógica, fica mais fácil qualquer manutenção futura.

Este exemplo consiste numa tela que faz (via ExecAuto) movimentos de entrada manual de estoque e, para os casos de produtos com controle de endereço, também realiza o endereçamento agilizando a operação do usuário e para os casos de controle de número de série oferece uma validação extra. O fonte opera com os seguintes campos customizados: B1_XXCSER (campo S ou N criado no cadastro de produto para indicar se o produto controla número de série), D3_XXFORI e DB_XXFORI (campos criados apenas para indicar que os movimentos foram criados pela rotina). Também precisamos criar o parâmetro MV_XXTM001 que deve ser preenchido com o Tipo de Movimento utilizado pela rotina.


#include “Protheus.ch”
#include “TopConn.ch”
#include “RWMake.ch”
#xtranslate bSetGet(<uVar>) => {|u| If(PCount()== 0, <uVar>,<uVar> := u)}

User Function XXX999()

Local aHead1 := {}
Local aSizeAuto := MsAdvSize()

Private aCols1 := {}
Private dEmissao := dDataBase
Private cPicture := PesqPict(“SD3″,”D3_QUANT”)
Private bValProduto := {|| fValid(“PRODUTO”)}
Private bValQuant := {|| fValid(“QUANT”)}
Private bValAlmox := {|| fValid(“ALMOX”)}
Private bValEnderec := {|| fValid(“ENDEREC”)}
Private bValNumSeri := {|| fValid(“NUMSERI”)}
Private bLinOk := {|| fValid(“LINHA”)}
Private bfDeleta := {|| fDeleta()}
Private nPosProduto
Private nPosDesc
Private nPosUnidade
Private nPosQuant
Private nPosAlmox
Private nPosEnderec
Private nPosNumSeri
Private nColDel

aHead1 := fHeader()

nPosProduto := aScan(aHead1, {|x| AllTrim(x[2]) == “PRODUTO”})
nPosDesc := aScan(aHead1, {|x| AllTrim(x[2]) == “DESC”})
nPosUnidade := aScan(aHead1, {|x| AllTrim(x[2]) == “UNIDADE”})
nPosQuant := aScan(aHead1, {|x| AllTrim(x[2]) == “QUANT”})
nPosAlmox := aScan(aHead1, {|x| AllTrim(x[2]) == “ALMOX”})
nPosEnderec := aScan(aHead1, {|x| AllTrim(x[2]) == “ENDEREC”})
nPosNumSeri := aScan(aHead1, {|x| AllTrim(x[2]) == “NUMSERI”})

aCols1 := fCols()

nColDel := Len(aCols1[1])

nOpc := GD_INSERT + GD_UPDATE + GD_DELETE

oDlgSep := MSDialog():New(aSizeAuto[7], 020, aSizeAuto[6]-20, aSizeAuto[5]-40,”Entrada de Produtos no Estoque”,,,.F.,,,,,,.T.,,,.T. )
oSayEmiss := TSay():New( 10,10 ,{||”Emissão:”} ,oDlgSep,,,.F.,.F.,.F.,.T.,CLR_BLACK,CLR_WHITE,024,008)
oGetEmiss := TGet():New( 10,42 ,bSetGet(dEmissao),oDlgSep,044,008,”,,CLR_BLACK,CLR_WHITE,,,,.T.,””,,,.F.,.F.,,.F.,.F.,””,””,,)
nBrwLarg := (oDlgSep:nClientWidth / 2) – 10
nBrwAlt := (oDlgSep:nClientHeight / 2) – 52 //* .20
oBrw1 := MsNewGetDados():New( 32 , 10, nBrwAlt, nBrwLarg,nOpc,’Eval(bLinOk)’,’AllwaysTrue()’,”,{“PRODUTO”,”QUANT”,”ALMOX”,”ENDEREC”,”NUMSERI”},0,99,’AllwaysTrue()’,,’Eval(bfDeleta)’,oDlgSep,aHead1,aCols1)
oBtConf := TButton():New( nBrwAlt + 10 , nBrwLarg – 075,”Confirmar”,oDlgSep,{|| If(fValid(“TODOS”),fGrava(),)},037,012,,,,.T.,,””,,,,.F. )
oBtCanc := TButton():New( nBrwAlt + 10 , nBrwLarg – 035,”Cancelar” ,oDlgSep,{|| RollBackSX8(),oDlgSep:End()} ,037,012,,,,.T.,,””,,,,.F. )

oGetEmiss:Disable()

oDlgSep:Activate(,,,.T.)

Return

 

Static Function fDeleta()

oBrw1:aCols[oBrw1:nAt, nColDel] := !oBrw1:aCols[oBrw1:nAt, nColDel]

oBrw1:Refresh()

Return()

 

Static Function fHeader()

Local aAux := {}

aAdd(aAux,{“Produto” ,”PRODUTO” ,”@!” ,TamSX3(“D3_COD”)[1] ,0 ,”Eval(bValProduto)” ,””,”C”,”SB1″,”” })
aAdd(aAux,{“Descrição” ,”DESC” ,”@!” ,TamSX3(“B1_DESC”)[1] ,0 ,”” ,””,”C”,”” ,”” })
aAdd(aAux,{“UM” ,”UNIDADE” ,”@!” ,TamSX3(“B1_UM”)[1] ,0 ,”” ,””,”C”,”” ,”” })
aAdd(aAux,{“Quant” ,”QUANT” ,cPicture ,TamSX3(“D3_QUANT”)[1] ,TamSX3(“D3_QUANT”)[2] ,”Eval(bValQuant)” ,””,”N”,”” ,”” })
aAdd(aAux,{“Local” ,”ALMOX” ,”@!” ,TamSX3(“D3_LOCAL”)[1] ,0 ,”Eval(bValAlmox)” ,””,”C”,”” ,”” })
aAdd(aAux,{“Endereço” ,”ENDEREC” ,”@!” ,TamSX3(“D3_LOCALIZ”)[1] ,0 ,”Eval(bValEnderec)” ,””,”C”,”SBE”,”” })
aAdd(aAux,{“Num.Série” ,”NUMSERI” ,”@!” ,TamSX3(“D3_NUMSERI”)[1] ,0 ,”Eval(bValNumSeri)” ,””,”C”,”” ,”” })

Return(aAux)

 

Static Function fCols()

Local aAux := {}

aAdd(aAux,{ Space(TamSX3(“D3_COD”)[1]),;
Space(TamSX3(“B1_DESC”)[1]),;
Space(TamSX3(“B1_UM”)[1]),;
0,;
Space(TamSX3(“D3_LOCAL”)[1]),;
Space(TamSX3(“D3_LOCALIZ”)[1]),;
Space(TamSX3(“D3_NUMSERI”)[1]),;
.F.})

Return(aAux)

 

Static Function fValid(cCampo)

Local lRet := .T.
Local nY

// Revalida todas as linhas
If cCampo == “TODOS”

For nY := 1 to Len(oBrw1:aCols)

If !oBrw1:aCols[nY][nColDel]

If !fValCampo(“PRODUTO”,nY,.F.) .Or.;
!fValCampo(“QUANT”,nY,.F.) .Or.;
!fValCampo(“ALMOX”,nY,.F.) .Or.;
!fValCampo(“ENDEREC”,nY,.F.) .Or.;
!fValCampo(“NUMSERI”,nY,.F.)

Aviso(“Atenção!”,”Problema encontrado na linha “+AllTrim(Str(nY))+”.”,{“OK”})
Return .F.

EndIf

EndIf

Next nY

// Valida a linha
ElseIf cCampo == “LINHA”

nY := oBrw1:nAt

If !oBrw1:aCols[nY][nColDel]

If !fValCampo(“PRODUTO”,nY,.F.) .Or.;
!fValCampo(“QUANT”,nY,.F.) .Or.;
!fValCampo(“ALMOX”,nY,.F.) .Or.;
!fValCampo(“ENDEREC”,nY,.F.) .Or.;
!fValCampo(“NUMSERI”,nY,.F.)

Return .F.

EndIf

EndIf

// Valida o campo digitado

Else

lRet := fValCampo(cCampo,oBrw1:nAt,.T.)

EndIf

Return (lRet)

 

Static Function fValCampo(cCampo,nY,lDigitado)

Local cAlias

If cCampo == “PRODUTO”

SB1->(DbSetOrder(1))
If !SB1->(DbSeek(xFilial(“SB1”)+If(lDigitado,M->PRODUTO,oBrw1:aCols[nY][nPosProduto]))) .Or. SB1->B1_MSBLQL == “1”

Aviso(“Atenção!”,”Produto inválido ou bloqueado!”,{“OK”})
Return(.F.)

EndIf

If Empty(If(lDigitado,M->PRODUTO,oBrw1:aCols[nY][nPosProduto]))

Aviso(“Atenção!”,”Preencha o código do produto.”,{“OK”})
Return(.F.)

EndIf

If lDigitado

If M->PRODUTO != oBrw1:aCols[nY][nPosProduto]

oBrw1:aCols[nY][nPosDesc] := SB1->B1_DESC
oBrw1:aCols[nY][nPosUnidade] := SB1->B1_UM
oBrw1:aCols[nY][nPosQuant] := 1
oBrw1:aCols[nY][nPosAlmox] := SB1->B1_LOCPAD
oBrw1:aCols[nY][nPosEnderec] := Space(TamSX3(“D3_LOCALIZ”)[1])
oBrw1:aCols[nY][nPosNumSeri] := Space(TamSX3(“D3_NUMSERI”)[1])

EndIf

EndIf

Else

SB1->(DbSetOrder(1))
SB1->(DbSeek(xFilial(“SB1”)+oBrw1:aCols[nY][nPosProduto]))

EndIf

If cCampo == “QUANT”

If If(lDigitado,M->QUANT,oBrw1:aCols[nY][nPosQuant]) <= 0

Aviso(“Atenção!”,”Preencha a quantidade a ser ajustada.”,{“OK”})
Return(.F.)

EndIf

If SB1->B1_LOCALIZ == “S” .And. SB1->B1_XXCSER == “S” .And. If(lDigitado,M->QUANT,oBrw1:aCols[nY][nPosQuant]) > 1

Aviso(“Atenção!”,”Produto controla número de série. A quantidade não pode ser diferente de 1.”,{“OK”})
Return(.F.)

EndIf

EndIf

If cCampo == “ALMOX”

If Empty(If(lDigitado,M->ALMOX,oBrw1:aCols[nY][nPosAlmox]))

Aviso(“Atenção!”,”Digite um local para a entrada.”,{“OK”})
Return(.F.)

EndIf

SB2->(DbSetOrder(1)) // B2_FILIAL+B2_COD+B2_LOCAL

If !SB2->(DbSeek(xFilial(“SB2”)+oBrw1:aCols[nY][nPosProduto]+If(lDigitado,M->ALMOX,oBrw1:aCols[nY][nPosAlmox])))

Aviso(“Atenção!”,”Este produto não possui controle de saldo cadastrado neste armazém. Corrija o armazém ou cadastre saldo inicial no estoque.”,{“OK”})
Return(.F.)

EndIf

EndIf

If cCampo == “ENDEREC”

SBE->(DbSetOrder(1)) // BE_FILIAL+BE_LOCAL+BE_LOCALIZ

If SB1->B1_LOCALIZ == “S” .And. Empty(If(lDigitado,M->ENDEREC,oBrw1:aCols[nY][nPosEnderec]))

Aviso(“Atenção!”,”É obrigatória a digitação do endereço pois o produto controla endereçamento.”,{“OK”})
Return(.F.)

ElseIf SB1->B1_LOCALIZ != “S” .And. !Empty(If(lDigitado,M->ENDEREC,oBrw1:aCols[nY][nPosEnderec]))

Aviso(“Atenção!”,”Não é permitido a digitação do endereço pois o produto não controla endereçamento.”,{“OK”})
Return(.F.)

ElseIf SB1->B1_LOCALIZ == “S” .And. !Empty(If(lDigitado,M->ENDEREC,oBrw1:aCols[nY][nPosEnderec])) .And. !SBE->(DbSeek(xFilial(“SBE”)+oBrw1:aCols[nY][nPosAlmox]+If(lDigitado,M->ENDEREC,oBrw1:aCols[nY][nPosEnderec])))

Aviso(“Atenção!”,”O endereço digitado não existe. Verifique a digitação e o armazém.”,{“OK”})
Return(.F.)

EndIf

EndIf

If cCampo == “NUMSERI”

If SB1->B1_XXCSER == “S” .And. Empty(If(lDigitado,M->NUMSERI,oBrw1:aCols[nY][nPosNumSeri]))

Aviso(“Atenção!”,”A digitação do número de série é obrigatória pois o produto controla número de série.”,{“OK”})
Return(.F.)

ElseIf SB1->B1_XXCSER != “S” .And. !Empty(If(lDigitado,M->NUMSERI,oBrw1:aCols[nY][nPosNumSeri]))

Aviso(“Atenção!”,”Não é permitida a digitação de número de série pois o produto não controla número de série.”,{“OK”})
Return(.F.)

ElseIf SB1->B1_XXCSER == “S” .And. !Empty(If(lDigitado,M->NUMSERI,oBrw1:aCols[nY][nPosNumSeri]))

// Verifica se já digitou o mesmo produto e número de série no grid
For nX := 1 to Len(oBrw1:aCols)

If nX != nY .And. !oBrw1:aCols[nX][8]

If oBrw1:aCols[nY][nPosProduto]+If(lDigitado,M->NUMSERI,oBrw1:aCols[nY][nPosNumSeri]) == oBrw1:aCols[nX][nPosProduto]+oBrw1:aCols[nX][nPosNumSeri]

Aviso(“Atenção!”,”Este número de série já foi lançado na linha “+AllTrim(Str(nX))+”.”,{“OK”})
Return(.F.)

EndIf

EndIf

Next

// Verificando se já existe saldo
SBF->(DbSetOrder(4)) // BF_FILIAL+BF_PRODUTO+BF_NUMSERI
SBF->(DbSeek(xFilial(“SBF”)+oBrw1:aCols[nY][nPosProduto]+If(lDigitado,M->NUMSERI,oBrw1:aCols[nY][nPosNumSeri])))

While !SBF->(Eof()) .And. SBF->(BF_FILIAL+BF_PRODUTO+BF_NUMSERI) == xFilial(“SBF”)+oBrw1:aCols[nY][nPosProduto]+If(lDigitado,M->NUMSERI,oBrw1:aCols[nY][nPosNumSeri])

If SBF->BF_QUANT > 0

Aviso(“Atenção!”,”A entrada deste número de série não será permitida pois este já se encontra atualmente no estoque da empresa. Armazém “+AllTrim(SBF->BF_LOCAL)+” e endereço “+AllTrim(SBF->BF_LOCALIZ)+”.”,{“OK”})
Return(.F.)

EndIf
SBF->(DbSkip())

End

EndIf

EndIf

Return(.T.)

 

Static Function fGrava()

Processa({||fProcessa1()}, “Aguarde o Processamento …”)

Return

 

 

Static Function fProcessa1()

Local nX, nY
Local cD3Doc
Local cD3TM := SuperGetMv(“MV_XXTM001″,.F.,”001”)
Local aUsuarios := ALLUSERS()
Private lMsErroAuto := .F.

nX := aScan(aUsuarios,{|x| x[1][1] == __cUserID})

If nX > 0

cUsuario := aUsuarios[nX][1][2]

EndIf

BEGIN TRANSACTION

For nY := 1 to Len(oBrw1:aCols)

If oBrw1:aCols[nY][8]

Loop

EndIf

// Movimento Interno

cD3Doc := GetSX8Num(“SD3”, “D3_DOC”)

aCab := { {“D3_DOC” , cD3Doc , NIL},;
{“D3_TM” , cD3TM , NIL},;
{“D3_EMISSAO” , dEmissao , NIL}}

aItem := { {“D3_COD” , oBrw1:aCols[nY][nPosProduto] , NIL},;
{“D3_QUANT” , oBrw1:aCols[nY][nPosQuant] , NIL},;
{“D3_UM” , oBrw1:aCols[nY][nPosUnidade] , NIL},;
{“D3_LOTECTL” , CriaVar(“D3_LOTECTL”,.F.) , NIL},;
{“D3_LOCAL” , oBrw1:aCols[nY][nPosAlmox] , NIL},;
{“D3_LOCALIZ” , CriaVar(“D3_LOCALIZ”,.F.) , NIL},;
{“D3_NUMSERI” , CriaVar(“D3_NUMSERI”,.F.) , NIL},;
{“D3_USUARIO” , cUsuario , NIL},;
{“D3_XXFORI” , “XXX999” , NIL}}

lMsErroAuto := .F.
MSExecAuto({|x,y,z| MATA241(x,y,z)}, aCab, {aItem}, 3)

If lMsErroAuto

MostraErro()
RollBackSX8()
DisarmTransaction()
Return .F.

EndIf

ConfirmSX8()

// Endereçamento

If !Empty(oBrw1:aCols[nY][nPosEnderec])

aItem := {}

aCab:= {;
{“DA_PRODUTO” ,oBrw1:aCols[nY][nPosProduto] ,NIL},;
{“DA_LOCAL” ,oBrw1:aCols[nY][nPosAlmox] ,NIL},;
{“DA_NUMSEQ” ,SD3->D3_NUMSEQ ,NIL};
}

aAdd(aItem,{ {“DB_ITEM” ,”001″ ,NIL},;
{“DB_LOCALIZ” ,oBrw1:aCols[nY][nPosEnderec] ,NIL},;
{“DB_DATA” ,dEmissao ,NIL},;
{“DB_QUANT” ,oBrw1:aCols[nY][nPosQuant] ,NIL},;
{“DB_NUMSERI” ,oBrw1:aCols[nY][nPosNumSeri] ,NIL},;
{“DB_XXFORI” ,”XXX999″ ,NIL}})

lMsErroAuto := .F.
MSExecAuto({|x,y,z| MATA265(x,y,z)},aCab,aItem,3) //Distribui

If lMsErroAuto

MostraErro()
DisarmTransaction()
Return(.F.)

EndIf

EndIf

Next

END TRANSACTION

oDlgSep:End()

Aviso(“Sucesso”,”Entradas de estoque realizadas com sucesso.”,{“OK”})

Return

Configuração de Balance no Protheus

Aviso: os números nesse post são baseados na minha experiência durante os anos de trabalho com consultoria. No entanto, não sou especialista em infra e por isso não leve como uma regra.

Normalmente quando mais de 20 usuários utilizam o sistema já é desejável dividir o serviço do Protheus em 2 ou mais aplicações para que todas operem longe do limite de memória (algo em torno de 1 GB). Quando uma aplicação Protheus ultrapassa esse limite de uso de memória física, não é raro experimentar lentidão no uso do Smartclient e até mesmo travamentos no Appserver.

A configuração do balance, então, se faz necessária. O que é balance? O próprio nome explica, balanço. É uma aplicação comumente chamada de Master, que faz uma divisão dos usuários logados para cada servidor de aplicação (Slaves) existente. Quando o usuário abre seu Smartclient e requisita uma conexão com o Master, este verifica os usuários logados em cada Slave e direciona o usuário para o Slave mais disponível entre todos de acordo com a porcentagem configurada para proporção de cada Slave.

Para saber em qual aplicação se encontra cada usuário, utilize a coluna “Servidor” no Monitor para identificar o endereço e porta do Slave:

monitor

Uma vez entendido o conceito podemos então concluir que é necessário a configuração de um Appserver Master e 2 ou mais Appservers Slaves. Não é nada complexo e compreende apenas numa configuração do arquivo INI de cada servidor.


1) Comece replicando a pasta “appserver” dentro de “bin” criando por exemplo 5 cópias da pasta: appserver_master, appserver_slave1, appserver_slave2, appserver_slave3 e appserver_slave4.

2) Vamos configurar 1 master com 4 slaves proporcionando 25% das conexões para cada um deles.

2.1) Appserver.ini – Master:

Configure neste appserver a porta TCP que deseja que os smartclients se conectem, por exemplo 5010. Depois adicione as seguintes configurações no fim do arquivo (modifique o endereço IP deste exemplo):

[ServerNetwork]
MasterConnection=0
Servers=SLAVE1, SLAVE2, SLAVE3, SLAVE4

[SLAVE1]
Type=TCPIP
Server=192.168.1.1
Port=5011
Connections=25

[SLAVE2]
Type=TCPIP
Server=192.168.1.1
Port=5012
Connections=25

[SLAVE3]
Type=TCPIP
Server=192.168.1.1
Port=5013
Connections=25

[SLAVE4]
Type=TCPIP
Server=192.168.1.1
Port=5014
Connections=25

2.2) Appserver.ini – Slaves:

Configure cada slave em sua respectiva porta TCP de acordo com o configurado em SLAVE1, SLAVE2, SLAVE3 e SLAVE4 no INI do Master.

2.3) Configuração como serviço:

Lembre-se de adicionar a configuração de nome do serviço diferente em todos os appservers configurados para que seja possível distinguí-los na lista de serviços do servidor.

servicos

Exemplo da configuração:

[Service]
Name=TOTVS11_MASTER
DisplayName=TOTVS 11 – Master

[Service]
Name=TOTVS11_SLAVE1
DisplayName=TOTVS 11 – Slave 1

[Service]
Name=TOTVS11_SLAVE2
DisplayName=TOTVS 11 – Slave 2

[Service]
Name=TOTVS11_SLAVE3
DisplayName=TOTVS 11 – Slave 3

[Service]
Name=TOTVS11_SLAVE4
DisplayName=TOTVS 11 – Slave 4

3) Smartclients

Todos os Smarclients devem apontar sempre para o endereço e porta da aplicação master.


Bastando essas configurações para colocar em funcionamento o Balance. Se o seu Protheus está trabalhando com 40 ou mais usuários, todos sentirão uma boa melhora na performance.

Esta configuração de exemplo funciona bem com até 120 usuários simultâneos. Eu utilizo como referência a base de 30 usuários por aplicação slave. Evidentemente isso depende do uso, características do sistema, rotinas utilizadas e hardware disponível para o sistema. E como disse no aviso ao início desse post: não sou perito em infra, portanto esses números são conclusões pessoais minhas no decorrer da minha experiência dentro das empresas.

Ponto de Entrada para NFESEFAZ

Este ponto de entrada foi disponibilizado no ano passado para que seja possível manipular os vetores antes da transmissão da NF-e sem a necessidade de customizar o fonte NFESEFAZ.PRW facilitando o processo de atualização desse RDMake. Segue um exemplo de uso:

/*
———————————————————
PE01NFESEFAZ
———————————————————
Parametros :ParamIXB

ParamIXB[01] aProd
ParamIXB[02] cMensCli
ParamIXB[03] cMensFis
ParamIXB[04] aDest
ParamIXB[05] aNota
ParamIXB[06] aInfoItem
ParamIXB[07] aDupl
ParamIXB[08] aTransp
ParamIXB[09] aEntrega
ParamIXB[10] aRetirada
ParamIXB[11] aVeiculo
ParamIXB[12] aReboque
———————————————————
Partida : NFESEFAZ (P.E.)
———————————————————

*/

****************************
User Function PE01NFESEFAZ()
****************************

aRet := ParamIXB

cTipo := If(aRet[5, 4] = “1”, “S”, “E”) //Tipo de Nota: 1 – Saída, 2 – Entrada
cDoc := aRet[5, 2] //Número da Nota
cSerie := aRet[5, 1] //Série da Nota

If cTipo = “S”

For I := 1 To Len(aRet[01])

// Adiciona os números dos lotes na mensagem complementar da NF-e.

If !Empty(aRet[01, I, 19])

aRet[02] += ” – ITEM “+AllTrim(Str(aRet[01, I, 1]))+”: “+AllTrim(aRet[01, I, 2])+” LOTE “+AllTrim(aRet[01, I, 19])

EndIf

Next

EndIf

Return(aRet)

Validação Documento de Entrada

/*
=============================================================================
Rotina : MT100TOK
—————————————————————————–
Autor : Thiago Coimbra
—————————————————————————–
Descricao : Ponto de entrada para validação de uma nota fiscal de entrada.
Executado no momento em que clica OK.
—————————————————————————–
Partida : MATA103 (Documento de Entrada)
Funcão “Retornar” em MATA410 (Pedidos de Venda)
=============================================================================
*/
#include “Protheus.ch”
#include “Rwmake.ch”
#include “TopConn.ch”

//—————————————————————————
User Function MT100TOK()
//—————————————————————————

Local nPosTES := aScan(aHeader,{|x| AllTrim(x[2]) == “D1_TES” })
Local aArea := GetArea()
Local nX
Local lOK := .T.

For nX := 1 To Len(aCols)

// Nao processa para os itens deletados

If !aCols[nX][Len(aHeader)+1]

//aCols[nX][nPosNOri]

Endif

Next nX

//CTIPO, DDEMISSAO, etc.

RestArea(aArea)

Return(lOK)