diff --git a/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/__init__.py b/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/energy_certificate_link.json b/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/energy_certificate_link.json new file mode 100644 index 0000000..759bb39 --- /dev/null +++ b/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/energy_certificate_link.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-06-03 10:04:29.254339", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "energy_certificate" + ], + "fields": [ + { + "fieldname": "energy_certificate", + "fieldtype": "Link", + "label": "Energy Certificate", + "options": "Energy Certificate Link" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-06-03 20:03:26.621275", + "modified_by": "Administrator", + "module": "Openimmo Propms", + "name": "Energy Certificate Link", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/energy_certificate_link.py b/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/energy_certificate_link.py new file mode 100644 index 0000000..37b2f03 --- /dev/null +++ b/openimmo_propms/openimmo_propms/doctype/energy_certificate_link/energy_certificate_link.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Talib sheikh and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class EnergyCertificateLink(Document): + pass diff --git a/openimmo_propms/openimmo_propms/doctype/integration_field_mapping/integration_field_mapping.json b/openimmo_propms/openimmo_propms/doctype/integration_field_mapping/integration_field_mapping.json index e65e259..24cc0af 100644 --- a/openimmo_propms/openimmo_propms/doctype/integration_field_mapping/integration_field_mapping.json +++ b/openimmo_propms/openimmo_propms/doctype/integration_field_mapping/integration_field_mapping.json @@ -8,7 +8,6 @@ "source_configuration_section", "source_value_type", "source_field", - "static_value", "column_break_source", "transformation", "value_mapping", @@ -17,6 +16,7 @@ "export_strip_prefix", "target_mapping_section", "target_field", + "static_value", "column_break_target", "is_unique", "auto_create_link", @@ -30,6 +30,7 @@ "label": "Source Configuration" }, { + "columns": 1, "default": "Field", "fieldname": "source_value_type", "fieldtype": "Select", @@ -38,6 +39,7 @@ "options": "Field\nStatic" }, { + "columns": 2, "description": "Example: immobilie.geo.plz", "fieldname": "source_field", "fieldtype": "Data", @@ -46,9 +48,11 @@ "reqd": 1 }, { + "columns": 2, "depends_on": "eval:doc.source_value_type=='Static'", "fieldname": "static_value", "fieldtype": "Data", + "in_list_view": 1, "label": "Static Value", "mandatory_depends_on": "eval:doc.source_value_type=='Static'" }, @@ -95,6 +99,7 @@ "label": "Target Mapping" }, { + "columns": 2, "description": "Frappe fieldname (e.g. zip_code)", "fieldname": "target_field", "fieldtype": "Data", @@ -110,14 +115,12 @@ "default": "0", "fieldname": "is_unique", "fieldtype": "Check", - "in_list_view": 1, "label": "Is Unique (Duplicate Check)" }, { "default": "0", "fieldname": "auto_create_link", "fieldtype": "Check", - "in_list_view": 1, "label": "Auto Create Link Record" }, { @@ -131,7 +134,6 @@ "description": "Specific DocType to create the record in.", "fieldname": "link_target_doctype", "fieldtype": "Link", - "in_list_view": 1, "label": "Link Target DocType", "mandatory_depends_on": "eval:doc.auto_create_link == 1", "options": "DocType" @@ -140,7 +142,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-23 08:53:30.154407", + "modified": "2026-06-03 12:40:53.596880", "modified_by": "Administrator", "module": "Openimmo Propms", "name": "Integration Field Mapping", @@ -150,4 +152,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.js b/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.js index c741112..75f47a3 100644 --- a/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.js +++ b/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.js @@ -1,10 +1,12 @@ frappe.ui.form.on("Integration Source", { onload(frm) { toggle_credential_fields(frm); + update_ftp_intro(frm); }, refresh(frm) { toggle_credential_fields(frm); + update_ftp_intro(frm); if (frm.doc.source_type === "FTP") { // 1. Connection Test Button @@ -111,6 +113,10 @@ frappe.ui.form.on("Integration Source", { } }, + ftp_transfer_enabled(frm) { + update_ftp_intro(frm); + }, + source_type(frm) { toggle_credential_fields(frm); }, @@ -120,6 +126,22 @@ frappe.ui.form.on("Integration Source", { }, }); +function update_ftp_intro(frm) { + if (frm.doc.source_type !== "FTP") { + frm.set_intro(""); // Clear intro if not FTP + return; + } + + // Clear existing intro first to avoid duplication + frm.set_intro(""); + + if (frm.doc.ftp_transfer_enabled) { + frm.set_intro(__("Exporting will trigger FTP Transfer."), "green"); + } else { + frm.set_intro(__("FTP Transfer is Disabled. Files will only be generated locally."), "red"); + } +} + function toggle_credential_fields(frm) { const is_import = frm.doc.operation_type === "Import"; const is_export = frm.doc.operation_type === "Export"; diff --git a/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.json b/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.json index 6e577c7..f22ce71 100644 --- a/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.json +++ b/openimmo_propms/openimmo_propms/doctype/integration_source/integration_source.json @@ -387,6 +387,7 @@ "label": "Field Mapping" }, { + "allow_bulk_edit": 1, "fieldname": "field_mappings", "fieldtype": "Table", "label": "Field Mappings", @@ -430,7 +431,7 @@ "link_fieldname": "source_name" } ], - "modified": "2026-05-02 10:20:23.772618", + "modified": "2026-06-03 11:14:55.523019", "modified_by": "Administrator", "module": "Openimmo Propms", "name": "Integration Source", @@ -464,4 +465,4 @@ } ], "track_changes": 1 -} +} \ No newline at end of file diff --git a/openimmo_propms/openimmo_propms/doctype/property_type/__init__.py b/openimmo_propms/openimmo_propms/doctype/property_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openimmo_propms/openimmo_propms/doctype/property_type/property_type.js b/openimmo_propms/openimmo_propms/doctype/property_type/property_type.js new file mode 100644 index 0000000..1ad86c8 --- /dev/null +++ b/openimmo_propms/openimmo_propms/doctype/property_type/property_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Talib sheikh and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Property Type", { +// refresh(frm) { + +// }, +// }); diff --git a/openimmo_propms/openimmo_propms/doctype/property_type/property_type.json b/openimmo_propms/openimmo_propms/doctype/property_type/property_type.json new file mode 100644 index 0000000..e86243a --- /dev/null +++ b/openimmo_propms/openimmo_propms/doctype/property_type/property_type.json @@ -0,0 +1,173 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:property_type_name", + "creation": "2026-06-02 08:19:19.220638", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "property_type_name", + "parent_property_type", + "is_group", + "is_active", + "description", + "mapping_section", + "openimmo_objektart", + "openimmo_attribute", + "openimmo_value", + "immowelt_value", + "usage_section", + "use_residential", + "use_commercial", + "use_investment", + "use_mixed", + "lft", + "rgt", + "old_parent" + ], + "fields": [ + { + "fieldname": "property_type_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Property Type Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "parent_property_type", + "fieldtype": "Link", + "label": "Parent Property Type", + "options": "Property Type" + }, + { + "default": "0", + "fieldname": "is_group", + "fieldtype": "Check", + "label": "Is Group" + }, + { + "default": "1", + "fieldname": "is_active", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Active" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "mapping_section", + "fieldtype": "Section Break", + "label": "Portal Mapping" + }, + { + "fieldname": "openimmo_objektart", + "fieldtype": "Select", + "label": "OpenImmo Objektart", + "options": "\nzimmer\nwohnung\nhaus\ngrundstueck\nbuero_praxen\neinzelhandel\ngastgewerbe\nhallen_lager_prod\nland_und_forstwirtschaft\nparken\nsonstige\nfreizeitimmobilie_gewerblich\nzinshaus_renditeobjekt" + }, + { + "fieldname": "openimmo_attribute", + "fieldtype": "Select", + "label": "OpenImmo Attribute", + "options": "\nzimmertyp\nwohnungtyp\nhaustyp\ngrundst_typ\nbuero_typ\nhandel_typ\ngastgew_typ\nhallen_typ\nland_typ\nparken_typ\nsonstige_typ\nfreizeit_typ\nzins_typ" + }, + { + "fieldname": "openimmo_value", + "fieldtype": "Select", + "label": "OpenImmo Value", + "options": "\nZIMMER\nDACHGESCHOSS\nMAISONETTE\nLOFT-STUDIO-ATELIER\nPENTHOUSE\nTERRASSEN\nETAGE\nERDGESCHOSS\nSOUTERRAIN\nAPARTMENT\nFERIENWOHNUNG\nGALERIE\nROHDACHBODEN\nATTIKAWOHNUNG\nREIHENHAUS\nREIHENEND\nREIHENMITTEL\nREIHENECK\nDOPPELHAUSHAELFTE\nEINFAMILIENHAUS\nSTADTHAUS\nBUNGALOW\nVILLA\nRESTHOF\nBAUERNHAUS\nLANDHAUS\nSCHLOSS\nZWEIFAMILIENHAUS\nMEHRFAMILIENHAUS\nFERIENHAUS\nBERGHUETTE\nCHALET\nSTRANDHAUS\nLAUBE-DATSCHE-GARTENHAUS\nAPARTMENTHAUS\nBURG\nHERRENHAUS\nFINCA\nRUSTICO\nFERTIGHAUS\nWOHNEN\nGEWERBE\nINDUSTRIE\nLAND_FORSTWIRTSCHAFT\nFREIZEIT\nGEMISCHT\nGEWERBEPARK\nSONDERNUTZUNG\nSEELIEGENSCHAFT\nBUEROFLAECHE\nBUEROHAUS\nBUEROZENTRUM\nLOFT_ATELIER\nPRAXIS\nPRAXISFLAECHE\nPRAXISHAUS\nAUSSTELLUNGSFLAECHE\nCOWORKING\nSHARED_OFFICE\nLADENLOKAL\nEINZELHANDELSLADEN\nVERBRAUCHERMARKT\nEINKAUFSZENTRUM\nKAUFHAUS\nFACTORY_OUTLET\nKIOSK\nVERKAUFSFLAECHE\nGASTRONOMIE\nGASTRONOMIE_UND_WOHNUNG\nPENSIONEN\nHOTELS\nWEITERE_BEHERBERGUNGSBETRIEBE\nBAR\nCAFE\nDISCOTHEK\nRESTAURANT\nRAUCHERLOKAL\nEINRAUMLOKAL\nHALLE\nINDUSTRIEHALLE\nLAGER\nLAGERFLAECHEN\nLAGER_MIT_FREIFLAECHE\nHOCHREGALLAGER\nSPEDITIONSLAGER\nPRODUKTION\nWERKSTATT\nSERVICE\nFREIFLAECHEN\nKUEHLHAUS\nLANDWIRTSCHAFTLICHE_BETRIEBE\nBAUERNHOF\nAUSSIEDLERHOF\nGARTENBAU\nACKERBAU\nWEINBAU\nVIEHWIRTSCHAFT\nJAGD_UND_FORSTWIRTSCHAFT\nTEICH_UND_FISCHWIRTSCHAFT\nSCHEUNEN\nREITERHOEFE\nSONSTIGE_LANDWIRTSCHAFTSIMMOBILIEN\nANWESEN\nJAGDREVIER\nSTELLPLATZ\nCARPORT\nDOPPELGARAGE\nDUPLEX\nTIEFGARAGE\nBOOTSLIEGEPLATZ\nEINZELGARAGE\nPARKHAUS\nTIEFGARAGENSTELLPLATZ\nPARKPLATZ_STROM\nTANKSTELLE\nKRANKENHAUS\nSONSTIGE\nSPORTANLAGEN\nVERGNUEGUNGSPARKS_UND_CENTER\nFREIZEITANLAGE\nWOHN_UND_GESCHAEFTSHAUS\nGESCHAEFTSHAUS\nBUEROGEBAEUDE\nSB_MAERKTE\nEINKAUFSCENTREN\nWOHNANLAGEN\nVERBRAUCHERMAERKTE\nINDUSTRIEANLAGEN\nPFLEGEHEIM\nSANATORIUM\nSENIORENHEIM\nBETREUTES-WOHNEN\nKEINE_ANGABE" + }, + { + "fieldname": "immowelt_value", + "fieldtype": "Data", + "label": "Immowelt Value" + }, + { + "fieldname": "usage_section", + "fieldtype": "Section Break", + "label": "Usage Configuration" + }, + { + "default": "0", + "fieldname": "use_residential", + "fieldtype": "Check", + "label": "Is Residential" + }, + { + "default": "0", + "fieldname": "use_commercial", + "fieldtype": "Check", + "label": "Is Commercial" + }, + { + "default": "0", + "fieldname": "use_investment", + "fieldtype": "Check", + "label": "Is Investment" + }, + { + "default": "0", + "fieldname": "use_mixed", + "fieldtype": "Check", + "label": "Is Mixed" + }, + { + "fieldname": "lft", + "fieldtype": "Int", + "hidden": 1, + "label": "Left", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "rgt", + "fieldtype": "Int", + "hidden": 1, + "label": "Right", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "old_parent", + "fieldtype": "Link", + "label": "Old Parent", + "options": "Property Type" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "is_tree": 1, + "links": [], + "modified": "2026-06-02 09:34:22.258083", + "modified_by": "Administrator", + "module": "Openimmo Propms", + "name": "Property Type", + "naming_rule": "By fieldname", + "nsm_parent_field": "parent_property_type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "property_type_name" +} diff --git a/openimmo_propms/openimmo_propms/doctype/property_type/property_type.py b/openimmo_propms/openimmo_propms/doctype/property_type/property_type.py new file mode 100644 index 0000000..595a716 --- /dev/null +++ b/openimmo_propms/openimmo_propms/doctype/property_type/property_type.py @@ -0,0 +1,14 @@ +# Copyright (c) 2026, Talib sheikh and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class PropertyType(Document): + def validate(self): + # Strip whitespace from all string fields + for field in ["property_type_name", "property_category", "openimmo_objektart", "openimmo_attribute", "openimmo_value", "immowelt_value"]: + val = self.get(field) + if val: + self.set(field, val.strip()) diff --git a/openimmo_propms/openimmo_propms/doctype/property_type/test_property_type.py b/openimmo_propms/openimmo_propms/doctype/property_type/test_property_type.py new file mode 100644 index 0000000..aea2f47 --- /dev/null +++ b/openimmo_propms/openimmo_propms/doctype/property_type/test_property_type.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Talib sheikh and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPropertyType(FrappeTestCase): + pass diff --git a/openimmo_propms/services/export_engine.py b/openimmo_propms/services/export_engine.py index c91c1bb..a0ec72c 100644 --- a/openimmo_propms/services/export_engine.py +++ b/openimmo_propms/services/export_engine.py @@ -626,6 +626,11 @@ def _get_requested_fieldnames(source): meta = frappe.get_meta(source.target_doctype) valid_fields = {f.fieldname for f in meta.fields} + # Always try to fetch property type and unit ID for resolution + for field in ["custom_property_type", "custom_unit_id"]: + if field in valid_fields: + fieldnames.add(field) + for mapping in source.field_mappings: fieldname = _get_configured_fieldname(mapping.target_field) if fieldname and "." not in fieldname and fieldname in valid_fields: diff --git a/openimmo_propms/services/export_mapper.py b/openimmo_propms/services/export_mapper.py index ee723be..c8553c0 100644 --- a/openimmo_propms/services/export_mapper.py +++ b/openimmo_propms/services/export_mapper.py @@ -1,4 +1,6 @@ import frappe +import math +from frappe.utils import get_url from openimmo_propms.services.mapper import apply_data_transformation @@ -33,7 +35,11 @@ def build_property_data(source, record): if val not in (None, ""): # Map to indexed XML path indexed_path = xml_path.replace("anhang", f"anhang.{i}") - mapped_data[indexed_path] = _normalize_value(val) + mapped_data[indexed_path] = _normalize_export_value( + source, + indexed_path, + val, + ) continue value = _get_record_value(record_data, fieldname, source.target_doctype) @@ -46,11 +52,18 @@ def build_property_data(source, record): value = apply_data_transformation(value, mapping, record_data) value = _strip_prefix_for_export(value, mapping.get("export_strip_prefix")) + value = _normalize_export_value(source, xml_path, value) mapped_data[xml_path] = value # Smart fallback for mandatory OpenImmo fields _ensure_mandatory_openimmo_fields(mapped_data, record_data, source) + # 10b. Marketing Title Fallback Logic + _apply_marketing_title_fallback(mapped_data, record_data) + + # Automatically set nutzungsart attributes from Property Type master + _set_nutzungsart_attributes(mapped_data, record_data.get("custom_property_type")) + # Manually collect images if not mapped via standard field_mappings image_gallery = record_data.get("custom_image_gallery") if image_gallery and isinstance(image_gallery, list): @@ -58,7 +71,7 @@ def build_property_data(source, record): img_path = row.get("picture") if isinstance(row, dict) else getattr(row, "picture", None) if img_path: indexed_path = f"anhaenge.anhang.{i}.daten.pfad" - mapped_data[indexed_path] = img_path + mapped_data[indexed_path] = _normalize_export_value(source, indexed_path, img_path) # Attribute mappings remain (these are structural, not content) mapped_data[f"anhaenge.anhang.{i}@location"] = "EXTERN" @@ -78,6 +91,20 @@ def build_property_data(source, record): return mapped_data +def _apply_marketing_title_fallback(mapped_data, record_data): + """Applies fallback chain: custom_marketing_title -> name1.""" + # 1. Primary: If already mapped from custom_marketing_title, do nothing + if mapped_data.get("freitexte.objekttitel"): + return + + # 2. Fallback: Try name1 + title = record_data.get("name1") + + # Only assign if a title was found + if title: + mapped_data["freitexte.objekttitel"] = title + + def _ensure_mandatory_openimmo_fields(mapped_data, record_data, source): """Ensure Immowelt doesn't reject due to missing type or category tags.""" @@ -118,42 +145,73 @@ def _ensure_mandatory_openimmo_fields(mapped_data, record_data, source): mapped_data["objektkategorie.vermarktungsart@KAUF"] = True # 3. Resolve Object Type (e.g., ) + _resolve_property_type_from_record(mapped_data, record_data, source) + + # Verify that an object type was mapped has_type_tag = any( - key.startswith("objektkategorie.objektart.") and "@" not in key + key.startswith("objektkategorie.objektart.") for key in mapped_data ) if not has_type_tag: - # Check both raw record and already mapped text in 'objektart' - type_hint = _normalize_value( - mapped_data.get("objektkategorie.objektart") or - record_data.get(source.name_field) or - record_data.get("property_type") or - record_data.get("type") or "" - ).lower() - - if any(w in type_hint for w in ["flat", "apartment", "wohnung", "etage"]): - mapped_data["objektkategorie.objektart.wohnung@wohnungtyp"] = "ETAGE" - elif any(w in type_hint for w in ["house", "haus", "villa"]): - mapped_data["objektkategorie.objektart.haus@haustyp"] = "EINFAMILIENHAUS" - elif any(w in type_hint for w in ["office", "buero", "praxis", "laden", "shop"]): - mapped_data["objektkategorie.objektart.buero_praxen@buerotyp"] = "BUEROFLAECHE" - elif any(w in type_hint for w in ["garage", "stellplatz", "parking"]): - mapped_data["objektkategorie.objektart.parken@parken_typ"] = "STELLPLATZ" - elif any(w in type_hint for w in ["zimmer"]): - mapped_data["objektkategorie.objektart.zimmer@zimmertyp"] = "ZIMMER" + frappe.log_error(f"Missing Property Type mapping for property: {record_data.get('name')}. Please configure the Property Type Master.") + + # 4. Ensure some optional but helpful 'proper' tags exist + if "objektkategorie.user_defined_simplefield@feldname" not in mapped_data: + mapped_data["objektkategorie.user_defined_simplefield@feldname"] = "" + + +def _resolve_property_type_from_record(mapped_data, record_data, source): + """Fetch Property Type details and map to OpenImmo objektart.""" + # Use erpnext_id from mapping if available, otherwise record name or custom_unit_id + erpnext_id = mapped_data.get("erpnext_id") or record_data.get("name") or record_data.get("custom_unit_id") + + property_type_name = record_data.get("custom_property_type") + + if not property_type_name and erpnext_id and source.target_doctype: + # Try fetching from the actual record in the target doctype + property_type_name = frappe.db.get_value( + source.target_doctype, erpnext_id, "custom_property_type" + ) + + # Fallback: maybe the ID is custom_unit_id as in user's example + if not property_type_name: + property_type_name = frappe.db.get_value( + source.target_doctype, + {"custom_unit_id": erpnext_id}, + "custom_property_type" + ) + + if not property_type_name: + return + + try: + prop_type = frappe.get_cached_doc("Property Type", property_type_name) + except Exception: + # If it's a string name but doc doesn't exist by name, try getting by property_type_name field + prop_type_name = frappe.db.get_value("Property Type", {"property_type_name": property_type_name}, "name") + if prop_type_name: + prop_type = frappe.get_cached_doc("Property Type", prop_type_name) else: - mapped_data["objektkategorie.objektart.zusatz_erweit@objektart_standard"] = "SONSTIGE" + return - # If 'objektart' text was mapped (like 'Apartment'), we usually want to clear it - # to avoid having both text AND sub-tags inside , which is non-standard. - if has_type_tag or not mapped_data.get("objektkategorie.objektart.zusatz_erweit@objektart_standard") == "SONSTIGE": + objektart = prop_type.get("openimmo_objektart") + attribute = prop_type.get("openimmo_attribute") + value = prop_type.get("openimmo_value") + + if objektart: + # Clear any existing text mapping to objektart to avoid validation errors if "objektkategorie.objektart" in mapped_data: - # Move the text to a more appropriate place or clear it - if not mapped_data.get("freitexte.objekttitel"): - mapped_data["freitexte.objekttitel"] = mapped_data["objektkategorie.objektart"] del mapped_data["objektkategorie.objektart"] + path = f"objektkategorie.objektart.{objektart}" + if attribute and value: + # Ensure value is uppercase for OpenImmo standards + mapped_data[f"{path}@{attribute}"] = str(value).upper() + else: + # Force tag creation (e.g. ) + mapped_data[path] = "" + # 4. Ensure some optional but helpful 'proper' tags exist if "objektkategorie.user_defined_simplefield@feldname" not in mapped_data: mapped_data["objektkategorie.user_defined_simplefield@feldname"] = "" @@ -256,3 +314,102 @@ def _normalize_value(value): cleaned = [str(item) for item in value if item not in (None, "")] return "\n".join(cleaned) if cleaned else None return value + + +def _normalize_export_value(source, xml_path, value): + # Fix for 'anzahl_stellplaetze' positive integer constraint + if "anzahl_stellplaetze" in xml_path: + try: + val_int = int(float(value)) + if val_int <= 0: + return None # Omit tag if 0 or less to pass XSD + return val_int + except (ValueError, TypeError): + return None + + # 31. Round all area fields up to next 5 + area_fields = [ + "wohnflaeche", "nutzflaeche", "gesamtflaeche", "ladenflaeche", + "lagerflaeche", "verkaufsflaeche", "bueroflaeche", "kellerflaeche", + "gartenflaeche", "balkon_terrasse_flaeche" + ] + if any(field in xml_path for field in area_fields): + try: + val_float = float(value) + return math.ceil(val_float / 5) * 5 + except (ValueError, TypeError): + pass + + # Fix for 'haustiere' boolean mapping + if "haustiere" in xml_path: + val_str = str(value).strip().lower() + if val_str in ["ja", "nach absprache", "1", "true", "yes"]: + return True + elif val_str in ["nein", "0", "false"]: + return False + else: + return None # Omit element if invalid + + # Fix for 'moebliert' enumeration mapping + if "moebliert" in xml_path and "@moeb" in xml_path: + val_str = str(value).strip().lower() + if val_str in ["voll", "teil"]: + return val_str.upper() + elif val_str in ["1", "true", "yes", "checked", "on"]: + return "VOLL" + else: + return None # Omit element if '0', 'false', or invalid/unchecked + + # Fix for 'heizungsart' and 'befeuerung' attribute mapping + if ("heizungsart" in xml_path or "befeuerung" in xml_path) and "@" in xml_path: + # If the attribute exists and has a value, it should be 'true' + val_str = str(value).strip().lower() + if val_str not in ["0", "false", "none", "", "no"]: + return "true" + return None # Omit attribute if false/0/no + + normalized = _normalize_value(value) + if not _is_media_path_field(xml_path): + return normalized + return _build_absolute_media_url(source, normalized) + +def _set_nutzungsart_attributes(mapped_data, property_type_name): + """Fetches usage attributes from Property Type master and maps to XML.""" + if not property_type_name: + return + + try: + # Fetch document from Property Type master + prop_type = frappe.get_cached_doc("Property Type", property_type_name) + + # Map XML attributes + mapped_data["objektkategorie.nutzungsart@WOHNEN"] = bool(prop_type.use_residential) + mapped_data["objektkategorie.nutzungsart@GEWERBE"] = bool(prop_type.use_commercial) + mapped_data["objektkategorie.nutzungsart@ANLAGE"] = bool(prop_type.use_investment) + mapped_data["objektkategorie.nutzungsart@WAZ"] = bool(prop_type.use_mixed) + + except Exception: + pass + + +def _is_media_path_field(xml_path): + path = (xml_path or "").strip() + return path == "pfad" or path.endswith(".pfad") + + +def _build_absolute_media_url(source, image_value): + if not image_value: + return image_value + + image_url = str(image_value).strip() + if not image_url: + return image_url + + if image_url.startswith(("http://", "https://")): + return image_url + + base_url = (getattr(source, "base_media_url", "") or "").strip() or get_url() + if not base_url: + return image_url + + return "{0}/{1}".format(base_url.rstrip("/"), image_url.lstrip("/")) diff --git a/openimmo_propms/services/xml_builder.py b/openimmo_propms/services/xml_builder.py index 2ce3f8f..3ca2773 100644 --- a/openimmo_propms/services/xml_builder.py +++ b/openimmo_propms/services/xml_builder.py @@ -37,7 +37,7 @@ def ensure_xml_path(parent, path): def set_xml_value(parent, path, value): """Set a text value or attribute at a dotted XML path.""" - if value is None or value == "": + if value is None: return # Standard OpenImmo booleans are lowercase "true" or "false" diff --git a/openimmo_propms/services/xsd_builder.py b/openimmo_propms/services/xsd_builder.py index a7dd39a..ea1555f 100644 --- a/openimmo_propms/services/xsd_builder.py +++ b/openimmo_propms/services/xsd_builder.py @@ -97,9 +97,14 @@ def _fill_element(self, node, schema_element, data, prefix=""): if schema_element.type.is_simple() or schema_element.type.has_simple_content(): val = data.get(prefix) - # Mandatory field 'stand_vom' needs a date - if schema_element.name == 'stand_vom' and (val in [None, ""]): - val = nowdate() + # Mandatory field 'stand_vom' needs a date (YYYY-MM-DD) + if schema_element.name == 'stand_vom': + from frappe.utils import getdate + if val in [None, ""]: + val = nowdate() + else: + # Ensure it's a date object (handles strings and datetime objects) + val = getdate(val) if val not in [None, ""]: node.text = self._format_value(val) @@ -184,11 +189,6 @@ def _get_indices(self, data, prefix): indices.add(int(parts[0])) return sorted(list(indices)) - def _format_value(self, val): - if isinstance(val, bool): - return "true" if val else "false" - return str(val) - def generate_xsd_based_xml(properties_data, anbieter_context, version="1.2.7"): builder = XSDDrivenBuilder() return builder.generate_xml(properties_data, anbieter_context, version)