Bulk Generate Vouchers Documentation

Overview

The bulkGenerateVouchers function is designed to automatically generate invoices for multiple bookings based on the billTo company's bulk billing configuration. Instead of creating separate invoices for each booking, this function intelligently groups bookings and generates consolidated invoices according to predefined business rules.

High-Level Process Flow

  1. Input Validation: Accept booking UUIDs or booking numbers with optional invoice type
  2. Booking Retrieval: Fetch bookings with cost items and related data
  3. Grouping by BillTo: Group bookings by their billToUuid (billing company)
  4. Further Grouping: Within each billTo group, apply secondary grouping based on bulkBill.groupInvoiceBy
  5. Invoice Generation: Create vouchers for each final group using appropriate templates and invoice types

BillTo Company bulkBill Settings

The bulkBill object in the company record controls how invoices are generated. It contains three key parameters:

Required Parameters

1. invoiceTemplate (String)

  • Purpose: Specifies which document creator template to use for generating the invoice
  • Default: Falls back to 'INVOICE' if not specified
  • Validation: Must exist in the documentCreatorTemplate table for the base company
  • Example: 'INVOICE', 'INVOICE_SOME_CUSTOMER'

2. invoiceType (String)

  • Purpose: Defines the transaction category for the voucher
  • Required: Yes (when no input invoiceType is provided)
  • Values: Common types include 'customerInvoice', 'supplierPayment', 'customsPayment', etc.
  • Validation: Must be supported by the booking's flow configuration

3. groupInvoiceBy (String)

  • Purpose: Lodash path string that determines how to group bookings for consolidation
  • Required:  No (defaults to 'uuid' if not specified)
  • Usage: Uses lodash.get(booking, groupInvoiceBy) to extract grouping key
  • Examples:
    • 'uuid' - One invoice per booking (default behavior)
    • 'job.uuid' - Group by job
    • 'details.shipperConsignee' - Group by Shipper/Consignee (note this variable may be populated by a plugin)
    • 'jobs[0].trips[0].to.areaCode'- by first trip areaCode
    • 'details.vesselName' - By Vessel Name
    • 'billToUuid' - All bookings in one invoice

Example Configuration

{
  "bulkBill": {
    "invoiceTemplate": "INVOICE_CONSOLIDATED",
    "invoiceType": "customerInvoice",
    "groupInvoiceBy": "billToUuid"
  }
}

Invoice Type Priority

The function determines invoice type using the following priority order:

  1. Input Parameter (Highest Priority): invoiceType passed to the function
  2. Company Configuration: billTo.bulkBill.invoiceType
  3. Flow Logic (Fallback): Uses app.svc.flow.getAccrecVoucherTypeMap() to determine from booking flow (Note: This fallback is rarely used since invoiceType or company config is typically required)

Grouping Logic

Primary Grouping: By BillTo Company

All bookings are first grouped by their billToUuid, ensuring each billing company gets separate invoice processing.

Secondary Grouping: By groupInvoiceBy Field

Within each billTo group, bookings are further grouped using the groupInvoiceBy path:

const groupKey = get(booking, billTo.bulkBill?.groupInvoiceBy || 'uuid')

Default Behavior

If no bulkBill settings exist, the system defaults to:

  • Throws an error if no invoiceType parameter is provided (company must have bulkBill.invoiceType configured or caller must provide invoiceType)
  • Uses 'INVOICE' as the default template

    Groups by 'uuid' (one invoice per booking)


Error Handling

The function validates several conditions and throws errors for:

  • Missing booking UUIDs/numbers
  • Non-existent bookings
  • Bookings without cost items
  • Invalid invoice types for specific bookings
  • Missing bulkBill settings (when no invoiceType provided)
  • Missing document creator templates
  • Missing billing addresses
  • Existing active vouchers of the same invoice type (vouchers with status other than DELETED or VOIDED)
    • Duplicate Invoice Prevention

      Before generating vouchers, the function checks for existing vouchers associated with the bookings. If any voucher with the same invoiceType exists and has a status other than DELETED or VOIDED, the operation is blocked.


      Blocked Statuses:


      DRAFT

      SUBMITTED

      APPROVED

      PAID

      PARTIALLY_PAID

      This prevents duplicate invoicing for the same booking and invoice type combination.


Use Cases

Scenario 1: Standard Individual Invoicing

"bulkBill": {
  "invoiceTemplate": "INVOICE",
  "invoiceType": "customerInvoice",
  "groupInvoiceBy": "uuid"
}

