All Activity
- Today
-
ROQ started following Updating auto renew next due dates
-
Hi guys, I've noticed I've had a few customers with auto renew products and the next due date has move slightly from the registration date. This has caused some billing issues. Is there a way to sync next due date on all existing auto renews to match the registration dates?
-
nncyprston24 joined the community
-
arthrovixcreamreport joined the community
-
## v5.8 Released (14 January 2026) - Screenshots are in the main post above New Features & Enhancements: * Improved predefined replies system to better align with WHMCS behavior and usage patterns * Predefined replies are now available across more report and email response contexts * Automatically closes any open abuse reports when a service is deleted (previously only on service termination) Bugfixes: * Fixed issue where the "Client" field was not auto-populated when creating reports from client summary * Fixed missing "Custom IP (Override)" option when creating abuse reports from service pages * Fixed UI layout issue caused by client-area top strip affecting service detail pages in Chrome * Fixed logic gaps in predefined replies loading and visibility handling For documentation please visit https://layerlabs.gitbook.io/abuse-manager-pro/
-
Mandy Web Design joined the community
-
Upgrading from earlier versions
WHMCS John replied to twhiting9275's topic in WHMCS 9.0 RC Discussion
Hi @twhiting9275, Thanks for the report. I've been able to replicate the issue when updating from 7.x or earlier to 9.0, and have opened WHMCS-24841 to investigate further. It appears that a clean upgrade will resolve the issue: 1. Delete all old files (except /templates and configuration.php) 2. Extract the fresh 9.0 files into place 3. Visit /install/install.php 4. The upgrade process can be completed successfully -
UK HIJIB HOUSE joined the community
-
setandar0808 joined the community
-
WeWeTalent started following WHMCS Project Management & Development Services — WeWe
-
WeWe provides structured WHMCS project management and development services for hosting companies and digital businesses that need reliable, production-ready solutions. We focus not only on development, but also on proper planning, execution, and long-term maintainability of WHMCS systems. Our team has multi-year hands-on experience working with WHMCS in real operational environments, supported by solid technical resources through our sister company, WebNIC, which has been active in this industry for over a decade. Our WHMCS Services Include: • Custom WHMCS Modules & Addon Development • WHMCS Setup, Migration, and System Optimization • Payment Gateway & Third-Party API Integrations • Domain Registrar Integrations • Server Integrations (cPanel, Plesk, DirectAdmin) • Custom Hooks & Workflow Automation • WHMCS Version Upgrades & Refactoring • Client Area & Template Customization • Ongoing WHMCS Maintenance & Support Why Work With WeWe: • Strong focus on structured project delivery • Practical experience with live WHMCS systems • Reliable resources backed by long-term industry presence • Clear communication and accountable execution • Suitable for both one-time projects and ongoing support If you are planning a WHMCS upgrade, custom development, or need a dependable partner to manage your WHMCS project end-to-end, feel free to reach out to: Email: muhammad.m@wewe.cc Website: wewe.cc Phone: +62 85780561051 WHMCS - WeWe Talent.pdf
-
- whmcs
- project management
-
(and 1 more)
Tagged with:
- Yesterday
-
leatherbond changed their profile photo
-
Arkian4334 joined the community
-
@twhiting9275, Sure thing, which ones do you need?
-
william.russell03 changed their profile photo
-
william.russell03 joined the community
-
pchcool joined the community
-
twhiting9275 started following Upgrading from earlier versions and Assigning variables in v9?
-
Great, then give us the hook points to do so 😉 The reason we use variables like this is because WHMCS does not provide the necessary hook points to work with. Kind of silly to have to use those variables, but, again, sometimes it has to be done
-
Already opened up a bug (ticket DVZ-331740), just creating this here as well I have a couple of really, really old WHMCS versions (some at 7.1.x), deliberately, to do testing and whatnot for clients. Since they're dev installs and locked down to me only, I'm not terribly worried about threats there. Tried to upgrade one of the 7.1.x versions to 9 and couldn't do so. 'Cannot read configuration file' the system said Rolled back backup, upgraded to 8.13, then to 9, worked like a charm Just a heads up for those that may be in the same position. Looks like some earlier versions aren't available for a direct upgrade
-
Ronniesligo started following Webhook setup for New ticket generate in WHMCS
- Last week
-
lhajjam started following IPTV website designs
-
bear started following WHMCS client area invoices
-
Plurality just means "the largest amount", which is not always the "majority". More like of 100 people, 36% said yes, 34 no and 30 undecided, the "yes" people would be the plurality. Of course, we might not learn the sampling group size, or if it's asked of anyone, so it's really vague.
-
I don't think you need a majority of users to request this. It is pretty straightforward that unpaid invoices should be at the top followed by the most recently paid invoices.
-
Critical Error</h1><p>Could not connect to the database.
WHMCS Alex replied to Nimesh Piyumal's topic in Using WHMCS
Hi, I would advise checking the required extenstions/modules are present, e.g: php -m We would expect to see: PDO pdo_mysql You may also find this documentation helpful: https://docs.whmcs.com/8-13/troubleshooting/troubleshoot-the-database/could-not-connect-errors/ -
Hi @Evolve Web Hosting, The oldest unpaid invoice is displayed first so that clients are guided to clear their most overdue debt to you first. If a pluraliry of users would like us to change the default sort order, we can certainly do so. @wtools please use a Child Theme to retain your customisation on update.
-
hostk.com changed their profile photo
-
Best Web Hosting Template - Premium WHMCS Template Hostina is a premium professional WHMCS template designed for modern web hosting companies, shared hosting providers, VPS and reseller hosting businesses, and domain registration services. Built to deliver performance, scalability, and conversions, Hostina features a fully responsive WHMCS theme with a custom client area dashboard, a professionally styled order form checkout, and complete bilingual support (English LTR and Arabic RTL). Whether you’re launching a new hosting website or upgrading an existing WHMCS installation, Hostina provides a clean, fast, and SEO-optimized hosting template that enhances user experience, improves Core Web Vitals, and helps hosting providers convert visitors into customers across all devices. ✨ Key Features Fully responsive & modern design Arabic (RTL) + English (LTR) bilingual support SEO optimized — Core Web Vitals ready Custom WHMCS Dashboard with sidebar navigation Styled orderform & checkout pages Easy installation — upload → activate → go 🧭 Frontend Pages Homepage (features, pricing, testimonials, FAQ) Domain Search & Transfer Hosting Plans (Shared, WordPress, Reseller, VPS, Dedicated, Game Servers) Cart & Checkout (Styled Orderform Flow) Blog + Blog Details Contact Page (form + map) 💼 Client Area Login & Register Custom Dashboard with Sidebar My Services & Products My Domains (Renewal, Auto-Renew, Management) Invoices & Billing Support & Tickets Membership / User Management ⚙️ Compatibility WHMCS: 8.0 – 8.13 + PHP: 8.1 – 8.3 Works perfectly with Plesk, DirectAdmin, cPanel, CloudLinux, LiteSpeed, Apache, Nginx 🖼️ Screenshots Preview 👉 Open Gallery — hostk.com/hostina-template 🧪 Live Demo 🌐 Frontend Demo: demo.hostk.com/hostina/ 🇬🇧 English Version: demo.hostk.com/hostina/en 🇸🇦 Arabic Version: demo.hostk.com/hostina/ar 🔑 WHMCS Demo: demo.hostk.com/index.php?systpl=hostina Login: test@hostk.com Password: test 🛒 Purchase Now 👉 Buy Hostina Template 🧾 About Hostina Hostina is a premium WHMCS template built specifically for web hosting companies, shared hosting providers, VPS and reseller hosting businesses, and domain registration services. This complete WHMCS solution includes a modern, fully responsive hosting website template, a custom WHMCS client area dashboard, and a professionally styled order form checkout optimized for usability and conversions. Hostina supports both Arabic (RTL) and English (LTR), is fully SEO optimized, Core Web Vitals ready, and compatible with the latest WHMCS versions — making it the ideal choice for launching or upgrading a professional web hosting business.
-
- whmcs theme
- hosting
- (and 7 more)
-
WHMCS v9 Beta? Nah waist of time and effort
bear replied to zomex's topic in WHMCS 9.0 RC Discussion
To meet the new pricing announced for January 1, at a guess. -
WHMCS v9 Beta? Nah waist of time and effort
ptomter replied to zomex's topic in WHMCS 9.0 RC Discussion
What I find somehow strange after being using WHMCS for 20 years.. when there was only 2-3 people in the company... bugs was fixed quickly often the same day. Now that I have a list of bugs reported many of them with 1 min solutions to implement, and they still doesnt get implement in X versions of whmcs 8x and still not fixed in WHMCS 9. So I dont know if being a large owned company is always a good thing. Just in December alone me and other people I work with found 3 new bugs of things that has been working since whmcs 5 and suddenly not working and per documentation should work.. I am actually concerned that the developing department / team doesnt have good working structure as they introduce bugs that should not be there that has been working for years. This make us need to use more time on testing before each upgrade and time testing is also a huge cost.. whmcs. should going forward be better in quality and also fixe the backlogs of bugs. -
Virtual Sky Host is a newly launched WHMCS-powered hosting platform, and the core site and service lineup have just been finalized. We’re currently refining product offerings, standardizing automated services, and continuing to improve the overall experience as the platform evolves. Additional features, enhancements, and services will be rolling out soon. Thanks to everyone who has taken the time to check it out and provide feedback.
-
@WHMCS John at least is there a way to do it by some hook? So we can set a default sorting behavior? Updating the template is a bad idea as it will be gone during the WHMCS upgrade.
-
@WHMCS John That is not intuitive at all for the end user (our customers). For the price increases we all just received, WHMCS should just fix this for all future releases. It will take a developer less than a minute to do so.
-
Hi @jonathanMahapa, The client area invoice list is sorted by status and then age, so that the oldest unpaid invoice is displayed first. The table supports sorting by multiple criteria, so one can Ctrl + Click the "Unpaid" and "Invoice Date" columns to sort by status and see the newest/oldest invoices. You can also customise this in the template by changing the sort order attribute from asc to desc. https://github.com/WHMCS/templates-twenty-one/blob/58393ff844e730f2c995a57b6a0834e0a003ded6/clientareainvoices.tpl#L8
-
canadianrootertoronto changed their profile photo
-
Thank you for this. It looks like the sort of thing I want, but. I will investigate it further. Many thanks
-
I think what you want is , https://docs.whmcs.com/8-13/products/configuration-options/product-downloads/ You can set up downloadable products in WHMCS
-
WHMCS Admin Widget: UK TAX Year Income/Profit/Loss
pjs32 replied to ConorBradley's topic in Developer Corner
Thats great - work well - thank you!😀 - Earlier
-
WHMCS Admin Widget: UK TAX Year Income/Profit/Loss
ConorBradley replied to ConorBradley's topic in Developer Corner
Hi, Thank you 😄 Try this code. I haven't tested it on my install, as I need it included in my totals, so you'll have to let me know (please test it NOT on a live install). You'll also need to switch to invoice-only mode as WHMCS doesn’t store “VAT” as a field on tblaccounts transactions. <?php if (!defined("WHMCS")) { die("No direct access"); } use WHMCS\Database\Capsule; use Carbon\Carbon; add_hook('AdminHomeWidgets', 1, function () { return new class extends \WHMCS\Module\AbstractWidget { protected $title = 'Profit / Loss (Month, Prev Month, UK Tax Year)'; protected $description = 'Invoice-linked only. Base-currency totals. Optional Excl. VAT. UK tax year 6 Apr – 5 Apr.'; protected $weight = 150; protected $columns = 2; // set 1/2/3 to suit your dashboard protected $cache = false; protected $cacheExpiry = 0; protected $requiredPermission = ''; public function getData() { try { $now = Carbon::now(); // Invoice-linked only (as requested) $opts = ['onlyInvoiceLinked' => true]; // VAT mode: inc|ex $vatMode = $this->getVatMode(); $excludeVat = ($vatMode === 'ex'); // Calendar months $startMonth = $now->copy()->startOfMonth(); $prevMonthStart = $now->copy()->subMonth()->startOfMonth(); $prevMonthEnd = $now->copy()->subMonth()->endOfMonth(); // UK tax year (6 Apr – 5 Apr) $tyStart = $this->ukTaxYearStart($now); $tyEnd = $tyStart->copy()->addYear()->subDay()->endOfDay(); // 5 Apr end-of-day // Last tax year (full) $lastTyStart = $tyStart->copy()->subYear(); $lastTyEnd = $lastTyStart->copy()->addYear()->subDay()->endOfDay(); // 5 Apr end-of-day // Query per-currency (invoice-linked only) $monthRows = $this->sumProfitByCurrency($startMonth, $now, $opts); // month-to-date $prevMonthRows = $this->sumProfitByCurrency($prevMonthStart, $prevMonthEnd, $opts); // full prev month $taxYearRows = $this->sumProfitByCurrency($tyStart, $now, $opts); // current tax year to date $lastTaxYearRows = $this->sumProfitByCurrency($lastTyStart, $lastTyEnd, $opts); // last tax year full $base = Capsule::table('tblcurrencies')->where('default', 1)->first(); // VAT totals by period (deduped per invoice) $monthVatRows = $this->sumInvoiceVatByCurrencyDistinctInvoices($startMonth, $now); $prevMonthVatRows = $this->sumInvoiceVatByCurrencyDistinctInvoices($prevMonthStart, $prevMonthEnd); $taxVatRows = $this->sumInvoiceVatByCurrencyDistinctInvoices($tyStart, $now); $lastTaxVatRows = $this->sumInvoiceVatByCurrencyDistinctInvoices($lastTyStart, $lastTyEnd); $monthVatBase = $this->toBaseVatTotal($monthVatRows, $base); $prevMonthVatBase = $this->toBaseVatTotal($prevMonthVatRows, $base); $taxVatBase = $this->toBaseVatTotal($taxVatRows, $base); $lastTaxVatBase = $this->toBaseVatTotal($lastTaxVatRows, $base); // Profit totals (base) $monthBaseProfit = $this->toBaseTotal($monthRows, $base); $prevMonthBaseProfit = $this->toBaseTotal($prevMonthRows, $base); $taxBaseProfit = $this->toBaseTotal($taxYearRows, $base); $lastTaxBaseProfit = $this->toBaseTotal($lastTaxYearRows, $base); if ($excludeVat) { // VAT is part of gross invoice totals; remove it from profit $monthBaseProfit['amount'] -= $monthVatBase; $prevMonthBaseProfit['amount'] -= $prevMonthVatBase; $taxBaseProfit['amount'] -= $taxVatBase; $lastTaxBaseProfit['amount'] -= $lastTaxVatBase; } // Multi-field totals (base) for averages row $monthTotalsMulti = $this->toBaseTotalsMulti($monthRows, $base); $prevMonthTotalsMulti = $this->toBaseTotalsMulti($prevMonthRows, $base); $taxTotalsMulti = $this->toBaseTotalsMulti($taxYearRows, $base); $lastTaxTotalsMulti = $this->toBaseTotalsMulti($lastTaxYearRows, $base); if ($excludeVat) { // Remove VAT from income and profit (leave fees/amount_out untouched) $monthTotalsMulti['amount_in'] -= $monthVatBase; $monthTotalsMulti['profit'] -= $monthVatBase; $prevMonthTotalsMulti['amount_in'] -= $prevMonthVatBase; $prevMonthTotalsMulti['profit'] -= $prevMonthVatBase; $taxTotalsMulti['amount_in'] -= $taxVatBase; $taxTotalsMulti['profit'] -= $taxVatBase; $lastTaxTotalsMulti['amount_in'] -= $lastTaxVatBase; $lastTaxTotalsMulti['profit'] -= $lastTaxVatBase; } // Averages $tyMonths = $this->ukTaxMonthsElapsed($tyStart, $now); $monthAvg = [ 'income' => $this->avgPerMonth($monthTotalsMulti['amount_in'], 1), 'expenses' => $this->avgPerMonth($monthTotalsMulti['fees'] + $monthTotalsMulti['amount_out'], 1), 'profit' => $this->avgPerMonth($monthTotalsMulti['profit'], 1), 'months' => 1, ]; $prevMonthAvg = [ 'income' => $this->avgPerMonth($prevMonthTotalsMulti['amount_in'], 1), 'expenses' => $this->avgPerMonth($prevMonthTotalsMulti['fees'] + $prevMonthTotalsMulti['amount_out'], 1), 'profit' => $this->avgPerMonth($prevMonthTotalsMulti['profit'], 1), 'months' => 1, ]; $taxYearAvg = [ 'income' => $this->avgPerMonth($taxTotalsMulti['amount_in'], $tyMonths), 'expenses' => $this->avgPerMonth($taxTotalsMulti['fees'] + $taxTotalsMulti['amount_out'], $tyMonths), 'profit' => $this->avgPerMonth($taxTotalsMulti['profit'], $tyMonths), 'months' => $tyMonths, ]; $lastTaxYearAvg = [ 'income' => $this->avgPerMonth($lastTaxTotalsMulti['amount_in'], 12), 'expenses' => $this->avgPerMonth($lastTaxTotalsMulti['fees'] + $lastTaxTotalsMulti['amount_out'], 12), 'profit' => $this->avgPerMonth($lastTaxTotalsMulti['profit'], 12), 'months' => 12, ]; return [ 'totals' => [ 'month' => $monthBaseProfit, 'prev_month' => $prevMonthBaseProfit, 'tax_year' => $taxBaseProfit, // current tax year to date 'last_tax_year' => $lastTaxBaseProfit, // last tax year full 'base' => $base ? (array)$base : ['code'=>'','prefix'=>'','suffix'=>''], ], 'averages' => [ 'month' => $monthAvg, 'prev_month' => $prevMonthAvg, 'tax_year' => $taxYearAvg, 'last_tax_year' => $lastTaxYearAvg, ], 'meta' => [ 'month_label' => $startMonth->format('M Y'), 'prev_month_label' => $prevMonthStart->format('M Y'), 'tax_year_label' => $this->formatTaxYearLabel($tyStart), 'tax_year_full_range' => $tyStart->format('j M Y') . ' – ' . $tyEnd->format('j M Y'), 'tax_year_asof' => $now->format('j M Y'), 'last_tax_year_label' => $this->formatTaxYearLabel($lastTyStart), 'last_tax_year_full_range' => $lastTyStart->format('j M Y') . ' – ' . $lastTyEnd->format('j M Y'), 'generated' => $now->toDayDateTimeString(), 'vat_mode' => $vatMode, // inc|ex ], ]; } catch (\Throwable $e) { return ['error' => $e->getMessage(), 'meta' => ['generated' => date('r')]]; } } /** VAT mode toggle: inc|ex using GET → session/cookie. */ private function getVatMode() { $valid = ['inc', 'ex']; $get = isset($_GET['pl_vat']) ? strtolower((string)$_GET['pl_vat']) : null; if ($get && in_array($get, $valid, true)) { if (isset($_SESSION)) { $_SESSION['pl_vat'] = $get; } @setcookie('pl_vat', $get, time() + 31536000, '/'); // 1 year return $get; } if (isset($_SESSION['pl_vat']) && in_array($_SESSION['pl_vat'], $valid, true)) { return $_SESSION['pl_vat']; } if (isset($_COOKIE['pl_vat']) && in_array($_COOKIE['pl_vat'], $valid, true)) { return $_COOKIE['pl_vat']; } return 'inc'; } /** Build a link to set query params while preserving the current path & query. */ private function toggleUrl(array $set) { $uri = $_SERVER['REQUEST_URI'] ?? 'index.php'; $parts = parse_url($uri); $path = $parts['path'] ?? 'index.php'; $q = []; if (!empty($parts['query'])) { parse_str($parts['query'], $q); } foreach ($set as $k => $v) { $q[$k] = $v; } $qs = http_build_query($q); return $path . ($qs ? '?' . $qs : ''); } private function ukTaxYearStart(Carbon $date) { $tyStart = Carbon::create($date->year, 4, 6, 0, 0, 0, $date->timezone); if ($date->lt($tyStart)) { $tyStart->subYear(); } return $tyStart->startOfDay(); } private function formatTaxYearLabel(Carbon $tyStart) { $startYear = (int)$tyStart->format('Y'); $endYY = (int)$tyStart->copy()->addYear()->format('y'); return sprintf('%d/%02d', $startYear, $endYY); } /** * UK tax months elapsed in the tax year (tax months run 6th → 5th). * Returns an integer 1..12. */ private function ukTaxMonthsElapsed(Carbon $tyStart, Carbon $to) { if ($to->lt($tyStart)) { return 0; } $startY = (int)$tyStart->format('Y'); $startM = 4; // tax year starts in April $toY = (int)$to->format('Y'); $toM = (int)$to->format('n'); $toD = (int)$to->format('j'); $diffMonths = (($toY - $startY) * 12) + ($toM - $startM); $monthsElapsed = $diffMonths + (($toD >= 6) ? 1 : 0); if ($monthsElapsed < 1) { $monthsElapsed = 1; } if ($monthsElapsed > 12) { $monthsElapsed = 12; } return $monthsElapsed; } /** Aggregate per currency for a period; invoice-linked only in this widget. */ private function sumProfitByCurrency(Carbon $from, Carbon $to, array $opts = []) { $q = Capsule::table('tblaccounts as a') ->leftJoin('tblclients as cl', 'cl.id', '=', 'a.userid') ->leftJoin('tblcurrencies as cur', 'cur.id', '=', 'cl.currency') ->whereBetween('a.date', [$from->toDateTimeString(), $to->toDateTimeString()]) ->where('a.invoiceid', '>', 0); // invoice-linked only $rows = $q->groupBy('cl.currency', 'cur.code', 'cur.prefix', 'cur.suffix', 'cur.rate') ->orderBy('cur.code') ->get([ Capsule::raw('cl.currency as currencyId'), Capsule::raw('cur.code as code'), Capsule::raw('cur.prefix as prefix'), Capsule::raw('cur.suffix as suffix'), Capsule::raw('cur.rate as rate'), Capsule::raw('SUM(a.amountin) as amount_in'), Capsule::raw('SUM(a.fees) as fees'), Capsule::raw('SUM(a.amountout) as amount_out'), Capsule::raw('SUM(a.amountin - a.fees - a.amountout) as profit'), ]); $defaultCurrency = Capsule::table('tblcurrencies')->where('default', 1)->first(); return $rows->map(function ($r) use ($defaultCurrency) { $rate = isset($r->rate) ? (float)$r->rate : (float)($defaultCurrency->rate ?? 1.0); $cid = isset($r->currencyId) ? (int)$r->currencyId : (int)($defaultCurrency->id ?? 0); return [ 'currencyId' => $cid, 'rate' => (float)$rate, 'amount_in' => (float)$r->amount_in, 'fees' => (float)$r->fees, 'amount_out' => (float)$r->amount_out, 'profit' => (float)$r->profit, ]; })->all(); } /** * Sum VAT (tax+tax2) from invoices linked to tblaccounts in date range, * counting each invoice once (prevents double-counting on partial/multiple payments). */ private function sumInvoiceVatByCurrencyDistinctInvoices(Carbon $from, Carbon $to) { $q = Capsule::table('tblaccounts as a') ->join('tblinvoices as i', 'i.id', '=', 'a.invoiceid') ->leftJoin('tblclients as cl', 'cl.id', '=', 'a.userid') ->leftJoin('tblcurrencies as cur', 'cur.id', '=', 'cl.currency') ->whereBetween('a.date', [$from->toDateTimeString(), $to->toDateTimeString()]) ->where('a.invoiceid', '>', 0) ->groupBy('a.invoiceid', 'cl.currency', 'cur.rate'); // One row per invoice per currency $invoiceRows = $q->get([ Capsule::raw('a.invoiceid as invoiceId'), Capsule::raw('cl.currency as currencyId'), Capsule::raw('cur.rate as rate'), Capsule::raw('MAX(COALESCE(i.tax,0) + COALESCE(i.tax2,0)) as vat_total'), ]); $defaultCurrency = Capsule::table('tblcurrencies')->where('default', 1)->first(); // Reduce to per-currency totals in PHP $byCurrency = []; foreach ($invoiceRows as $r) { $cid = isset($r->currencyId) ? (int)$r->currencyId : (int)($defaultCurrency->id ?? 0); $rate = isset($r->rate) ? (float)$r->rate : (float)($defaultCurrency->rate ?? 1.0); $vat = (float)$r->vat_total; if (!isset($byCurrency[$cid])) { $byCurrency[$cid] = ['currencyId' => $cid, 'rate' => $rate, 'vat_total' => 0.0]; } $byCurrency[$cid]['vat_total'] += $vat; } return array_values($byCurrency); } /** Convert VAT rows to base-currency total. */ private function toBaseVatTotal(array $rows, $baseCurrency) { if (!$baseCurrency) { return 0.0; } $baseId = (int)$baseCurrency->id; $sum = 0.0; foreach ($rows as $r) { $vat = (float)($r['vat_total'] ?? 0); $rate = (float)($r['rate'] ?? 1.0); if ($rate == 0.0) { $rate = 1.0; } $sum += ((int)$r['currencyId'] === $baseId) ? $vat : ($vat / $rate); } return $sum; } /** Convert to base-currency total (profit only). */ private function toBaseTotal(array $rows, $baseCurrency) { if (!$baseCurrency) { return ['amount' => 0.0, 'prefix' => '', 'suffix' => '', 'code' => '']; } $baseId = (int)$baseCurrency->id; $sum = 0.0; foreach ($rows as $r) { $profit = (float)$r['profit']; $rate = (float)($r['rate'] ?? 1.0); if ($rate == 0.0) { $rate = 1.0; } $sum += ((int)$r['currencyId'] === $baseId) ? $profit : ($profit / $rate); } return [ 'amount' => $sum, 'prefix' => (string)$baseCurrency->prefix, 'suffix' => (string)$baseCurrency->suffix, 'code' => (string)$baseCurrency->code, ]; } /** Convert rows to base totals for multiple fields (amount_in, fees, amount_out, profit). */ private function toBaseTotalsMulti(array $rows, $baseCurrency) { if (!$baseCurrency) { return [ 'amount_in' => 0.0, 'fees' => 0.0, 'amount_out' => 0.0, 'profit' => 0.0, ]; } $baseId = (int)$baseCurrency->id; $totals = [ 'amount_in' => 0.0, 'fees' => 0.0, 'amount_out' => 0.0, 'profit' => 0.0, ]; foreach ($rows as $r) { $rate = (float)($r['rate'] ?? 1.0); if ($rate == 0.0) { $rate = 1.0; } $isBase = ((int)$r['currencyId'] === $baseId); $conv = function ($v) use ($isBase, $rate) { $v = (float)$v; return $isBase ? $v : ($v / $rate); }; $totals['amount_in'] += $conv($r['amount_in'] ?? 0); $totals['fees'] += $conv($r['fees'] ?? 0); $totals['amount_out'] += $conv($r['amount_out'] ?? 0); $totals['profit'] += $conv($r['profit'] ?? 0); } return $totals; } /** Safe average helper. */ private function avgPerMonth($total, $months) { $months = (int)$months; if ($months <= 0) { return 0.0; } return (float)$total / $months; } public function generateOutput($data) { if (!empty($data['error'])) { return '<div class="widget-content-padded"><strong>Error:</strong> ' . htmlspecialchars($data['error'], ENT_QUOTES, 'UTF-8') . '<br><small>Generated ' . htmlspecialchars($data['meta']['generated'], ENT_QUOTES, 'UTF-8') . '</small></div>'; } $fmt = function ($prefix, $amount, $suffix) { return sprintf('%s%s%s', htmlspecialchars($prefix ?? '', ENT_QUOTES, 'UTF-8'), number_format((float)$amount, 2), htmlspecialchars($suffix ?? '', ENT_QUOTES, 'UTF-8') ); }; $t = $data['totals']; $m = $data['meta']; $avg = $data['averages'] ?? []; $monthBase = $fmt($t['base']['prefix'] ?? '', $t['month']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $prevMonthBase = $fmt($t['base']['prefix'] ?? '', $t['prev_month']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $tyBase = $fmt($t['base']['prefix'] ?? '', $t['tax_year']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyBase = $fmt($t['base']['prefix'] ?? '', $t['last_tax_year']['amount'] ?? 0, $t['base']['suffix'] ?? ''); $mAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['month']['income'] ?? 0, $t['base']['suffix'] ?? ''); $mAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['month']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $mAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['month']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $pmAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['prev_month']['income'] ?? 0, $t['base']['suffix'] ?? ''); $pmAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['prev_month']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $pmAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['prev_month']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $tyAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['tax_year']['income'] ?? 0, $t['base']['suffix'] ?? ''); $tyAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['tax_year']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $tyAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['tax_year']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyAvgIncome = $fmt($t['base']['prefix'] ?? '', $avg['last_tax_year']['income'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyAvgExpenses = $fmt($t['base']['prefix'] ?? '', $avg['last_tax_year']['expenses'] ?? 0, $t['base']['suffix'] ?? ''); $lastTyAvgProfit = $fmt($t['base']['prefix'] ?? '', $avg['last_tax_year']['profit'] ?? 0, $t['base']['suffix'] ?? ''); $tyMonths = (int)($avg['tax_year']['months'] ?? 0); // VAT toggle UI $isVatEx = (($m['vat_mode'] ?? 'inc') === 'ex'); $vatLabel = $isVatEx ? 'Excl. VAT' : 'Incl. VAT'; $vatTo = $isVatEx ? 'inc' : 'ex'; $vatTxt = $isVatEx ? 'Switch to Incl. VAT' : 'Switch to Excl. VAT'; $vatUrl = $this->toggleUrl(['pl_vat' => $vatTo]); return <<<HTML <div class="widget-content-padded"> <div class="clearfix"> <div class="pull-left"> <strong>Totals in Base ({$t['base']['code']})</strong> <span class="label label-info" style="margin-left:8px;">Mode: Invoice-linked</span> <span class="label label-default" style="margin-left:8px;">{$vatLabel}</span> <a href="{$vatUrl}" style="margin-left:8px;">{$vatTxt}</a> </div> <div class="pull-right text-muted" style="font-size:12px"> Generated {$m['generated']} </div> </div> <table class="table table-condensed" style="margin-top:10px"> <thead> <tr> <th></th> <th class="text-right">This Month<br><small>({$m['month_label']})</small></th> <th class="text-right">Previous Month<br><small>({$m['prev_month_label']})</small></th> <th class="text-right"> Current Tax Year (to date) <br><small>({$m['tax_year_label']}: {$m['tax_year_full_range']})</small> <br><small class="text-muted">As of {$m['tax_year_asof']}</small> </th> <th class="text-right"> Last Tax Year (full) <br><small>({$m['last_tax_year_label']}: {$m['last_tax_year_full_range']})</small> </th> </tr> </thead> <tbody> <tr> <th>Total Profit (Base {$t['base']['code']})</th> <td class="text-right">{$monthBase}</td> <td class="text-right">{$prevMonthBase}</td> <td class="text-right">{$tyBase}</td> <td class="text-right">{$lastTyBase}</td> </tr> <tr> <th> Average per month (Income, Expenses, Profit) <br><small class="text-muted">Current tax year: {$tyMonths}/12 tax months elapsed; last tax year: 12/12</small> </th> <td class="text-right"> <div><small class="text-muted">Income</small> {$mAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$mAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$mAvgProfit}</div> </td> <td class="text-right"> <div><small class="text-muted">Income</small> {$pmAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$pmAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$pmAvgProfit}</div> </td> <td class="text-right"> <div><small class="text-muted">Income</small> {$tyAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$tyAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$tyAvgProfit}</div> </td> <td class="text-right"> <div><small class="text-muted">Income</small> {$lastTyAvgIncome}</div> <div><small class="text-muted">Expenses</small> {$lastTyAvgExpenses}</div> <div><small class="text-muted">Profit</small> {$lastTyAvgProfit}</div> </td> </tr> </tbody> </table> <p class="text-muted" style="font-size:12px;margin-top:8px;"> Profit formula: <code>Amount In − Fees − Amount Out</code>. UK tax year runs <strong>6 Apr – 5 Apr</strong>. <br>Average per month uses: <code>Income = Amount In</code>, <code>Expenses = Fees + Amount Out</code>, <code>Profit = Amount In − Fees − Amount Out</code>. <br><small>When “Excl. VAT” is enabled, invoice VAT (tax + tax2) is subtracted from Income and Profit. This is invoice-linked only.</small> </p> </div> HTML; } }; }); -
WHMCS Admin Widget: UK TAX Year Income/Profit/Loss
pjs32 replied to ConorBradley's topic in Developer Corner
Great widget - thanks! Is it possible to show without VAT being included on the totals?
