Commit b16455bb4be03ae34bde792478bd1fc000f31d32

Authored by 李宇
2 parents ecbae9ec 8b44d4bd

Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP

Showing 41 changed files with 1036 additions and 357 deletions
antis-ncc-admin/.env.development
@@ -2,8 +2,8 @@ @@ -2,8 +2,8 @@
2 2
3 VUE_CLI_BABEL_TRANSPILE_MODULES = true 3 VUE_CLI_BABEL_TRANSPILE_MODULES = true
4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com' 4 # VUE_APP_BASE_API = 'https://erp.lvqianmeiye.com'
5 -VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'  
6 -# VUE_APP_BASE_API = 'http://localhost:2011' 5 +# VUE_APP_BASE_API = 'http://erp_test.lvqianmeiye.com'
  6 +VUE_APP_BASE_API = 'http://localhost:2011'
7 # VUE_APP_BASE_API = 'http://localhost:2011' 7 # VUE_APP_BASE_API = 'http://localhost:2011'
8 VUE_APP_IMG_API = '' 8 VUE_APP_IMG_API = ''
9 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket' 9 VUE_APP_BASE_WSS = 'ws://192.168.110.45:2011/websocket'
antis-ncc-admin/src/api/extend/healthCoachSalary.js
@@ -27,9 +27,10 @@ export function getExtraCalculationList(params) { @@ -27,9 +27,10 @@ export function getExtraCalculationList(params) {
27 } 27 }
28 28
29 // 导入额外计算数据 29 // 导入额外计算数据
30 -export function importExtraCalculationFromExcel(file) { 30 +export function importExtraCalculationFromExcel(file, clearBeforeImport = false) {
31 const formData = new FormData() 31 const formData = new FormData()
32 formData.append('file', file) 32 formData.append('file', file)
  33 + formData.append('clearBeforeImport', clearBeforeImport)
33 return request({ 34 return request({
34 url: '/api/Extend/lqsalaryextracalculation/ImportFromExcel', 35 url: '/api/Extend/lqsalaryextracalculation/ImportFromExcel',
35 method: 'POST', 36 method: 'POST',
antis-ncc-admin/src/views/lqCooperationCost/import-dialog.vue
@@ -21,12 +21,25 @@ @@ -21,12 +21,25 @@
21 <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> 21 <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
22 <div class="el-upload__tip" slot="tip">只能上传xlsx/xls文件,且不超过10MB</div> 22 <div class="el-upload__tip" slot="tip">只能上传xlsx/xls文件,且不超过10MB</div>
23 </el-upload> 23 </el-upload>
  24 + <div style="margin-top: 15px;">
  25 + <el-checkbox v-model="clearBeforeImport">
  26 + 清理导入月份数据
  27 + </el-checkbox>
  28 + </div>
24 <div v-if="uploadResult" class="upload-result" :class="uploadResult.type"> 29 <div v-if="uploadResult" class="upload-result" :class="uploadResult.type">
25 <div class="result-title">{{ uploadResult.title }}</div> 30 <div class="result-title">{{ uploadResult.title }}</div>
26 <div class="result-description">{{ uploadResult.description }}</div> 31 <div class="result-description">{{ uploadResult.description }}</div>
27 <div v-if="uploadResult.failMessages && uploadResult.failMessages.length > 0" class="fail-messages"> 32 <div v-if="uploadResult.failMessages && uploadResult.failMessages.length > 0" class="fail-messages">
28 <div v-for="(msg, index) in uploadResult.failMessages" :key="index" class="fail-message">{{ msg }}</div> 33 <div v-for="(msg, index) in uploadResult.failMessages" :key="index" class="fail-message">{{ msg }}</div>
29 </div> 34 </div>
  35 + <div v-if="uploadResult.successRecords && uploadResult.successRecords.length > 0" class="success-records">
  36 + <div class="success-title">成功导入的记录(包含成本类型):</div>
  37 + <div v-for="(record, index) in uploadResult.successRecords" :key="index" class="success-record">
  38 + 门店ID: {{ record.storeId }}, 年份: {{ record.year }}, 月份: {{ record.month }},
  39 + 金额: {{ record.totalAmount }}, 成本类型: {{ record.costType || '无' }},
  40 + 备注: {{ record.remarks || '无' }}
  41 + </div>
  42 + </div>
30 </div> 43 </div>
31 </div> 44 </div>
32 <div slot="footer" class="dialog-footer"> 45 <div slot="footer" class="dialog-footer">
@@ -45,6 +58,7 @@ export default { @@ -45,6 +58,7 @@ export default {
45 visible: false, 58 visible: false,
46 fileList: [], 59 fileList: [],
47 uploading: false, 60 uploading: false,
  61 + clearBeforeImport: true, // 是否需要清理导入月份数据,默认true
48 uploadResult: null 62 uploadResult: null
49 } 63 }
50 }, 64 },
@@ -53,6 +67,7 @@ export default { @@ -53,6 +67,7 @@ export default {
53 this.visible = true 67 this.visible = true
54 this.fileList = [] 68 this.fileList = []
55 this.uploading = false 69 this.uploading = false
  70 + this.clearBeforeImport = true
56 this.uploadResult = null 71 this.uploadResult = null
57 }, 72 },
58 handleFileChange(file, fileList) { 73 handleFileChange(file, fileList) {
@@ -73,6 +88,7 @@ export default { @@ -73,6 +88,7 @@ export default {
73 this.uploadResult = null 88 this.uploadResult = null
74 const formData = new FormData() 89 const formData = new FormData()
75 formData.append('file', file.raw) 90 formData.append('file', file.raw)
  91 + formData.append('clearBeforeImport', this.clearBeforeImport)
76 request({ 92 request({
77 url: '/api/Extend/LqCooperationCost/Actions/Import', 93 url: '/api/Extend/LqCooperationCost/Actions/Import',
78 method: 'POST', 94 method: 'POST',
@@ -88,7 +104,8 @@ export default { @@ -88,7 +104,8 @@ export default {
88 type: 'success', 104 type: 'success',
89 title: '导入成功', 105 title: '导入成功',
90 description: `成功导入 ${data.successCount || 0} 条,失败 ${data.failCount || 0} 条`, 106 description: `成功导入 ${data.successCount || 0} 条,失败 ${data.failCount || 0} 条`,
91 - failMessages: data.failMessages || [] 107 + failMessages: data.failMessages || [],
  108 + successRecords: data.successRecords || []
92 } 109 }
93 this.$message.success('导入成功') 110 this.$message.success('导入成功')
94 setTimeout(() => { 111 setTimeout(() => {
@@ -163,6 +180,24 @@ export default { @@ -163,6 +180,24 @@ export default {
163 margin-bottom: 5px; 180 margin-bottom: 5px;
164 } 181 }
165 } 182 }
  183 +
  184 + .success-records {
  185 + margin-top: 10px;
  186 + padding-top: 10px;
  187 + border-top: 1px solid #ddd;
  188 +
  189 + .success-title {
  190 + font-weight: 600;
  191 + margin-bottom: 8px;
  192 + color: #67C23A;
  193 + }
  194 +
  195 + .success-record {
  196 + color: #606266;
  197 + margin-bottom: 5px;
  198 + font-size: 12px;
  199 + }
  200 + }
166 } 201 }
167 </style> 202 </style>
168 203
antis-ncc-admin/src/views/wageManagement/extra-data-dialog.vue
@@ -57,6 +57,12 @@ @@ -57,6 +57,12 @@
57 > 57 >
58 导入 58 导入
59 </el-button> 59 </el-button>
  60 + <el-checkbox
  61 + v-model="clearBeforeImport"
  62 + style="margin-left: 20px;"
  63 + >
  64 + 清理导入月份数据
  65 + </el-checkbox>
60 </div> 66 </div>
61 67
62 <!-- 数据表格 --> 68 <!-- 数据表格 -->
@@ -190,6 +196,7 @@ export default { @@ -190,6 +196,7 @@ export default {
190 loading: false, 196 loading: false,
191 exportTemplateLoading: false, 197 exportTemplateLoading: false,
192 importLoading: false, 198 importLoading: false,
  199 + clearBeforeImport: true, // 是否需要清理导入月份数据,默认true
193 list: [], 200 list: [],
194 total: 0, 201 total: 0,
195 queryParams: { 202 queryParams: {
@@ -451,7 +458,7 @@ export default { @@ -451,7 +458,7 @@ export default {
451 458
452 this.importLoading = true 459 this.importLoading = true
453 try { 460 try {
454 - const response = await importExtraCalculationFromExcel(file) 461 + const response = await importExtraCalculationFromExcel(file, this.clearBeforeImport)
455 462
456 if (response.code === 200) { 463 if (response.code === 200) {
457 this.$message.success('导入成功') 464 this.$message.success('导入成功')
excel/健康师额外数据模板_2025年11月_20251219120434.xlsx 0 → 100644
No preview for this file type
excel/合作成本表.xlsx
No preview for this file type
excel/合作成本表_测试导入.xlsx 0 → 100644
No preview for this file type
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqContract/ContractExpenseStatisticsInput.cs
@@ -16,20 +16,16 @@ namespace NCC.Extend.Entitys.Dto.LqContract @@ -16,20 +16,16 @@ namespace NCC.Extend.Entitys.Dto.LqContract
16 public string StoreId { get; set; } 16 public string StoreId { get; set; }
17 17
18 /// <summary> 18 /// <summary>
19 - /// 统计年份(必填 19 + /// 统计年份(可选
20 /// </summary> 20 /// </summary>
21 - [Required(ErrorMessage = "年份不能为空")]  
22 - [Range(2020, 2100, ErrorMessage = "年份必须在2020-2100之间")]  
23 [Display(Name = "年份")] 21 [Display(Name = "年份")]
24 - public int Year { get; set; } 22 + public int? Year { get; set; }
25 23
26 /// <summary> 24 /// <summary>
27 - /// 统计月份(必填,1-12) 25 + /// 统计月份(可选,1-12)
28 /// </summary> 26 /// </summary>
29 - [Required(ErrorMessage = "月份不能为空")]  
30 - [Range(1, 12, ErrorMessage = "月份必须在1-12之间")]  
31 [Display(Name = "月份")] 27 [Display(Name = "月份")]
32 - public int Month { get; set; } 28 + public int? Month { get; set; }
33 29
34 /// <summary> 30 /// <summary>
35 /// 分类列表(可选,不传则统计所有分类) 31 /// 分类列表(可选,不传则统计所有分类)
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqCooperationCost/LqCooperationCostCrInput.cs
@@ -36,6 +36,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost @@ -36,6 +36,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost
36 /// 备注说明 36 /// 备注说明
37 /// </summary> 37 /// </summary>
38 public string remarks { get; set; } 38 public string remarks { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 成本类型
  42 + /// </summary>
  43 + public string costType { get; set; }
39 } 44 }
40 } 45 }
41 46
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqCooperationCost/LqCooperationCostInfoOutput.cs
@@ -43,6 +43,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost @@ -43,6 +43,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost
43 public string remarks { get; set; } 43 public string remarks { get; set; }
44 44
45 /// <summary> 45 /// <summary>
  46 + /// 成本类型
  47 + /// </summary>
  48 + public string costType { get; set; }
  49 +
  50 + /// <summary>
46 /// 创建人ID 51 /// 创建人ID
47 /// </summary> 52 /// </summary>
48 public string createUser { get; set; } 53 public string createUser { get; set; }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqCooperationCost/LqCooperationCostListOutput.cs
@@ -43,6 +43,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost @@ -43,6 +43,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost
43 public string remarks { get; set; } 43 public string remarks { get; set; }
44 44
45 /// <summary> 45 /// <summary>
  46 + /// 成本类型
  47 + /// </summary>
  48 + public string costType { get; set; }
  49 +
  50 + /// <summary>
46 /// 创建人ID 51 /// 创建人ID
47 /// </summary> 52 /// </summary>
48 public string createUser { get; set; } 53 public string createUser { get; set; }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqCooperationCost/LqCooperationCostUpInput.cs
@@ -41,6 +41,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost @@ -41,6 +41,11 @@ namespace NCC.Extend.Entitys.Dto.LqCooperationCost
41 /// 备注说明 41 /// 备注说明
42 /// </summary> 42 /// </summary>
43 public string remarks { get; set; } 43 public string remarks { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 成本类型
  47 + /// </summary>
  48 + public string costType { get; set; }
44 } 49 }
45 } 50 }
46 51
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdKdjlb/BillingItemDetailListOutput.cs
@@ -86,6 +86,11 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb @@ -86,6 +86,11 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb
86 /// 付款医院 86 /// 付款医院
87 /// </summary> 87 /// </summary>
88 public string fkyy { get; set; } 88 public string fkyy { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 付款方式
  92 + /// </summary>
  93 + public string paymentMethod { get; set; }
89 } 94 }
90 } 95 }
91 96
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowReturnInput.cs
  1 +using System;
1 using System.ComponentModel.DataAnnotations; 2 using System.ComponentModel.DataAnnotations;
2 3
3 namespace NCC.Extend.Entitys.Dto.LqLaundryFlow 4 namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
@@ -37,6 +38,12 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow @@ -37,6 +38,12 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
37 [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")] 38 [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
38 [Display(Name = "备注")] 39 [Display(Name = "备注")]
39 public string Remark { get; set; } 40 public string Remark { get; set; }
  41 +
  42 + /// <summary>
  43 + /// 送回时间(可选,如果不传则使用当前时间)
  44 + /// </summary>
  45 + [Display(Name = "送回时间")]
  46 + public DateTime? ReturnTime { get; set; }
40 } 47 }
41 } 48 }
42 49
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowSendInput.cs
  1 +using System;
1 using System.ComponentModel.DataAnnotations; 2 using System.ComponentModel.DataAnnotations;
2 3
3 namespace NCC.Extend.Entitys.Dto.LqLaundryFlow 4 namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
@@ -45,6 +46,12 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow @@ -45,6 +46,12 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow
45 [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")] 46 [StringLength(1000, ErrorMessage = "备注长度不能超过1000个字符")]
46 [Display(Name = "备注")] 47 [Display(Name = "备注")]
47 public string Remark { get; set; } 48 public string Remark { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 送出时间(可选,如果不传则使用当前时间)
  52 + /// </summary>
  53 + [Display(Name = "送出时间")]
  54 + public DateTime? SendTime { get; set; }
48 } 55 }
49 } 56 }
50 57
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectTeacherSalary/MajorProjectTeacherSalaryOutput.cs
@@ -18,12 +18,12 @@ namespace NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary @@ -18,12 +18,12 @@ namespace NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary
18 public string StatisticsMonth { get; set; } 18 public string StatisticsMonth { get; set; }
19 19
20 /// <summary> 20 /// <summary>
21 - /// 门店ID 21 + /// 部门ID(使用StoreId字段存储部门ID)
22 /// </summary> 22 /// </summary>
23 public string StoreId { get; set; } 23 public string StoreId { get; set; }
24 24
25 /// <summary> 25 /// <summary>
26 - /// 门店名称 26 + /// 部门名称(使用StoreName字段存储部门名称)
27 /// </summary> 27 /// </summary>
28 public string StoreName { get; set; } 28 public string StoreName { get; set; }
29 29
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_cooperation_cost/LqCooperationCostEntity.cs
@@ -54,6 +54,12 @@ namespace NCC.Extend.Entitys.lq_cooperation_cost @@ -54,6 +54,12 @@ namespace NCC.Extend.Entitys.lq_cooperation_cost
54 public string Remarks { get; set; } 54 public string Remarks { get; set; }
55 55
56 /// <summary> 56 /// <summary>
  57 + /// 成本类型
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "F_CostType")]
  60 + public string CostType { get; set; }
  61 +
  62 + /// <summary>
57 /// 是否有效(1:有效 0:无效) 63 /// 是否有效(1:有效 0:无效)
58 /// </summary> 64 /// </summary>
59 [SugarColumn(ColumnName = "F_IsEffective")] 65 [SugarColumn(ColumnName = "F_IsEffective")]
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_share_statistics_store/LqShareStatisticsStoreEntity.cs
@@ -183,12 +183,18 @@ namespace NCC.Extend.Entitys.lq_share_statistics_store @@ -183,12 +183,18 @@ namespace NCC.Extend.Entitys.lq_share_statistics_store
183 public decimal SalaryBaseStoreManager { get; set; } 183 public decimal SalaryBaseStoreManager { get; set; }
184 184
185 /// <summary> 185 /// <summary>
186 - /// 人工工资-总经理/经理底薪 186 + /// 人工工资-总经理底薪
187 /// </summary> 187 /// </summary>
188 [SugarColumn(ColumnName = "F_SalaryBaseGeneralManager")] 188 [SugarColumn(ColumnName = "F_SalaryBaseGeneralManager")]
189 public decimal SalaryBaseGeneralManager { get; set; } 189 public decimal SalaryBaseGeneralManager { get; set; }
190 190
191 /// <summary> 191 /// <summary>
  192 + /// 人工工资-经理底薪
  193 + /// </summary>
  194 + [SugarColumn(ColumnName = "F_SalaryBaseManager")]
  195 + public decimal SalaryBaseManager { get; set; }
  196 +
  197 + /// <summary>
192 /// 人工工资-健康师提成 198 /// 人工工资-健康师提成
193 /// </summary> 199 /// </summary>
194 [SugarColumn(ColumnName = "F_SalaryCommissionHealthCoach")] 200 [SugarColumn(ColumnName = "F_SalaryCommissionHealthCoach")]
@@ -213,12 +219,18 @@ namespace NCC.Extend.Entitys.lq_share_statistics_store @@ -213,12 +219,18 @@ namespace NCC.Extend.Entitys.lq_share_statistics_store
213 public decimal SalaryCommissionStoreManager { get; set; } 219 public decimal SalaryCommissionStoreManager { get; set; }
214 220
215 /// <summary> 221 /// <summary>
216 - /// 人工工资-总经理/经理提成 222 + /// 人工工资-总经理提成
217 /// </summary> 223 /// </summary>
218 [SugarColumn(ColumnName = "F_SalaryCommissionGeneralManager")] 224 [SugarColumn(ColumnName = "F_SalaryCommissionGeneralManager")]
219 public decimal SalaryCommissionGeneralManager { get; set; } 225 public decimal SalaryCommissionGeneralManager { get; set; }
220 226
221 /// <summary> 227 /// <summary>
  228 + /// 人工工资-经理提成
  229 + /// </summary>
  230 + [SugarColumn(ColumnName = "F_SalaryCommissionManager")]
  231 + public decimal SalaryCommissionManager { get; set; }
  232 +
  233 + /// <summary>
222 /// 人工工资-手工 234 /// 人工工资-手工
223 /// </summary> 235 /// </summary>
224 [SugarColumn(ColumnName = "F_SalaryManual")] 236 [SugarColumn(ColumnName = "F_SalaryManual")]
netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
@@ -278,21 +278,27 @@ namespace NCC.Extend @@ -278,21 +278,27 @@ namespace NCC.Extend
278 } 278 }
279 279
280 // 2.3 获取门店目标信息(门店生命线和阶段目标) 280 // 2.3 获取门店目标信息(门店生命线和阶段目标)
  281 + // 如果没有设置门店目标,则相关字段设为0(适用于新店未开张等情况)
281 if (!storeTargetDict.ContainsKey(storeId)) 282 if (!storeTargetDict.ContainsKey(storeId))
282 { 283 {
283 - throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算店助工资"); 284 + // 门店目标未设置时,所有目标相关字段设为0
  285 + salary.StoreLifeline = 0;
  286 + salary.Stage1TargetHeadCount = 0;
  287 + salary.Stage2TargetHeadCount = 0;
  288 + }
  289 + else
  290 + {
  291 + var storeTarget = storeTargetDict[storeId];
  292 + salary.StoreLifeline = storeTarget.StoreLifeline;
  293 +
  294 + // 阶段目标设置(如果未设置,则奖励金额为0)
  295 + salary.Stage1TargetHeadCount = storeTarget.AssistantHeadcountTargetStage1 > 0
  296 + ? (int)storeTarget.AssistantHeadcountTargetStage1
  297 + : 0;
  298 + salary.Stage2TargetHeadCount = storeTarget.AssistantHeadcountTargetStage2 > 0
  299 + ? (int)storeTarget.AssistantHeadcountTargetStage2
  300 + : 0;
284 } 301 }
285 -  
286 - var storeTarget = storeTargetDict[storeId];  
287 - salary.StoreLifeline = storeTarget.StoreLifeline;  
288 -  
289 - // 阶段目标设置(如果未设置,则奖励金额为0)  
290 - salary.Stage1TargetHeadCount = storeTarget.AssistantHeadcountTargetStage1 > 0  
291 - ? (int)storeTarget.AssistantHeadcountTargetStage1  
292 - : 0;  
293 - salary.Stage2TargetHeadCount = storeTarget.AssistantHeadcountTargetStage2 > 0  
294 - ? (int)storeTarget.AssistantHeadcountTargetStage2  
295 - : 0;  
296 302
297 // 2.4 计算门店业绩 303 // 2.4 计算门店业绩
298 decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0; 304 decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
@@ -328,53 +334,18 @@ namespace NCC.Extend @@ -328,53 +334,18 @@ namespace NCC.Extend
328 decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline; 334 decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline;
329 335
330 // 根据岗位类型确定提成比例 336 // 根据岗位类型确定提成比例
331 - if (isDirector) 337 + // 店助和店助主任使用相同的阶梯提成规则
  338 + // 先计算总提成金额(阶梯计算)
  339 + decimal totalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  340 +
  341 + // 计算平均提成比例(用于显示)
  342 + if (salary.StoreTotalPerformance > 0)
332 { 343 {
333 - // 店助主任:使用固定比例(按规则文档,业绩≥100%时使用阶梯提成,但为保持比例固定,使用平均比例)  
334 - // 业绩 < 70%:0%  
335 - // 70% ≤ 业绩 < 100%:0.4%  
336 - // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例  
337 - if (performanceRatio < 0.7m)  
338 - {  
339 - commissionRate = 0;  
340 - }  
341 - else if (performanceRatio < 1.0m)  
342 - {  
343 - commissionRate = 0.004m; // 0.4%  
344 - }  
345 - else  
346 - {  
347 - // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例  
348 - // ≤生命线部分:0.6%,>生命线部分:1%  
349 - decimal storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline);  
350 - if (salary.StoreTotalPerformance > 0)  
351 - {  
352 - commissionRate = storeTotalCommission / salary.StoreTotalPerformance;  
353 - }  
354 - else  
355 - {  
356 - commissionRate = 0;  
357 - }  
358 - } 344 + commissionRate = totalCommission / salary.StoreTotalPerformance;
359 } 345 }
360 else 346 else
361 { 347 {
362 - // 店助:使用固定比例  
363 - // 业绩 < 70%:0%  
364 - // 70% ≤ 业绩 < 100%:0.4%  
365 - // 业绩 ≥ 100%:0.6%  
366 - if (performanceRatio < 0.7m)  
367 - {  
368 - commissionRate = 0;  
369 - }  
370 - else if (performanceRatio < 1.0m)  
371 - {  
372 - commissionRate = 0.004m; // 0.4%  
373 - }  
374 - else  
375 - {  
376 - commissionRate = 0.006m; // 0.6%  
377 - } 348 + commissionRate = 0;
378 } 349 }
379 } 350 }
380 351
@@ -461,12 +432,15 @@ namespace NCC.Extend @@ -461,12 +432,15 @@ namespace NCC.Extend
461 } 432 }
462 433
463 // 2.11 按在店天数比例计算店助的提成和奖励 434 // 2.11 按在店天数比例计算店助的提成和奖励
464 - // 逻辑:提成金额 = 门店业绩 × 提成比例 / 当月天数 × 在店天数 435 + // 逻辑:提成金额 = 门店总提成(阶梯计算) / 当月天数 × 在店天数
465 // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数 436 // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数
466 if (daysInMonth > 0 && workingDays > 0) 437 if (daysInMonth > 0 && workingDays > 0)
467 { 438 {
468 - // 按比例计算提成:门店业绩 × 提成比例 / 当月天数 × 在店天数  
469 - salary.CommissionAmount = salary.StoreTotalPerformance * commissionRate / daysInMonth * workingDays; 439 + // 先计算门店总提成(阶梯计算)- 店助和店助主任使用相同的规则
  440 + decimal storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
  441 +
  442 + // 按比例计算提成:门店总提成 / 当月天数 × 在店天数
  443 + salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays;
470 444
471 // 按比例计算奖励 445 // 按比例计算奖励
472 salary.StageRewardAmount = storeTotalStageReward / daysInMonth * workingDays; 446 salary.StageRewardAmount = storeTotalStageReward / daysInMonth * workingDays;
@@ -644,21 +618,18 @@ namespace NCC.Extend @@ -644,21 +618,18 @@ namespace NCC.Extend
644 else 618 else
645 { 619 {
646 // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成 620 // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成
647 - // 70%以下部分:0%  
648 - // 70%-100%部分:0.4%  
649 - // ≤生命线部分:0.6%,>生命线部分:1%  
650 - decimal stage70 = storeLifeline * 0.7m; 621 + // ≤生命线部分(整个生命线):0.6%
  622 + // >生命线部分:1%
651 decimal stage100 = storeLifeline; 623 decimal stage100 = storeLifeline;
652 - decimal performance70To100 = stage100 - stage70;  
653 - decimal performanceAbove100 = storePerformance - stage100; 624 + decimal performanceUpToLifeline = stage100; // ≤生命线部分(整个生命线)
  625 + decimal performanceAbove100 = storePerformance - stage100; // >生命线部分
654 626
655 - // 70%-100%部分:0.4%  
656 - decimal commission70To100 = performance70To100 * 0.004m;  
657 - // ≤生命线部分(0-70%):0%,70%-100%部分:0.4%,已计算 627 + // ≤生命线部分:0.6%
  628 + decimal commissionUpToLifeline = performanceUpToLifeline * 0.006m;
658 // >生命线部分:1% 629 // >生命线部分:1%
659 decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1% 630 decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1%
660 631
661 - return commission70To100 + commissionAbove100; 632 + return commissionUpToLifeline + commissionAbove100;
662 } 633 }
663 } 634 }
664 } 635 }
netcore/src/Modularity/Extend/NCC.Extend/LqContractService.cs
@@ -1177,10 +1177,15 @@ namespace NCC.Extend @@ -1177,10 +1177,15 @@ namespace NCC.Extend
1177 /// 1177 ///
1178 /// 参数说明: 1178 /// 参数说明:
1179 /// - storeId: 门店ID(必填) 1179 /// - storeId: 门店ID(必填)
1180 - /// - year: 统计年份(必填)  
1181 - /// - month: 统计月份(必填,1-12) 1180 + /// - year: 统计年份(可选,不传则统计所有年份)
  1181 + /// - month: 统计月份(可选,1-12,不传则统计所有月份;如果只传月份不传年份,则使用当前年份)
