shish 10 ore în urmă
părinte
comite
f0f130cd97

+ 63 - 0
app-ghs/controllers/StatItemController.php

@@ -5,6 +5,7 @@ namespace ghs\controllers;
 use biz\stat\classes\StatItemClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderItemClass;
+use bizGhs\order\services\NextDayRefundService;
 use bizGhs\product\classes\ProductClass;
 use bizGhs\shop\classes\ShopAdminClass;
 use bizGhs\stat\classes\StatSaleClass;
@@ -78,6 +79,7 @@ class StatItemController extends BaseController
         $totalAmount = 0;
         $totalNum = 0;
         $totalMl = 0;
+        $relateOrderSns = [];
         foreach ($list as $order) {
             $orderSn = $order['orderSn'] ?? '';
             $status = $order['status'] ?? 0;
@@ -90,6 +92,9 @@ class StatItemController extends BaseController
                 //如果全部退款的花材也不统计
                 continue;
             }
+            if (!empty($orderSn)) {
+                $relateOrderSns[] = $orderSn;
+            }
             $itemList = OrderItemClass::getAllByCondition(['orderSn' => $orderSn], null, '*', null, true);
             if (!empty($itemList)) {
                 foreach ($itemList as $item) {
@@ -141,6 +146,64 @@ class StatItemController extends BaseController
                 }
             }
         }
+
+        // 隔天退货已计入 refundNum:先按原单加回,再按售后通过日扣,避免支付日/通过日重复或漏扣
+        $addBackMap = NextDayRefundService::sumItemByProductForRelateOrderSns($this->mainId, $relateOrderSns);
+        foreach ($addBackMap as $pid => $row) {
+            if (!empty($staffId)) {
+                $currentCgStaffId = $productInfo[$pid]['cgStaffId'] ?? 0;
+                if ($staffId != $currentCgStaffId) {
+                    continue;
+                }
+            }
+            if (!isset($stat[$pid])) {
+                $stat[$pid] = [
+                    'productId' => $pid,
+                    'name' => $productInfo[$pid]['name'] ?? '',
+                    'py' => $productInfo[$pid]['py'] ?? '',
+                    'num' => '0.00',
+                    'amount' => '0.00',
+                    'gross' => '0.00',
+                ];
+            }
+            $addNum = (string)($row['num'] ?? '0');
+            $addAmt = (string)($row['amount'] ?? '0');
+            $stat[$pid]['num'] = bcadd((string)$stat[$pid]['num'], $addNum, 2);
+            $stat[$pid]['amount'] = bcadd((string)$stat[$pid]['amount'], $addAmt, 2);
+            // 毛利按退款金额近似加回(与客户业绩扣金额口径一致)
+            $stat[$pid]['gross'] = bcadd((string)$stat[$pid]['gross'], $addAmt, 2);
+            $totalNum = bcadd((string)$totalNum, $addNum, 2);
+            $totalAmount = bcadd((string)$totalAmount, $addAmt, 2);
+            $totalMl = bcadd((string)$totalMl, $addAmt, 2);
+        }
+
+        // 通过日:退货扣数量+金额;仅退款只扣金额(按原单花材占比分摊)
+        $deductMaps = [
+            NextDayRefundService::sumItemByProduct($this->mainId, $currentStartTime, $currentEndTime),
+            NextDayRefundService::sumMoneyOnlyAmountByProduct($this->mainId, $currentStartTime, $currentEndTime),
+        ];
+        foreach ($deductMaps as $deductMap) {
+            foreach ($deductMap as $pid => $row) {
+                if (!empty($staffId)) {
+                    $currentCgStaffId = $productInfo[$pid]['cgStaffId'] ?? 0;
+                    if ($staffId != $currentCgStaffId) {
+                        continue;
+                    }
+                }
+                if (!isset($stat[$pid])) {
+                    continue;
+                }
+                $subNum = (string)($row['num'] ?? '0');
+                $subAmt = (string)($row['amount'] ?? '0');
+                $stat[$pid]['num'] = bcsub((string)$stat[$pid]['num'], $subNum, 2);
+                $stat[$pid]['amount'] = bcsub((string)$stat[$pid]['amount'], $subAmt, 2);
+                $stat[$pid]['gross'] = bcsub((string)$stat[$pid]['gross'], $subAmt, 2);
+                $totalNum = bcsub((string)$totalNum, $subNum, 2);
+                $totalAmount = bcsub((string)$totalAmount, $subAmt, 2);
+                $totalMl = bcsub((string)$totalMl, $subAmt, 2);
+            }
+        }
+
         //增加毛利率
         foreach ($stat as $key => $val) {
             $profit = $val['gross'] ?? 0;

+ 141 - 0
biz-ghs/order/services/NextDayRefundService.php

@@ -611,4 +611,145 @@ class NextDayRefundService
         }
         return $map;
     }
