cqp hai 2 meses
pai
achega
f23850c022

+ 1 - 3
app/Model/AuxiliaryAccountDetails.php

@@ -15,12 +15,10 @@ class AuxiliaryAccountDetails extends DataScopeBaseModel
     const TYPE_ONE = 1;
     const TYPE_TWO = 2;
     const TYPE_THREE = 3;
-    const TYPE_FOUR = 4;
     const Type = [
         self::TYPE_ONE => '人员人工费用',
         self::TYPE_TWO => '设备折旧费用',
-        self::TYPE_THREE => '报销',
-        self::TYPE_FOUR => '费用',
+        self::TYPE_THREE => '费用',
     ];
 
 }

+ 13 - 0
app/Model/ExpenseClaimsDetails.php

@@ -14,4 +14,17 @@ class ExpenseClaimsDetails extends DataScopeBaseModel
     public static $field = ['id','item_id','employee_id','fee_id',"amount","amount","remark","remark","claim_date","entrust_type","expense_type","expense_attachments"];
 
     const Order_type = "expense_claims_details";
+
+    const TYPE_ZERO = 0;
+    const TYPE_ONE = 1;
+    const TYPE_TWO = 2;
+    const State_Type = [
+        self::TYPE_ZERO => '无',
+        self::TYPE_ONE => '境内委托',
+        self::TYPE_TWO => '境外委托',
+    ];
+    const State_Type_2 = [
+        self::TYPE_ZERO => '否',
+        self::TYPE_ONE => '是',
+    ];
 }

+ 100 - 5
app/Service/AuxiliaryAccountService.php

@@ -212,7 +212,7 @@ class AuxiliaryAccountService extends Service
             ]);
             $model->month = $data['month'];
             $model->crt_id = $user['id'];
-            $model->top_depart_id = $user['top_depart_id'];
+            $model->top_depart_id = $data['top_depart_id'];
             $model->save();
             $this->saveDetail($model->id, time(), $data,$user['id'],$user);
 
@@ -255,6 +255,7 @@ class AuxiliaryAccountService extends Service
     }
 
     private function auxiliaryAccountRule(&$data, $user, $is_add = true){
+        $data['top_depart_id'] = $user['top_depart_id'];
         if (empty($data['month'])) return [false, '月份不能为空'];
         $data['month'] = $this->changeDateToDate($data['month']);
         $monthStart = $data['month'];
@@ -264,16 +265,33 @@ class AuxiliaryAccountService extends Service
         // 初始化各类型的计数器
         $typeCounters = [];
 
+        $fee_map = Fee::where('top_depart_id', $data['top_depart_id'])
+            ->whereIn('id', array_unique(array_column($data['details'], 'fee_id')))
+            ->pluck('title', 'id')
+            ->all();
         foreach ($data['details'] as $item) {
             if(empty($item['type']) || ! isset(AuxiliaryAccountDetails::Type[$item['type']])) return [false, 'type类型不能为空或错误'];
             $type = $item['type'];
             $tabName = AuxiliaryAccountDetails::Type[$item['type']];
 
             // 针对当前 type 的行数进行累加
-            if (!isset($typeCounters[$type])) {
-                $typeCounters[$type] = 1;
-            } else {
-                $typeCounters[$type]++;
+            if($type == AuxiliaryAccountDetails::TYPE_ONE || $type == AuxiliaryAccountDetails::TYPE_TWO){
+                if (!isset($typeCounters[$type])) {
+                    $typeCounters[$type] = 1;
+                } else {
+                    $typeCounters[$type]++;
+                }
+            }else{
+                if(empty($item['fee_id'])) return [false, '费用类型id不能为空'];
+                $fee_t = $fee_map[$item['fee_id']] ?? "";
+                if(empty($fee_t)) return [false, '费用类型不存在'];
+                $tabName = $fee_t;
+                $new_key = $type . $item['fee_id'];
+                if (!isset($typeCounters[$new_key])) {
+                    $typeCounters[$new_key] = 1;
+                } else {
+                    $typeCounters[$new_key]++;
+                }
             }
 
             $errorPrefix = "【{$tabName}】第 " . $typeCounters[$type] . " 行:";
@@ -409,6 +427,83 @@ class AuxiliaryAccountService extends Service
             ->toIso8601ZuluString();
     }
 
+    public function fillDataForExport($data, $column, &$return)
+    {
+        if (empty($data)) return;
+
+        $mainIds = array_column($data, 'id');
+        // 获取详情映射 [expense_claims_id => [details...]]
+        $detailsMap = $this->getDetailsMap($mainIds);
+
+        // 默认空行模板
+        $defaultRow = array_fill_keys($column, '');
 
+        foreach ($data as $main) {
+            $mainId = $main['id'];
+            $details = $detailsMap[$mainId] ?? [];
 
+            // 提取主表信息
+            $mainInfo = [
+                'code'  => $main['code'],
+                'month' => $main['month'] ? date('Y-m', $main['month']) : '',
+            ];
+
+            if (empty($details)) {
+                // 如果没有详情,至少导出一行主表信息
+                $return[] = array_merge($defaultRow, $mainInfo);
+            } else {
+                // 遍历明细,每一行明细都带上主表的 code 和 month
+                foreach ($details as $sub) {
+                    // 合并主表字段 + 详情字段
+                    $fullRow = array_merge($mainInfo, $sub);
+                    // 仅保留 column 配置中要求的列
+                    $return[] = array_merge($defaultRow, array_intersect_key($fullRow, $defaultRow));
+                }
+            }
+        }
+    }
+
+    public function getDetailsMap($main_ids)
+    {
+        // 1. 获取详情
+        $details = AuxiliaryAccountDetails::where('del_time', 0)
+            ->whereIn('auxiliary_account_id', $main_ids)
+            ->get();
+
+        if ($details->isEmpty()) return [];
+
+        // 2. 批量提取所有关联 ID 用于预加载
+        $feeIds = $details->pluck('fee_id')->unique();
+
+        // 3. 建立档案映射 Map
+        $feeMap = Fee::whereIn('id', $feeIds)->get()->keyBy('id');
+
+        $res = [];
+        foreach ($details as $item) {
+            $tmpFee = $feeMap[$item->fee_id] ?? null;
+            $t = $tmpFee ? $tmpFee->title : '';
+
+            if($item->type == AuxiliaryAccountDetails::TYPE_ONE || $item->type == AuxiliaryAccountDetails::TYPE_TWO){
+                $type_title = AuxiliaryAccountDetails::Type[$item->type] ?? "";
+            }else{
+                $type_title = $t;
+            }
+            // 4. 组装明细行字段 (key 要与模板配置中的 export 对应)
+            $res[$item->auxiliary_account_id][] = [
+                'type_title' => $type_title,
+                'voucher_date' => $item->voucher_date ? date('Y-m-d', $item->voucher_date) : '',
+                'voucher_type' => $item->voucher_type,
+                'voucher_no' => $item->voucher_no,
+                'voucher_remark' => $item->voucher_remark,
+                'voucher_amount' => $item->voucher_amount,
+                'aggregation_amount' => $item->aggregation_amount,
+                'total_amount' => $item->total_amount,
+                'entrust1_amount' => $item->entrust1_amount,
+                'entrust2_amount' => $item->entrust2_amount,
+                'remark' => $item->remark,
+                'fee_title' => $t,
+            ];
+        }
+        return $res;
+    }
 }

+ 83 - 3
app/Service/ExpenseClaimsService.php

@@ -20,7 +20,7 @@ class ExpenseClaimsService extends Service
         return [true, $list];
     }
 
