diff --git a/ChangeLog b/ChangeLog index 30c741222d434ec33e966d3efc8e6c75092544bd..43dc578e49bb52c7d88aa1e7d91d151d1b0de5ac 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,8 +6,8 @@ English Dolibarr ChangeLog For users: - New: [ task #877 ] Reorganize menus. - New: [ task #858 ] Holiday module: note on manual holiday assignation. -- New: [ task #892 ] Add hidden option in thirdparty customer/supplier module to - hide non active companies in select_company method. +- New: [ task #892 ] Add hidden option in thirdparty customer/supplier module to hide non active + companies in select_company method. - New: [ task #531 ] Add a workload field on tasks. - New: Add graph of bank account input/output into input-output report page. - New: Add script export-bank-receipts.php @@ -24,7 +24,8 @@ For users: - New: [ task #928 ] Add extrafield feature on invoice lines. - New: Add option ADHERENT_LOGIN_NOT_REQUIRED. - New: Add a cron module to define scheduled jobs. -- New: Add new graphical boxes (customer invoices per month). +- New: Add new graphical boxes (customer invoices and orders per month). +- New: [ task #286 ] Enhance rounding function of prices to allow round of sum instead of sum of rounding. - Qual: Implement same rule for return value of all command line scripts (0 when success, <>0 if error). For translators: diff --git a/htdocs/core/class/commonobject.class.php b/htdocs/core/class/commonobject.class.php index 1bb675a6a0bf18b7e306b3edd0a47de5a4ef271b..0de5f388b42f2e185a0da08719850e4581fc4c7f 100644 --- a/htdocs/core/class/commonobject.class.php +++ b/htdocs/core/class/commonobject.class.php @@ -42,7 +42,7 @@ abstract class CommonObject public $firstname; public $civility_id; public $import_key; - + public $array_options=array(); public $linkedObjectsIds; @@ -53,7 +53,7 @@ abstract class CommonObject /** * Method to output saved errors - * + * * @return string String with errors */ function errorsToString() @@ -1464,15 +1464,18 @@ abstract class CommonObject } /** - * Update total_ht, total_ttc and total_vat for an object (sum of lines) + * Update total_ht, total_ttc, total_vat, total_localtax1, total_localtax2 for an object (sum of lines). + * Must be called at end of methods addline, updateline. * * @param int $exclspec Exclude special product (product_type=9) - * @param int $roundingadjust -1=Use default method (MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND or 0), 0=Use total of rounding, 1=Use rounding of total + * @param int $roundingadjust -1=Use default method (MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND if defined, or 0), 0=Force use total of rounding, 1=Force use rounding of total * @param int $nodatabaseupdate 1=Do not update database. Update only properties of object. * @return int <0 if KO, >0 if OK */ function update_price($exclspec=0,$roundingadjust=-1,$nodatabaseupdate=0) { + global $conf; + include_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php'; if ($roundingadjust < 0 && isset($conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND)) $roundingadjust=$conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND; @@ -1486,7 +1489,7 @@ abstract class CommonObject $fieldlocaltax2='total_localtax2'; if ($this->element == 'facture_fourn' || $this->element == 'invoice_supplier') $fieldtva='tva'; - $sql = 'SELECT qty, total_ht, '.$fieldtva.' as total_tva, total_ttc, '.$fieldlocaltax1.' as total_localtax1, '.$fieldlocaltax2.' as total_localtax2,'; + $sql = 'SELECT rowid, qty, total_ht, '.$fieldtva.' as total_tva, total_ttc, '.$fieldlocaltax1.' as total_localtax1, '.$fieldlocaltax2.' as total_localtax2,'; $sql.= ' tva_tx as vatrate, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type'; $sql.= ' FROM '.MAIN_DB_PREFIX.$this->table_element_line; $sql.= ' WHERE '.$this->fk_element.' = '.$this->id; @@ -1496,6 +1499,7 @@ abstract class CommonObject if ($this->table_element_line == 'contratdet') $product_field=''; // contratdet table has no product_type field if ($product_field) $sql.= ' AND '.$product_field.' <> 9'; } + $sql.= ' ORDER by rowid'; // We want to be sure to always use same order of line to not change lines differently when option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND is used dol_syslog(get_class($this)."::update_price sql=".$sql); $resql = $this->db->query($sql); @@ -1506,8 +1510,9 @@ abstract class CommonObject $this->total_localtax1 = 0; $this->total_localtax2 = 0; $this->total_ttc = 0; - $vatrates = array(); - $vatrates_alllines = array(); + $total_ht_by_vats = array(); + $total_tva_by_vats = array(); + $total_ttc_by_vats = array(); $num = $this->db->num_rows($resql); $i = 0; @@ -1515,42 +1520,34 @@ abstract class CommonObject { $obj = $this->db->fetch_object($resql); - $this->total_ht += $obj->total_ht; + $this->total_ht += $obj->total_ht; // The only field visible at line level $this->total_tva += $obj->total_tva; $this->total_localtax1 += $obj->total_localtax1; $this->total_localtax2 += $obj->total_localtax2; $this->total_ttc += $obj->total_ttc; + $total_ht_by_vats[$obj->vatrate] += $obj->total_ht; + $total_tva_by_vats[$obj->vatrate] += $obj->total_tva; + $total_ttc_by_vats[$obj->vatrate] += $obj->total_ttc; - // Check if there is a global invoice tax for this vat rate - // FIXME: We should have no database access into this function. Also localtax 7 seems to have problem so i add condition to avoid it into standard usage without loosing it. - if (! empty($conf->global->MAIN_USE_LOCALTAX_TYPE_7)) + if ($roundingadjust) // Check if we need adjustement onto line for vat { - if ($this->total_localtax1 == 0) - { - // Search to know if there is a localtax of type 7 - // TODO : store local taxes types into object lines and remove this. We should use here $obj->localtax1_type but it is not yet filled into database, so we search into table of vat rate - global $mysoc; - $localtax1_array=getLocalTaxesFromRate($vatrate,1,$mysoc); - if (empty($obj->localtax1_type)) - { - $obj->localtax1_type = $localtax1_array[0]; - $obj->localtax1_tx = $localtax1_array[1]; - } - //end TODO - } - if ($this->total_localtax2 == 0) - { - // Search to know if there is a localtax of type 7 - // TODO : store local taxes types into object lines and remove this. We should use here $obj->localtax1_type but it is not yet filled into database, so we search into table of vat rate - global $mysoc; - $localtax2_array=getLocalTaxesFromRate($vatrate,2,$mysoc); - if (empty($obj->localtax2_type)) - { - $obj->localtax2_type = $localtax2_array[0]; - $obj->localtax2_tx = $localtax2_array[1]; - } - //end TODO - } + $tmpvat=price2num($total_ht_by_vats[$obj->vatrate] * $obj->vatrate / 100, 'MT', 1); + $diff=price2num($total_tva_by_vats[$obj->vatrate]-$tmpvat, 'MT', 1); + //print 'Line '.$i.' rowid='.$obj->rowid.' vat_rate='.$obj->vatrate.' total_ht='.$obj->total_ht.' total_tva='.$obj->total_tva.' total_ttc='.$obj->total_ttc.' total_ht_by_vats='.$total_ht_by_vats[$obj->vatrate].' total_tva_by_vats='.$total_tva_by_vats[$obj->vatrate].' (new calculation = '.$tmpvat.') total_ttc_by_vats='.$total_ttc_by_vats[$obj->vatrate].($diff?" => DIFF":"")."<br>\n"; + if ($diff) + { + if ($diff > 0.1) { dol_print_error('','A rounding difference was detected but is to high to be corrected'); exit; } + $sqlfix="UPDATE ".MAIN_DB_PREFIX.$this->table_element_line." SET ".$fieldtva." = ".($obj->total_tva - $diff).", total_ttc = ".($obj->total_ttc - $diff)." WHERE rowid = ".$obj->rowid; + //print 'We found a difference of '.$diff.' for line rowid = '.$obj->rowid.". Run sqlfix = ".$sqlfix."<br>\n"; + dol_syslog('We found a difference of '.$diff.' for line rowid = '.$obj->rowid.". Run sqlfix = ".$sqlfix); + $resqlfix=$this->db->query($sqlfix); + if (! $resqlfix) dol_print_error($this->db,'Failed to update line'); + $this->total_tva -= $diff; + $this->total_ttc -= $diff; + $total_tva_by_vats[$obj->vatrate] -= $diff; + $total_ttc_by_vats[$obj->vatrate] -= $diff; + + } } $i++; @@ -1567,6 +1564,7 @@ abstract class CommonObject $fieldlocaltax1='localtax1'; $fieldlocaltax2='localtax2'; $fieldttc='total_ttc'; + // Specific code for backward compatibility with old field names if ($this->element == 'facture' || $this->element == 'facturerec') $fieldht='total'; if ($this->element == 'facture_fourn' || $this->element == 'invoice_supplier') $fieldtva='total_tva'; if ($this->element == 'propal') $fieldttc='total'; diff --git a/test/phpunit/FactureTestRounding.php b/test/phpunit/FactureTestRounding.php index f000b74289d529992fac187a61aafe274c2ff6dd..446b6893ae0ac18ed77ca5729549a6924ae5f890 100644 --- a/test/phpunit/FactureTestRounding.php +++ b/test/phpunit/FactureTestRounding.php @@ -1,5 +1,5 @@ <?php -/* Copyright (C) 2012 Laurent Destailleur <eldy@users.sourceforge.net> +/* Copyright (C) 2012-2013 Laurent Destailleur <eldy@users.sourceforge.net> * * 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 @@ -157,9 +157,9 @@ class FactureTestRounding extends PHPUnit_Framework_TestCase /** * testFactureRoundingCreate2 - * + * * @return int - * + * * @depends testFactureRoundingCreate1 * Test according to page http://wiki.dolibarr.org/index.php/Draft:VAT_calculation_and_rounding#Standard_usage */ @@ -194,5 +194,133 @@ class FactureTestRounding extends PHPUnit_Framework_TestCase //$this->assertEquals($newlocalobject->total_ttc, 2.73); return $result; } + + + /** + * testFactureAddLine1 + * + * @return void + */ + public function testFactureAddLine1() + { + global $conf,$user,$langs,$db; + $conf=$this->savconf; + $user=$this->savuser; + $langs=$this->savlangs; + $db=$this->savdb; + + // With option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 0 + $conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=0; + $localobject1a=new Facture($this->savdb); + $localobject1a->initAsSpecimen('nolines'); + $facid=$localobject1a->create($user); + $localobject1a->addline($facid, 'Line 1', 6.36, 15, 21); // This include update_price + print __METHOD__." id=".$facid." total_ttc=".$localobject1a->total_ttc."\n"; + $this->assertEquals( 95.40, $localobject1a->total_ht); + $this->assertEquals( 20.03, $localobject1a->total_tva); + $this->assertEquals(115.43, $localobject1a->total_ttc); + + // With option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1 + $conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=1; + $localobject1b=new Facture($this->savdb); + $localobject1b->initAsSpecimen('nolines'); + $facid=$localobject1b->create($user); + $localobject1b->addline($facid, 'Line 1', 6.36, 15, 21); // This include update_price + print __METHOD__." id=".$facid." total_ttc=".$localobject1b->total_ttc."\n"; + $this->assertEquals( 95.40, $localobject1b->total_ht, 'testFactureAddLine1 total_ht'); + $this->assertEquals( 20.03, $localobject1b->total_tva, 'testFactureAddLine1 total_tva'); + $this->assertEquals(115.43, $localobject1b->total_ttc, 'testFactureAddLine1 total_ttc'); + } + + /** + * testFactureAddLine2 + * + * @return void + * + * @depends testFactureAddLine1 + * The depends says test is run only if previous is ok + */ + public function testFactureAddLine2() + { + global $conf,$user,$langs,$db; + $conf=$this->savconf; + $user=$this->savuser; + $langs=$this->savlangs; + $db=$this->savdb; + + // With option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 0 + $conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=0; + $localobject2=new Facture($this->savdb); + $localobject2->initAsSpecimen('nolines'); + $facid=$localobject2->create($user); + $localobject2->addline($facid, 'Line 1', 6.36, 5, 21); + $localobject2->addline($facid, 'Line 2', 6.36, 5, 21); + $localobject2->addline($facid, 'Line 3', 6.36, 5, 21); + print __METHOD__." id=".$facid." total_ttc=".$localobject2->total_ttc."\n"; + $this->assertEquals( 95.40, $localobject2->total_ht); + $this->assertEquals( 20.04, $localobject2->total_tva); + $this->assertEquals(115.44, $localobject2->total_ttc); + + // With option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1 + $conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=1; + $localobject2=new Facture($this->savdb); + $localobject2->initAsSpecimen('nolines'); + $facid=$localobject2->create($user); + $localobject2->addline($facid, 'Line 1', 6.36, 5, 21); + $localobject2->addline($facid, 'Line 2', 6.36, 5, 21); + $localobject2->addline($facid, 'Line 3', 6.36, 5, 21); + print __METHOD__." id=".$facid." total_ttc=".$localobject2->total_ttc."\n"; + $this->assertEquals( 95.40, $localobject2->total_ht); + $this->assertEquals( 20.03, $localobject2->total_tva); + $this->assertEquals(115.43, $localobject2->total_ttc); + } + + /** + * testFactureAddLine3 + * + * @return void + * + * @depends testFactureAddLine2 + * The depends says test is run only if previous is ok + */ + public function testFactureAddLine3() + { + global $conf,$user,$langs,$db; + $conf=$this->savconf; + $user=$this->savuser; + $langs=$this->savlangs; + $db=$this->savdb; + + // With option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 0 + $conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=0; + $localobject3=new Facture($this->savdb); + $localobject3->initAsSpecimen('nolines'); + $facid=$localobject3->create($user); + $localobject3->addline($facid, 'Line 1', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 2', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 3', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 4', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 5', 6.36, 3, 21); + print __METHOD__." id=".$facid." total_ttc=".$localobject3->total_ttc."\n"; + $this->assertEquals( 95.40, $localobject3->total_ht); + $this->assertEquals( 20.05, $localobject3->total_tva); + $this->assertEquals(115.45, $localobject3->total_ttc); + + // With option MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND = 1 + $conf->global->MAIN_ROUNDOFTOTAL_NOT_TOTALOFROUND=1; + $localobject3=new Facture($this->savdb); + $localobject3->initAsSpecimen('nolines'); + $facid=$localobject3->create($user); + $localobject3->addline($facid, 'Line 1', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 2', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 3', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 4', 6.36, 3, 21); + $localobject3->addline($facid, 'Line 5', 6.36, 3, 21); + print __METHOD__." id=".$facid." total_ttc=".$localobject3->total_ttc."\n"; + $this->assertEquals( 95.40, $localobject3->total_ht); + $this->assertEquals( 20.03, $localobject3->total_tva); + $this->assertEquals(115.43, $localobject3->total_ttc); + } + } ?> \ No newline at end of file