Análisis y Mapeo de Órdenes de Compra Zoho
Resumen Ejecutivo
Este documento proporciona un análisis detallado de la estructura de datos recibida del webhook de Zoho Books para órdenes de compra y su mapeo hacia los modelos PurchaseHeaderImp y PurchaseDetailImp de DocuCenter.
Fecha de Análisis: 2025-10-02
Origen de Datos: Log del endpoint /api/acicloud/create_purchase_order_zoho
Tipo de Webhook: Zoho Books Purchase Order / Bill
Estructura de Datos de Zoho
Datos del Header (Cabecera de la Orden)
El webhook de Zoho envía los siguientes campos principales para la cabecera:
{
"bill_id": "6088114000000487426",
"bill_number": "88931",
"reference_number": "22831",
"vendor_id": "6088114000000487410",
"vendor_name": "Medtrition",
"date": "2025-10-02",
"due_date": "2025-10-02",
"currency_code": "USD",
"currency_symbol": "$",
"exchange_rate": "1.0",
"sub_total": "60.0",
"tax_total": "0.0",
"total": "60.0",
"balance": "60.0",
"adjustment": "0.0",
"adjustment_description": "Ajuste",
"status": "open",
"payment_terms": "0",
"payment_made": "0.0",
"discount_amount": "0.0",
"is_discount_before_tax": "true"
}
Datos de Line Items (Detalles de la Orden)
Los detalles vienen en un array JSON dentro del campo line_items:
[
{
"line_item_id": "6088114000000487433",
"item_id": "6088114000000463033",
"sku": "V504413",
"name": "DIENAT G Vainilla",
"description": "",
"quantity": 20,
"unit": "und",
"rate": 3,
"item_total": 60,
"account_id": "6088114000000034001",
"account_name": "Activo de inventario",
"location_id": "6088114000000091182",
"location_name": "Oficina principal",
"item_type": "inventory",
"tax_id": "",
"tax_name": "",
"tax_percentage": 0,
"discount": 0,
"discount_amount": 0,
"line_item_taxes": []
}
]
Estructura con Impuestos (ITBMS)
Para órdenes con impuestos, la estructura incluye campos adicionales:
Header con impuestos:
{
"sub_total": "5.0",
"tax_total": "0.35",
"total": "5.35",
"discount_amount": "0.0",
"is_discount_before_tax": "true"
}
Line item con impuestos:
{
"sku": "11525",
"name": "PRO SOURCE NO CARB",
"quantity": 1,
"rate": 5,
"item_total": 5,
"tax_id": "6088114000000466001",
"tax_name": "ITBMS",
"tax_percentage": 7,
"line_item_taxes": [
{
"tax_amount": 0.35,
"tax_name": "ITBMS (7%)",
"tax_amount_formatted": "$0.35",
"tax_id": "6088114000000466001"
}
]
}
Mapeo hacia Modelos DocuCenter
PurchaseHeaderImp - Mapeo de Campos
| Campo DocuCenter | Campo Zoho | Transformación | Notas |
|---|---|---|---|
PurchaseNumber |
bill_number |
Directo | Número de factura/orden |
VendorID |
vendor_id |
Directo | ID único del proveedor en Zoho |
VendorName |
vendor_name |
Directo | Nombre del proveedor |
Date |
date |
Carbon::parse() |
Convertir a formato fecha |
DueDate |
due_date |
Carbon::parse() |
Fecha de vencimiento |
Subtotal |
sub_total |
(float) |
Subtotal sin impuestos |
Net_due |
total |
(float) |
Total de la orden (incluye impuestos) |
AP_Account |
account_name |
Tomar del primer line_item | Cuenta contable |
ErrorPT |
tax_info |
buildTaxInfo() |
Información de impuestos en JSON |
Export_date |
- | now() |
Fecha de importación |
Enviado |
- | 0 |
Estado inicial |
Error |
- | 0 |
Sin errores inicialmente |
PurchaseDetailImp - Mapeo de Campos
| Campo DocuCenter | Campo Zoho Line Item | Transformación | Notas |
|---|---|---|---|
Item_id |
sku o item_id |
Priorizar sku |
Código del producto |
Description |
name |
Directo | Nombre/descripción del producto |
GL_Acct |
account_name |
Directo | Cuenta contable del item |
Quantity |
quantity |
(float) |
Cantidad |
Unit_Price |
rate |
(float) |
Precio unitario |
Net_line |
item_total |
(float) |
Total de la línea |
JobID |
tax_info |
buildItemTaxInfo() |
Información de impuestos del item |
JobPhaseID |
tax_id |
Directo | ID del impuesto en Zoho |
Sequential |
- | Auto-incremental | Número secuencial por orden |
Implementación Sugerida
1. Clase de Transformación
<?php
namespace App\Services\Zoho;
use Carbon\Carbon;
use App\Models\PurchaseHeaderImp;
use App\Models\PurchaseDetailImp;
class ZohoPurchaseOrderTransformer
{
public function transformHeader(array $zohoData): array
{
return [
'PurchaseNumber' => $zohoData['bill_number'] ?? '',
'VendorID' => $zohoData['vendor_id'] ?? '',
'VendorName' => $zohoData['vendor_name'] ?? '',
'Date' => Carbon::parse($zohoData['date'])->format('Y-m-d H:i:s'),
'DueDate' => Carbon::parse($zohoData['due_date'])->format('Y-m-d H:i:s'),
'Subtotal' => (float) ($zohoData['sub_total'] ?? 0),
'Net_due' => (float) ($zohoData['total'] ?? 0),
'AP_Account' => $this->getAccountFromLineItems($zohoData),
'Export_date' => now(),
'Enviado' => 0,
'Error' => 0,
];
}
public function transformLineItems(array $lineItems, int $transactionId): array
{
$details = [];
$sequential = 1;
foreach ($lineItems as $item) {
$details[] = [
'TransactionID' => $transactionId,
'Item_id' => $item['sku'] ?: $item['item_id'],
'Description' => $item['name'],
'GL_Acct' => $item['account_name'],
'Quantity' => (float) $item['quantity'],
'Unit_Price' => (float) $item['rate'],
'Net_line' => (float) $item['item_total'],
'Sequential' => $sequential++,
];
}
return $details;
}
private function getAccountFromLineItems(array $zohoData): string
{
$lineItems = json_decode($zohoData['line_items'] ?? '[]', true);
return $lineItems[0]['account_name'] ?? '';
}
}
2. Servicio de Importación
<?php
namespace App\Services\Zoho;
use DB;
use Illuminate\Support\Facades\Log;
use App\Models\PurchaseHeaderImp;
use App\Models\PurchaseDetailImp;
class ZohoPurchaseOrderImporter
{
protected $transformer;
public function __construct(ZohoPurchaseOrderTransformer $transformer)
{
$this->transformer = $transformer;
}
public function importPurchaseOrder(array $zohoData, int $organizationId): array
{
try {
DB::beginTransaction();
// Verificar si ya existe
$existingOrder = PurchaseHeaderImp::where('PurchaseNumber', $zohoData['bill_number'])
->where('VendorID', $zohoData['vendor_id'])
->first();
if ($existingOrder) {
return [
'success' => false,
'message' => 'Orden de compra ya existe',
'transaction_id' => $existingOrder->TransactionID
];
}
// Crear header
$headerData = $this->transformer->transformHeader($zohoData);
$headerData['ID_compania'] = $organizationId;
$header = PurchaseHeaderImp::create($headerData);
// Crear detalles
$lineItems = json_decode($zohoData['line_items'], true);
$detailsData = $this->transformer->transformLineItems($lineItems, $header->TransactionID);
foreach ($detailsData as $detail) {
$detail['ID_compania'] = $organizationId;
PurchaseDetailImp::create($detail);
}
DB::commit();
Log::info('Orden de compra Zoho importada exitosamente', [
'transaction_id' => $header->TransactionID,
'bill_number' => $zohoData['bill_number'],
'vendor_name' => $zohoData['vendor_name'],
'total' => $zohoData['total']
]);
return [
'success' => true,
'message' => 'Orden de compra importada exitosamente',
'transaction_id' => $header->TransactionID
];
} catch (\Exception $e) {
DB::rollBack();
Log::error('Error importando orden de compra Zoho', [
'error' => $e->getMessage(),
'bill_number' => $zohoData['bill_number'] ?? 'N/A',
'vendor_name' => $zohoData['vendor_name'] ?? 'N/A'
]);
return [
'success' => false,
'message' => 'Error importando orden de compra: ' . $e->getMessage()
];
}
}
}
3. Actualización del Controller
// En ACIcloudController.php - método createPurchaseOrderZoho()
public function createPurchaseOrderZoho(Request $request)
{
// ... logging existente ...
// Agregar procesamiento real
$importer = new ZohoPurchaseOrderImporter(new ZohoPurchaseOrderTransformer());
// Obtener organización del usuario autenticado
$organizationId = auth()->user()->getORG();
$result = $importer->importPurchaseOrder($request->all(), $organizationId);
return response()->json($result);
}
Campos Adicionales Disponibles en Zoho
Datos del Proveedor
billing_address: Dirección de facturación (JSON)vendor_credits: Créditos del proveedor
Datos Financieros
payments: Array de pagos realizadospayment_made: Monto total pagadobalance: Saldo pendientediscount: Descuentos aplicadostax_total: Total de impuestos
Metadatos
custom_fields: Campos personalizadosreference_number: Número de referenciastatus: Estado de la orden (open, closed, etc.)payment_terms: Términos de pago
Consideraciones Técnicas
1. Validaciones Requeridas
- Verificar que el
vendor_idexista en el sistema - Validar que los
item_id/skucorrespondan a productos válidos - Confirmar que las cuentas contables existan
2. Manejo de Errores
- Duplicación de órdenes (por
bill_number+vendor_id) - Productos no encontrados
- Cuentas contables inválidas
- Errores de conexión a base de datos
3. Logging y Auditoría
- Registrar todas las importaciones exitosas
- Documentar errores con contexto completo
- Mantener trazabilidad de cambios
4. Performance
- Procesar en lotes para múltiples line items
- Usar transacciones para consistencia
- Cachear validaciones frecuentes
Casos de Uso Identificados
Escenario 1: Orden Simple
- 1 proveedor, múltiples productos
- Moneda USD, sin descuentos
- Estado "open"
Escenario 2: Orden con Descuentos
- Descuentos por línea y/o totales
- Cálculos de impuestos
Escenario 3: Orden Multi-Moneda
- Tipos de cambio
- Conversiones automáticas
Próximos Pasos
- Implementar clases de transformación
- Crear validaciones específicas
- Agregar tests unitarios
- Documentar API endpoints
- Configurar monitoreo de errores
Testing
Datos de Prueba del Log Analizado
{
"vendor_name": "Medtrition",
"bill_number": "88931",
"total": "60.0",
"line_items": [
{
"sku": "V504413",
"name": "DIENAT G Vainilla",
"quantity": 20,
"rate": 3,
"item_total": 60
}
]
}
Este análisis proporciona la base completa para implementar la integración de órdenes de compra desde Zoho Books hacia DocuCenter.