From 5e8e47ef773bb213445716c9a28006fe48f22694 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 5 May 2009 08:10:51 +0000 Subject: [PATCH] FEATURE Added Money class for managing monetary amounts with currencies through the Money design pattern. Uses the CompositeDBField interface to contain multiple database columns in a single DBField git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@76100 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/model/fieldtypes/Currency.php | 2 +- core/model/fieldtypes/Money.php | 262 +++++++++++++++++++++++++++ tests/model/MoneyTest.php | 273 +++++++++++++++++++++++++++++ tests/model/MoneyTest.yml | 4 + 4 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 core/model/fieldtypes/Money.php create mode 100644 tests/model/MoneyTest.php create mode 100644 tests/model/MoneyTest.yml diff --git a/core/model/fieldtypes/Currency.php b/core/model/fieldtypes/Currency.php index da8f41de9..f77bd3e55 100644 --- a/core/model/fieldtypes/Currency.php +++ b/core/model/fieldtypes/Currency.php @@ -3,7 +3,7 @@ * Represents a decimal field containing a currency amount. * Currency the currency class only supports single currencies. * - * @todo Add localization support, see http://open.silverstripe.com/ticket/2931 + * @deprecated 2.5 Use Money class * * @package sapphire * @subpackage model diff --git a/core/model/fieldtypes/Money.php b/core/model/fieldtypes/Money.php new file mode 100644 index 000000000..680502e9f --- /dev/null +++ b/core/model/fieldtypes/Money.php @@ -0,0 +1,262 @@ + "Varchar(3)", + "Amount" => "Decimal(14,2)", + ); + + function __construct($name = null) { + $this->currencyLib = new Zend_Currency(i18n::default_locale()); + + parent::__construct($name); + } + + public function composite_db(){ + return self::$composite_db; + } + + function requireField() { + $composite_db = $this->composite_db(); + foreach($composite_db as $name => $type){ + DB::requireField($this->tableName, $this->name.$name, $type); + } + } + + function writeToManipulation(&$manipulation) { + $manipulation['fields'][$this->name.'Currency'] = $this->prepValueForDB($this->getCurrency()); + $manipulation['fields'][$this->name.'Amount'] = $this->getAmount(); + } + + function setValue($value,$record=null) { + //var_dump($value); + //var_dump($record); + if($record && isset($record[$this->name . 'Currency']) && isset($record[$this->name . 'Amount'])) { + if($record[$this->name . 'Currency'] && $record[$this->name . 'Amount']) { + $this->setCurrency($record[$this->name . 'Currency']); + $this->setAmount($record[$this->name . 'Amount']); + } else { + $this->value = $this->nullValue(); + } + } elseif ($value instanceof Money) { + $this->setCurrency($value->getCurrency()); + $this->setAmount($value->getAmount()); + } else if (is_array($value)) { + if (array_key_exists('Currency', $value)) { + $this->setCurrency($value['Currency']); + $this->isChanged = true; + } + if (array_key_exists('Amount', $value)) { + $this->setAmount($value['Amount']); + $this->isChanged = true; + } + } else { + user_error('Invalid value in Money->setValue()', E_USER_ERROR); + } + } + + /** + * @return string + */ + function Nice($options = array()) { + return $this->currencyLib->toCurrency($this->getAmount(), $options); + } + + /** + * @return string + */ + function getCurrency() { + return $this->currency; + } + + /** + * @param string + */ + function setCurrency($currency) { + $this->currency = $currency; + } + + /** + * @todo Return casted Float DBField? + * + * @return float + */ + function getAmount() { + return $this->amount; + } + + /** + * @param float $amount + */ + function setAmount($amount) { + $this->amount = (float)$amount; + } + + /** + * @return boolean + */ + function hasValue() { + return ($this->getCurrency() && is_numeric($this->getAmount())); + } + + function isChanged() { + return $this->isChanged; + } + + /** + * @param string $locale + */ + function setLocale($locale) { + $this->locale = $locale; + $this->currencyLib->setLocale($locale); + } + + /** + * @return string + */ + function getLocale() { + return ($this->locale) ? $this->locale : i18n::get_locale(); + } + + /** + * @return string + */ + function getSymbol($currency = null, $locale = null) { + + if($locale === null) $locale = $this->getLocale(); + if($currency === null) $currency = $this->getCurrency(); + + return $this->currencyLib->getSymbol($currency, $locale); + } + + /** + * @return string + */ + function getShortName($currency = null, $locale = null) { + if($locale === null) $locale = $this->getLocale(); + if($currency === null) $currency = $this->getCurrency(); + + return $this->currencyLib->getShortName($currency, $locale); + } + + /** + * @return string + */ + function getName($currency = null, $locale = null) { + if($locale === null) $locale = $this->getLocale(); + if($currency === null) $currency = $this->getCurrency(); + + return $this->currencyLib->getName($currency, $locale); + } + + /** + * @param array $arr + */ + function setAllowedCurrencies($arr) { + $this->allowedCurrencies = $arr; + } + + /** + * @return array + */ + function getAllowedCurrencies() { + return $this->allowedCurrencies; + } + + /** + * @todo Implement this + */ + function toString() { + + } + + /** + * Returns a CompositeField instance used as a default + * for form scaffolding. + * + * Used by {@link SearchContext}, {@link ModelAdmin}, {@link DataObject::scaffoldFormFields()} + * + * @param string $title Optional. Localized title of the generated instance + * @return FormField + */ + public function scaffoldFormField($title = null) { + $fieldAmount = new NumericField($this->name."Amount", 'Amount'); + + $allowedCurrencies = $this->getAllowedCurrencies(); + if($allowedCurrencies) { + $fieldCurrency = new DropdownField( + $this->name."Currency", + 'Currency', + array_combine($allowedCurrencies,$allowedCurrencies) + ); + } else { + $fieldCurrency = new TextField($this->name."Currency", 'Currency'); + } + + $field = new FieldGroup( + $fieldAmount, + $fieldCurrency + ); + + return $field; + } +} +?> \ No newline at end of file diff --git a/tests/model/MoneyTest.php b/tests/model/MoneyTest.php new file mode 100644 index 000000000..fb5afe976 --- /dev/null +++ b/tests/model/MoneyTest.php @@ -0,0 +1,273 @@ +setAmount(987.65); + $m->setCurrency('USD'); + $obj->MyMoney = $m; + $this->assertEquals("$987.65", $obj->MyMoney->Nice(), + "Money field not added to data object properly when read prior to first writing the record." + ); + + $objID = $obj->write(); + + $moneyTest = DataObject::get_by_id('MoneyTest_DataObject',$objID); + $this->assertTrue($moneyTest instanceof MoneyTest_DataObject); + $this->assertEquals('USD', $moneyTest->MyMoneyCurrency); + $this->assertEquals(987.65, $moneyTest->MyMoneyAmount); + $this->assertEquals("$987.65", $moneyTest->MyMoney->Nice(), + "Money field not added to data object properly when read." + ); + } + + public function testToCurrency() { + $USD = new Money(); + $USD->setCurrency('USD'); + $USD->setLocale('en_US'); + + $EGP = new Money(); + $EGP->setCurrency('EGP'); + $EGP->setLocale('ar_EG'); + + $USD->setAmount(53292.18); + $this->assertSame('$53,292.18', $USD->Nice()); + $USD->setAmount(53292.18); + $this->assertSame('$٥٣,٢٩٢.١٨', $USD->Nice(array('script' => 'Arab' ))); + $USD->setAmount(53292.18); + $this->assertSame('$ ٥٣.٢٩٢,١٨', $USD->Nice(array('script' => 'Arab', 'format' => 'de_AT'))); + $USD->setAmount(53292.18); + $this->assertSame('$ 53.292,18', $USD->Nice(array('format' => 'de_AT'))); + + $EGP->setAmount(53292.18); + $this->assertSame('ج.م.‏ 53٬292٫18', $EGP->Nice()); + $EGP->setAmount(53292.18); + $this->assertSame('ج.م.‏ ٥٣٬٢٩٢٫١٨', $EGP->Nice(array('script' => 'Arab' ))); + $EGP->setAmount(53292.18); + $this->assertSame('ج.م.‏ ٥٣.٢٩٢,١٨', $EGP->Nice(array('script' =>'Arab', 'format' => 'de_AT'))); + $EGP->setAmount(53292.18); + $this->assertSame('ج.م.‏ 53.292,18', $EGP->Nice(array('format' => 'de_AT'))); + + $USD = new Money(); + $USD->setLocale('en_US'); + $USD->setAmount(53292.18); + $this->assertSame('$53,292.18', $USD->Nice()); + /* + try { + $this->assertSame('$ 53,292.18', $USD->Nice('nocontent')); + $this->fail("No currency expected"); + } catch (Exception $e) { + $this->assertContains("has to be numeric", $e->getMessage()); + } + */ + + /* + $INR = new Money(); + $INR->setLocale('de_AT'); + $INR->setCurrency('INR'); + $INR->setAmount(1.2); + $this->assertSame('Rs. 1,20', $INR->Nice()); + $INR->setAmount(1); + $this->assertSame('Re. 1,00', $INR->Nice()); + $INR->setAmount(0); + $this->assertSame('Rs. 0,00', $INR->Nice()); + $INR->setAmount(-3); + $this->assertSame('-Rs. 3,00', $INR->Nice()); + */ + } + + public function testGetSign() { + $EGP = new Money(); + $EGP->setValue(array( + 'Currency' => 'EGP', + 'Amount' => 3.44 + )); + $EGP->setLocale('ar_EG'); + + $this->assertSame('ج.م.‏', $EGP->getSymbol('EGP','ar_EG')); + $this->assertSame('€', $EGP->getSymbol('EUR','de_AT')); + $this->assertSame('ج.م.‏', $EGP->getSymbol(null, 'ar_EG')); + //$this->assertSame('€', $EGP->getSymbol(null, 'de_AT')); + $this->assertSame('ج.م.‏', $EGP->getSymbol()); + + try { + $EGP->getSymbol('EGP', 'de_XX'); + $this->setExpectedException("Exception"); + } catch(Exception $e) { + } + + $EUR = new Money(); + $EUR->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 3.44 + )); + $EUR->setLocale('de_DE'); + $this->assertSame('€', $EUR->getSymbol()); + } + + public function testGetName() + { + $m = new Money(); + $m->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 3.44 + )); + $m->setLocale('ar_EG'); + + $this->assertSame('جنيه مصرى', $m->getName('EGP','ar_EG')); + $this->assertSame('Estnische Krone', $m->getName('EEK','de_AT')); + //$this->assertSame('جنيه مصرى', $m->getName(null, 'ar_EG')); + //$this->assertSame('Euro', $m->getName('de_AT')); + $this->assertSame('يورو', $m->getName()); + + try { + $m->getName('EGP', 'xy_XY'); + $this->setExpectedException("Exception"); + } catch(Exception $e) { + } + } + + public function testGetShortName() { + $m = new Money(); + $m->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 3.44 + )); + $m->setLocale('de_AT'); + + $this->assertSame('EUR', $m->getShortName('Euro', 'de_AT')); + $this->assertSame('USD', $m->getShortName('US-Dollar','de_AT')); + //$this->assertSame('EUR', $m->getShortName(null, 'de_AT')); + $this->assertSame('EUR', $m->getShortName()); + + try { + $m->getShortName('EUR', 'xy_ZT'); + $this->setExpectedException("Exception"); + } catch(Exception $e) { + } + } + + function testSetValueAsArray() { + $m = new Money(); + $m->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 3.44 + )); + $this->assertEquals( + $m->getCurrency(), + 'EUR' + ); + $this->assertEquals( + $m->getAmount(), + 3.44 + ); + } + + function testSetValueAsMoney() { + $m1 = new Money(); + $m1->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 3.44 + )); + $m2 = new Money(); + $m2->setValue($m1); + $this->assertEquals( + $m2->getCurrency(), + 'EUR' + ); + $this->assertEquals( + $m2->getAmount(), + 3.44 + ); + } + + function testHasValue() { + $m1 = new Money(); + $this->assertFalse($m1->hasValue()); + + $m2 = new Money(); + $m2->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 3.44 + )); + $this->assertTrue($m2->hasValue()); + + $m3 = new Money(); + $m3->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 0 + )); + $this->assertTrue($m3->hasValue()); + } + + function testLoadIntoDataObject() { + $obj = new MoneyTest_DataObject(); + + $this->assertType('Money', $obj->obj('MyMoney')); + + $m = new Money(); + $m->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 1.23 + )); + $obj->MyMoney = $m; + + $this->assertEquals($obj->MyMoney->getCurrency(), 'EUR'); + $this->assertEquals($obj->MyMoney->getAmount(), 1.23); + } + + function testWriteToDataObject() { + $obj = new MoneyTest_DataObject(); + $m = new Money(); + $m->setValue(array( + 'Currency' => 'EUR', + 'Amount' => 1.23 + )); + $obj->MyMoney = $m; + $obj->write(); + + $this->assertEquals( + 'EUR', + DB::query(sprintf( + 'SELECT "MyMoneyCurrency" FROM "MoneyTest_DataObject" WHERE "ID" = %d', + $obj->ID + ))->value() + ); + $this->assertEquals( + '1.23', + DB::query(sprintf( + 'SELECT "MyMoneyAmount" FROM "MoneyTest_DataObject" WHERE "ID" = %d', + $obj->ID + ))->value() + ); + } +} + +class MoneyTest_DataObject extends DataObject implements TestOnly { + static $db = array( + 'MyMoney' => 'Money', + //'MyOtherMoney' => 'Money', + ); + +} +?> \ No newline at end of file diff --git a/tests/model/MoneyTest.yml b/tests/model/MoneyTest.yml new file mode 100644 index 000000000..1e49e6e35 --- /dev/null +++ b/tests/model/MoneyTest.yml @@ -0,0 +1,4 @@ +MoneyTest_DataObject: + test1: + MyMoneyCurrency: EUR + MyMoneyAmount: 1.23 \ No newline at end of file