From 17076a1e4795c0290ffc4aa8c43e2fac9f8f399b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tanguy=20Ch=C3=A9nier?= <22911157+tanguychenier@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:18:44 +0200 Subject: [PATCH] FIX Stock balance truncated to int for fractional qty (#38806) calculateBalanceForProductBefore() cast the SUM of stock movement values to (int), which truncated the fractional part. For products tracked in fractional units (kg, liters, ...), the before/after period stock balance displayed in the stock movement list (product/stock/movement_list.php) was wrong (for example a real balance of 10.5 was shown as 10). Cast the computed balance to (float) instead and update the method return type accordingly. Add a PHPUnit test (MouvementStockTest::testCalculateBalanceForProductBefore) that records a fractional reception and checks the returned balance is not truncated. --- .../stock/class/mouvementstock.class.php | 4 +- test/phpunit/MouvementStockTest.php | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/htdocs/product/stock/class/mouvementstock.class.php b/htdocs/product/stock/class/mouvementstock.class.php index 05a70765b4a..58d22729166 100644 --- a/htdocs/product/stock/class/mouvementstock.class.php +++ b/htdocs/product/stock/class/mouvementstock.class.php @@ -900,7 +900,7 @@ class MouvementStock extends CommonObject * * @param int $productidselected Id of product to count * @param integer $datebefore Date limit - * @return int Number + * @return float Number */ public function calculateBalanceForProductBefore($productidselected, $datebefore) { @@ -915,7 +915,7 @@ class MouvementStock extends CommonObject if ($resql) { $obj = $this->db->fetch_object($resql); if ($obj) { - $nb = (int) $obj->nb; + $nb = (float) $obj->nb; } return (empty($nb) ? 0 : $nb); } else { diff --git a/test/phpunit/MouvementStockTest.php b/test/phpunit/MouvementStockTest.php index 6e61b203ce8..4a23d2df855 100644 --- a/test/phpunit/MouvementStockTest.php +++ b/test/phpunit/MouvementStockTest.php @@ -48,6 +48,10 @@ $conf->global->MAIN_DISABLE_ALL_MAILS = 1; * @backupGlobals disabled * @backupStaticAttributes enabled * @remarks backupGlobals must be disabled to have db,conf,user and lang not erased. + * @phan-file-suppress PhanUndeclaredClass + * @phan-file-suppress PhanUndeclaredExtendedClass + * @phan-file-suppress PhanUndeclaredMethod + * @phan-file-suppress PhanUndeclaredProperty */ class MouvementStockTest extends CommonClassTest { @@ -239,4 +243,40 @@ class MouvementStockTest extends CommonClassTest return $localobject; } + + /** + * testCalculateBalanceForProductBefore + * + * @return void + */ + public function testCalculateBalanceForProductBefore() + { + global $conf,$user,$langs,$db; + $conf = $this->savconf; + $user = $this->savuser; + $langs = $this->savlangs; + $db = $this->savdb; + + $product = new Product($db); + $product->initAsSpecimen(); + $product->ref .= ' phpunit balance'; + $product->label .= ' phpunit balance'; + $productid = $product->create($user); + $this->assertGreaterThan(0, $productid, 'Failed to create product'); + + $warehouse = new Entrepot($db); + $warehouse->initAsSpecimen(); + $warehouse->label .= ' phpunit balance'; + $warehouseid = $warehouse->create($user); + $this->assertGreaterThan(0, $warehouseid, 'Failed to create warehouse'); + + $movement = new MouvementStock($db); + $movementdate = dol_mktime(0, 0, 0, 1, 1, 2020); + $result = $movement->reception($user, $productid, $warehouseid, 2.5, 0, 'Fractional qty for unit test', '', '', '', $movementdate, 0, 'Inventory Code Balance'); + $this->assertGreaterThan(0, $result, 'Failed to create fractional stock movement'); + + $balance = $movement->calculateBalanceForProductBefore($productid, dol_now()); + print __METHOD__." balance=".$balance."\n"; + $this->assertEquals(2.5, $balance, 'Fractional stock balance must not be truncated to an integer'); + } }