> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aps-store.com/llms.txt
> Use this file to discover all available pages before exploring further.

# forms.lua

Paper forms: tickets, invoices, receipts, vehicle sale agreements, supplies, the company billings overview and form props. See the [Paper Forms](https://docs.aps-store.com/assets/aps_cityhall/guides/paper-forms.md) guide.

> [!NOTE]
>
> Runs on: **shared**. This file stays editable when the resource is escrow-protected. Restart the resource after editing.

***

## Full View

Preview File Updated: 1.1.0 - 05.11.2026

```lua config/forms.lua expandable lines theme={null}
Config = Config or {}

-- ═════════════════════════════════════════════
--  Paper forms — tickets, invoices, receipts and vehicle sale agreements.
--  Players write them in game with inventory items. Switch them with
--  Config.Features.billingForms and Config.Features.vehicleSale.
--
--  The tables use the same keys as vms_cityhall (Config.BillingItems,
--  Config.AllowedJobBillings, Config.BillingPayments, Config.VehicleSaleAgreement),
--  so an existing vms config can be pasted in.
--  All items must exist in your inventory: see docs → Paper Forms.
-- ═════════════════════════════════════════════

-- ─────────────────────────────────────────────
--  General
--    distance       the receiver / buyer must stand this close
--    showRealNames  false: the receiver list shows "Citizen 1", "Citizen 2" …
--                   so players don't learn a stranger's name from the form
--    allowSelf      also list yourself (for testing alone; keep false on live servers)
--    issueCooldown  seconds between two forms from the same player
--    maxRows        most lines on an invoice / receipt
--    maxQuantity    highest quantity on one line
--    maxPenaltyPoints  most points one traffic ticket can add
-- ─────────────────────────────────────────────
Config.Forms = {
    distance         = 5.0,
    showRealNames    = false,
    allowSelf        = false,
    issueCooldown    = 3,
    maxRows          = 15,
    maxQuantity      = 999,
    maxPenaltyPoints = 12,
}

-- ─────────────────────────────────────────────
--  Items per form
--    requiredItem    must be carried, not used up (e.g. the payment terminal)
--    emptyItemName   the blank form (nil = none needed)
--    removeOnIssue   use up the blank form when the bill is issued
--    itemName        the filled form the receiver gets
--    addForReceiver  give the receiver that item
--    payWithItem     the receiver can pay by using the item, anywhere
--                    (false = only on a City Hall PC)
--  'ticket' covers both ticket and traffic-ticket.
--  With neither requiredItem nor emptyItemName the form can only be opened
--  with exports['aps_cityhall']:openBillingForm(source, 'ticket').
-- ─────────────────────────────────────────────
Config.BillingItems = {
    ['ticket'] = {
        emptyItemName  = 'empty_ticket',
        removeOnIssue  = true,
        itemName       = 'ticket',
        addForReceiver = true,
        payWithItem    = false,
    },
    ['invoice'] = {
        emptyItemName  = 'empty_invoice',
        removeOnIssue  = true,
        itemName       = 'invoice',
        addForReceiver = true,
        payWithItem    = false,
    },
    ['receipt'] = {
        requiredItem   = 'payment_terminal',
        emptyItemName  = 'thermal_paper',
        removeOnIssue  = true,
        itemName       = 'receipt',
        addForReceiver = true,
        payWithItem    = true,
    },
}

-- ─────────────────────────────────────────────
--  Payment by using the item (payWithItem above)
-- ─────────────────────────────────────────────
Config.BillingPayments = {
    withItem = { ['cash'] = false, ['bank'] = true },
}

-- ─────────────────────────────────────────────
--  Jobs that can write forms
--    label                 shown on the form
--    society               account that receives the society share
--    percentageForSociety  share of a paid bill for the society
--    percentageForIssuer   share for the player who wrote it (online or offline)
--    types                 forms this job can write:
--                          'traffic-ticket', 'ticket', 'invoice', 'receipt'
--    amountLimits          lowest / highest amount per form, before tax
--    dueDates              choices in days (-1 = no due date)
--    taxFromInvoice / taxFromReceipt  percent added on top. With
--                          Config.Features.taxes it goes to the treasury
--    allowLicenseRevocation / licenseRevocationList  licenses a traffic ticket
--                          can take: City Hall id (or Config.Compat.licenseAliases
--                          name) = label
--    allowLicenseSuspension / licenseSuspensionDays  suspension choices in days
--    allowPenaltyPoints    traffic tickets can add penalty points
-- ─────────────────────────────────────────────
Config.AllowedJobBillings = {
    ['police'] = {
        label                = 'LSPD',
        society              = 'society_police',
        percentageForSociety = 85,
        percentageForIssuer  = 15,
        types                = { ['traffic-ticket'] = true, ['ticket'] = true },
        amountLimits         = {
            ['traffic-ticket'] = { min = 1, max = 12000 },
            ['ticket']         = { min = 1, max = 50000 },
        },
        dueDates               = { -1, 1, 2, 3, 4, 5, 6, 7, 14, 30 },
        allowLicenseRevocation = true,
        licenseRevocationList  = {
            ['motorcycle'] = 'Motorcycle',
            ['car']        = 'Car',
            ['truck']      = 'Truck',
        },
        allowLicenseSuspension = true,
        licenseSuspensionDays  = { 3, 4, 5, 6, 7, 14 },
        allowPenaltyPoints     = true,
    },
    ['ambulance'] = {
        label                = 'EMS',
        society              = 'society_ambulance',
        percentageForSociety = 90,
        percentageForIssuer  = 10,
        types                = { ['invoice'] = true },
        amountLimits         = { ['invoice'] = { min = 1, max = 100000 } },
        dueDates             = { -1, 1, 2, 3, 4, 5, 6, 7, 14, 30 },
        taxFromInvoice       = 4.5,
    },
    ['mechanic'] = {
        label                = 'MECHANIC',
        society              = 'society_mechanic',
        percentageForSociety = 90,
        percentageForIssuer  = 10,
        types                = { ['receipt'] = true },
        amountLimits         = { ['receipt'] = { min = 1, max = 10000 } },
        taxFromReceipt       = 2.3,
    },
}

-- ─────────────────────────────────────────────
--  Supplies: sold in the Document Office app (tab "Forms & Supplies")
--    name / label / description   item, and the text in the shop
--    price                        taken from Config.MoneyAccount (0 = free)
--    jobs                         only these jobs can buy it (nil = everyone)
-- ─────────────────────────────────────────────
Config.EmptyPapers = {
    { name = 'vehicle_sale_agreement', label = 'Sale Agreement',   description = 'Vehicle purchase and sale agreement', price = 0 },
    { name = 'empty_ticket',           label = 'Ticket Pad Sheet', description = 'Blank ticket',                         price = 0,    jobs = { police = true } },
    { name = 'empty_invoice',          label = 'Blank Invoice',    description = 'Blank invoice',                        price = 0,    jobs = { ambulance = true } },
    { name = 'thermal_paper',          label = 'Thermal Paper',    description = 'Paper for the payment terminal',       price = 5,    jobs = { mechanic = true } },
    { name = 'payment_terminal',       label = 'Payment Terminal', description = 'Card terminal for receipts',           price = 500,  jobs = { mechanic = true } },
}

-- ─────────────────────────────────────────────
--  Vehicle sale agreement (Config.Features.vehicleSale)
--    UseOnItem           open the agreement by using ItemName
--    ItemName            blank agreement, used up when both parties signed
--    SignedItemName      copy of the signed agreement
--    GiveSignedItemToSeller / GiveSignedItemToBuyer
--    UseOnCommand        also open it with /CommandName (no item needed)
--    PaymentMethods      'cash', 'bank'
--    MinPrice / MaxPrice allowed sale price
--    OfferSeconds        how long the buyer has to sign
--    BlacklistVehicles   model names that can't be sold
--    AllowSellForCompany the buyer can also sign for their company: the vehicle
--                        then gets the company in CompanyColumn (needs that
--                        column, vms_garagesv2 adds it). The buyer pays
--    CompaniesList       job → grades that may buy, and allowed models
--                        (model names, or vms style [GetHashKey('taxi')] = true)
--  Business, job, company and gang vehicles can't be sold.
--  With vms_garagesv2: set its Config.UseSaleAgreement = false.
-- ─────────────────────────────────────────────
Config.VehicleSaleAgreement = {
    UseOnItem              = true,
    ItemName               = 'vehicle_sale_agreement',
    SignedItemName         = 'signed_vehicle_sale_agreement',
    GiveSignedItemToSeller = true,
    GiveSignedItemToBuyer  = true,
    UseOnCommand           = false,
    CommandName            = 'sellvehicle',
    PaymentMethods         = { 'cash', 'bank' },
    MinPrice               = 1,
    MaxPrice               = 100000000,
    OfferSeconds           = 120,
    BlacklistVehicles      = { 'rhino', 'police', 'police2', 'police3' },
    AllowSellForCompany    = GetResourceState('vms_garagesv2') ~= 'missing',
    CompanyColumn          = 'company',
    CompaniesList          = {
        ['ambulance'] = { grades = { 5, 6 }, allowedModels = { 'ambulance' } },
        ['taxi']      = { grades = { 3, 4 }, allowedModels = { 'taxi', 'baller' } },
    },
}

-- ─────────────────────────────────────────────
--  Company billings overview: every bill of the player's job, for jobs in
--  Config.AllowedJobBillings (nil command = off)
--    command / description / key   chat command, its help text, optional key
--    fullAccessGrade   this grade and up see the whole company, like bosses
--                      (nil = bosses only). Everybody else sees their own bills
--    historyDays       how far back paid and cancelled bills are listed
--    limit             most bills listed
--    showPaid / showCancelled
-- ─────────────────────────────────────────────
Config.CompanyBillings = {
    command         = 'billings',
    description     = 'Company billings',
    key             = nil,
    fullAccessGrade = nil,
    historyDays     = 60,
    limit           = 200,
    showPaid        = true,
    showCancelled   = false,
}

-- ─────────────────────────────────────────────
--  Prop and animation while a form is open (false = none)
--    paper      tickets, invoices, agreements
--    terminal   receipts. The model is the free payment terminal by BzZz
--               (https://forum.cfx.re/u/bzzzi), included in stream/ (THIRD_PARTY.md).
--               `fallback` is used if the model isn't available
-- ─────────────────────────────────────────────
Config.FormProps = {
    paper = {
        dict = 'missheistdockssetup1clipboard@base', anim = 'base',
        prop = 'p_amb_clipboard_01', bone = 36029,
        offset = vector3(0.16, 0.08, 0.1), rotation = vector3(-130.0, -50.0, 0.0),
    },
    terminal = {
        dict = 'cellphone@', anim = 'cellphone_text_read_base',
        prop = 'bzzz_prop_payment_terminal', bone = 57005,
        offset = vector3(0.18, 0.01, 0.0), rotation = vector3(-54.0, 220.0, 43.0),
        fallback = {
            prop = 'prop_cs_hand_radio', bone = 28422,
            offset = vector3(0.0, 0.0, 0.0), rotation = vector3(0.0, 0.0, 0.0),
        },
    },
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.