-    private function expenseClaimsSetCommon($data,$user, $field = []){
+    public function expenseClaimsSetCommon($data,$user, $field = []){
         if(empty($field)) $field = ExpenseClaims::$field;
 
         $model = ExpenseClaims::Clear($user,$data);
@@ -135,12 +135,12 @@ class ExpenseClaimsService extends Service
             $res = $this->checkNumber($item['amount'], 2, 'non-negative');
             if (! $res['valid']) return [false,  "第" . ($index + 1) . "行费用金额" . $res['error']];
 
-            if (!isset($item['claim_date'])) return [false, "第" . ($index + 1) . "行项报销日期缺失"];
+            if (!isset($item['claim_date'])) return [false, "第" . ($index + 1) . "行项费用产生日期缺失"];
             // 将报销日期转换为时间戳
             $claimTime = strtotime($item['claim_date']);
             // 判断:claim_date 必须早于 month
             // 如果 claim_date 是 2026-02-28,而 month 是 2026-03-01,则校验通过
-            if ($claimTime < $monthStart || $claimTime > $monthEnd) return [false, "第" . ($index + 1) . "行项报销日期必须在当前月份内(" . date("Y-m", $data['month']) . ")"];
+            if ($claimTime < $monthStart || $claimTime > $monthEnd) return [false, "第" . ($index + 1) . "行项费用产生日期必须在当前月份内(" . date("Y-m", $data['month']) . ")"];
         }
 
         $query = ExpenseClaims::where('top_depart_id', $data['top_depart_id'])
@@ -271,4 +271,84 @@ class ExpenseClaimsService extends Service
         return  Carbon::createFromTimestamp($time, 'UTC')
             ->toIso8601ZuluString();
     }