1182 /// - categories: 分类列表(可选,不传则统计所有分类) 1182 /// - categories: 分类列表(可选,不传则统计所有分类)
1183 /// 1183 ///
  1184 + /// 时间范围说明:
  1185 + /// - 如果同时提供年份和月份:统计指定月份的数据
  1186 + /// - 如果只提供年份:统计整年的数据
  1187 + /// - 如果都不提供:统计所有时间的数据
  1188 + ///
1184 /// 返回数据说明: 1189 /// 返回数据说明:
1185 /// - storeId: 门店ID 1190 /// - storeId: 门店ID
1186 /// - storeName: 门店名称 1191 /// - storeName: 门店名称
@@ -1203,16 +1208,42 @@ namespace NCC.Extend @@ -1203,16 +1208,42 @@ namespace NCC.Extend
1203 { 1208 {
1204 try 1209 try
1205 { 1210 {
1206 - // 验证月份  
1207 - if (input.Month < 1 || input.Month > 12) 1211 + // 验证年份(如果提供了年份)
  1212 + if (input.Year.HasValue && (input.Year < 2020 || input.Year > 2100))
  1213 + {
  1214 + throw NCCException.Oh("年份必须在2020-2100之间");
  1215 + }
  1216 +
  1217 + // 验证月份(如果提供了月份)
  1218 + if (input.Month.HasValue && (input.Month < 1 || input.Month > 12))
1208 { 1219 {
1209 throw NCCException.Oh("月份必须在1-12之间"); 1220 throw NCCException.Oh("月份必须在1-12之间");
1210 } 1221 }
1211 1222
1212 - // 构建统计月份的开始和结束时间  
1213 - var statisticsMonth = new DateTime(input.Year, input.Month, 1);  
1214 - var monthStart = statisticsMonth;  
1215 - var monthEnd = statisticsMonth.AddMonths(1).AddDays(-1); 1223 + // 如果提供了月份但没有提供年份,使用当前年份
  1224 + if (input.Month.HasValue && !input.Year.HasValue)
  1225 + {
  1226 + input.Year = DateTime.Now.Year;
  1227 + }
  1228 +
  1229 + // 构建时间范围
  1230 + DateTime? monthStart = null;
  1231 + DateTime? monthEnd = null;
  1232 +
  1233 + if (input.Year.HasValue && input.Month.HasValue)
  1234 + {
  1235 + // 统计指定月份
  1236 + var statisticsMonth = new DateTime(input.Year.Value, input.Month.Value, 1);
  1237 + monthStart = statisticsMonth;
  1238 + monthEnd = statisticsMonth.AddMonths(1).AddDays(-1);
  1239 + }
  1240 + else if (input.Year.HasValue)
  1241 + {
  1242 + // 只提供了年份,统计整年
  1243 + monthStart = new DateTime(input.Year.Value, 1, 1);
  1244 + monthEnd = new DateTime(input.Year.Value, 12, 31, 23, 59, 59);
  1245 + }
  1246 + // 如果年份和月份都没提供,monthStart 和 monthEnd 保持为 null,表示不按时间过滤
1216 1247
1217 // 查询门店信息 1248 // 查询门店信息
1218 var store = await _db.Queryable<LqMdxxEntity>() 1249 var store = await _db.Queryable<LqMdxxEntity>()
@@ -1225,16 +1256,25 @@ namespace NCC.Extend @@ -1225,16 +1256,25 @@ namespace NCC.Extend
1225 throw NCCException.Oh("门店不存在"); 1256 throw NCCException.Oh("门店不存在");
1226 } 1257 }
1227 1258
1228 - // 查询该门店在指定月份的月租明细  
1229 - var details = await _db.Queryable<LqContractRentDetailEntity, LqContractEntity>( 1259 + // 查询该门店在指定时间范围的月租明细
  1260 + var detailsQuery = _db.Queryable<LqContractRentDetailEntity, LqContractEntity>(
1230 (detail, contract) => new JoinQueryInfos( 1261 (detail, contract) => new JoinQueryInfos(
1231 JoinType.Inner, detail.ContractId == contract.Id)) 1262 JoinType.Inner, detail.ContractId == contract.Id))
1232 .Where((detail, contract) => 1263 .Where((detail, contract) =>
1233 contract.StoreId == input.StoreId && 1264 contract.StoreId == input.StoreId &&
1234 contract.IsEffective == StatusEnum.有效.GetHashCode() && 1265 contract.IsEffective == StatusEnum.有效.GetHashCode() &&
1235 - detail.IsEffective == StatusEnum.有效.GetHashCode() &&  
1236 - detail.PaymentMonth >= monthStart &&  
1237 - detail.PaymentMonth <= monthEnd) 1266 + detail.IsEffective == StatusEnum.有效.GetHashCode());
  1267 +
  1268 + // 如果提供了时间范围,则添加时间过滤条件
  1269 + if (monthStart.HasValue && monthEnd.HasValue)
  1270 + {
  1271 + detailsQuery = detailsQuery.Where((detail, contract) =>
  1272 + detail.PaymentMonth >= monthStart.Value &&
  1273 + detail.PaymentMonth <= monthEnd.Value);
  1274 + }
  1275 +
  1276 + // 如果指定了分类,则添加分类过滤条件
  1277 + var details = await detailsQuery
1238 .WhereIF(input.Categories != null && input.Categories.Length > 0, 1278 .WhereIF(input.Categories != null && input.Categories.Length > 0,
1239 (detail, contract) => contract.Category != null && input.Categories.Contains(contract.Category)) 1279 (detail, contract) => contract.Category != null && input.Categories.Contains(contract.Category))
1240 .Select((detail, contract) => new 1280 .Select((detail, contract) => new
@@ -1289,8 +1329,8 @@ namespace NCC.Extend @@ -1289,8 +1329,8 @@ namespace NCC.Extend
1289 { 1329 {
1290 storeId = input.StoreId, 1330 storeId = input.StoreId,
1291 storeName = store.Dm ?? "", 1331 storeName = store.Dm ?? "",
1292 - year = input.Year,  
1293 - month = input.Month, 1332 + year = input.Year ?? 0,
  1333 + month = input.Month ?? 0,
1294 totalAmount = totalAmount, 1334 totalAmount = totalAmount,
1295 categoryDetails = categoryDetails 1335 categoryDetails = categoryDetails
1296 }; 1336 };
netcore/src/Modularity/Extend/NCC.Extend/LqCooperationCostService.cs
@@ -121,6 +121,7 @@ namespace NCC.Extend.LqCooperationCost @@ -121,6 +121,7 @@ namespace NCC.Extend.LqCooperationCost
121 month = x.Month, 121 month = x.Month,
122 totalAmount = x.TotalAmount, 122 totalAmount = x.TotalAmount,
123 remarks = x.Remarks, 123 remarks = x.Remarks,
  124 + costType = x.CostType,
124 createUser = x.CreateUser, 125 createUser = x.CreateUser,
125 createTime = x.CreateTime, 126 createTime = x.CreateTime,
126 updateUser = x.UpdateUser, 127 updateUser = x.UpdateUser,
@@ -185,6 +186,7 @@ namespace NCC.Extend.LqCooperationCost @@ -185,6 +186,7 @@ namespace NCC.Extend.LqCooperationCost
185 month = x.Month, 186 month = x.Month,
186 totalAmount = x.TotalAmount, 187 totalAmount = x.TotalAmount,
187 remarks = x.Remarks, 188 remarks = x.Remarks,
  189 + costType = x.CostType,
188 createUser = x.CreateUser, 190 createUser = x.CreateUser,
189 createTime = x.CreateTime, 191 createTime = x.CreateTime,
190 updateUser = x.UpdateUser, 192 updateUser = x.UpdateUser,
@@ -243,6 +245,7 @@ namespace NCC.Extend.LqCooperationCost @@ -243,6 +245,7 @@ namespace NCC.Extend.LqCooperationCost
243 entity.Month = input.month; 245 entity.Month = input.month;
244 entity.TotalAmount = input.totalAmount; 246 entity.TotalAmount = input.totalAmount;
245 entity.Remarks = input.remarks; 247 entity.Remarks = input.remarks;
  248 + entity.CostType = input.costType;
246 entity.UpdateUser = _userManager.UserId; 249 entity.UpdateUser = _userManager.UserId;
247 entity.UpdateTime = DateTime.Now; 250 entity.UpdateTime = DateTime.Now;
248 251
@@ -301,7 +304,7 @@ namespace NCC.Extend.LqCooperationCost @@ -301,7 +304,7 @@ namespace NCC.Extend.LqCooperationCost
301 { 304 {
302 exportData = await this.GetNoPagingList(input); 305 exportData = await this.GetNoPagingList(input);
303 } 306 }
304 - List<ParamsModel> paramList = "[{\"value\":\"门店ID\",\"field\":\"storeId\"},{\"value\":\"门店名称\",\"field\":\"storeName\"},{\"value\":\"年份\",\"field\":\"year\"},{\"value\":\"月份\",\"field\":\"month\"},{\"value\":\"合计金额\",\"field\":\"totalAmount\"},{\"value\":\"备注说明\",\"field\":\"remarks\"},{\"value\":\"创建人\",\"field\":\"createUser\"},{\"value\":\"创建时间\",\"field\":\"createTime\"},]".ToList<ParamsModel>(); 307 + List<ParamsModel> paramList = "[{\"value\":\"门店名称\",\"field\":\"storeName\"},{\"value\":\"年份\",\"field\":\"year\"},{\"value\":\"月份\",\"field\":\"month\"},{\"value\":\"合计金额\",\"field\":\"totalAmount\"},{\"value\":\"成本类型\",\"field\":\"costType\"},{\"value\":\"备注说明\",\"field\":\"remarks\"},{\"value\":\"创建人\",\"field\":\"createUser\"},{\"value\":\"创建时间\",\"field\":\"createTime\"},]".ToList<ParamsModel>();
305 ExcelConfig excelconfig = new ExcelConfig(); 308 ExcelConfig excelconfig = new ExcelConfig();
306 excelconfig.FileName = "合作成本表.xls"; 309 excelconfig.FileName = "合作成本表.xls";
307 excelconfig.HeadFont = "微软雅黑"; 310 excelconfig.HeadFont = "微软雅黑";
@@ -329,25 +332,89 @@ namespace NCC.Extend.LqCooperationCost @@ -329,25 +332,89 @@ namespace NCC.Extend.LqCooperationCost
329 } 332 }
330 333
331 /// <summary> 334 /// <summary>
  335 + /// 下载导入模板
  336 + /// </summary>
  337 + /// <remarks>
  338 + /// 下载合作成本表导入模板Excel文件
  339 + ///
  340 + /// Excel格式:
  341 + /// 第一行为标题行:门店名称、年份、月份、合计金额、成本类型、备注说明
  342 + /// </remarks>
  343 + /// <returns>模板文件下载信息</returns>
  344 + [HttpGet("Actions/TemplateDownload")]
  345 + public dynamic TemplateDownload()
  346 + {
  347 + try
  348 + {
  349 + // 创建模板数据(只有标题行)
  350 + var templateData = new List<Dictionary<string, object>>
  351 + {
  352 + new Dictionary<string, object>
  353 + {
  354 + { "storeName", "" },
  355 + { "year", "" },
  356 + { "month", "" },
  357 + { "totalAmount", "" },
  358 + { "costType", "" },
  359 + { "remarks", "" }
  360 + }
  361 + };
  362 +
  363 + ExcelConfig excelconfig = new ExcelConfig();
  364 + excelconfig.FileName = "合作成本表模板.xlsx";
  365 + excelconfig.HeadFont = "微软雅黑";
  366 + excelconfig.HeadPoint = 10;
  367 + excelconfig.IsAllSizeColumn = true;
  368 + excelconfig.ColumnModel = new List<ExcelColumnModel>
  369 + {
  370 + new ExcelColumnModel { Column = "storeName", ExcelColumn = "门店名称" },
  371 + new ExcelColumnModel { Column = "year", ExcelColumn = "年份" },
  372 + new ExcelColumnModel { Column = "month", ExcelColumn = "月份" },
  373 + new ExcelColumnModel { Column = "totalAmount", ExcelColumn = "合计金额" },
  374 + new ExcelColumnModel { Column = "costType", ExcelColumn = "成本类型" },
  375 + new ExcelColumnModel { Column = "remarks", ExcelColumn = "备注说明" }
  376 + };
  377 +
  378 + var addPath = FileVariable.TemporaryFilePath + excelconfig.FileName;
  379 + var columnList = new List<string> { "门店名称", "年份", "月份", "合计金额", "成本类型", "备注说明" };
  380 + ExcelExportHelper<Dictionary<string, object>>.Export(templateData, excelconfig, addPath, columnList);
  381 +
  382 + var fileName = _userManager.UserId + "|" + addPath + "|xlsx";
  383 + return new
  384 + {
  385 + name = excelconfig.FileName,
  386 + url = "/api/File/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC")
  387 + };
  388 + }
  389 + catch (Exception ex)
  390 + {
  391 + throw NCCException.Oh($"生成模板失败:{ex.Message}");
  392 + }
  393 + }
  394 +
  395 + /// <summary>
332 /// 导入合作成本数据 396 /// 导入合作成本数据
333 /// </summary> 397 /// </summary>
334 /// <remarks> 398 /// <remarks>
335 /// 从Excel文件导入合作成本数据 399 /// 从Excel文件导入合作成本数据
336 /// 400 ///
337 /// Excel格式要求: 401 /// Excel格式要求:
338 - /// 第一行为标题行:门店ID、门店名称、年份、月份、合计金额、备注说明 402 + /// 第一行为标题行:门店名称、年份、月份、合计金额、成本类型、备注说明
339 /// 从第二行开始为数据行 403 /// 从第二行开始为数据行
340 /// 404 ///
  405 + /// 注意:导入时通过门店名称查找门店ID,不需要填写门店ID
  406 + ///
341 /// 示例请求: 407 /// 示例请求:
342 /// POST /api/Extend/LqCooperationCost/Actions/Import 408 /// POST /api/Extend/LqCooperationCost/Actions/Import
343 /// Content-Type: multipart/form-data 409 /// Content-Type: multipart/form-data
344 /// </remarks> 410 /// </remarks>
345 /// <param name="file">Excel文件</param> 411 /// <param name="file">Excel文件</param>
  412 + /// <param name="clearBeforeImport">是否需要清理导入月份数据(默认:true,清理)</param>
346 /// <returns>导入结果</returns> 413 /// <returns>导入结果</returns>
347 /// <response code="200">导入成功</response> 414 /// <response code="200">导入成功</response>
348 /// <response code="400">文件格式错误或数据验证失败</response> 415 /// <response code="400">文件格式错误或数据验证失败</response>
349 [HttpPost("Actions/Import")] 416 [HttpPost("Actions/Import")]
350 - public async Task<dynamic> Import(IFormFile file) 417 + public async Task<dynamic> Import(IFormFile file, bool clearBeforeImport = true)
351 { 418 {
352 try 419 try
353 { 420 {
@@ -367,6 +434,7 @@ namespace NCC.Extend.LqCooperationCost @@ -367,6 +434,7 @@ namespace NCC.Extend.LqCooperationCost
367 var successCount = 0; 434 var successCount = 0;
368 var failCount = 0; 435 var failCount = 0;
369 var failMessages = new List<string>(); 436 var failMessages = new List<string>();
  437 + var importData = new List<(string StoreId, int Year, string Month, decimal TotalAmount, string CostType, string Remarks)>();
370 438
371 // 保存临时文件 439 // 保存临时文件
372 var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); 440 var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName));
@@ -391,27 +459,43 @@ namespace NCC.Extend.LqCooperationCost @@ -391,27 +459,43 @@ namespace NCC.Extend.LqCooperationCost
391 try 459 try
392 { 460 {
393 var row = dataTable.Rows[i]; 461 var row = dataTable.Rows[i];
394 - var storeId = row[0]?.ToString()?.Trim();  
395 - var storeName = row[1]?.ToString()?.Trim();  
396 - var yearText = row[2]?.ToString()?.Trim();  
397 - var monthText = row[3]?.ToString()?.Trim();  
398 - var totalAmountText = row[4]?.ToString()?.Trim(); 462 + // Excel列顺序:门店名称、年份、月份、合计金额、成本类型、备注说明
  463 + var storeName = row[0]?.ToString()?.Trim();
  464 + var yearText = row[1]?.ToString()?.Trim();
  465 + var monthText = row[2]?.ToString()?.Trim();
  466 + var totalAmountText = row[3]?.ToString()?.Trim();
  467 + var costType = row[4]?.ToString()?.Trim();
399 var remarks = row[5]?.ToString()?.Trim(); 468 var remarks = row[5]?.ToString()?.Trim();
400 469
401 // 跳过空行 470 // 跳过空行
402 - if (string.IsNullOrEmpty(storeId) && string.IsNullOrEmpty(storeName)) 471 + if (string.IsNullOrEmpty(storeName))
  472 + {
  473 + continue;
  474 + }
  475 +
  476 + // 验证必填字段:门店名称
  477 + if (string.IsNullOrEmpty(storeName))
403 { 478 {
  479 + failMessages.Add($"第{i + 1}行:门店名称不能为空");
  480 + failCount++;
404 continue; 481 continue;
405 } 482 }
406 483
407 - // 验证必填字段  
408 - if (string.IsNullOrEmpty(storeId)) 484 + // 根据门店名称查找门店ID
  485 + var store = await _db.Queryable<LqMdxxEntity>()
  486 + .Where(x => x.Dm == storeName)
  487 + .FirstAsync();
  488 +
  489 + if (store == null)
409 { 490 {
410 - failMessages.Add($"第{i + 1}行:门店ID不能为空"); 491 + failMessages.Add($"第{i + 1}行:找不到门店名称'{storeName}'对应的门店");
411 failCount++; 492 failCount++;
412 continue; 493 continue;
413 } 494 }
414 495
  496 + var storeId = store.Id;
  497 +
  498 + // 验证年份
415 if (string.IsNullOrEmpty(yearText) || !int.TryParse(yearText, out int year)) 499 if (string.IsNullOrEmpty(yearText) || !int.TryParse(yearText, out int year))
416 { 500 {
417 failMessages.Add($"第{i + 1}行:年份格式错误(应为数字)"); 501 failMessages.Add($"第{i + 1}行:年份格式错误(应为数字)");
@@ -419,6 +503,7 @@ namespace NCC.Extend.LqCooperationCost @@ -419,6 +503,7 @@ namespace NCC.Extend.LqCooperationCost
419 continue; 503 continue;
420 } 504 }
421 505
  506 + // 验证月份
422 if (string.IsNullOrEmpty(monthText) || monthText.Length != 6) 507 if (string.IsNullOrEmpty(monthText) || monthText.Length != 6)
423 { 508 {
424 failMessages.Add($"第{i + 1}行:月份格式错误(应为YYYYMM格式,如:202501)"); 509 failMessages.Add($"第{i + 1}行:月份格式错误(应为YYYYMM格式,如:202501)");
@@ -426,6 +511,7 @@ namespace NCC.Extend.LqCooperationCost @@ -426,6 +511,7 @@ namespace NCC.Extend.LqCooperationCost
426 continue; 511 continue;
427 } 512 }
428 513
  514 + // 验证合计金额
429 if (string.IsNullOrEmpty(totalAmountText) || !decimal.TryParse(totalAmountText, out decimal totalAmount)) 515 if (string.IsNullOrEmpty(totalAmountText) || !decimal.TryParse(totalAmountText, out decimal totalAmount))
430 { 516 {
431 failMessages.Add($"第{i + 1}行:合计金额格式错误(应为数字)"); 517 failMessages.Add($"第{i + 1}行:合计金额格式错误(应为数字)");
@@ -433,53 +519,14 @@ namespace NCC.Extend.LqCooperationCost @@ -433,53 +519,14 @@ namespace NCC.Extend.LqCooperationCost
433 continue; 519 continue;
434 } 520 }
435 521
436 - // 如果未提供门店名称,根据门店ID查询  
437 - if (string.IsNullOrEmpty(storeName))  
438 - {  
439 - var store = await _db.Queryable<LqMdxxEntity>()  
440 - .Where(x => x.Id == storeId)  
441 - .Select(x => x.Dm)  
442 - .FirstAsync();  
443 - storeName = store ?? "";  
444 - } 522 + // 注意:一个门店在同一个月份可以有多笔成本记录(包括相同成本类型、相同金额)
  523 + // 因为业务上可能需要记录多笔成本,比如多次合作项目、多次设备维护等
  524 + // 统计时会按门店汇总所有记录的金额,所以不需要做重复检查
  525 + // 如果用户需要避免重复导入,可以使用"清理导入月份数据"功能
445 526
446 - // 检查是否已存在相同门店、年份、月份的记录  
447 - var exists = await _db.Queryable<LqCooperationCostEntity>()  
448 - .Where(x => x.StoreId == storeId && x.Year == year && x.Month == monthText && x.IsEffective == StatusEnum.有效.GetHashCode())  
449 - .AnyAsync();  
450 -  
451 - if (exists)  
452 - {  
453 - failMessages.Add($"第{i + 1}行:该门店{year}年{monthText}月的记录已存在");  
454 - failCount++;  
455 - continue;  
456 - }  
457 -  
458 - // 创建记录  
459 - var entity = new LqCooperationCostEntity  
460 - {  
461 - Id = YitIdHelper.NextId().ToString(),  
462 - StoreId = storeId,  
463 - StoreName = storeName,  
464 - Year = year,  
465 - Month = monthText,  
466 - TotalAmount = totalAmount,  
467 - Remarks = remarks,  
468 - IsEffective = StatusEnum.有效.GetHashCode(),  
469 - CreateUser = _userManager.UserId,  
470 - CreateTime = DateTime.Now  
471 - };  
472 -  
473 - var isOk = await _db.Insertable(entity).ExecuteCommandAsync();  
474 - if (isOk > 0)  
475 - {  
476 - successCount++;  
477 - }  
478 - else  
479 - {  
480 - failMessages.Add($"第{i + 1}行:保存失败");  
481 - failCount++;  
482 - } 527 + // 添加到导入数据列表
  528 + importData.Add((storeId, year, monthText, totalAmount, costType, remarks));
  529 + successCount++;
483 } 530 }
484 catch (Exception ex) 531 catch (Exception ex)
485 { 532 {
@@ -497,13 +544,71 @@ namespace NCC.Extend.LqCooperationCost @@ -497,13 +544,71 @@ namespace NCC.Extend.LqCooperationCost
497 } 544 }
498 } 545 }
499 546
  547 + // 如果需要清理导入月份数据,先删除导入数据中所有涉及的年份+月份组合的数据
  548 + if (clearBeforeImport && importData.Any())
  549 + {
  550 + // 获取导入数据中所有唯一的年份+月份组合
  551 + var yearMonthPairs = importData
  552 + .Select(x => new { x.Year, x.Month })
  553 + .Distinct()
  554 + .ToList();
  555 +
  556 + foreach (var pair in yearMonthPairs)
  557 + {
  558 + await _db.Deleteable<LqCooperationCostEntity>()
  559 + .Where(x => x.Year == pair.Year && x.Month == pair.Month && x.IsEffective == StatusEnum.有效.GetHashCode())
  560 + .ExecuteCommandAsync();
  561 + }
  562 + }
  563 +
  564 + // 批量插入导入数据
  565 + if (importData.Any())
  566 + {
  567 + // 批量获取门店信息
  568 + var storeIds = importData.Select(x => x.StoreId).Distinct().ToList();
  569 + var stores = await _db.Queryable<LqMdxxEntity>()
  570 + .Where(x => storeIds.Contains(x.Id))
  571 + .Select(x => new { x.Id, x.Dm })
  572 + .ToListAsync();
  573 + var storeDict = stores.ToDictionary(x => x.Id, x => x.Dm);
  574 +
  575 + var entities = importData.Select(x => new LqCooperationCostEntity
  576 + {
  577 + Id = YitIdHelper.NextId().ToString(),
  578 + StoreId = x.StoreId,
  579 + StoreName = storeDict.ContainsKey(x.StoreId) ? storeDict[x.StoreId] : "",
  580 + Year = x.Year,
  581 + Month = x.Month,
  582 + TotalAmount = x.TotalAmount,
  583 + CostType = x.CostType,
  584 + Remarks = x.Remarks,
  585 + IsEffective = StatusEnum.有效.GetHashCode(),
  586 + CreateUser = _userManager.UserId,
  587 + CreateTime = DateTime.Now
  588 + }).ToList();
  589 +
  590 + await _db.Insertable(entities).ExecuteCommandAsync();
  591 + }
  592 +
  593 + // 构建返回结果,包含成功导入的记录的成本类型信息
  594 + var successRecords = importData.Select(x => new
  595 + {
  596 + storeId = x.StoreId,
  597 + year = x.Year,
  598 + month = x.Month,
  599 + totalAmount = x.TotalAmount,
  600 + costType = x.CostType,
  601 + remarks = x.Remarks
  602 + }).ToList();
  603 +
