{"Root":{"etl":[{"etlstamp":"cb4792bbc9f1f138802059   ","resumo":"Redução de 50% do IRS de novembro de 2026","ecran":"prform","texto":"Redução IRS nov/2026 (50%)","ctooltip":"Despacho n.º 12523-A/2026: reduz a 50% a retenção de IRS dos recibos de novembro de 2026","expressao":"'==============================================================================================================================================================\n' OPÇÃO DE ECRÃ (etl, ecrã = 'prform'): redução de 50% da retenção de IRS nos recibos de novembro de 2026 conforme Despacho n.º 12523-A/2026\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' O que faz:\n'   1. Lê as linhas de IRS dos recibos de novembro de 2026 que cumprem as condições do despacho (ver getEligibleLines das 2. Leitura das linhas de IRS\n'      elegíveis).\n'   2. Mostra-as numa grelha para o utilizador confirmar o que quer processar.\n'   3. Para cada linha de recibo assinalado: reduz o IRS a metade, recalcula o recibo, acerta o IRS acumulado e grava tudo numa só transação.\n'   4. Apresenta no fim uma mensagem para cada linha com o resultado.\n'\n' Zonas: só funcionários das zonas definidas abaixo em Declaração de variáveis na variável do tipo string \"zones\".\n'\n' Limite de rendimento (cláusula 3 do despacho): o valor sujeito de cada linha não pode exceder o limite superior do penúltimo escalão da tabela aplicável:\n'   - Com deficiência (tabelas IV a VII) .......................... 20 468,00\n'   - Sem deficiência, casado e único titular (tabela III) ........ 20 265,00\n'   - Restantes casos (tabelas I e II) ............................ 20 221,00\n'\n' Fluxo técnico:\n'   1. O botão abre um BrowList com uma linha por cada linha de IRS (prde com cm6.codirs = 1).\n'   2. Ao confirmar, cada recibo é gravado na sua própria PhcTransaction: linhas do prde, depois os totais do recibo, depois os acumulados de IRS\n'      dos recibos seguintes.\n'   3. Os callbacks do BrowList correm em pedidos posteriores (o contexto fica na sessão): não podem usar o mPage.\n'==============================================================================================================================================================\n\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' Declaração de variáveis\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' Zonas fiscais abrangidas (pe.czonag): C = Continente, A = Açores, \n' M = Madeira. Para incluir a Madeira: \"'C','A','M'\"\nDim zones As String = \"'C','A','M'\"\n\n' O limite do n.º 3 (limite superior do penúltimo escalão) vem na consulta, por\n' funcionário (coluna \"lim\"):\n' com deficiência 20468; casado e único titular 20265; restantes casos 20221.\n' Compara-se com o valor sujeito da linha.\nDim reductionFactor As Decimal = 0.5D     ' n.º 2: 50% do valor das tabelas\nDim marker As String = \"Desp.12523-A/26 \" ' escrito em prde.obs (C(50)); marca também a linha como já tratada\nDim periodStart As New DateTime(2026, 11, 1)\nDim periodEnd As New DateTime(2026, 12, 1)  ' exclusivo\nDim ptCulture As New System.Globalization.CultureInfo(\"pt-PT\")\n\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' 1. Estado guardado para os callbacks (só valores simples, sem referências à página)\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\nDim shownEde As New System.Collections.Generic.Dictionary(Of String, Decimal)   ' prdestamp -> IRS apresentado na lista\nDim receiptLabels As New System.Collections.Generic.Dictionary(Of String, String) ' prdestamp -> \"Recibo n.º X de Y\"\nDim openReceiptStamp As String = If(mStamp, \"\").Trim()\n\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' 2. Leitura das linhas de IRS elegíveis\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' Linhas de IRS elegíveis para a redução (uma por linha de prde com cm6.codirs = 1).\n'  - lim: limite do despacho para o funcionário.\n'  - suj: valor sujeito a IRS da linha, a partir da prre. Cada linha de IRS (prde) tem um tipo (x.tp): 0 normal, 1 natal,\n'    2 férias, 3 férias+natal. A soma da prre desse tipo (s0, s1, s2) reparte-se pelas linhas do mesmo tipo em proporção ao IRS.\nDim getEligibleLines = Function() As System.Collections.Generic.List(Of DataRow)\n    ' Filtros da consulta\n    Dim filters As New PhcTableFilter\n    filters.AddFilter(\"pr.data >= @periodStart AND pr.data < @periodEnd AND d.dtref >= @periodStart AND d.dtref < @periodEnd\")\n    filters.AddFilter(\"pr.moeda IN ('', 'EURO') AND d.ede <> 0\")\n    filters.AddFilter(\"pe.czonag IN (\" & zones & \")\")\n    filters.AddFilter(\"pr.txirs = 0 AND pr.resestr = 0 AND ISNULL(pe.txirs, 0) = 0 AND ISNULL(pe2.organizacao, 0) = 0\")\n    filters.AddFilter(\"pr.pago = 0 AND pr.plano = 0 AND pr.olcodigo = '' AND pr.evalorjaemtb = 0\")\n    filters.AddFilter(\"d.ajirs = 0 AND d.txextirs = 0 AND d.txextmirs = 0 AND d.ajtxextmirs = 0\")\n    filters.AddFilter(\"NOT EXISTS (SELECT 1 FROM prde p WHERE p.prstamp = pr.prstamp AND p.penstamp <> '')\")\n    filters.AddFilter(\"CHARINDEX('12523-A', ISNULL(d.obs, '')) = 0\") ' ainda não tratada\n \n    ' Consulta e filtro final pelo limite do despacho (suj <= lim)\n    Return PhcCommandBuilder.CreateSelect(\"pr.prstamp, pr.recibo, pr.no, pr.nome, pr.data AS prdata, d.prdestamp, d.ede, d.de, d.dqtt, RTRIM(d.obs) AS obs, \" &\n                                          \"CASE WHEN ISNULL(pe.deficiente, 0) = 1 THEN 20468 \" &\n                                          \"WHEN ISNULL(pe2.casado, 0) = 1 AND ISNULL(pe.ntit, 0) = 1 THEN 20265 ELSE 20221 END AS lim, \" &\n                                          \"ISNULL(ROUND(CASE x.tp WHEN 0 THEN s.s0 WHEN 1 THEN s.s1 WHEN 2 THEN s.s2 ELSE s.s1 + s.s2 END * d.ede / NULLIF(t.tede, 0), 2), 0) AS suj\").\n        From(\"pr\").\n        InnerJoin(\"prde\", \"d\").On(\"d.prstamp = pr.prstamp\").\n        InnerJoin(\"cm6\", \"c\").On(\"c.cm = d.cd AND c.codirs = 1\").\n        AppendText(\" CROSS APPLY (SELECT CASE WHEN d.nat = 1 AND d.fer = 1 THEN 3 WHEN d.nat = 1 THEN 1 WHEN d.fer = 1 THEN 2 ELSE 0 END AS tp) x \" &\n                   \"LEFT JOIN (SELECT r.prstamp, \" &\n                   \"SUM(CASE WHEN m.natal = 0 AND m.ferias = 0 THEN r.ereirs ELSE 0 END) AS s0, \" &\n                   \"SUM(CASE WHEN m.natal = 1 THEN r.ereirs ELSE 0 END) AS s1, \" &\n                   \"SUM(CASE WHEN m.natal = 0 AND m.ferias = 1 THEN r.ereirs ELSE 0 END) AS s2 \" &\n                   \"FROM prre r INNER JOIN cm6 m ON m.cm = r.cr GROUP BY r.prstamp) s ON s.prstamp = pr.prstamp \" &\n                   \"LEFT JOIN (SELECT p.prstamp, y.tp, SUM(p.ede) AS tede FROM prde p INNER JOIN cm6 k ON k.cm = p.cd AND k.codirs = 1 \" &\n                   \"CROSS APPLY (SELECT CASE WHEN p.nat = 1 AND p.fer = 1 THEN 3 WHEN p.nat = 1 THEN 1 WHEN p.fer = 1 THEN 2 ELSE 0 END AS tp) y \" &\n                   \"GROUP BY p.prstamp, y.tp) t ON t.prstamp = pr.prstamp AND t.tp = x.tp \").\n        LeftJoin(\"pe\").On(\"pe.no = pr.no\").\n        LeftJoin(\"pe2\").On(\"pe2.pe2stamp = pe.pestamp\").\n        Where(filters).\n        AddSqlParameter(\"@periodStart\", periodStart).\n        AddSqlParameter(\"@periodEnd\", periodEnd).\n        OrderBy(\"pr.recibo, pr.nome, d.prdestamp\").\n        GetDatatable().Rows.Cast(Of DataRow)().\n        Where(Function(r) r.ItemToDecimal(\"suj\") <= r.ItemToDecimal(\"lim\")).ToList()\nEnd Function\n \n' Texto de identificação do recibo, usado nas mensagens\nDim getLabel = Function(r As DataRow) As String\n    Return \"Recibo n.º \" & r.ItemToDecimal(\"recibo\").ToString(\"0\") & \" de \" & r.ItemToString(\"nome\").Trim()\nEnd Function\n \n' IRS novo: 50% do valor atual, truncado à unidade\nDim reduce = Function(ede As Decimal) As Decimal\n    Return Math.Truncate(ede * reductionFactor)\nEnd Function\n\n' Taxa efetiva nova: IRS novo / valor sujeito. Sem valor sujeito, é metade da taxa da linha (dqtt), como na tecla do desktop.\nDim rateOf = Function(ede As Decimal, taxable As Decimal, currentRate As Decimal) As Decimal\n    Return Math.Min(If(taxable > 0D, Math.Round(ede / taxable * 100D, 2), Math.Round(currentRate / 2D, 2)), 99.99D)\nEnd Function\n\n' Grava um recibo (todas as suas linhas de IRS selecionadas) numa só transação; devolve o IRS retirado, em euros.\n' Qualquer verificação falhada lança uma exceção e a transação é revertida: o recibo fica totalmente alterado ou intacto.\nDim reducePayslip = Function(payslipStamp As String, lines As System.Collections.Generic.List(Of DataRow)) As Decimal\n    Dim first As DataRow = lines.First()\n    Dim diffEur As Decimal = 0D\n    Dim diffEsc As Decimal = 0D\n    For Each r In lines\n        If Not shownEde.ContainsKey(r.ItemToString(\"prdestamp\").Trim()) OrElse shownEde(r.ItemToString(\"prdestamp\").Trim()) <> r.ItemToDecimal(\"ede\") Then\n            Throw New Exception(\"a linha foi alterada desde que a lista foi apresentada\")\n        End If\n    Next\n    Using tran As New PhcTransaction(\"irs26\")\n        Try\n            For Each r In lines\n                Dim ede As Decimal = r.ItemToDecimal(\"ede\")\n                Dim newEde As Decimal = reduce(ede)\n                Dim newDe As Decimal = Math.Round(newEde.EuroToEsc(), 5)\n                diffEur += ede - newEde\n                diffEsc += r.ItemToDecimal(\"de\") - newDe\n                ' ede = @ede protege contra uma alteração concorrente da linha\n                If PhcCommandBuilder.CreateUpdate(\"prde\").\n                        AddUpdateColValue(\"ede\", newEde).\n                        AddUpdateColValue(\"de\", newDe).\n                        AddUpdateColValue(\"dqtt\", rateOf(newEde, r.ItemToDecimal(\"suj\"), r.ItemToDecimal(\"dqtt\"))).\n                        AddUpdateColString(\"obs\", marker & r.ItemToString(\"obs\").Trim(), 50).\n                        Where(\"prdestamp = @prdestamp AND ede = @ede\").\n                        AddSqlStampParameter(\"@prdestamp\", r.ItemToString(\"prdestamp\").Trim()).\n                        AddSqlParameter(\"@ede\", ede).\n                        ExecuteNonQuery(tran, True) <> 1 Then\n                    Throw New Exception(\"a linha foi alterada entretanto\")\n                End If\n            Next\n            ' totais do recibo (as mesmas verificações da consulta de elegíveis)\n            If PhcCommandBuilder.CreateUpdate(\"pr\").\n                    AddUpdateColExpression(\"ettdesc = ettdesc - @de\").\n                    AddUpdateColExpression(\"ttdesc = ttdesc - @dd\").\n                    AddUpdateColExpression(\"eliquido = eliquido + @de\").\n                    AddUpdateColExpression(\"liquido = liquido + @dd\").\n                    AddUpdateColExpression(\"evalorol = evalorol + @de\").\n                    AddUpdateColExpression(\"valorol = valorol + @dd\").\n                    Where(\"prstamp = @prstamp AND pago = 0 AND plano = 0 AND olcodigo = '' AND evalorjaemtb = 0\").\n                    AddSqlParameter(\"@de\", diffEur).\n                    AddSqlParameter(\"@dd\", diffEsc).\n                    AddSqlStampParameter(\"@prstamp\", payslipStamp).\n                    ExecuteNonQuery(tran, True) <> 1 Then\n                Throw New Exception(\"o recibo foi alterado, pago ou contabilizado entretanto\")\n            End If\n            ' acumulados de IRS deste recibo e dos seguintes do mesmo funcionário\n            PhcCommandBuilder.CreateUpdate(\"pr\").\n                AddUpdateColExpression(\"eirsacum = eirsacum - @de\").\n                AddUpdateColExpression(\"irsacum = irsacum - @dd\").\n                Where(\"no = @no AND ano = 2026 AND (prstamp = @prstamp OR data > @data OR (data = @data AND recibo > @recibo))\").\n                AddSqlParameter(\"@de\", diffEur).\n                AddSqlParameter(\"@dd\", diffEsc).\n                AddSqlParameter(\"@no\", first.ItemToDecimal(\"no\")).\n                AddSqlStampParameter(\"@prstamp\", payslipStamp).\n                AddSqlParameter(\"@data\", first.ItemToDate(\"prdata\")).\n                AddSqlParameter(\"@recibo\", first.ItemToDecimal(\"recibo\")).\n                ExecuteNonQuery(tran, True)\n            tran.Commit()\n        Catch\n            tran.Rollback()\n            Throw\n        End Try\n    End Using\n    Return diffEur\nEnd Function\n\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\n' 3. Grelha de confirmação\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------\nDim browList As New BrowList(BootstrapModal.ModalSizeEnum.Xl)\nbrowList.Type = BrowListTypeEnum.Grid\nbrowList.ID = \"Irs26Browlist\"\nbrowList.Title = \"Redução de 50% do IRS de novembro de 2026: assinale as linhas a processar\"\nbrowList.AddMainformModel = False ' a lista não depende do recibo aberto\nbrowList.Grid.FieldId = \"prdestamp\"\nbrowList.Grid.ShowColumnToSelect = True\nbrowList.Grid.MultiSelectRows = True\nbrowList.Grid.AddGridNumberColumn(\"recibo\", \"Recibo\", 0)\nbrowList.Grid.AddGridColumn(\"nome\", \"Funcionário\")\nbrowList.Grid.AddGridColumn(\"obs\", \"Observação\")\nbrowList.Grid.AddGridNumberColumn(\"suj\", \"Valor Sujeito a IRS\", 2, True, True) ' mostra também o zero\nbrowList.Grid.AddGridNumberColumn(\"tx_atu\", \"Taxa Efetiva Atual\", 2)\nbrowList.Grid.AddGridNumberColumn(\"ede\", \"Valor IRS Atual\", 2)\nbrowList.Grid.AddGridNumberColumn(\"tx_nova\", \"Taxa Efetiva Nova\", 2)\nbrowList.Grid.AddGridNumberColumn(\"ede_nova\", \"Valor IRS Novo\", 2)\nbrowList.GetModelFunction = Function(args)\n    Dim table As New DataTable(\"irs26\")\n    table.Columns.Add(\"prdestamp\", GetType(String))\n    table.Columns.Add(\"recibo\", GetType(Decimal))\n    table.Columns.Add(\"nome\", GetType(String))\n    table.Columns.Add(\"obs\", GetType(String))\n    table.Columns.Add(\"suj\", GetType(Decimal))\n    table.Columns.Add(\"tx_atu\", GetType(Decimal))\n    table.Columns.Add(\"ede\", GetType(Decimal))\n    table.Columns.Add(\"tx_nova\", GetType(Decimal))\n    table.Columns.Add(\"ede_nova\", GetType(Decimal))\n    shownEde.Clear()\n    receiptLabels.Clear()\n    For Each r In getEligibleLines()\n        Dim id As String = r.ItemToString(\"prdestamp\").Trim()\n        Dim ede As Decimal = r.ItemToDecimal(\"ede\")\n        Dim suj As Decimal = r.ItemToDecimal(\"suj\")\n        Dim newEde As Decimal = reduce(ede)\n        shownEde(id) = ede\n        receiptLabels(id) = getLabel(r)\n        table.Rows.Add(id, r.ItemToDecimal(\"recibo\"), r.ItemToString(\"nome\").Trim(),\n                       If(r.ItemToString(\"obs\").Trim() = \"\", \"Remuneração Mensal\", r.ItemToString(\"obs\").Trim()),\n                       suj, If(suj > 0D, Math.Round(ede / suj * 100D, 2), r.ItemToDecimal(\"dqtt\")), ede, rateOf(newEde, suj, r.ItemToDecimal(\"dqtt\")), newEde)\n    Next\n    Dim model As New PhcDynamicObject\n    model.SetValue(\"data\", table)\n    Return model\nEnd Function\n\nbrowList.ListContext.SetSelectRowsFunction = Function(args)\n    Dim selectedIds As New System.Collections.Generic.HashSet(Of String)(args.Ids.Select(Function(x) Convert.ToString(x).Trim()))\n    If selectedIds.Count = 0 Then\n        Return JsonMessage.GetNewJsonError.AddMessage(\"Não assinalou nenhuma linha, pelo que não foi alterado nada.\")\n    End If\n    ' voltar a ler as linhas agora: o que foi listado pode ter sido pago, contabilizado ou tratado entretanto\n    Dim lines = getEligibleLines().Where(Function(r) selectedIds.Contains(r.ItemToString(\"prdestamp\").Trim())).ToList()\n    Dim stillEligible As New System.Collections.Generic.HashSet(Of String)(lines.Select(Function(r) r.ItemToString(\"prdestamp\").Trim()))\n    Dim result As JsonMessage = JsonMessage.GetNewJsonSuccess\n    Dim changedStamps As New System.Collections.Generic.HashSet(Of String)\n    Dim changedCount As Integer = 0\n    Dim failedCount As Integer = 0\n    Dim totalReduced As Decimal = 0D\n    ' linhas selecionadas que já não são elegíveis\n    For Each label In selectedIds.Where(Function(id) Not stillEligible.Contains(id)).\n            Select(Function(id) If(receiptLabels.ContainsKey(id), receiptLabels(id), id)).Distinct().ToList()\n        failedCount += 1\n        result.AddWarningAlert(label & \": não foi alterado (a linha já não está elegível: recibo pago, contabilizado, alterado ou já tratado).\")\n    Next\n    For Each payslip In lines.GroupBy(Function(r) r.ItemToString(\"prstamp\").Trim()).ToList()\n        Dim label As String = getLabel(payslip.First())\n        Try\n            Dim reduced As Decimal = reducePayslip(payslip.Key, payslip.ToList())\n            changedCount += 1\n            changedStamps.Add(payslip.Key)\n            totalReduced += reduced\n            result.AddMessage(label & \": IRS reduzido em \" & reduced.ToString(\"N2\", ptCulture) & \".\")\n        Catch ex As Exception\n            failedCount += 1\n            result.AddWarningAlert(label & \": não foi alterado (\" & ex.Message & \").\")\n        End Try\n    Next\n    result.AddMessage(\"Concluído. Recibos alterados: \" & changedCount.ToString() & \". Recibos não alterados: \" & failedCount.ToString() & \". IRS reduzido: \" & totalReduced.ToString(\"N2\", ptCulture) & \".\")\n    PhcCache.ResetTable(\"PR\")\n    PhcCache.ResetTable(\"PRDE\")\n    ' recarregar o ecrã apaga os avisos: só se faz quando o recibo aberto foi alterado\n    If changedStamps.Contains(openReceiptStamp) Then\n        Return result.SetToSuccess.SetReloadScreen\n    End If\n    Return result.SetToSuccess\nEnd Function\n\nbrowList.AddFormToPageAndShowBrowlist(CType(mPage, Normalform))\nReturn Nothing\n'--------------------------------------------------------------------------------------------------------------------------------------------------------------","inactivo":false,"ordem":0.0,"local":2.0,"cacesso":false,"actab":"          ","actip":"                                        ","iedicao":false,"iconsulta":false,"ieof":false,"dgest":false,"dpess":false,"dcont":false,"dintr":false,"dsupo":false,"dcrm":false,"dteam":false,"lc1":true,"nc1":"Intranet                                ","lc2":false,"nc2":"                                        ","lc3":false,"nc3":"                                        ","lc4":false,"nc4":"                                        ","lc5":false,"nc5":"                                        ","lc6":false,"nc6":"                                        ","lc7":false,"nc7":"                                        ","lc8":false,"nc8":"                                        ","lc9":false,"nc9":"                                        ","emostamp":"                         ","emotit":"","emodis":1.0,"emoistamp":"                         ","emoires":"","emoidis":1.0,"ccss":"","posmainform":1.0,"etlid":1642.0,"ndoc":0.0,"doctypemulti":"","ousrinis":"João Ferreira","ousrdata":"2026-10-27T00:00:00","ousrhora":"12:31:40","usrinis":"João Ferreira","usrdata":"2026-10-30T00:00:00","usrhora":"15:34:48","marcada":false}]}}