2
0
forked from Wavyzz/dolibarr
Files
dolibarr-fork/test/phpunit/CommonClassTest.class.php
Yannis Hoareau 1301cd36a1 NEW Subtotal module (#33502)
* Changes to follow mvc logic

* Reworked admin page and form to add lines

* Reworked adding line logic

* Adding options when editing subtotal lines

* Fix translations

* Fixed errors/displays and started pdf

* Color for subtotals pdf lines

* FIX display of subtotal totht

* Added pdf azur for propal

* Fix duplicate translation

* Added subtotal support for facture pdf

* Added subtotal support for commande pdf

* Improve UI/translations

* Restored old pdf

* Info to warn user for unsupported pdf

* Added title lines VAT rate and discount support

This is meant for future feature wich is block mass changing vat rate
and discount percentage

* Working on block apply vat and discount

* Added buttons for block actions

* Handle editing vat and discount for subtotal lines

* Editing vat and discount for subtotal lines bloc working

* Added possibility to move by block

* Updated adding and updating a subtotal line

* Improved vat and discount block update

* Improvement for block vat/discount and line edition

Editing a title line edits the corresponding subtotal line

* Improvement for moving by block

* Bad tile or st line placement managing

* Improved bad title or st line placement managing

* Adding subtotal line improved

Adding a subtotal line adds it right under its corresponding title

* Improved deleting subtotal line

Added possibility to choose if you want to delete the corresponding
subtotal line when deleting a title line

* Preventing too high level titles to be created

* Create and update line errors managing

* Improved bad title or st line placement managing

* Improved st line creation

* improve headers

* fix bad block placement managing

* fix adding st line not working if duplicates titles

* fix translations

* Fix block update

* FIX special chars bug

If special char like " ' " was used in title it could be converted to
special char entity.

* Prepare for pdf options

Pdf options like page break befor title should be shown as a picto if activated on a subtotal line on a document

* Added option managing

* Improved subtotal options and PDF integration

* Code refactor

* Action name/Error managment/PDF refreshing

When adding or updating a subtotal line

* Reformat

* Reworked subtotal options database managment

* Changed access to special code

* Remove unecessary call to php trait

* Changed definition of subtotals special code constant

* Reworked align on PDF

* Removed unecessary function and improved error managment

* Typo fix and removed treated todos

* Post typo changed for to better match subtotals names

* Disabled edit if status is not draft

* Changed way subtotals options are stored

* Added view managing when creating a document form an other

* Improved creating document from an other

Can check subtotals lines with table head checkbox and removed highlight
class for better UI.

* Make include of subtotals tpl more clear

* Manage centered or justified case

If user chose to center or justify, we don't change nothing

* Improved pdf

* Removed unecessary code block

* optimisation

* Code sniffer fix

* Code sniffer fix

* Code sniffer fix

* Code sniffer fix

* Code sniffer fix and added missing translation

* Fix php code sniffer

* Reload page when setup saved on subtotals admin page

* Fix php code sniffer

* pjan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* phan fix

* php warning fix

* php warning fix

* php phan fix

* php phan fix

* php phan fix

* Fix bug admin page not loading because of const not defined

* php phan fix

* php phan fix

* FIX subtotals admin page display

* php phan fix

* FIX php phan

* Fix bugs and langs

* Fix bug pdf align

* Replace include by require

* Add headers to avoid refreshing and adding unwanted lines

* Fix phan

* Add GETPOST check for security

* Fix phan

* Fix phan

* Fix phan

* Subtotal option when creating a document from another

* Fix phan

* Add field subtotal options for subtotal lines

* Fix phan

* Fix phan

* Fix phan

* Update to follow mvc

* Fix php phan

* Fix php phan

* Fix phpstan/phan

* Fix phpstan

* Fix phpstan

* Fix phpstan

* Fix phpstan

* Fix phpstan

* Update to switch to extraparams

* Update to switch to extraparams

* Retrieve extraparams from db to objectline

* Modified last things to switch to extraparams

* Cleaning unnecessary code lines

* Fix php-stan

* Section subtotal in extraparams to differentiate if needed for further devs

* Fix phan

* Keep extraparams when creating from another object

* Change default value to false when creating a subtotal line

* Fix clone would not keep extraparams in new object

* Fix dark subtotal line background color

Fix when a subtotal background color is too dark and edit pencil or
delete trash could not be seen

* Fix typo

* Fix typo

* Added subtotals for facturerec

* Fix precommit

* Added extraparams when creating rec from fac and other way

* Fix phan

* Fix objectline null

* Desactivating block vat / discount update for facturerec

* reformating code

* Added expeditions for subtotal

* Save extraparams for shipping lines

* Display of subtotals lines in shipments

* Display when creating facture from shipments

* Improve display of lines and invoice creation from shipments

* Fix error if missing line rang

* Deleted duplicate

* Added deletion of subtotal line in shipping documents

* Not including subtotal lines if there is no product line in between

* Update get subtotal lines in shipment docs to disable

* Delete possibility to edit subtotal lines in shipments

* Handle pdf for shipment

* Handle conf stock or shipment supporting services

* Fix precommit

* Fix duplicate name creating bug

* Fix bug where id could be changed by the line id and would create bug

* Deleted subtotal lines when STOCK_SUPPORTS_SERVICES is enabled and block would only have service lines

* Disable shipments in admin modules

Shimpements subtotals lines are only created from commands

* Fixing phan and stan

* Deleted unused template and phan fix

* Fix phan

* Fix phan

* Fix phan

* FIX: phan

* Fix template bug

If document was not in subtotal scope and would use a template used by
subtotal it would create an error.

* Fix php-stan

* Fix bad display when modules and confs were activated

* Fix php codesniffer

* Excluding subtotal lines when mass updating

* Fix shipments service lines exluded + showing subtotal line with specific configs

* Fix line display with situation invoices + bug block line update

* Added ODT managment

This works with invoices but has not been tested with other documents

* Fix bug when editing VAT/discount by block

* Fix error raised when subtotal line added

Subtotal line had no fk_product and raised the error but we want this
line to have no fk product

* Fix precommit

* Added ODT template for documents that uses subtotals

This template can be usefull to understand how to create an odt template
using subtotals module

* Fix phan

* Fix phan

* fix: buttons showing in bad document status

* clean: unwanted commited files

* add: table examples to use with subtotals on ODT templates

* fix: Unwanted print of value when creating a document from another

* feat: renaming for better understanding

* feat: Improved templates for documents related to subtotals

---------

Co-authored-by: Marc de Lima Lucio <68746600+marc-dll@users.noreply.github.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2025-05-10 02:44:31 +02:00

557 lines
16 KiB
PHP

<?php
/* Copyright (C) 2018 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2023 Alexandre Janniaux <alexandre.janniaux@gmail.com>
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* or see https://www.gnu.org/
*/
/**
* \file test/phpunit/CommonClassTest.php
* \ingroup test
* \brief PHPUnit test
* \remarks Class that extends all PHPunit tests. To share similar code between each test.
*/
// Workaround for false security issue with main.inc.php on Windows in tests:
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$_SERVER['PHP_SELF'] = "phpunit";
}
global $conf,$user,$langs,$db;
//define('TEST_DB_FORCE_TYPE','mysql'); // This is to force using mysql driver
//require_once 'PHPUnit/Autoload.php';
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
// Delete the log file to avoid problem of writing permission on it
@unlink(DOL_DATA_ROOT.'/dolibarr.log');
if (empty($user->id)) {
print "Load permissions for admin user nb 1\n";
$user->fetch(1);
$user->loadRights();
}
$conf->global->MAIN_DISABLE_ALL_MAILS = 1;
use PHPUnit\Framework\TestCase;
/**
* Class for PHPUnit tests
*
* @backupGlobals disabled
* @backupStaticAttributes enabled
* @remarks backupGlobals must be disabled to have db,conf,user and lang not erased.
*/
abstract class CommonClassTest extends TestCase
{
protected $savconf;
protected $savuser;
protected $savlangs;
protected $savdb;
/**
* Number of Dolibarr log lines to show in case of error
*
* @var integer
*/
public $nbLinesToShow = 100;
/**
* Log file from which to extract lines in case of failing test
*/
public $logfile = DOL_DATA_ROOT.'/dolibarr.log';
/**
* Log file size before a test started (=in setUp() call)
*/
public $logSizeAtSetup = 0;
/**
* Constructor
* We save global variables into local variables
*
* @param string $name Name
* @param array $data Test data
* @param string $dataName Test data name.
*/
public function __construct($name = null, array $data = array(), $dataName = '')
{
parent::__construct($name, $data, $dataName);
//$this->sharedFixture
global $conf,$user,$langs,$db;
$this->savconf = $conf;
$this->savuser = $user;
$this->savlangs = $langs;
$this->savdb = $db;
if ((int) getenv('PHPUNIT_DEBUG') > 0) {
print get_called_class()." db->type=".$db->type." user->id=".$user->id.PHP_EOL;
}
//print " - db ".$db->db;
}
/**
* setUpBeforeClass
*
* @return void
*/
public static function setUpBeforeClass(): void
{
global $conf,$user,$langs,$db;
$db->begin(); // This is to have all actions inside a transaction even if test launched without suite.
if ((int) getenv('PHPUNIT_DEBUG') > 0) {
print get_called_class()."::".__FUNCTION__.PHP_EOL;
}
}
/**
* This method is called when a test fails
*
* @param Throwable $t Throwable object
* @return void
*/
protected function onNotSuccessfulTest(Throwable $t): void
{
// Get the lines that were added since the start of the test
if (file_exists($this->logfile)) {
$filecontent = (string) @file_get_contents($this->logfile);
} else {
$filecontent = '';
}
$currentSize = strlen($filecontent);
if ($currentSize >= $this->logSizeAtSetup) {
$filecontent = substr($filecontent, $this->logSizeAtSetup);
}
$lines = preg_split("/\r?\n/", $filecontent, -1, PREG_SPLIT_NO_EMPTY);
// Determine the number of lines to show
$nbLinesToShow = $this->nbLinesToShow;
if ($t instanceof PHPUnit\Framework\Error\Notice) {
$nbLinesToShow = 3;
}
// Determine test information to show
$failedTestMethod = $this->getName(false);
$className = get_called_class();
// Get the test method's reflection
$reflectionMethod = new ReflectionMethod($className, $failedTestMethod);
// Get the test method's data set
$argsText = $this->getDataSetAsString(true);
$totalLines = count($lines);
$first_line = max(0, $totalLines - $nbLinesToShow);
// Get the last line of the log
$last_lines = array_slice($lines, $first_line, $nbLinesToShow);
// Show log information
print PHP_EOL;
// Use GitHub Action compatible group output (:warning: arguments not encoded)
print "##[group]$className::$failedTestMethod failed - $argsText.".PHP_EOL;
print "## ".get_class($t).": {$t->getMessage()}".PHP_EOL;
// Show some information about where it happened
foreach ($t->getTrace() as $idx => $trace) {
if (isset($trace['file'], $trace['line']) // Only if we have a file name
&& !preg_match('/(?:\bphar\b|Framework)/', $trace['file']) // Only if it's not in phpunit
) {
print "## backtrace($idx): From {$trace['file']}:{$trace['line']}.".PHP_EOL;
}
}
if ($nbLinesToShow) {
print "## We try to output the last ".$nbLinesToShow." lines of the log file ".basename($this->logfile)." (that has ".$totalLines." lines)".PHP_EOL;
$newLines = count($last_lines);
if ($newLines > 0) {
// Show partial log file contents when requested.
print "## Show last ".count($last_lines)." lines of dolibarr.log file -----".PHP_EOL;
foreach ($last_lines as $line) {
print $line.PHP_EOL;
}
print "## end of dolibarr.log for $className::$failedTestMethod".PHP_EOL;
} else {
print "## No new lines in 'dolibarr.log' since start of this test.".PHP_EOL;
}
}
print "##[endgroup]".PHP_EOL;
parent::onNotSuccessfulTest($t);
}
/**
* Init phpunit tests
*
* @return void
*/
protected function setUp(): void
{
global $conf,$user,$langs,$db;
$conf = $this->savconf;
$user = $this->savuser;
$langs = $this->savlangs;
$db = $this->savdb;
// Record the filesize to determine which part of the log to show on error
if (file_exists($this->logfile)) {
$this->logSizeAtSetup = (int) filesize($this->logfile);
} else {
$this->logSizeAtSetup = 0;
}
if ((int) getenv('PHPUNIT_DEBUG') > 0) {
print get_called_class().'::'.$this->getName(false)."::".__FUNCTION__.PHP_EOL;
}
//print $db->getVersion()."\n";
}
/**
* End phpunit tests
*
* @return void
*/
protected function tearDown(): void
{
if ((int) getenv('PHPUNIT_DEBUG') > 0) {
print get_called_class().'::'.$this->getName(false)."::".__FUNCTION__.PHP_EOL;
}
}
/**
* tearDownAfterClass
*
* @return void
*/
public static function tearDownAfterClass(): void
{
global $db;
$db->rollback();
if ((int) getenv('PHPUNIT_DEBUG') > 0) {
print get_called_class()."::".__FUNCTION__.PHP_EOL;
}
}
/**
* Call method, even if protected.
*
* @param object $obj Object on which to call method
* @param string $name Method to call
* @param array $args Arguments to provide in method call
* @return mixed Return value
*/
public static function callMethod($obj, $name, array $args = [])
{
$class = new \ReflectionClass($obj);
$method = $class->getMethod($name);
// If PHP is older then 8.1.0
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}
return $method->invokeArgs($obj, $args);
}
/**
* Compare all public properties values of 2 objects
*
* @param Object $oA Object operand 1
* @param Object $oB Object operand 2
* @param boolean $ignoretype False will not report diff if type of value differs
* @param array $fieldstoignorearray Array of fields to ignore in diff
* @return array Array with differences
*/
public function objCompare($oA, $oB, $ignoretype = true, $fieldstoignorearray = array('id'))
{
$retAr = array();
if (get_class($oA) !== get_class($oB)) {
$retAr[] = "Supplied objects are not of same class.";
} else {
$oVarsA = get_object_vars($oA);
$oVarsB = get_object_vars($oB);
$aKeys = array_keys($oVarsA);
if (method_exists($oA, 'deprecatedProperties')) {
// Update exclusions
foreach (self::callMethod($oA, 'deprecatedProperties') as $deprecated => $new) {
if (in_array($deprecated, $fieldstoignorearray)) {
$fieldstoignorearray[] = $new;
}
}
}
foreach ($aKeys as $sKey) {
if (in_array($sKey, $fieldstoignorearray)) {
continue;
}
if (! $ignoretype && ($oVarsA[$sKey] !== $oVarsB[$sKey])) {
$retAr[] = get_class($oA).'::'.$sKey.' : '.(is_object($oVarsA[$sKey]) ? get_class($oVarsA[$sKey]) : json_encode($oVarsA[$sKey])).' <> '.(is_object($oVarsB[$sKey]) ? get_class($oVarsB[$sKey]) : json_encode($oVarsB[$sKey]));
}
if ($ignoretype && ($oVarsA[$sKey] != $oVarsB[$sKey])) {
$retAr[] = get_class($oA).'::'.$sKey.' : '.(is_object($oVarsA[$sKey]) ? get_class($oVarsA[$sKey]) : json_encode($oVarsA[$sKey])).' <> '.(is_object($oVarsB[$sKey]) ? get_class($oVarsB[$sKey]) : json_encode($oVarsB[$sKey]));
}
}
}
return $retAr;
}
/**
* Map deprecated module names to new module names
*/
const DEPRECATED_MODULE_MAPPING = array(
'actioncomm' => 'agenda',
'adherent' => 'member',
'adherent_type' => 'member_type',
'banque' => 'bank',
'categorie' => 'category',
'commande' => 'order',
'contrat' => 'contract',
'entrepot' => 'stock',
'expedition' => 'shipping',
'facture' => 'invoice',
'fichinter' => 'intervention',
'product_fournisseur_price' => 'productsupplierprice',
'product_price' => 'productprice',
'projet' => 'project',
'propale' => 'propal',
'socpeople' => 'contact',
);
const EFFECTIVE_DEPRECATED_MODULE_MAPPING = array(
'adherent' => 'member',
'adherent_type' => 'member_type',
'banque' => 'bank',
'contrat' => 'contract',
'entrepot' => 'stock',
'ficheinter' => 'fichinter',
'projet' => 'project',
);
/**
* Map module names to the 'class' name (the class is: mod<CLASSNAME>)
* Value is null when the module is not internal to the default
* Dolibarr setup.
*/
const VALID_MODULE_MAPPING = array(
'accounting' => 'Accounting',
'agenda' => 'Agenda',
'ai' => 'Ai',
'anothermodule' => null, // Not used in code, used in translations.lang
'api' => 'Api',
'asset' => 'Asset',
'bank' => 'Banque',
'barcode' => 'Barcode',
'blockedlog' => 'BlockedLog',
'bom' => 'Bom',
'bookcal' => 'BookCal',
'bookmark' => 'Bookmark',
'cashdesk' => null,
'category' => 'Categorie',
'clicktodial' => 'ClickToDial',
'collab' => 'Collab', // TODO: fill in proper name
'comptabilite' => 'Comptabilite',
'contact' => null, // TODO: fill in proper class
'contract' => 'Contrat',
'cron' => 'Cron',
'datapolicy' => 'DataPolicy',
'dav' => 'Dav',
'debugbar' => 'DebugBar',
'shipping' => 'Expedition',
'deplacement' => 'Deplacement',
"documentgeneration" => 'DocumentGeneration', // TODO: fill in proper name
'don' => 'Don',
'dynamicprices' => 'DynamicPrices',
'ecm' => 'ECM',
'ecotax' => null, // TODO: External module ?
'emailcollector' => 'EmailCollector',
'eventorganization' => 'EventOrganization',
'expensereport' => 'ExpenseReport',
'export' => 'Export',
'externalrss' => 'ExternalRss', // TODO: fill in proper name
'externalsite' => 'ExternalSite',
'fckeditor' => 'Fckeditor',
'fournisseur' => 'Fournisseur',
'ftp' => 'FTP',
'geoipmaxmind' => 'GeoIPMaxmind', // TODO: fill in proper name
'google' => null, // External ?
'gravatar' => 'Gravatar',
'holiday' => 'Holiday',
'hrm' => 'HRM',
'import' => 'Import',
'incoterm' => 'Incoterm',
'intervention' => 'Ficheinter',
'intracommreport' => 'Intracommreport',
'invoice' => 'Facture',
'knowledgemanagement' => 'KnowledgeManagement',
'label' => 'Label',
'ldap' => 'Ldap',
'loan' => 'Loan',
'mailing' => 'Mailing',
'mailman' => null, // Same module as mailmanspip -> MailmanSpip ??
'mailmanspip' => 'MailmanSpip',
'margin' => 'Margin',
'member' => 'Adherent',
'memcached' => null, // TODO: External module?
'modulebuilder' => 'ModuleBuilder',
'mrp' => 'Mrp',
'multicompany' => null, // Not provided by default, no module tests
'multicurrency' => 'MultiCurrency',
'mymodule' => null, // modMyModule - Name used in module builder (avoid false positives)
'notification' => 'Notification',
'numberwords' => null, // Not provided by default, no module tests
'oauth' => 'Oauth',
'openstreetmap' => null, // External module?
'opensurvey' => 'OpenSurvey',
'order' => 'Commande',
'partnership' => 'Partnership',
'paybox' => 'Paybox',
'paymentbybanktransfer' => 'PaymentByBankTransfer',
'paypal' => 'Paypal',
'paypalplus' => null,
'prelevement' => 'Prelevement',
'printing' => 'Printing', // TODO: set proper name
'product' => 'Product',
'productbatch' => 'ProductBatch',
'productprice' => null,
'productsupplierprice' => null,
'project' => 'Projet',
'propal' => 'Propale',
'receiptprinter' => 'ReceiptPrinter',
'reception' => 'Reception',
'recruitment' => 'Recruitment',
'resource' => 'Resource',
'salaries' => 'Salaries',
'service' => 'Service',
'socialnetworks' => 'SocialNetworks',
'societe' => 'Societe',
'stock' => 'Stock',
'stocktransfer' => 'StockTransfer',
'stripe' => 'Stripe',
'subtotals' => 'Subtotals',
'supplier_invoice' => null, // Special case, uses invoice
'supplier_order' => null, // Special case, uses invoice
'supplier_proposal' => 'SupplierProposal',
'syslog' => 'Syslog',
'takepos' => 'TakePos',
'tax' => 'Tax',
'ticket' => 'Ticket',
'user' => 'User',
'variants' => 'Variants',
'webhook' => 'Webhook',
'webportal' => 'WebPortal',
'webservices' => 'WebServices',
'website' => 'Website',
'workflow' => 'Workflow',
'workstation' => 'Workstation',
'zapier' => 'Zapier',
);
/**
* Run php script (file) using the php binary used for running phpunit.
*
* The PHP executable may not be in the path, or refer to an uncontrolled
* version.
* This ensures that the php script is properly run on multiple platforms.
*
* @param string $phpScriptCommand The command and arguments are run by the php binary.
* @param array $output The output returned by the command
* @param int $exitCode The exit code returned for the execution.
* @return false|string False on failure, else last line if the output from the command
*/
protected function runPhpScript($phpScriptCommand, &$output, &$exitCode)
{
$phpExecutable = PHP_BINARY;
// Build the command to execute the PHP script
$command = "$phpExecutable $phpScriptCommand";
// Execute the command
return exec($command, $output, $exitCode);
}
/**
* Assert that a directory does not exist without triggering deprecation
*
* @param string $directory The directory to test
* @param string $message The message to show if the directory exists
*
* @return void
*/
protected function assertDirectoryNotExistsCompat($directory, $message = '')
{
$phpunitVersion = \PHPUnit\Runner\Version::id();
// Check if PHPUnit version is less than 9.0.0
if (version_compare($phpunitVersion, '9.0.0', '<')) {
$this->assertDirectoryNotExists($directory, $message);
} else {
$this->assertDirectoryDoesNotExist($directory, $message);
}
}
/**
* Assert that a file does not exist without triggering deprecation
*
* @param string $file The file to test
* @param string $message The message to show if the directory exists
*
* @return void
*/
protected function assertFileNotExistsCompat($file, $message = '')
{
$phpunitVersion = \PHPUnit\Runner\Version::id();
// Check if PHPUnit version is less than 9.0.0
if (version_compare($phpunitVersion, '9.0.0', '<')) {
$this->assertFileNotExists($file, $message);
} else {
$this->assertFileDoesNotExist($file, $message);
}
}
/**
* Skip test if test is not running on "Unix"
*
* @param string $message Message to indicate which test requires "Unix"
*
* @return bool True if this is not *nix, and fake assert generated
*/
protected function fakeAssertIfNotUnix($message)
{
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$this->assertTrue(true, "Dummy test to not mark the test as risky");
// $this->markTestSkipped("PHPUNIT is running on windows. $message");
return true;
}
return false;
}
}