SAPB1SL Connector - Customer Update
This document details the Customer Update pipeline for the SAP Business One Service Layer (B1SL) connector. It outlines how customer-related data from SAP B1SL is synchronized with App4Sales, covering Customers, Addresses, Contact Persons, Item Class Value Discounts, and Dashboards. The process ensures data consistency, handles transformations, and manages various synchronization settings.
Data Source Configuration
Customer data is retrieved from SAP Business One Service Layer (B1SL) using OData queries. The base URL and authentication details are configured within the connector settings. Salesperson data is also retrieved from SAP B1SL.
The synchronization can be a full sync (e.g., weekly on Saturday, or when explicitly configured) or a partial sync based on the `UpdateDate` of customer records since the last successful synchronization.
Additional customer data, including free fields and item filters, can be sourced from external FTP/CSV files.
Customer Core Fields
App4Sales Field | Source Field (API/Excel/DB) | Logic/Notes |
CustomerCode | SapB1SLCustomer.CustomerCode | Direct mapping. For new customers, a code is generated based on `NewCustomersCodePrefix` and `NewCustomerCodeNumber` settings, ensuring sequential numbering. If `SendEmptyCardCode` setting is true, an empty string is sent to SAP. |
CustomerName | SapB1SLCustomer.CustomerName | Direct mapping. Required field; customers without a name are skipped. |
SapB1SLCustomer.Email | Direct mapping. | |
Phone | SapB1SLCustomer.Phone1 | Direct mapping. |
VatLiable | SapB1SLCustomer.VatLiable (enum), SapB1SLCustomer.VatNumber, SapB1SLAddress.FederalTaxID | Determined by connector settings:
If the App4Sales administration is set to USA, `VatLiable` is always set to `false`. |
UsesPriceField | SapB1SLCustomer.PriceListCode | Direct mapping. If price deduplication is enabled, the PriceListCode may be migrated to a new ID based on internal lookup tables. Fallbacks to default price list (ID 1) if migrated ID not found. |
Currency | SapB1SLCustomer.Currency | Direct mapping. |
Discount | SapB1SLCustomer.DiscountPercent | Mapped directly if `UseCustomerDiscount` setting is true, otherwise defaults to 0.0m. |
LanguageCode | SapB1SLCustomer.LanguageCode | Mapped from SAP LanguageCode (converted to string). Further mapped to App4Sales internal language codes using `MappedLanguagesContext`. |
VatCode | SapB1SLCustomer.VatNumber, SapB1SLCustomer.FederalTaxID | Mapped from `SapB1SLCustomer.VatNumber` or `SapB1SLCustomer.FederalTaxID`. When sending to SAP, if `SendVatNumberInFederalTaxIDField` is true, it is sent as `FederalTaxID`, otherwise as `VatNumber`. |
PaymentConditionCode | SapB1SLCustomer.PaymentTermsGroupCode | Mapped from integer `PaymentTermsGroupCode` to string. If empty string is received, it's converted to null to prevent foreign key issues. |
CustomerEnabled | Derived | Set to `true` if customer is active in SAP, or based on `setAsActive` parameter in conversion. In `SapB1SLConnector.cs`, customers with `Valid` as "False" or `Frozen` as "True" in SAP B1SL are excluded from synchronization (set as `false` for `setAsActive`). |
CustomerClassification | SapB1SLCustomer.GroupCode, or property defined by `CustomerClassificationProperty` setting | If `CustomerClassificationProperty` setting is defined, value is retrieved from the specified property (either a direct property or an unknown element/UDF) of the `SapB1SLCustomer`. Otherwise, mapped from `SapB1SLCustomer.GroupCode`. |
CustomerGuid | Derived | Retrieved from local cache (`_customerCodeLinks`), Portal API, or a new GUID is generated. Existing GUIDs for inactive customers (prefixed with '~') are reused. |
Created | Derived | Defaults to `DateTime.Now` if `MinValue`. Truncated to seconds. |
Sysmodified | Derived | Truncated to seconds. |
PasswordWebshop | Hardcoded | Set to `null` if `UpdaterHelper.IsUpdater` is true. |
CustomerDashboard | SapB1SLCustomer (Base64 string) | If not empty, the Base64 string is decoded and stored separately via `CustomerDataManager`. The field itself is then set to `null` on the customer object for memory. |
CustomerNote | SapB1SLCustomer (via an extra field) | If present, existing notes from "BackOffice" are deleted, and a new note is inserted into `CustomerNotes` table. |
ActionPriceList | SapB1SLCustomer (via an extra field or custom property) | If `DefaultActionPriceListCode` setting is configured and `ActionPriceList` is not set, it's retrieved from `PriceListsContext`. If price deduplication is enabled, it may be migrated to a new ID. |
InternalCode | Existing App4Sales Customer.InternalCode | If syncing from App4Sales and `customer.InternalCode` is empty, it retains the existing value from the database. |
ExtraData | Existing App4Sales Customer.ExtraData | If syncing from App4Sales and `customer.ExtraData` is empty, it retains the existing value from the database. |
ItemFilter | Derived from `extraCustomerData.UnknownElements` (prefixed with "ItemFilter_") or existing App4Sales Customer.ItemFilter | Item filters can be extracted from external CSV data. If `CustomerItemFilter` setting is enabled and the update is not from the updater, the existing `ItemFilter` from the database is retained. |
FreeFields | Derived from `customer.FreeFieldList` or `extraCustomerData.UnknownElements` (prefixed with "FreeField_") | If `customer.FreeFieldList` is not empty, it's serialized to XML. `UnknownElements` from extra data are converted to `CustomerFreeField` objects. |
Addresses
App4Sales Field | Source Field (API/Excel/DB) | Logic/Notes |
IsMainAddress | SapB1SLCustomer.Address, SapB1SLAddress.Name | Set if `SapB1SLCustomer.Address` (main address name) matches `SapB1SLAddress.Name`. When sending to SAP, the `sapAddress.Name` is constructed using Customer Name and a readable address type. |
AddressLine.Street | SapB1SLAddress.Street | Direct mapping. |
AddressLine.Number | SapB1SLAddress.Number | Direct mapping. |
AddressLine1 | SapB1SLAddress.Street, SapB1SLAddress.Number | Concatenation of `Street` and `Number` when converting from SAP. Parsed into `AddressLine.Street`, `AddressLine.Number`, `AddressLine.Addition` if `AddressLine` is null. Formatted from `AddressLine` components when sending to SAP. |
City | SapB1SLAddress.City | Direct mapping. |
Country | SapB1SLAddress.Country | Direct mapping. If empty, derived from `Iso2`. |
Iso2 | SapB1SLAddress.Country | Converted from `SapB1SLAddress.Country` using `CultureHelper.GetCountryCodeByCountry`. Mapped using `MappedCountriesContext`. When sending to SAP, mapped from `address.Iso2`. If empty, derived from `Country`. |
State | SapB1SLAddress.State | Direct mapping. |
PostCode | SapB1SLAddress.ZipCode | Direct mapping. |
AddressType | SapB1SLAddress.AddressType | Mapped from SAP B1SL to App4Sales:
When sending to SAP B1SL:
If `AddressType` is 'Invoice' from SAP, a 'Visit' address is created as a copy. Automatically creates corresponding 'Visit' or 'Delivery' addresses if only one exists. Defaults to 'Visit' if empty. |
SapB1SLCustomer.Email | Inherits customer's email if address email is empty and `IsMainAddress` is true. Also inherits from other customer addresses or contact persons if no email is found. Trimmed and invalid XML characters removed. | |
ExternalId | SapB1SLAddress.Name, SapB1SLAddress.RowNumber | A serialized string of `AddressName` and `RowNumber` from SAP. Used for updating existing addresses in SAP. |
Contacts
App4Sales Field | Source Field (API/Excel/DB) | Logic/Notes |
ContactId | SapB1SLContactPerson.ContactId | Mapped from `SapB1SLContactPerson.ContactId`. If `UpdaterHelper.IsUpdater` is true and `ContactId` is empty, a hash code-based `ContactId` is generated. |
IsMainContactPerson | SapB1SLCustomer.ContactPersonName, SapB1SLContactPerson.FullName | Set if `SapB1SLCustomer.ContactPersonName` matches `SapB1SLContactPerson.FullName`. If a `MainContactPerson` is provided from App4Sales, it's marked as `true`. If no main contact is specified and multiple contacts exist, the first contact is marked as main. |
SapB1SLContactPerson.Email | Direct mapping. Trimmed and invalid XML characters removed. | |
FirstName | SapB1SLContactPerson.FirstName | Direct mapping. If individual name components are empty but `FullName` exists, `FullName` is parsed into `FirstName`, `MiddleName`, `LastName`. |
MiddleName | SapB1SLContactPerson.MiddleName | Direct mapping. If individual name components are empty but `FullName` exists, `FullName` is parsed into `FirstName`, `MiddleName`, `LastName`. |
LastName | SapB1SLContactPerson.LastName | Direct mapping. If individual name components are empty but `FullName` exists, `FullName` is parsed into `FirstName`, `MiddleName`, `LastName`. |
FullName | SapB1SLContactPerson.FirstName, MiddleName, LastName | Constructed from `FirstName`, `MiddleName`, `LastName`. If `FullName` is empty but individual name components exist, it's constructed. |
MobileNumber | SapB1SLContactPerson.MobilePhone | Direct mapping. |
CustomerGuid | Derived | Inherits `CustomerGuid` from the parent customer. |
DynamicFreeFields | App4Sales ContactPerson.DynamicFreeFields (XML payload) | Converted to extra fields based on `CustomCustomerFieldsMapping` setting. |
Gender | App4Sales ContactPerson.Gender | Direct mapping to SAP B1SL. |
Item Class Value Discounts
App4Sales Field | Source Field (API/Excel/DB) | Logic/Notes |
discount | App4Sales Customer.DiscountsPerItemCLassValue.discount | Direct mapping. Only discounts with a value greater than 0 are stored. |
CustomerGuid | Derived | Inherits `CustomerGuid` from the parent customer. |
Extra Data & Free Fields
Customer records can be enriched with extra data originating from external FTP/CSV files.
Property Overrides: If the `extraCustomerData` (from FTP/CSV) contains properties with names matching existing `Customer` object properties, these values will overwrite the corresponding `Customer` properties.
Free Fields: Any unknown elements in `extraCustomerData` that are prefixed with "FreeField_" will be extracted and stored as `CustomerFreeField` objects in `customer.FreeFieldList`. If `customer.FreeFieldList` contains data, it will be serialized to XML and stored in `customer.FreeFields`.
Dynamic Free Fields: The `DynamicFreeFields` (an XML payload) on the App4Sales `Customer` object can be mapped to SAP B1SL properties or User-Defined Fields (UDFs) on `SapB1SLCustomer`, `SapB1SLContactPerson`, or `SapB1SLAddress` based on the `CustomCustomerFieldsMapping` connector setting.
Discounts & Dashboards
Item Class Value Discounts: Discounts defined per item class for a customer (`DiscountsPerItemCLassValue`) are stored in the App4Sales database. Only discounts with a value greater than 0 are processed.
Customer Dashboards: A Base64-encoded string representing a customer-specific dashboard layout can be provided. This string is decoded and saved separately using the `CustomerDataManager`. After saving, the `CustomerDashboard` field on the customer object is cleared to optimize memory usage.
Customer Notes: If `customer.CustomerNote` is present, any existing "BackOffice" notes for that customer are deleted from the `CustomerNotes` table, and the new note is inserted.
Special Logic & Filters
Invalid Customer Handling: Customers with empty `CustomerName` or `CustomerCode` are logged as warnings and skipped.
OData Filtering: Customer retrieval from SAP B1SL uses OData filters. These can include a `UpdateDate` filter based on the last synchronization time and an additional filter specified in the `FilterCustomersUsingOData` setting.
Customer Type Filtering: Only customers matching the `CardType` specified in `SynchronizeOnlyСustomersTypes` setting are processed (defaults to `Customer` type).
Exclusion of Invalid Customers: If `ExcludeInvalidCustomersFromSync` setting is enabled, customers marked as `Valid = "False"` or `Frozen = "True"` in SAP B1SL are excluded from synchronization.
Batch Processing: Customers are processed in batches of 100 to optimize performance.
Scripting Hooks: The `BeforeUpdateCustomers` and `UpdateCustomers` scripts are executed before and after processing customer batches, respectively, allowing for custom logic injection.
GUID Management: Existing `CustomerGuid` values are prioritized, either from a local cache or the Portal API. For "updater" scenarios, `CustomerGuid` values of previously inactive customers are reused. If no GUID is found, a new one is generated.
Address Standardization: Addresses undergo normalization, including country code mapping, email inheritance, and parsing of address lines. Logic exists to ensure both "Visit" and "Delivery" addresses are present, even if they need to be duplicated from an existing address. Invalid XML characters are removed from address fields.
Contact Person Name Parsing: If a contact person's individual name fields (FirstName, MiddleName, LastName) are empty but `FullName` is present, the `FullName` is parsed to populate the individual fields. Conversely, `FullName` is constructed from individual fields if it's empty.
Contact ID Generation: In "updater" mode, if a contact person's `ContactId` is empty, a hash code-based ID is generated.
Related Settings & Prerequisites
`DoNextSyncAsFullSync`: Boolean. If true, the next synchronization will be a full sync, ignoring `UpdateDate` filters.
`FilterCustomersUsingOData`: String. An additional OData filter applied when retrieving customers from SAP B1SL.
`SynchronizeOnlyСustomersTypes`: String (comma-separated). Specifies which SAP B1SL customer card types to synchronize (e.g., "Customer", "Lead"). Defaults to "Customer".
`ExcludeInvalidCustomersFromSync`: Boolean. If true, customers marked as invalid or frozen in SAP B1SL will not be synchronized to App4Sales.
`UseVatLiableForCustomerVatLiable`: Boolean. Influences how `VatLiable` is determined from SAP B1SL.
`UseBillAddressFederalTaxIDAsCustomerVATLiableIndicator`: Boolean. Influences how `VatLiable` is determined from SAP B1SL based on address FederalTaxID.
`UseCustomerDiscount`: Boolean. If true, `DiscountPercent` from SAP B1SL is mapped to `Customer.Discount`, otherwise `Customer.Discount` is 0.
`DefaultActionPriceListCode`: String. Specifies a default action price list to apply to customers if none is set.
`EnablePriceDeduplication`: Boolean. Enables logic to migrate price list IDs if deduplication has occurred.
`SendVatNumberInFederalTaxIDField`: Boolean. If true, `VatCode` is sent to SAP as `FederalTaxID`, otherwise as `VatNumber`.
`CustomerSeries`: Integer. Specifies the series to use for new customers created in SAP.
`SendEmptyCardCode`: Boolean. If true, an empty customer code is sent to SAP for new customer creation.
`NewCustomersCodePrefix`: String. Prefix used when generating new customer codes.
`NewCustomerCodeNumber`: Integer. Starting number for new customer codes.
`CustomerClassificationProperty`: String. Defines the SAP B1SL property (direct or UDF) to use for `CustomerClassification`.
`CustomCustomerFieldsMapping`: JSON string. Defines mappings for `DynamicFreeFields` to SAP B1SL properties/UDFs on Customer, Contact Person, or Address.
`CustomerItemFilter`: Boolean. Enables processing of item filters from extra data and retention of existing item filters.
Known Limitations
SAP B1SL does not natively support "Visit" addresses. The connector creates a copy of the "Invoice" address and labels it as "Visit" to ensure compatibility with App4Sales.
The `LanguageCode` mapping to SAP B1SL is noted as a TODO in the code, indicating potential future enhancements or complexities in determining the correct integer values for SAP's language codes.