FIX #37826 country_code must be an ISO code, not a label (#39191)

* FIX #37826 country_code must be an ISO code, not a label

On a proposal/order, the 2nd+ line got VAT 0% with the error "no vat rates
defined for country 'France'/'Suisse'". Root cause: the seller country_code
held a translated country label instead of the ISO code, so the
"c.code IN (...)" lookup in Form::load_cache_vatrates() matched nothing.

- Societe::setMysoc(): the canonical MAIN_INFO_SOCIETE_COUNTRY is
  "id:code:label"; a legacy "id:label" value put the label into
  country_code. Only treat the full 3-token form as "id:code:label" and
  otherwise rebuild code/label from the authoritative country id.
- Form::load_tva(): defensively recover the seller ISO code from its
  country id when country_code is not a known c_country code, so a bad
  value can never silently force the VAT rate to 0%.

Signed-off-by: Jam Balaya <jambalaya.pyoncafe@outlook.jp>

* FIX #37826 avoid useless SQL per page: recover country ISO code from id only when code is not a 2-char ISO code

---------

Signed-off-by: Jam Balaya <jambalaya.pyoncafe@outlook.jp>
This commit is contained in:
Jam Balaya 2026-07-16 20:56:33 +09:00 committed by GitHub
parent 6d828c29fc
commit b2eae1b7d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 8 deletions

View file

@ -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';

View file

@ -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

View file

@ -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";
}
}