From 0e7e34606d9e15c1252a444547ef06afe308bfbd Mon Sep 17 00:00:00 2001 From: “wangming” <“wangming@antissoft.com”> Date: Fri, 19 Dec 2025 17:04:11 +0800 Subject: [PATCH] feat: 修改科技部老师工资提成为阶梯式,清洗流水接口返回送出/送回时间,主任工资合作成本查询使用YYYYMM格式 --- excel/合作成本表.xlsx | Bin 10010 -> 0 bytes netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs | 7 +++++++ netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs | 12 ++++++++++++ netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs | 31 +++++++++++++++++++------------ netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs | 30 +++++++++++++++++++++--------- netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs | 8 +++++--- netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++----------- netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs | 27 ++++++++++++++++----------- netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs | 33 ++++++++++++++++++++++----------- 主任工资计算逻辑完整梳理.md | 397 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 科技部老师工资计算规则.md | 25 +++++++++++++++++-------- 11 files changed, 569 insertions(+), 65 deletions(-) create mode 100644 主任工资计算逻辑完整梳理.md diff --git a/excel/合作成本表.xlsx b/excel/合作成本表.xlsx index 443d055..1ce4686 100644 Binary files a/excel/合作成本表.xlsx and b/excel/合作成本表.xlsx differ diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs index dc363eb..c19bc97 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqInventoryUsage/StoreReceiveStatisticsInput.cs @@ -29,6 +29,13 @@ namespace NCC.Extend.Entitys.Dto.LqInventoryUsage [StringLength(50, ErrorMessage = "门店ID长度不能超过50个字符")] [Display(Name = "门店ID")] public string StoreId { get; set; } + + /// + /// 仓库名称(可选,用于筛选特定仓库) + /// + [StringLength(100, ErrorMessage = "仓库名称长度不能超过100个字符")] + [Display(Name = "仓库名称")] + public string Warehouse { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs index 09071e6..17d03c6 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqLaundryFlow/LqLaundryFlowInfoOutput.cs @@ -109,6 +109,18 @@ namespace NCC.Extend.Entitys.Dto.LqLaundryFlow /// [Display(Name = "创建时间")] public DateTime createTime { get; set; } + + /// + /// 送出时间(流水类型为0时使用) + /// + [Display(Name = "送出时间")] + public DateTime? sendTime { get; set; } + + /// + /// 送回时间(流水类型为1时使用) + /// + [Display(Name = "送回时间")] + public DateTime? returnTime { get; set; } } } diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs index 446ba3b..5e9aa84 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs @@ -271,8 +271,10 @@ namespace NCC.Extend .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0)); // 1.10 合作项目成本统计 + // Month字段格式为"11"(月份数字),不是"202511"(YYYYMM格式) + var cooperationCostMonth = $"{month:D2}"; // 格式化为"11" var cooperationCostList = await _db.Queryable() - .Where(x => x.Year == year && x.Month == monthStr && x.IsEffective == StatusEnum.有效.GetHashCode()) + .Where(x => x.Year == year && x.Month == cooperationCostMonth && x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new { x.StoreId, x.TotalAmount }) .ToListAsync(); var cooperationCostDict = cooperationCostList @@ -372,26 +374,31 @@ namespace NCC.Extend // 2.3 获取门店目标信息(门店生命线、目标人头、目标消耗) if (!storeTargetDict.ContainsKey(storeId)) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算主任工资"); + // 如果没有配置目标数据,使用默认值0 + salary.StoreLifeline = 0; + salary.TargetHeadCount = 0; + salary.TargetConsume = 0; + } + else + { + var storeTarget = storeTargetDict[storeId]; + salary.StoreLifeline = storeTarget.StoreLifeline; + salary.TargetHeadCount = storeTarget.StoreHeadcountTarget; + salary.TargetConsume = storeTarget.StoreConsumeTarget; } - var storeTarget = storeTargetDict[storeId]; - salary.StoreLifeline = storeTarget.StoreLifeline; - salary.TargetHeadCount = storeTarget.StoreHeadcountTarget; - salary.TargetConsume = storeTarget.StoreConsumeTarget; - - // 数据校验:门店生命线、目标人头、目标消耗必须设置 + // 如果目标值未设置(<=0),使用默认值0,允许继续计算 if (salary.StoreLifeline <= 0) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】的门店生命线未设置,无法计算主任工资"); + salary.StoreLifeline = 0; } if (salary.TargetHeadCount <= 0) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标人头数未设置,无法计算主任工资"); + salary.TargetHeadCount = 0; } - if (!isNewStore && salary.TargetConsume <= 0) + if (salary.TargetConsume <= 0) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算主任工资(老店需要考核消耗)"); + salary.TargetConsume = 0; } // 2.4 计算销售业绩(开单业绩-退款业绩) diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs index ebc1b01..0d5d1e3 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs @@ -1590,7 +1590,8 @@ namespace NCC.Extend /// { /// "year": 2025, /// "month": 11, - /// "storeId": "门店ID(可选)" + /// "storeId": "门店ID(可选)", + /// "warehouse": "仓库名称(可选)" /// } /// ``` /// @@ -1598,6 +1599,7 @@ namespace NCC.Extend /// - year: 统计年份(必填,范围:2020-2100) /// - month: 统计月份(必填,范围:1-12) /// - storeId: 门店ID(可选,不传则统计所有门店) + /// - warehouse: 仓库名称(可选,不传则统计所有仓库) /// /// 返回说明: /// - 按门店分组统计 @@ -1651,15 +1653,25 @@ namespace NCC.Extend // 获取所有批次ID var batchIds = applications.Select(x => x.UsageBatchId).Distinct().ToList(); - // 查询这些批次的使用记录,计算总金额 - var usageRecords = await _db.Queryable() - .Where(x => batchIds.Contains(x.UsageBatchId)) - .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) - .Select(x => new + // 查询这些批次的使用记录,关联产品表获取仓库信息,计算总金额 + var usageRecordsQuery = _db.Queryable( + (usage, product) => usage.ProductId == product.Id) + .Where((usage, product) => batchIds.Contains(usage.UsageBatchId)) + .Where((usage, product) => usage.IsEffective == StatusEnum.有效.GetHashCode()); + + // 如果指定了仓库筛选,添加仓库条件 + if (!string.IsNullOrWhiteSpace(input.Warehouse)) + { + usageRecordsQuery = usageRecordsQuery.Where((usage, product) => product.Warehouse == input.Warehouse); + } + + var usageRecords = await usageRecordsQuery + .Select((usage, product) => new { - x.UsageBatchId, - x.StoreId, - x.TotalAmount + usage.UsageBatchId, + usage.StoreId, + usage.TotalAmount, + product.Warehouse }) .ToListAsync(); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs index ad9c813..509d82d 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqLaundryFlowService.cs @@ -473,7 +473,9 @@ namespace NCC.Extend isEffective = flow.IsEffective, createUser = flow.CreateUser, createUserName = "", - createTime = flow.CreateTime + createTime = flow.CreateTime, + sendTime = flow.SendTime, + returnTime = flow.ReturnTime }) .FirstAsync(); @@ -533,7 +535,7 @@ namespace NCC.Extend StoreName = store.Dm ?? "", flow.ProductType, SendQuantity = flow.Quantity, - SendTime = flow.CreateTime + SendTime = flow.SendTime }) .ToListAsync(); @@ -558,7 +560,7 @@ namespace NCC.Extend { x.BatchNumber, ReturnQuantity = x.Quantity, - ReturnTime = x.CreateTime, + ReturnTime = x.ReturnTime, x.Remark }) .ToListAsync(); diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs index 4a7b3a2..919bd31 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqSalaryExtraCalculationService.cs @@ -318,27 +318,39 @@ namespace NCC.Extend var entitiesToInsert = new List(); var entitiesToUpdate = new List(); + // 用于跟踪已处理的员工记录,避免重复(Key: EmployeeId_Year_Month) + var processedEmployeeRecords = new Dictionary(); + foreach (var item in importData) { try { - // 1. 查找用户ID(优先使用ID,否则通过姓名和电话查找) + // 1. 查找用户ID(优先使用Excel中的id作为员工ID,否则通过姓名和电话查找) UserEntity user = null; LqSalaryExtraCalculationEntity existingRecord = null; - // 如果提供了ID,先尝试根据ID查找现有记录,获取EmployeeId + // 优先使用Excel中的id作为员工ID(EmployeeId)直接查找用户 if (!string.IsNullOrEmpty(item.Id)) { - existingRecord = await _db.Queryable() - .Where(x => x.Id == item.Id) + // 先尝试将id作为员工ID查找用户 + user = await _db.Queryable() + .Where(u => u.Id == item.Id) .FirstAsync(); - if (existingRecord != null) + // 如果找不到,尝试将id作为记录ID查找现有记录,获取EmployeeId + if (user == null) { - // 通过EmployeeId查找用户 - user = await _db.Queryable() - .Where(u => u.Id == existingRecord.EmployeeId) + existingRecord = await _db.Queryable() + .Where(x => x.Id == item.Id) .FirstAsync(); + + if (existingRecord != null) + { + // 通过EmployeeId查找用户 + user = await _db.Queryable() + .Where(u => u.Id == existingRecord.EmployeeId) + .FirstAsync(); + } } } @@ -382,7 +394,27 @@ namespace NCC.Extend continue; } - // 2. 如果还没有找到现有记录,则根据健康师ID、年份、月份查找 + // 2. 检查是否已经在本次导入中处理过该员工(避免重复,使用员工ID而不是姓名) + var recordKey = $"{user.Id}_{item.Year}_{item.Month}"; + if (processedEmployeeRecords.ContainsKey(recordKey)) + { + // 如果已经处理过,更新记录而不是创建新记录 + existingRecord = processedEmployeeRecords[recordKey]; + existingRecord.BaseRewardPerformance = item.BaseRewardPerformance; + existingRecord.CooperationRewardPerformance = item.CooperationRewardPerformance; + existingRecord.NewCustomerPerformance = item.NewCustomerPerformance; + existingRecord.NewCustomerConversionRate = item.NewCustomerConversionRate; + existingRecord.UpgradePerformance = item.UpgradePerformance; + existingRecord.UpgradeConversionRate = item.UpgradeConversionRate; + existingRecord.UpgradeCustomerCount = item.UpgradeCustomerCount; + existingRecord.OtherPerformanceAdd = item.OtherPerformanceAdd; + existingRecord.OtherPerformanceSubtract = item.OtherPerformanceSubtract; + // 注意:这里不添加到entitiesToUpdate,因为已经在processedEmployeeRecords中,会在最后统一处理 + successCount++; + continue; + } + + // 3. 如果还没有找到现有记录,则根据健康师ID、年份、月份查找数据库中的记录 if (existingRecord == null) { existingRecord = await _db.Queryable() @@ -403,6 +435,8 @@ namespace NCC.Extend existingRecord.OtherPerformanceAdd = item.OtherPerformanceAdd; existingRecord.OtherPerformanceSubtract = item.OtherPerformanceSubtract; entitiesToUpdate.Add(existingRecord); + // 记录到已处理字典中 + processedEmployeeRecords[recordKey] = existingRecord; } else { @@ -424,6 +458,8 @@ namespace NCC.Extend OtherPerformanceSubtract = item.OtherPerformanceSubtract }; entitiesToInsert.Add(entity); + // 记录到已处理字典中(注意:这里需要创建一个临时对象,因为entity还没有ID) + processedEmployeeRecords[recordKey] = entity; } successCount++; @@ -441,12 +477,18 @@ namespace NCC.Extend await _db.Insertable(entitiesToInsert).ExecuteCommandAsync(); } - // 批量更新现有记录 + // 批量更新现有记录(去重,确保每个员工只有一条记录被更新) if (entitiesToUpdate.Any()) { + // 根据主键ID去重,确保每个记录只更新一次 + var uniqueEntitiesToUpdate = entitiesToUpdate + .GroupBy(x => x.Id) + .Select(g => g.First()) + .ToList(); + // 明确指定要更新的字段,确保所有字段都被更新(包括升单业绩) // 注意:Updateable接收实体列表时,会自动根据主键更新,不需要Where条件 - await _db.Updateable(entitiesToUpdate) + await _db.Updateable(uniqueEntitiesToUpdate) .UpdateColumns(it => new { it.BaseRewardPerformance, diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs index c2cd884..5763631 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqStoreManagerSalaryService.cs @@ -384,26 +384,31 @@ namespace NCC.Extend // 2.3 获取门店目标信息(门店生命线、目标人头、目标消耗) if (!storeTargetDict.ContainsKey(storeId)) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】在门店目标表中未配置{monthStr}月份的目标数据,无法计算店长工资"); + // 如果没有配置目标数据,使用默认值0 + salary.StoreLifeline = 0; + salary.TargetHeadCount = 0; + salary.TargetConsume = 0; + } + else + { + var storeTarget = storeTargetDict[storeId]; + salary.StoreLifeline = storeTarget.StoreLifeline; + salary.TargetHeadCount = storeTarget.StoreHeadcountTarget; + salary.TargetConsume = storeTarget.StoreConsumeTarget; } - var storeTarget = storeTargetDict[storeId]; - salary.StoreLifeline = storeTarget.StoreLifeline; - salary.TargetHeadCount = storeTarget.StoreHeadcountTarget; - salary.TargetConsume = storeTarget.StoreConsumeTarget; - - // 数据校验:门店生命线、目标人头必须设置 + // 如果目标值未设置(<=0),使用默认值0,允许继续计算 if (salary.StoreLifeline <= 0) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】的门店生命线未设置,无法计算店长工资"); + salary.StoreLifeline = 0; } if (salary.TargetHeadCount <= 0) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标人头数未设置,无法计算店长工资"); + salary.TargetHeadCount = 0; } - if (!isNewStore && salary.TargetConsume <= 0) + if (salary.TargetConsume <= 0) { - throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算店长工资(老店需要考核消耗)"); + salary.TargetConsume = 0; } // 2.4 计算门店业绩 diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs index 3118f50..f19ab56 100644 --- a/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs +++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechTeacherSalaryService.cs @@ -453,32 +453,43 @@ namespace NCC.Extend } /// - /// 计算业绩提成(分段累进) + /// 计算业绩提成(阶梯式) /// /// 总业绩 /// 提成比例和金额 private (decimal Rate, decimal Amount) CalculatePerformanceCommission(decimal totalPerformance) { - if (totalPerformance < 10000m) + // 提成前提:业绩必须大于1万才能进行提成 + if (totalPerformance <= 10000m) { - // < 10,000元 → 0% + // ≤ 10,000元 → 0%(无提成) return (0m, 0m); } - else if (totalPerformance < 70000m) + + decimal rate; + decimal amount; + + // 阶梯式提成计算(整个业绩按对应比例) + if (totalPerformance > 150000m) { - // 10,000-70,000元 → 2% - return (2m, totalPerformance * 0.02m); + // > 15万 → 3% + rate = 3m; + amount = totalPerformance * 0.03m; } - else if (totalPerformance < 150000m) + else if (totalPerformance > 70000m) { - // 70,000-150,000元 → 2.5% - return (2.5m, totalPerformance * 0.025m); + // > 7万 且 ≤ 15万 → 2.5% + rate = 2.5m; + amount = totalPerformance * 0.025m; } else { - // > 150,000元 → 3% - return (3m, totalPerformance * 0.03m); + // > 1万 且 ≤ 7万 → 2% + rate = 2m; + amount = totalPerformance * 0.02m; } + + return (rate, amount); } /// diff --git a/主任工资计算逻辑完整梳理.md b/主任工资计算逻辑完整梳理.md new file mode 100644 index 0000000..22e665f --- /dev/null +++ b/主任工资计算逻辑完整梳理.md @@ -0,0 +1,397 @@ +# 主任工资计算逻辑完整梳理 + +## 📋 概述 + +主任工资由以下几个部分组成: +1. **底薪**:固定3500元,根据考核指标扣款 +2. **提成**:基于**毛利**计算,使用阶梯提成模式 + +**重要说明**: +- **业绩(StoreTotalPerformance)就是毛利** +- **提成计算基于毛利**,而不是销售业绩(开单-退卡) + +--- + +## 💰 核心计算公式 + +### 1. 销售业绩 + +``` +销售业绩 = 开单业绩 - 退款业绩 +``` + +### 2. 毛利(核心指标) + +``` +毛利 = 销售业绩 - - 合作项目成本 - 店内支出 - 洗毛巾费用 +``` + +### 3. 业绩(用于提成计算) + +``` +业绩(StoreTotalPerformance)= 毛利(GrossProfit) +``` + +**关键点**:`StoreTotalPerformance` 字段存储的是**毛利**,用于提成计算。 + +--- + +## 📊 数据来源详解 + +### 1. 开单业绩 + +**数据来源**: +- 表:`lq_kd_kdjlb`(开单记录表) +- 字段:`sfyj`(实付业绩) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `Djmd = @StoreId`(门店ID) + - `Kdrq >= @StartDate AND Kdrq <= @EndDate`(时间范围) + +**代码位置**:第185-192行 + +### 2. 退款业绩 + +**数据来源**: +- 表:`lq_hytk_hytk`(退卡记录表) +- 字段:`F_ActualRefundAmount`(实际退款金额,如果没有则使用 `tkje`) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `md = @StoreId`(门店ID) + - `Tksj >= @StartDate AND Tksj <= @EndDate`(时间范围) + +**代码位置**:第194-202行 + +### 3. 产品物料 + +**数据来源**: +- 表:`lq_inventory_usage`(库存使用记录表) +- 字段:`F_TotalAmount`(合计金额) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `F_StoreId = @StoreId`(门店ID) + - **特殊规则**:11月工资算10月数据 + - 如果计算月份是11月,则查询10月的数据 + - 其他月份正常查询当月数据 + +**代码位置**:第252-271行 + +### 4. 合作项目成本 + +**数据来源**: +- 表:`lq_cooperation_cost`(合作成本表) +- 字段:`F_TotalAmount`(合计金额) +- 条件: + - `F_Year = @Year`(年份) + - `F_Month = @MonthStr`(月份,YYYYMM格式) + - `F_StoreId = @StoreId`(门店ID) + - `F_IsEffective = 1`(有效记录) + +**代码位置**:第273-281行 + +### 5. 店内支出 + +**数据来源**: +- 表:`lq_store_expense`(店内支出表) +- 字段:`F_Amount`(金额) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `F_StoreId = @StoreId`(门店ID) + - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @MonthStr`(月份,YYYYMM格式) + +**代码位置**:第283-296行 + +### 6. 洗毛巾费用 + +**数据来源**: +- 表:`lq_laundry_flow`(清洗流水表) +- 字段:`F_TotalPrice`(总费用) +- 条件: + - `F_IsEffective = 1`(有效记录) + - `F_FlowType = 0`(只统计送出的记录) + - `F_StoreId = @StoreId`(门店ID) + - 优先使用 `F_SendTime`,如果为空则使用 `F_CreateTime` + - `DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @MonthStr`(月份,YYYYMM格式) + +**代码位置**:第298-313行 + +--- + +## 🔄 计算流程 + +### 步骤1:计算销售业绩 + +```csharp +// 2.4 计算销售业绩(开单业绩-退款业绩) +decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0; +decimal refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0; +salary.StoreBillingPerformance = billing; +salary.StoreRefundPerformance = refund; +salary.SalesPerformance = billing - refund; +``` + +**代码位置**:第402-407行 + +### 步骤2:统计各项成本 + +```csharp +// 2.5 统计各项成本 +salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0; +salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0; +salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0; +salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0; +``` + +**代码位置**:第409-420行 + +### 步骤3:计算毛利 + +```csharp +// 2.6 计算毛利 +// 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾 +salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost; +``` + +**代码位置**:第422-424行 + +### 步骤4:将毛利赋值给StoreTotalPerformance(用于提成计算) + +```csharp +// 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算) +salary.StoreTotalPerformance = salary.GrossProfit; +``` + +**代码位置**:第426-427行 + +**关键点**:`StoreTotalPerformance` 存储的是**毛利**,不是销售业绩。 + +### 步骤5:计算业绩完成率(基于毛利) + +```csharp +// 2.8 计算业绩完成率(基于毛利与生命线比较) +if (salary.StoreLifeline > 0) +{ + salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline; +} +``` + +**代码位置**:第429-437行 + +### 步骤6:判断业绩是否达标(基于毛利) + +```csharp +// 2.11 计算考核指标(业绩、人头、消耗是否达标) +// 业绩达标判断基于毛利 +bool performanceReached = salary.GrossProfit >= salary.StoreLifeline; +``` + +**代码位置**:第445-447行 + +### 步骤7:计算提成(基于毛利) + +```csharp +// 2.14 计算阶梯提成(先计算门店总提成,基于毛利) +CalculateCommission(salary, isNewStore); +``` + +**代码位置**:第495-496行 + +**提成计算方法**: + +```csharp +// 提成计算基于毛利(StoreTotalPerformance存储的是毛利) +decimal performance = salary.StoreTotalPerformance; // 这里已经是毛利了 +decimal lifeline = salary.StoreLifeline; + +// 计算阶梯提成 +if (performance <= lifeline) +{ + // 业绩 ≤ 生命线:只计算≤生命线部分的提成 + salary.CommissionAmountBelowLifeline = performance * rateBelowLifeline; + salary.CommissionAmountAboveLifeline = 0; +} +else +{ + // 业绩 > 生命线:分别计算≤生命线部分和>生命线部分的提成 + salary.CommissionAmountBelowLifeline = lifeline * rateBelowLifeline; + salary.CommissionAmountAboveLifeline = (performance - lifeline) * rateAboveLifeline; +} +``` + +**代码位置**:第567-640行 + +--- + +## 💰 提成规则详解 + +### 提成计算方式 + +**阶梯提成模式**:根据业绩是否超过生命线,使用不同的提成比例。 + +### 老店主任提成规则 + +根据门店分类(A、B、C类)和业绩是否超过生命线,使用不同的阶梯提成比例: + +| 门店分类 | 业绩 ≤ 生命线部分 | 业绩 > 生命线部分 | +|---------|----------------|-----------------| +| A类门店 | 2% | 2.5% | +| B类门店 | 2.5% | 3% | +| C类门店 | 3% | 3.5% | + +**计算公式**: +``` +如果 业绩(毛利)≤ 生命线: + 提成 = 业绩(毛利) × 对应提成比例(≤生命线部分) + +如果 业绩(毛利)> 生命线: + 提成 = 生命线 × 对应提成比例(≤生命线部分) + (业绩(毛利) - 生命线) × 对应提成比例(>生命线部分) +``` + +### 新店主任提成规则 + +**统一标准**,不区分门店分类: + +| 业绩范围 | 提成比例 | +|---------|---------| +| 业绩 ≤ 生命线部分 | 2% | +| 业绩 > 生命线部分 | 2.5% | + +**计算公式**: +``` +如果 业绩(毛利)≤ 生命线: + 提成 = 业绩(毛利) × 2% + +如果 业绩(毛利)> 生命线: + 提成 = 生命线 × 2% + (业绩(毛利) - 生命线) × 2.5% +``` + +### 提成按在店天数比例计算 + +```csharp +// 2.15 按在店天数比例计算提成金额 +if (daysInMonth > 0 && workingDays > 0) +{ + // 提成金额按在店天数比例计算 + salary.CommissionAmountBelowLifeline = salary.CommissionAmountBelowLifeline / daysInMonth * workingDays; + salary.CommissionAmountAboveLifeline = salary.CommissionAmountAboveLifeline / daysInMonth * workingDays; + salary.TotalCommissionAmount = salary.CommissionAmountBelowLifeline + salary.CommissionAmountAboveLifeline; +} +``` + +**代码位置**:第498-512行 + +--- + +## 📝 关键字段说明 + +### StoreTotalPerformance(门店总业绩) + +**字段含义**:**毛利** + +**计算公式**: +``` +StoreTotalPerformance = GrossProfit = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用 +``` + +**用途**: +- 用于提成计算 +- 用于业绩完成率计算 +- 用于业绩达标判断 + +**重要说明**:此字段存储的是**毛利**,不是销售业绩(开单-退卡)。 + +### GrossProfit(毛利) + +**字段含义**:毛利 + +**计算公式**: +``` +GrossProfit = SalesPerformance - ProductMaterial - CooperationCost - StoreExpense - LaundryCost +``` + +### SalesPerformance(销售业绩) + +**字段含义**:销售业绩(开单-退卡) + +**计算公式**: +``` +SalesPerformance = StoreBillingPerformance - StoreRefundPerformance +``` + +--- + +## ✅ 验证要点 + +### 1. 业绩就是毛利 + +- ✅ `StoreTotalPerformance = GrossProfit`(第427行) +- ✅ 提成计算使用 `StoreTotalPerformance`(第581行) +- ✅ 业绩完成率使用 `GrossProfit`(第432行) +- ✅ 业绩达标判断使用 `GrossProfit`(第447行) + +### 2. 提成计算基于毛利 + +- ✅ `CalculateCommission` 方法注释明确说明"基于毛利"(第563行) +- ✅ 方法内部使用 `StoreTotalPerformance`(即毛利)进行计算(第581行) +- ✅ 所有提成计算都基于 `performance`(即毛利) + +--- + +## 📋 总结 + +### 核心结论 + +1. **业绩(StoreTotalPerformance)就是毛利** + - `StoreTotalPerformance = GrossProfit` + - 不是销售业绩(开单-退卡) + +2. **提成计算基于毛利** + - 提成计算使用 `StoreTotalPerformance`(即毛利) + - 不是基于销售业绩 + +3. **毛利计算公式** + ``` + 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用 + ``` + +4. **所有业绩相关的判断都基于毛利** + - 业绩完成率 = 毛利 / 生命线 + - 业绩达标判断 = 毛利 >= 生命线 + - 提成计算 = 基于毛利使用阶梯提成 + +### 数据流向 + +``` +开单业绩 - 退款业绩 + ↓ +销售业绩(SalesPerformance) + ↓ +销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾费用 + ↓ +毛利(GrossProfit) + ↓ +StoreTotalPerformance(用于提成计算) + ↓ +阶梯提成计算 +``` + +--- + +## 🔍 代码关键位置 + +| 功能 | 代码位置 | 说明 | +|-----|---------|------| +| 计算销售业绩 | 第402-407行 | 开单-退卡 | +| 统计各项成本 | 第409-420行 | 产品物料、合作成本、店内支出、洗毛巾 | +| 计算毛利 | 第422-424行 | 销售业绩 - 各项成本 | +| 将毛利赋值给StoreTotalPerformance | 第426-427行 | **关键:业绩就是毛利** | +| 计算业绩完成率 | 第429-437行 | 基于毛利 | +| 判断业绩达标 | 第445-447行 | 基于毛利 | +| 计算提成 | 第495-496行 | 调用CalculateCommission,基于毛利 | +| 提成计算方法 | 第567-640行 | 使用StoreTotalPerformance(即毛利)计算 | + +--- + +**文档版本**:2025-01-20 +**最后更新**:确认业绩就是毛利,提成计算基于毛利 + diff --git a/科技部老师工资计算规则.md b/科技部老师工资计算规则.md index 8e943fa..6530051 100644 --- a/科技部老师工资计算规则.md +++ b/科技部老师工资计算规则.md @@ -29,18 +29,27 @@ ### 2. 业绩提成规则 -业绩提成基于**总业绩**计算,采用分段累进方式: +业绩提成基于**总业绩**计算,采用阶梯式方式: -| 总业绩范围 | 提成比例 | -|-----------|---------| -| < 10,000元 | 0% (无提成) | -| 10,000元 - 70,000元 | 2% | -| 70,000元 - 150,000元 | 2.5% | -| > 150,000元 | 3% | +**提成前提**:业绩必须大于1万才能进行提成 + +| 总业绩范围 | 提成比例 | 说明 | +|-----------|---------|------| +| ≤ 10,000元 | 0% | 无提成 | +| > 10,000元 且 ≤ 70,000元 | 2% | 整个业绩按2%计算 | +| > 70,000元 且 ≤ 150,000元 | 2.5% | 整个业绩按2.5%计算 | +| > 150,000元 | 3% | 整个业绩按3%计算 | **计算说明**: - 提成金额 = 总业绩 × 对应提成比例 -- 采用分段计算,不同区间按不同比例计算 +- 采用阶梯式计算,整个业绩按对应区间的比例计算(不是分段累进) +- 业绩必须大于1万才有提成资格 + +**示例**: +- 总业绩 = 5,000元 → 提成 = 0(无提成,未达到1万门槛) +- 总业绩 = 50,000元 → 提成 = 50,000 × 2% = 1,000元 +- 总业绩 = 100,000元 → 提成 = 100,000 × 2.5% = 2,500元 +- 总业绩 = 200,000元 → 提成 = 200,000 × 3% = 6,000元 --- -- libgit2 0.21.4