500 return new 604 return new
501 { 605 {
502 success = true, 606 success = true,
503 message = $"导入完成:成功{successCount}条,失败{failCount}条", 607 message = $"导入完成:成功{successCount}条,失败{failCount}条",
504 successCount = successCount, 608 successCount = successCount,
505 failCount = failCount, 609 failCount = failCount,
506 - failMessages = failMessages 610 + failMessages = failMessages,
  611 + successRecords = successRecords
507 }; 612 };
508 } 613 }
509 catch (Exception ex) 614 catch (Exception ex)
@@ -514,3 +619,4 @@ namespace NCC.Extend.LqCooperationCost @@ -514,3 +619,4 @@ namespace NCC.Extend.LqCooperationCost
514 } 619 }
515 } 620 }
516 621
  622 +
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
@@ -3802,11 +3802,11 @@ namespace NCC.Extend.LqKdKdjlb @@ -3802,11 +3802,11 @@ namespace NCC.Extend.LqKdKdjlb
3802 { 3802 {
3803 var billings = await _db.Queryable<LqKdKdjlbEntity>() 3803 var billings = await _db.Queryable<LqKdKdjlbEntity>()
3804 .Where(x => billingIds.Contains(x.Id)) 3804 .Where(x => billingIds.Contains(x.Id))
3805 - .Select(x => new { x.Id, x.Djmd, x.Hgjg, x.Fkyy }) 3805 + .Select(x => new { x.Id, x.Djmd, x.Hgjg, x.Fkyy, x.Fkfs })
3806 .ToListAsync(); 3806 .ToListAsync();
3807 3807
3808 billingStoreDict = billings.ToDictionary(x => x.Id, x => x.Djmd ?? ""); 3808 billingStoreDict = billings.ToDictionary(x => x.Id, x => x.Djmd ?? "");
3809 - billingExtraInfoDict = billings.ToDictionary(x => x.Id, x => (dynamic)new { Hgjg = x.Hgjg, Fkyy = x.Fkyy }); 3809 + billingExtraInfoDict = billings.ToDictionary(x => x.Id, x => (dynamic)new { Hgjg = x.Hgjg, Fkyy = x.Fkyy, Fkfs = x.Fkfs });
3810 } 3810 }
3811 3811
3812 // 批量查询门店信息 3812 // 批量查询门店信息
@@ -3836,7 +3836,8 @@ namespace NCC.Extend.LqKdKdjlb @@ -3836,7 +3836,8 @@ namespace NCC.Extend.LqKdKdjlb
3836 storeId = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) ? billingStoreDict[pxmx.Glkdbh] : "", 3836 storeId = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) ? billingStoreDict[pxmx.Glkdbh] : "",
3837 storeName = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) && !string.IsNullOrEmpty(billingStoreDict[pxmx.Glkdbh]) && storeDict.ContainsKey(billingStoreDict[pxmx.Glkdbh]) ? storeDict[billingStoreDict[pxmx.Glkdbh]] : "", 3837 storeName = pxmx.Glkdbh != null && billingStoreDict.ContainsKey(pxmx.Glkdbh) && !string.IsNullOrEmpty(billingStoreDict[pxmx.Glkdbh]) && storeDict.ContainsKey(billingStoreDict[pxmx.Glkdbh]) ? storeDict[billingStoreDict[pxmx.Glkdbh]] : "",
3838 hgjg = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Hgjg : "", 3838 hgjg = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Hgjg : "",
3839 - fkyy = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Fkyy : "" 3839 + fkyy = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Fkyy : "",
  3840 + paymentMethod = pxmx.Glkdbh != null && billingExtraInfoDict.ContainsKey(pxmx.Glkdbh) ? billingExtraInfoDict[pxmx.Glkdbh].Fkfs : ""
