Reference
- Activity
- Addresses
- Calls
- Chat
- Custom Field Groups
- Custom Fields
- Customers
- Deals
- Deliveries
- Email Marketing
- Features
- Files
- Invoices
- Labels
- Lead Sources
- Leads
- Lunches
- Meetings
- Monitoring
- Notes
- Orders
- Organisations
- PDF Templates
- People
- Permissions
- Pipelines
- Product Categories
- Product Variations
- Products
- Purchase Orders
- Quotes
- Roles
- Settings
- SMS Marketing
- Tasks
- Tax Rates
- Teams
- Users
Products
Overview
Products represent goods or services that can be sold. Products can have multiple price points (per currency), variations, and belong to Product Categories. Products are referenced as line items on Deals, Quotes, Orders, Invoices and Purchase Orders.
Model: VentureDrake\LaravelCrm\Models\Product
Table: {prefix}products (default: crm_products)
Attributes
| Attribute | Type | Description |
|---|---|---|
external_id |
string |
UUID used in URLs (auto-generated) |
name |
string |
Product name |
code |
string |
Product code/SKU |
description |
text |
Description |
unit |
string |
Unit of measure |
active |
boolean |
Whether the product is active |
tax_rate_id |
integer |
Default tax rate |
product_category_id |
integer |
Category |
user_owner_id |
integer |
Owner user |
Relationships
| Method | Type | Related Model | Description |
|---|---|---|---|
productPrices() |
hasMany |
ProductPrice |
Product-level prices per currency (excludes variation prices since 2.5.0) |
productVariations() |
hasMany |
ProductVariation |
Variations, in display order |
activeProductVariations() |
hasMany |
ProductVariation |
Active variations only |
productCategory() |
belongsTo |
ProductCategory |
Category |
taxRate() |
belongsTo |
TaxRate |
Default tax rate |
ownerUser() |
belongsTo |
User |
Owner |
Helper Methods
getDefaultPrice()
Returns the default price for the configured system currency.
$price = $product->getDefaultPrice();
getPrimaryPrice()
Returns the default price, or failing that whichever product-level price row the product has. The edit form, its save and the show page all read this row. A product priced only in another currency therefore keeps that price on edit, rather than gaining a second, empty one.
unitPriceFor()
Returns the unit price, in cents, for a line of this product. A variation's price in the given currency wins. Otherwise the line falls back to the product's default price, exactly as a line with no variation.
$cents = $product->unitPriceFor($variation, 'USD'); // $variation may be null
Note: Variation prices live in the same
crm_product_pricestable. Since 2.5.0,productPrices()and theProductPrice::default()scope exclude rows with aproduct_variation_id. Before that,getDefaultPrice(), the API product payload and the Xero product sync read the first matching row, which could be a variation's price. Thedefault()scope also compared the currency against aSettingmodel rather than its value.
Scopes
active
Filters to only active products.
$products = Product::active()->get();
Creating a Product
use VentureDrake\LaravelCrm\Models\Product;
$product = Product::create([
'name' => 'Web Design Package',
'code' => 'WDP-001',
'description' => 'Full website design package',
'unit' => 'each',
'active' => true,
'user_owner_id' => auth()->id(),
]);
// Add a price
$product->productPrices()->create([
'unit_price' => 5000, // in cents
'currency' => 'USD',
]);
Variations
The product create and edit form has a Variations card for adding, editing, reordering, deactivating and removing variations, and the show page lists them. Each variation has its own SKU, barcode, price and active flag. See Product Variations.
Deleting a Product
Deleting a product soft-deletes it. The distinction matters because a product is referenced by every quote, order, invoice, purchase order, delivery and deal line ever raised against it:
- It stays readable on the documents that already reference it. The line-item relations resolve
withTrashed(), so the historic line description still renders on the show views, in all 14 PDF templates and in the Xero sync. - It stays out of the product pickers. Those query
Product::directly, so a deleted product cannot be chosen on a new document.
Readable on the documents that already reference it, not choosable on new ones. Before 2.4.1 deleting a product made every document that referenced it permanently unviewable — the foreign-key column was still populated but the relation resolved to null, so reading the product's name fatalled on a quote or invoice that had been fine for months.
Searching & Filtering
Searchable by name. Since 2.5.0 the products index also matches a variation's SKU, so searching for it finds the parent product. Filterable by user_owner_id and labels.id.
Traits
| Trait | Description |
|---|---|
SoftDeletes |
Soft delete support |
BelongsToTeams |
Multi-tenant team scoping |
HasCrmFields |
Custom field support |
SearchFilters |
Search and filter capabilities |