diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php
index e2a267607c6..14e4e0231f4 100644
--- a/htdocs/core/class/html.form.class.php
+++ b/htdocs/core/class/html.form.class.php
@@ -8032,12 +8032,22 @@ class Form
//exit;
// Define list of countries to use to search VAT rates to show
- // First we defined code_country to use to find list
- if (is_object($societe_vendeuse)) {
- $code_country = "'" . $societe_vendeuse->country_code . "'";
- } else {
- $code_country = "'" . $mysoc->country_code . "'"; // Pour compatibilite ascendente
+ // First we defined code_country to use to find list.
+ // country_code must be a c_country ISO code (e.g. 'FR', 'CH'). In some setups it may hold a
+ // country label (e.g. 'Suisse') instead, which would make the "c.code IN (...)" lookup done by
+ // load_cache_vatrates() match nothing and wrongly force the VAT rate to 0%. A valid ISO code is
+ // always 2 chars, so when the value is not a well formed ISO code (empty or a label) and we have
+ // a valid country id, we recover the ISO code from the authoritative country id. This way we do
+ // not run any SQL on each page access when we already have a valid ISO code.
+ $sellercountrycode = is_object($societe_vendeuse) ? $societe_vendeuse->country_code : $mysoc->country_code;
+ $sellercountryid = is_object($societe_vendeuse) ? $societe_vendeuse->country_id : $mysoc->country_id;
+ if ((int) $sellercountryid > 0 && strlen((string) $sellercountrycode) != 2) {
+ $tmpcountrycode = dol_getIdFromCode($this->db, (string) $sellercountryid, 'c_country', 'rowid', 'code');
+ if (!empty($tmpcountrycode) && !is_numeric($tmpcountrycode)) {
+ $sellercountrycode = $tmpcountrycode;
+ }
}
+ $code_country = "'" . $sellercountrycode . "'"; // Pour compatibilite ascendente
if ($societe_vendeuse == $mysoc && getDolGlobalString('SERVICE_ARE_ECOMMERCE_200238EC')) { // If option to have vat for end customer for services is on
require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
diff --git a/htdocs/societe/class/societe.class.php b/htdocs/societe/class/societe.class.php
index 41d5591d4b8..8ec9caa8fcb 100644
--- a/htdocs/societe/class/societe.class.php
+++ b/htdocs/societe/class/societe.class.php
@@ -4771,11 +4771,14 @@ class Societe extends CommonObject
if (getDolGlobalString('MAIN_INFO_SOCIETE_COUNTRY')) {
$tmp = explode(':', getDolGlobalString('MAIN_INFO_SOCIETE_COUNTRY'));
$country_id = (is_numeric($tmp[0])) ? (int) $tmp[0] : 0;
- if (!empty($tmp[1])) { // If $conf->global->MAIN_INFO_SOCIETE_COUNTRY is "id:code:label"
+ if (!empty($tmp[1]) && !empty($tmp[2])) { // MAIN_INFO_SOCIETE_COUNTRY is the canonical "id:code:label"
$country_code = $tmp[1];
$country_label = $tmp[2];
- } else {
- // For backward compatibility
+ } elseif ($country_id > 0) {
+ // For backward compatibility. The value is "id" only, or a legacy "id:label" where the
+ // second token is a country label (e.g. 'Suisse') and not an ISO code. In both cases we
+ // must not keep a label in country_code (it would break code-based lookups like VAT rates),
+ // so we rebuild code and label from the authoritative country id.
dol_syslog("Your country setup use an old syntax. Reedit it using setup area.", LOG_WARNING);
include_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
$country_code = getCountry($country_id, '2', $this->db); // This need a SQL request, but it's the old feature that should not be used anymore
diff --git a/test/phpunit/SocieteTest.php b/test/phpunit/SocieteTest.php
index 4f8a4e0fa72..3a1aeaf3d5b 100644
--- a/test/phpunit/SocieteTest.php
+++ b/test/phpunit/SocieteTest.php
@@ -568,4 +568,50 @@ class SocieteTest extends CommonClassTest
print __METHOD__." result=".$result."\n";
return $result;
}
+
+ /**
+ * Test that Societe::setMysoc() always resolves country_code to an ISO country code.
+ *
+ * Regression test for issue #37826: when MAIN_INFO_SOCIETE_COUNTRY uses the legacy
+ * "id:label" (2-token) syntax, the second token is a country label (e.g. 'France'),
+ * not an ISO code. setMysoc() must not leave that label in country_code (it would break
+ * code-based lookups such as VAT rates and force a 0% rate), but rebuild it from the id.
+ *
+ * @return void
+ */
+ public function testSetMysocCountryCode()
+ {
+ global $conf,$user,$langs,$db;
+ $conf = $this->savconf;
+ $user = $this->savuser;
+ $langs = $this->savlangs;
+ $db = $this->savdb;
+
+ require_once dirname(__FILE__).'/../../htdocs/core/lib/company.lib.php';
+
+ $frid = (int) getCountry('FR', '3', $db);
+ $this->assertGreaterThan(0, $frid, 'Country FR must exist in dictionary c_country');
+
+ $savcountryconst = getDolGlobalString('MAIN_INFO_SOCIETE_COUNTRY');
+
+ // Canonical 3-token syntax "id:code:label".
+ $conf->global->MAIN_INFO_SOCIETE_COUNTRY = $frid.':FR:France';
+ $soc3 = new Societe($db);
+ $soc3->setMysoc($conf);
+
+ // Legacy 2-token syntax "id:label" (the #37826 case), second token is a label not a code.
+ $conf->global->MAIN_INFO_SOCIETE_COUNTRY = $frid.':France';
+ $soc2 = new Societe($db);
+ $soc2->setMysoc($conf);
+
+ // Restore the constant before asserting so a failure does not leak state to other tests.
+ $conf->global->MAIN_INFO_SOCIETE_COUNTRY = $savcountryconst;
+
+ $this->assertEquals('FR', $soc3->country_code, 'country_code must be the ISO code from a 3-token constant');
+ $this->assertEquals($frid, $soc3->country_id);
+ $this->assertEquals('FR', $soc2->country_code, 'country_code must be an ISO code, not a label, from a legacy 2-token constant (#37826)');
+ $this->assertEquals($frid, $soc2->country_id);
+
+ print __METHOD__." ok\n";
+ }
}