Jump to content

All Activity

This stream auto-updates

  1. Today
  2. 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?
  3. ## 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/
  4. 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
  5. 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
  6. Yesterday
  7. @twhiting9275, Sure thing, which ones do you need?
  8. 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
  9. 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
  10. Last week
  11. 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.
  12. 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.
  13. 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/
  14. 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.
  15. 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.
  16. To meet the new pricing announced for January 1, at a guess.
  17. 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.
  18. 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.
  19. Thanks for pointing that out — the old Automotive VPS URL has been redirected to the updated Automation VPS page. Everything should be working correctly now.
  20. @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.
  21. @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.
  22. 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
  23. Thank you for this. It looks like the sort of thing I want, but. I will investigate it further. Many thanks
  24. 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
  25. Earlier
  26. 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; } }; });
  27. Great widget - thanks! Is it possible to show without VAT being included on the totals?
  1. Load more activity
×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use & Guidelines and understand your posts will initially be pre-moderated