Commit graph

155976 commits

Author SHA1 Message Date
54e223decc feat: add local development server script and simplify README documentation 2026-08-19 09:58:42 +05:30
a810ac47d1 NEW: #0 Add Forgejo deployment workflow for beta branch 2026-08-19 09:58:19 +05:30
Laurent Destailleur
1af9b2d870 Merge branch 'develop' of git@github.com:Dolibarr/dolibarr.git into develop
Some checks failed
CI-PUSH / pre-commit (push) Failing after 12m22s
CI-PUSH / phan (push) Has been skipped
CI-PUSH / phpstan (push) Has been skipped
CI-PUSH / windows-ci (push) Has been cancelled
2026-08-19 04:52:01 +02:00
Laurent Destailleur
c16e51ea4a Merge branch '24.0' of git@github.com:Dolibarr/dolibarr.git into develop 2026-08-19 04:51:37 +02:00
Laurent Destailleur
19dd30672b CSS 2026-08-19 04:50:01 +02:00
Laurent Destailleur
6fad0f7312 Merge branch '24.0' of git@github.com:Dolibarr/dolibarr.git into 24.0 2026-08-19 04:35:22 +02:00
Laurent Destailleur
fdef665e72 Trans 2026-08-19 04:20:31 +02:00
VIAL-GOUTEYRON Quentin
ea54342891
NEW Drag and drop a file on the 23 remaining cards (#39473)
* FIX Drag and drop of a file reports a wrong error, or none at all

The error handler of dragAndDropFileUpload() had three defects that all end on
the user believing the file was attached when it was not.

 - The first assignment of the message was dead code, there was no return after
   the test on the http code 403, so a refusal was reported as a generic error.
 - The key ErrorUploadPermissionDenied it used exists in no language file, so
   the raw key was shown.
 - JSON.parse() was called with no try/catch on an answer that is not always a
   json, a fatal error of the endpoint or a request over post_max_size for
   example. The exception left the user on a page with no message at all.
 - An empty list of files was treated as a success, while it means the endpoint
   stored nothing.

The value of PHP_SELF is also escaped before it is written into the 6 generated
javascript strings. It holds the path info of the request on a server that
accepts it, so it is a user input. dol_escape_js() is called with the mode that
escapes a double quote by a double quote, the strings being delimited by double
quotes, and the sequence '</' is escaped too because the function does not do it
and the path info could otherwise close the script tag and open one of its own.

Adds the key ErrorOnAtLeastOneFileUpload to en_US, used when some files of a
batch failed and some did not.

* FIX getMultidirOutput returns a directory the Documents tab does not read

Four defects of the same function, all ending on a file stored where the user
will never see it, or written outside the documents directory.

 - The ref of the project of a task was only passed through dol_sanitizePathName(),
   which keeps a slash, a colon and the accented chars, while projet/tasks/document.php
   sanitizes it with dol_sanitizeFileName(). A project ref holding a slash even
   created an extra level of directory. Measured on real databases: 304 projects
   over 330 hold a slash on one of them, carrying 701 tasks over 824.
 - The same case calls $object->fetchProject() with no guard, while the signature
   of the function accepts an object that is not a CommonObject, and even a null
   when a module is given. Such a caller gets a fatal error where it expects the
   error string. No core caller is in that case today, an external module or a
   hook can be.
 - The entity of the object may have no declared directory, an object shared by
   another entity for example. The undefined index returned a relative path, so
   the caller read or wrote under the web root. The current entity is used
   instead, and the fallback is logged because the directory is then not the one
   of the entity of the object, which matters for a caller that deletes files.
 - When the current entity has no declared directory either, the fallback
   returned the sub directory alone, again a relative path. The same error than
   for a module that declares no directory at all is now returned.

Adds the sub directory of a partnership and of a stock transfer, which their own
document tabs already read.

* FIX getElementProperties answers wrong properties for 9 elements

A customer payment, a supplier payment, a various payment, a stock transfer and
the 4 objects of the hrm module had no properties at all, or wrong ones, so any
caller that resolves a class, a table or a document directory from an element
failed on them.

 - payment, payment_supplier and payment_various had no branch. The branch of a
   customer payment tests $elementType and not $element, because the rule on the
   elements named myobject_mysubobject rewrites $element to 'payment' for
   'payment_salary' too, which is stored somewhere else.
 - job, position, skill and evaluation answered a wrong table (hrm_job_user for
   a position, and so on) and no sub directory, while their document tabs read
   one named after the element.
 - stocktransfer answered an empty classname, because it is not the ucfirst() of
   the element, so a caller doing new $classname($db) ended on a fatal error.
 - The sub directory was concatenated even when the module is disabled and the
   directory is empty, which answered a path at the root of the file system.
 - A contact and a conference are stored into a sub directory their tab reads.

isModEnabled('invoice') is tested for a customer payment: there is no module
named 'compta', so testing it was always false, while $conf->compta->payment is
set unconditionally by Conf::setValues() and could not be used as a proxy.

* FIX Access refused to everyone on 11 objects of the core

restrictedArea() and checkUserAccessToObject() refuse the access to objects that
no permission and no rule can match, whatever the user, an administrator
included.

 - The hrm module declares no permission at its first level, only 'all', and the
   stocktransfer module only 'stocktransfer'. A check on the module itself
   therefore tests a permission that does not exist. The mapping is the same one
   as into User::hasRight().
 - The module of an event organization declares no permission at all, its whole
   permission block being commented out on purpose, and its cards check the
   parent project instead. The feature is mapped onto that project, with the two
   guards the card has: an external user is refused, and so is a conference with
   no parent project, whose id of 0 would otherwise grant an access with no check
   on the record at all.
 - The default rule of checkUserAccessToObject() builds its sql on the columns
   entity and fk_soc of the table. llx_asset, llx_paiement, llx_paiementfourn and
   llx_workstation_workstation have no fk_soc, and llx_hrm_job, llx_hrm_job_user
   and llx_hrm_skill have neither. The sql failed, so the access was refused to
   every user this rule applies to. These tables are now checked on their entity
   only, which is what the $check rule already does for the same class of tables,
   and the 3 tables of hrm can be checked on nothing at all. The rule is selected
   on the table and not on the element, because $object is an id and not an
   object for most of the callers, the cards of an asset and of a workstation
   included, which are broken today for any user without the permission to see
   all third parties.
 - An external user is refused explicitly on those tables: none of these objects
   is linked to a third party, so the default rule refused him through a link
   that does not exist, and the rules that replace it do not look at the third
   party of the user at all.

Measured on a vanilla instance with 5 profiles, an administrator, an internal
user with every right, one without the permission to see all third parties, one
that is not a sales representative of the third party of the object, and an
external user: the 11 objects go from refused to granted for the internal users
and stay refused for the external one, and the 26 other elements answer exactly
the same for the 5 profiles.

* FIX A file dropped on a card is lost, or reported as refused when it was stored

FileUpload stores the file into a directory that the "Attached files" tab of the
object never reads, so the user attaches a file that no screen will ever show,
and nothing is indexed in database to find it back. Measured on real databases:
216487 thirdparties over 216887 and 157852 products over 280319 are in that case
on the cards that already enable the drag and drop.

 - The directory of the object is now forged with get_exdir(), the way the tabs
   do: it always uses the id for a thirdparty, whose ref is a company name and is
   not unique, and it falls back on the id when the ref is empty. The sub
   directory of the module is read with getMultidirOutput(), which knows the
   elements that store their documents into one. That function does not return
   an empty string when it fails but a string starting with 'error-', so only an
   absolute path is accepted: writing into that string would create the files
   under the web root.
 - fetchObjectByElement() returns an object even when fetch() returned 0. The
   object was then not loaded, and the file was stored at the root of the
   directory of the module, out of any object. The constructor now throws, and
   the endpoint answers the error with the same json contract than a successful
   call so that the caller can show it, instead of a fatal error and an http 500.
 - An attachment of the same name was silently overwritten, dol_move_uploaded_file()
   being called with $allowoverwrite = 1 while the name was checked before the
   ref of the object was added as a prefix. The check is done again on the final
   name, and on the .noexe suffixed name too, which that function appends to an
   executable file.
 - An executable file was renamed with that .noexe suffix and then reported as an
   error, while it was correctly stored.
 - The endpoint called restrictedArea() with an empty feature when the element is
   unknown, and the loop of that function then takes no branch at all and grants
   the access with no check. It refuses before, with the same http code and the
   same message than a refusal, so that a user cannot tell an object that exists
   but is not allowed from an object that does not exist. The refusal of an
   external user on an object of another third party answered a message of its
   own, which allowed the same enumeration.

Comes with the tests of the path resolution, of the fallback of get_exdir(), of
the rejection of the error string of getMultidirOutput(), and of the file name
deduplication.

* FIX Remove the drop area from 13 cards where dropping a file is harmful

Two distinct groups, both of them removing a drop area that only produces a
result the user does not want.

7 pages have no "Attached files" tab at all, so a file dropped on them can be
reached by no screen: a fiscal year and its info page, an accounting model, a
webhook trigger history, an intracomm report, a bookcal calendar and its booking
list. Measured: 4 of them accepted the upload and wrote an orphan file, the
accounting model wrote it at the root of the directory of the module, out of any
object, and the 2 bookcal ones answered an http 500.

6 cards print their tabs inside their edit form, so the drop area covered that
form: a product, an expense report, a VAT payment, a social contribution, a loan
and a salary. Dropping a file there reloads the page, which discards what the
user is typing. A product and an expense report have a second call for the read
only view, so the drop area is only removed from the call of the edit branch;
the 4 others share a single call between both views, so the parameter is now
conditional.

* NEW Drag and drop a file on the 23 remaining cards

Every card of the core that owns an "Attached files" tab can now receive a file
by drag and drop, which was the case of 32 of them only. The 23 added here are
an asset, an event, a various payment, a customer payment, a contact, a donation,
a conference or booth, a shipment, a supplier payment, a leave request, an
evaluation, a job, a position, a skill, a knowledge record, a manufacturing
order, a partnership, a lot, a stock transfer, a task, a reception, a resource
and a workstation.

The tab bar of a leave request and of a resource is printed inside their edit
form, so the drop area is not enabled there: dropping a file reloads the page,
which would discard what the user is typing. The card of a supplier payment
prints its tab bar even when the object was not loaded, so the drop area is only
enabled when it is.

* FIX getMultidirOutput refuses an entity with no directory instead of falling back [skip-claudemd]

The previous revision of this PR fell back on the directory of the current
entity when the entity of the object had none, with a LOG_WARNING. On a
multicompany install that made a caller read, write and above all delete
files in the directory of another entity. Refuse instead: the function
already answers 'error-diroutput-not-defined-for-this-object' when the
module declares no directory at all, so the caller has one behaviour to
handle, not two.

The entity is cast to int, which is what the array index needs and what
silences the four PhanTypeMismatchDimFetchNullable this function reported.

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-19 02:46:21 +02:00
VIAL-GOUTEYRON Quentin
5847c6b687
NEW : Add llx_socpeople.use_thirdparty_address column (#39499)
Data structure only, submitted standalone as asked by CONTRIBUTING.md, so it can be
reviewed and accepted before the code that uses it.

A contact can either carry its own address or reuse the address of its thirdparty.
Today the choice is guessed from the content of the address fields, which cannot tell
an empty address from a deliberate "use the thirdparty address". The new column stores
the choice explicitly.

  0     = use the address of the contact
  1     = use the address of the linked thirdparty
  NULL  = legacy resolution, the value for every existing record, so an existing
          alternative address stays independent from its thirdparty address.

Code side: #38118.
2026-08-19 02:41:55 +02:00
Frédéric FRANCE
1cafc092df
pdf logo header mutualization (#39503)
* FIX: Restore PHPUnit 11 compatibility in CommonClassTest

onNotSuccessfulTest() overrode PHPUnit\Framework\TestCase's method with
an incompatible return type (void vs never), and the assertMatchesRegularExpression
back-compat shim for PHPUnit <8 now illegally overrides that method, which
PHPUnit 11 declares final. Both faults made every test extending
CommonClassTest fatal immediately on this environment's PHPUnit 11.5.55.
Pre-existing breakage, unrelated to the PDF mutualization work in this
branch — fixed here because Task 1's plan requires a passing
BuildDocTest.php baseline before any implementer is dispatched.

* NEW: Mutualize PDF header logo block into pdf_writeLogoOrCompanyName() (sponge, einstein, azur, muscadet)

* NEW: Mutualize PDF header logo block into pdf_writeLogoOrCompanyName() (cornas, standard, cyan, octopus, strato)

* FIX: Resolve $logodir before calling pdf_writeLogoOrCompanyName() in cornas, standard, cyan, octopus, strato

* NEW: Mutualize PDF header logo block into pdf_writeLogoOrCompanyName() (eratosthene, eagle_proforma, standard_asset)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* NEW: Mutualize PDF header logo block into pdf_writeLogoOrCompanyName() (aurore, zenith)

* FIX: Complete PHPUnit 11 compatibility fix in CommonClassTest::onNotSuccessfulTest

The prior compatibility fix (f29129aae43) resolved the two class-load
fatals but left onNotSuccessfulTest() calling getName()/getDataSetAsString(),
neither of which exists on PHPUnit 11's TestCase (renamed to name() and
dataSetAsStringWithData()/dataSetAsString()). The undefined-method error
was silently absorbed by PHPUnit's own exception handling, so every test
failure across the suite still ran and reported correctly, but the
method's entire diagnostic block (log tail, backtrace, DB info dump) never
executed. Verified with a throwaway reproduction test before and after:
the diagnostic ##[group] output was empty before this fix and complete
after it.

Found during the final whole-branch review of the PDF logo-block
mutualization branch; unrelated to that refactor but touches the same
file already modified by the earlier compatibility commit.

* FIX: Drop unneeded by-ref $pdf and rename $ltrdirection to $align in pdf_writeLogoOrCompanyName()

TCPDF objects are handles, so passing $pdf by reference bought nothing
and was inconsistent with every sibling function in this file
(pdf_pagehead, pdf_watermark, pdf_bank, pdf_pagefoot, ...), all of which
take $pdf by value. $ltrdirection was also a misnomer: three of the 14
callers pass the literal 'L' and one passes 'J' (justify) - none of those
is a text direction, it's an alignment. Renaming is call-site-transparent
(all 14 callers use positional arguments).

Found during the final whole-branch review of the PDF logo-block
mutualization branch.

* NEW: Document pdf_writeLogoOrCompanyName() helper in ChangeLog

Per repo convention (CLAUDE.md: "Always update ChangeLog for
significant changes"). Flagged during the final whole-branch review
of the PDF logo-block mutualization branch: a new reusable pdf_lib.php
function is part of the surface third-party PDF document-model modules
build against.

* restore commonclasstest

* fix

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-19 02:37:18 +02:00
Frédéric FRANCE
b7413ee622
NEW: Improve phpunit test coverage for Commande, Propal and Facture (#39517)
* NEW: Improve phpunit test coverage for Commande, Propal and Facture

Add line CRUD (addline/updateline/deleteLine) and total-consistency
assertions, reuse the specimen-invariant regression check (previously
only in FactureTest) across all three, and add a new integration test
covering the full Propal -> Commande -> Facture conversion chain
(thirdparty, notes, totals and object_linked propagation).

Two shared assertions were added to CommonClassTest for this:
assertLineTotalsMatchHeader() and assertMatchesFreshSpecimen().

* Update commande.class.php

* FIX PropalCommandeFactureWorkflowTest reads wrong invoice after createFromOrder

Facture::createFromOrder() returns a status flag (1/-1), not the new
invoice id, unlike Commande::createFromProposal(). The test was doing
fetch($result) with $result==1, which happened to no-op locally (no
invoice with rowid=1) but fetched an unrelated invoice in CI, causing
a spurious thirdparty mismatch. Use $facture->id, already set by
create() inside createFromOrder(), like every other caller of this
method does.
2026-08-19 02:35:04 +02:00
Frédéric FRANCE
b254e27660
NEW: Add phpunit test for Mo class (#39535)
* NEW: Add phpunit test for Mo class

Add a unit-level test for the Mo (manufacturing order) class, which
had no direct test coverage yet (RestAPIMoTest.php only exercises
the REST API layer over HTTP, and its most interesting part -
produce/consume - is entirely commented out there).

Covers create() (including the automatic "to produce" line it
creates for the finished product), fetch, update, the
draft -> validated -> canceled -> validated status workflow
(validate/cancel/reopen, including the provisional ref being
replaced on validate()), and delete.

Uses a freshly created specimen Product for fk_product rather than a
random real catalog product: Mo::create() rejects kit/BOM products
unless ALLOW_USE_KITS_INTO_BOM_AND_MO is set, and a specimen product
is guaranteed not to be one.

The module is auto-activated in setUpBeforeClass() if not already
enabled, following the same pattern as the other recently added
tests (activation is real and persists after the test run, it is not
undone by the rollback in tearDownAfterClass - see comment in
setUpBeforeClass() for why).

* FIX MoTest crash in the full test suite (stale $db)

Same class of bug as StockTransferTest (see that commit for the full
analysis): modMrp depends on modBom, which itself depends on
modProduct, whose constructor queries the DB via Societe::useNPR().
If $db is stale/closed when this class's setUpBeforeClass() runs in
the full suite (all classes in one continuous process), activating
modMrp crashes on the dead connection the same way.

Reconnect $db (and refresh $mysoc->db/$user->db, which stashed their
own reference to the old connection at bootstrap) before calling
activateModule(), same pattern as StockTransferTest.

Verified the same way: closing $db and calling
MoTest::setUpBeforeClass() directly reproduces the crash without
this fix and is resolved with it.

* FIX MoTest: resync $mysoc/$user->db unconditionally

Same follow-up fix as StockTransferTest (see that commit for the full
analysis): the previous defensive-reconnect only refreshed
$mysoc->db/$user->db inside the branch where the global $db itself
was detected stale. $mysoc/$user can diverge from a healthy $db
independently (they stash their own ->db reference at bootstrap), so
resync them unconditionally instead.

Verified the same way: closing the original $db but reconnecting only
the global $db variable (leaving $mysoc->db pointing at the closed
one) reproduces the crash with the old code and is resolved with this
fix.

* NEW Centralize the stale-$db reconnect logic in CommonClassTest

Same follow-up as StockTransferTest (#39542): the defensive
reconnect-and-resync-$mysoc/$user logic was duplicated identically
across 3 test classes. Extract it into a shared
CommonClassTest::ensureDbIsConnected() helper so future test classes
that activate a module depending on modProduct don't need to
reimplement it, and so the fix can't drift between test files.

MoTest::setUpBeforeClass() now just calls self::ensureDbIsConnected()
before activateModule().

Re-verified the same way as before: closing the original $db but
reconnecting only the global $db variable (leaving $mysoc->db
pointing at the closed one) still reproduces the crash without this
fix and is resolved with it.

* Remove setUpBeforeClass from MoTest

Removed setUpBeforeClass method to simplify test setup.

* Update CommonClassTest.class.php

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-19 02:28:13 +02:00
VIAL-GOUTEYRON Quentin
2d44fdf49c
CLOSE #29244 Feature to merge contacts (#39512)
Add the ability to merge two contacts, the same way third parties and members
can already be merged. The merged contact is absorbed by the current one, all
its satellite data is moved, then it is deleted.

Contact::mergeContact() orchestrates everything in a single transaction, using
the new CommonObject::commonReplaceContact() helper and the replaceContact()
static methods of Contact, ActionComm and User. A replaceContact hook lets the
external modules move their own data. No schema change, no migration.

Three specific traps of this table are handled:
- llx_element_contact.fk_socpeople references llx_socpeople only when
  c_type_contact.source is 'external', and llx_user when it is 'internal', so
  both queries filter on it. Without that filter the internal assignments of
  the users would silently be moved to the merged contact.
- Contact::updateRoles(), called by update(), deletes then reinserts every
  societe_contacts row of the contact from $this->roles. It is neutralised by
  setting roles to null, and update() is called before the links are moved.
- Every table having a unique index on the contact id is deduplicated before
  its update, so the update cannot violate it. $ignoreerrors is never used:
  the FIXME of commonReplaceThirdparty() documents the links it loses.

The files are moved after the commit and not before, unlike mergeCompany() and
mergeMembers(): a failure after the move leaves them with the database rolled
back but the files already gone. A name collision renames the file instead of
losing it, and a failure is reported to the user instead of being ignored.

Access guards live in the class and not in the page, so the REST API, the
scheduled jobs and the modules benefit from them too. They cover both contacts,
fetch() by rowid applying neither an entity nor a permission filter: entity,
private contact of another user, external user, sales representative perimeter,
absorbing a shared contact into a private one, and a contact linked to a user
account (moving llx_user.fk_socpeople would let the email of that user be
rewritten from the contact card, so it requires the permission to create users).

Form::select_contact() gets a one line fix on the way: it accepts a $filter
parameter, documents it, forwards it to the ajax branch, but dropped it when
building the combo, so the merge popup could not exclude the current contact.

Known limitations, out of the scope of this pull request: merging contacts of
different entities is refused, Contact::update() neither writes url, no_email,
fk_parent nor import_key while its $nosyncuser parameter is declared but never
read, and with CONTACT_USE_SEARCH_TO_SELECT set the current contact is still
offered in the popup, contact/ajax/contact.php overwriting the filter it gets.
2026-08-19 02:26:34 +02:00
Frédéric FRANCE
a0b0e44278
NEW Link manage extrafields and phpunit test for the class (#39514)
* NEW: Add extrafields support to Link class

Link now declares isextrafieldmanaged and wires fetch_optionals()/
insertExtraFields()/deleteExtraFields() into fetch(), fetchAll(),
create(), update() and delete(), matching the llx_links_extrafields
table added previously.

* FIX: Several bugs in Link class review

- create(): wrong duplicate-record error message (copy-pasted from
  Societe, referenced undefined $this->name) replaced by the generic
  ErrorDuplicateField, consistent with update().
- create()/update(): missing "NoURL" translation key replaced by the
  standard ErrorFieldRequired pattern.
- fetch(): guard against running an unfiltered query (no rowid, no
  hashforshare) that could silently return an arbitrary link; a caller
  in actions_linkedfiles.inc.php could hit this when 'linkid' was
  missing from the request.
- actions_linkedfiles.inc.php: check fetch() result with `> 0` instead
  of a truthy test, since -1 (error) is truthy in PHP.
- delete(): add User type hint (consistent with create()/update()) and
  a $notrigger parameter to optionally skip the LINK_DELETE trigger.
- update(): fix copy-pasted docblock ("third party" -> "link").

* NEW: Add PHPUnit test for Link class

Covers create/fetch/update/fetchAll/count/delete, plus regression
tests for the two bugs fixed in the previous commit: create() rejects
an empty url, and fetch() rejects a call with neither rowid nor
hashforshare instead of returning an arbitrary record.

* FIX Phan false positive on $object in actions_linkedfiles.inc.php

Phan's ambient type inference for the loosely-typed global $object in
this shared include file was picking up CommonSocialNetworks (an
unrelated trait, not even a class), reported as undeclared
->id/->entity/->addThumbs()/->delThumbs() in a real CI Phan run on
this branch, once this file was analyzed on its own via the
changed-files file-list (this file has no prior baseline entry, so it
was apparently never previously exercised in isolation like this).

Force $object's type explicitly to CommonObject via the same
@phan-var-force string-literal idiom this file already uses for
$upload_dir/$upload_dirold/$confirm/$forceFullTextIndexation -
CommonObject genuinely declares addThumbs()/delThumbs(), which
resolves those two errors outright. $id/$entity remain reported as
PhanUndeclaredProperty (CommonObject itself does not declare them,
only its concrete subclasses do at runtime) - baseline-suppress that
for this file the same way it is already suppressed for the sibling
shared-include files actions_addupdatedelete.inc.php,
actions_massactions.inc.php and actions_sendmails.inc.php, which have
the exact same $object typing situation.
2026-08-19 02:18:46 +02:00
VIAL-GOUTEYRON Quentin
47978f21e0
FIX : Empty contact name in the CONTACT_MODIFY agenda event (#39515)
The label of the automatic agenda event logged when a contact is modified is
built from $object->name, a property the Contact class does not declare and
fetch() never fills, so the name is always empty and PHP 8 reports an undefined
property. The CONTACT_CREATE branch a few lines above already uses
getFullName(), which is what this branch was meant to call too.

Also adds the @phan-var-force annotation the neighbouring branches carry.
2026-08-19 02:17:27 +02:00
Frédéric FRANCE
0ba0fb0625
NEW: Add phpunit tests for Don and PaymentDonation classes (#39539)
Add tests for Don and PaymentDonation, which had no test coverage
yet: create/fetch/update/delete, the draft -> validated -> paid
status workflow (setValid/setPaid) and a separate self-contained
cancel test (set_cancel() has no status precondition, unlike
setValid()/setPaid()).

Note: create() never writes $this->status to the fk_statut column
(it always defaults to draft in DB) even though initAsSpecimen() sets
status=1 in memory - the test re-fetches to check the real status.

PaymentDonationTest also cross-checks Don::getRemainToPay() after a
full payment: unlike Tva::addPayment()/getSommePaiement() (see
TvaTest.php), PaymentDonation::create() and Don::getRemainToPay() do
agree on the same fk_donation link, so this is a genuine regression
check, not a workaround.

The module is auto-activated in setUpBeforeClass() if not already
enabled, following the same pattern as the other recently added
tests (activation is real and persists after the test run, it is not
undone by the rollback in tearDownAfterClass - see comment in
setUpBeforeClass() for why).

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-19 01:40:33 +02:00
Frédéric FRANCE
efe2137240
NEW: Add phpunit test for RemiseCheque class (#39543)
Add a test for RemiseCheque (chequereceipt / bank check remittance
slip), which had no test coverage yet.

RemiseCheque does not create a standalone record: it gathers existing
not-yet-remitted check payments (llx_bank rows with fk_type='CHQ' and
fk_bordereau=0) for a given bank account into a receipt. The fixture
therefore goes through the real path: create+validate an invoice, pay
it with a CHQ-coded payment, reconcile it to a bank account (this is
what produces the llx_bank row RemiseCheque::create() picks up), then
remit that check. Covers create/fetch/validate/delete.

Note: unlike most other classes in this suite, RemiseCheque::delete()/
updateAmount() return an "errno" convention (0=success, <0=error)
rather than the usual ">0=success" one used by create()/validate() in
the same class - see docblock and inline comments.

No module activation needed here: the cheque receipt feature is not
gated behind any separately activatable module (only the always-on
bank module).
2026-08-19 01:39:35 +02:00
Frédéric FRANCE
46e48e2a82
NEW: Add phpunit tests for LoanSchedule and PaymentLoan classes (#39537)
Add CRUD tests (create/fetch/update/delete) for LoanSchedule and
PaymentLoan, which had no test coverage yet, each against a freshly
created specimen Loan. LoanScheduleTest also covers
calcMonthlyPayments() (pure amortization formula, no DB) with both
the zero-rate and standard amortization branches.

Note: PaymentLoan::create() is inconsistent with its own fetch()/
update(): it declares (and even cleans) $fk_loan and $fk_typepayment
like fetch()/update() use, but its INSERT statement actually reads
$chid and $paymenttype instead (left uncleaned). This matches the
real caller (loan/payment/payment.php), so the test sets chid/
paymenttype for create() - see class docblock in PaymentLoanTest.php.

The module is auto-activated in setUpBeforeClass() if not already
enabled, following the same pattern as the other recently added
tests (activation is real and persists after the test run, it is not
undone by the rollback in tearDownAfterClass - see comment in
setUpBeforeClass() for why).
2026-08-19 01:37:51 +02:00
Frédéric FRANCE
f79e1764e8
NEW Add PHPUnit tests for dol_print_size, dolGetFirstLastname, jsonOrUnserialize, picto_from_langcode (#39553)
Continuing the audit of commonly-used but untested functions in
functions.lib.php.

- testDolPrintSize: the 10240-byte threshold that decides whether
  $shortvalue actually converts to Kilobytes, and the short vs long
  unit label.
- testDolGetFirstLastname: all 6 documented $nameorder values (0-5),
  including the "if defined else the other" fallback behavior of
  3 and 5, and the -1 auto mode.
- testJsonOrUnserialize: valid JSON (object and array forms), a
  legacy PHP-serialized string falling back to unserialize(), and a
  string that is neither (returns false; the resulting unserialize()
  warning is expected and suppressed in the test).
- testPictoFromLangcode: empty input, the 'auto' special case, a
  hardcoded special-cased language code (fr_CA), the generic
  'xx_YY' -> country-part flag resolution, a bare country code, the
  $notitlealt flag, and how a 'class="..."' in $moreatt merges into
  the span's own class instead of becoming a separate attribute.

All expected values were verified empirically against the real
function outputs before being written into the test.
2026-08-19 01:20:00 +02:00
dependabot[bot]
5a41ffd8b2
Bump mdeweerd/logToCheckStyle from 2026.7.1 to 2026.8.2 (#39565)
Bumps [mdeweerd/logToCheckStyle](https://github.com/mdeweerd/logtocheckstyle) from 2026.7.1 to 2026.8.2.
- [Commits](https://github.com/mdeweerd/logtocheckstyle/compare/v2026.7.1...v2026.8.2)

---
updated-dependencies:
- dependency-name: mdeweerd/logToCheckStyle
  dependency-version: 2026.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-19 01:16:46 +02:00
Pichinov-Jose
0dd56d89a2
Add thirdparty (supplier) line to FactureFournisseur and SupplierProposal tooltips (#39570)
CommandeFournisseur::getTooltipContentArray() already adds a "Supplier"
line (thirdparty getNomUrl) to the getNomUrl tooltip, but
FactureFournisseur and SupplierProposal do not. Hovering a supplier
invoice or supplier proposal link therefore never shows which thirdparty
it belongs to, an inconsistency between three otherwise-similar objects.

Align both with CommandeFournisseur: add the same $datas['supplier']
entry, right after RefSupplier, honoring the 'nofetch' param.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 01:03:48 +02:00
Régis Houssin
950628b540
QUAL add hook printWebsitePageContent (#39576)
* FIX avoid php warnings (#39556)

* Fix help

* Doc

* FIX: Broken feature 'Generate Child Mo' (#39560)

* Fix broken 'Generate Child Mo'

* Fix phan

* QUAL add hook printWebsitePageContent

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
Co-authored-by: Francis Appels <francis.appels@yahoo.com>
2026-08-19 01:01:45 +02:00
MDW
994d8c5ead
Qual: Update Phan baseline (#39578)
# Qual: Update Phan baseline
2026-08-19 00:43:40 +02:00
Frédéric FRANCE
597b91ab65
fix CI dolSort() (#39582)
* fix CI dolSort()

* Update functions.lib.php

* Update functions.lib.php

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-19 00:42:05 +02:00
Laurent Destailleur
179112007f Merge branch 'develop' of git@github.com:Dolibarr/dolibarr.git into develop 2026-08-18 14:06:56 +02:00
Laurent Destailleur
c56858af27 Merge branch '24.0' of git@github.com:Dolibarr/dolibarr.git into develop 2026-08-18 14:06:45 +02:00
Laurent Destailleur
e6448ea675 Fix cloning of page 2026-08-18 13:53:03 +02:00
Laurent Destailleur
2e627e55cb Fix import sql with -- into content 2026-08-18 13:37:28 +02:00
Laurent Destailleur
e5afc4a0b4 Fix autoset of discount percent when choosing a product line 2026-08-18 13:31:18 +02:00
Laurent Destailleur
c85d0e8407 Fix add guard on salary field permission when module salary is -
reported by vulncheck
2026-08-18 12:45:00 +02:00
Laurent Destailleur
c100564d05 Fix missing protection for external user - reported by Vulncheck 2026-08-18 12:31:40 +02:00
Laurent Destailleur
6873eca813 Fix: do no try to insert if calendar closed. 2026-08-18 12:15:11 +02:00
Laurent Destailleur
5391c3e313 Fix refuse use of .. into PHP_SELF - reported by Wenhao Wu 2026-08-18 12:01:01 +02:00
Laurent Destailleur
6acce68070 Doc 2026-08-18 10:43:30 +02:00
Laurent Destailleur
da94a9b1ae Doc 2026-08-18 10:40:28 +02:00
VIAL-GOUTEYRON Quentin
e837854094
NEW: ModuleBuilder - consistent status labels and explicit trigger naming (#38883)
* NEW : Derive ModuleBuilder status labels from arrayofkeyval

LibStatut() in the ModuleBuilder object template hardcoded the
Draft/Enabled/Disabled labels, which diverged from the labels defined
in the 'status' field arrayofkeyval used by the list filter and the
select. Build labelStatus/labelStatusShort from arrayofkeyval so the
badge, the filter and the select all show the same configurable labels.

Add ModuleBuilderTemplateConventionsTest covering the derivation.

Next: normalize trigger codes to MYMODULE_MYOBJECT_ACTION in class line 637 + typo line 50, sql/data.sql lines 27/30-32, myobject_card.php line 378; extend the test with trigger assertions.

* NEW : Use explicit MODULE_OBJECT_ACTION trigger naming in template

The ModuleBuilder object template emitted the validate trigger as the
generic MYOBJECT_VALIDATE, inconsistent with the MYMODULE_MYOBJECT
prefix already advertised by $TRIGGER_PREFIX and used by the
unvalidate/cancel/reopen/sentbymail trigger codes. Normalize the
validate trigger, the seed data.sql agenda triggers and the close
notification example to the MYMODULE_MYOBJECT_<ACTION> policy so
generated modules expose clear, non-ambiguous trigger codes. Also fix a
typo in the $TRIGGER_PREFIX comment.

Extend ModuleBuilderTemplateConventionsTest with trigger naming checks.

Next: ChangeLog entry covering both features, then dolibarr-audit + functional verification, then push to remote quentin and run pr-review-v2.

* DOC : Add ChangeLog entries for ModuleBuilder status labels and triggers

Next: dolibarr-audit on both features, then functional verification, then push to remote quentin and run pr-review-v2.

* FIX : Guard ModuleBuilder LibStatut against undefined status key

Hardening from audit: when LibStatut() is called with a status value
that is not present in the 'status' field arrayofkeyval, reading
labelStatus/labelStatusShort raised an undefined-array-key warning under
PHP 8. Default both labels to an empty string, which dolGetStatus
already accepts, so the badge degrades gracefully to the status code.

Next: push branch to remote quentin and run dolibarr-pr-review-v2 on the full diff vs develop.

* CHORE Drop the files CONTRIBUTING forbids to edit in a PR [skip-claudemd]

ChangeLog is generated from the commit messages at release time, and the
language files other than en_US are synced from Transifex.

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
Co-authored-by: Alexandre SPANGARO <aspangaro.dolibarr@gmail.com>
2026-08-17 21:41:18 +02:00
github-actions[bot]
cac0e048ca
PHPStan > Update baseline (#39571)
Co-authored-by: Dolibot <dolibarr-bot@users.noreply.github.com>
2026-08-17 21:29:05 +02:00
Laurent Destailleur
c1e582588d Debug v24 2026-08-17 18:26:39 +02:00
Laurent Destailleur
5d6d21264d Merge branch 'develop' of git@github.com:Dolibarr/dolibarr.git into develop 2026-08-17 16:12:17 +02:00
Laurent Destailleur
34e639a604 Merge branch '24.0' of git@github.com:Dolibarr/dolibarr.git into develop 2026-08-17 16:11:54 +02:00
Laurent Destailleur
690f811047 Fix label 2026-08-17 16:11:30 +02:00
Aymeric Cucherousset
da73d2daed Fix #39559 Invisible Reject check icon in cheque deposit slips (#39562)
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-17 15:35:24 +02:00
Aymeric Cucherousset
62e0c30d65
Fix #39559 Invisible Reject check icon in cheque deposit slips (#39562)
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-08-17 15:34:58 +02:00
Laurent Destailleur
cc9cb8958c Merge branch '24.0' of git@github.com:Dolibarr/dolibarr.git into develop 2026-08-17 15:29:02 +02:00
Laurent Destailleur
20b4f05018 Fix must encrypt const with _SECRET_ inside 2026-08-17 15:26:17 +02:00
Laurent Destailleur
2fdfd822b3 Merge branch 'develop' of git@github.com:Dolibarr/dolibarr.git into
develop
2026-08-17 04:49:53 +02:00
Laurent Destailleur
fb43184024 CI 2026-08-17 04:47:09 +02:00
Frédéric FRANCE
c8aa61198b
NEW Add PHPUnit test for getElementProperties() (#39550)
getElementProperties() is the central registry used across the
codebase (fetchObjectByElement(), generic links/extrafields, document
generation...) to resolve an element type string into its module,
classpath, classfile, classname, table and parent element, but it had
no test coverage despite being a ~480 line function with about 60
special-case branches plus several generic fallback paths.

Covers the representative paths rather than every branch:
- the generic completion of classfile/classname from subelement
  (project)
- the 'myobject@mymodule' external-module syntax, including the
  surprising fact that table_element keeps the raw '@'-string
- the 'myobject_mysubobject' syntax combined with a dedicated case
  branch overriding module (project_task)
- the generic '...det' object-line fallback for an unknown module,
  including the non-capitalized classname it produces (myobjectdet)
- a real '...det' case where the generic fallback runs first and a
  dedicated branch only adds parent_element on top (contratdet)
- a real '...det' case where the dedicated branch instead overrides
  classpath and classname set by the generic fallback (facturedet)
- the action/actioncomm special case, where table_element differs
  from the raw input element
2026-08-17 04:44:13 +02:00
Frédéric FRANCE
585afa14de
Qual: Use dolBuildUrl() instead of manual urlencode() concatenation in ecm (#39557)
* Qual: Use dolBuildUrl() instead of manual urlencode() concatenation in ecm

Replaces manual '?key='.urlencode($val).'&key2='.urlencode($val2)
string-building with dolBuildUrl($path, $params) across the ECM
directory-tree/file-manager code, for consistency with the rest of
the codebase (see htdocs/core/lib/ecm.lib.php, which already uses
this pattern for the same page) and to benefit from dolBuildUrl()'s
buildurl hook.

- core/ajax/ajaxdirtree.php: the dir_card.php edit link built from
  the sql tree loop.
- ecm/dir_card.php: the edit/add-section action buttons and the two
  delete confirmation URLs. Also switches the three buttons that used
  to manually concatenate '&token='.newToken() to dolBuildUrl()'s own
  $addtoken parameter.
- ecm/class/ecmfiles.class.php: EcmFiles::getNomUrl()'s document.php
  and file_card.php URLs.
- ecm/tpl/enablefiletreeajax.tpl.php: the ajaxdirtree.php script URL
  and the ajaxdirpreview.php URL. The token here intentionally stays
  currentToken() (not dolBuildUrl()'s own newToken()-based
  $addtoken), per the existing comment: ajaxdirtree.php has
  NOTOKENRENEWAL defined, so the token must match the one already
  valid on the calling page. $paramwithoutsection is a pre-built raw
  query-string fragment from an external caller and is appended as-is
  after the dolBuildUrl() result rather than folded into it.

Verified all five refactored URL-building expressions produce byte-
identical output to the original code for representative inputs
(including values with '/', '&' and spaces), except for query
parameter order (which has no effect) and one real, minor pre-
existing bug this incidentally fixes: the delete-section confirm URL
in dir_card.php was building '&module='.$module without urlencode(),
now correctly encoded by dolBuildUrl()/http_build_query().

Could not do a live browser check (no Chrome available for Playwright
in this environment) - verified via php -l, phpcs, and a standalone
script comparing old vs new output for each call site instead.

* Qual: Use dolBuildUrl() instead of manual urlencode() concatenation in filemanager.tpl.php

Same refactor as the previous commit, applied to the 7 remaining
manually-concatenated URLs in core/tpl/filemanager.tpl.php (used by
the ECM/medias file manager): the delete-file/delete-section/
convert-to-webp confirm URLs, the create-directory and refresh-list
toolbar buttons (now using dolBuildUrl()'s $addtoken instead of a
manual '&token='.newToken()), the two generate-webp buttons, and the
"Root" link.

$websitekeyandpageid is kept as a helper to build the raw sub-query
string embedded once (single-encoded) as the create-directory
button's 'backtopage' value - it is not itself passed to dolBuildUrl.

Verified all 7 refactored URL-building expressions produce the same
query parameters as the original code for representative inputs
(compared as parsed, order-independent query strings, since
http_build_query() does not preserve insertion order the same way as
the original manual concatenation), including one case
(convertimgwebp confirm with sortfield/sortorder) where the original
code had a harmless but sloppy leading '?&' that dolBuildUrl() no
longer produces.

* fix

* Qual: Use dolBuildUrl() instead of manual urlencode() concatenation in index_auto.php

Same refactor as the previous commits, applied to the 4 manually-
concatenated URLs in ecm/index_auto.php: the delete-file and
delete-section confirm URLs, the refresh-list toolbar link, and the
per-directory link in the auto-directories list.

Verified all 4 refactored URL-building expressions produce the same
query parameters as the original code for representative inputs
(including an empty-module/empty-section case for the refresh link,
and values with '/' and spaces for the others).

* Qual: Use dolBuildUrl() instead of manual urlencode() concatenation in ecm (file_card, dir_add_card, index_medias)

Same refactor as the previous commits, applied to the remaining
manually-concatenated URLs in:
- ecm/file_card.php: the cancel and rename-file redirects, the
  internal download link (document.php), the delete-file confirm URL
  and the edit button.
- ecm/dir_add_card.php: the delete-section confirm URL and the delete
  button (now using dolBuildUrl()'s $addtoken instead of a manual
  '&token='.newToken()).
- ecm/index_medias.php: the $backtopage URL used by
  core/actions_linkedfiles.inc.php after a confirm_deletefile.

Left ecm/search.php's '$param = "&section=".urlencode($section)'
alone: it is a raw query-string fragment (starting with '&', no
leading path) passed into FormFile::list_of_documents(), not a
base+params URL build, so it does not fit the dolBuildUrl($path,
$params) shape - same reasoning as $paramwithoutsection in the
already-refactored enablefiletreeajax.tpl.php.

Verified all 8 refactored URL-building expressions produce the same
query parameters as the original code for representative inputs
(order-independent comparison, since http_build_query() does not
preserve the original insertion order).
2026-08-17 04:41:05 +02:00
Laurent Destailleur
8c49a00c2c Debug v24 2026-08-17 04:29:49 +02:00