+
+    public function fillDataForExport($data, $column, &$return)
+    {
+        if (empty($data)) return;
+
+        $mainIds = array_column($data, 'id');
+        // 获取详情映射 [expense_claims_id => [details...]]
+        $detailsMap = $this->getDetailsMap($mainIds);
+
+        // 默认空行模板
+        $defaultRow = array_fill_keys($column, '');
+
+        foreach ($data as $main) {
+            $mainId = $main['id'];
+            $details = $detailsMap[$mainId] ?? [];
+
+            // 提取主表信息
+            $mainInfo = [
+                'code'  => $main['code'],
+                'month' => $main['month'] ? date('Y-m', $main['month']) : '',
+            ];
+
+            if (empty($details)) {
+                // 如果没有详情,至少导出一行主表信息
+                $return[] = array_merge($defaultRow, $mainInfo);
+            } else {
+                // 遍历明细,每一行明细都带上主表的 code 和 month
+                foreach ($details as $sub) {
+                    // 合并主表字段 + 详情字段
+                    $fullRow = array_merge($mainInfo, $sub);
+                    // 仅保留 column 配置中要求的列
+                    $return[] = array_merge($defaultRow, array_intersect_key($fullRow, $defaultRow));
+                }
+            }
+        }
+    }
+
+    public function getDetailsMap($main_ids)
+    {
+        // 1. 获取详情
+        $details = ExpenseClaimsDetails::where('del_time', 0)
+            ->whereIn('expense_claims_id', $main_ids)
+            ->get();
+
+        if ($details->isEmpty()) return [];
+
+        // 2. 批量提取所有关联 ID 用于预加载
+        $empIds = $details->pluck('employee_id')->filter()->unique();
+        $itemIds = $details->pluck('item_id')->unique();
+        $feeIds = $details->pluck('fee_id')->unique();
+
+        // 3. 建立档案映射 Map
+        $empMap = Employee::whereIn('id', $empIds)->get()->keyBy('id');
+        $itemMap = Item::whereIn('id', $itemIds)->get()->keyBy('id');
+        $feeMap = Fee::whereIn('id', $feeIds)->get()->keyBy('id');
+
+        $res = [];
+        foreach ($details as $item) {
+            $tmpEmp = $empMap[$item->employee_id] ?? null;
+            $tmpItem = $itemMap[$item->item_id] ?? null;
+            $tmpFee = $feeMap[$item->fee_id] ?? null;
+
+            // 4. 组装明细行字段 (key 要与模板配置中的 export 对应)
+            $res[$item->expense_claims_id][] = [
+                'employee_number'     => $tmpEmp ? $tmpEmp->number : '',
+                'employee_title'      => $tmpEmp ? $tmpEmp->title : '',
+                'item_code'           => $tmpItem ? $tmpItem->code : '',
+                'item_title'          => $tmpItem ? $tmpItem->title : '',
+                'fee_code'            => $tmpFee ? $tmpFee->code : '',
+                'fee_title'           => $tmpFee ? $tmpFee->title : '',
+                'amount'              => $item->amount,
+                'claim_date'          => $item->claim_date ? date('Y-m-d', $item->claim_date) : '',
+                'voucher_no'          => $item->voucher_no,
+                'remark'              => $item->remark,
+                'entrust_type_title'  => ExpenseClaimsDetails::State_Type[$item->entrust_type] ?? '',
+                'expense_type_title'  => ExpenseClaimsDetails::State_Type_2[$item->expense_type] ?? '',
+            ];
+        }
+        return $res;
+    }
 }

+ 39 - 0
app/Service/ExportFileService.php

@@ -362,6 +362,45 @@ class ExportFileService extends Service
         return [true, $this->saveExportData($return,$header)];
     }
 
+    public function feeOrder($ergs, $user)
+    {
+        // 导出配置
+        $return = [];
+        $header_default = $user['e_header_default']; // 假设前端已下发报销单的模板配置
+        $column = array_column($header_default, 'export');
+        $header = array_column($header_default, 'value');
+
+        $service = new ExpenseClaimsService();
+        // 使用你提到的 common 方法获取 Query Builder
+        $model = $service->expenseClaimsSetCommon($ergs, $user);
+
+        $model->chunk(500, function ($data) use (&$return, $service, $column) {
+            // 直接处理这一批主表数据,将其与详情合并平铺
+            $service->fillDataForExport($data->toArray(), $column, $return);
+        });
+
+        return [true, $this->saveExportData($return, $header)];
+    }
+
+    public function RDOrder($ergs, $user)
+    {
+        // 导出配置
+        $return = [];
+        $header_default = $user['e_header_default'];
+        $column = array_column($header_default, 'export');
+        $header = array_column($header_default, 'value');
+
+        $service = new AuxiliaryAccountService();
+        $model = $service->setCommon($ergs, $user);
+
+        $model->chunk(500, function ($data) use (&$return, $service, $column) {
+            // 直接处理这一批主表数据,将其与详情合并平铺
+            $service->fillDataForExport($data->toArray(), $column, $return);
+        });
+
+        return [true, $this->saveExportData($return, $header)];
+    }
+
     // 项目工资统计表
     public function exportEmployeeSalary($data, $user)
     {

+ 245 - 251
app/Service/ImportService.php

@@ -42,6 +42,8 @@ class ImportService extends Service
         'dailyDwOrder', // 设备日工时单
         'leaveOrder', // 请假单
         'overtimeOrder', // 加班单
+        'feeOrder', // 项目费用报销单
+        'RDOrder', // 研发支出辅助帐
     ];
 
     public function getTableTitleXls($data,$user){
@@ -1518,134 +1520,6 @@ class ImportService extends Service
         return [!empty($errors) ? implode('|', $errors) : "", $update_map, $dbEmps];
     }
 
