mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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
This commit is contained in:
parent
a9e1d45381
commit
5e8e47ef77
@ -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
|
||||
|
262
core/model/fieldtypes/Money.php
Normal file
262
core/model/fieldtypes/Money.php
Normal file
@ -0,0 +1,262 @@
|
||||
<?php
|
||||
/**
|
||||
* Partially based on Zend_Currency.
|
||||
*
|
||||
* @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
* @version $Id: Currency.php 6137 2007-08-19 14:55:27Z shreef $
|
||||
*/
|
||||
|
||||
set_include_path(get_include_path() . PATH_SEPARATOR . BASE_PATH . '/sapphire/thirdparty');
|
||||
require_once 'Zend/Currency.php';
|
||||
|
||||
/**
|
||||
* Implements the "Money" pattern.
|
||||
*
|
||||
* @see http://www.martinfowler.com/eaaCatalog/money.html
|
||||
*
|
||||
* @todo Support different ways of rounding
|
||||
* @todo Equality operators
|
||||
* @todo Addition, substraction and allocation of values
|
||||
* @todo Model validation for $allowedCurrencies
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage model
|
||||
*/
|
||||
class Money extends DBField implements CompositeDBField {
|
||||
|
||||
/**
|
||||
* @var string $getCurrency()
|
||||
*/
|
||||
protected $currency;
|
||||
|
||||
/**
|
||||
* @var float $currencyAmount
|
||||
*/
|
||||
protected $amount;
|
||||
|
||||
/**
|
||||
* @var boolean $isChanged
|
||||
*/
|
||||
protected $isChanged = false;
|
||||
|
||||
/**
|
||||
* @var string $locale
|
||||
*/
|
||||
protected $locale = null;
|
||||
|
||||
/**
|
||||
* @var Zend_Currency
|
||||
*/
|
||||
protected $currencyLib;
|
||||
|
||||
/**
|
||||
* Limit the currencies
|
||||
* @var array $allowedCurrencies
|
||||
*/
|
||||
protected $allowedCurrencies;
|
||||
|
||||
/**
|
||||
* @param array
|
||||
*/
|
||||
static $composite_db = array(
|
||||
"Currency" => "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;
|
||||
}
|
||||
}
|
||||
?>
|
273
tests/model/MoneyTest.php
Normal file
273
tests/model/MoneyTest.php
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* Partially based on Zend_CurrencyTest.
|
||||
*
|
||||
* @copyright Copyright (c) 2006 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
* @license http://framework.zend.com/license/new-bsd New BSD License
|
||||
* @version $Id: CurrencyTest.php 14644 2009-04-04 18:59:08Z thomas $
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage tests
|
||||
*/
|
||||
class MoneyTest extends SapphireTest {
|
||||
|
||||
static $fixture_file = 'sapphire/tests/model/MoneyTest.yml';
|
||||
|
||||
/**
|
||||
* Write a Money object to the database, then re-read it to ensure it
|
||||
* is re-read properly.
|
||||
*/
|
||||
function testGettingWrittenDataObject() {
|
||||
$obj = new MoneyTest_DataObject();
|
||||
|
||||
$m = new Money();
|
||||
$m->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',
|
||||
);
|
||||
|
||||
}
|
||||
?>
|
4
tests/model/MoneyTest.yml
Normal file
4
tests/model/MoneyTest.yml
Normal file
@ -0,0 +1,4 @@
|
||||
MoneyTest_DataObject:
|
||||
test1:
|
||||
MyMoneyCurrency: EUR
|
||||
MyMoneyAmount: 1.23
|
Loading…
Reference in New Issue
Block a user