+
+    /**
+     * 关联原单上「已发生」的隔天退货(不限通过日):用于销量按 payTime 统计时加回 refundNum 中的隔天部分,
+     * 避免与按 passTime 扣减重复;口径对齐客户业绩(支付日先全额,通过日再扣)。
+     *
+     * @param int $mainId
+     * @param string[] $relateOrderSns 原销售单号列表
+     * @return array productId => [num, amount]
+     */
+    public static function sumItemByProductForRelateOrderSns($mainId, $relateOrderSns)
+    {
+        $relateOrderSns = array_values(array_unique(array_filter($relateOrderSns)));
+        if (empty($relateOrderSns)) {
+            return [];
+        }
+        $snParams = [];
+        foreach ($relateOrderSns as $i => $sn) {
+            $snParams[':sn' . $i] = $sn;
+        }
+        $in = implode(',', array_keys($snParams));
+        $sql = "SELECT i.productId, COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount
+            FROM xhRefundItem i
+            INNER JOIN xhRefund r ON r.orderSn = i.orderSn
+            WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
+              AND r.refundType=:rtype
+              AND r.relateOrderSn IN ($in)
+            GROUP BY i.productId";
+        $params = array_merge([
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
+        ], $snParams);
+        $rows = Yii::$app->db->createCommand($sql, $params)->queryAll();
+        $map = [];
+        foreach ($rows as $row) {
+            $map[$row['productId']] = [
+                'num' => bcadd($row['num'] ?? '0', '0', 2),
+                'amount' => bcadd($row['amount'] ?? '0', '0', 2),
+            ];
+        }
+        return $map;
+    }
+
+    /**
+     * 隔天「仅退款」按通过日汇总金额(无花材明细,进收支分类「销售仅退款」等)
+     */
+    public static function sumMoneyOnlyAmountByMainAndTime($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhRefund
+            WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay
+              AND refundType=:rtype
+              AND IFNULL(NULLIF(passTime,'0000-00-00 00:00:00'), addTime) BETWEEN :start AND :end";
+        $row = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryOne();
+        return bcadd($row['total'] ?? '0', '0', 2);
+    }
+
+    /**
+     * 隔天「仅退款」按通过日分摊到原单花材(无明细时按原单行金额占比分摊 refundPrice)
+     * 花材销量页用:通过日扣金额,数量不扣。
+     *
+     * @return array productId => [num=>0, amount=>...]
+     */
+    public static function sumMoneyOnlyAmountByProduct($mainId, $startTime, $endTime)
+    {
+        $sql = "SELECT r.id, r.refundPrice, r.relateOrderSn
+            FROM xhRefund r
+            WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
+              AND r.refundType=:rtype
+              AND IFNULL(NULLIF(r.relateOrderSn,''), '') <> ''
+              AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end";
+        $rows = Yii::$app->db->createCommand($sql, [
+            ':mainId' => $mainId,
+            ':status' => RefundOrderClass::STATUS_COMPLETE,
+            ':sameDay' => self::SAME_DAY_NO,
+            ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY,
+            ':start' => $startTime,
+            ':end' => $endTime,
+        ])->queryAll();
+        if (empty($rows)) {
+            return [];
+        }
+        $map = [];
+        foreach ($rows as $row) {
+            $refundPrice = bcadd((string)($row['refundPrice'] ?? '0'), '0', 2);
+            if (bccomp($refundPrice, '0', 2) <= 0) {
+                continue;
+            }
+            $relateSn = $row['relateOrderSn'] ?? '';
+            $items = \bizGhs\order\classes\OrderItemClass::getAllByCondition(
+                ['orderSn' => $relateSn],
+                null,
+                'productId,xhNum,xhUnitPrice',
+                null,
+                true
+            );
+            if (empty($items)) {
+                continue;
+            }
+            $weights = [];
+            $weightSum = '0.00';
+            foreach ($items as $item) {
+                $pid = intval($item->productId ?? 0);
+                if ($pid <= 0) {
+                    continue;
+                }
+                $line = bcmul((string)($item->xhNum ?? '0'), (string)($item->xhUnitPrice ?? '0'), 2);
+                if (bccomp($line, '0', 2) <= 0) {
+                    continue;
+                }
+                $weights[$pid] = bcadd($weights[$pid] ?? '0', $line, 2);
+                $weightSum = bcadd($weightSum, $line, 2);
+            }
+            if (bccomp($weightSum, '0', 2) <= 0) {
+                continue;
+            }
+            $allocated = '0.00';
+            $pids = array_keys($weights);
+            $last = count($pids) - 1;
+            foreach ($pids as $idx => $pid) {
+                if ($idx === $last) {
+                    $part = bcsub($refundPrice, $allocated, 2);
+                } else {
+                    $part = bcdiv(bcmul($refundPrice, $weights[$pid], 4), $weightSum, 2);
+                    $allocated = bcadd($allocated, $part, 2);
+                }
+                if (!isset($map[$pid])) {
+                    $map[$pid] = ['num' => '0.00', 'amount' => '0.00'];
+                }
+                $map[$pid]['amount'] = bcadd($map[$pid]['amount'], $part, 2);
+            }
+        }
+        return $map;
+    }
 }