Result: One invoice per booking (traditional approach)

Scenario 2: Consolidated by ShipperConsignee

"bulkBill": {
  "invoiceTemplate": "INVOICE_SPECIAL_CUSTOMER",
  "invoiceType": "transportInvoiceTemplate",
  "groupInvoiceBy": "details.shipperConsignee"
}

Result: All bookings sharing the same shipperConsignee get one consolidated invoice

Scenario 3: Group by Customer AreaCode

"bulkBill": {
  "invoiceTemplate": "INVOICE_MONTHLY",
  "invoiceType": "customerInvoice",
  "groupInvoiceBy": "jobs[0].trips[0].to.areaCode"
}

Result: All bookings for the to areaCode for the first job and trip get one consolidated invoice

Generated Voucher Properties

Each generated voucher includes:

  • Vendor: bookings[0].ownerUuid (service provider)
  • Customer: billTo.uuid (billing company)
  • Address: Billing address of the billTo company
  • Payment Type: Based on billTo.debtorTerm (Credit if > 0, Cash if = 0)
  • Currency: From the first booking's cost items
  • Template: As specified in bulkBill.invoiceTemplate

Integration Points

  • Document Creator: Uses specified templates for PDF generation
  • Flow System: Validates invoice types against booking flows
  • Address Management: Requires activated billing addresses
  • Cost Sheet: Pulls all cost items for voucher line items
  • Currency: Handles multi-currency scenarios with exchange rates


Sample properties to extract groupInvoiceBy

Below is a sample of a booking which you can use the selector (groupInvoiceBy) to group by Booking. 


