# Mapeo de campos Shopify → SAP Business One Referencia de cómo se arma cada documento SAP a partir del JSON de una orden de Shopify. Los constructores viven en `domain/*.py`. ## RUT del cliente (`domain/business_partner.py`) | Prioridad | Fuente | Detalle | |---|---|---| | 1 | `order.note_attributes` | Se busca una entrada cuyo `name` contenga "rut" (sin distinguir mayúsculas). Se valida el `value` con el formato y el dígito verificador (módulo 11). | | 2 (respaldo) | Metafields del `customer` (Shopify) | Solo si no apareció en `note_attributes`. Se recorre cada metafield buscando un `value` con formato de RUT válido. | - `normalizar_rut`: quita puntos/guión, mayúsculas (ej. `"123456785"`) — uso interno para validar. - `formatear_rut`: formato que usa SAP, `"NNNNNNNN-D"` (ej. `"12345678-5"`) — se usa en `FederalTaxID`, `CardCode` y el filtro de búsqueda en SAP. ## Resolución de CardCode (`resolver_card_code`) | Caso | CardCode usado | |---|---| | Sin RUT válido | `sap.generic_card_code` (config de la tienda) | | RUT válido, existe `BusinessPartner` con ese `FederalTaxID` | El `CardCode` de ese BP | | RUT válido, no existe | Se **crea** un BP nuevo, `CardCode = "C" + rut_sap` | | RUT válido, pero falla la creación del BP | Cae a `sap.generic_card_code` | En los tres primeros casos con RUT válido, `rut_sap` se guarda igual (para `FederalTaxID` del Invoice), aunque termine usándose el `CardCode` genérico. --- ## `BusinessPartners` (creación, `domain/business_partner.py`) | Campo SAP | Origen Shopify | Nota | |---|---|---| | `CardCode` | — | `"C" + rut_sap` | | `CardName` | `billing_address` (nombre completo), si no `customer` | fallback `"Cliente Shopify"` | | `CardType` | — | fijo `"cCustomer"` | | `GroupCode` | — | fijo `100` ("Clientes", mismo grupo que el genérico) | | `FederalTaxID` | — | `rut_sap` | | `Phone1` | `billing_address.phone`, si no `customer.phone` | | | `EmailAddress` | `order.email`, si no `order.contact_email` | | ### `BPAddresses` (dos entradas) | Campo SAP | Origen Shopify | Nota | |---|---|---| | `AddressName` | — | `"BillTo"` / `"ShipTo"` | | `AddressType` | — | `bo_BillTo` / `bo_ShipTo` | | `Street` | `address1` | | | `Block` | `address2` | | | `City` | `city` | | | `ZipCode` | `zip` | | | `County` | `province` | texto libre, no se traduce a código de región | | `Country` | `country_code` | solo si es `"CL"` | | `TaxCode` | — | fijo `"IVA"`, solo en la dirección `bo_ShipTo` | `bo_BillTo` sale de `order.billing_address`; `bo_ShipTo` de `order.shipping_address` (si no hay, usa `billing_address`). --- ## `Invoices` (factura de reserva, `domain/invoice.py`) | Campo SAP | Origen Shopify | Nota | |---|---|---| | `CardCode` | — | resuelto (ver arriba) | | `CardName` | `billing_address` (nombre completo), si no `customer` | fallback `"Cliente Shopify"` | | `DocDate` / `DocDueDate` | `created_at` (solo fecha) | mismo valor en ambos | | `Series` | — | `sap.numbering_series.INVOICE` (config tienda) | | `DocType` | — | fijo `"dDocument_Items"` | | `DocCurrency` | — | fijo `"CLP"` | | `ReserveInvoice` | — | fijo `"tYES"` (factura de reserva, no releva stock al crearse) | | `NumAtCard` | `order.name` | ej. `"#7863"`, para ubicarla fácil | | `FederalTaxID` | `rut_sap` | solo si hay RUT válido; sobrescribe el del CardCode genérico | | `U_ShopifyOrderId` | `order.id` | UDF | | `U_ShopifyStore` | — | `store_code` de la config | ### `AddressExtension` (prefijo `BillTo`/`ShipTo`) | Campo SAP | Origen Shopify | Nota | |---|---|---| | `{p}Street` | `address1` | | | `{p}Block` | `address2` | | | `{p}Building` | `phone` | reutilizado para guardar el teléfono | | `{p}City` | `city` | | | `{p}ZipCode` | `zip` | | | `{p}County` | `province` | | | `{p}Country` | `country_code` | solo si es `"CL"` | `BillTo*` sale de `billing_address`, `ShipTo*` de `shipping_address`. SAP calcula `Address`/`Address2` (texto plano) solo a partir de estos campos — no se arman a mano. ### `DocumentLines` (una por `line_item` con `sku`) | Campo SAP | Origen Shopify | Nota | |---|---|---| | `ItemCode` | `sku` | | | `Quantity` | `quantity` | | | `WarehouseCode` | — | `sap.default_warehouse_invoice` (bodega virtual, fija por tienda) | | `PriceAfterVAT` | `price` menos descuento, ver nota | precio unitario, IVA incluido | | `TaxCode` | — | fijo `"IVA"` (hardcode, sin lista de precios) | **Cálculo de `PriceAfterVAT`**: `item.price` es el precio unitario **antes** de descuento. El descuento real está en `discount_allocations` (el campo `total_discount` no siempre lo refleja). Fórmula: ``` PriceAfterVAT = (price × quantity − Σ discount_allocations.amount) / quantity ``` --- ## `IncomingPayments` (`domain/payment.py`) | Campo SAP | Origen | Nota | |---|---|---| | `CardCode` | `invoice.CardCode` | mismo cliente que la Invoice | | `CardName` | `invoice.CardName` | | | `DocType` | — | fijo `"rCustomer"` | | `DocDate` | `invoice.DocDate` | misma fecha que la Invoice | | `Series` | — | `sap.numbering_series.INCOMING_PAYMENT` | | `DocCurrency` | — | fijo `"CLP"` | | `PaymentCreditCards[]` | una línea por transacción efectiva con `type = credit_card` | `CreditCard` = `sap.payment_methods[gateway].credit_card`; `CreditCardNumber` fijo `"1234"`; `CardValidUntil` fijo `"2030-12-31"`; `VoucherNum` = `order.name` (máx. 20); `CreditSum` = `transaction.amount`; `CreditCur` `"CLP"`; `CreditType` `"cr_Regular"`; `NumOfPayments` y `SplitPayments` del config; `FirstPaymentDue` = `invoice.DocDate` | | `TransferAccount` / `TransferSum` / `TransferDate` / `TransferReference` | solo si hay transacciones con `type = transfer` (reversso) | cuenta de `sap.payment_methods[gateway].transfer_account`; `TransferSum` = suma de esas transacciones; `TransferReference` = `order.name` | | `Remarks` | gateways de las transacciones efectivas | join con `" + "`, sin repetir. **No** se usa `payment_gateway_names` (lista también los intentos fallidos) | | `PaymentInvoices[0].DocEntry` | `invoice.DocEntry` | | | `PaymentInvoices[0].InvoiceType` | — | fijo `"it_Invoice"` | | `PaymentInvoices[0].SumApplied` | total de la Invoice | debe igualar la suma de las transacciones efectivas; si no, `error` y no se crea el pago. Ver `docs/casos_especiales.md` | --- ## `InventoryTransferRequests` (`domain/inventory_transfer.py`) Una por cada `fulfillment` de la orden (tienda o CD). | Campo SAP | Origen | Nota | |---|---|---| | `CardCode` / `CardName` | `invoice.CardCode` / `invoice.CardName` | | | `Comments` | `order.name` | | | `DocDate` / `DueDate` | `order.created_at` (solo fecha) | | | `Series` | — | `sap.numbering_series.INVENTORY_TRANSFER_REQUEST` | | `FromWarehouse` | `sap.locations[location_id].warehouse_code` | bodega real de ese `location_id` | | `ToWarehouse` | — | `sap.default_warehouse_invoice` (misma bodega virtual del Invoice) | | `U_ShopifyOrderId` / `U_ShopifyStore` | `order.id` / `store_code` | | ### `StockTransferLines` (una por `line_item` del fulfillment) | Campo SAP | Origen | Nota | |---|---|---| | `ItemCode` | `sku` del line_item del fulfillment | no toda la orden, solo lo de ese fulfillment | | `Quantity` | `quantity` | | | `FromWarehouseCode` | igual a `FromWarehouse` | | | `WarehouseCode` | igual a `ToWarehouse` | | `location_id → {is_store, warehouse_code}` sale de `config/stores/.json → sap.locations`. --- ## `StockTransfers` (`domain/stock_transfer.py`, solo si `is_store: true`) Se arma a partir de la **ITR ya creada en SAP** (se relee completa para tener `LineNum`). | Campo SAP | Origen | Nota | |---|---|---| | `CardCode` / `CardName` / `Comments` | copiados de la ITR | | | `DocDate` | `itr.DocDate` | | | `FromWarehouse` / `ToWarehouse` | copiados de la ITR | mismo sentido que la solicitud | | `Series` | — | `sap.numbering_series.STOCK_TRANSFER` | | `U_ShopifyOrderId` / `U_ShopifyStore` | copiados de la ITR | | ### `StockTransferLines` (una por línea de la ITR) | Campo SAP | Origen | Nota | |---|---|---| | `ItemCode` / `Quantity` / `FromWarehouseCode` / `WarehouseCode` | copiados de la línea de la ITR | | | `BaseType` | — | `1250000001` (`DocObjectCode` de `InventoryTransferRequests`) | | `BaseEntry` | `itr.DocEntry` | | | `BaseLine` | `line.LineNum` de la ITR | | --- ## `DeliveryNotes` (`domain/delivery.py`, solo si `is_store: true`) Se arma a partir de la **Invoice** (base) y el **StockTransfer** ya creado (para saber qué se transfirió realmente en este fulfillment). | Campo SAP | Origen | Nota | |---|---|---| | `CardCode` / `CardName` | `invoice.CardCode` / `invoice.CardName` | | | `Comments` | `invoice.NumAtCard` | | | `DocDate` | `invoice.DocDate` | | | `Series` | — | `sap.numbering_series.DELIVERY` | | `U_ShopifyOrderId` / `U_ShopifyStore` | copiados de la Invoice | | ### `DocumentLines` (una por línea del StockTransfer, emparejada con la Invoice por `ItemCode`) | Campo SAP | Origen | Nota | |---|---|---| | `ItemCode` / `Quantity` | línea del StockTransfer | solo lo de este fulfillment, no toda la orden | | `WarehouseCode` | línea de la Invoice con el mismo `ItemCode` | | | `BaseType` | — | `13` (ObjType clásico de Sales Invoice, `oInvoices`) | | `BaseEntry` | `invoice.DocEntry` | | | `BaseLine` | `LineNum` de la línea de la Invoice | | --- ## Referencias cruzadas en la Invoice (`agregar_referencia_documento`) Cada vez que se crea ITR, StockTransfer o Delivery, se hace `PATCH Invoices` agregando una entrada a `DocumentReferences` (sin pisar las que ya existan): | Documento creado | `RefObjType` | |---|---| | `InventoryTransferRequests` | `rot_InventoryTransferRequest` | | `StockTransfers` | `rot_InventoryTransfer` | | `DeliveryNotes` | `rot_DeliveryNotes` | --- ## Campos fijos / hardcode (pendientes de hacer configurables si hace falta) | Campo | Valor | Dónde | |---|---|---| | `TaxCode` (líneas de Invoice) | `"IVA"` | `domain/invoice.py` | | `TaxCode` (dirección ShipTo del BP) | `"IVA"` | `domain/business_partner.py` | | `DocCurrency` | `"CLP"` | Invoice, Payment | | Datos de tarjeta (Payment) | últimos dígitos `"1234"`, vencimiento `"2030-12-31"` | `domain/payment.py` (provisorios) | | `GroupCode` (BusinessPartner) | `100` | `domain/business_partner.py` | | `BaseType` ITR→StockTransfer | `1250000001` | `domain/stock_transfer.py` | | `BaseType` Invoice→Delivery | `13` | `domain/delivery.py` |