* Test: add unit tests for TTC unit price rounding (#37658) Covers the "unit price entered including tax" workflow (subtotal title, product line, free line, no-op update, discount on all lines) for sale/purchase objects, checking totals and that each line keeps its typed TTC value (subprice_ttc). * Test - Make TTC rounding test portable across versions (guard subtotal step with method_exists) * Test - Use non-deprecated module names in isModEnabled (order/invoice/contract) * Fix #37658 Persist TTC unit price entry mode on order/invoice/contract and supplier proposal A line entered with the unit price including tax keeps its typed value (subprice_ttc) and computes totals from it, avoiding a 0.01 rounding drift on create, no-op edit and bulk actions. Moves wasEnteredIncludingTax() to CommonObjectLine. * Cast unit price to float on supplier proposal updateline (phan) * FIX #37658 Preserve TTC unit price entry mode on clone (createFromClone) * Pass socid (not object id) to SupplierProposal::createFromClone in TTC clone test * Preserve TTC unit price entry mode on origin conversion and credit note * Fix phan: cast pu_ht to float for Contrat::defineBuyPrice() argument * Preserve TTC unit price entry mode on supplier order, invoice and proposal (socle, clone, conversion, line edit and bulk actions) * Fix cast ht to float for CommandeFournisseur::updateline() argument * Fix cast subprice_ttc to float in ContratLigne::update SQL (phan SqlInjection) * Add TTC round-of-total stability tests Cover MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=1 (and _SUPPLIER=1) on fresh customer and supplier objects with mixed HT/TTC lines: create, no-op, clone and discount must not drift and must keep the entry mode. * Preserve TTC entry mode when creating a contract from origin * Add getPriceBaseType() and preserve TTC entry mode on proposal from origin --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
1840 lines
66 KiB
PHP
1840 lines
66 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.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/TtcRoundingTest.php
|
|
* \ingroup test
|
|
* \brief PHPUnit test for unit price entered including tax (price_base_type='TTC').
|
|
* \remarks To run this script as CLI: phpunit filename.php
|
|
*
|
|
* When a line is entered with the unit price including tax, the line total AND the persisted
|
|
* "TTC entry mode" (subprice_ttc) must be kept consistent so that no 0.01 rounding drift appears,
|
|
* including after a no-op edit or a bulk action (discount on all lines).
|
|
*
|
|
* Full workflow covered per object (like a real quote): a subtotal title line, a product line and
|
|
* a free line, all entered in TTC mode, then a no-op update, then a global discount on all lines.
|
|
*
|
|
* Scenario per priced line: qty = 680, VAT = 20%, unit price TTC = 3.35
|
|
* - on create: total_ht = 1898.33, total_tva = 379.67, total_ttc = 2278.00
|
|
* - after 10% discount: total_ht = 1708.50, total_tva = 341.70, total_ttc = 2050.20
|
|
* - the line always keeps subprice_ttc = 3.35 (the value typed by the user)
|
|
*
|
|
* All customer and supplier objects persist the TTC entry mode: customer proposal, order, invoice,
|
|
* contract, supplier proposal, supplier order and supplier invoice. This test guards against a
|
|
* regression of that behaviour. Contract lines have no subtotals; each per-object test is skipped
|
|
* when its module is disabled.
|
|
*/
|
|
|
|
global $conf,$user,$langs,$db;
|
|
//define('TEST_DB_FORCE_TYPE','mysql'); // This is to force using mysql driver
|
|
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/societe/class/societe.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/product/class/product.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/comm/propal/class/propal.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/commande/class/commande.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/compta/facture/class/facture.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/contrat/class/contrat.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/contrat/class/contratligne.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/fourn/class/fournisseur.commande.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/fourn/class/fournisseur.facture.class.php';
|
|
require_once dirname(__FILE__).'/../../htdocs/supplier_proposal/class/supplier_proposal.class.php';
|
|
require_once dirname(__FILE__).'/CommonClassTest.class.php';
|
|
|
|
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;
|
|
|
|
|
|
/**
|
|
* Class for PHPUnit tests
|
|
*
|
|
* @backupGlobals disabled
|
|
* @backupStaticAttributes enabled
|
|
* @remarks backupGlobals must be disabled to have db,conf,user and lang not erased.
|
|
*/
|
|
class TtcRoundingTest extends CommonClassTest
|
|
{
|
|
// Common scenario: a unit price typed including tax that does not divide evenly once converted to HT.
|
|
const QTY = 680;
|
|
const VAT = 20;
|
|
const PU_TTC = 3.35;
|
|
const PU_HT = 2.79167; // 3.35 / 1.2 rounded at MAIN_MAX_DECIMALS_UNIT=5 - the stored HT unit price
|
|
const REMISE = 10;
|
|
|
|
// Expected per priced line, entered in TTC mode (calibrated, MAIN_MAX_DECIMALS_UNIT=5 / _TOT=2).
|
|
const LINE_HT = 1898.33;
|
|
const LINE_TVA = 379.67;
|
|
const LINE_TTC = 2278.00;
|
|
|
|
// Expected per priced line after a 10% discount, TTC mode preserved.
|
|
const LINE_HT_REMISE = 1708.50;
|
|
const LINE_TVA_REMISE = 341.70;
|
|
const LINE_TTC_REMISE = 2050.20;
|
|
|
|
// Expected per priced line entered in HT mode (pu_ht = PU_HT). subprice_ttc must be 0 (not marked as
|
|
// TTC entry), so HT lines are never hijacked by the TTC preservation and the total keeps its HT rounding.
|
|
const LINE_HT_HTMODE = 1898.34;
|
|
const LINE_TVA_HTMODE = 379.66;
|
|
const LINE_TTC_HTMODE = 2278.00;
|
|
|
|
/** @var int Shared thirdparty (customer and supplier) created once for the whole class */
|
|
protected static $socid;
|
|
|
|
/** @var int Shared product created once for the whole class */
|
|
protected static $productid;
|
|
|
|
/**
|
|
* Create the shared fixtures (one thirdparty and one product) once for the whole class.
|
|
* They live inside the class transaction and are rolled back in tearDownAfterClass().
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function setUpBeforeClass(): void
|
|
{
|
|
global $user,$db;
|
|
|
|
parent::setUpBeforeClass();
|
|
|
|
$soc = new Societe($db);
|
|
$soc->name = 'Test TTC rounding '.uniqid();
|
|
$soc->client = 1;
|
|
$soc->fournisseur = 1;
|
|
$soc->country_id = 1;
|
|
$soc->code_client = -1;
|
|
$soc->code_fournisseur = -1;
|
|
self::$socid = $soc->create($user);
|
|
self::assertGreaterThan(0, self::$socid, 'Create shared thirdparty: '.$soc->error);
|
|
|
|
// A dedicated simple product (no packaging) so a product line behaves deterministically.
|
|
$prod = new Product($db);
|
|
$prod->ref = 'TTCTEST'.substr(uniqid(), -8);
|
|
$prod->label = 'TTC test product';
|
|
$prod->type = 0;
|
|
$prod->status = 1;
|
|
$prod->status_buy = 1;
|
|
$prod->price_base_type = 'HT';
|
|
$prod->price = 10;
|
|
$prod->tva_tx = self::VAT;
|
|
self::$productid = $prod->create($user);
|
|
self::assertGreaterThan(0, self::$productid, 'Create shared product: '.$prod->error);
|
|
}
|
|
|
|
/**
|
|
* Restore globals saved by CommonClassTest and force a deterministic rounding mode.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function restoreGlobals()
|
|
{
|
|
global $conf,$user,$langs,$db;
|
|
$conf = $this->savconf;
|
|
$user = $this->savuser;
|
|
$langs = $this->savlangs;
|
|
$db = $this->savdb;
|
|
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 0;
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 0;
|
|
}
|
|
|
|
/**
|
|
* Return only the priced lines (exclude subtotal title/section lines).
|
|
*
|
|
* @param CommonObject $object Object with lines loaded
|
|
* @return CommonObjectLine[]
|
|
*/
|
|
private function pricedLines($object)
|
|
{
|
|
$out = array();
|
|
foreach ($object->lines as $line) {
|
|
if (defined('SUBTOTALS_SPECIAL_CODE') && $line->special_code == SUBTOTALS_SPECIAL_CODE) {
|
|
continue;
|
|
}
|
|
$out[] = $line;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Assert a priced line entered in TTC keeps its TTC value and has the expected totals.
|
|
*
|
|
* @param CommonObjectLine $line Reloaded line
|
|
* @param string $tag Message prefix
|
|
* @param bool $discounted true to check the post-discount totals
|
|
* @return void
|
|
*/
|
|
private function assertPricedLine($line, $tag, $discounted = false)
|
|
{
|
|
$this->assertEquals(self::PU_TTC, (float) ($line->subprice_ttc ?? 0), $tag.' subprice_ttc (TTC entry mode kept)');
|
|
$this->assertEquals(self::PU_HT, (float) $line->subprice, $tag.' subprice (HT unit price kept)');
|
|
if ($discounted) {
|
|
$this->assertEquals(self::LINE_HT_REMISE, (float) $line->total_ht, $tag.' total_ht (after discount)');
|
|
$this->assertEquals(self::LINE_TVA_REMISE, (float) $line->total_tva, $tag.' total_tva (after discount)');
|
|
$this->assertEquals(self::LINE_TTC_REMISE, (float) $line->total_ttc, $tag.' total_ttc (after discount)');
|
|
} else {
|
|
$this->assertEquals(self::LINE_HT, (float) $line->total_ht, $tag.' total_ht');
|
|
$this->assertEquals(self::LINE_TVA, (float) $line->total_tva, $tag.' total_tva');
|
|
$this->assertEquals(self::LINE_TTC, (float) $line->total_ttc, $tag.' total_ttc');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert both priced lines (product + free) of a reloaded object.
|
|
*
|
|
* @param CommonObject $object Reloaded object (lines loaded)
|
|
* @param string $tag Message prefix
|
|
* @param bool $discounted true to check post-discount totals
|
|
* @return void
|
|
*/
|
|
private function assertBothPricedLines($object, $tag, $discounted = false)
|
|
{
|
|
$priced = $this->pricedLines($object);
|
|
$this->assertCount(2, $priced, $tag.' must have 2 priced lines');
|
|
$this->assertPricedLine($priced[0], $tag.' product line', $discounted);
|
|
$this->assertPricedLine($priced[1], $tag.' free line', $discounted);
|
|
}
|
|
|
|
/**
|
|
* Assert both priced lines of a credit note built by inverting an invoice: the TTC entry mode
|
|
* is kept with the inverted sign so there is no rounding drift.
|
|
*
|
|
* @param CommonObject $object Reloaded credit note (lines loaded)
|
|
* @param string $tag Message prefix
|
|
* @return void
|
|
*/
|
|
private function assertBothPricedLinesInverted($object, $tag)
|
|
{
|
|
$priced = $this->pricedLines($object);
|
|
$this->assertCount(2, $priced, $tag.' must have 2 priced lines');
|
|
foreach ($priced as $k => $line) {
|
|
$this->assertEquals(-self::PU_TTC, (float) ($line->subprice_ttc ?? 0), $tag." line $k subprice_ttc (inverted TTC entry mode kept)");
|
|
$this->assertEquals(-self::PU_HT, (float) $line->subprice, $tag." line $k subprice (inverted HT unit price)");
|
|
$this->assertEquals(-self::LINE_HT, (float) $line->total_ht, $tag." line $k total_ht (inverted)");
|
|
$this->assertEquals(-self::LINE_TVA, (float) $line->total_tva, $tag." line $k total_tva (inverted)");
|
|
$this->assertEquals(-self::LINE_TTC, (float) $line->total_ttc, $tag." line $k total_ttc (inverted)");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert both priced lines were entered in HT mode: subprice_ttc must be 0 (not a TTC entry marker),
|
|
* subprice = PU_HT and the total keeps its HT-mode rounding (never hijacked by TTC preservation).
|
|
*
|
|
* @param CommonObject $object Reloaded object (lines loaded)
|
|
* @param string $tag Message prefix
|
|
* @return void
|
|
*/
|
|
private function assertBothHtLines($object, $tag)
|
|
{
|
|
$priced = $this->pricedLines($object);
|
|
$this->assertCount(2, $priced, $tag.' must have 2 priced lines');
|
|
foreach ($priced as $k => $line) {
|
|
$this->assertEquals(0, (float) ($line->subprice_ttc ?? 0), $tag." line $k subprice_ttc must be 0 (HT entry mode, not marked TTC)");
|
|
$this->assertEquals(self::PU_HT, (float) $line->subprice, $tag." line $k subprice (HT unit price)");
|
|
$this->assertEquals(self::LINE_HT_HTMODE, (float) $line->total_ht, $tag." line $k total_ht (HT mode)");
|
|
$this->assertEquals(self::LINE_TTC_HTMODE, (float) $line->total_ttc, $tag." line $k total_ttc (HT mode)");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Snapshot the priced lines (subprice, subprice_ttc and totals) for later stability comparison.
|
|
* Used for the "rounding of total" mode where absolute totals are not deterministic, so we assert
|
|
* a no-op / clone does not change the stored values rather than hardcoding them.
|
|
*
|
|
* @param CommonObject $object Reloaded object (lines loaded)
|
|
* @return array<int,array<string,float>>
|
|
*/
|
|
private function snapshotPricedLines($object)
|
|
{
|
|
$snap = array();
|
|
foreach ($this->pricedLines($object) as $line) {
|
|
$snap[] = array(
|
|
'subprice' => (float) $line->subprice,
|
|
'subprice_ttc' => (float) ($line->subprice_ttc ?? 0),
|
|
'total_ht' => (float) $line->total_ht,
|
|
'total_tva' => (float) $line->total_tva,
|
|
'total_ttc' => (float) $line->total_ttc,
|
|
);
|
|
}
|
|
return $snap;
|
|
}
|
|
|
|
/**
|
|
* Assert two priced-line snapshots are identical (no rounding drift on a no-op / clone).
|
|
*
|
|
* @param array<int,array<string,float>> $before Snapshot before the operation
|
|
* @param array<int,array<string,float>> $after Snapshot after the operation
|
|
* @param string $tag Message prefix
|
|
* @return void
|
|
*/
|
|
private function assertLinesUnchanged($before, $after, $tag)
|
|
{
|
|
$this->assertCount(count($before), $after, $tag.' same number of priced lines');
|
|
foreach ($before as $k => $b) {
|
|
foreach (array('subprice', 'subprice_ttc', 'total_ht', 'total_tva', 'total_ttc') as $f) {
|
|
$this->assertEquals($b[$f], $after[$k][$f], $tag." line $k $f stable");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert the mixed 4-line layout kept each entry mode: exactly 2 HT lines (subprice_ttc = 0, subprice =
|
|
* PU_HT) and 2 TTC lines (subprice_ttc = PU_TTC), whatever the round-of-total totals are.
|
|
*
|
|
* @param CommonObject $object Reloaded object (lines loaded)
|
|
* @param string $tag Message prefix
|
|
* @return void
|
|
*/
|
|
private function assertMixedSubpriceTtc($object, $tag)
|
|
{
|
|
$priced = $this->pricedLines($object);
|
|
$this->assertCount(4, $priced, $tag.' must have 4 priced lines');
|
|
$htCount = 0;
|
|
$ttcCount = 0;
|
|
foreach ($priced as $k => $line) {
|
|
$stt = (float) ($line->subprice_ttc ?? 0);
|
|
if ($stt == 0.0) {
|
|
$htCount++;
|
|
$this->assertEquals(self::PU_HT, (float) $line->subprice, $tag." line $k HT subprice (PU_HT kept)");
|
|
} else {
|
|
$ttcCount++;
|
|
$this->assertEquals(self::PU_TTC, $stt, $tag." line $k TTC subprice_ttc (PU_TTC kept)");
|
|
}
|
|
}
|
|
$this->assertEquals(2, $htCount, $tag.' must have 2 HT-entered lines');
|
|
$this->assertEquals(2, $ttcCount, $tag.' must have 2 TTC-entered lines');
|
|
}
|
|
|
|
/**
|
|
* Whether the object supports subtotal lines (subtotals module enabled and object type wired).
|
|
* Kept portable across versions: supplier objects gained subtotals only in later Dolibarr releases.
|
|
*
|
|
* @param CommonObject $object Object to test
|
|
* @return bool
|
|
*/
|
|
private function subtotalsSupported($object)
|
|
{
|
|
return defined('SUBTOTALS_SPECIAL_CODE') && method_exists($object, 'addSubtotalLine');
|
|
}
|
|
|
|
/**
|
|
* Add a subtotal title line when the object supports it (no-op otherwise, for portability).
|
|
*
|
|
* @param CommonObject $object Object to add the title to
|
|
* @param Translate $langs Language object
|
|
* @return void
|
|
*/
|
|
private function addSubtotalTitle($object, $langs)
|
|
{
|
|
if ($this->subtotalsSupported($object)) {
|
|
$object->addSubtotalLine($langs, 'Section 1', 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert that a subtotal title line is present, when the object supports subtotals.
|
|
*
|
|
* @param CommonObject $object Reloaded object (lines loaded)
|
|
* @param string $tag Message prefix
|
|
* @return void
|
|
*/
|
|
private function assertSubtotalLinePresent($object, $tag)
|
|
{
|
|
if (!$this->subtotalsSupported($object)) {
|
|
return;
|
|
}
|
|
$found = false;
|
|
foreach ($object->lines as $line) {
|
|
if ($line->special_code == SUBTOTALS_SPECIAL_CODE) {
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
$this->assertTrue($found, $tag.' must have a subtotal line');
|
|
}
|
|
|
|
/**
|
|
* Customer proposal - reference object, fix already applied. Full workflow, expected green.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerProposalTtcWorkflow()
|
|
{
|
|
global $user,$langs,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('propal')) {
|
|
$this->markTestSkipped('Module propal disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Propal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer proposal create');
|
|
|
|
$this->addSubtotalTitle($object, $langs);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
// Create state
|
|
$reloaded = new Propal($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertSubtotalLinePresent($reloaded, 'Customer proposal');
|
|
$this->assertBothPricedLines($reloaded, 'Customer proposal create');
|
|
|
|
// No-op update, preserving TTC mode (mirrors card.php using wasEnteredIncludingTax()/subprice_ttc)
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, (float) $line->subprice_ttc, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, $line->desc, 'TTC');
|
|
}
|
|
$afterUpdate = new Propal($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Customer proposal no-op update');
|
|
|
|
// Discount for all lines
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, (float) $line->subprice_ttc, $line->qty, self::REMISE, $line->tva_tx, 0, 0, $line->desc, 'TTC');
|
|
}
|
|
$afterRemise = new Propal($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Customer proposal discount', true);
|
|
print __METHOD__." id=".$id." total_ht=".$afterRemise->total_ht."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer proposal - clone must preserve the TTC entry mode (subprice_ttc) so the cloned
|
|
* line keeps the same totals with no rounding drift.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerProposalCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('propal')) {
|
|
$this->markTestSkipped('Module propal disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Propal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCCLONE'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer proposal clone create source');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new Propal($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer proposal createFromClone');
|
|
|
|
$clone = new Propal($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Customer proposal clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer order - full workflow. subprice_ttc persistence to be added -> red.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerOrderTtcWorkflow()
|
|
{
|
|
global $user,$langs,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('order')) {
|
|
$this->markTestSkipped('Module order disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Commande($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer order create');
|
|
|
|
$this->addSubtotalTitle($object, $langs);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Commande($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertSubtotalLinePresent($reloaded, 'Customer order');
|
|
$this->assertBothPricedLines($reloaded, 'Customer order create');
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, 'TTC');
|
|
}
|
|
$afterUpdate = new Commande($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Customer order no-op update');
|
|
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, self::REMISE, $line->tva_tx, 0, 0, 'TTC');
|
|
}
|
|
$afterRemise = new Commande($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Customer order discount', true);
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer order - clone must preserve the TTC entry mode (subprice_ttc).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerOrderCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('order')) {
|
|
$this->markTestSkipped('Module order disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Commande($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCCLONE'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer order clone create source');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new Commande($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer order createFromClone');
|
|
|
|
$clone = new Commande($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Customer order clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer invoice - full workflow. subprice_ttc persistence to be added -> red.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerInvoiceTtcWorkflow()
|
|
{
|
|
global $user,$langs,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('invoice')) {
|
|
$this->markTestSkipped('Module invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Facture($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer invoice create');
|
|
|
|
$this->addSubtotalTitle($object, $langs);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Facture($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertSubtotalLinePresent($reloaded, 'Customer invoice');
|
|
$this->assertBothPricedLines($reloaded, 'Customer invoice create');
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, $line->remise_percent, $line->date_start, $line->date_end, $line->tva_tx, 0, 0, 'TTC');
|
|
}
|
|
$afterUpdate = new Facture($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Customer invoice no-op update');
|
|
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, self::REMISE, $line->date_start, $line->date_end, $line->tva_tx, 0, 0, 'TTC');
|
|
}
|
|
$afterRemise = new Facture($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Customer invoice discount', true);
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer invoice - clone must preserve the TTC entry mode (subprice_ttc).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerInvoiceCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('invoice')) {
|
|
$this->markTestSkipped('Module invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Facture($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCCLONE'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer invoice clone create source');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new Facture($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $id);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer invoice createFromClone');
|
|
|
|
$clone = new Facture($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Customer invoice clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer contract - full workflow (no subtotals on contracts) -> red.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerContractTtcWorkflow()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('contract')) {
|
|
$this->markTestSkipped('Module contract disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Contrat($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer contract create');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Contrat($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($reloaded, 'Customer contract create');
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, $line->remise_percent, $line->date_start, $line->date_end, $line->tva_tx, 0, 0, '', '', 'TTC');
|
|
}
|
|
$afterUpdate = new Contrat($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Customer contract no-op update');
|
|
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, self::REMISE, $line->date_start, $line->date_end, $line->tva_tx, 0, 0, '', '', 'TTC');
|
|
}
|
|
$afterRemise = new Contrat($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Customer contract discount', true);
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer contract - line edit through ContratLigne::update() (the HT-only UI edit path, distinct
|
|
* from Contrat::updateline()). A no-op edit must keep the TTC total; changing the HT unit price
|
|
* drops the TTC entry mode.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerContractLineUpdateTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('contract')) {
|
|
$this->markTestSkipped('Module contract disabled');
|
|
return;
|
|
}
|
|
|
|
$object = new Contrat($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = self::$socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Contrat create');
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, self::$productid, 0, '', '', 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Contrat($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$lineid = $reloaded->lines[0]->id;
|
|
|
|
// No-op edit (price unchanged): TTC total must be preserved, no rounding drift.
|
|
$line = new ContratLigne($db);
|
|
$line->fetch($lineid);
|
|
$line->oldcopy = dol_clone($line, 2);
|
|
$line->tva_tx = self::VAT;
|
|
$line->update($user);
|
|
|
|
$afterNoop = new ContratLigne($db);
|
|
$afterNoop->fetch($lineid);
|
|
$this->assertEquals(self::PU_TTC, (float) $afterNoop->subprice_ttc, 'Contrat line no-op: subprice_ttc kept');
|
|
$this->assertEquals(self::LINE_HT, (float) $afterNoop->total_ht, 'Contrat line no-op: total_ht preserved (no drift)');
|
|
|
|
// Edit that actually changes the HT unit price: TTC entry mode dropped, HT-based total.
|
|
$newHt = 2.50;
|
|
$line2 = new ContratLigne($db);
|
|
$line2->fetch($lineid);
|
|
$line2->oldcopy = dol_clone($line2, 2);
|
|
$line2->subprice = $newHt;
|
|
// Mirror contrat/card.php: drop the TTC mode when the HT unit price actually changed.
|
|
if ((float) $line2->subprice != (float) $line2->oldcopy->subprice) {
|
|
$line2->subprice_ttc = 0;
|
|
}
|
|
$line2->tva_tx = self::VAT;
|
|
$line2->update($user);
|
|
|
|
$afterHt = new ContratLigne($db);
|
|
$afterHt->fetch($lineid);
|
|
$this->assertEquals(0, (float) $afterHt->subprice_ttc, 'Contrat line HT edit: subprice_ttc reset');
|
|
$this->assertEquals((float) price2num($newHt * self::QTY, 'MT'), (float) $afterHt->total_ht, 'Contrat line HT edit: HT-based total');
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer contract - clone must preserve the TTC entry mode (subprice_ttc).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerContractCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('contract')) {
|
|
$this->markTestSkipped('Module contract disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Contrat($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCCLONE'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer contract clone create source');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 'TTC', self::PU_TTC);
|
|
|
|
$source = new Contrat($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer contract createFromClone');
|
|
|
|
$clone = new Contrat($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Customer contract clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier proposal - full workflow. subprice_ttc already handled upstream.
|
|
* Same addline()/updateline() signatures as the customer proposal.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierProposalTtcWorkflow()
|
|
{
|
|
global $user,$langs,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('supplier_proposal')) {
|
|
$this->markTestSkipped('Module supplier_proposal disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new SupplierProposal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier proposal create');
|
|
|
|
$this->addSubtotalTitle($object, $langs);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new SupplierProposal($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertSubtotalLinePresent($reloaded, 'Supplier proposal');
|
|
$this->assertBothPricedLines($reloaded, 'Supplier proposal create');
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, (float) $line->subprice_ttc, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, $line->desc, 'TTC');
|
|
}
|
|
$afterUpdate = new SupplierProposal($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Supplier proposal no-op update');
|
|
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, (float) $line->subprice_ttc, $line->qty, self::REMISE, $line->tva_tx, 0, 0, $line->desc, 'TTC');
|
|
}
|
|
$afterRemise = new SupplierProposal($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Supplier proposal discount', true);
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier proposal - clone must preserve the TTC entry mode (subprice_ttc).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierProposalCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('supplier_proposal')) {
|
|
$this->markTestSkipped('Module supplier_proposal disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new SupplierProposal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCCLONE'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier proposal clone create source');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new SupplierProposal($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier proposal createFromClone');
|
|
|
|
$clone = new SupplierProposal($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Supplier proposal clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier order - full workflow. subprice_ttc already handled upstream.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierOrderTtcWorkflow()
|
|
{
|
|
global $user,$langs,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_order')) {
|
|
$this->markTestSkipped('Module supplier order disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new CommandeFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier order create');
|
|
|
|
$this->addSubtotalTitle($object, $langs);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new CommandeFournisseur($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertSubtotalLinePresent($reloaded, 'Supplier order');
|
|
$this->assertBothPricedLines($reloaded, 'Supplier order create');
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, 'TTC');
|
|
}
|
|
$afterUpdate = new CommandeFournisseur($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Supplier order no-op update');
|
|
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->qty, self::REMISE, $line->tva_tx, 0, 0, 'TTC');
|
|
}
|
|
$afterRemise = new CommandeFournisseur($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Supplier order discount', true);
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier order - clone must preserve the TTC entry mode (subprice_ttc).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierOrderCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_order')) {
|
|
$this->markTestSkipped('Module supplier order disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new CommandeFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier order clone create source');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new CommandeFournisseur($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier order createFromClone');
|
|
|
|
$clone = new CommandeFournisseur($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Supplier order clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier invoice - full workflow. subprice_ttc already handled upstream.
|
|
* Note the different addline()/updateline() parameter order (pu in position 2, no pu_ttc: in TTC
|
|
* mode the $pu argument carries the price including tax).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierInvoiceTtcWorkflow()
|
|
{
|
|
global $user,$langs,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_invoice')) {
|
|
$this->markTestSkipped('Module supplier invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new FactureFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier invoice create');
|
|
|
|
$this->addSubtotalTitle($object, $langs);
|
|
$object->addline('Product TTC', self::PU_TTC, self::VAT, 0, 0, self::QTY, $pid, 0, 0, 0, 0, 0, 'TTC');
|
|
$object->addline('Free TTC', self::PU_TTC, self::VAT, 0, 0, self::QTY, 0, 0, 0, 0, 0, 0, 'TTC');
|
|
|
|
$reloaded = new FactureFournisseur($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertSubtotalLinePresent($reloaded, 'Supplier invoice');
|
|
$this->assertBothPricedLines($reloaded, 'Supplier invoice create');
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->tva_tx, 0, 0, $line->qty, 0, 'TTC', 0, 0, $line->remise_percent);
|
|
}
|
|
$afterUpdate = new FactureFournisseur($db);
|
|
$afterUpdate->fetch($id);
|
|
if (method_exists($afterUpdate, 'fetch_lines')) {
|
|
$afterUpdate->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterUpdate, 'Supplier invoice no-op update');
|
|
|
|
foreach ($this->pricedLines($afterUpdate) as $line) {
|
|
$object->updateline($line->id, $line->desc, (float) $line->subprice_ttc, $line->tva_tx, 0, 0, $line->qty, 0, 'TTC', 0, 0, self::REMISE);
|
|
}
|
|
$afterRemise = new FactureFournisseur($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($afterRemise, 'Supplier invoice discount', true);
|
|
print __METHOD__." id=".$id."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier invoice - clone must preserve the TTC entry mode (subprice_ttc).
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierInvoiceCloneTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_invoice')) {
|
|
$this->markTestSkipped('Module supplier invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new FactureFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref_supplier = 'TTCCLONE'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier invoice clone create source');
|
|
|
|
$object->addline('Product TTC', self::PU_TTC, self::VAT, 0, 0, self::QTY, $pid, 0, 0, 0, 0, 0, 'TTC');
|
|
$object->addline('Free TTC', self::PU_TTC, self::VAT, 0, 0, self::QTY, 0, 0, 0, 0, 0, 0, 'TTC');
|
|
|
|
// createFromClone() takes the new supplier ref from $this (fallback "CopyOf <ref>"): call it on a
|
|
// fresh object so the cloned supplier invoice gets a unique ref_supplier.
|
|
$cloner = new FactureFournisseur($db);
|
|
$clonedId = $cloner->createFromClone($user, $id);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier invoice createFromClone');
|
|
|
|
$clone = new FactureFournisseur($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($clone, 'Supplier invoice clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer proposal -> order (Commande::createFromProposal) must preserve the TTC entry mode.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerProposalToOrderTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('propal') || !isModEnabled('order')) {
|
|
$this->markTestSkipped('Module propal or order disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Propal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCORIG'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Proposal create for origin');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new Propal($db);
|
|
$source->fetch($id);
|
|
if (method_exists($source, 'fetch_lines')) {
|
|
$source->fetch_lines();
|
|
}
|
|
|
|
$order = new Commande($db);
|
|
$orderId = $order->createFromProposal($source, $user);
|
|
$this->assertGreaterThan(0, $orderId, 'createFromProposal');
|
|
|
|
$reloaded = new Commande($db);
|
|
$reloaded->fetch($orderId);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($reloaded, 'Proposal to order');
|
|
print __METHOD__." propal=".$id." order=".$orderId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer order -> invoice (Facture::createFromOrder) must preserve the TTC entry mode.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerOrderToInvoiceTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('order') || !isModEnabled('invoice')) {
|
|
$this->markTestSkipped('Module order or invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Commande($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCORIG'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Order create for origin');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new Commande($db);
|
|
$source->fetch($id);
|
|
if (method_exists($source, 'fetch_lines')) {
|
|
$source->fetch_lines();
|
|
}
|
|
|
|
// createFromOrder() returns 1 on success; the new invoice id is in $invoice->id.
|
|
$invoice = new Facture($db);
|
|
$this->assertGreaterThan(0, $invoice->createFromOrder($source, $user), 'createFromOrder');
|
|
$invoiceId = $invoice->id;
|
|
|
|
$reloaded = new Facture($db);
|
|
$reloaded->fetch($invoiceId);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($reloaded, 'Order to invoice');
|
|
print __METHOD__." order=".$id." invoice=".$invoiceId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer contract -> invoice (Facture::createFromContract) must preserve the TTC entry mode.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerContractToInvoiceTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('contract') || !isModEnabled('invoice')) {
|
|
$this->markTestSkipped('Module contract or invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Contrat($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCORIG'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Contract create for origin');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 'TTC', self::PU_TTC);
|
|
|
|
$source = new Contrat($db);
|
|
$source->fetch($id);
|
|
if (method_exists($source, 'fetch_lines')) {
|
|
$source->fetch_lines();
|
|
}
|
|
|
|
// createFromContract() returns 1 on success; the new invoice id is in $invoice->id.
|
|
$invoice = new Facture($db);
|
|
$this->assertGreaterThan(0, $invoice->createFromContract($source, $user), 'createFromContract');
|
|
$invoiceId = $invoice->id;
|
|
|
|
$reloaded = new Facture($db);
|
|
$reloaded->fetch($invoiceId);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLines($reloaded, 'Contract to invoice');
|
|
print __METHOD__." contract=".$id." invoice=".$invoiceId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer invoice -> credit note (Facture::createFromCurrent with invertdetail) must keep the TTC
|
|
* entry mode with the inverted sign.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerInvoiceToCreditNoteTtc()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('invoice')) {
|
|
$this->markTestSkipped('Module invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Facture($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCORIG'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Invoice create for credit note');
|
|
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$source = new Facture($db);
|
|
$source->fetch($id);
|
|
if (method_exists($source, 'fetch_lines')) {
|
|
$source->fetch_lines();
|
|
}
|
|
|
|
// Credit note = invoice with all amounts inverted (invertdetail = 1).
|
|
$creditId = $source->createFromCurrent($user, 1);
|
|
$this->assertGreaterThan(0, $creditId, 'createFromCurrent invertdetail');
|
|
|
|
$credit = new Facture($db);
|
|
$credit->fetch($creditId);
|
|
if (method_exists($credit, 'fetch_lines')) {
|
|
$credit->fetch_lines();
|
|
}
|
|
$this->assertBothPricedLinesInverted($credit, 'Invoice to credit note');
|
|
print __METHOD__." invoice=".$id." creditnote=".$creditId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier order - a line entered in HT must keep subprice_ttc = 0 (not marked TTC) on create and on
|
|
* clone, so it is never hijacked by the TTC preservation. Guards the supplier socle gating.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierOrderHtLineClone()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_order')) {
|
|
$this->markTestSkipped('Module supplier order disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new CommandeFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCHT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier order HT create source');
|
|
|
|
$object->addline('Product HT', self::PU_HT, self::QTY, self::VAT, 0, 0, $pid, 0, '', 0, 'HT', 0);
|
|
$object->addline('Free HT', self::PU_HT, self::QTY, self::VAT, 0, 0, 0, 0, '', 0, 'HT', 0);
|
|
|
|
$reloaded = new CommandeFournisseur($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertBothHtLines($reloaded, 'Supplier order HT create');
|
|
|
|
$source = new CommandeFournisseur($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier order HT createFromClone');
|
|
|
|
$clone = new CommandeFournisseur($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothHtLines($clone, 'Supplier order HT clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier invoice - a line entered in HT must keep subprice_ttc = 0 on create (via
|
|
* SupplierInvoiceLine::insert) and on clone. Guards the supplier invoice socle gating.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierInvoiceHtLineClone()
|
|
{
|
|
global $user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_invoice')) {
|
|
$this->markTestSkipped('Module supplier invoice disabled');
|
|
return;
|
|
}
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new FactureFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref_supplier = 'TTCHT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier invoice HT create source');
|
|
|
|
$object->addline('Product HT', self::PU_HT, self::VAT, 0, 0, self::QTY, $pid, 0, 0, 0, 0, 0, 'HT');
|
|
$object->addline('Free HT', self::PU_HT, self::VAT, 0, 0, self::QTY, 0, 0, 0, 0, 0, 0, 'HT');
|
|
|
|
$reloaded = new FactureFournisseur($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertBothHtLines($reloaded, 'Supplier invoice HT create');
|
|
|
|
$cloner = new FactureFournisseur($db);
|
|
$clonedId = $cloner->createFromClone($user, $id);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier invoice HT createFromClone');
|
|
|
|
$clone = new FactureFournisseur($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertBothHtLines($clone, 'Supplier invoice HT clone');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer proposal - with MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1 (rounding of total), a mixed order
|
|
* (2 lines entered HT + 2 lines entered TTC, like customer proposal id=205) must survive create /
|
|
* no-op / clone with no drift, and a discount keeps each entry mode.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerProposalRoundOfTotalStable()
|
|
{
|
|
global $conf,$user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('propal')) {
|
|
$this->markTestSkipped('Module propal disabled');
|
|
return;
|
|
}
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1;
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Propal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCROT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer proposal ROT create');
|
|
$object->addline('Product HT', self::PU_HT, self::QTY, self::VAT, 0, 0, $pid, 0, 'HT', 0);
|
|
$object->addline('Free HT', self::PU_HT, self::QTY, self::VAT, 0, 0, 0, 0, 'HT', 0);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Propal($db);
|
|
$reloaded->fetch($id);
|
|
$reloaded->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($reloaded, 'Customer proposal ROT create');
|
|
$s0 = $this->snapshotPricedLines($reloaded);
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $pu, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, $line->desc, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterNoop = new Propal($db);
|
|
$afterNoop->fetch($id);
|
|
$afterNoop->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($afterNoop), 'Customer proposal ROT no-op');
|
|
|
|
$source = new Propal($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer proposal ROT clone');
|
|
$clone = new Propal($db);
|
|
$clone->fetch($clonedId);
|
|
$clone->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($clone), 'Customer proposal ROT clone');
|
|
|
|
foreach ($this->pricedLines($afterNoop) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $pu, $line->qty, self::REMISE, $line->tva_tx, 0, 0, $line->desc, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterRemise = new Propal($db);
|
|
$afterRemise->fetch($id);
|
|
$afterRemise->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($afterRemise, 'Customer proposal ROT discount');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer order - mixed HT/TTC order, MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerOrderRoundOfTotalStable()
|
|
{
|
|
global $conf,$user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('order')) {
|
|
$this->markTestSkipped('Module order disabled');
|
|
return;
|
|
}
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1;
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Commande($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCROT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer order ROT create');
|
|
$object->addline('Product HT', self::PU_HT, self::QTY, self::VAT, 0, 0, $pid, 0, 0, 0, 'HT', 0);
|
|
$object->addline('Free HT', self::PU_HT, self::QTY, self::VAT, 0, 0, 0, 0, 0, 0, 'HT', 0);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Commande($db);
|
|
$reloaded->fetch($id);
|
|
$reloaded->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($reloaded, 'Customer order ROT create');
|
|
$s0 = $this->snapshotPricedLines($reloaded);
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterNoop = new Commande($db);
|
|
$afterNoop->fetch($id);
|
|
$afterNoop->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($afterNoop), 'Customer order ROT no-op');
|
|
|
|
$source = new Commande($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer order ROT clone');
|
|
$clone = new Commande($db);
|
|
$clone->fetch($clonedId);
|
|
$clone->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($clone), 'Customer order ROT clone');
|
|
|
|
foreach ($this->pricedLines($afterNoop) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->qty, self::REMISE, $line->tva_tx, 0, 0, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterRemise = new Commande($db);
|
|
$afterRemise->fetch($id);
|
|
$afterRemise->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($afterRemise, 'Customer order ROT discount');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Customer invoice - mixed HT/TTC order, MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testCustomerInvoiceRoundOfTotalStable()
|
|
{
|
|
global $conf,$user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('invoice')) {
|
|
$this->markTestSkipped('Module invoice disabled');
|
|
return;
|
|
}
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1;
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new Facture($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCROT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Customer invoice ROT create');
|
|
$object->addline('Product HT', self::PU_HT, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 0, 0, 0, 'HT', 0);
|
|
$object->addline('Free HT', self::PU_HT, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 0, 0, 0, 'HT', 0);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', '', 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new Facture($db);
|
|
$reloaded->fetch($id);
|
|
$reloaded->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($reloaded, 'Customer invoice ROT create');
|
|
$s0 = $this->snapshotPricedLines($reloaded);
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->qty, $line->remise_percent, $line->date_start, $line->date_end, $line->tva_tx, 0, 0, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterNoop = new Facture($db);
|
|
$afterNoop->fetch($id);
|
|
$afterNoop->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($afterNoop), 'Customer invoice ROT no-op');
|
|
|
|
$source = new Facture($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $id);
|
|
$this->assertGreaterThan(0, $clonedId, 'Customer invoice ROT clone');
|
|
$clone = new Facture($db);
|
|
$clone->fetch($clonedId);
|
|
$clone->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($clone), 'Customer invoice ROT clone');
|
|
|
|
foreach ($this->pricedLines($afterNoop) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->qty, self::REMISE, $line->date_start, $line->date_end, $line->tva_tx, 0, 0, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterRemise = new Facture($db);
|
|
$afterRemise->fetch($id);
|
|
$afterRemise->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($afterRemise, 'Customer invoice ROT discount');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier proposal - mixed HT/TTC order, MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 1.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierProposalRoundOfTotalStable()
|
|
{
|
|
global $conf,$user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('supplier_proposal')) {
|
|
$this->markTestSkipped('Module supplier_proposal disabled');
|
|
return;
|
|
}
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 1;
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new SupplierProposal($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCROT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier proposal ROT create');
|
|
$object->addline('Product HT', self::PU_HT, self::QTY, self::VAT, 0, 0, $pid, 0, 'HT', 0);
|
|
$object->addline('Free HT', self::PU_HT, self::QTY, self::VAT, 0, 0, 0, 0, 'HT', 0);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new SupplierProposal($db);
|
|
$reloaded->fetch($id);
|
|
if (method_exists($reloaded, 'fetch_lines')) {
|
|
$reloaded->fetch_lines();
|
|
}
|
|
$this->assertMixedSubpriceTtc($reloaded, 'Supplier proposal ROT create');
|
|
$s0 = $this->snapshotPricedLines($reloaded);
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $pu, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, $line->desc, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterNoop = new SupplierProposal($db);
|
|
$afterNoop->fetch($id);
|
|
if (method_exists($afterNoop, 'fetch_lines')) {
|
|
$afterNoop->fetch_lines();
|
|
}
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($afterNoop), 'Supplier proposal ROT no-op');
|
|
|
|
$source = new SupplierProposal($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier proposal ROT clone');
|
|
$clone = new SupplierProposal($db);
|
|
$clone->fetch($clonedId);
|
|
if (method_exists($clone, 'fetch_lines')) {
|
|
$clone->fetch_lines();
|
|
}
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($clone), 'Supplier proposal ROT clone');
|
|
|
|
foreach ($this->pricedLines($afterNoop) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $pu, $line->qty, self::REMISE, $line->tva_tx, 0, 0, $line->desc, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterRemise = new SupplierProposal($db);
|
|
$afterRemise->fetch($id);
|
|
if (method_exists($afterRemise, 'fetch_lines')) {
|
|
$afterRemise->fetch_lines();
|
|
}
|
|
$this->assertMixedSubpriceTtc($afterRemise, 'Supplier proposal ROT discount');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier order - mixed HT/TTC order, MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 1.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierOrderRoundOfTotalStable()
|
|
{
|
|
global $conf,$user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_order')) {
|
|
$this->markTestSkipped('Module supplier order disabled');
|
|
return;
|
|
}
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 1;
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new CommandeFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref = 'TTCROT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier order ROT create');
|
|
$object->addline('Product HT', self::PU_HT, self::QTY, self::VAT, 0, 0, $pid, 0, '', 0, 'HT', 0);
|
|
$object->addline('Free HT', self::PU_HT, self::QTY, self::VAT, 0, 0, 0, 0, '', 0, 'HT', 0);
|
|
$object->addline('Product TTC', 0, self::QTY, self::VAT, 0, 0, $pid, 0, '', 0, 'TTC', self::PU_TTC);
|
|
$object->addline('Free TTC', 0, self::QTY, self::VAT, 0, 0, 0, 0, '', 0, 'TTC', self::PU_TTC);
|
|
|
|
$reloaded = new CommandeFournisseur($db);
|
|
$reloaded->fetch($id);
|
|
$reloaded->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($reloaded, 'Supplier order ROT create');
|
|
$s0 = $this->snapshotPricedLines($reloaded);
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->qty, $line->remise_percent, $line->tva_tx, 0, 0, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterNoop = new CommandeFournisseur($db);
|
|
$afterNoop->fetch($id);
|
|
$afterNoop->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($afterNoop), 'Supplier order ROT no-op');
|
|
|
|
$source = new CommandeFournisseur($db);
|
|
$source->fetch($id);
|
|
$clonedId = $source->createFromClone($user, $socid);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier order ROT clone');
|
|
$clone = new CommandeFournisseur($db);
|
|
$clone->fetch($clonedId);
|
|
$clone->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($clone), 'Supplier order ROT clone');
|
|
|
|
foreach ($this->pricedLines($afterNoop) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->qty, self::REMISE, $line->tva_tx, 0, 0, $ttc ? 'TTC' : 'HT');
|
|
}
|
|
$afterRemise = new CommandeFournisseur($db);
|
|
$afterRemise->fetch($id);
|
|
$afterRemise->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($afterRemise, 'Supplier order ROT discount');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
|
|
/**
|
|
* Supplier invoice - mixed HT/TTC order, MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 1.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function testSupplierInvoiceRoundOfTotalStable()
|
|
{
|
|
global $conf,$user,$db;
|
|
$this->restoreGlobals();
|
|
if (!isModEnabled('fournisseur') && !isModEnabled('supplier_invoice')) {
|
|
$this->markTestSkipped('Module supplier invoice disabled');
|
|
return;
|
|
}
|
|
$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND_SUPPLIER = 1;
|
|
|
|
$socid = self::$socid;
|
|
$pid = self::$productid;
|
|
|
|
$object = new FactureFournisseur($db);
|
|
$object->initAsSpecimen();
|
|
$object->socid = $socid;
|
|
$object->ref_supplier = 'TTCROT'.substr(uniqid(), -8);
|
|
$object->lines = array();
|
|
$id = $object->create($user);
|
|
$this->assertGreaterThan(0, $id, 'Supplier invoice ROT create');
|
|
$object->addline('Product HT', self::PU_HT, self::VAT, 0, 0, self::QTY, $pid, 0, 0, 0, 0, 0, 'HT');
|
|
$object->addline('Free HT', self::PU_HT, self::VAT, 0, 0, self::QTY, 0, 0, 0, 0, 0, 0, 'HT');
|
|
$object->addline('Product TTC', self::PU_TTC, self::VAT, 0, 0, self::QTY, $pid, 0, 0, 0, 0, 0, 'TTC');
|
|
$object->addline('Free TTC', self::PU_TTC, self::VAT, 0, 0, self::QTY, 0, 0, 0, 0, 0, 0, 'TTC');
|
|
|
|
$reloaded = new FactureFournisseur($db);
|
|
$reloaded->fetch($id);
|
|
$reloaded->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($reloaded, 'Supplier invoice ROT create');
|
|
$s0 = $this->snapshotPricedLines($reloaded);
|
|
|
|
foreach ($this->pricedLines($reloaded) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->tva_tx, 0, 0, $line->qty, 0, $ttc ? 'TTC' : 'HT', 0, 0, $line->remise_percent);
|
|
}
|
|
$afterNoop = new FactureFournisseur($db);
|
|
$afterNoop->fetch($id);
|
|
$afterNoop->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($afterNoop), 'Supplier invoice ROT no-op');
|
|
|
|
$cloner = new FactureFournisseur($db);
|
|
$clonedId = $cloner->createFromClone($user, $id);
|
|
$this->assertGreaterThan(0, $clonedId, 'Supplier invoice ROT clone');
|
|
$clone = new FactureFournisseur($db);
|
|
$clone->fetch($clonedId);
|
|
$clone->fetch_lines();
|
|
$this->assertLinesUnchanged($s0, $this->snapshotPricedLines($clone), 'Supplier invoice ROT clone');
|
|
|
|
foreach ($this->pricedLines($afterNoop) as $line) {
|
|
$ttc = $line->wasEnteredIncludingTax();
|
|
$pu = $ttc ? (float) $line->subprice_ttc : (float) $line->subprice;
|
|
$object->updateline($line->id, $line->desc, $pu, $line->tva_tx, 0, 0, $line->qty, 0, $ttc ? 'TTC' : 'HT', 0, 0, self::REMISE);
|
|
}
|
|
$afterRemise = new FactureFournisseur($db);
|
|
$afterRemise->fetch($id);
|
|
$afterRemise->fetch_lines();
|
|
$this->assertMixedSubpriceTtc($afterRemise, 'Supplier invoice ROT discount');
|
|
print __METHOD__." id=".$id." clone=".$clonedId."\n";
|
|
}
|
|
}
|