-    private function monthPwOrderCheck1(&$array, $user, $table_config)
-    {
-        $keys = array_column($table_config, 'key');
-        $codeIdx = array_search('code', $keys);
-        $monthIdx = array_search('month', $keys);
-        $empIdx = array_search('employee_id', $keys);
-
-        $numIdx = array_search('total_days', $keys);
-        $num2Idx = array_search('rd_total_days', $keys);
-        $num3Idx = array_search('total_hours', $keys);
-        $num4Idx = array_search('rd_total_hours', $keys);
-
-        // 1. 预加载基础数据
-        $allEmpNumbers = array_filter(array_unique(array_column($array, $empIdx)));
-        $dbEmps = Employee::where('del_time', 0)
-            ->whereIn('number', $allEmpNumbers)
-            ->where('top_depart_id', $user['top_depart_id'])
-            ->pluck('id', 'number')->toArray();
-
-        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
-        $dbOrders = DB::table('monthly_pw_order')
-            ->whereIn('code', $allCodes)
-            ->where('top_depart_id', $user['top_depart_id'])
-            ->where('del_time', 0)
-            ->get()->keyBy('code');
-
-        $errors = [];
-        $uniqueMonths = [];
-
-        // --- 步骤 1:月份预处理 ---
-        foreach ($array as $rowIndex => $row) {
-            $valMonthRaw = trim($row[$monthIdx] ?? '');
-            if ($valMonthRaw === '') continue;
-
-            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
-            if (!$mStatus) {
-                $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误({$valMonthRaw})";
-                continue;
-            }
-
-            $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
-            $array[$rowIndex][$monthIdx] = $valMonthTs;
-            $uniqueMonths[] = $valMonthTs;
-        }
-
-        // 查重数据库中已有的月份
-        $existingMonthsMap = [];
-        if (!empty($uniqueMonths)) {
-            $existingMonths = DB::table('monthly_pw_order')
-                ->where('top_depart_id', $user['top_depart_id'])
-                ->where('del_time', 0)
-                ->whereIn('month', array_unique($uniqueMonths))
-                ->pluck('month')
-                ->toArray();
-            $existingMonthsMap = array_fill_keys($existingMonths, true);
-        }
-
-        $excelAggregator = [];
-        $update_map = [];
-        $monthToKeyMap = []; // 校验同一月份是否指向了多个 AggKey
-
-        // --- 步骤 2:全量业务检查 ---
-        foreach ($array as $rowIndex => $row) {
-            $displayLine = $rowIndex + 1;
-
-            $valCode = trim($row[$codeIdx] ?? '');
-            $valMonthTs = $row[$monthIdx] ?? '';
-            $valEmp = trim($row[$empIdx] ?? '');
-
-            if (!is_numeric($valMonthTs)) continue;
-
-            // 确定单据组标识
-            $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
-
-            // B. 校验单据存在性 & 匹配主表 ID
-            if ($valCode) {
-                if (isset($dbOrders[$valCode])) {
-                    $dbOrder = $dbOrders[$valCode];
-                    if ($dbOrder->month != $valMonthTs) {
-                        $errors[] = "第{$displayLine}行:单据[{$valCode}]月份应为[" . date('Y-m', $dbOrder->month) . "],与导入不符";
-                    }
-                    $update_map[$rowIndex] = $dbOrder->id;
-                }
-                // 注意:如果填了 Code 但数据库没搜到,不进 update_map,会被下方视为“新增”
-            }
-
-            // C. 【核心修正】月份唯一性拦截
-            // 如果该行被判定为“新增”,但数据库里该月已经有单据了,直接报错
-            if (!isset($update_map[$rowIndex])) {
-                if (isset($existingMonthsMap[$valMonthTs])) {
-                    $errors[] = "第{$displayLine}行:月份[" . date('Y-m', $valMonthTs) . "]已存在单据,请填写编码编辑";
-                }
-            }
-
-            // D. Excel 内部逻辑一致性
-            // 确保同一个月份在同一个 Excel 里不会既有“单据 A”又有“单据 B”或者“新增单据”
-            if (!isset($monthToKeyMap[$valMonthTs])) {
-                $monthToKeyMap[$valMonthTs] = $aggKey;
-            } elseif ($monthToKeyMap[$valMonthTs] !== $aggKey) {
-                $errors[] = "第{$displayLine}行:同月份[" . date('Y-m', $valMonthTs) . "]在 Excel 中存在多组单据冲突";
-            }
-
-            // E. 人员与单据内唯一性
-            if (!isset($dbEmps[$valEmp])) {
-                $errors[] = "第{$displayLine}行:工号[{$valEmp}]不存在";
-            } else {
-                if (isset($excelAggregator[$aggKey]['emps'][$valEmp])) {
-                    $errors[] = "第{$displayLine}行:人员[{$valEmp}]在同单据内重复";
-                }
-                $excelAggregator[$aggKey]['emps'][$valEmp] = true;
-            }
-
-            // G. 数字有效性校验
-            $numFields = [
-                $numIdx => '出勤天数', $num2Idx => '研发天数',
-                $num3Idx => '出勤工时', $num4Idx => '研发工时'
-            ];
-            foreach ($numFields as $idx => $label) {
-                $val = $row[$idx] ?? 0;
-                $res = $this->checkNumber($val, 2, 'non-negative');
-                if (!$res['valid']) $errors[] = "第{$displayLine}行{$label}:" . $res['error'];
-            }
-        }
-
-        $error_string = !empty($errors) ? implode('|', $errors) : "";
-        return [$error_string, $update_map, $dbEmps];
-    }
-
     // 设备月度工时单
     public function monthDwOrderImport($array, $user, $other_param)
     {
@@ -1907,129 +1781,6 @@ class ImportService extends Service
         return [$error_string, $update_map, $dbDevs];
     }
 