3840 }).ToList(); 3841 }).ToList();
3841 3842
3842 // 6. 返回分页结果 3843 // 6. 返回分页结果
netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs
@@ -57,9 +57,18 @@ namespace NCC.Extend @@ -57,9 +57,18 @@ namespace NCC.Extend
57 /// "productType": "毛巾", 57 /// "productType": "毛巾",
58 /// "laundrySupplierId": "清洗商ID", 58 /// "laundrySupplierId": "清洗商ID",
59 /// "quantity": 100, 59 /// "quantity": 100,
60 - /// "remark": "备注" 60 + /// "remark": "备注",
  61 + /// "sendTime": "2025-01-15T10:30:00" // 可选,如果不传则使用当前时间
61 /// } 62 /// }
62 /// ``` 63 /// ```
  64 + ///
  65 + /// 参数说明:
  66 + /// - storeId: 门店ID(必填)
  67 + /// - productType: 产品类型(必填)
  68 + /// - laundrySupplierId: 清洗商ID(必填)
  69 + /// - quantity: 送出数量(必填)
  70 + /// - remark: 备注(可选)
  71 + /// - sendTime: 送出时间(可选,如果不传则使用当前时间)
63 /// </remarks> 72 /// </remarks>
64 /// <param name="input">送出输入</param> 73 /// <param name="input">送出输入</param>
65 /// <returns>创建结果(包含批次号)</returns> 74 /// <returns>创建结果(包含批次号)</returns>
@@ -116,7 +125,7 @@ namespace NCC.Extend @@ -116,7 +125,7 @@ namespace NCC.Extend
116 IsEffective = StatusEnum.有效.GetHashCode(), 125 IsEffective = StatusEnum.有效.GetHashCode(),
117 CreateUser = _userManager.UserId, 126 CreateUser = _userManager.UserId,
118 CreateTime = DateTime.Now, 127 CreateTime = DateTime.Now,
119 - SendTime = DateTime.Now // 设置送出时间 128 + SendTime = input.SendTime ?? DateTime.Now // 如果传入了送出时间则使用,否则使用当前时间
120 }; 129 };
121 130
122 var isOk = await _db.Insertable(entity).ExecuteCommandAsync(); 131 var isOk = await _db.Insertable(entity).ExecuteCommandAsync();
@@ -145,9 +154,17 @@ namespace NCC.Extend @@ -145,9 +154,17 @@ namespace NCC.Extend
145 /// "batchNumber": "批次号(对应的送出记录的ID)", 154 /// "batchNumber": "批次号(对应的送出记录的ID)",
146 /// "laundrySupplierId": "清洗商ID", 155 /// "laundrySupplierId": "清洗商ID",
147 /// "quantity": 95, 156 /// "quantity": 95,
148 - /// "remark": "5条损坏" 157 + /// "remark": "5条损坏",
  158 + /// "returnTime": "2025-01-20T14:30:00" // 可选,如果不传则使用当前时间
149 /// } 159 /// }
150 /// ``` 160 /// ```
  161 + ///
  162 + /// 参数说明:
  163 + /// - batchNumber: 批次号(必填)
  164 + /// - laundrySupplierId: 清洗商ID(必填)
  165 + /// - quantity: 送回数量(必填)
  166 + /// - remark: 备注(可选)
  167 + /// - returnTime: 送回时间(可选,如果不传则使用当前时间)
151 /// </remarks> 168 /// </remarks>
152 /// <param name="input">送回输入</param> 169 /// <param name="input">送回输入</param>
153 /// <returns>创建结果</returns> 170 /// <returns>创建结果</returns>
@@ -214,7 +231,7 @@ namespace NCC.Extend @@ -214,7 +231,7 @@ namespace NCC.Extend
214 IsEffective = StatusEnum.有效.GetHashCode(), 231 IsEffective = StatusEnum.有效.GetHashCode(),
215 CreateUser = _userManager.UserId, 232 CreateUser = _userManager.UserId,
216 CreateTime = DateTime.Now, 233 CreateTime = DateTime.Now,
217 - ReturnTime = DateTime.Now // 设置送回时间 234 + ReturnTime = input.ReturnTime ?? DateTime.Now // 如果传入了送回时间则使用,否则使用当前时间
218 }; 235 };
219 236
220 var isOk = await _db.Insertable(entity).ExecuteCommandAsync(); 237 var isOk = await _db.Insertable(entity).ExecuteCommandAsync();
netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs
@@ -7,7 +7,10 @@ using NCC.DynamicApiController; @@ -7,7 +7,10 @@ using NCC.DynamicApiController;
7 using NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary; 7 using NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary;
8 using NCC.Extend.Entitys.lq_attendance_summary; 8 using NCC.Extend.Entitys.lq_attendance_summary;
9 using NCC.Extend.Entitys.lq_hytk_hytk; 9 using NCC.Extend.Entitys.lq_hytk_hytk;
  10 +using NCC.Extend.Entitys.lq_hytk_mx;
10 using NCC.Extend.Entitys.lq_kd_kdjlb; 11 using NCC.Extend.Entitys.lq_kd_kdjlb;
  12 +using NCC.Extend.Entitys.lq_kd_pxmx;
  13 +using NCC.Extend.Entitys.lq_kd_jksyj;
11 using NCC.Extend.Entitys.lq_md_target; 14 using NCC.Extend.Entitys.lq_md_target;
12 using NCC.Extend.Entitys.lq_mdxx; 15 using NCC.Extend.Entitys.lq_mdxx;
13 using NCC.Extend.Entitys.lq_major_project_director_salary_statistics; 16 using NCC.Extend.Entitys.lq_major_project_director_salary_statistics;
@@ -214,19 +217,33 @@ namespace NCC.Extend @@ -214,19 +217,33 @@ namespace NCC.Extend
214 { 217 {
215 foreach (var storeId in allManagedStoreIds) 218 foreach (var storeId in allManagedStoreIds)
216 { 219 {
217 - // 该门店的开单金额  
218 - var storeBillingAmount = await _db.Queryable<LqKdKdjlbEntity>()  
219 - .Where(x => x.IsEffective == 1  
220 - && x.Djmd == storeId  
221 - && x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1))  
222 - .SumAsync(x => (decimal?)x.Sfyj) ?? 0m;  
223 -  
224 - // 该门店的退卡金额(优先使用ActualRefundAmount,如果没有则使用Tkje)  
225 - var storeRefundAmount = await _db.Queryable<LqHytkHytkEntity>()  
226 - .Where(x => x.IsEffective == 1  
227 - && x.Md == storeId  
228 - && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1))  
229 - .SumAsync(x => (decimal?)(x.ActualRefundAmount ?? x.Tkje ?? 0)) ?? 0m; 220 + // 该门店的开单金额(只统计医美类型的开单,从lq_kd_jksyj表统计医美品项的健康师业绩)
  221 + var storeBillingList = await _db.Queryable<LqKdJksyjEntity, LqKdKdjlbEntity>(
  222 + (jksyj, kdjlb) => new JoinQueryInfos(
  223 + JoinType.Inner, jksyj.Glkdbh == kdjlb.Id))
  224 + .Where((jksyj, kdjlb) =>
  225 + kdjlb.IsEffective == 1
  226 + && jksyj.IsEffective == 1
  227 + && kdjlb.Djmd == storeId
  228 + && kdjlb.Kdrq >= startDate && kdjlb.Kdrq <= endDate.AddDays(1)
  229 + && jksyj.ItemCategory == "医美"
  230 + && !string.IsNullOrEmpty(jksyj.Jksyj))
  231 + .Select((jksyj, kdjlb) => jksyj.Jksyj)
  232 + .ToListAsync();
  233 + var storeBillingAmount = storeBillingList
  234 + .Sum(x => decimal.TryParse(x, out var val) ? val : 0m);
  235 +
  236 + // 该门店的退卡金额(只统计医美类型的退卡,从lq_hytk_mx表统计医美品项的退卡金额)
  237 + var storeRefundAmount = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity>(
  238 + (mx, hytk) => new JoinQueryInfos(
  239 + JoinType.Inner, mx.RefundInfoId == hytk.Id))
  240 + .Where((mx, hytk) =>
  241 + hytk.IsEffective == 1
  242 + && mx.IsEffective == 1
  243 + && hytk.Md == storeId
  244 + && hytk.Tksj >= startDate && hytk.Tksj <= endDate.AddDays(1)
  245 + && mx.ItemCategory == "医美")
  246 + .SumAsync((mx, hytk) => (decimal?)(mx.Tkje ?? 0)) ?? 0m;
230 247
231 // 该门店的净总业绩 248 // 该门店的净总业绩
232 var storeTotalPerformance = storeBillingAmount - storeRefundAmount; 249 var storeTotalPerformance = storeBillingAmount - storeRefundAmount;
netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectTeacherSalaryService.cs
@@ -7,7 +7,10 @@ using NCC.DynamicApiController; @@ -7,7 +7,10 @@ using NCC.DynamicApiController;
7 using NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary; 7 using NCC.Extend.Entitys.Dto.LqMajorProjectTeacherSalary;
8 using NCC.Extend.Entitys.lq_attendance_summary; 8 using NCC.Extend.Entitys.lq_attendance_summary;
9 using NCC.Extend.Entitys.lq_hytk_hytk; 9 using NCC.Extend.Entitys.lq_hytk_hytk;
  10 +using NCC.Extend.Entitys.lq_hytk_mx;
10 using NCC.Extend.Entitys.lq_kd_kdjlb; 11 using NCC.Extend.Entitys.lq_kd_kdjlb;
  12 +using NCC.Extend.Entitys.lq_kd_pxmx;
  13 +using NCC.Extend.Entitys.lq_kd_jksyj;
11 using NCC.Extend.Entitys.lq_md_major_project_teacher_assignment; 14 using NCC.Extend.Entitys.lq_md_major_project_teacher_assignment;
12 using NCC.Extend.Entitys.lq_md_xdbhsj; 15 using NCC.Extend.Entitys.lq_md_xdbhsj;
13 using NCC.Extend.Entitys.lq_mdxx; 16 using NCC.Extend.Entitys.lq_mdxx;
@@ -182,25 +185,40 @@ namespace NCC.Extend @@ -182,25 +185,40 @@ namespace NCC.Extend
182 .ToDictionary(g => g.Key, g => g.First()); 185 .ToDictionary(g => g.Key, g => g.First());
183 186
184 // 1.4 门店总业绩计算 (开单实付 - 退卡金额) 187 // 1.4 门店总业绩计算 (开单实付 - 退卡金额)
185 - // 开单实付(从lq_kd_kdjlb表统计sfyj字段)  
186 - var storeBillingList = await _db.Queryable<LqKdKdjlbEntity>()  
187 - .Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1)  
188 - .Select(x => new { x.Djmd, x.Sfyj }) 188 + // 开单实付(只统计医美类型的开单,从lq_kd_jksyj表统计医美品项的健康师业绩)
  189 + var storeBillingList = await _db.Queryable<LqKdJksyjEntity, LqKdKdjlbEntity>(
  190 + (jksyj, kdjlb) => new JoinQueryInfos(
  191 + JoinType.Inner, jksyj.Glkdbh == kdjlb.Id))
  192 + .Where((jksyj, kdjlb) =>
  193 + kdjlb.Kdrq >= startDate &&
  194 + kdjlb.Kdrq <= endDate.AddDays(1) &&
  195 + kdjlb.IsEffective == 1 &&
  196 + jksyj.IsEffective == 1 &&
  197 + jksyj.ItemCategory == "医美" &&
  198 + !string.IsNullOrEmpty(jksyj.Jksyj))
  199 + .Select((jksyj, kdjlb) => new { kdjlb.Djmd, Jksyj = jksyj.Jksyj })
189 .ToListAsync(); 200 .ToListAsync();
190 var storeBillingDict = storeBillingList 201 var storeBillingDict = storeBillingList
191 .Where(x => !string.IsNullOrEmpty(x.Djmd)) 202 .Where(x => !string.IsNullOrEmpty(x.Djmd))
192 .GroupBy(x => x.Djmd) 203 .GroupBy(x => x.Djmd)
193 - .ToDictionary(g => g.Key, g => g.Sum(x => x.Sfyj));  
194 -  
195 - // 退卡金额(从lq_hytk_hytk表统计,使用F_ActualRefundAmount,如果没有则使用tkje)  
196 - var storeRefundList = await _db.Queryable<LqHytkHytkEntity>()  
197 - .Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1)  
198 - .Select(x => new { x.Md, x.ActualRefundAmount, x.Tkje }) 204 + .ToDictionary(g => g.Key, g => g.Sum(x => decimal.TryParse(x.Jksyj, out var val) ? val : 0m));
  205 +
  206 + // 退卡金额(只统计医美类型的退卡,从lq_hytk_mx表统计医美品项的退卡金额)
  207 + var storeRefundList = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity>(
  208 + (mx, hytk) => new JoinQueryInfos(
  209 + JoinType.Inner, mx.RefundInfoId == hytk.Id))
  210 + .Where((mx, hytk) =>
  211 + hytk.Tksj >= startDate &&
  212 + hytk.Tksj <= endDate.AddDays(1) &&
  213 + hytk.IsEffective == 1 &&
  214 + mx.IsEffective == 1 &&
  215 + mx.ItemCategory == "医美")
  216 + .Select((mx, hytk) => new { hytk.Md, mx.Tkje })
199 .ToListAsync(); 217 .ToListAsync();
200 var storeRefundDict = storeRefundList 218 var storeRefundDict = storeRefundList
201 .Where(x => !string.IsNullOrEmpty(x.Md)) 219 .Where(x => !string.IsNullOrEmpty(x.Md))
202 .GroupBy(x => x.Md) 220 .GroupBy(x => x.Md)
203 - .ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? x.Tkje ?? 0)); 221 + .ToDictionary(g => g.Key, g => g.Sum(x => x.Tkje ?? 0));
204 222
205 // 1.5 考勤数据 (lq_attendance_summary) 223 // 1.5 考勤数据 (lq_attendance_summary)
206 var attendanceList = await _db.Queryable<LqAttendanceSummaryEntity>() 224 var attendanceList = await _db.Queryable<LqAttendanceSummaryEntity>()
@@ -212,10 +230,18 @@ namespace NCC.Extend @@ -212,10 +230,18 @@ namespace NCC.Extend
212 var teacherIds = assignmentList.Select(x => x.TeacherId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); 230 var teacherIds = assignmentList.Select(x => x.TeacherId).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
213 var userList = await _db.Queryable<UserEntity>() 231 var userList = await _db.Queryable<UserEntity>()
214 .Where(x => teacherIds.Contains(x.Id)) 232 .Where(x => teacherIds.Contains(x.Id))
215 - .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob }) 233 + .Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob, x.OrganizeId })
216 .ToListAsync(); 234 .ToListAsync();
217 var userDict = userList.ToDictionary(x => x.Id, x => x); 235 var userDict = userList.ToDictionary(x => x.Id, x => x);
218 236
  237 + // 1.7 获取部门信息 (BASE_ORGANIZE)
  238 + var organizeIds = userList.Where(x => !string.IsNullOrEmpty(x.OrganizeId)).Select(x => x.OrganizeId).Distinct().ToList();
  239 + var organizeList = await _db.Queryable<OrganizeEntity>()
  240 + .Where(x => organizeIds.Contains(x.Id))
  241 + .Select(x => new { x.Id, x.FullName })
  242 + .ToListAsync();
  243 + var organizeDict = organizeList.ToDictionary(x => x.Id, x => x);
  244 +
219 // 2. 按大项目部老师聚合数据 245 // 2. 按大项目部老师聚合数据
220 var teacherStats = new Dictionary<string, LqMajorProjectTeacherSalaryStatisticsEntity>(); 246 var teacherStats = new Dictionary<string, LqMajorProjectTeacherSalaryStatisticsEntity>();
221 247
@@ -268,35 +294,29 @@ namespace NCC.Extend @@ -268,35 +294,29 @@ namespace NCC.Extend
268 salary.EmployeeName = user.RealName ?? ""; 294 salary.EmployeeName = user.RealName ?? "";
269 salary.EmployeeAccount = user.Account ?? ""; 295 salary.EmployeeAccount = user.Account ?? "";
270 salary.IsTerminated = user.IsOnJob == 0 ? 1 : 0; 296 salary.IsTerminated = user.IsOnJob == 0 ? 1 : 0;
271 - }  
272 297
273 - // 2.3 填充门店信息  
274 - if (storeDict.ContainsKey(storeId))  
275 - {  
276 - var store = storeDict[storeId];  
277 - salary.StoreId = storeId;  
278 - salary.StoreName = store.Dm ?? "";  
279 - salary.StoreType = store.StoreType;  
280 - salary.StoreCategory = store.StoreCategory; 298 + // 2.3 填充部门信息(使用StoreId和StoreName字段存储部门信息)
  299 + if (!string.IsNullOrEmpty(user.OrganizeId) && organizeDict.ContainsKey(user.OrganizeId))
  300 + {
  301 + var organize = organizeDict[user.OrganizeId];
  302 + salary.StoreId = user.OrganizeId; // 存储部门ID
  303 + salary.StoreName = organize.FullName ?? ""; // 存储部门名称
  304 + }
  305 + else
  306 + {
  307 + salary.StoreId = user.OrganizeId ?? "";
  308 + salary.StoreName = "";
  309 + }
281 } 310 }
282 else 311 else
283 { 312 {
284 - salary.StoreId = storeId; 313 + salary.StoreId = "";
285 salary.StoreName = ""; 314 salary.StoreName = "";
286 } 315 }
287 316
288 - // 2.4 新店保护信息  
289 - if (!string.IsNullOrEmpty(salary.StoreId) && newStoreProtectionDict.ContainsKey(salary.StoreId))  
290 - {  
291 - var protection = newStoreProtectionDict[salary.StoreId];  
292 - salary.IsNewStore = "是";  
293 - salary.NewStoreProtectionStage = protection.Stage;  
294 - }  
295 - else  
296 - {  
297 - salary.IsNewStore = "否";  
298 - salary.NewStoreProtectionStage = 0;  
299 - } 317 + // 2.4 新店保护信息(不再使用,因为现在存储的是部门信息)
  318 + salary.IsNewStore = "否";
  319 + salary.NewStoreProtectionStage = 0;
