diff --git a/.run/dev.run.xml b/.run/dev.run.xml index 46d8ec8..1a16db6 100644 --- a/.run/dev.run.xml +++ b/.run/dev.run.xml @@ -7,6 +7,9 @@ + + + \ No newline at end of file diff --git a/README.md b/README.md index 5587d28..c2540e2 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,94 @@ # [Preprocesador de reportes de conciliación de Mercado Pago](https://premp.binderplus.com.ar/) -Este proyecto transforma el reporte de _todas las operaciones_ de Mercado Pago a uno más similar a los +Este proyecto transforma el reporte de liquidaciones de Mercado Pago a uno más similar a los extractos bancarios tradicionales, con el propósito de permitir la importación en programas de conciliación no diseñados -específicamente para MP. +específicamente para MP (como, por ejemplo, Odoo). Actualmente está optimizado para el caso de uso de empresas que reciben y envían transferencias. Puede no tener toda la funcionalidad necesaria para empresas que venden por Mercado Libre. -Específicamente, toma las columnas `TAXES_DISAGGREGATED` y `FEE_AMOUNT` y las coloca en filas nuevas. +Específicamente, elimina las filas de saldo inicial, total de movimientos, reserve for payout +(y cualquier otra sin `EXTERNAL_REFERENCE`), cambia el formato de la columna `DATE` para eliminar el huso horario y +las fracciones de segundo, traduce algunos términos de la columna `DESCRIPTION`, +y toma las columnas `TAXES_DISAGGREGATED` y `MP_FEE_AMOUNT` y las coloca en filas nuevas. Por ejemplo, transforma: -| SOURCE_ID | TRANSACTION_TYPE | TRANSACTION_AMOUNT | FEE_AMOUNT | SETTLEMENT_NET_AMOUNT | TAXES_DISAGGREGATED | -|-------------|------------------|--------------------|------------|-----------------------|-----------------------------------------------------------------------------------------------------| -| 95932048395 | SETTLEMENT | 100000 | -1000 | 9600 | [{financial_entity":"debitos_creditos", "amount": "-3000", "detail": "tax_withholding_collector"}]" | +| DATE | SOURCE_ID | EXTERNAL_REFERENCE | RECORD_TYPE | DESCRIPTION | GROSS_AMOUNT | MP_FEE_AMOUNT | TAXES_DISAGGREGATED | BALANCE | +|-------------------------------|-------------|------------------------|---------------------------|--------------------|--------------|---------------|-----------------------------------------------------------------------------------------------------|---------| +| 2024-10-01T00:00:00.000-03:00 | | | initial_available_balance | | | | | 0 | +| 2024-10-01T00:00:00.000-03:00 | 95932048395 | 3670377883134 | release | payment | 100000 | -1000 | [{financial_entity":"debitos_creditos", "amount": "-3000", "detail": "tax_withholding_collector"}]" | 96000 | +| 2024-10-01T13:00:00.000-03:00 | 89344433513 | | release | reserve_for_payout | -96000 | | [] | 0 | +| 2024-10-01T13:00:01.000-03:00 | 89344433513 | | release | reserve_for_payout | 96000 | | [] | 96000 | +| 2024-10-01T13:00:01.000-03:00 | 89344433513 | LEORZG90K0R84X0P9EGJ46 | release | payout | -96000 | | [] | 0 | +| | | | total | | 4000 | -1000 | | 0 | En: -| SOURCE_ID | TRANSACTION_TYPE | TRANSACTION_AMOUNT | FEE_AMOUNT | SETTLEMENT_NET_AMOUNT | TAXES_DISAGGREGATED | -|------------------|-----------------------------------------------------|--------------------|------------|-----------------------|-----------------------------------------------------------------------------------------------------| -| 95932048395 | SETTLEMENT | 100000 | -1000 | 9600 | [{financial_entity":"debitos_creditos", "amount": "-3000", "detail": "tax_withholding_collector"}]" | -| 95932048395_tax0 | Impuesto debitos_creditos tax_withholding_collector | -3000 | -1000 | 9600 | [{financial_entity":"debitos_creditos", "amount": "-3000", "detail": "tax_withholding_collector"}]" | -| 95932048395_fee | Comisión + IVA | -1000 | -1000 | 9600 | [{financial_entity":"debitos_creditos", "amount": "-3000", "detail": "tax_withholding_collector"}]" | +| DATE | SOURCE_ID | EXTERNAL_REFERENCE | RECORD_TYPE | DESCRIPTION | GROSS_AMOUNT | BALANCE | +|---------------------|------------------|------------------------|---------------------------|-------------------------------------------------------|--------------|---------| +| 2024-10-01 00:00:00 | 95932048395 | 3670377883134 | release | Transferencia recibida | 100000 | 100000 | +| 2024-10-01 00:00:00 | 95932048395_tax0 | 3670377883134 | release | Impuesto debitos creditos (tax withholding collector) | -3000 | 97000 | +| 2024-10-01 00:00:00 | 95932048395_fee | 3670377883134 | release | Comisión + IVA | -1000 | 96000 | +| 2024-10-01 13:00:01 | 89344433513 | LEORZG90K0R84X0P9EGJ46 | release | Transferencia enviada | -96000 | 0 | -Para evitar problemas, se recomienda generar el reporte con todas las columnas y en inglés. \ No newline at end of file +Para evitar problemas, se recomienda generar el reporte con todas las columnas y en inglés. + +## Desarrollo y extensión +Cualquier extensión a este proyecto debe hacerse en un fork. No se aceptan PRs o Issues. + +Recomendamos hacer deploy a GitHub Pages con la acción incluida (en `.github/workflows`) y apuntar un dominio +o subdominio a la misma. Si no se desea hacer esto último +(es decir, se prefiere acceder desde usuario.github.io/repo/preprocesador_mp), hay que modificar `vite.config.js` +siguiendo [esta documentacion](https://es.vitejs.dev/guide/static-deploy#github-pages). + +Para agregar funcionalidad, se recomienda agregar funciones que modifiquen elementos de una fila +a la lista `updateRules` y funciones que transformen columnas en nuevas filas al diccionario `transposeRules`. +Ambos están definidos en `PreprocessorWorker.js`. + +Por ejemplo, para consolidar las columnas `DESCRIPTION`, `PAYER_NAME`, `PAYER_ID_TYPE` y `PAYER_ID_NUMBER`, +se debería agregar el siguiente elemento a `updateRules`: + +```js +// Reglas que modifican filas. +const updateRules = [ + [...], + consolidateColumns +] + +function consolidateColumns(row) { + row['DESCRIPTION'] = `${row['DESCRIPTION']} ${row['PAYER_NAME']} ${row['PAYER_ID_TYPE']} ${row['PAYER_ID_NUMBER']}` + delete row['PAYER_NAME'] + delete row['PAYER_ID_TYPE'] + delete row['PAYER_ID_NUMBER'] +} +``` + +Y para agregar una regla que transforme la columna `FINANCING_FEE_AMOUNT` en filas, habría que agregar +el siguiente elemento a `transposeRules`: + +```js +// Reglas que transforman columnas en filas. El key es el nombre de la columna a transponer. +// originalRow: Fila antes de aplicarle updateRules +// updatedRow: Fila después de aplicarle updateRules +// Ambas son necesarias porque updateRules puede eliminar columnas necesarias para las nuevas filas. +const transposeRules = { + [...], + + FINANCING_FEE_AMOUNT: (originalRow, updatedRow) => { + let rows = [] + const fee = parseFloat(originalRow['FINANCING_FEE_AMOUNT']) + + if (fee !== 0) { + let newRow = structuredClone(updatedRow) + newRow['SOURCE_ID'] += "_financing_fee" + newRow['GROSS_AMOUNT'] = fee + newRow['DESCRIPTION'] = "Comisión por ofrecer cuotas sin interés" + + rows.push(newRow) + } + + return rows + }, +} +``` diff --git a/src/App.css b/src/App.css index 2caa6fd..650cd82 100644 --- a/src/App.css +++ b/src/App.css @@ -6,5 +6,14 @@ } .read-the-docs { + margin: 0 auto; color: #888; + text-align: justify; + text-align-last: justify; + max-width: 700px; } + +.title { + max-width: 720px; + margin: 15px auto; +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 16a3901..f992cb0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,16 +3,22 @@ import './App.css' import {FileHandler} from "./FileHandler.jsx"; function App() { - const [count, setCount] = useState(0) return ( <> -