{
  "isEditable": true,
  "isFinanceEditable": true,
  "isCancelled": false,
  "isAccepted": true,
  "isCloseOperation": false,
  "isCloseFinance": false,
  "isImport": false,
  "isExport": false,
  "isTransport": false,
  "uuid": "9c51abc2-5d2c-4e65-83c6-7227d8151df2",
  "type": "FTL",
  "status": "ACCEPTED",
  "statusUpdatedAt": null,
  "state": [
    {
      "key": "transHlg",
      "value": "transHlgPendTransPodSubmitted",
      "next": "transHlgPendTransPodVerified",
      "date": "2025-08-26T08:55:07.014Z"
    },
    {
      "key": "transCompAr",
      "value": "transCompPendOutstandingConsigneeInvoiceVoucher",
      "next": "transCompPendCustomerVoucherOprAaApproval",
      "date": "2025-08-26T08:55:07.014Z"
    }
  ],
  "stat": [
    {
      "i": 100,
      "t": "Submitted",
      "ts": "2025-07-02T01:07:35.063Z"
    },
    {
      "i": 120,
      "t": "Booked",
      "ts": "2025-08-15T00:58:18.529Z"
    },
    {
      "i": 300,
      "t": "Del Planned",
      "ts": "2025-08-15T05:24:00.000Z",
      "meta": {
        "drId": "DRIVER001",
        "trId": "J404",
        "vCode": "JUN1234"
      }
    },
    {
      "i": 500,
      "t": "Del Started",
      "ts": "2025-08-26T08:54:00.000Z",
      "meta": {
        "drId": "DRIVER001",
        "trId": "J404",
        "vCode": "JUN1234"
      }
    },
    {
      "i": 1000,
      "t": "Delivered",
      "ts": "2025-08-26T08:55:00.000Z",
      "meta": {
        "drId": "DRIVER001",
        "trId": "J404",
        "vCode": "JUN1234"
      }
    }
  ],
  "lStat": {
    "i": 1000,
    "t": "Delivered",
    "ts": "2025-08-26T08:55:00.000Z",
    "meta": {
      "drId": "DRIVER001",
      "trId": "J404",
      "vCode": "JUN1234"
    }
  },
  "serviceTypes": [],
  "tags": [],
  "no": "_SWE250107-014",
  "shortId": null,
  "remarks": "",
  "details": {
    "custRefs": [
      "13002",
      "DO_20250701_LCL_1_v1_2999"
    ],
    "truckSize": "5T",
    "truckType": "AMBIENT",
    "customerSo": "1001236638",
    "references": "2002951452",
    "shipperUuid": "ce6c3d2b-8a6c-4ec4-bbb0-b01a1270701e",
    "consigneeUuid": "eb444243-0f99-4757-ba5d-a2543e9061c4",
    "shipperAddressUuid": "1de6d914-4c25-4983-bb5d-8b60cc068ebf",
    "shipperRequiredDate": "2025-01-06T16:00:00.000Z",
    "consigneeAddressUuid": "b09513b8-22ae-4284-ab1a-a8e3afae3fa0"
  },
  "shipperUuid": null,
  "consigneeUuid": null,
  "approvedAt": "2025-08-15T00:58:18.529Z",
  "operationDate": "2025-08-26T08:55:00.000Z",
  "completionDate": "2025-08-26T08:55:00.000Z",
  "accountDate": null,
  "activityDate": "2025-08-26T08:55:00.000Z",
  "categories": null,
  "createdAt": "2025-07-02T01:07:35.063Z",
  "updatedAt": "2025-08-26T08:55:07.023Z",
  "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
  "quotationUuid": null,
  "billToUuid": "19429fc4-5bcc-496a-957c-15c41e9f15bc",
  "shipperAddressUuid": null,
  "consigneeAddressUuid": null,
  "costItems": [
    {
      "uuid": "c6bc6813-bd65-4c1f-a170-7671cf375c31",
      "accrual": null,
      "quantity": 1,
      "size": null,
      "unit": "TRIP",
      "description": null,
      "sellBaseRate": "1.000000",
      "sellRate": "1.000000",
      "sellExchangeRate": "1.000000",
      "sellTotal": "1.000000",
      "sellAccrual": "0.000000",
      "costBaseRate": "2.000000",
      "costRate": "2.000000",
      "costExchangeRate": "1.000000",
      "costTotal": "2.000000",
      "costAccrual": "0.000000",
      "isDeleted": false,
      "grossProfit": "0.000000",
      "shortBill": "0.000000",
      "estimatedProfit": "-1.000000",
      "accountPayable": "0.000000",
      "accountReceivable": "0.000000",
      "accountPayableDraft": "0.000000",
      "accountReceivableDraft": "1.000000",
      "vendorUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
      "bookingUuid": "9c51abc2-5d2c-4e65-83c6-7227d8151df2",
      "externalId": null,
      "createdAt": "2025-08-15T01:02:31.140Z",
      "updatedAt": "2025-08-15T05:33:37.723Z",
      "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
      "chargeItemUuid": "d3e7f7ea-ee6c-4985-a67f-e491e7b37a7b",
      "sellCurrencyUuid": "edac2598-fb86-4857-8e83-c26b32984db1",
      "costCurrencyUuid": "edac2598-fb86-4857-8e83-c26b32984db1",
      "editByUuid": "00a7d37c-59f2-4ea5-b553-7f691cc9e141",
      "chargeItem": {
        "uuid": "d3e7f7ea-ee6c-4985-a67f-e491e7b37a7b",
        "code": "10312",
        "name": "TRANSPORT CS-ID (N)",
        "status": "activated",
        "size": null,
        "sequence": null,
        "description": null,
        "unit": "TRIP",
        "rateType": "FIXED",
        "taxRate": null,
        "sellRate": "0.000000",
        "sellRateId": null,
        "costRate": "0.000000",
        "costRateId": null,
        "term": 0,
        "tags": [],
        "createdAt": "2021-08-20T13:48:36.300Z",
        "updatedAt": "2024-07-16T05:53:51.043Z",
        "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
        "companyUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
        "sellTaxUuid": "945ee28d-a9fe-4134-ae3d-0e6c9d3a8a84",
        "costTaxUuid": "945ee28d-a9fe-4134-ae3d-0e6c9d3a8a84",
        "sellCurrencyUuid": "edac2598-fb86-4857-8e83-c26b32984db1",
        "costCurrencyUuid": "edac2598-fb86-4857-8e83-c26b32984db1",
        "revenueCodeUuid": "0b6b51c4-cfdd-49f9-ad33-e7b808c38649",
        "expenseCodeUuid": "0b6b51c4-cfdd-49f9-ad33-e7b808c38649",
        "accrualExpenseCodeUuid": null,
        "accrualRevenueCodeUuid": null,
        "editByUuid": null,
        "sellTax": {
          "uuid": "945ee28d-a9fe-4134-ae3d-0e6c9d3a8a84",
          "name": "SV06",
          "code": "SV06",
          "percentage": "6.000000",
          "isActive": true,
          "exemptionDocumentUuid": null,
          "createdAt": "2021-09-17T12:56:24.629Z",
          "updatedAt": "2024-07-16T05:31:56.388Z",
          "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
          "companyUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8"
        }
      },
      "sellCurrency": {
        "uuid": "edac2598-fb86-4857-8e83-c26b32984db1",
        "code": "MYR",
        "name": "Malaysian Ringgit",
        "createdAt": "2021-06-24T09:00:17.791Z",
        "updatedAt": "2021-06-24T09:00:17.791Z"
      }
    }
  ],
  "jobs": [
    {
      "uuid": "620ba351-1ff8-4e65-ba6c-f72517a4a764",
      "status": "ACTIVE",
      "state": "COMPLETE",
      "type": "FTL",
      "tripFormat": "sameStart",
      "no": null,
      "jobNo": "_SWE250107-014-1",
      "remarks": null,
      "details": {},
      "categories": null,
      "createdAt": "2025-07-02T01:07:35.069Z",
      "updatedAt": "2025-08-26T08:55:06.932Z",
      "bookingUuid": "9c51abc2-5d2c-4e65-83c6-7227d8151df2",
      "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
      "trips": [
        {
          "uuid": "8114deb0-2e05-4d6d-a070-974c01c21684",
          "waybillId": "Y8EXY8",
          "type": "DELIVERY",
          "status": "COMPLETED",
          "sequence": 1,
          "remarks": null,
          "details": {
            "uom": [
              "M3",
              "M3",
              "M3",
              "M3",
              "M3",
              "M3",
              "M3",
              "M3",
              "M3",
              "M3"
            ],
            "quantity": [
              "0.252",
              "0.417",
              "0.860",
              "1.160",
              "1.080",
              "0.096",
              "0.773",
              "1.842",
              "1.285",
              "0.460"
            ]
          },
          "createdAt": "2025-07-02T01:07:35.073Z",
          "updatedAt": "2025-08-26T08:55:06.848Z",
          "jobUuid": "620ba351-1ff8-4e65-ba6c-f72517a4a764",
          "fromUuid": "77063a4c-765c-4145-8101-6b58329b161a",
          "toUuid": "e0ed687c-fd8d-4f01-9884-ac1fdd30397f",
          "to": {
            "uuid": "e0ed687c-fd8d-4f01-9884-ac1fdd30397f",
            "type": [
              "DELIVERY"
            ],
            "status": "activated",
            "name": "COCORO LIFE (MALAYSIA) SDN. BHD.",
            "address1": "MTS(ASMR)- SUZILA",
            "address2": "PERSIARAN KUALA LANGAT 28/24",
            "address3": "SHAH ALAM",
            "address4": "",
            "city": "",
            "district": "",
            "postCode": "40400",
            "areaCode": "SHAH ALAM",
            "zone": "C",
            "state": "",
            "phone": "",
            "fax": "",
            "location": null,
            "plusCode": "",
            "placeId": "",
            "tags": [],
            "createdAt": "2025-07-02T00:42:23.898Z",
            "updatedAt": "2025-07-02T00:42:23.898Z",
            "portUuid": null,
            "countryAlpha3": "MYS",
            "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
            "companyUuid": "f54158d1-05e1-415a-a83a-f84a8596c4d5"
          },
          "from": {
            "uuid": "77063a4c-765c-4145-8101-6b58329b161a",
            "type": [
              "BILLING",
              "DELIVERY"
            ],
            "status": "activated",
            "name": "SHARP ELECTRONICS (MALAYSIA) SDN BHD",
            "address1": "NO 1A, PERSIARAN KUALA LANGAT,",
            "address2": "SECTION 27,",
            "address3": "40400 SHAH ALAM,",
            "address4": "SELANGOR DARUL EHSAN.",
            "city": "SHAH ALAM",
            "district": "SELANGOR",
            "postCode": "40300",
            "areaCode": "SGR-SHAH ALAM",
            "zone": "C",
            "state": "",
            "phone": "",
            "fax": "",
            "location": null,
            "plusCode": "",
            "placeId": "",
            "tags": [],
            "createdAt": "2024-04-17T07:30:21.424Z",
            "updatedAt": "2025-07-02T01:05:26.023Z",
            "portUuid": null,
            "countryAlpha3": "MYS",
            "ownerUuid": "8f90b01f-362d-4b27-800f-ab7f410b81c8",
            "companyUuid": "19429fc4-5bcc-496a-957c-15c41e9f15bc"
          }
        }
      ]
    }
  ]
}