300 320
301 // 2.5 统计门店总业绩(开单业绩 - 退卡业绩) 321 // 2.5 统计门店总业绩(开单业绩 - 退卡业绩)
302 var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0; 322 var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs
@@ -106,9 +106,10 @@ namespace NCC.Extend @@ -106,9 +106,10 @@ namespace NCC.Extend
106 /// 从Excel导入健康师工资额外计算数据 106 /// 从Excel导入健康师工资额外计算数据
107 /// </summary> 107 /// </summary>
108 /// <param name="file">Excel文件</param> 108 /// <param name="file">Excel文件</param>
  109 + /// <param name="clearBeforeImport">是否需要清理导入月份数据(默认:true,清理)</param>
109 /// <returns>导入结果</returns> 110 /// <returns>导入结果</returns>
110 [HttpPost("ImportFromExcel")] 111 [HttpPost("ImportFromExcel")]
111 - public async Task<dynamic> ImportFromExcel(IFormFile file) 112 + public async Task<dynamic> ImportFromExcel(IFormFile file, bool clearBeforeImport = true)
112 { 113 {
113 try 114 try
114 { 115 {
@@ -277,6 +278,23 @@ namespace NCC.Extend @@ -277,6 +278,23 @@ namespace NCC.Extend
277 throw NCCException.Oh("Excel文件中没有有效的数据行"); 278 throw NCCException.Oh("Excel文件中没有有效的数据行");
278 } 279 }
279 280
  281 + // 如果需要清理导入月份数据,先删除导入数据中所有涉及的年份+月份组合的数据
  282 + if (clearBeforeImport)
  283 + {
  284 + // 获取导入数据中所有唯一的年份+月份组合
  285 + var yearMonthPairs = importData
  286 + .Select(x => new { x.Year, x.Month })
  287 + .Distinct()
  288 + .ToList();
  289 +
  290 + foreach (var pair in yearMonthPairs)
  291 + {
  292 + await _db.Deleteable<LqSalaryExtraCalculationEntity>()
  293 + .Where(x => x.Year == pair.Year && x.Month == pair.Month)
  294 + .ExecuteCommandAsync();
  295 + }
  296 + }
  297 +
280 // 处理导入数据 298 // 处理导入数据
281 return await ProcessImportData(importData); 299 return await ProcessImportData(importData);
282 } 300 }
netcore/src/Modularity/Extend/NCC.Extend/LqSalaryService.cs
@@ -327,18 +327,26 @@ namespace NCC.Extend @@ -327,18 +327,26 @@ namespace NCC.Extend
327 327
328 // 1.8 批量获取员工信息 (BASE_USER + BASE_POSITION) 328 // 1.8 批量获取员工信息 (BASE_USER + BASE_POSITION)
329 // 使用 allEmployeeIds 作为驱动,查询 BASE_USER 329 // 使用 allEmployeeIds 作为驱动,查询 BASE_USER
  330 + // 重要:只统计岗位为"健康师"的员工
330 var userList = await _db.Queryable<UserEntity>() 331 var userList = await _db.Queryable<UserEntity>()
331 - .Where(x => allEmployeeIds.Contains(x.Id)) 332 + .Where(x => allEmployeeIds.Contains(x.Id)
  333 + && x.Gw == "健康师" // 只统计岗位为"健康师"的员工
  334 + && x.DeleteMark == null
  335 + && x.EnabledMark == 1)
332 .Select(x => new { x.Id, x.RealName, x.PositionId, x.Mdid }) 336 .Select(x => new { x.Id, x.RealName, x.PositionId, x.Mdid })
333 .ToListAsync(); 337 .ToListAsync();
334 338
  339 + // 过滤出健康师ID列表
  340 + var healthCoachIds = userList.Select(x => x.Id).ToList();
  341 +
335 var userDict = userList.ToDictionary(x => x.Id, x => x); 342 var userDict = userList.ToDictionary(x => x.Id, x => x);
336 343
337 var positionIds = userList.Select(x => x.PositionId).Distinct().ToList(); 344 var positionIds = userList.Select(x => x.PositionId).Distinct().ToList();
338 var positionList = await _db.Queryable<PositionEntity>().Where(x => positionIds.Contains(x.Id)).ToListAsync(); 345 var positionList = await _db.Queryable<PositionEntity>().Where(x => positionIds.Contains(x.Id)).ToListAsync();
339 var positionLookup = positionList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x.FullName); 346 var positionLookup = positionList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x.FullName);
340 347
341 - foreach (var empId in allEmployeeIds) 348 + // 只处理健康师员工
  349 + foreach (var empId in allEmployeeIds.Where(x => healthCoachIds.Contains(x)))
342 { 350 {
343 var salary = new LqSalaryStatisticsEntity 351 var salary = new LqSalaryStatisticsEntity
344 { 352 {
@@ -671,6 +679,10 @@ namespace NCC.Extend @@ -671,6 +679,10 @@ namespace NCC.Extend
671 // 计算新客转化率提成和升单人头提成(根据新店阶段) 679 // 计算新客转化率提成和升单人头提成(根据新店阶段)
672 // isNewStore 和 newStoreStage 已在上面定义 680 // isNewStore 和 newStoreStage 已在上面定义
673 681
  682 + // 升单提点总是赋值(根据升单人头数)
  683 + decimal upgradeCommissionRate = GetUpgradeCommissionRate(salary.UpgradeCustomerCount);
  684 + salary.UpgradePoint = upgradeCommissionRate;
  685 +
674 if (isNewStore) 686 if (isNewStore)
675 { 687 {
676 if (newStoreStage == 1) 688 if (newStoreStage == 1)
@@ -695,10 +707,23 @@ namespace NCC.Extend @@ -695,10 +707,23 @@ namespace NCC.Extend
695 // 注意:这里需要重新判断是否是顾问,因为可能被降级了 707 // 注意:这里需要重新判断是否是顾问,因为可能被降级了
696 if (salary.Position == "顾问" && !string.IsNullOrEmpty(salary.GoldTriangleId)) 708 if (salary.Position == "顾问" && !string.IsNullOrEmpty(salary.GoldTriangleId))
697 { 709 {
698 - salary.ConsultantCommission = CalculateConsultantCommission(  
699 - salary.TeamPerformance,  
700 - employeeStats.Values.Where(x => x.GoldTriangleId == salary.GoldTriangleId).ToList(),  
701 - isNewStore); 710 + // 获取战队人数
  711 + var teamMemberList = employeeStats.Values.Where(x => x.GoldTriangleId == salary.GoldTriangleId).ToList();
  712 + var teamMemberCount = teamMemberList.Count;
  713 +
  714 + // 单人或者一个人的战队就没有顾问
  715 + if (teamMemberCount > 1)
  716 + {
  717 + salary.ConsultantCommission = CalculateConsultantCommission(
  718 + salary.TeamPerformance,
  719 + teamMemberList,
  720 + teamMemberCount,
  721 + isNewStore);
  722 + }
  723 + else
  724 + {
  725 + salary.ConsultantCommission = 0;
  726 + }
702 } 727 }
703 728
704 // 计算门店T区提成 729 // 计算门店T区提成
@@ -865,18 +890,25 @@ namespace NCC.Extend @@ -865,18 +890,25 @@ namespace NCC.Extend
865 /// <summary> 890 /// <summary>
866 /// 计算顾问提成 891 /// 计算顾问提成
867 /// </summary> 892 /// </summary>
868 - private decimal CalculateConsultantCommission(decimal teamPerformance, List<LqSalaryStatisticsEntity> teamMembers, bool isNewStore) 893 + /// <param name="teamPerformance">战队总业绩</param>
  894 + /// <param name="teamMembers">战队成员列表</param>
  895 + /// <param name="teamMemberCount">战队人数</param>
  896 + /// <param name="isNewStore">是否为新店</param>
  897 + /// <returns>顾问提成金额</returns>
  898 + private decimal CalculateConsultantCommission(decimal teamPerformance, List<LqSalaryStatisticsEntity> teamMembers, int teamMemberCount, bool isNewStore)
869 { 899 {
870 // 顾问提成规则: 900 // 顾问提成规则:
871 - // 高级顾问:战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8%  
872 - // 普通顾问:战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3% 901 + // 战队人数3人:战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8%
  902 + // 战队人数2人:战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3%
  903 + // 如果3人以上,就按照3人的规则来
  904 + // 单人或者一个人的战队就没有顾问(已在调用处处理)
873 905
874 // 注意: 906 // 注意:
875 // 1. "组员业绩"指除顾问外的其他成员业绩总和 907 // 1. "组员业绩"指除顾问外的其他成员业绩总和
876 // 2. 只统计有效战队成员(考勤≥20天,未被剔除的成员) 908 // 2. 只统计有效战队成员(考勤≥20天,未被剔除的成员)
877 // 3. "达到X%以上"指:组员业绩总和 ≥ 团队总业绩 × X% 909 // 3. "达到X%以上"指:组员业绩总和 ≥ 团队总业绩 × X%
878 // 4. 新店顾问不考核消耗 910 // 4. 新店顾问不考核消耗
879 - // 5. 消耗达标:高级顾问整组消耗>=6万,普通顾问整组消耗>=4万 911 + // 5. 消耗达标:3人战队整组消耗>=6万,2人战队整组消耗>=4万
880 912
881 // 使用传入的 teamMembers 计算总消耗 913 // 使用传入的 teamMembers 计算总消耗
882 var teamConsumption = teamMembers.Sum(x => x.Consumption); 914 var teamConsumption = teamMembers.Sum(x => x.Consumption);
@@ -884,28 +916,29 @@ namespace NCC.Extend @@ -884,28 +916,29 @@ namespace NCC.Extend
884 // 计算组员(非顾问)业绩总和 916 // 计算组员(非顾问)业绩总和
885 var memberPerformance = teamMembers.Where(x => x.Position != "顾问").Sum(x => x.TotalPerformance); 917 var memberPerformance = teamMembers.Where(x => x.Position != "顾问").Sum(x => x.TotalPerformance);
886 918
887 - // 高级顾问:业绩≥6万 且 组员业绩≥40% 且 (新店(第1,2阶段) 或 消耗≥6万)  
888 - // 注意:isNewStore 仅代表是否为新店,具体免考核阶段需确认。假设新店前两个阶段免考核,第三阶段需考核。  
889 - // 这里暂且沿用 isNewStore 逻辑,如果需要更细粒度控制,应传入 NewStoreProtectionStage  
890 - // 如果 isNewStore 为 true,则默认免考核消耗(根据原需求描述:新店第3个阶段时,有金三角,但是不考核消耗)  
891 - // 用户最新指示:新店第3个阶段时,有金三角,但是不考核消耗,默认达标 -> 意味着只要是新店,不管阶段,都不考核消耗  
892 -  
893 - if (teamPerformance >= 60000 && memberPerformance >= teamPerformance * 0.4m) 919 + // 3人及以上战队:业绩≥6万 且 组员业绩≥40% 且 (新店 或 消耗≥6万) → 0.8%
  920 + if (teamMemberCount >= 3)
894 { 921 {
895 - if (isNewStore || teamConsumption >= 60000) 922 + if (teamPerformance >= 60000 && memberPerformance >= teamPerformance * 0.4m)
896 { 923 {
897 - return teamPerformance * 0.008m; 924 + if (isNewStore || teamConsumption >= 60000)
  925 + {
  926 + return teamPerformance * 0.008m;
  927 + }
898 } 928 }
899 } 929 }
900 -  
901 - // 普通顾问:业绩≥4万 且 组员业绩≥30% 且 (新店 或 消耗≥4万)  
902 - if (teamPerformance >= 40000 && memberPerformance >= teamPerformance * 0.3m) 930 + // 2人战队:业绩≥4万 且 组员业绩≥30% 且 (新店 或 消耗≥4万) → 0.3%
  931 + else if (teamMemberCount == 2)
903 { 932 {
904 - if (isNewStore || teamConsumption >= 40000) 933 + if (teamPerformance >= 40000 && memberPerformance >= teamPerformance * 0.3m)
905 { 934 {
906 - return teamPerformance * 0.003m; 935 + if (isNewStore || teamConsumption >= 40000)
  936 + {
  937 + return teamPerformance * 0.003m;
  938 + }
907 } 939 }
908 } 940 }
  941 + // 1人战队:没有顾问,返回0(已在调用处处理,这里不会执行到)
909 942
910 return 0; 943 return 0;
911 } 944 }
@@ -926,16 +959,23 @@ namespace NCC.Extend @@ -926,16 +959,23 @@ namespace NCC.Extend
926 } 959 }
927 960
928 /// <summary> 961 /// <summary>
  962 + /// 获取升单提成比例
  963 + /// </summary>
  964 + private decimal GetUpgradeCommissionRate(decimal upgradeCustomerCount)
  965 + {
  966 + if (upgradeCustomerCount >= 10) return 0.12m; // 大于等于10:12%
  967 + else if (upgradeCustomerCount >= 7 && upgradeCustomerCount < 10) return 0.10m; // 大于等于7且小于10:10%
  968 + else if (upgradeCustomerCount >= 4 && upgradeCustomerCount < 7) return 0.07m; // 大于等于4且小于7:7%
  969 + // 小于4:0%
  970 + return 0m;
  971 + }
  972 +
  973 + /// <summary>
929 /// 计算升单人头提成 974 /// 计算升单人头提成
930 /// </summary> 975 /// </summary>
931 private decimal CalculateUpgradeCustomerCommission(decimal upgradePerformance, decimal upgradeCustomerCount) 976 private decimal CalculateUpgradeCustomerCommission(decimal upgradePerformance, decimal upgradeCustomerCount)
932 { 977 {
933 - decimal commissionRate = 0;  
934 -  
935 - if (upgradeCustomerCount >= 10) commissionRate = 0.20m;  
936 - else if (upgradeCustomerCount >= 4) commissionRate = 0.10m;  
937 - // 0-4个: 0%  
938 - 978 + decimal commissionRate = GetUpgradeCommissionRate(upgradeCustomerCount);
939 return upgradePerformance * commissionRate; 979 return upgradePerformance * commissionRate;
940 } 980 }
941 } 981 }
netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsHqService.cs
@@ -8,6 +8,8 @@ using NCC.Extend.Entitys.lq_kd_kdjlb; @@ -8,6 +8,8 @@ using NCC.Extend.Entitys.lq_kd_kdjlb;
8 using NCC.Extend.Entitys.lq_hytk_hytk; 8 using NCC.Extend.Entitys.lq_hytk_hytk;
9 using NCC.Extend.Entitys.lq_kd_pxmx; 9 using NCC.Extend.Entitys.lq_kd_pxmx;
10 using NCC.Extend.Entitys.lq_contract_rent_detail; 10 using NCC.Extend.Entitys.lq_contract_rent_detail;
  11 +using NCC.Extend.Entitys.lq_contract;
  12 +using NCC.Extend.Entitys;
11 using SqlSugar; 13 using SqlSugar;
12 using System; 14 using System;
13 using System.Linq; 15 using System.Linq;
@@ -151,23 +153,64 @@ namespace NCC.Extend @@ -151,23 +153,64 @@ namespace NCC.Extend
151 /// </summary> 153 /// </summary>
152 private async Task CalculateCost(LqShareStatisticsHqEntity entity, DateTime startDate, DateTime endDate, string statisticsMonth) 154 private async Task CalculateCost(LqShareStatisticsHqEntity entity, DateTime startDate, DateTime endDate, string statisticsMonth)
153 { 155 {
154 - // 1. 成本-报销 (TODO: 需要确认总部报销的判定方式)  
155 - entity.CostReimbursement = 0; 156 + // 1. 成本-报销:筛选一级分类为"总部费用"的申请,审批状态为"已通过"
  157 + var reimbursementAmount = await _db.Queryable<LqReimbursementApplicationEntity, LqPurchaseRecordsEntity, LqReimbursementCategoryEntity>(
  158 + (app, purchase, category) => app.PurchaseRecordsId == purchase.Id && purchase.ReimbursementCategoryId == category.Id)
  159 + .Where((app, purchase, category) =>
  160 + category.Level1Name == "总部费用"
  161 + && app.ApprovalStatus == "已通过"
  162 + && app.ApplicationTime >= startDate
  163 + && app.ApplicationTime <= endDate.AddDays(1))
  164 + .SumAsync((app, purchase, category) => purchase.Amount);
  165 + entity.CostReimbursement = reimbursementAmount;
156 166
157 // 2. 成本-人工 (保留) 167 // 2. 成本-人工 (保留)
158 entity.CostLabor = 0; 168 entity.CostLabor = 0;
159 169
160 // 3. 成本-教育部房租 170 // 3. 成本-教育部房租
161 - // TODO: 需要确认如何识别教育部合同  
162 - entity.CostEducationRent = 0; 171 + // 从合同月租明细表获取:门店是总部,分类是教育,统计月份匹配
  172 + // 使用月份字符串匹配,避免时区问题
  173 + var educationRent = await _db.Ado.SqlQuerySingleAsync<decimal>(
  174 + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0)
  175 + FROM lq_contract_rent_detail d
  176 + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id
  177 + WHERE c.F_StoreId = '1649328471923847168'
  178 + AND c.F_Category = '教育'
  179 + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth
  180 + AND c.F_IsEffective = 1
  181 + AND d.F_IsEffective = 1",
  182 + new { StatisticsMonth = statisticsMonth });
  183 + entity.CostEducationRent = educationRent;
163 184
164 // 4. 成本-仓库房租 185 // 4. 成本-仓库房租
165 - // TODO: 需要确认如何识别仓库合同  
166 - entity.CostWarehouseRent = 0; 186 + // 从合同月租明细表获取:门店是总部,分类是仓库,统计月份匹配
  187 + // 使用月份字符串匹配,避免时区问题
  188 + var warehouseRent = await _db.Ado.SqlQuerySingleAsync<decimal>(
  189 + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0)
  190 + FROM lq_contract_rent_detail d
  191 + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id
  192 + WHERE c.F_StoreId = '1649328471923847168'
  193 + AND c.F_Category = '仓库'
  194 + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth
  195 + AND c.F_IsEffective = 1
  196 + AND d.F_IsEffective = 1",
  197 + new { StatisticsMonth = statisticsMonth });
  198 + entity.CostWarehouseRent = warehouseRent;
167 199
168 // 5. 成本-总部房租 200 // 5. 成本-总部房租
169 - // TODO: 需要确认如何识别总部合同  
170 - entity.CostHQRent = 0; 201 + // 从合同月租明细表获取:门店是总部,分类是总部,统计月份匹配
  202 + // 使用月份字符串匹配,避免时区问题
  203 + var hqRent = await _db.Ado.SqlQuerySingleAsync<decimal>(
  204 + $@"SELECT COALESCE(SUM(d.F_DueAmount), 0)
  205 + FROM lq_contract_rent_detail d
  206 + INNER JOIN lq_contract c ON d.F_ContractId = c.F_Id
  207 + WHERE c.F_StoreId = '1649328471923847168'
  208 + AND c.F_Category = '总部'
  209 + AND DATE_FORMAT(d.F_PaymentMonth, '%Y%m') = @StatisticsMonth
  210 + AND c.F_IsEffective = 1
  211 + AND d.F_IsEffective = 1",
  212 + new { StatisticsMonth = statisticsMonth });
  213 + entity.CostHQRent = hqRent;
171 } 214 }
172 215
173 /// <summary> 216 /// <summary>
netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsStoreService.cs
1 using Microsoft.AspNetCore.Mvc; 1 using Microsoft.AspNetCore.Mvc;
  2 +using Microsoft.Extensions.Logging;