- Preprocesador de reportes de
- conciliación de MercadoPago +

+ Preprocesador de reportes de conciliación de MercadoPago

- +

+ Cargue el reporte de liquidaciones para obtener un reporte con un formato similar al de un extracto + bancario tradicional. + + Para un correcto funcionamiento, configure el reporte para que incluya todas las columnas y que los + nombres de las columnas estén en inglés. Más información. +

+ + ) diff --git a/src/FileHandler.jsx b/src/FileHandler.jsx index fb4d33d..bf967c1 100644 --- a/src/FileHandler.jsx +++ b/src/FileHandler.jsx @@ -39,8 +39,8 @@ export const FileHandler = () => { if (state === "initial") { Icon = FaFileArrowUp - message = "Arrastre el reporte de todas las operaciones para obtener un reporte con un formato similar al de un extracto bancario tradicional." - containerColor = "neutral" + message = "Arrastre un archivo o haga click para cargar." + containerColor = styles.neutral } if (state === "error") { diff --git a/src/FileHandler.module.css b/src/FileHandler.module.css index fe4dd87..378a7c2 100644 --- a/src/FileHandler.module.css +++ b/src/FileHandler.module.css @@ -7,7 +7,8 @@ border: 3px dashed; border-radius: 10px; transition: .25s ease-in-out; - padding: 20px + padding: 20px; + margin: 30px 0; } .neutral { @@ -28,10 +29,6 @@ color: #663733; } -p { - max-width: 500px -} - svg { width: 200px; height: 100px; diff --git a/src/Preprocessor.js b/src/Preprocessor.js index 1a64611..fccee24 100644 --- a/src/Preprocessor.js +++ b/src/Preprocessor.js @@ -2,7 +2,6 @@ export function process(file) { return new Promise((resolve, reject) => { const worker = new Worker(new URL('./PreprocessorWorker.js', import.meta.url), {type: 'module'}); - console.log(file) worker.postMessage(file) worker.onmessage = event => { diff --git a/src/PreprocessorWorker.js b/src/PreprocessorWorker.js index c7002c9..93a32a4 100644 --- a/src/PreprocessorWorker.js +++ b/src/PreprocessorWorker.js @@ -1,10 +1,91 @@ import {read, write, utils} from "xlsx"; +// Reglas que transforman columnas en filas. El key es el nombre de la columna a transponer. +// originalRow: Fila antes de aplicarle updateRules +// updatedRow: Fila después de aplicarle updateRules +// Ambas son necesarias porque updateRules puede eliminar columnas necesarias para las nuevas filas. +const transposeRules = { + TAXES_DISAGGREGATED: (originalRow, updatedRow) => { + let taxes + try { + taxes = JSON.parse(originalRow['TAXES_DISAGGREGATED']) + } catch (e) { + console.warn("Can't parse taxes: ", originalRow['TAXES_DISAGGREGATED'], e) + return [] + } + + if (!Array.isArray(taxes) || !taxes.length) { + return [] + } + + let rows = [] + for (const [i, tax] of taxes.entries()) { + let newRow = structuredClone(updatedRow) + + newRow['SOURCE_ID'] += `_tax${i}` + newRow['GROSS_AMOUNT'] = parseFloat(tax['amount']) + newRow['DESCRIPTION'] = `Impuesto ${tax['financial_entity'].replaceAll("_", " ")} (${tax['detail'].replaceAll("_", " ")})` + + rows.push(newRow) + } + + return rows + }, + + MP_FEE_AMOUNT: (originalRow, updatedRow) => { + let rows = [] + const fee = parseFloat(originalRow['MP_FEE_AMOUNT']) + + if (fee !== 0) { + let newRow = structuredClone(updatedRow) + newRow['SOURCE_ID'] += "_fee" + newRow['GROSS_AMOUNT'] = fee + newRow['DESCRIPTION'] = "Comisión + IVA" + + rows.push(newRow) + } + + return rows + }, +} + +// Reglas que modifican filas. +const updateRules = [ + deleteColumnsHandledByTransposeRules, + formatDate, + translateDescription, +] + +function deleteColumnsHandledByTransposeRules(row) { + for (const rule in transposeRules) { + if (rule in row) { + delete row[rule] + } + } +} + +function formatDate(row) { + if (!('DATE' in row)) return; + row['DATE'] = row['DATE'].substring(0, row['DATE'].length - 10).replaceAll("T", " ") +} + +function translateDescription(row) { + const translations = { + "payment": "Transferencia recibida", + "payout": "Transferencia enviada" + } + + for (const [key, value] of Object.entries(translations)) { + row['DESCRIPTION'] = row['DESCRIPTION'].replaceAll(key, value) + } + +} + onmessage = async function (event) { const inputFile = event.data const [inFileName, inFileExtension] = getNameAndExtension(inputFile); - const wb = read(await inputFile.bytes(), {type: "array"}) + const wb = read(await inputFile.arrayBuffer(), {type: "array"}) const sheet = wb.SheetNames[0]; let data = utils.sheet_to_json(wb.Sheets[sheet]); @@ -19,17 +100,6 @@ onmessage = async function (event) { postMessage(outFile) } -function getNameAndExtension(file) { - const path = file.name - - const path_arr = path.split(".") - const name = path_arr.slice(0, path_arr.length - 1).join(".") - const extension = path_arr[path_arr.length - 1]; - - return [name, extension] - -} - function processData(data) { let outData = [] @@ -51,7 +121,7 @@ function getReplacementRows(row) { const updatedRow = applyUpdateRules(row) replacementRows.push(updatedRow) - const newRows = applyInsertRules(row, updatedRow) + const newRows = applyTransposeRules(row, updatedRow) replacementRows.push(...newRows) return replacementRows @@ -59,44 +129,22 @@ function getReplacementRows(row) { function applyUpdateRules(row) { let updatedRow = structuredClone(row) - deleteColumnsHandledByInsertRules(updatedRow) - formatDate(updatedRow) - translateDescription(updatedRow) - - return updatedRow -} - -function deleteColumnsHandledByInsertRules(row) { - for (const rule in insertRules) { - if (rule in row) { - delete row[rule] - } - } -} - -function formatDate(row) { - if (!('DATE' in row)) return; - row['DATE'] = row['DATE'].substring(0, row['DATE'].length - 10).replaceAll("T", " ") -} -function translateDescription(row) { - const translations = { - "payment": "Transferencia recibida", - "payout": "Transferencia enviada" - } - - for (const [key, value] of Object.entries(translations)) { - row['DESCRIPTION'] = row['DESCRIPTION'].replaceAll(key, value) + for (const updateRule of updateRules) { + updateRule(updatedRow) } + return updatedRow } -function applyInsertRules(originalRow, updatedRow) { +function applyTransposeRules(originalRow, updatedRow) { let newRows = [] - for (const [column, rule] of Object.entries(insertRules)) { + for (const [column, rule] of Object.entries(transposeRules)) { if (column in originalRow) { try { - newRows.push(...rule(originalRow, updatedRow)) + const rows = rule(originalRow, updatedRow) + recalculateBalanceAmount(updatedRow, rows) + newRows.push(...rows) } catch (e) { console.log("originalRow:", originalRow) console.log("updatedRow:", updatedRow) @@ -108,50 +156,24 @@ function applyInsertRules(originalRow, updatedRow) { return newRows } -const insertRules = { - TAXES_DISAGGREGATED: (originalRow, updatedRow) => { - let taxes - try { - taxes = JSON.parse(originalRow['TAXES_DISAGGREGATED']) - } catch (e) { - console.warn("Can't parse taxes: ", originalRow['TAXES_DISAGGREGATED'], e) - return [] - } - - if (!Array.isArray(taxes) || !taxes.length) { - return [] - } - - let rows = [] - for (const [i, tax] of taxes.entries()) { - let newRow = structuredClone(updatedRow) - - newRow['SOURCE_ID'] += `_tax${i}` - newRow['GROSS_AMOUNT'] = parseFloat(tax['amount']) - newRow['DESCRIPTION'] = `Impuesto ${tax['financial_entity'].replaceAll("_", " ")} (${tax['detail'].replaceAll("_", " ")})` - updatedRow['BALANCE_AMOUNT'] = parseFloat(updatedRow['BALANCE_AMOUNT']) - newRow['GROSS_AMOUNT'] +function recalculateBalanceAmount(updatedRow, newRows) { + const newRowsGrossAmount = newRows.reduce((acc, curr) => acc + parseFloat(curr['GROSS_AMOUNT']), 0) - rows.push(newRow) - } + updatedRow['BALANCE_AMOUNT'] = parseFloat(updatedRow['BALANCE_AMOUNT']) - newRowsGrossAmount - return rows - }, - - FEE_AMOUNT: (originalRow, updatedRow) => { - let rows = [] - const fee = parseFloat(originalRow['FEE_AMOUNT']) + let previousBalanceAmount = updatedRow['BALANCE_AMOUNT'] + for (const row of newRows) { + row['BALANCE_AMOUNT'] = previousBalanceAmount + parseFloat(row['GROSS_AMOUNT']) + } +} - if (fee !== 0) { - let newRow = structuredClone(updatedRow) - newRow['SOURCE_ID'] += "_fee" - newRow['GROSS_AMOUNT'] = fee - newRow['DESCRIPTION'] = "Comisión + IVA" - updatedRow['BALANCE_AMOUNT'] = parseFloat(updatedRow['BALANCE_AMOUNT']) - newRow['GROSS_AMOUNT'] +function getNameAndExtension(file) { + const path = file.name - rows.push(newRow) - } + const path_arr = path.split(".") + const name = path_arr.slice(0, path_arr.length - 1).join(".") + const extension = path_arr[path_arr.length - 1]; - return rows - }, -} + return [name, extension] +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 6119ad9..f4d860f 100644 --- a/src/index.css +++ b/src/index.css @@ -15,7 +15,7 @@ a { font-weight: 500; - color: #646cff; + color: #7a7c99; text-decoration: inherit; } a:hover { @@ -35,25 +35,6 @@ h1 { line-height: 1.1; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - @media (prefers-color-scheme: light) { :root { color: #213547;