Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP]商品一覧と商品詳細で商品別税率が反映されないのを修正 #298

Closed
wants to merge 9 commits into from
130 changes: 121 additions & 9 deletions data/class/SC_Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ public function getDetail($product_id)
$arrProduct = (array)$objQuery->getRow('*', $from, $where, $arrWhereVal);

// 税込金額を設定する
SC_Product_Ex::setIncTaxToProduct($arrProduct);
$arrTaxRules = SC_Product_Ex::getProductsClassRelateTaxRule(array($arrProduct['product_id']));
SC_Product_Ex::setIncTaxToProduct($arrProduct, $arrTaxRules[$arrProduct['product_id']]);

return $arrProduct;
}
Expand Down Expand Up @@ -563,13 +564,16 @@ public function reduceStock($productClassId, $quantity)
*/
public static function setPriceTaxTo(&$arrProducts)
{
$arrTaxRules = SC_Product_Ex::getProductsClassRelateTaxRule(array_values(array_map(function ($arrProduct) {
return $arrProduct['product_id'];
}, $arrProducts)));
foreach ($arrProducts as &$arrProduct) {
$arrProduct['price01_min_format'] = number_format($arrProduct['price01_min']);
$arrProduct['price01_max_format'] = number_format($arrProduct['price01_max']);
$arrProduct['price02_min_format'] = number_format($arrProduct['price02_min']);
$arrProduct['price02_max_format'] = number_format($arrProduct['price02_max']);

SC_Product_Ex::setIncTaxToProduct($arrProduct);
SC_Product_Ex::setIncTaxToProduct($arrProduct, $arrTaxRules[$arrProduct['product_id']]);

$arrProduct['price01_min_inctax_format'] = number_format($arrProduct['price01_min_inctax']);
$arrProduct['price01_max_inctax_format'] = number_format($arrProduct['price01_max_inctax']);
Expand Down Expand Up @@ -598,23 +602,31 @@ public static function setPriceTaxTo(&$arrProducts)
*/
public static function setIncTaxToProducts(&$arrProducts)
{
$arrTaxRules = SC_Product_Ex::getProductsClassRelateTaxRule(array_values(array_map(function ($arrProduct) {
return $arrProduct['product_id'];
}, $arrProducts)));

foreach ($arrProducts as &$arrProduct) {
SC_Product_Ex::setIncTaxToProduct($arrProduct);
SC_Product_Ex::setIncTaxToProduct($arrProduct, $arrTaxRules[$arrProduct['product_id']]);
}
}

/**
* 商品情報の配列に税込金額を設定する
*
* @param array $arrProduct 商品情報の配列
* @param array $arrProduct 商品情報の配列
* @param array $rules 商品規格IDを添字とした商品規格別の税率
* @return void
*/
public static function setIncTaxToProduct(&$arrProduct)
public static function setIncTaxToProduct(&$arrProduct, $rules = [])
{
$arrProduct['price01_min_inctax'] = isset($arrProduct['price01_min']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price01_min'], $arrProduct['product_id']) : null;
$arrProduct['price01_max_inctax'] = isset($arrProduct['price01_max']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price01_max'], $arrProduct['product_id']) : null;
$arrProduct['price02_min_inctax'] = isset($arrProduct['price02_min']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price02_min'], $arrProduct['product_id']) : null;
$arrProduct['price02_max_inctax'] = isset($arrProduct['price02_max']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price02_max'], $arrProduct['product_id']) : null;
// XXX 同一商品で規格ごとに税率も価格も異なり、税抜と税込で最小値、最大値の規格が変わるというケースは正確な処理ができない.
// このようなケースは希だと思われるため、実運用で発生する場合は以下のパッチを推奨
// https://github.com/EC-CUBE/eccube-2_13/issues/99#issuecomment-518100061
$arrProduct['price01_min_inctax'] = isset($arrProduct['price01_min']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price01_min'], $arrProduct['product_id'], SC_Product_Ex::findProductClassIdByRule('price01', $rules, 'min')) : null;
$arrProduct['price01_max_inctax'] = isset($arrProduct['price01_max']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price01_max'], $arrProduct['product_id'], SC_Product_Ex::findProductClassIdByRule('price01', $rules, 'max')) : null;
$arrProduct['price02_min_inctax'] = isset($arrProduct['price02_min']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price02_min'], $arrProduct['product_id'], SC_Product_Ex::findProductClassIdByRule('price02', $rules, 'min')) : null;
$arrProduct['price02_max_inctax'] = isset($arrProduct['price02_max']) ? SC_Helper_TaxRule_Ex::sfCalcIncTax($arrProduct['price02_max'], $arrProduct['product_id'], SC_Product_Ex::findProductClassIdByRule('price02', $rules, 'max')) : null;
}

/**
Expand Down Expand Up @@ -740,4 +752,104 @@ public function isValidProductId($product_id, $include_hidden = false, $include_
}
return false;
}

/**
* 商品規格別の税率を取得する.
*
* 商品規格別税率の設定の無い商品は基本税率を適用する
*
* @param array $product_ids 取得対象の商品ID
* @param int $option_product_tax_rule 商品別税率オプション
* @return array 税率を含む商品ID, 商品規格IDごとの配列. $option_product_tax_rule が 0 の場合は空の配列を返す
*/
protected static function getProductsClassRelateTaxRule(array $product_ids, $option_product_tax_rule = OPTION_PRODUCT_TAX_RULE)
{
if ($option_product_tax_rule == 0) {
return [];
}

$arrDefaultTaxRule = SC_Helper_TaxRule_Ex::getTaxRule();
$objQuery = SC_Query_Ex::getSingletonInstance();
$objQuery->setOrder('product_class_id');
$arrProductClasses = $objQuery->select(
'*,
COALESCE(
(SELECT tax_rate FROM dtb_tax_rule
WHERE product_class_id = dtb_products_class.product_class_id
AND product_id = dtb_products_class.product_id
AND del_flg = 0 AND apply_date < CURRENT_TIMESTAMP
ORDER BY apply_date DESC LIMIT 1
)
, '.$arrDefaultTaxRule['tax_rate'].') as tax_rate',
'dtb_products_class',
'product_id IN ('.SC_Utils_Ex::repeatStrWithSeparator('?', count($product_ids)).') AND del_flg = 0',
$product_ids
);
$arrTaxRules = [];
if (is_array($arrProductClasses)) {
foreach ($arrProductClasses as $arrProductClass) {
if (!array_key_exists($arrProductClass['product_id'], $arrTaxRules)) {
$arrTaxRules[$arrProductClass['product_id']] = [];
}
$arrTaxRules[$arrProductClass['product_id']][$arrProductClass['product_class_id']] = $arrProductClass;
}
}
return $arrTaxRules;
}

/**
* getProductsClassRelateTaxRule の結果から最大値または最小値の金額の product_class_id を取得する.
*
* @param string $col 比較対象のカラム
* @param array|null $rules 商品規格IDを添字とした商品規格別の税率
* @param string $operator max or min
* @return int product_class_id
*/
protected static function findProductClassIdByRule($col, $rules, $operator = 'max')
{
if (empty($rules)) {
return 0;
}

// 価格が null の商品規格は除外
$rules = array_filter($rules, function ($rule) use ($col) {
return $rule[$col] !== null;
});
return array_reduce($rules, function ($carry, $rule) use ($col, $rules, $operator) {
if (SC_Product_Ex::checkPriceAndTaxRate($rules[$carry][$col], $rule[$col], $rules[$carry]['tax_rate'], $rule['tax_rate'], $operator)) {
return $rule['product_class_id'];
}

return $carry;
}, empty($rules) ? 0 : $rules[min(array_keys($rules))]['product_class_id']);
}

/**
* 税込金額の max or min を評価する.
*
* @param int $carry_price 現在の金額
* @param int $price 比較対象の金額
* @param int $carry_rate 現在の税率
* @param int $rate 比較対象の税率
* @param string $operator max or min
* @return bool
*/
protected static function checkPriceAndTaxRate($carry_price, $price, $carry_rate, $rate, $operator = 'max')
{
if ($price === null) {
return false;
}

$carry_intax = $carry_price + ($carry_price * ($carry_rate / 100));
$intax = $price + ($price * ($rate / 100));
// 比較するのみなので端数処理は考慮しない
switch ($operator) {
case 'min':
return ($carry_intax >= $intax);
break;
case 'max':
default:
return ($carry_intax <= $intax);
}
}
}
5 changes: 5 additions & 0 deletions tests/class/SC_Product/SC_Product_TestBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
*/
class SC_Product_TestBase extends Common_TestCase
{
/**
* @var SC_Product_Ex
*/
protected $objProducts;

protected function setUp()
{
parent::setUp();
Expand Down
191 changes: 191 additions & 0 deletions tests/class/SC_Product/SC_Product_setIncTaxToProductTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

class SC_Product_setIncTaxToProductTest extends SC_Product_TestBase
{
protected function setUp()
{
parent::setUp();
// $this->setUpProductClass();
// $this->objProducts = new SC_Product_Ex();
}

public function checkPriceAndTaxRateProvider()
{
return [
// operator to max
[1, 2, 8, 10, 'max', true],
[1, 1, 8, 8, 'max', true],
[1, 0, 8, 8, 'max', false],
[1, 1, 8, 5, 'max', false],
// operator to min
[1, 2, 8, 10, 'min', false],
[1, 1, 8, 8, 'min', true],
[1, 0, 8, 8, 'min', true],
[1, 1, 8, 5, 'min', true],
// operator to unknown
[1, 2, 8, 10, 'n', true],
[1, 1, 8, 8, 'n', true],
[1, 0, 8, 8, 'n', false],
[1, 1, 8, 5, 'n', false],
// operator to empty
[1, 2, 8, 10, '', true],
[1, 1, 8, 8, '', true],
[1, 0, 8, 8, '', false],
[1, 1, 8, 5, '', false],
];
}

/**
* @dataProvider checkPriceAndTaxRateProvider
*/
public function testCheckPriceAndTaxRate($carry_price, $price, $carry_rate, $rate, $operator = 'max', $expected)
{
$this->expected = $expected;

$this->actual = $this->wrapperToCheckPriceAndTaxRate($carry_price, $price, $carry_rate, $rate, $operator);
$this->verify();
}

public function findProductClassIdByRuleProvider()
{
return [
['price01',
[
1 => ['price01' => 100, 'tax_rate' => 10, 'product_class_id' => 1],
2 => ['price01' => 103, 'tax_rate' => 8, 'product_class_id' => 2],
3 => ['price01' => 102, 'tax_rate' => 8, 'product_class_id' => 3],
],
'max', 2],
['price02',
[
1 => ['price02' => 100, 'tax_rate' => 10, 'product_class_id' => 1],
2 => ['price02' => 103, 'tax_rate' => 8, 'product_class_id' => 2],
3 => ['price02' => 102, 'tax_rate' => 8, 'product_class_id' => 3],
],
'min', 1],
['price01',
[
1 => ['price01' => null, 'tax_rate' => 10, 'product_class_id' => 1],
2 => ['price01' => 103, 'tax_rate' => 8, 'product_class_id' => 2],
3 => ['price01' => 102, 'tax_rate' => 8, 'product_class_id' => 3],
],
'min', 3],
['price01',
[
1 => ['price01' => null, 'tax_rate' => 10, 'product_class_id' => 1],
2 => ['price01' => 103, 'tax_rate' => 8, 'product_class_id' => 2],
3 => ['price01' => 102, 'tax_rate' => 8, 'product_class_id' => 3],
],
'max', 2],
['price01',
[
1 => ['price01' => null, 'tax_rate' => 10, 'product_class_id' => 1],
2 => ['price01' => null, 'tax_rate' => 8, 'product_class_id' => 2],
3 => ['price01' => null, 'tax_rate' => 8, 'product_class_id' => 3],
],
'max', 0],
['price01',
[
1 => ['price01' => null, 'tax_rate' => 10, 'product_class_id' => 1],
2 => ['price01' => null, 'tax_rate' => 8, 'product_class_id' => 2],
3 => ['price01' => null, 'tax_rate' => 8, 'product_class_id' => 3],
],
'min', 0],
// see https://github.com/EC-CUBE/eccube-2_13/pull/298#issuecomment-522072546
// ['price02',
// [
// 1 => ['price02' => 1000, 'tax_rate' => 10, 'product_class_id' => 1],
// 2 => ['price02' => 1010, 'tax_rate' => 8, 'product_class_id' => 2],
// ],
// 'min', 1],
];
}

/**
* @dataProvider findProductClassIdByRuleProvider
*/
public function testFindProductClassIdByRule($col, $rules, $type, $expected)
{
$this->expected = $expected;
$this->actual = $this->wrapperToFindProductClassIdByRule($col, $rules, $type);
$this->verify();
}

public function testGetProductsClassRelateTaxRule()
{
$_SESSION['member_id'] = '1';
$objGenerator = new FixtureGenerator($this->objQuery);
$product_ids = [];
for ($i = 0; $i < 3; $i++) {
$product_ids[] = $objGenerator->createProduct();
}

// 0 番目商品の規格に商品別税率を設定する
$now = new \DateTime();
$now->modify('-1 days');
$product_class_ids = $this->objQuery->getCol('product_class_id', 'dtb_products_class', 'product_id = ? AND del_flg = 0', [$product_ids[0]]);
foreach ($product_class_ids as $product_class_id) {
SC_Helper_TaxRule_Ex::setTaxRule(1, 10, $now->format('Y/m/d H:i:s'), null, 0, $product_ids[0], $product_class_id);
}

$arrTaxRules = $this->wrapperToGetProductsClassRelateTaxRule($product_ids, 1);
$this->actual = [];
foreach ($arrTaxRules as $product_id => $rules) {
foreach ($rules as $product_class_id => $rule) {
$this->actual[] = $rule['tax_rate'];
}
}

$this->expected = [10, 10, 10, 8, 8, 8, 8, 8, 8];
$this->verify();
}

/**
* @param array $product_ids 取得対象の商品ID
* @param int $option_product_tax_rule 商品別税率オプション
* @return array 税率を含む商品ID, 商品規格IDごとの配列. $option_product_tax_rule が 0 の場合は空の配列を返す
*/
private function wrapperToGetProductsClassRelateTaxRule(array $product_ids, $option_product_tax_rule = OPTION_PRODUCT_TAX_RULE)
{
$method = self::getMethod('getProductsClassRelateTaxRule');
return $method->invoke(null, $product_ids, $option_product_tax_rule);
}

/**
* @param string $col 比較対象のカラム
* @param array|null $rules 商品規格IDを添字とした商品規格別の税率
* @param string $operator max or min
* @return int product_class_id
*/
private function wrapperToFindProductClassIdByRule($col, $rules, $operator)
{
$method = self::getMethod('findProductClassIdByRule');
return $method->invoke(null, $col, $rules, $operator);
}

/**
* @param int $carry_price 現在の金額
* @param int $price 比較対象の金額
* @param int $carry_rate 現在の税率
* @param int $rate 比較対象の税率
* @param string $operator max or min
* @return bool
*/
private function wrapperToCheckPriceAndTaxRate($carry_price, $price, $carry_rate, $rate, $operator = 'max')
{
$method = self::getMethod('checkPriceAndTaxRate');
return $method->invoke(null, $carry_price, $price, $carry_rate, $rate, $operator);
}

/**
* @param string $name
* @return ReflectionMethod
*/
private static function getMethod($name)
{
$class = new \ReflectionClass('SC_Product_Ex');
$method = $class->getMethod($name);
$method->setAccessible(true);
return $method;
}
}