2 using NCC.Common.Filter; 3 using NCC.Common.Filter;
3 using NCC.Dependency; 4 using NCC.Dependency;
4 using NCC.DynamicApiController; 5 using NCC.DynamicApiController;
@@ -16,6 +17,7 @@ using NCC.Extend.Entitys.lq_director_salary_statistics; @@ -16,6 +17,7 @@ using NCC.Extend.Entitys.lq_director_salary_statistics;
16 using NCC.Extend.Entitys.lq_store_manager_salary_statistics; 17 using NCC.Extend.Entitys.lq_store_manager_salary_statistics;
17 using NCC.Extend.Entitys.lq_business_unit_manager_salary_statistics; 18 using NCC.Extend.Entitys.lq_business_unit_manager_salary_statistics;
18 using NCC.Extend.Entitys.lq_contract_rent_detail; 19 using NCC.Extend.Entitys.lq_contract_rent_detail;
  20 +using NCC.Extend.Entitys.lq_contract_monthly_cost;
19 using NCC.Extend.Entitys.lq_mdxx; 21 using NCC.Extend.Entitys.lq_mdxx;
20 using NCC.Extend.Entitys.lq_kd_pxmx; 22 using NCC.Extend.Entitys.lq_kd_pxmx;
21 using NCC.Extend.Entitys.lq_product; 23 using NCC.Extend.Entitys.lq_product;
@@ -38,10 +40,12 @@ namespace NCC.Extend @@ -38,10 +40,12 @@ namespace NCC.Extend
38 public class LqShareStatisticsStoreService : IDynamicApiController, ITransient 40 public class LqShareStatisticsStoreService : IDynamicApiController, ITransient
39 { 41 {
40 private readonly ISqlSugarClient _db; 42 private readonly ISqlSugarClient _db;
  43 + private readonly Microsoft.Extensions.Logging.ILogger<LqShareStatisticsStoreService> _logger;
41 44
42 - public LqShareStatisticsStoreService(ISqlSugarClient db) 45 + public LqShareStatisticsStoreService(ISqlSugarClient db, Microsoft.Extensions.Logging.ILogger<LqShareStatisticsStoreService> logger)
43 { 46 {
44 _db = db; 47 _db = db;
  48 + _logger = logger;
45 } 49 }
46 50
47 /// <summary> 51 /// <summary>
@@ -60,7 +64,7 @@ namespace NCC.Extend @@ -60,7 +64,7 @@ namespace NCC.Extend
60 var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); 64 var year = int.Parse(input.StatisticsMonth.Substring(0, 4));
61 var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); 65 var month = int.Parse(input.StatisticsMonth.Substring(4, 2));
62 var startDate = new DateTime(year, month, 1); 66 var startDate = new DateTime(year, month, 1);
63 - var endDate = startDate.AddMonths(1).AddDays(-1); 67 + var endDate = startDate.AddMonths(1).AddDays(-1).Date.AddHours(23).AddMinutes(59).AddSeconds(59);
64 68
65 // 获取门店列表 69 // 获取门店列表
66 var storeQuery = _db.Queryable<LqMdxxEntity>(); 70 var storeQuery = _db.Queryable<LqMdxxEntity>();
@@ -346,13 +350,26 @@ namespace NCC.Extend @@ -346,13 +350,26 @@ namespace NCC.Extend
346 && x.IsEffective == 1) 350 && x.IsEffective == 1)
347 .SumAsync(x => x.TotalPrice); 351 .SumAsync(x => x.TotalPrice);
348 352
349 - // 4. 科技部成本 = 科美业绩 * 30%  
350 - var kemeiPerformance = await _db.Queryable<LqKdPxmxEntity>()  
351 - .Where(x => x.Glkdbh.StartsWith(entity.StoreId) && x.Yjsj >= startDate && x.Yjsj <= endDate && x.IsEffective == 1)  
352 - .Where(x => x.ItemCategory == "科美")  
353 - .SumAsync(x => x.TotalPrice); 353 + // 4. 科技部成本 = (科美开单业绩 - 科美退款业绩) * 30%
  354 + // 4.1 统计科美开单业绩(通过关联开单表获取门店信息)
  355 + var kemeiBilling = await _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>((px, kd) => new JoinQueryInfos(
  356 + JoinType.Inner, px.Glkdbh == kd.Id))
  357 + .Where((px, kd) => kd.Djmd == entity.StoreId
  358 + && px.Yjsj >= startDate && px.Yjsj <= endDate
  359 + && px.IsEffective == 1
  360 + && px.ItemCategory == "科美")
  361 + .SumAsync((px, kd) => (decimal?)px.TotalPrice);
  362 +
  363 + // 4.2 统计科美退款业绩(从退卡健康师业绩表统计)
  364 + var kemeiRefund = await _db.Queryable<LqHytkJksyjEntity>()
  365 + .Where(x => x.StoreId == entity.StoreId
  366 + && x.Tksj >= startDate && x.Tksj <= endDate
  367 + && x.IsEffective == 1
  368 + && x.ItemCategory == "科美")
  369 + .SumAsync(x => (decimal?)(x.Jksyj ?? 0));
354 370
355 - entity.CostTechDept = kemeiPerformance * 0.3m; 371 + // 4.3 计算科技部成本 = (开单业绩 - 退款业绩) * 30%
  372 + entity.CostTechDept = ((kemeiBilling ?? 0m) - (kemeiRefund ?? 0m)) * 0.3m;
356 373
357 // 5. 管理费 = 总业绩 * 9% 374 // 5. 管理费 = 总业绩 * 9%
358 var totalPerformance = await _db.Queryable<LqKdKdjlbEntity>() 375 var totalPerformance = await _db.Queryable<LqKdKdjlbEntity>()
@@ -444,42 +461,131 @@ namespace NCC.Extend @@ -444,42 +461,131 @@ namespace NCC.Extend
444 461
445 // 5. 总经理/经理底薪和提成 462 // 5. 总经理/经理底薪和提成
446 // 底薪按门店数平均分摊,提成需要从 F_StorePerformanceDetail JSON 中提取该门店的提成 463 // 底薪按门店数平均分摊,提成需要从 F_StorePerformanceDetail JSON 中提取该门店的提成
447 - var gmSalary = await _db.Queryable<LqBusinessUnitManagerSalaryStatisticsEntity>() 464 + var businessUnitSalaries = await _db.Queryable<LqBusinessUnitManagerSalaryStatisticsEntity>()
448 .Where(x => x.StatisticsMonth == statisticsMonth) 465 .Where(x => x.StatisticsMonth == statisticsMonth)
449 .Where(x => x.StorePerformanceDetail.Contains(entity.StoreId)) 466 .Where(x => x.StorePerformanceDetail.Contains(entity.StoreId))
450 .ToListAsync(); 467 .ToListAsync();
451 468
452 decimal gmBaseSalary = 0; 469 decimal gmBaseSalary = 0;
453 decimal gmCommission = 0; 470 decimal gmCommission = 0;
  471 + decimal managerBaseSalary = 0;
  472 + decimal managerCommission = 0;
454 473
455 - foreach (var gm in gmSalary) 474 + foreach (var salaryRecord in businessUnitSalaries)
456 { 475 {
457 // 底薪平均分摊 476 // 底薪平均分摊
458 - if (!string.IsNullOrEmpty(gm.StorePerformanceDetail)) 477 + if (!string.IsNullOrEmpty(salaryRecord.StorePerformanceDetail))
459 { 478 {
460 try 479 try
461 { 480 {
462 - var storeDetails = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(gm.StorePerformanceDetail); 481 + var storeDetails = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(salaryRecord.StorePerformanceDetail);
463 var storeCount = storeDetails?.Count ?? 1; 482 var storeCount = storeDetails?.Count ?? 1;
464 - gmBaseSalary += gm.BaseSalary / storeCount;  
465 483
466 - // 提取该门店的提成  
467 var storeDetail = storeDetails?.FirstOrDefault(s => s.ContainsKey("StoreId") && s["StoreId"].ToString() == entity.StoreId); 484 var storeDetail = storeDetails?.FirstOrDefault(s => s.ContainsKey("StoreId") && s["StoreId"].ToString() == entity.StoreId);
468 - if (storeDetail != null && storeDetail.ContainsKey("Commission")) 485 + if (storeDetail != null)
469 { 486 {
470 - gmCommission += Convert.ToDecimal(storeDetail["Commission"]); 487 + // 根据ManagerType区分总经理和经理
  488 + if (salaryRecord.ManagerType == 1) // 总经理
  489 + {
  490 + gmBaseSalary += salaryRecord.BaseSalary / storeCount;
  491 +
  492 + // 提取该门店的提成(JSON字段名是CommissionAmount)
  493 + object commissionValue = null;
  494 + if (storeDetail.ContainsKey("CommissionAmount"))
  495 + {
  496 + commissionValue = storeDetail["CommissionAmount"];
  497 + }
  498 + else
  499 + {
  500 + // 尝试不区分大小写查找
  501 + var key = storeDetail.Keys.FirstOrDefault(k => k.Equals("CommissionAmount", StringComparison.OrdinalIgnoreCase));
  502 + if (key != null)
  503 + {
  504 + commissionValue = storeDetail[key];
  505 + }
  506 + }
  507 +
  508 + if (commissionValue != null)
  509 + {
  510 + // JSON反序列化后,数值类型可能是long或double,需要先转换为decimal
  511 + decimal commission = 0;
  512 + if (commissionValue is long longValue)
  513 + {
  514 + commission = longValue;
  515 + }
  516 + else if (commissionValue is double doubleValue)
  517 + {
  518 + commission = (decimal)doubleValue;
  519 + }
  520 + else if (commissionValue is decimal decimalValue)
  521 + {
  522 + commission = decimalValue;
  523 + }
  524 + else
  525 + {
  526 + commission = Convert.ToDecimal(commissionValue);
  527 + }
  528 + gmCommission += commission;
  529 + }
  530 + }
  531 + else if (salaryRecord.ManagerType == 0) // 经理
  532 + {
  533 + managerBaseSalary += salaryRecord.BaseSalary / storeCount;
  534 +
  535 + // 提取该门店的提成(JSON字段名是CommissionAmount)
  536 + object commissionValue = null;
  537 + if (storeDetail.ContainsKey("CommissionAmount"))
  538 + {
  539 + commissionValue = storeDetail["CommissionAmount"];
  540 + }
  541 + else
  542 + {
  543 + // 尝试不区分大小写查找
  544 + var key = storeDetail.Keys.FirstOrDefault(k => k.Equals("CommissionAmount", StringComparison.OrdinalIgnoreCase));
  545 + if (key != null)
  546 + {
  547 + commissionValue = storeDetail[key];
  548 + }
  549 + }
  550 +
  551 + if (commissionValue != null)
  552 + {
  553 + // JSON反序列化后,数值类型可能是long或double,需要先转换为decimal
  554 + decimal commission = 0;
  555 + if (commissionValue is long longValue)
  556 + {
  557 + commission = longValue;
  558 + }
  559 + else if (commissionValue is double doubleValue)
  560 + {
  561 + commission = (decimal)doubleValue;
  562 + }
  563 + else if (commissionValue is decimal decimalValue)
  564 + {
  565 + commission = decimalValue;
  566 + }
  567 + else
  568 + {
  569 + commission = Convert.ToDecimal(commissionValue);
  570 + }
  571 + managerCommission += commission;
  572 + }
  573 + }
471 } 574 }
472 } 575 }
473 - catch 576 + catch (Exception ex)
474 { 577 {
475 - // JSON 解析失败,使用默认分摊  
476 - gmBaseSalary += gm.BaseSalary; 578 + _logger.LogError(ex, $"解析门店业绩明细JSON失败,使用默认分摊。BatchId: {salaryRecord.Id}");
  579 + // JSON 解析失败,使用默认分摊到总经理
  580 + gmBaseSalary += salaryRecord.BaseSalary;
477 } 581 }
478 } 582 }
479 } 583 }
480 584
481 entity.SalaryBaseGeneralManager = gmBaseSalary; 585 entity.SalaryBaseGeneralManager = gmBaseSalary;
482 entity.SalaryCommissionGeneralManager = gmCommission; 586 entity.SalaryCommissionGeneralManager = gmCommission;
  587 + entity.SalaryBaseManager = managerBaseSalary;
  588 + entity.SalaryCommissionManager = managerCommission;
