venturedrake/laravel-crm
Laravel CRM is a free, open-source, self-hosted CRM package for Laravel applications. It provides leads, contacts, accounts, deals, activities, notes, and customizable sales pipelines, plus modern Laravel authentication and Blade UI components to help you add CRM functionality quickly and easily.
composer require → php artisan vendor:publish for the CRM provider's config/migrations. The config lands at config/package.php.2.5.0. If you carry your own copy, bump its version key in the same commit — see the version note below.php artisan laravelcrm:update. This applies migrations and writes the db_version marker so SystemCheckService::normalisedVersion() stops reporting "database behind code". On 2.5.0 it also runs the two database/updates migrations that add the product-variation code, barcode, order, active and user_*_id columns and the crm_purchase_order_lines.comments / order_product_id columns. Both are idempotent, so re-running is safe.HasPersonSuggest / HasOrganizationSuggest to the component, bind wire:model to personName / organizationName, and render the suggestion dropdown. Picking a suggestion sets person_id / organization_id; typing a different name clears it so saving creates a new record. New in 2.5.0: saving with both a person and an organization now links them as related contacts automatically (PersonService::linkToOrganization()), so each shows the other on its show page without extra work.Look first at config/package.php (version string + encrypt_db_fields), then SystemCheckService, UpdateController, and the Settings middleware flag crm.settings-seeded.<version> — all four read the same version value. For the new feature, read ProductVariation, ProductVariationObserver, ResolvesProductVariation, and Product::unitPriceFor().
config/package.php, every package upgrade needs the version string bumped there too. Missing the bump keeps the update banner hidden, keeps the system check comparing against the previous release, and — most importantly — leaves the crm.settings-seeded.<version> middleware flag already set, so the seeding pass never re-arms on deploy. 2.5.0 is a minor release precisely because it adds a feature and an API sub-resource; don't downgrade the number to a patch on the assumption it's fixes-only. Deploy order: publish/merge config → php artisan laravelcrm:update → config:cache.Product::unitPriceFor() — variation price in the document currency, falling back to the product price when the variation has no price there. Changing the product must clear the variation. Only persist a variation that belongs to that line's product in the current team (ResolvesProductVariation enforces this); the REST API returns 422 for a cross-product or cross-team variation, so mirror that validation in custom forms to avoid a silent drop.order column; deactivating is the "hide from the picker but keep history" lever. ProductVariationObserver handles external_id and the user_*_id columns — don't set them by hand.displayName() (Product — Variation) and displayCode() (variation SKU, else product SKU); all five PDF styles and the emailed/portal documents reuse those views, so custom documents should call the same helpers rather than formatting names inline. In Xero, Description gets the variation name and ItemCode is the variation SKU only if a synced Xero item already has that code — otherwise the parent product's item code (src/Support/XeroLineItem.php). Variations are never created as Xero items.updatedHasPersonSuggest() / updatedHasOrganizationSuggest() hook, which compares the trimmed name to the decrypted linked record and nulls the FK. That's what makes "Paul Clark" → "Paul" create a new person instead of silently re-attaching Paul Clark. Note the 2.5.0 person↔organization link fix does not duplicate an existing link, and changing the person or organization on a record keeps the old link — so don't try to "clean up" links on edit.updatedPersonName() / updatedOrganizationName() for normalisation, validation, or slug generation — the trait hooks are scoped to their own properties and fire independently. Just don't let your hook re-assign the FK after the trait has cleared it.PurchaseOrderService::update() now reads $request->products using the same row keys as create(); it updates matched lines, adds new ones, and removes lines no longer submitted, matching only lines belonging to the order being edited. Any custom PO form, Livewire component, or API wrapper must post products, not purchaseOrderLines, or edits will be silently lost.encrypt_db_fields on, name comparisons must go through the decrypted model accessor. The trait already does this; any custom suggestion query or duplicate check you write must do the same rather than comparing raw ciphertext columns.ContactSuggestSearchTest (select→rename leaves person_id null, "New" badge reappears, saving persists a fresh record), DealProductVariationTest / ProductVariationConversionTest (variation pricing and carry-through), PurchaseOrderService coverage via the PO tests, XeroLineItemTest, and Api/V2/ProductVariationApiTest.config/package.php. It's easy to miss when diffing upgrades — and its version key drives the update banner, the system check, the db_version marker, and the settings-seeding flag simultaneously.php artisan config:cache during deploy will happily cache a stale version string. Always re-run it after publishing the new config, or the update prompt and seeding pass stay suppressed.crm_product_prices holds both product-level and variation-level rows. As of 2.5.0, Product::productPrices() returns product-level rows only and ProductPrice::scopeDefault() excludes variation prices (and now compares the currency against the Setting value, not the model). If you wrote your own "first matching price row" query before this release, it has been quietly returning a variation price — switch to productPrices(), scopeDefault(), or unitPriceFor()./customers routes. The laravel-crm.customers.* route group, CustomerController, the customers/index view, and StoreClientRequest / UpdateClientRequest are gone. Any menu entry, redirect, or hard-coded route('laravel-crm.customers.index') in your app will now throw. The Customer model, observer, and policy stay for the V1→V2 upgrade path and the customer_id columns on leads/deals/quotes/orders — use People and Organizations for new UI work. Covered by CustomersRoutesRemovedTest.php.product_attribute* lang keys are gone. Custom nav or settings links to it will break.ProductObserver, LunchObserver, and FileObserver used to write the restoring user into user_deleted_id. They now clear user_deleted_id and set user_restored_id. If you copied that observer pattern into your own models, you have the same bug — check every restored() you own.PurchaseOrderService::create() wrote comments and order_product_id with no migration adding them; the suite passed only because no test saved a PO with lines. If you skipped laravelcrm:update after upgrading, PO saves with lines will still fail — run the command, don't assume the schema is current.hasPersonSuggest, hasOrganizationSuggest). Renaming that bound property, or moving the name into a nested/array property, silently disables link invalidation and the stale-link bug returns.hasXSuggest properties in sync — setting person_id after the trait clears it re-creates the original attach-the-wrong-record bug. Same rule for product_variation_id: clearing it on product change is load-bearing, not cosmetic.variations, and products/{product}/variations is authorised against the parent product (no new permission needed). Quote, order, and invoice line_items accept product_variation_id. Check your client's response parsing tolerates the extra key.updated* methods is supported; the risk is ordering, so re-read the id properties at save time rather than trusting values set mid-request. Same for variations — resolve the variation through ResolvesProductVariation at save time rather than in the request handler.How can I help you explore Laravel packages today?