-    private function monthDwOrderCheck1(&$array, $user, $table_config)
-    {
-        $keys = array_column($table_config, 'key');
-        $codeIdx = array_search('code', $keys);
-        $monthIdx = array_search('month', $keys);
-        $devIdx = array_search('device_id', $keys);
-
-        $numIdx = array_search('total_days', $keys);
-        $num2Idx = array_search('rd_total_days', $keys);
-        $num3Idx = array_search('total_hours', $keys);
-        $num4Idx = array_search('rd_total_hours', $keys);
-
-        // 1. 基础数据预加载
-        $allDevCodes = array_filter(array_unique(array_column($array, $devIdx)));
-        $dbDevs = Device::where('del_time', 0)
-            ->whereIn('code', $allDevCodes)
-            ->where('top_depart_id', $user['top_depart_id'])
-            ->pluck('id', 'code')->toArray();
-
-        $allCodes = array_filter(array_unique(array_column($array, $codeIdx)));
-        $dbOrders = DB::table('monthly_dw_order')
-            ->whereIn('code', $allCodes)
-            ->where('top_depart_id', $user['top_depart_id'])
-            ->where('del_time', 0)
-            ->get()->keyBy('code');
-
-        $errors = [];
-        $uniqueMonths = [];
-
-        // --- 步骤 1:预处理月份时间戳 ---
-        foreach ($array as $rowIndex => $row) {
-            $valMonthRaw = trim($row[$monthIdx] ?? '');
-            if ($valMonthRaw === '') continue;
-
-            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
-            if (!$mStatus) {
-                $errors[] = "第" . ($rowIndex + 1) . "行:月份格式错误({$valMonthRaw})";
-                continue;
-            }
-            $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
-            $array[$rowIndex][$monthIdx] = $valMonthTs;
-            $uniqueMonths[] = $valMonthTs;
-        }
-
-        // 查询数据库中已存在的月份单据
-        $existingMonthsMap = [];
-        if (!empty($uniqueMonths)) {
-            $existingMonths = DB::table('monthly_dw_order')
-                ->where('top_depart_id', $user['top_depart_id'])
-                ->where('del_time', 0)
-                ->whereIn('month', array_unique($uniqueMonths))
-                ->pluck('month')
-                ->toArray();
-            $existingMonthsMap = array_fill_keys($existingMonths, true);
-        }
-
-        $excelAggregator = [];
-        $update_map = [];
-        $monthToKeyMap = []; // 校验 Excel 内部月份是否指向了多个单据
-
-        // --- 步骤 2:核心校验循环 ---
-        foreach ($array as $rowIndex => $row) {
-            $displayLine = $rowIndex + 1;
-            $valCode = trim($row[$codeIdx] ?? '');
-            $valMonthTs = $row[$monthIdx] ?? '';
-            $valDev = trim($row[$devIdx] ?? '');
-
-            if (!is_numeric($valMonthTs)) continue;
-
-            // A. 确定该行的归组 Key
-            $aggKey = $valCode ?: "NEW_ORDER_" . $valMonthTs;
-
-            // B. 校验单据与月份一致性 & 确定 Update 映射
-            if ($valCode) {
-                if (isset($dbOrders[$valCode])) {
-                    $dbOrder = $dbOrders[$valCode];
-                    if ($dbOrder->month != $valMonthTs) {
-                        $errors[] = "第{$displayLine}行:单据[{$valCode}]对应月份为[" . date('Y-m', $dbOrder->month) . "],与 Excel 不符";
-                    }
-                    $update_map[$rowIndex] = $dbOrder->id;
-                } else {
-                    // 如果填了编码但在数据库没找到,它会被视为“新增”,需进入下方的月份查重
-                    // 这里暂不报错,由下方的唯一性检查拦截
-                }
-            }
-
-            // C. 【关键修正】月份唯一单据校验
-            // 如果系统判定该行是“新增”(没进 update_map),且月份已存在,则拦截
-            if (!isset($update_map[$rowIndex])) {
-                if (isset($existingMonthsMap[$valMonthTs])) {
-                    $errors[] = "第{$displayLine}行:月份[" . date('Y-m', $valMonthTs) . "]已存在单据,请填写正确的单据编码进行编辑";
-                }
-            }
-
-            // D. Excel 内部一致性校验
-            // 防止 Excel 里同一个月出现了两个不同的 aggKey(例如一个填了码,一个没填)
-            if (!isset($monthToKeyMap[$valMonthTs])) {
-                $monthToKeyMap[$valMonthTs] = $aggKey;
-            } elseif ($monthToKeyMap[$valMonthTs] !== $aggKey) {
-                $errors[] = "第{$displayLine}行:同一个月份[" . date('Y-m', $valMonthTs) . "]在 Excel 中指向了不同的单据组";
-            }
-
-            // E. 设备存在性与唯一性
-            if (!isset($dbDevs[$valDev])) {
-                $errors[] = "第{$displayLine}行:资产编码[{$valDev}]不存在";
-            } else {
-                if (isset($excelAggregator[$aggKey]['devs'][$valDev])) {
-                    $errors[] = "第{$displayLine}行:资产编码[{$valDev}]在同单据中重复";
-                }
-                $excelAggregator[$aggKey]['devs'][$valDev] = true;
-            }
-
-            // G. 数字格式校验 (checkNumber 方法)
-            foreach ([$numIdx => '天数', $num2Idx => '研发天数', $num3Idx => '工时', $num4Idx => '研发工时'] as $idx => $label) {
-                $res = $this->checkNumber($row[$idx] ?? 0, 2, 'non-negative');
-                if (!$res['valid']) $errors[] = "第{$displayLine}行{$label}:" . $res['error'];
-            }
-        }
-
-        $error_string = !empty($errors) ? implode('|', $errors) : "";
-        return [$error_string, $update_map, $dbDevs];
-    }
-
     // 人员月度工资单
     public function monthPsOrderImport($array, $user, $other_param)
     {
@@ -3782,6 +3533,249 @@ class ImportService extends Service
         return [!empty($errors) ? implode('|', $errors) : "", $update_map, $dbEmps];
     }
 