+ 69 - 0
biz-ghs/stat/classes/StatSaleClass.php

@@ -962,6 +962,63 @@ class StatSaleClass extends BaseClass
             }
         }
 
+        // 隔天售后:与花材销量同口径——加回原单 refundNum 中的隔天退货,再按通过日扣分类收入;仅退款进「销售仅退款」
+        $classRelateSns = [];
+        if (!empty($ghsOrderList)) {
+            foreach ($ghsOrderList as $ghsOrder) {
+                $book = $ghsOrder['book'] ?? 0;
+                $status = $ghsOrder['status'] ?? 0;
+                if ($book == 1 && $status == 2) {
+                    continue;
+                }
+                if ($status == 0 || $status == 5) {
+                    continue;
+                }
+                if (($ghsOrder['actPrice'] ?? 0) <= 0) {
+                    continue;
+                }
+                if (!empty($ghsOrder['orderSn'])) {
+                    $classRelateSns[] = $ghsOrder['orderSn'];
+                }
+            }
+        }
+        $productClassMap = ProductClass::getAllByCondition(['mainId' => $mainId], null, 'id,classId', 'id');
+        $applyClassDelta = function ($pidMap, $sign) use (&$arr, &$classList, $productClassMap) {
+            foreach ($pidMap as $pid => $row) {
+                $classId = $productClassMap[$pid]['classId'] ?? 0;
+                $deltaNum = bcmul((string)($row['num'] ?? '0'), (string)$sign, 2);
+                $deltaAmt = bcmul((string)($row['amount'] ?? '0'), (string)$sign, 2);
+                if (!isset($arr[$classId])) {
+                    if (bccomp($deltaAmt, '0', 2) == 0 && bccomp($deltaNum, '0', 2) == 0) {
+                        continue;
+                    }
+                    $arr[$classId] = [
+                        'className' => $classList[$classId]['name'] ?? '已删除',
+                        'classId' => $classId,
+                        'expend' => 0,
+                        'income' => '0.00',
+                        'saleNum' => '0.00',
+                        'cgNum' => 0,
+                    ];
+                }
+                $arr[$classId]['income'] = bcadd((string)($arr[$classId]['income'] ?? 0), $deltaAmt, 2);
+                $arr[$classId]['saleNum'] = bcadd((string)($arr[$classId]['saleNum'] ?? 0), $deltaNum, 2);
+            }
+        };
+        $applyClassDelta(
+            NextDayRefundService::sumItemByProductForRelateOrderSns($mainId, $classRelateSns),
+            1
+        );
+        $applyClassDelta(
+            NextDayRefundService::sumItemByProduct($mainId, $currentStartTime, $currentEndTime),
+            -1
+        );
+        $onlyRefund = bcadd(
+            (string)$onlyRefund,
+            NextDayRefundService::sumMoneyOnlyAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime),
+            2
+        );
+
         //零售开单
         $lsIncome = 0;
         $hdWhere = ['mainId' => $mainId, 'status' => ['in', [HdOrderClass::ORDER_STATUS_UN_SEND, HdOrderClass::ORDER_STATUS_SENDING, HdOrderClass::ORDER_STATUS_COMPLETE]]];
@@ -1496,6 +1553,18 @@ class StatSaleClass extends BaseClass
                     ];
                 }
             }
+            // 隔天售后按客户扣金额后归到片区(笔数不减;无当日开单的片区不新建行)
+            $fwdMap = NextDayRefundService::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
+            foreach ($fwdMap as $cid => $amt) {
+                $distId = $customList[$cid]['distId'] ?? 0;
+                if (!isset($arr[$distId])) {
+                    continue;
+                }
+                $arr[$distId]['amount'] = bcsub((string)$arr[$distId]['amount'], (string)$amt, 2);
+                $arr[$distId]['profit'] = bcsub((string)$arr[$distId]['profit'], (string)$amt, 2);
+                $totalAmount = bcsub((string)$totalAmount, (string)$amt, 2);
+                $totalProfit = bcsub((string)$totalProfit, (string)$amt, 2);
+            }
             $arr = arrayUtil::arraySort($arr, 'profit');
         }
         $totalAmount = floatval($totalAmount);