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;