Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,150 @@ def test_import_n43(self):
self.assertEqual(statement_lines[1].ref, "/")
self.assertEqual(statement_lines[0].ref, "5540014210128010")

def test_sabadell_incoming_partner_detection(self):
"""Test that Sabadell incoming transfers detect partner from
'NOMBRE DEL ORDENANTE' prefix in concept 01."""
partner = self.env["res.partner"].create(
{"name": "TEST EMPRESA SL", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
# Simulate Sabadell incoming transfer concept record:
# First 35 chars: "NOMBRE DEL ORDENANTE TEST EMP"
# Remaining: "RESA,S.L."
conceptos = {
"01": ("NOMBRE DEL ORDENANTE TEST EMP", "RESA,S.L."),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_incoming_partner_no_suffix(self):
"""Test Sabadell incoming detection for a person (no legal suffix)."""
partner = self.env["res.partner"].create(
{"name": "MARIA CARMEN SERRAT FREIXA", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": ("NOMBRE DEL ORDENANTE MARIA CARME", "N SERRAT FREIXA"),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_ordenante_prefix(self):
"""Test 'ORDENANTE DE LA TRANSFERENCIA' prefix with colon separator."""
partner = self.env["res.partner"].create(
{"name": "ACME DISTRIBUCIONES SL", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": (
"ORDENANTE DE LA TRANSFERENCIA :",
" ACME DISTRIBUCIONES S.L.",
),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_transferenc_prefix(self):
"""Test 'TRANSFERENC. DE' prefix."""
partner = self.env["res.partner"].create(
{"name": "PEDRO MARTINEZ SANCHEZ", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": ("TRANSFERENC. DE PEDRO MARTINEZ SAN", "CHEZ"),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_beneficiario_transferencia_prefix(self):
"""Test 'BENEFICIARIO DE LA TRANSFERENCIA' prefix (outgoing transfer)."""
partner = self.env["res.partner"].create(
{"name": "ANA GARCIA LOPEZ", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": (
"BENEFICIARIO DE LA TRANSFERENCIA :",
" ANA GARCIA LOPEZ",
),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_beneficiario_prefix(self):
"""Test 'BENEFICIARIO' prefix (short form, outgoing transfer)."""
partner = self.env["res.partner"].create(
{"name": "CARLOS RUIZ MARTINEZ", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": ("BENEFICIARIO CARLOS RUIZ MARTINEZ", ""),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_core_sepa_prefix(self):
"""Test 'CORE' prefix (SEPA direct debit) — name glued without space."""
partner = self.env["res.partner"].create(
{"name": "Suministros Eolicos del Norte SL", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": ("CORESuministros Eolicos del Norte", " SL"),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_word_boundary_match(self):
"""Test word boundary regex fallback finds partner by unique word."""
partner = self.env["res.partner"].create(
{
"name": "FUNDACION GREENFIELD EDUCATIVA SL",
"company_id": self.env.company.id,
}
)
wizard = self.env["account.statement.import"]
# Name truncated — ilike on full extracted text won't match
conceptos = {
"01": (
"NOMBRE DEL ORDENANTE FUNDACION G",
"REENFIELD EDUCATIVA DE ESPA",
),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertEqual(result, partner)

def test_sabadell_word_boundary_no_substring(self):
"""Test word boundary regex does NOT match substrings.

MARTIN should not match MARTINEZA — word boundaries prevent it.
"""
self.env["res.partner"].create(
{"name": "MARTINEZA, S.L.", "company_id": self.env.company.id}
)
wizard = self.env["account.statement.import"]
conceptos = {
"01": (
"NOMBRE DEL ORDENANTE MARTIN ROM",
"ERO LOPEZ",
),
}
result = wizard._get_n43_partner_from_sabadell(conceptos)
self.assertFalse(result)

def test_clean_partner_name_suffix(self):
"""Test legal suffix removal for partner name matching."""
wizard = self.env["account.statement.import"]
self.assertEqual(
wizard._clean_partner_name_suffix("VILA FOPE,S.L."), "VILA FOPE"
)
self.assertEqual(wizard._clean_partner_name_suffix("EMPRESA S.A."), "EMPRESA")
self.assertEqual(wizard._clean_partner_name_suffix("CORP SLU"), "CORP")
self.assertEqual(
wizard._clean_partner_name_suffix("MARIA CARMEN SERRAT"),
"MARIA CARMEN SERRAT",
)

def test_import_n43_fecha_oper(self):
self.journal.n43_date_type = "fecha_oper"
action = self.import_wizard.import_file_button()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging
import re
from datetime import datetime

from odoo import _, api, exceptions, fields, models
Expand Down Expand Up @@ -310,14 +311,99 @@ def _get_n43_partner_from_santander(self, conceptos):
partner = partner_obj.search([("name", "ilike", name)], limit=1)
return partner

_LEGAL_SUFFIX_RE = re.compile(
r"[,.\s]*(S\.?L\.?U?\.?|S\.?A\.?U?\.?|S\.?C\.?|S\.?C\.?P\.?)\s*$",
re.IGNORECASE,
)

def _clean_partner_name_suffix(self, name):
"""Remove common Spanish legal form suffixes for fuzzy matching.

Examples: "VILA FOPE,S.L." -> "VILA FOPE"
"EMPRESA S.A." -> "EMPRESA"
"""
return self._LEGAL_SUFFIX_RE.sub("", name).strip()

def _get_n43_partner_from_sabadell(self, conceptos):
partner_obj = self.env["res.partner"]
partner = partner_obj.browse()
# Try to match from partner name
if conceptos.get("01"):
name = conceptos["01"][1]
if name and len(name) > 7:
partner = partner_obj.search([("name", "ilike", name)], limit=1)
if not conceptos.get("01"):
return partner
# 1) Existing logic: try matching from second part of concept 01
name = conceptos["01"][1]
if name and len(name) > 7:
partner = partner_obj.search([("name", "ilike", name)], limit=1)
if partner:
return partner
# 2) Transfer / direct debit detection: concept 01 starts with a
# known prefix followed by the partner name.
# Sabadell uses these formats for transfers and SEPA direct debits.
full_name = (conceptos["01"][0] + conceptos["01"][1]).strip()
prefixes = (
"NOMBRE DEL ORDENANTE",
"ORDENANTE DE LA TRANSFERENCIA",
"BENEFICIARIO DE LA TRANSFERENCIA",
"TRANSFERENC. DE",
"BENEFICIARIO",
"CORE",
)
extracted = ""
for prefix in prefixes:
if full_name.upper().startswith(prefix):
extracted = full_name[len(prefix) :].strip()
# Strip leading colon/spaces (ORDENANTE format uses ": name")
extracted = extracted.lstrip(": ").strip()
break
if extracted:
# Always strip legal suffixes before searching, as the partner
# name in Odoo may use a different notation (e.g., "SL" vs
# "S.L."). Searching by the core name avoids mismatches.
clean = self._clean_partner_name_suffix(extracted)
name_to_search = clean if clean and len(clean) > 3 else extracted
if len(name_to_search) > 3:
partner = partner_obj.search(
[("name", "ilike", name_to_search)], limit=1
)
# Last resort: search by individual words using word boundary
# matching to avoid substring false positives (e.g. CARMEN
# matching CARMENET). Only accept if exactly one partner matches.
if not partner:
_stop = {
"de",
"del",
"la",
"las",
"el",
"los",
"i",
"y",
"en",
"por",
"con",
"para",
"the",
"and",
}
words = [
w
for w in name_to_search.split()
if len(w) > 3 and w.lower() not in _stop
]
for word in words:
# Use raw SQL with PostgreSQL word boundary regex
# (\m = start, \M = end of word) to avoid substring
# false positives (e.g. CARMEN != CARMENET).
# Odoo domains don't support the ~* operator.
self.env.cr.execute(
"SELECT id FROM res_partner"
" WHERE name ~* %s AND active = true"
" LIMIT 2",
[r"\m" + word + r"\M"],
)
rows = self.env.cr.fetchall()
if len(rows) == 1:
partner = partner_obj.browse(rows[0][0])
break
return partner

def _get_n43_partner(self, line, journal):
Expand Down Expand Up @@ -369,9 +455,13 @@ def _parse_single_file_n43(self, n43):
for line in group["lines"]:
conceptos = []
for concept_line in line["conceptos"]:
conceptos.extend(
x.strip() for x in line["conceptos"][concept_line] if x.strip()
)
# Concatenate both halves of the same concept record
# without adding a space — they are continuous text
# split at the fixed-width boundary (position 39).
parts = line["conceptos"][concept_line]
joined = (parts[0] + parts[1]).strip()
if joined:
conceptos.append(joined)
vals_line = {
"payment_ref": " ".join(conceptos)
or self._get_n43_ref(line)
Expand Down
Loading