+    // 项目费用报销单月度导入 ------------------------------
+    // 项目费用报销单月度导入 ------------------------------
+    public function feeOrderImport($array, $user, $other_param)
+    {
+        $upload = $array[0];
+        list($status, $msg) = $this->compareTableAndReturn($upload, $other_param);
+        if (!$status) return [false, $msg];
+        $table_config = $msg;
+
+        unset($array[0]);
+        if (empty($array)) return [false, '导入数据不能为空'];
+
+        // 1. 基础校验(格式、必填项等)
+        list($array, $error) = $this->checkCommon($array, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        // 2. 业务详细校验 (获取 update_map 和 基础档案映射)
+        // 此时 claim_date 会在 check 方法内部被转换为时间戳并校验月份归属
+        list($error, $update_map, $maps) = $this->feeOrderCheck($array, $user, $table_config);
+        if (!empty($error)) return [0, $error];
+
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $monthIdx = array_search('month', $keys);
+
+        // --- 步骤 1: 数据聚合分组 ---
+        $groups = [];
+        foreach ($array as $rowIndex => $row) {
+            $cCode = trim($row[$codeIdx] ?? '');
+            $cMonthTs = $row[$monthIdx];
+
+            // 聚合 Key:有编码用编码,没编码用月份
+            $aggKey = $cCode ?: "MONTH_" . $cMonthTs;
+
+            if (!isset($groups[$aggKey])) {
+                $groups[$aggKey] = [
+                    'main_id'  => $update_map[$rowIndex] ?? 0,
+                    'code'     => $cCode,
+                    'month'    => $cMonthTs,
+                    'details'  => []
+                ];
+            }
+
+            // 准备详情行数据
+            $detailTmp = [];
+            foreach ($table_config as $k => $conf) {
+                if (!$conf['is_main']) {
+                    $fieldKey = $conf['key'];
+                    // 跳过仅用于展示的名称字段
+                    if (in_array($fieldKey, ['employee_title', 'item_title', 'fee_title'])) continue;
+
+                    $fieldVal = $row[$k];
+
+                    // 转换编号为 ID
+                    if ($fieldKey == 'employee_id') $fieldVal = $maps['emps'][$fieldVal] ?? 0;
+                    if ($fieldKey == 'item_id') $fieldVal = $maps['items'][$fieldVal] ?? 0;
+                    if ($fieldKey == 'fee_id') $fieldVal = $maps['fees'][$fieldVal] ?? 0;
+
+                    // 转换枚举值(委托方式、是否定位到人)
+                    if ($fieldKey == 'entrust_type') {
+                        $fieldVal = array_search($fieldVal, \App\Model\ExpenseClaimsDetails::State_Type) ?: 0;
+                    }
+                    if ($fieldKey == 'expense_type') {
+                        $fieldVal = array_search($fieldVal, \App\Model\ExpenseClaimsDetails::State_Type_2) ?: 0;
+                    }
+
+                    // 注意:claim_date 已经在 check 方法里转成了时间戳,这里直接赋值即可
+                    $detailTmp[$fieldKey] = $fieldVal;
+                }
+            }
+            $groups[$aggKey]['details'][] = $detailTmp;
+        }
+
+        // --- 步骤 2: 事务写入 ---
+        DB::beginTransaction();
+        try {
+            $time = time();
+            foreach ($groups as $group) {
+                $mainId = $group['main_id'];
+
+                if ($mainId > 0) {
+                    // 编辑:逻辑删除旧明细
+                    DB::table('expense_claims_details')->where('del_time', 0)
+                        ->where('expense_claims_id', $mainId)
+                        ->update(['del_time' => $time]);
+
+                    // 更新主表更新时间
+                    DB::table('expense_claims')->where('id', $mainId)->update(['upd_time' => $time]);
+                } else {
+                    // 新增:生成单号
+                    $newCode = $this->generateBillNo([
+                        'top_depart_id' => $user['top_depart_id'],
+                        'type'          => \App\Model\ExpenseClaims::Order_type,
+                        'period'        => date("Ym", $group['month'])
+                    ]);
+
+                    $mainId = DB::table('expense_claims')->insertGetId([
+                        'code'          => $newCode,
+                        'month'         => $group['month'],
+                        'top_depart_id' => $user['top_depart_id'],
+                        'crt_id'        => $user['id'],
+                        'crt_time'      => $time,
+                        'upd_time'      => $time,
+                        'del_time'      => 0
+                    ]);
+                }
+
+                // 批量插入详情
+                $insertDetails = [];
+                foreach ($group['details'] as $detail) {
+                    $detail['expense_claims_id'] = $mainId;
+                    $detail['top_depart_id']     = $user['top_depart_id'];
+                    $detail['crt_id']            = $user['id'];
+                    $detail['crt_time']          = $time;
+                    $detail['upd_time']          = $time;
+                    $detail['del_time']          = 0;
+                    $insertDetails[] = $detail;
+                }
+
+                if (!empty($insertDetails)) {
+                    DB::table('expense_claims_details')->insert($insertDetails);
+                }
+            }
+            DB::commit();
+        } catch (\Exception $e) {
+            DB::rollBack();
+            return [false, "写入失败:" . $e->getMessage() . " (行号:" . $e->getLine() . ")"];
+        }
+
+        return [true, ''];
+    }
+
+    private function feeOrderCheck(&$array, $user, $table_config)
+    {
+        $keys = array_column($table_config, 'key');
+        $codeIdx = array_search('code', $keys);
+        $monthIdx = array_search('month', $keys);
+        $empIdx = array_search('employee_id', $keys);
+        $itemIdx = array_search('item_id', $keys);
+        $feeIdx = array_search('fee_id', $keys);
+        $claimDateIdx = array_search('claim_date', $keys);
+
+        $topDepartId = $user['top_depart_id'];
+        $errors = [];
+
+        $allEmpNos = []; $allItemCodes = []; $allFeeCodes = []; $allMonthsTs = []; $allCodes = [];
+
+        // --- 1. 预处理:日期转换与基础编号提取 ---
+        foreach ($array as $rowIndex => $row) {
+            $line = $rowIndex + 1;
+
+            // A. 报销主月份处理
+            $valMonthRaw = trim($row[$monthIdx] ?? '');
+            list($mStatus, $valMonthTs) = $this->convertExcelCellToDate($valMonthRaw);
+            if (!$mStatus) {
+                $errors[] = "第{$line}行:月份格式错误";
+                continue;
+            }
+            $valMonthTs = strtotime(date('Y-m-01', $valMonthTs));
+            $array[$rowIndex][$monthIdx] = $valMonthTs; // 回写转换后的时间戳
+            $allMonthsTs[] = $valMonthTs;
+
+            // B. 费用产生日期校验 (claim_date)
+            $valClaimDateRaw = trim($row[$claimDateIdx] ?? '');
+            if (empty($valClaimDateRaw)) {
+                $errors[] = "第{$line}行:费用产生日期不能为空";
+            } else {
+                list($dStatus, $dTs) = $this->convertExcelCellToDate($valClaimDateRaw);
+                if (!$dStatus) {
+                    $errors[] = "第{$line}行:费用产生日期格式错误";
+                } else {
+                    // 核心逻辑:判断产生日期是否在主月份内
+                    $monthStr = date('Y-m', $valMonthTs);
+                    $claimMonthStr = date('Y-m', $dTs);
+                    if ($monthStr !== $claimMonthStr) {
+                        $errors[] = "第{$line}行:产生日期[{$claimMonthStr}]与报销月份[{$monthStr}]不符";
+                    }
+                    $array[$rowIndex][$claimDateIdx] = $dTs; // 回写转换后的时间戳
+                }
+            }
+
+            // C. 提取其他编号
+            if (!empty($row[$empIdx]))  $allEmpNos[] = trim($row[$empIdx]);
+            if (!empty($row[$itemIdx])) $allItemCodes[] = trim($row[$itemIdx]);
+            if (!empty($row[$feeIdx]))  $allFeeCodes[] = trim($row[$feeIdx]);
+            if (!empty($row[$codeIdx])) $allCodes[] = trim($row[$codeIdx]);
+        }
+
+        if (!empty($errors)) return [implode('|', $errors), [], []];
+
+        // --- 2. 批量预加载档案映射 ---
+        $dbEmps = DB::table('employee')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('number', array_unique($allEmpNos))->pluck('id', 'number')->toArray();
+
+        $dbItems = DB::table('item')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('code', array_unique($allItemCodes))->pluck('id', 'code')->toArray();
+
+        $dbFees = DB::table('fee')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('code', array_unique($allFeeCodes))->pluck('id', 'code')->toArray();
+
+        $existingMonthsMap = DB::table('expense_claims')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('month', array_unique($allMonthsTs))->pluck('code', 'month')->toArray();
+
+        $dbOrdersByCode = DB::table('expense_claims')->where('del_time', 0)->where('top_depart_id', $topDepartId)
+            ->whereIn('code', array_unique($allCodes))->get()->keyBy('code');
+
+        // --- 3. 逐行校验档案存在性与单据逻辑 ---
+        $update_map = [];
+        foreach ($array as $rowIndex => $row) {
+            $line = $rowIndex + 1;
+            $valCode = trim($row[$codeIdx] ?? '');
+            $valMonthTs = $row[$monthIdx];
+
+            if (!empty($row[$empIdx]) && !isset($dbEmps[trim($row[$empIdx])])) {
+                $errors[] = "第{$line}行:工号[{$row[$empIdx]}]不存在";
+            }
+            if (!isset($dbItems[trim($row[$itemIdx])])) {
+                $errors[] = "第{$line}行:项目编号[{$row[$itemIdx]}]不存在";
+            }
+            if (!isset($dbFees[trim($row[$feeIdx])])) {
+                $errors[] = "第{$line}行:费用类型编号[{$row[$feeIdx]}]不存在";
+            }
+
+            if ($valCode !== '') {
+                if (isset($dbOrdersByCode[$valCode])) {
+                    if ($dbOrdersByCode[$valCode]->month != $valMonthTs) {
+                        $errors[] = "第{$line}行:单据[{$valCode}]对应月份为" . date('Y-m', $dbOrdersByCode[$valCode]->month) . ",Excel中月份不符";
+                    }
+                    $update_map[$rowIndex] = $dbOrdersByCode[$valCode]->id;
+                } else {
+                    $errors[] = "第{$line}行:填写的单据编码[{$valCode}]系统中不存在";
+                }
+            } else {
+                if (isset($existingMonthsMap[$valMonthTs])) {
+                    $errors[] = "第{$line}行:月份[" . date('Y-m', $valMonthTs) . "]已存在单据[{$existingMonthsMap[$valMonthTs]}],请填写该单号进行编辑";
+                }
+            }
+        }
+
+        $maps = ['emps' => $dbEmps, 'items' => $dbItems, 'fees' => $dbFees];
+        return [!empty($errors) ? implode('|', $errors) : "", $update_map, $maps];
+    }
+
     /**
      * 解析并校验时间
      */

+ 160 - 0
config/excel/RDOrder.php

@@ -0,0 +1,160 @@
+<?php
+return [
+    "name" => "研发支出辅助帐单",
+    "array" => [
+        [
+            'key' =>'code',
+            'export' =>'code',
+            'value' => '单据编码',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '(如果填写则是编辑)'
+        ],
+        [
+            'key' =>'month',
+            'export' =>'month',
+            'value' => '月份',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(2026-02)'
+        ],
+        [
+            'key' =>'type_title',
+            'export' =>'type_title',
+            'value' => '明细类型',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => ''
+        ],
+        [
+            'key' =>'voucher_date',
+            'export' =>'voucher_date',
+            'value' => '凭证日期',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(2026-02-01)'
+        ],
+        [
+            'key' =>'voucher_type',
+            'export' =>'voucher_type',
+            'value' => '凭证种类',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'voucher_no',
+            'export' =>'voucher_no',
+            'value' => '凭证号数',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'voucher_remark',
+            'export' =>'voucher_remark',
+            'value' => '凭证摘要',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'voucher_amount',
+            'export' =>'voucher_amount',
+            'value' => '会计凭证记载金额',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'aggregation_amount',
+            'export' =>'aggregation_amount',
+            'value' => '税法规定的归集金额',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'total_amount',
+            'export' =>'total_amount',
+            'value' => '月度人员人工费用总计|月度设备折旧费用|报销总金额|费用总金额',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'fee_title',
+            'export' =>'fee_title',
+            'value' => '费用类型',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'remark',
+            'export' =>'remark',
+            'value' => '费用说明',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'entrust1_amount',
+            'export' =>'entrust1_amount',
+            'value' => '委内金额',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'entrust2_amount',
+            'export' =>'entrust2_amount',
+            'value' => '委外金额',
+            'required' => false,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ]
+    ]
+];

+ 160 - 0
config/excel/feeOrder.php

@@ -0,0 +1,160 @@
+<?php
+return [
+    "name" => "项目费用报销单",
+    "array" => [
+        [
+            'key' =>'code',
+            'export' =>'code',
+            'value' => '单据编码',
+            'required' => false,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '(如果填写则是编辑)'
+        ],
+        [
+            'key' =>'month',
+            'export' =>'month',
+            'value' => '月份',
+            'required' => true,
+            'is_main' => true,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(2026-02)'
+        ],
+        [
+            'key' =>'employee_id',
+            'export' =>'employee_number',
+            'value' => '人员工号',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'employee_title',
+            'export' =>'employee_title',
+            'value' => '人员名称',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
+        [
+            'key' =>'item_id',
+            'export' =>'item_code',
+            'value' => '项目编号',
+            'required' => true,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(填写项目编号)'
+        ],
+        [
+            'key' =>'item_title',
+            'export' =>'item_title',
+            'value' => '项目名称',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
+        [
+            'key' =>'fee_id',
+            'export' =>'fee_code',
+            'value' => '费用类型编号',
+            'required' => true,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(填写费用类型编号)'
+        ],
+        [
+            'key' =>'fee_title',
+            'export' =>'fee_title',
+            'value' => '费用类型名称',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填(便于操作人查看编码对应的名称)'
+        ],
+        [
+            'key' =>'amount',
+            'export' =>'amount',
+            'value' => '费用金额',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填'
+        ],
+        [
+            'key' =>'claim_date',
+            'export' =>'claim_date',
+            'value' => '费用产生日期',
+            'required' => true,
+            'is_main' => false,
+            'default' => 0,
+            'unique' => false,
+            'enums' => [],
+            'comments' => '必填(如 2026-01-03)'
+        ],
+        [
+            'key' =>'voucher_no',
+            'export' =>'voucher_no',
+            'value' => '凭证号/流水号',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'remark',
+            'export' =>'remark',
+            'value' => '费用说明',
+            'required' => false,
+            'is_main' => false,
+            'default' => "",
+            'unique' => false,
+            'enums' => [],
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'entrust_type',
+            'export' =>'entrust_type_title',
+            'value' => '委托方式',
+            'required' => false,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => array_values(\App\Model\ExpenseClaimsDetails::State_Type),
+            'default' => \App\Model\ExpenseClaimsDetails::TYPE_ZERO,
+            'comments' => '选填'
+        ],
+        [
+            'key' =>'expense_type',
+            'export' =>'expense_type_title',
+            'value' => '是否定位到人',
+            'required' => false,
+            'is_main' => false,
+            'unique' => false,
+            'enums' => array_values(\App\Model\ExpenseClaimsDetails::State_Type_2),
+            'default' => \App\Model\ExpenseClaimsDetails::TYPE_ZERO,
+            'comments' => '选填'
+        ],
+    ]
+];