483 589
484 // 保留字段 590 // 保留字段
485 entity.SalaryAttendance = 0; 591 entity.SalaryAttendance = 0;
@@ -490,22 +596,22 @@ namespace NCC.Extend @@ -490,22 +596,22 @@ namespace NCC.Extend
490 /// </summary> 596 /// </summary>
491 private async Task CalculateExpense(LqShareStatisticsStoreEntity entity, DateTime startDate, DateTime endDate, string statisticsMonth) 597 private async Task CalculateExpense(LqShareStatisticsStoreEntity entity, DateTime startDate, DateTime endDate, string statisticsMonth)
492 { 598 {
493 - // 1. 门店房租 - 需要通过合同关联门店  
494 - // 从合同表关联租金明细 599 + // 1. 门店房租 - 从合同成本按月统计表获取
495 // 解析统计月份 600 // 解析统计月份
496 var year = int.Parse(statisticsMonth.Substring(0, 4)); 601 var year = int.Parse(statisticsMonth.Substring(0, 4));
497 var month = int.Parse(statisticsMonth.Substring(4, 2)); 602 var month = int.Parse(statisticsMonth.Substring(4, 2));
  603 + var monthDate = new DateTime(year, month, 1);
498 604
499 - var rentDetail = await _db.Queryable<LqContractRentDetailEntity, NCC.Extend.Entitys.lq_contract.LqContractEntity>((rd, c) => new JoinQueryInfos(  
500 - JoinType.Inner, rd.ContractId == c.Id))  
501 - .Where((rd, c) => c.StoreId == entity.StoreId  
502 - && rd.PaymentMonth.Year == year  
503 - && rd.PaymentMonth.Month == month  
504 - && rd.IsEffective == 1)  
505 - .Select((rd, c) => rd.DueAmount)  
506 - .ToListAsync(); 605 + // 从 lq_contract_monthly_cost 表统计分类为"门店"的月度成本
  606 + var storeRent = await _db.Queryable<LqContractMonthlyCostEntity>()
  607 + .Where(x => x.StoreId == entity.StoreId
  608 + && x.Month.Year == year
  609 + && x.Month.Month == month
  610 + && x.Category == "门店"
  611 + && x.IsEffective == 1)
  612 + .SumAsync(x => (decimal?)x.MonthlyCost);
507 613
508 - entity.StoreRent = rentDetail.Sum(); 614 + entity.StoreRent = storeRent ?? 0;
509 615
510 // 2. 当期费用 = 报销费用中一级分类为“当期费用”的已审核通过记录 616 // 2. 当期费用 = 报销费用中一级分类为“当期费用”的已审核通过记录
511 // 只统计:报销申请审批通过 + 申请门店=当前门店 + 申请时间在当月 617 // 只统计:报销申请审批通过 + 申请门店=当前门店 + 申请时间在当月
@@ -541,9 +647,9 @@ namespace NCC.Extend @@ -541,9 +647,9 @@ namespace NCC.Extend
541 { 647 {
542 // 人工工资汇总 648 // 人工工资汇总
543 var totalSalary = entity.SalaryBaseHealthCoach + entity.SalaryBaseAssistant + entity.SalaryBaseDirector 649 var totalSalary = entity.SalaryBaseHealthCoach + entity.SalaryBaseAssistant + entity.SalaryBaseDirector
544 - + entity.SalaryBaseStoreManager + entity.SalaryBaseGeneralManager 650 + + entity.SalaryBaseStoreManager + entity.SalaryBaseGeneralManager + entity.SalaryBaseManager
545 + entity.SalaryCommissionHealthCoach + entity.SalaryCommissionAssistant + entity.SalaryCommissionDirector 651 + entity.SalaryCommissionHealthCoach + entity.SalaryCommissionAssistant + entity.SalaryCommissionDirector
546 - + entity.SalaryCommissionStoreManager + entity.SalaryCommissionGeneralManager 652 + + entity.SalaryCommissionStoreManager + entity.SalaryCommissionGeneralManager + entity.SalaryCommissionManager
547 + entity.SalaryManual + entity.SalaryAttendance + entity.SalaryPhoneCustody 653 + entity.SalaryManual + entity.SalaryAttendance + entity.SalaryPhoneCustody
548 + entity.SalaryHeadcountReward + entity.SalaryTZone; 654 + entity.SalaryHeadcountReward + entity.SalaryTZone;
549 655
netcore/src/Modularity/Extend/NCC.Extend/LqShareStatisticsTechDeptService.cs
@@ -9,6 +9,12 @@ using NCC.Extend.Entitys.lq_kd_pxmx; @@ -9,6 +9,12 @@ using NCC.Extend.Entitys.lq_kd_pxmx;
9 using NCC.Extend.Entitys.lq_kd_kdjlb; 9 using NCC.Extend.Entitys.lq_kd_kdjlb;
10 using NCC.Extend.Entitys.lq_hytk_jksyj; 10 using NCC.Extend.Entitys.lq_hytk_jksyj;
11 using NCC.Extend.Entitys.lq_xh_jksyj; 11 using NCC.Extend.Entitys.lq_xh_jksyj;
  12 +using NCC.Extend.Entitys.lq_xh_kjbsyj;
  13 +using NCC.Extend.Entitys.lq_tech_teacher_salary_statistics;
  14 +using NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics;
  15 +using NCC.Extend.Entitys.lq_md_general_manager_lifeline;
  16 +using NCC.System.Entitys.Permission;
  17 +using NCC.Extend.Entitys;
12 using SqlSugar; 18 using SqlSugar;
13 using System; 19 using System;
14 using System.Collections.Generic; 20 using System.Collections.Generic;
@@ -48,7 +54,7 @@ namespace NCC.Extend @@ -48,7 +54,7 @@ namespace NCC.Extend
48 var year = int.Parse(input.StatisticsMonth.Substring(0, 4)); 54 var year = int.Parse(input.StatisticsMonth.Substring(0, 4));
49 var month = int.Parse(input.StatisticsMonth.Substring(4, 2)); 55 var month = int.Parse(input.StatisticsMonth.Substring(4, 2));
50 var startDate = new DateTime(year, month, 1); 56 var startDate = new DateTime(year, month, 1);
51 - var endDate = startDate.AddMonths(1).AddDays(-1); 57 + var endDate = startDate.AddMonths(1); // 下个月的第一天,用于 < 比较
52 58
53 // 确定要生成的部门列表 59 // 确定要生成的部门列表
54 var departments = new List<string>(); 60 var departments = new List<string>();
@@ -171,9 +177,20 @@ namespace NCC.Extend @@ -171,9 +177,20 @@ namespace NCC.Extend
171 /// </summary> 177 /// </summary>
172 private async Task CalculateIncome(LqShareStatisticsTechDeptEntity entity, string deptName, DateTime startDate, DateTime endDate) 178 private async Task CalculateIncome(LqShareStatisticsTechDeptEntity entity, string deptName, DateTime startDate, DateTime endDate)
173 { 179 {
174 - // 1. 找到该科技部管辖的所有门店 180 + // 1. 通过组织名称找到组织ID
  181 + var organize = await _db.Queryable<OrganizeEntity>()
  182 + .Where(x => x.FullName == deptName && x.DeleteMark == null && x.EnabledMark == 1)
  183 + .FirstAsync();
  184 +
  185 + if (organize == null)
  186 + {
  187 + entity.Income = 0;
  188 + return;
  189 + }
  190 +
  191 + // 2. 找到该科技部管辖的所有门店(通过组织ID)
175 var stores = await _db.Queryable<LqMdxxEntity>() 192 var stores = await _db.Queryable<LqMdxxEntity>()
176 - .Where(x => x.Kjb == deptName) 193 + .Where(x => x.Kjb == organize.Id)
177 .Select(x => x.Id) 194 .Select(x => x.Id)
178 .ToListAsync(); 195 .ToListAsync();
179 196
@@ -183,21 +200,57 @@ namespace NCC.Extend @@ -183,21 +200,57 @@ namespace NCC.Extend
183 return; 200 return;
184 } 201 }
185 202
186 - // 2. 统计这些门店的科美项目开单实付业绩 203 + // 3. 统计这些门店的科美项目开单实付业绩
187 // 需要关联 lq_kd_kdjlb 来获取门店信息 204 // 需要关联 lq_kd_kdjlb 来获取门店信息
188 - var kemeiIncome = await _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity>((px, kd) => new JoinQueryInfos(  
189 - JoinType.Inner, px.Glkdbh == kd.Id))  
190 - .Where((px, kd) => stores.Contains(kd.Djmd) && px.Yjsj >= startDate && px.Yjsj <= endDate && px.IsEffective == 1)  
191 - .Where((px, kd) => px.ItemCategory == "科美")  
192 - .SumAsync((px, kd) => px.TotalPrice);  
193 -  
194 - // 3. 减去对应的科美项目实退金额  
195 - var kemeiRefund = await _db.Queryable<LqHytkJksyjEntity>()  
196 - .Where(x => stores.Contains(x.StoreId) && x.Tksj >= startDate && x.Tksj <= endDate && x.IsEffective == 1)  
197 - .Where(x => x.ItemCategory == "科美")  
198 - .SumAsync(x => x.Jksyj ?? 0);  
199 -  
200 - // 4. 结果 * 30% 205 + decimal kemeiIncome = 0;
  206 + if (stores.Count > 0)
  207 + {
  208 + var storeIdsStr = string.Join("','", stores);
  209 + var kemeiIncomeSql = $@"
  210 + SELECT COALESCE(SUM(px.F_TotalPrice), 0) as Total
  211 + FROM lq_kd_pxmx px
  212 + INNER JOIN lq_kd_kdjlb kd ON px.glkdbh = kd.F_Id
  213 + WHERE kd.djmd IN ('{storeIdsStr}')
  214 + AND px.yjsj >= '{startDate:yyyy-MM-dd} 00:00:00'
  215 + AND px.yjsj < '{endDate:yyyy-MM-dd} 00:00:00'
  216 + AND px.F_IsEffective = 1
  217 + AND px.F_ItemCategory = '科美'";
  218 + var kemeiIncomeResult = await _db.Ado.SqlQueryAsync<dynamic>(kemeiIncomeSql);
  219 + if (kemeiIncomeResult != null && kemeiIncomeResult.Any())
  220 + {
  221 + var first = kemeiIncomeResult.FirstOrDefault();
  222 + if (first != null)
  223 + {
  224 + kemeiIncome = Convert.ToDecimal(first.Total ?? 0);
  225 + }
  226 + }
  227 + }
  228 +
  229 + // 4. 减去对应的科美项目实退金额
  230 + decimal kemeiRefund = 0;
  231 + if (stores.Count > 0)
  232 + {
  233 + var storeIdsStr = string.Join("','", stores);
  234 + var kemeiRefundSql = $@"
  235 + SELECT COALESCE(SUM(jksyj), 0) as Total
  236 + FROM lq_hytk_jksyj
  237 + WHERE F_StoreId IN ('{storeIdsStr}')
  238 + AND tksj >= '{startDate:yyyy-MM-dd} 00:00:00'
  239 + AND tksj < '{endDate:yyyy-MM-dd} 00:00:00'
  240 + AND F_IsEffective = 1
  241 + AND F_ItemCategory = '科美'";
  242 + var kemeiRefundResult = await _db.Ado.SqlQueryAsync<dynamic>(kemeiRefundSql);
  243 + if (kemeiRefundResult != null && kemeiRefundResult.Any())
  244 + {
  245 + var first = kemeiRefundResult.FirstOrDefault();
  246 + if (first != null)
  247 + {
  248 + kemeiRefund = Convert.ToDecimal(first.Total ?? 0);
  249 + }
  250 + }
  251 + }
  252 +
  253 + // 5. 结果 * 30%
201 entity.Income = (kemeiIncome - kemeiRefund) * 0.3m; 254 entity.Income = (kemeiIncome - kemeiRefund) * 0.3m;
202 } 255 }
203 256
@@ -206,26 +259,103 @@ namespace NCC.Extend @@ -206,26 +259,103 @@ namespace NCC.Extend
206 /// </summary> 259 /// </summary>
207 private async Task CalculateCost(LqShareStatisticsTechDeptEntity entity, string deptName, DateTime startDate, DateTime endDate, string statisticsMonth) 260 private async Task CalculateCost(LqShareStatisticsTechDeptEntity entity, string deptName, DateTime startDate, DateTime endDate, string statisticsMonth)
208 { 261 {
209 - // 1. 成本-报销 (TODO: 需要确认报销分类的具体判定方式)  
210 - entity.CostReimbursement = 0;  
211 -  
212 - // 2. 成本-人工-科技部老师底薪 (TODO: 需要确认科技部老师工资表名和字段)  
213 - entity.CostTeacherBase = 0;  
214 -  
215 - // 3. 成本-人工-科技部手工费  
216 - // 从消耗表中统计科技部老师的手工费  
217 - // TODO: 需要确认如何识别科技部老师  
218 - entity.CostTeacherManual = 0; 262 + // 1. 通过组织名称找到组织ID
  263 + var organize = await _db.Queryable<OrganizeEntity>()
  264 + .Where(x => x.FullName == deptName && x.DeleteMark == null && x.EnabledMark == 1)
  265 + .FirstAsync();
219 266
220 - // 4. 成本-人工-科技部开单提成 (TODO: 需要确认字段名)  
221 - entity.CostTeacherBillingComm = 0; 267 + if (organize == null)
  268 + {
  269 + // 如果找不到组织,所有成本为0
  270 + entity.CostReimbursement = 0;
  271 + entity.CostTeacherBase = 0;
  272 + entity.CostTeacherManual = 0;
  273 + entity.CostTeacherBillingComm = 0;
  274 + entity.CostTeacherConsumeComm = 0;
  275 + entity.CostGMBase = 0;
  276 + entity.CostGMComm = 0;
  277 + entity.CostTeacherExpertComm = 0;
  278 + entity.CostTeacherOvertime = 0;
  279 + entity.RewardTechDept = 0;
  280 + entity.CostOther1 = 0;
  281 + entity.CostOther2 = 0;
  282 + return;
  283 + }
222 284
223 - // 5. 成本-人工-科技部消耗提成 (TODO: 需要确认字段名)  
224 - entity.CostTeacherConsumeComm = 0; 285 + // 2. 找到该科技部管辖的所有门店(通过组织ID)
  286 + var stores = await _db.Queryable<LqMdxxEntity>()
  287 + .Where(x => x.Kjb == organize.Id)
  288 + .Select(x => x.Id)
  289 + .ToListAsync();
225 290
226 - // 6. 成本-人工-科技部总经理 (TODO: 需要确认科技部总经理工资表)  
227 - entity.CostGMBase = 0;  
228 - entity.CostGMComm = 0; 291 + // 1. 成本-报销:筛选一级分类为"科技部费用"的申请,根据申请门店的归属区分一部/二部
  292 + var reimbursementAmount = await _db.Queryable<LqReimbursementApplicationEntity, LqPurchaseRecordsEntity, LqReimbursementCategoryEntity>(
  293 + (app, purchase, category) => app.PurchaseRecordsId == purchase.Id && purchase.ReimbursementCategoryId == category.Id)
  294 + .Where((app, purchase, category) =>
  295 + category.Level1Name == "科技部费用"
  296 + && app.ApprovalStatus == "已通过"
  297 + && stores.Contains(app.ApplicationStoreId)
  298 + && app.ApplicationTime >= startDate
  299 + && app.ApplicationTime <= endDate.AddDays(1))
  300 + .SumAsync((app, purchase, category) => purchase.Amount);
  301 +
  302 + entity.CostReimbursement = reimbursementAmount;
  303 +
  304 + // 2. 成本-人工-科技部老师底薪:从科技部老师工资统计表统计
  305 + // 需要筛选该科技部管理的门店的老师
  306 + var teacherBaseSalary = await _db.Queryable<LqTechTeacherSalaryStatisticsEntity>()
  307 + .Where(x => x.StatisticsMonth == statisticsMonth
  308 + && stores.Contains(x.StoreId))
  309 + .SumAsync(x => x.BaseSalary);
  310 +
  311 + entity.CostTeacherBase = teacherBaseSalary;
  312 +
  313 + // 3. 成本-人工-科技部手工费:从消耗表中统计科技部老师的手工费
  314 + // 数据来源:lq_xh_kjbsyj(科技部老师业绩表)
  315 + // 条件:ItemCategory = 科美,根据门店归属区分一部/二部
  316 + var teacherManualFee = await _db.Queryable<LqXhKjbsyjEntity>()
  317 + .Where(x => x.ItemCategory == "科美"
  318 + && stores.Contains(x.StoreId)
  319 + && x.Yjsj >= startDate
  320 + && x.Yjsj < endDate
  321 + && x.IsEffective == 1)
  322 + .SumAsync(x => x.LaborCost ?? 0);
  323 +
  324 + entity.CostTeacherManual = teacherManualFee;
  325 +
  326 + // 4. 成本-人工-科技部开单提成:从科技部老师工资统计表统计业绩提成金额
  327 + var teacherBillingComm = await _db.Queryable<LqTechTeacherSalaryStatisticsEntity>()
  328 + .Where(x => x.StatisticsMonth == statisticsMonth
  329 + && stores.Contains(x.StoreId))
  330 + .SumAsync(x => x.PerformanceCommissionAmount);
  331 +
  332 + entity.CostTeacherBillingComm = teacherBillingComm;
  333 +
  334 + // 5. 成本-人工-科技部消耗提成:从科技部老师工资统计表统计消耗提成金额
  335 + var teacherConsumeComm = await _db.Queryable<LqTechTeacherSalaryStatisticsEntity>()
  336 + .Where(x => x.StatisticsMonth == statisticsMonth
  337 + && stores.Contains(x.StoreId))
  338 + .SumAsync(x => x.ConsumeCommissionAmount);
  339 +
  340 + entity.CostTeacherConsumeComm = teacherConsumeComm;
  341 +
  342 + // 6. 成本-人工-科技部总经理底薪和提成
  343 + // 直接根据工资统计表中的 F_Position 字段区分科技一部/二部,不需要通过用户表过滤
  344 + // 科技部总经理底薪
  345 + var gmBaseSalary = await _db.Queryable<LqTechGeneralManagerSalaryStatisticsEntity>()
  346 + .Where(x => x.StatisticsMonth == statisticsMonth
  347 + && x.Position == deptName)
  348 + .SumAsync(x => x.BaseSalary);
  349 +
  350 + entity.CostGMBase = gmBaseSalary;
  351 +
  352 + // 科技部总经理提成
  353 + var gmComm = await _db.Queryable<LqTechGeneralManagerSalaryStatisticsEntity>()
  354 + .Where(x => x.StatisticsMonth == statisticsMonth
  355 + && x.Position == deptName)
  356 + .SumAsync(x => x.TotalCommission);
  357 +
  358 + entity.CostGMComm = gmComm;
229 359
230 // 保留字段 360 // 保留字段
231 entity.CostTeacherExpertComm = 0; 361 entity.CostTeacherExpertComm = 0;
@@ -240,19 +370,19 @@ namespace NCC.Extend @@ -240,19 +370,19 @@ namespace NCC.Extend
240 /// </summary> 370 /// </summary>
241 private void CalculateProfit(LqShareStatisticsTechDeptEntity entity) 371 private void CalculateProfit(LqShareStatisticsTechDeptEntity entity)
242 { 372 {
243 - // 科技部利润 = 收入 - 成本报销 - 成本人工 - 其他  
244 - var totalCost = entity.CostReimbursement  
245 - + entity.CostTeacherBase  
246 - + entity.CostTeacherManual  
247 - + entity.CostTeacherBillingComm  
248 - + entity.CostTeacherConsumeComm  
249 - + entity.CostTeacherExpertComm  
250 - + entity.CostTeacherOvertime  
251 - + entity.CostGMBase  
252 - + entity.CostGMComm  
253 - + entity.RewardTechDept  
254 - + entity.CostOther1  
255 - + entity.CostOther2; 373 + // 科技部利润 = 收入 - 成本报销 - 成本人工(底薪 + 手工 + 开单提成 + 消耗提成 + 专家提成 + 加班 + 总经理底薪 + 总经理提成) - 其他
  374 + var totalCost = entity.CostReimbursement // 成本报销
  375 + + entity.CostTeacherBase // 成本人工-底薪
  376 + + entity.CostTeacherManual // 成本人工-手工
  377 + + entity.CostTeacherBillingComm // 成本人工-开单提成
  378 + + entity.CostTeacherConsumeComm // 成本人工-消耗提成
  379 + + entity.CostTeacherExpertComm // 成本人工-专家提成
  380 + + entity.CostTeacherOvertime // 成本人工-加班
  381 + + entity.CostGMBase // 成本人工-总经理底薪
  382 + + entity.CostGMComm // 成本人工-总经理提成
  383 + + entity.CostOther1 // 其他1
  384 + + entity.CostOther2; // 其他2
  385 + // 注意:RewardTechDept(奖励-科技部)是保留字段,不计入成本
256 386
257 entity.Profit = entity.Income - totalCost; 387 entity.Profit = entity.Income - totalCost;
258 } 388 }
netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs
@@ -194,7 +194,7 @@ namespace NCC.Extend @@ -194,7 +194,7 @@ namespace NCC.Extend
194 // 1.5 消耗业绩和项目数数据 (lq_xh_kjbsyj,关联lq_xh_hyhk获取时间) 194 // 1.5 消耗业绩和项目数数据 (lq_xh_kjbsyj,关联lq_xh_hyhk获取时间)
195 var consumePerformanceList = await _db.Queryable<LqXhKjbsyjEntity, LqXhHyhkEntity>( 195 var consumePerformanceList = await _db.Queryable<LqXhKjbsyjEntity, LqXhHyhkEntity>(
196 (kjbsyj, hyhk) => kjbsyj.Glkdbh == hyhk.Id && hyhk.IsEffective == 1) 196 (kjbsyj, hyhk) => kjbsyj.Glkdbh == hyhk.Id && hyhk.IsEffective == 1)
197 - .Where((kjbsyj, hyhk) => kjbsyj.IsEffective == 1 197 + .Where((kjbsyj, hyhk) => kjbsyj.IsEffective == 1
198 && hyhk.Hksj >= startDate && hyhk.Hksj <= endDate.AddDays(1)) 198 && hyhk.Hksj >= startDate && hyhk.Hksj <= endDate.AddDays(1))
199 .Select((kjbsyj, hyhk) => new 199 .Select((kjbsyj, hyhk) => new
200 { 200 {
@@ -314,8 +314,8 @@ namespace NCC.Extend @@ -314,8 +314,8 @@ namespace NCC.Extend
314 .Sum(x => x.Kjblsyj ?? 0m); 314 .Sum(x => x.Kjblsyj ?? 0m);
315 salary.RefundAchievement = refundPerformance; 315 salary.RefundAchievement = refundPerformance;
316 316
317 - // 总业绩  
318 - salary.TotalPerformance = salary.OrderAchievement + salary.ConsumeAchievement + salary.RefundAchievement; 317 + // 总业绩 = 开单业绩 - 退卡业绩(退卡是扣除)
  318 + salary.TotalPerformance = salary.OrderAchievement - salary.RefundAchievement;
319 319
320 // 2.5 考勤数据 320 // 2.5 考勤数据
321 var attendance = attendanceDict.ContainsKey(teacherId) ? attendanceDict[teacherId] : null; 321 var attendance = attendanceDict.ContainsKey(teacherId) ? attendanceDict[teacherId] : null;
@@ -630,8 +630,8 @@ namespace NCC.Extend @@ -630,8 +630,8 @@ namespace NCC.Extend
630 630
631 // 7. 统计人头(按月份+客户去重) 631 // 7. 统计人头(按月份+客户去重)
632 var personCountRecords = await _db.Queryable<LqPersonTimesRecordEntity>() 632 var personCountRecords = await _db.Queryable<LqPersonTimesRecordEntity>()
633 - .Where(x => x.PersonType == "科技老师"  
634 - && x.WorkMonth == monthStr 633 + .Where(x => x.PersonType == "科技老师"
  634 + && x.WorkMonth == monthStr
635 && x.IsEffective == 1 635 && x.IsEffective == 1
636 && teacherIds.Contains(x.PersonId)) 636 && teacherIds.Contains(x.PersonId))
637 .ToListAsync(); 637 .ToListAsync();
netcore/src/Modularity/Extend/NCC.Extend/LqTkjlbService.cs
@@ -248,6 +248,8 @@ namespace NCC.Extend.LqTkjlb @@ -248,6 +248,8 @@ namespace NCC.Extend.LqTkjlb
248 IsEffective = StatusEnum.有效.GetHashCode(), 248 IsEffective = StatusEnum.有效.GetHashCode(),
249 CreateTIme = DateTime.Now, 249 CreateTIme = DateTime.Now,
250 ItemCategory = await _db.Queryable<LqXmzlEntity>().Where(x => x.Id == "61").Select(x => x.Qt2).FirstAsync(), 250 ItemCategory = await _db.Queryable<LqXmzlEntity>().Where(x => x.Id == "61").Select(x => x.Qt2).FirstAsync(),
  251 + PerformanceType = await _db.Queryable<LqXmzlEntity>().Where(x => x.Id == "61").Select(x => x.Fl3).FirstAsync(),
  252 + BeautyType = await _db.Queryable<LqXmzlEntity>().Where(x => x.Id == "61").Select(x => x.BeautyType).FirstAsync(),
251 }; 253 };
252 var pxmxResult = await _db.Insertable(pxmxentity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync(); 254 var pxmxResult = await _db.Insertable(pxmxentity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync();
253 if (!(pxmxResult > 0)) 255 if (!(pxmxResult > 0))
netcore/src/Modularity/Extend/NCC.Extend/Utils/WeChatBotService.cs
@@ -25,7 +25,7 @@ namespace NCC.Extend.Utils @@ -25,7 +25,7 @@ namespace NCC.Extend.Utils
25 _httpClient = httpClient; 25 _httpClient = httpClient;
26 26
27 // 从配置文件中读取企业微信机器人配置 27 // 从配置文件中读取企业微信机器人配置
28 - _botApiUrl = App.Configuration["WeChatBot:BotApiUrl"] ?? "http://wx.lvqianmeiye.com/api/Bot/send-text"; 28 + _botApiUrl = App.Configuration["WeChatBot:BotApiUrl"] ?? "https://wx.lvqianmeiye.com/api/Bot/send-text";
29 29
30 // 从配置文件中读取Webhook地址(正式或测试地址,通过配置文件切换) 30 // 从配置文件中读取Webhook地址(正式或测试地址,通过配置文件切换)
31 _webhookUrl = App.Configuration["WeChatBot:WebhookUrl"]; 31 _webhookUrl = App.Configuration["WeChatBot:WebhookUrl"];
sql/同步业绩表门店ID字段.sql
@@ -37,21 +37,27 @@ WHERE yj.glkdbh IS NOT NULL @@ -37,21 +37,27 @@ WHERE yj.glkdbh IS NOT NULL
37 37
38 -- 3. 消耗健康师业绩表:通过glkdbh关联开单记录表获取门店ID 38 -- 3. 消耗健康师业绩表:通过glkdbh关联开单记录表获取门店ID
39 UPDATE lq_xh_jksyj yj 39 UPDATE lq_xh_jksyj yj
40 -INNER JOIN lq_kd_kdjlb kd ON yj.glkdbh = kd.F_Id  
41 -SET yj.F_StoreId = kd.djmd  
42 -WHERE yj.glkdbh IS NOT NULL 40 +INNER JOIN lq_xh_hyhk xh ON yj.glkdbh = xh.F_Id
  41 +SET yj.F_StoreId = xh.md
  42 +WHERE (yj.F_StoreId IS NULL OR yj.F_StoreId = '')
  43 + AND yj.glkdbh IS NOT NULL
43 AND yj.glkdbh != '' 44 AND yj.glkdbh != ''
44 - AND kd.djmd IS NOT NULL  
45 - AND kd.djmd != ''; 45 + AND xh.F_Id IS NOT NULL
  46 + AND xh.md IS NOT NULL
  47 + AND xh.md != ''
  48 + AND xh.F_IsEffective = 1;
46 49
47 -- 4. 消耗科技老师业绩表:通过glkdbh关联开单记录表获取门店ID 50 -- 4. 消耗科技老师业绩表:通过glkdbh关联开单记录表获取门店ID
48 UPDATE lq_xh_kjbsyj yj 51 UPDATE lq_xh_kjbsyj yj
49 -INNER JOIN lq_kd_kdjlb kd ON yj.glkdbh = kd.F_Id  
50 -SET yj.F_StoreId = kd.djmd  
51 -WHERE yj.glkdbh IS NOT NULL 52 +INNER JOIN lq_xh_hyhk xh ON yj.glkdbh = xh.F_Id
  53 +SET yj.F_StoreId = xh.md
  54 +WHERE (yj.F_StoreId IS NULL OR yj.F_StoreId = '')
  55 + AND yj.glkdbh IS NOT NULL
52 AND yj.glkdbh != '' 56 AND yj.glkdbh != ''
53 - AND kd.djmd IS NOT NULL  
54 - AND kd.djmd != ''; 57 + AND xh.F_Id IS NOT NULL
  58 + AND xh.md IS NOT NULL
  59 + AND xh.md != ''
  60 + AND xh.F_IsEffective = 1;
55 61
56 -- 5. 退卡健康师业绩表:通过gltkbh关联退卡表获取门店ID 62 -- 5. 退卡健康师业绩表:通过gltkbh关联退卡表获取门店ID
57 UPDATE lq_hytk_jksyj yj 63 UPDATE lq_hytk_jksyj yj
sql/排查生美业绩统计差异-简化版.sql
@@ -120,3 +120,9 @@ ORDER BY 生美业绩 DESC; @@ -120,3 +120,9 @@ ORDER BY 生美业绩 DESC;
120 120
121 121
122 122
  123 +
  124 +
  125 +
  126 +
  127 +
  128 +
sql/排查生美业绩统计差异详细.sql
@@ -172,3 +172,9 @@ HAVING COUNT(*) &gt; 1; @@ -172,3 +172,9 @@ HAVING COUNT(*) &gt; 1;
172 172
173 173
174 174
  175 +
  176 +
  177 +
  178 +
  179 +
  180 +
sql/检查生美业绩统计差异.sql
@@ -142,3 +142,9 @@ ORDER BY 生美业绩 DESC; @@ -142,3 +142,9 @@ ORDER BY 生美业绩 DESC;
142 142
143 143
144 144
  145 +
  146 +
  147 +
  148 +
  149 +
  150 +
sql/添加合作成本表成本类型字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 添加合作成本表成本类型字段
  3 +-- ============================================
  4 +-- 说明:
  5 +-- 1. 在 lq_cooperation_cost 表中添加 F_CostType 字段
  6 +-- 2. 字段类型:varchar(50),允许为空
  7 +-- 3. 字段说明:成本类型
  8 +-- ============================================
  9 +
  10 +ALTER TABLE `lq_cooperation_cost`
  11 +ADD COLUMN `F_CostType` varchar(50) DEFAULT NULL COMMENT '成本类型' AFTER `F_Remarks`;
  12 +
test_tianwang_api.py
@@ -75,3 +75,7 @@ print(f&quot;\n教育一部+教育二部合计 BillingPerformance: {total2}&quot;) @@ -75,3 +75,7 @@ print(f&quot;\n教育一部+教育二部合计 BillingPerformance: {total2}&quot;)
75 75
76 76
77 77
  78 +
  79 +
  80 +
  81 +
科技部老师工资计算规则.md
@@ -65,21 +65,24 @@ @@ -65,21 +65,24 @@
65 65
66 ### 总业绩(TotalPerformance) 66 ### 总业绩(TotalPerformance)
67 67
68 -**定义**:开单业绩 + 消耗业绩 + 退卡业绩 68 +**定义**:开单业绩 - 退卡业绩(退卡是扣除)
69 69
70 **数据来源表及字段**: 70 **数据来源表及字段**:
71 71
72 | 业绩类型 | 数据表 | 字段 | 说明 | 72 | 业绩类型 | 数据表 | 字段 | 说明 |
73 |---------|--------|------|------| 73 |---------|--------|------|------|
74 | **开单业绩** | `lq_kd_kjbsyj` | `kjblsyj` | 科技部老师开单业绩 | 74 | **开单业绩** | `lq_kd_kjbsyj` | `kjblsyj` | 科技部老师开单业绩 |
75 -| **消耗业绩** | `lq_xh_kjbsyj` | `kjblsyj` | 科技部老师消耗业绩 |  
76 -| **退卡业绩** | `lq_hytk_kjbsyj` | `kjblsyj` | 科技部老师退卡业绩 | 75 +| **退卡业绩** | `lq_hytk_kjbsyj` | `kjblsyj` | 科技部老师退卡业绩(扣除项) |
77 76
78 **计算方式**: 77 **计算方式**:
79 ```sql 78 ```sql
80 -总业绩 = SUM(开单业绩) + SUM(消耗业绩) + SUM(退卡业绩) 79 +总业绩 = SUM(开单业绩) - SUM(退卡业绩)
81 ``` 80 ```
82 81
  82 +**注意**:
  83 +- 消耗业绩不包含在总业绩中
  84 +- 退卡业绩是扣除项,所以用减法
  85 +
83 **过滤条件**: 86 **过滤条件**:
84 - 所有表记录必须满足:`F_IsEffective = 1`(有效记录) 87 - 所有表记录必须满足:`F_IsEffective = 1`(有效记录)
85 - 按统计月份(YYYYMM格式)过滤时间范围 88 - 按统计月份(YYYYMM格式)过滤时间范围
@@ -171,10 +174,10 @@ WHERE lq_xh_kjbsyj.F_IsEffective = 1 @@ -171,10 +174,10 @@ WHERE lq_xh_kjbsyj.F_IsEffective = 1
171 174
172 ### 步骤3:数据汇总(在职员工) 175 ### 步骤3:数据汇总(在职员工)
173 - 查询开单业绩:从 `lq_kd_kjbsyj` 表汇总 `kjblsyj` 176 - 查询开单业绩:从 `lq_kd_kjbsyj` 表汇总 `kjblsyj`
174 -- 查询消耗业绩:从 `lq_xh_kjbsyj` 表汇总 `kjblsyj`  
175 -- 查询退卡业绩:从 `lq_hytk_kjbsyj` 表汇总 `kjblsyj` 177 +- 查询消耗业绩:从 `lq_xh_kjbsyj` 表汇总 `kjblsyj`(用于消耗提成计算)
  178 +- 查询退卡业绩:从 `lq_hytk_kjbsyj` 表汇总 `kjblsyj`(扣除项)
176 - 查询项目数:从 `lq_xh_kjbsyj` 表汇总 `F_hdpxNumber` 179 - 查询项目数:从 `lq_xh_kjbsyj` 表汇总 `F_hdpxNumber`
177 -- 计算总业绩:开单业绩 + 消耗业绩 + 退卡业绩 180 +- 计算总业绩:开单业绩 - 退卡业绩(退卡是扣除)
178 181
179 ### 步骤4:工资计算(在职员工) 182 ### 步骤4:工资计算(在职员工)
180 183
@@ -277,7 +280,8 @@ GROUP BY kjbsyj.kjblszh, kjbsyj.kjblsxm, hyhk.md, md.mdbm, md.dm @@ -277,7 +280,8 @@ GROUP BY kjbsyj.kjblszh, kjbsyj.kjblsxm, hyhk.md, md.mdbm, md.dm
277 - 离职员工规则优先级最高 280 - 离职员工规则优先级最高
278 281
279 3. **数据一致性**: 282 3. **数据一致性**:
280 - - 总业绩 = 开单业绩 + 消耗业绩 + 退卡业绩 283 + - 总业绩 = 开单业绩 - 退卡业绩(退卡是扣除)
  284 + - 消耗业绩不包含在总业绩中,仅用于消耗提成计算
281 - 消耗和项目数必须来自同一数据源(`lq_xh_kjbsyj`) 285 - 消耗和项目数必须来自同一数据源(`lq_xh_kjbsyj`)
282 286
283 --- 287 ---
项目信息-薪酬规则与名词解释.md
@@ -39,8 +39,10 @@ @@ -39,8 +39,10 @@
39 ### 提成相关 39 ### 提成相关
40 12. **T区提成**:门店总业绩 × 0.05 × 0.05 40 12. **T区提成**:门店总业绩 × 0.05 × 0.05
41 13. **战队提成**:按照战队总业绩得到提成比例(提成比例有固定规则表) 41 13. **战队提成**:按照战队总业绩得到提成比例(提成比例有固定规则表)
42 -14. **基础业绩提成**:基础业绩 × 0.95 × 提成点  
43 -15. **合作业绩提成**:合作业绩 × 0.95 × 0.65 × 提成点 42 +14. **基础业绩提成**:基础业绩 × 0.95 × 提成点(剩余0.05归门店T区)
  43 +15. **合作业绩提成**:合作业绩 × 0.95 × 0.65 × 提成点(剩余0.05归门店T区)
  44 +16. **新客转化率提成**:新客业绩 × 转化率提成比例 × 0.95(剩余0.05归门店T区)
  45 +17. **升单业绩提成**:升单业绩 × 升单提点 × 0.95(剩余0.05归门店T区)
44 46
45 ### 考核相关 47 ### 考核相关
46 16. **主任、店长考核指标**:生命线-业绩、人头-固定、消耗-固定 48 16. **主任、店长考核指标**:生命线-业绩、人头-固定、消耗-固定
@@ -80,9 +82,19 @@ @@ -80,9 +82,19 @@
80 - **0星**:月消耗未达到10000元 且 项目数未达到96个 → 底薪1800元 82 - **0星**:月消耗未达到10000元 且 项目数未达到96个 → 底薪1800元
81 - **特殊情况**:月消耗或项目数中有一个为0星(未达到最低标准),底薪为一星(2000元) 83 - **特殊情况**:月消耗或项目数中有一个为0星(未达到最低标准),底薪为一星(2000元)
82 84
  85 +**底薪计算说明**:
  86 +- 底薪按日均计算:日均消耗 = 月消耗 / 当月天数,日均项目数 = 项目数 / 当月天数
  87 +- 星级标准按日均值判断:日均消耗 ≥ 10000/当月天数 且 日均项目数 ≥ 96/当月天数 → 一星
  88 +- 最终底薪 = (档位底薪 / 当月天数) × 在店天数
  89 +
83 #### 新店健康师薪酬规则 90 #### 新店健康师薪酬规则
84 **重要说明**:新店不考核消耗,健康师分成3个阶段: 91 **重要说明**:新店不考核消耗,健康师分成3个阶段:
85 92
  93 +**新店底薪规则**:
  94 +- 新店底薪计算时,仍然会计算消耗和项目数的星级
  95 +- 但如果计算出的底薪 < 2000元,则保底为2000元(一星)
  96 +- 即:新店底薪最低为一星(2000元),不满足一星条件时按一星计算
  97 +
86 **第一阶段:新客转化阶段** 98 **第一阶段:新客转化阶段**
87 - 主要考核新客转化率 99 - 主要考核新客转化率
88 - 提成主要来源于新客转化业绩 100 - 提成主要来源于新客转化业绩
@@ -90,6 +102,11 @@ @@ -90,6 +102,11 @@
90 **第二阶段:升单人头阶段** 102 **第二阶段:升单人头阶段**
91 - 主要考核升单人头数 103 - 主要考核升单人头数
92 - 提成主要来源于升单业绩 104 - 提成主要来源于升单业绩
  105 +- **升单提点规则**:
  106 + - 升单人头数 >= 10:升单提点 = 12%
  107 + - 升单人头数 >= 7 且 < 10:升单提点 = 10%
  108 + - 升单人头数 >= 4 且 < 7:升单提点 = 7%
  109 + - 升单人头数 < 4:升单提点 = 0%
93 110
94 **第三阶段:业绩和项目数阶段** 111 **第三阶段:业绩和项目数阶段**
95 - 无新客转化和升单人头提成 112 - 无新客转化和升单人头提成
@@ -124,9 +141,20 @@ @@ -124,9 +141,20 @@
124 141
125 ### 顾问提成规则 142 ### 顾问提成规则
126 143
  144 +#### 3人及以上战队
127 - 战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8% 145 - 战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8%
  146 +
  147 +#### 2人战队
128 - 战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3% 148 - 战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3%
129 149
  150 +#### 单人战队
  151 +- 单人或者一个人的战队就没有顾问,不计算顾问提成
  152 +
  153 +**注意**:
  154 +- 如果战队人数3人以上,按照3人的规则来计算
  155 +- "组员业绩"指除顾问外的其他成员业绩总和
  156 +- 新店顾问不考核消耗
  157 +
130 ### 店助考核规则 158 ### 店助考核规则
131 159
132 店助底薪规则 160 店助底薪规则