Commit c63026bcaa223e67e238bb9c9cde58fc3017bb5c

Authored by “wangming”
1 parent a32e3ff6

feat: 优化工资计算逻辑

1. 主任工资计算改为基于毛利
   - 新增毛利相关字段(销售业绩、产品物料、合作项目成本、店内支出、洗毛巾费用、毛利)
   - 提成计算基于毛利而非销售业绩
   - 业绩完成率和业绩达标判断基于毛利

2. 店助工资计算优化
   - 底薪按在店天数比例计算
   - 提成按在店天数比例计算
   - 手机管理费按在店天数比例计算
   - 阶段奖励按在店天数比例计算

3. 主任工资计算优化
   - 底薪按在店天数比例计算
   - 提成按在店天数比例计算

4. 新增工资计算功能
   - 大项目主管工资计算
   - 科技部总经理工资计算
Showing 32 changed files with 4638 additions and 257 deletions
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryInput.cs 0 → 100644
  1 +using NCC.Common.Filter;
  2 +using System;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary
  5 +{
  6 + /// <summary>
  7 + /// 大项目主管工资查询参数
  8 + /// </summary>
  9 + public class MajorProjectDirectorSalaryInput : PageInputBase
  10 + {
  11 + /// <summary>
  12 + /// 年份
  13 + /// </summary>
  14 + public int Year { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 月份
  18 + /// </summary>
  19 + public int Month { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 岗位(大项目一部/大项目二部等,不传则查询全部)
  23 + /// </summary>
  24 + public string Position { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 员工姓名/账号(可选,用于模糊搜索)
  28 + /// </summary>
  29 + public string Keyword { get; set; }
  30 + }
  31 +}
  32 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary
  4 +{
  5 + /// <summary>
  6 + /// 大项目主管工资输出
  7 + /// </summary>
  8 + public class MajorProjectDirectorSalaryOutput
  9 + {
  10 + /// <summary>
  11 + /// 主键ID
  12 + /// </summary>
  13 + public string Id { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 统计月份
  17 + /// </summary>
  18 + public string StatisticsMonth { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 核算岗位(大项目一部/大项目二部等)
  22 + /// </summary>
  23 + public string Position { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 员工姓名
  27 + /// </summary>
  28 + public string EmployeeName { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 员工ID
  32 + /// </summary>
  33 + public string EmployeeId { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 员工账号
  37 + /// </summary>
  38 + public string EmployeeAccount { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 是否离职
  42 + /// </summary>
  43 + public int IsTerminated { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 管理的门店明细(JSON格式)
  47 + /// </summary>
  48 + public string StoreDetail { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 总业绩
  52 + /// </summary>
  53 + public decimal TotalPerformance { get; set; }
  54 +
  55 + /// <summary>
  56 + /// 开单金额
  57 + /// </summary>
  58 + public decimal BillingAmount { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 退卡金额
  62 + /// </summary>
  63 + public decimal RefundAmount { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 底薪
  67 + /// </summary>
  68 + public decimal BaseSalary { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 提成比例
  72 + /// </summary>
  73 + public decimal? CommissionRate { get; set; }
  74 +
  75 + /// <summary>
  76 + /// 提成金额
  77 + /// </summary>
  78 + public decimal CommissionAmount { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 在店天数
  82 + /// </summary>
  83 + public decimal WorkingDays { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 请假天数
  87 + /// </summary>
  88 + public decimal LeaveDays { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 核算应发工资
  92 + /// </summary>
  93 + public decimal CalculatedGrossSalary { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 最终应发工资
  97 + /// </summary>
  98 + public decimal FinalGrossSalary { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 当月培训补贴
  102 + /// </summary>
  103 + public decimal MonthlyTrainingSubsidy { get; set; }
  104 +
  105 + /// <summary>
  106 + /// 当月交通补贴
  107 + /// </summary>
  108 + public decimal MonthlyTransportSubsidy { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 上月培训补贴
  112 + /// </summary>
  113 + public decimal LastMonthTrainingSubsidy { get; set; }
  114 +
  115 + /// <summary>
  116 + /// 上月交通补贴
  117 + /// </summary>
  118 + public decimal LastMonthTransportSubsidy { get; set; }
  119 +
  120 + /// <summary>
  121 + /// 补贴合计
  122 + /// </summary>
  123 + public decimal TotalSubsidy { get; set; }
  124 +
  125 + /// <summary>
  126 + /// 缺卡扣款
  127 + /// </summary>
  128 + public decimal MissingCard { get; set; }
  129 +
  130 + /// <summary>
  131 + /// 迟到扣款
  132 + /// </summary>
  133 + public decimal LateArrival { get; set; }
  134 +
  135 + /// <summary>
  136 + /// 请假扣款
  137 + /// </summary>
  138 + public decimal LeaveDeduction { get; set; }
  139 +
  140 + /// <summary>
  141 + /// 扣社保
  142 + /// </summary>
  143 + public decimal SocialInsuranceDeduction { get; set; }
  144 +
  145 + /// <summary>
  146 + /// 扣除奖励
  147 + /// </summary>
  148 + public decimal RewardDeduction { get; set; }
  149 +
  150 + /// <summary>
  151 + /// 扣住宿费
  152 + /// </summary>
  153 + public decimal AccommodationDeduction { get; set; }
  154 +
  155 + /// <summary>
  156 + /// 扣学习期费用
  157 + /// </summary>
  158 + public decimal StudyPeriodDeduction { get; set; }
  159 +
  160 + /// <summary>
  161 + /// 扣工作服费用
  162 + /// </summary>
  163 + public decimal WorkClothesDeduction { get; set; }
  164 +
  165 + /// <summary>
  166 + /// 扣款合计
  167 + /// </summary>
  168 + public decimal TotalDeduction { get; set; }
  169 +
  170 + /// <summary>
  171 + /// 发奖金
  172 + /// </summary>
  173 + public decimal Bonus { get; set; }
  174 +
  175 + /// <summary>
  176 + /// 退手机押金
  177 + /// </summary>
  178 + public decimal ReturnPhoneDeposit { get; set; }
  179 +
  180 + /// <summary>
  181 + /// 退住宿押金
  182 + /// </summary>
  183 + public decimal ReturnAccommodationDeposit { get; set; }
  184 +
  185 + /// <summary>
  186 + /// 实发工资
  187 + /// </summary>
  188 + public decimal ActualSalary { get; set; }
  189 +
  190 + /// <summary>
  191 + /// 当月是否发放
  192 + /// </summary>
  193 + public string MonthlyPaymentStatus { get; set; }
  194 +
  195 + /// <summary>
  196 + /// 支付金额
  197 + /// </summary>
  198 + public decimal PaidAmount { get; set; }
  199 +
  200 + /// <summary>
  201 + /// 待支付金额
  202 + /// </summary>
  203 + public decimal PendingAmount { get; set; }
  204 +
  205 + /// <summary>
  206 + /// 补发上月
  207 + /// </summary>
  208 + public decimal LastMonthSupplement { get; set; }
  209 +
  210 + /// <summary>
  211 + /// 当月支付总额
  212 + /// </summary>
  213 + public decimal MonthlyTotalPayment { get; set; }
  214 +
  215 + /// <summary>
  216 + /// 是否锁定
  217 + /// </summary>
  218 + public int IsLocked { get; set; }
  219 +
  220 + /// <summary>
  221 + /// 更新时间
  222 + /// </summary>
  223 + public DateTime UpdateTime { get; set; }
  224 + }
  225 +}
  226 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInfoOutput.cs
... ... @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqProduct
23 23 public decimal price { get; set; }
24 24  
25 25 /// <summary>
  26 + /// 平均单价(加权平均成本,用于出库计价)
  27 + /// </summary>
  28 + public decimal averagePrice { get; set; }
  29 +
  30 + /// <summary>
26 31 /// 产品类别
27 32 /// </summary>
28 33 public string productCategory { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListOutput.cs
... ... @@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqProduct
23 23 public decimal price { get; set; }
24 24  
25 25 /// <summary>
  26 + /// 平均单价(加权平均成本,用于出库计价)
  27 + /// </summary>
  28 + public decimal averagePrice { get; set; }
  29 +
  30 + /// <summary>
26 31 /// 产品类别
27 32 /// </summary>
28 33 public string productCategory { get; set; }
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryInput.cs 0 → 100644
  1 +using NCC.Common.Filter;
  2 +using System;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary
  5 +{
  6 + /// <summary>
  7 + /// 科技部总经理工资查询参数
  8 + /// </summary>
  9 + public class TechGeneralManagerSalaryInput : PageInputBase
  10 + {
  11 + /// <summary>
  12 + /// 年份
  13 + /// </summary>
  14 + public int Year { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 月份
  18 + /// </summary>
  19 + public int Month { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 岗位(科技一部/科技二部等,不传则查询全部)
  23 + /// </summary>
  24 + public string Position { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 员工姓名/账号(可选,用于模糊搜索)
  28 + /// </summary>
  29 + public string Keyword { get; set; }
  30 + }
  31 +}
  32 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryOutput.cs 0 → 100644
  1 +using System;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary
  4 +{
  5 + /// <summary>
  6 + /// 科技部总经理工资输出
  7 + /// </summary>
  8 + public class TechGeneralManagerSalaryOutput
  9 + {
  10 + /// <summary>
  11 + /// 主键ID
  12 + /// </summary>
  13 + public string Id { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 统计月份
  17 + /// </summary>
  18 + public string StatisticsMonth { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 核算岗位(科技一部/科技二部等)
  22 + /// </summary>
  23 + public string Position { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 员工姓名
  27 + /// </summary>
  28 + public string EmployeeName { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 员工ID
  32 + /// </summary>
  33 + public string EmployeeId { get; set; }
  34 +
  35 + /// <summary>
  36 + /// 员工账号
  37 + /// </summary>
  38 + public string EmployeeAccount { get; set; }
  39 +
  40 + /// <summary>
  41 + /// 是否离职
  42 + /// </summary>
  43 + public int IsTerminated { get; set; }
  44 +
  45 + /// <summary>
  46 + /// 管理的门店明细(JSON格式)
  47 + /// </summary>
  48 + public string StoreDetail { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 溯源金额
  52 + /// </summary>
  53 + public decimal TraceabilityAmount { get; set; }
  54 +
  55 + /// <summary>
  56 + /// Cell金额
  57 + /// </summary>
  58 + public decimal CellAmount { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 底薪
  62 + /// </summary>
  63 + public decimal BaseSalary { get; set; }
  64 +
  65 + /// <summary>
  66 + /// 溯源金额提成比例
  67 + /// </summary>
  68 + public decimal? TraceabilityCommissionRate { get; set; }
  69 +
  70 + /// <summary>
  71 + /// 溯源金额提成金额
  72 + /// </summary>
  73 + public decimal TraceabilityCommissionAmount { get; set; }
  74 +
  75 + /// <summary>
  76 + /// Cell金额提成比例
  77 + /// </summary>
  78 + public decimal? CellCommissionRate { get; set; }
  79 +
  80 + /// <summary>
  81 + /// Cell金额提成金额
  82 + /// </summary>
  83 + public decimal CellCommissionAmount { get; set; }
  84 +
  85 + /// <summary>
  86 + /// 提成合计
  87 + /// </summary>
  88 + public decimal TotalCommission { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 在店天数
  92 + /// </summary>
  93 + public decimal WorkingDays { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 请假天数
  97 + /// </summary>
  98 + public decimal LeaveDays { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 核算应发工资
  102 + /// </summary>
  103 + public decimal CalculatedGrossSalary { get; set; }
  104 +
  105 + /// <summary>
  106 + /// 最终应发工资
  107 + /// </summary>
  108 + public decimal FinalGrossSalary { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 当月培训补贴
  112 + /// </summary>
  113 + public decimal MonthlyTrainingSubsidy { get; set; }
  114 +
  115 + /// <summary>
  116 + /// 当月交通补贴
  117 + /// </summary>
  118 + public decimal MonthlyTransportSubsidy { get; set; }
  119 +
  120 + /// <summary>
  121 + /// 上月培训补贴
  122 + /// </summary>
  123 + public decimal LastMonthTrainingSubsidy { get; set; }
  124 +
  125 + /// <summary>
  126 + /// 上月交通补贴
  127 + /// </summary>
  128 + public decimal LastMonthTransportSubsidy { get; set; }
  129 +
  130 + /// <summary>
  131 + /// 补贴合计
  132 + /// </summary>
  133 + public decimal TotalSubsidy { get; set; }
  134 +
  135 + /// <summary>
  136 + /// 缺卡扣款
  137 + /// </summary>
  138 + public decimal MissingCard { get; set; }
  139 +
  140 + /// <summary>
  141 + /// 迟到扣款
  142 + /// </summary>
  143 + public decimal LateArrival { get; set; }
  144 +
  145 + /// <summary>
  146 + /// 请假扣款
  147 + /// </summary>
  148 + public decimal LeaveDeduction { get; set; }
  149 +
  150 + /// <summary>
  151 + /// 扣社保
  152 + /// </summary>
  153 + public decimal SocialInsuranceDeduction { get; set; }
  154 +
  155 + /// <summary>
  156 + /// 扣除奖励
  157 + /// </summary>
  158 + public decimal RewardDeduction { get; set; }
  159 +
  160 + /// <summary>
  161 + /// 扣住宿费
  162 + /// </summary>
  163 + public decimal AccommodationDeduction { get; set; }
  164 +
  165 + /// <summary>
  166 + /// 扣学习期费用
  167 + /// </summary>
  168 + public decimal StudyPeriodDeduction { get; set; }
  169 +
  170 + /// <summary>
  171 + /// 扣工作服费用
  172 + /// </summary>
  173 + public decimal WorkClothesDeduction { get; set; }
  174 +
  175 + /// <summary>
  176 + /// 扣款合计
  177 + /// </summary>
  178 + public decimal TotalDeduction { get; set; }
  179 +
  180 + /// <summary>
  181 + /// 发奖金
  182 + /// </summary>
  183 + public decimal Bonus { get; set; }
  184 +
  185 + /// <summary>
  186 + /// 退手机押金
  187 + /// </summary>
  188 + public decimal ReturnPhoneDeposit { get; set; }
  189 +
  190 + /// <summary>
  191 + /// 退住宿押金
  192 + /// </summary>
  193 + public decimal ReturnAccommodationDeposit { get; set; }
  194 +
  195 + /// <summary>
  196 + /// 实发工资
  197 + /// </summary>
  198 + public decimal ActualSalary { get; set; }
  199 +
  200 + /// <summary>
  201 + /// 当月是否发放
  202 + /// </summary>
  203 + public string MonthlyPaymentStatus { get; set; }
  204 +
  205 + /// <summary>
  206 + /// 支付金额
  207 + /// </summary>
  208 + public decimal PaidAmount { get; set; }
  209 +
  210 + /// <summary>
  211 + /// 待支付金额
  212 + /// </summary>
  213 + public decimal PendingAmount { get; set; }
  214 +
  215 + /// <summary>
  216 + /// 补发上月
  217 + /// </summary>
  218 + public decimal LastMonthSupplement { get; set; }
  219 +
  220 + /// <summary>
  221 + /// 当月支付总额
  222 + /// </summary>
  223 + public decimal MonthlyTotalPayment { get; set; }
  224 +
  225 + /// <summary>
  226 + /// 是否锁定
  227 + /// </summary>
  228 + public int IsLocked { get; set; }
  229 +
  230 + /// <summary>
  231 + /// 更新时间
  232 + /// </summary>
  233 + public DateTime UpdateTime { get; set; }
  234 + }
  235 +}
  236 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_director_salary_statistics/LqDirectorSalaryStatisticsEntity.cs
... ... @@ -96,6 +96,42 @@ namespace NCC.Extend.Entitys.lq_director_salary_statistics
96 96 public decimal StoreRefundPerformance { get; set; }
97 97  
98 98 /// <summary>
  99 + /// 销售业绩(开单业绩-退款业绩)
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_SalesPerformance")]
  102 + public decimal SalesPerformance { get; set; }
  103 +
  104 + /// <summary>
  105 + /// 产品物料(仓库领用金额)
  106 + /// </summary>
  107 + [SugarColumn(ColumnName = "F_ProductMaterial")]
  108 + public decimal ProductMaterial { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 合作项目成本
  112 + /// </summary>
  113 + [SugarColumn(ColumnName = "F_CooperationCost")]
  114 + public decimal CooperationCost { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 店内支出
  118 + /// </summary>
  119 + [SugarColumn(ColumnName = "F_StoreExpense")]
  120 + public decimal StoreExpense { get; set; }
  121 +
  122 + /// <summary>
  123 + /// 洗毛巾费用
  124 + /// </summary>
  125 + [SugarColumn(ColumnName = "F_LaundryCost")]
  126 + public decimal LaundryCost { get; set; }
  127 +
  128 + /// <summary>
  129 + /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
  130 + /// </summary>
  131 + [SugarColumn(ColumnName = "F_GrossProfit")]
  132 + public decimal GrossProfit { get; set; }
  133 +
  134 + /// <summary>
99 135 /// 门店生命线
100 136 /// </summary>
101 137 [SugarColumn(ColumnName = "F_StoreLifeline")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_application_node/LqInventoryUsageApplicationNodeEntity.cs
... ... @@ -66,3 +66,5 @@ namespace NCC.Extend.Entitys.lq_inventory_usage_application_node
66 66  
67 67  
68 68  
  69 +
  70 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_approval_record/LqInventoryUsageApprovalRecordEntity.cs
... ... @@ -84,3 +84,5 @@ namespace NCC.Extend.Entitys.lq_inventory_usage_approval_record
84 84  
85 85  
86 86  
  87 +
  88 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_major_project_director_salary_statistics/LqMajorProjectDirectorSalaryStatisticsEntity.cs 0 → 100644
  1 +using System;
  2 +using NCC.Common.Const;
  3 +using SqlSugar;
  4 +
  5 +namespace NCC.Extend.Entitys.lq_major_project_director_salary_statistics
  6 +{
  7 + /// <summary>
  8 + /// 大项目主管工资统计表
  9 + /// </summary>
  10 + [SugarTable("lq_major_project_director_salary_statistics")]
  11 + [Tenant(ClaimConst.TENANT_ID)]
  12 + public class LqMajorProjectDirectorSalaryStatisticsEntity
  13 + {
  14 + /// <summary>
  15 + /// 主键ID
  16 + /// </summary>
  17 + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
  18 + public string Id { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 统计月份(YYYYMM)
  22 + /// </summary>
  23 + [SugarColumn(ColumnName = "F_StatisticsMonth", Length = 6)]
  24 + public string StatisticsMonth { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 核算岗位(大项目一部/大项目二部等)
  28 + /// </summary>
  29 + [SugarColumn(ColumnName = "F_Position")]
  30 + public string Position { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 员工姓名
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_EmployeeName")]
  36 + public string EmployeeName { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 员工ID
  40 + /// </summary>
  41 + [SugarColumn(ColumnName = "F_EmployeeId")]
  42 + public string EmployeeId { get; set; }
  43 +
  44 + /// <summary>
  45 + /// 员工账号
  46 + /// </summary>
  47 + [SugarColumn(ColumnName = "F_EmployeeAccount")]
  48 + public string EmployeeAccount { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 是否离职(0=在职,1=离职)
  52 + /// </summary>
  53 + [SugarColumn(ColumnName = "F_IsTerminated")]
  54 + public int IsTerminated { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 管理的门店明细(JSON格式)
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "F_StoreDetail", ColumnDataType = "TEXT")]
  60 + public string StoreDetail { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 总业绩(管理的所有门店的总业绩总和,开单-退卡)
  64 + /// </summary>
  65 + [SugarColumn(ColumnName = "F_TotalPerformance")]
  66 + public decimal TotalPerformance { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 开单金额(管理的所有门店的开单金额总和)
  70 + /// </summary>
  71 + [SugarColumn(ColumnName = "F_BillingAmount")]
  72 + public decimal BillingAmount { get; set; }
  73 +
  74 + /// <summary>
  75 + /// 退卡金额(管理的所有门店的退卡金额总和)
  76 + /// </summary>
  77 + [SugarColumn(ColumnName = "F_RefundAmount")]
  78 + public decimal RefundAmount { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 底薪金额(固定3500元)
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_BaseSalary")]
  84 + public decimal BaseSalary { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 提成比例(根据总业绩分段:0%/1%/1.5%)
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_CommissionRate")]
  90 + public decimal? CommissionRate { get; set; }
  91 +
  92 + /// <summary>
  93 + /// 提成金额(总业绩 × 提成比例)
  94 + /// </summary>
  95 + [SugarColumn(ColumnName = "F_CommissionAmount")]
  96 + public decimal CommissionAmount { get; set; }
  97 +
  98 + /// <summary>
  99 + /// 在店天数
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_WorkingDays")]
  102 + public decimal WorkingDays { get; set; }
  103 +
  104 + /// <summary>
  105 + /// 请假天数
  106 + /// </summary>
  107 + [SugarColumn(ColumnName = "F_LeaveDays")]
  108 + public decimal LeaveDays { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 核算应发工资(底薪 + 提成金额)
  112 + /// </summary>
  113 + [SugarColumn(ColumnName = "F_CalculatedGrossSalary")]
  114 + public decimal CalculatedGrossSalary { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 最终应发工资
  118 + /// </summary>
  119 + [SugarColumn(ColumnName = "F_FinalGrossSalary")]
  120 + public decimal FinalGrossSalary { get; set; }
  121 +
  122 + /// <summary>
  123 + /// 当月培训补贴
  124 + /// </summary>
  125 + [SugarColumn(ColumnName = "F_MonthlyTrainingSubsidy")]
  126 + public decimal MonthlyTrainingSubsidy { get; set; }
  127 +
  128 + /// <summary>
  129 + /// 当月交通补贴
  130 + /// </summary>
  131 + [SugarColumn(ColumnName = "F_MonthlyTransportSubsidy")]
  132 + public decimal MonthlyTransportSubsidy { get; set; }
  133 +
  134 + /// <summary>
  135 + /// 上月培训补贴
  136 + /// </summary>
  137 + [SugarColumn(ColumnName = "F_LastMonthTrainingSubsidy")]
  138 + public decimal LastMonthTrainingSubsidy { get; set; }
  139 +
  140 + /// <summary>
  141 + /// 上月交通补贴
  142 + /// </summary>
  143 + [SugarColumn(ColumnName = "F_LastMonthTransportSubsidy")]
  144 + public decimal LastMonthTransportSubsidy { get; set; }
  145 +
  146 + /// <summary>
  147 + /// 补贴合计
  148 + /// </summary>
  149 + [SugarColumn(ColumnName = "F_TotalSubsidy")]
  150 + public decimal TotalSubsidy { get; set; }
  151 +
  152 + /// <summary>
  153 + /// 缺卡扣款
  154 + /// </summary>
  155 + [SugarColumn(ColumnName = "F_MissingCard")]
  156 + public decimal MissingCard { get; set; }
  157 +
  158 + /// <summary>
  159 + /// 迟到扣款
  160 + /// </summary>
  161 + [SugarColumn(ColumnName = "F_LateArrival")]
  162 + public decimal LateArrival { get; set; }
  163 +
  164 + /// <summary>
  165 + /// 请假扣款
  166 + /// </summary>
  167 + [SugarColumn(ColumnName = "F_LeaveDeduction")]
  168 + public decimal LeaveDeduction { get; set; }
  169 +
  170 + /// <summary>
  171 + /// 扣社保
  172 + /// </summary>
  173 + [SugarColumn(ColumnName = "F_SocialInsuranceDeduction")]
  174 + public decimal SocialInsuranceDeduction { get; set; }
  175 +
  176 + /// <summary>
  177 + /// 扣除奖励
  178 + /// </summary>
  179 + [SugarColumn(ColumnName = "F_RewardDeduction")]
  180 + public decimal RewardDeduction { get; set; }
  181 +
  182 + /// <summary>
  183 + /// 扣住宿费
  184 + /// </summary>
  185 + [SugarColumn(ColumnName = "F_AccommodationDeduction")]
  186 + public decimal AccommodationDeduction { get; set; }
  187 +
  188 + /// <summary>
  189 + /// 扣学习期费用
  190 + /// </summary>
  191 + [SugarColumn(ColumnName = "F_StudyPeriodDeduction")]
  192 + public decimal StudyPeriodDeduction { get; set; }
  193 +
  194 + /// <summary>
  195 + /// 扣工作服费用
  196 + /// </summary>
  197 + [SugarColumn(ColumnName = "F_WorkClothesDeduction")]
  198 + public decimal WorkClothesDeduction { get; set; }
  199 +
  200 + /// <summary>
  201 + /// 扣款合计
  202 + /// </summary>
  203 + [SugarColumn(ColumnName = "F_TotalDeduction")]
  204 + public decimal TotalDeduction { get; set; }
  205 +
  206 + /// <summary>
  207 + /// 发奖金
  208 + /// </summary>
  209 + [SugarColumn(ColumnName = "F_Bonus")]
  210 + public decimal Bonus { get; set; }
  211 +
  212 + /// <summary>
  213 + /// 退手机押金
  214 + /// </summary>
  215 + [SugarColumn(ColumnName = "F_ReturnPhoneDeposit")]
  216 + public decimal ReturnPhoneDeposit { get; set; }
  217 +
  218 + /// <summary>
  219 + /// 退住宿押金
  220 + /// </summary>
  221 + [SugarColumn(ColumnName = "F_ReturnAccommodationDeposit")]
  222 + public decimal ReturnAccommodationDeposit { get; set; }
  223 +
  224 + /// <summary>
  225 + /// 实发工资
  226 + /// </summary>
  227 + [SugarColumn(ColumnName = "F_ActualSalary")]
  228 + public decimal ActualSalary { get; set; }
  229 +
  230 + /// <summary>
  231 + /// 当月是否发放
  232 + /// </summary>
  233 + [SugarColumn(ColumnName = "F_MonthlyPaymentStatus")]
  234 + public string MonthlyPaymentStatus { get; set; }
  235 +
  236 + /// <summary>
  237 + /// 支付金额
  238 + /// </summary>
  239 + [SugarColumn(ColumnName = "F_PaidAmount")]
  240 + public decimal PaidAmount { get; set; }
  241 +
  242 + /// <summary>
  243 + /// 待支付金额
  244 + /// </summary>
  245 + [SugarColumn(ColumnName = "F_PendingAmount")]
  246 + public decimal PendingAmount { get; set; }
  247 +
  248 + /// <summary>
  249 + /// 补发上月
  250 + /// </summary>
  251 + [SugarColumn(ColumnName = "F_LastMonthSupplement")]
  252 + public decimal LastMonthSupplement { get; set; }
  253 +
  254 + /// <summary>
  255 + /// 当月支付总额
  256 + /// </summary>
  257 + [SugarColumn(ColumnName = "F_MonthlyTotalPayment")]
  258 + public decimal MonthlyTotalPayment { get; set; }
  259 +
  260 + /// <summary>
  261 + /// 是否锁定(0=未锁定,1=已锁定)
  262 + /// </summary>
  263 + [SugarColumn(ColumnName = "F_IsLocked")]
  264 + public int IsLocked { get; set; }
  265 +
  266 + /// <summary>
  267 + /// 创建时间
  268 + /// </summary>
  269 + [SugarColumn(ColumnName = "F_CreateTime")]
  270 + public DateTime CreateTime { get; set; }
  271 +
  272 + /// <summary>
  273 + /// 更新时间
  274 + /// </summary>
  275 + [SugarColumn(ColumnName = "F_UpdateTime")]
  276 + public DateTime UpdateTime { get; set; }
  277 +
  278 + /// <summary>
  279 + /// 创建人
  280 + /// </summary>
  281 + [SugarColumn(ColumnName = "F_CreateUser")]
  282 + public string CreateUser { get; set; }
  283 +
  284 + /// <summary>
  285 + /// 更新人
  286 + /// </summary>
  287 + [SugarColumn(ColumnName = "F_UpdateUser")]
  288 + public string UpdateUser { get; set; }
  289 + }
  290 +}
  291 +
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_product/LqProductEntity.cs
... ... @@ -30,6 +30,12 @@ namespace NCC.Extend.Entitys.lq_product
30 30 public decimal Price { get; set; }
31 31  
32 32 /// <summary>
  33 + /// 平均单价(加权平均成本,用于出库计价)
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_AveragePrice")]
  36 + public decimal AveragePrice { get; set; }
  37 +
  38 + /// <summary>
33 39 /// 产品类别
34 40 /// </summary>
35 41 [SugarColumn(ColumnName = "F_ProductCategory")]
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_tech_general_manager_salary_statistics/LqTechGeneralManagerSalaryStatisticsEntity.cs 0 → 100644
  1 +using System;
  2 +using NCC.Common.Const;
  3 +using SqlSugar;
  4 +
  5 +namespace NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics
  6 +{
  7 + /// <summary>
  8 + /// 科技部总经理工资统计表
  9 + /// </summary>
  10 + [SugarTable("lq_tech_general_manager_salary_statistics")]
  11 + [Tenant(ClaimConst.TENANT_ID)]
  12 + public class LqTechGeneralManagerSalaryStatisticsEntity
  13 + {
  14 + /// <summary>
  15 + /// 主键ID
  16 + /// </summary>
  17 + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
  18 + public string Id { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 统计月份(YYYYMM)
  22 + /// </summary>
  23 + [SugarColumn(ColumnName = "F_StatisticsMonth", Length = 6)]
  24 + public string StatisticsMonth { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 核算岗位(科技一部/科技二部等)
  28 + /// </summary>
  29 + [SugarColumn(ColumnName = "F_Position")]
  30 + public string Position { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 员工姓名
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_EmployeeName")]
  36 + public string EmployeeName { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 员工ID
  40 + /// </summary>
  41 + [SugarColumn(ColumnName = "F_EmployeeId")]
  42 + public string EmployeeId { get; set; }
  43 +
  44 + /// <summary>
  45 + /// 员工账号
  46 + /// </summary>
  47 + [SugarColumn(ColumnName = "F_EmployeeAccount")]
  48 + public string EmployeeAccount { get; set; }
  49 +
  50 + /// <summary>
  51 + /// 是否离职(0=在职,1=离职)
  52 + /// </summary>
  53 + [SugarColumn(ColumnName = "F_IsTerminated")]
  54 + public int IsTerminated { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 管理的门店明细(JSON格式)
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "F_StoreDetail", ColumnDataType = "TEXT")]
  60 + public string StoreDetail { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 溯源金额(管理的所有门店的溯源金额总和,开单-退卡)
  64 + /// </summary>
  65 + [SugarColumn(ColumnName = "F_TraceabilityAmount")]
  66 + public decimal TraceabilityAmount { get; set; }
  67 +
  68 + /// <summary>
  69 + /// Cell金额(管理的所有门店的Cell金额总和,开单-退卡)
  70 + /// </summary>
  71 + [SugarColumn(ColumnName = "F_CellAmount")]
  72 + public decimal CellAmount { get; set; }
  73 +
  74 + /// <summary>
  75 + /// 底薪金额(固定4000元)
  76 + /// </summary>
  77 + [SugarColumn(ColumnName = "F_BaseSalary")]
  78 + public decimal BaseSalary { get; set; }
  79 +
  80 + /// <summary>
  81 + /// 溯源金额提成比例(分段计算,存储平均比例)
  82 + /// </summary>
  83 + [SugarColumn(ColumnName = "F_TraceabilityCommissionRate")]
  84 + public decimal? TraceabilityCommissionRate { get; set; }
  85 +
  86 + /// <summary>
  87 + /// 溯源金额提成金额
  88 + /// </summary>
  89 + [SugarColumn(ColumnName = "F_TraceabilityCommissionAmount")]
  90 + public decimal TraceabilityCommissionAmount { get; set; }
  91 +
  92 + /// <summary>
  93 + /// Cell金额提成比例(分段计算,存储平均比例)
  94 + /// </summary>
  95 + [SugarColumn(ColumnName = "F_CellCommissionRate")]
  96 + public decimal? CellCommissionRate { get; set; }
  97 +
  98 + /// <summary>
  99 + /// Cell金额提成金额
  100 + /// </summary>
  101 + [SugarColumn(ColumnName = "F_CellCommissionAmount")]
  102 + public decimal CellCommissionAmount { get; set; }
  103 +
  104 + /// <summary>
  105 + /// 提成合计(溯源提成+Cell提成)
  106 + /// </summary>
  107 + [SugarColumn(ColumnName = "F_TotalCommission")]
  108 + public decimal TotalCommission { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 在店天数
  112 + /// </summary>
  113 + [SugarColumn(ColumnName = "F_WorkingDays")]
  114 + public decimal WorkingDays { get; set; }
  115 +
  116 + /// <summary>
  117 + /// 请假天数
  118 + /// </summary>
  119 + [SugarColumn(ColumnName = "F_LeaveDays")]
  120 + public decimal LeaveDays { get; set; }
  121 +
  122 + /// <summary>
  123 + /// 核算应发工资(底薪 + 提成合计)
  124 + /// </summary>
  125 + [SugarColumn(ColumnName = "F_CalculatedGrossSalary")]
  126 + public decimal CalculatedGrossSalary { get; set; }
  127 +
  128 + /// <summary>
  129 + /// 最终应发工资
  130 + /// </summary>
  131 + [SugarColumn(ColumnName = "F_FinalGrossSalary")]
  132 + public decimal FinalGrossSalary { get; set; }
  133 +
  134 + /// <summary>
  135 + /// 当月培训补贴
  136 + /// </summary>
  137 + [SugarColumn(ColumnName = "F_MonthlyTrainingSubsidy")]
  138 + public decimal MonthlyTrainingSubsidy { get; set; }
  139 +
  140 + /// <summary>
  141 + /// 当月交通补贴
  142 + /// </summary>
  143 + [SugarColumn(ColumnName = "F_MonthlyTransportSubsidy")]
  144 + public decimal MonthlyTransportSubsidy { get; set; }
  145 +
  146 + /// <summary>
  147 + /// 上月培训补贴
  148 + /// </summary>
  149 + [SugarColumn(ColumnName = "F_LastMonthTrainingSubsidy")]
  150 + public decimal LastMonthTrainingSubsidy { get; set; }
  151 +
  152 + /// <summary>
  153 + /// 上月交通补贴
  154 + /// </summary>
  155 + [SugarColumn(ColumnName = "F_LastMonthTransportSubsidy")]
  156 + public decimal LastMonthTransportSubsidy { get; set; }
  157 +
  158 + /// <summary>
  159 + /// 补贴合计
  160 + /// </summary>
  161 + [SugarColumn(ColumnName = "F_TotalSubsidy")]
  162 + public decimal TotalSubsidy { get; set; }
  163 +
  164 + /// <summary>
  165 + /// 缺卡扣款
  166 + /// </summary>
  167 + [SugarColumn(ColumnName = "F_MissingCard")]
  168 + public decimal MissingCard { get; set; }
  169 +
  170 + /// <summary>
  171 + /// 迟到扣款
  172 + /// </summary>
  173 + [SugarColumn(ColumnName = "F_LateArrival")]
  174 + public decimal LateArrival { get; set; }
  175 +
  176 + /// <summary>
  177 + /// 请假扣款
  178 + /// </summary>
  179 + [SugarColumn(ColumnName = "F_LeaveDeduction")]
  180 + public decimal LeaveDeduction { get; set; }
  181 +
  182 + /// <summary>
  183 + /// 扣社保
  184 + /// </summary>
  185 + [SugarColumn(ColumnName = "F_SocialInsuranceDeduction")]
  186 + public decimal SocialInsuranceDeduction { get; set; }
  187 +
  188 + /// <summary>
  189 + /// 扣除奖励
  190 + /// </summary>
  191 + [SugarColumn(ColumnName = "F_RewardDeduction")]
  192 + public decimal RewardDeduction { get; set; }
  193 +
  194 + /// <summary>
  195 + /// 扣住宿费
  196 + /// </summary>
  197 + [SugarColumn(ColumnName = "F_AccommodationDeduction")]
  198 + public decimal AccommodationDeduction { get; set; }
  199 +
  200 + /// <summary>
  201 + /// 扣学习期费用
  202 + /// </summary>
  203 + [SugarColumn(ColumnName = "F_StudyPeriodDeduction")]
  204 + public decimal StudyPeriodDeduction { get; set; }
  205 +
  206 + /// <summary>
  207 + /// 扣工作服费用
  208 + /// </summary>
  209 + [SugarColumn(ColumnName = "F_WorkClothesDeduction")]
  210 + public decimal WorkClothesDeduction { get; set; }
  211 +
  212 + /// <summary>
  213 + /// 扣款合计
  214 + /// </summary>
  215 + [SugarColumn(ColumnName = "F_TotalDeduction")]
  216 + public decimal TotalDeduction { get; set; }
  217 +
  218 + /// <summary>
  219 + /// 发奖金
  220 + /// </summary>
  221 + [SugarColumn(ColumnName = "F_Bonus")]
  222 + public decimal Bonus { get; set; }
  223 +
  224 + /// <summary>
  225 + /// 退手机押金
  226 + /// </summary>
  227 + [SugarColumn(ColumnName = "F_ReturnPhoneDeposit")]
  228 + public decimal ReturnPhoneDeposit { get; set; }
  229 +
  230 + /// <summary>
  231 + /// 退住宿押金
  232 + /// </summary>
  233 + [SugarColumn(ColumnName = "F_ReturnAccommodationDeposit")]
  234 + public decimal ReturnAccommodationDeposit { get; set; }
  235 +
  236 + /// <summary>
  237 + /// 实发工资
  238 + /// </summary>
  239 + [SugarColumn(ColumnName = "F_ActualSalary")]
  240 + public decimal ActualSalary { get; set; }
  241 +
  242 + /// <summary>
  243 + /// 当月是否发放
  244 + /// </summary>
  245 + [SugarColumn(ColumnName = "F_MonthlyPaymentStatus")]
  246 + public string MonthlyPaymentStatus { get; set; }
  247 +
  248 + /// <summary>
  249 + /// 支付金额
  250 + /// </summary>
  251 + [SugarColumn(ColumnName = "F_PaidAmount")]
  252 + public decimal PaidAmount { get; set; }
  253 +
  254 + /// <summary>
  255 + /// 待支付金额
  256 + /// </summary>
  257 + [SugarColumn(ColumnName = "F_PendingAmount")]
  258 + public decimal PendingAmount { get; set; }
  259 +
  260 + /// <summary>
  261 + /// 补发上月
  262 + /// </summary>
  263 + [SugarColumn(ColumnName = "F_LastMonthSupplement")]
  264 + public decimal LastMonthSupplement { get; set; }
  265 +
  266 + /// <summary>
  267 + /// 当月支付总额
  268 + /// </summary>
  269 + [SugarColumn(ColumnName = "F_MonthlyTotalPayment")]
  270 + public decimal MonthlyTotalPayment { get; set; }
  271 +
  272 + /// <summary>
  273 + /// 是否锁定(0=未锁定,1=已锁定)
  274 + /// </summary>
  275 + [SugarColumn(ColumnName = "F_IsLocked")]
  276 + public int IsLocked { get; set; }
  277 +
  278 + /// <summary>
  279 + /// 创建时间
  280 + /// </summary>
  281 + [SugarColumn(ColumnName = "F_CreateTime")]
  282 + public DateTime CreateTime { get; set; }
  283 +
  284 + /// <summary>
  285 + /// 更新时间
  286 + /// </summary>
  287 + [SugarColumn(ColumnName = "F_UpdateTime")]
  288 + public DateTime UpdateTime { get; set; }
  289 +
  290 + /// <summary>
  291 + /// 创建人
  292 + /// </summary>
  293 + [SugarColumn(ColumnName = "F_CreateUser")]
  294 + public string CreateUser { get; set; }
  295 +
  296 + /// <summary>
  297 + /// 更新人
  298 + /// </summary>
  299 + [SugarColumn(ColumnName = "F_UpdateUser")]
  300 + public string UpdateUser { get; set; }
  301 + }
  302 +}
  303 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
... ... @@ -311,54 +311,69 @@ namespace NCC.Extend
311 311 salary.PerformanceCompletionRate = 0;
312 312 }
313 313  
314   - // 2.5 计算门店总提成(先计算门店级别的提成
  314 + // 2.5 计算提成比例(固定比例,不随在店天数变化
315 315 // 判断岗位类型:店助 或 店助主任
316 316 bool isDirector = salary.Position == "店助主任";
317 317  
318   - decimal storeTotalCommission = 0;
319 318 decimal commissionRate = 0;
320 319  
321 320 // 如果门店生命线未设置(<=0),则没有提成
322 321 if (salary.StoreLifeline <= 0)
323 322 {
324 323 commissionRate = 0;
325   - storeTotalCommission = 0;
326 324 }
327 325 else
328 326 {
329   - // 根据岗位类型计算提成
  327 + // 计算业绩完成率
  328 + decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline;
  329 +
  330 + // 根据岗位类型确定提成比例
330 331 if (isDirector)
331 332 {
332   - // 店助主任:使用阶梯提成模式
  333 + // 店助主任:使用固定比例(按规则文档,业绩≥100%时使用阶梯提成,但为保持比例固定,使用平均比例)
333 334 // 业绩 < 70%:0%
334   - // 70% ≤ 业绩 < 100%:0.4%(阶梯)
335   - // 超过生命线部分:1.6%(阶梯)
336   - storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
337   - // 店助主任的提成比例用于显示,计算平均比例
338   - if (salary.StoreTotalPerformance > 0)
  335 + // 70% ≤ 业绩 < 100%:0.4%
  336 + // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例
  337 + if (performanceRatio < 0.7m)
  338 + {
  339 + commissionRate = 0;
  340 + }
  341 + else if (performanceRatio < 1.0m)
339 342 {
340   - commissionRate = storeTotalCommission / salary.StoreTotalPerformance;
  343 + commissionRate = 0.004m; // 0.4%
341 344 }
342 345 else
343 346 {
344   - commissionRate = 0;
  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 + }
345 358 }
346 359 }
347 360 else
348 361 {
349   - // 店助:使用阶梯提成模式
  362 + // 店助:使用固定比例
350 363 // 业绩 < 70%:0%
351   - // 70% ≤ 业绩 < 100%:0.4%(阶梯)
352   - // 业绩 ≥ 100%:0.6%(阶梯)
353   - storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
354   - // 店助的提成比例用于显示,计算平均比例
355   - if (salary.StoreTotalPerformance > 0)
  364 + // 70% ≤ 业绩 < 100%:0.4%
  365 + // 业绩 ≥ 100%:0.6%
  366 + if (performanceRatio < 0.7m)
356 367 {
357   - commissionRate = storeTotalCommission / salary.StoreTotalPerformance;
  368 + commissionRate = 0;
  369 + }
  370 + else if (performanceRatio < 1.0m)
  371 + {
  372 + commissionRate = 0.004m; // 0.4%
358 373 }
359 374 else
360 375 {
361   - commissionRate = 0;
  376 + commissionRate = 0.006m; // 0.6%
362 377 }
363 378 }
364 379 }
... ... @@ -408,13 +423,7 @@ namespace NCC.Extend
408 423 }
409 424 }
410 425  
411   - // 2.8 计算底薪(店助和店助主任底薪规则相同,都根据门店分类确定)
412   - salary.BaseSalary = CalculateBaseSalary(salary.StoreCategory.Value);
413   -
414   - // 2.9 固定奖励(手机管理费)
415   - salary.PhoneManagementFee = 150m;
416   -
417   - // 2.10 考勤数据
  426 + // 2.8 考勤数据
418 427 int workingDays = 0;
419 428 if (attendanceDict.ContainsKey(assistantUser.Id))
420 429 {
... ... @@ -429,13 +438,35 @@ namespace NCC.Extend
429 438 salary.LeaveDays = 0;
430 439 }
431 440  
  441 + // 2.9 计算底薪(店助和店助主任底薪规则相同,都根据门店分类确定,按在店天数比例计算)
  442 + decimal baseSalaryFull = CalculateBaseSalary(salary.StoreCategory.Value);
  443 + if (daysInMonth > 0 && workingDays > 0)
  444 + {
  445 + salary.BaseSalary = baseSalaryFull / daysInMonth * workingDays;
  446 + }
  447 + else
  448 + {
  449 + salary.BaseSalary = 0;
  450 + }
  451 +
  452 + // 2.10 计算手机管理费(按在店天数比例计算)
  453 + decimal phoneManagementFeeFull = 150m;
  454 + if (daysInMonth > 0 && workingDays > 0)
  455 + {
  456 + salary.PhoneManagementFee = phoneManagementFeeFull / daysInMonth * workingDays;
  457 + }
  458 + else
  459 + {
  460 + salary.PhoneManagementFee = 0;
  461 + }
  462 +
432 463 // 2.11 按在店天数比例计算店助的提成和奖励
433   - // 逻辑:门店总提成 / 当月天数 * 在店天数 = 店助提成
434   - // 门店总奖励 / 当月天数 * 在店天数 = 店助奖励
  464 + // 逻辑:提成金额 = 门店业绩 × 提成比例 / 当月天数 × 在店天数
  465 + // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数
435 466 if (daysInMonth > 0 && workingDays > 0)
436 467 {
437   - // 按比例计算提成
438   - salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays;
  468 + // 按比例计算提成:门店业绩 × 提成比例 / 当月天数 × 在店天数
  469 + salary.CommissionAmount = salary.StoreTotalPerformance * commissionRate / daysInMonth * workingDays;
439 470  
440 471 // 按比例计算奖励
441 472 salary.StageRewardAmount = storeTotalStageReward / daysInMonth * workingDays;
... ... @@ -615,14 +646,17 @@ namespace NCC.Extend
615 646 // 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成
616 647 // 70%以下部分:0%
617 648 // 70%-100%部分:0.4%
618   - // 超过生命线部分:1.6%
  649 + // ≤生命线部分:0.6%,>生命线部分:1%
619 650 decimal stage70 = storeLifeline * 0.7m;
620 651 decimal stage100 = storeLifeline;
621 652 decimal performance70To100 = stage100 - stage70;
622 653 decimal performanceAbove100 = storePerformance - stage100;
623 654  
  655 + // 70%-100%部分:0.4%
624 656 decimal commission70To100 = performance70To100 * 0.004m;
625   - decimal commissionAbove100 = performanceAbove100 * 0.016m;
  657 + // ≤生命线部分(0-70%):0%,70%-100%部分:0.4%,已计算
  658 + // >生命线部分:1%
  659 + decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1%
626 660  
627 661 return commission70To100 + commissionAbove100;
628 662 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
... ... @@ -866,7 +866,7 @@ namespace NCC.Extend
866 866 /// - ManagerId: 经理用户ID
867 867 /// - ManagerName: 经理姓名
868 868 /// - StoreId/StoreName: 门店信息
869   - /// - Target1/2/3: 目标业绩一/二/三阶段(来自门店总经理生命线设置表lq_md_general_manager_lifeline的F_Lifeline1/F_Lifeline2/F_Lifeline3字段,根据开始时间所在月份获取,如果未查询到则为0
  869 + /// - Target1/2/3: 目标业绩一/二/三阶段(来自门店目标表lq_md_target的F_StoreTarget字段,根据开始时间所在月份获取,如果未查询到则为0,三个阶段都使用同一个门店目标值
870 870 /// - CompletedPerformance: 完成业绩(指定时间范围内的开单业绩总和)
871 871 /// - CompletionRate1/2/3: 完成率各阶段(百分比)
872 872 /// </remarks>
... ... @@ -889,40 +889,42 @@ namespace NCC.Extend
889 889 var managerFilter = "";
890 890 if (!string.IsNullOrWhiteSpace(input.ManagerId))
891 891 {
892   - managerFilter = $"AND target.F_GeneralManagerId = '{input.ManagerId}'";
  892 + managerFilter = $"AND gm.F_GeneralManagerId = '{input.ManagerId}'";
893 893 }
894 894  
895 895 // 构建经理类型过滤条件
896 896 if (input.ManagerType.HasValue)
897 897 {
898   - managerFilter += $" AND target.F_ManagerType = {input.ManagerType.Value}";
  898 + managerFilter += $" AND gm.F_ManagerType = {input.ManagerType.Value}";
899 899 }
900 900  
901 901 // SQL查询:获取经理在各门店的目标业绩和完成业绩
  902 + // 目标业绩从门店目标表(lq_md_target)的F_StoreTarget字段获取
902 903 var sql = $@"
903 904 SELECT
904   - target.F_GeneralManagerId as ManagerId,
  905 + gm.F_GeneralManagerId as ManagerId,
905 906 u.F_RealName as ManagerName,
906   - target.F_StoreId as StoreId,
  907 + gm.F_StoreId as StoreId,
907 908 store.dm as StoreName,
908   - target.F_Lifeline1 as Target1,
909   - target.F_Lifeline2 as Target2,
910   - target.F_Lifeline3 as Target3,
  909 + COALESCE(md_target.F_StoreTarget, 0) as Target1,
  910 + COALESCE(md_target.F_StoreTarget, 0) as Target2,
  911 + COALESCE(md_target.F_StoreTarget, 0) as Target3,
911 912 -- 完成业绩
912 913 COALESCE((
913 914 SELECT SUM(billing.sfyj)
914 915 FROM lq_kd_kdjlb billing
915   - WHERE billing.djmd = target.F_StoreId
  916 + WHERE billing.djmd = gm.F_StoreId
916 917 AND billing.F_IsEffective = 1
917   - AND DATE(billing.kdrq) >= '{startDate:yyyy-MM-dd}'
918   - AND DATE(billing.kdrq) <= '{endDate:yyyy-MM-dd}'
  918 + AND DATE(billing.kdrq) >= '{startDate.ToString("yyyy-MM-dd")}'
  919 + AND DATE(billing.kdrq) <= '{endDate.ToString("yyyy-MM-dd")}'
919 920 ), 0) as CompletedPerformance
920   - FROM lq_md_general_manager_lifeline target
921   - INNER JOIN BASE_USER u ON target.F_GeneralManagerId = u.F_Id
922   - INNER JOIN lq_mdxx store ON target.F_StoreId = store.F_Id
923   - WHERE target.F_Month = '{month}'
  921 + FROM lq_md_general_manager_lifeline gm
  922 + INNER JOIN BASE_USER u ON gm.F_GeneralManagerId = u.F_Id
  923 + INNER JOIN lq_mdxx store ON gm.F_StoreId = store.F_Id
  924 + LEFT JOIN lq_md_target md_target ON gm.F_StoreId = md_target.F_StoreId AND md_target.F_Month = '{month}'
  925 + WHERE gm.F_Month = '{month}'
924 926 {managerFilter}
925   - ORDER BY target.F_GeneralManagerId, store.dm";
  927 + ORDER BY gm.F_GeneralManagerId, store.dm";
926 928  
927 929 var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
928 930  
... ... @@ -981,7 +983,7 @@ namespace NCC.Extend
981 983 /// 返回说明:
982 984 /// - ManagerId: 经理用户ID
983 985 /// - ManagerName: 经理姓名
984   - /// - TotalTarget1/2/3: 总目标业绩一/二/三阶段(来自门店总经理生命线设置表lq_md_general_manager_lifeline的F_Lifeline1/F_Lifeline2/F_Lifeline3字段,根据开始时间所在月份获取并汇总
  986 + /// - TotalTarget1/2/3: 总目标业绩一/二/三阶段(来自门店目标表lq_md_target的F_StoreTarget字段,根据开始时间所在月份获取并汇总所有管理门店的门店目标,三个阶段都使用同一个汇总值
985 987 /// - TotalCompletedPerformance: 总完成业绩(指定时间范围内的开单业绩总和)
986 988 /// - TotalCompletionRate1/2/3: 总完成率各阶段
987 989 /// - StoreCount: 管理门店数量(根据门店总经理生命线设置表中归属该经理的门店数统计)
... ... @@ -1005,39 +1007,41 @@ namespace NCC.Extend
1005 1007 var managerFilter = "";
1006 1008 if (!string.IsNullOrWhiteSpace(input.ManagerId))
1007 1009 {
1008   - managerFilter = $"AND target.F_GeneralManagerId = '{input.ManagerId}'";
  1010 + managerFilter = $"AND gm.F_GeneralManagerId = '{input.ManagerId}'";
1009 1011 }
1010 1012  
1011 1013 // 构建经理类型过滤条件
1012 1014 if (input.ManagerType.HasValue)
1013 1015 {
1014   - managerFilter += $" AND target.F_ManagerType = {input.ManagerType.Value}";
  1016 + managerFilter += $" AND gm.F_ManagerType = {input.ManagerType.Value}";
1015 1017 }
1016 1018  
1017 1019 // SQL查询:获取经理汇总业绩(基于lq_md_general_manager_lifeline表中的经理和门店关系)
  1020 + // 目标业绩从门店目标表(lq_md_target)的F_StoreTarget字段获取并汇总
1018 1021 var sql = $@"
1019 1022 SELECT
1020   - target.F_GeneralManagerId as ManagerId,
  1023 + gm.F_GeneralManagerId as ManagerId,
1021 1024 u.F_RealName as ManagerName,
1022   - SUM(target.F_Lifeline1) as TotalTarget1,
1023   - COALESCE(SUM(target.F_Lifeline2), 0) as TotalTarget2,
1024   - COALESCE(SUM(target.F_Lifeline3), 0) as TotalTarget3,
1025   - COUNT(DISTINCT target.F_StoreId) as StoreCount,
  1025 + COALESCE(SUM(md_target.F_StoreTarget), 0) as TotalTarget1,
  1026 + COALESCE(SUM(md_target.F_StoreTarget), 0) as TotalTarget2,
  1027 + COALESCE(SUM(md_target.F_StoreTarget), 0) as TotalTarget3,
  1028 + COUNT(DISTINCT gm.F_StoreId) as StoreCount,
1026 1029 -- 总完成业绩(基于lq_md_general_manager_lifeline表中的门店关系计算)
1027 1030 SUM(COALESCE((
1028 1031 SELECT SUM(billing.sfyj)
1029 1032 FROM lq_kd_kdjlb billing
1030   - WHERE billing.djmd = target.F_StoreId
  1033 + WHERE billing.djmd = gm.F_StoreId
1031 1034 AND billing.F_IsEffective = 1
1032   - AND DATE(billing.kdrq) >= '{startDate:yyyy-MM-dd}'
1033   - AND DATE(billing.kdrq) <= '{endDate:yyyy-MM-dd}'
  1035 + AND DATE(billing.kdrq) >= '{startDate.ToString("yyyy-MM-dd")}'
  1036 + AND DATE(billing.kdrq) <= '{endDate.ToString("yyyy-MM-dd")}'
1034 1037 ), 0)) as TotalCompletedPerformance
1035   - FROM lq_md_general_manager_lifeline target
1036   - INNER JOIN BASE_USER u ON target.F_GeneralManagerId = u.F_Id
1037   - INNER JOIN lq_mdxx store ON target.F_StoreId = store.F_Id
1038   - WHERE target.F_Month = '{month}'
  1038 + FROM lq_md_general_manager_lifeline gm
  1039 + INNER JOIN BASE_USER u ON gm.F_GeneralManagerId = u.F_Id
  1040 + INNER JOIN lq_mdxx store ON gm.F_StoreId = store.F_Id
  1041 + LEFT JOIN lq_md_target md_target ON gm.F_StoreId = md_target.F_StoreId AND md_target.F_Month = '{month}'
  1042 + WHERE gm.F_Month = '{month}'
1039 1043 {managerFilter}
1040   - GROUP BY target.F_GeneralManagerId, u.F_RealName
  1044 + GROUP BY gm.F_GeneralManagerId, u.F_RealName
1041 1045 ORDER BY TotalCompletedPerformance DESC";
1042 1046  
1043 1047 var result = await _db.Ado.SqlQueryAsync<dynamic>(sql);
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
... ... @@ -5,13 +5,17 @@ using NCC.Common.Helper;
5 5 using NCC.Dependency;
6 6 using NCC.DynamicApiController;
7 7 using NCC.Extend.Entitys.Dto.LqDirectorSalary;
  8 +using NCC.Extend.Entitys.Enum;
8 9 using NCC.Extend.Entitys.lq_attendance_summary;
  10 +using NCC.Extend.Entitys.lq_cooperation_cost;
9 11 using NCC.Extend.Entitys.lq_director_salary_statistics;
10 12 using NCC.Extend.Entitys.lq_hytk_hytk;
11 13 using NCC.Extend.Entitys.lq_kd_kdjlb;
  14 +using NCC.Extend.Entitys.lq_laundry_flow;
12 15 using NCC.Extend.Entitys.lq_md_target;
13 16 using NCC.Extend.Entitys.lq_md_xdbhsj;
14 17 using NCC.Extend.Entitys.lq_mdxx;
  18 +using NCC.Extend.Entitys.lq_store_expense;
15 19 using NCC.Extend.Entitys.lq_xh_hyhk;
16 20 using NCC.Extend.Entitys.lq_xh_jksyj;
17 21 using NCC.System.Entitys.Permission;
... ... @@ -140,6 +144,7 @@ namespace NCC.Extend
140 144 var startDate = new DateTime(year, month, 1);
141 145 var endDate = startDate.AddMonths(1).AddDays(-1);
142 146 var monthStr = $"{year}{month:D2}";
  147 + var daysInMonth = DateTime.DaysInMonth(year, month); // 当月天数
143 148  
144 149 // 1. 获取基础数据
145 150  
... ... @@ -244,6 +249,68 @@ namespace NCC.Extend
244 249 .ToListAsync();
245 250 var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
246 251  
  252 + // 1.9 产品物料统计(仓库领用金额,注意11月特殊规则)
  253 + var queryMonth = monthStr;
  254 + if (month == 11)
  255 + {
  256 + // 11月工资算10月数据
  257 + queryMonth = $"{year}10";
  258 + }
  259 + var productMaterialSql = $@"
  260 + SELECT
  261 + F_StoreId as StoreId,
  262 + COALESCE(SUM(F_TotalAmount), 0) as MaterialAmount
  263 + FROM lq_inventory_usage
  264 + WHERE F_IsEffective = 1
  265 + AND DATE_FORMAT(F_UsageTime, '%Y%m') = @queryMonth
  266 + GROUP BY F_StoreId";
  267 +
  268 + var productMaterialData = await _db.Ado.SqlQueryAsync<dynamic>(productMaterialSql, new { queryMonth });
  269 + var productMaterialDict = productMaterialData
  270 + .Where(x => x.StoreId != null)
  271 + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0));
  272 +
  273 + // 1.10 合作项目成本统计
  274 + var cooperationCostList = await _db.Queryable<LqCooperationCostEntity>()
  275 + .Where(x => x.Year == year && x.Month == monthStr && x.IsEffective == StatusEnum.有效.GetHashCode())
  276 + .Select(x => new { x.StoreId, x.TotalAmount })
  277 + .ToListAsync();
  278 + var cooperationCostDict = cooperationCostList
  279 + .Where(x => !string.IsNullOrEmpty(x.StoreId))
  280 + .GroupBy(x => x.StoreId)
  281 + .ToDictionary(g => g.Key, g => g.Sum(x => x.TotalAmount));
  282 +
  283 + // 1.11 店内支出统计
  284 + var storeExpenseSql = $@"
  285 + SELECT
  286 + F_StoreId as StoreId,
  287 + COALESCE(SUM(F_Amount), 0) as ExpenseAmount
  288 + FROM lq_store_expense
  289 + WHERE F_IsEffective = 1
  290 + AND DATE_FORMAT(F_ExpenseDate, '%Y%m') = @monthStr
  291 + GROUP BY F_StoreId";
  292 +
  293 + var storeExpenseData = await _db.Ado.SqlQueryAsync<dynamic>(storeExpenseSql, new { monthStr });
  294 + var storeExpenseDict = storeExpenseData
  295 + .Where(x => x.StoreId != null)
  296 + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0));
  297 +
  298 + // 1.12 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0)
  299 + var laundryCostSql = $@"
  300 + SELECT
  301 + F_StoreId as StoreId,
  302 + COALESCE(SUM(F_TotalPrice), 0) as LaundryAmount
  303 + FROM lq_laundry_flow
  304 + WHERE F_IsEffective = 1
  305 + AND F_FlowType = 0
  306 + AND DATE_FORMAT(F_CreateTime, '%Y%m') = @monthStr
  307 + GROUP BY F_StoreId";
  308 +
  309 + var laundryCostData = await _db.Ado.SqlQueryAsync<dynamic>(laundryCostSql, new { monthStr });
  310 + var laundryCostDict = laundryCostData
  311 + .Where(x => x.StoreId != null)
  312 + .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.LaundryAmount ?? 0));
  313 +
247 314 // 2. 计算每个主任的工资
248 315 var directorSalaryList = new List<LqDirectorSalaryStatisticsEntity>();
249 316  
... ... @@ -326,31 +393,52 @@ namespace NCC.Extend
326 393 throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算主任工资(老店需要考核消耗)");
327 394 }
328 395  
329   - // 2.4 计算门店业绩
  396 + // 2.4 计算销售业绩(开单业绩-退款业绩)
330 397 decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
331 398 decimal refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0;
332 399 salary.StoreBillingPerformance = billing;
333 400 salary.StoreRefundPerformance = refund;
334   - salary.StoreTotalPerformance = billing - refund;
  401 + salary.SalesPerformance = billing - refund;
  402 +
  403 + // 2.5 统计各项成本
  404 + // 产品物料(注意11月特殊规则已在查询时处理)
  405 + salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
  406 +
  407 + // 合作项目成本
  408 + salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
  409 +
  410 + // 店内支出
  411 + salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
  412 +
  413 + // 洗毛巾费用
  414 + salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
  415 +
  416 + // 2.6 计算毛利
  417 + // 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
  418 + salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
  419 +
  420 + // 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算)
  421 + salary.StoreTotalPerformance = salary.GrossProfit;
335 422  
336   - // 计算业绩完成率
  423 + // 2.8 计算业绩完成率(基于毛利与生命线比较)
337 424 if (salary.StoreLifeline > 0)
338 425 {
339   - salary.PerformanceCompletionRate = salary.StoreTotalPerformance / salary.StoreLifeline;
  426 + salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline;
340 427 }
341 428 else
342 429 {
343 430 salary.PerformanceCompletionRate = 0;
344 431 }
345 432  
346   - // 2.5 统计门店消耗金额
  433 + // 2.9 统计门店消耗金额
347 434 salary.StoreConsume = storeConsumeDict.ContainsKey(storeId) ? storeConsumeDict[storeId] : 0;
348 435  
349   - // 2.6 统计进店消耗人数
  436 + // 2.10 统计进店消耗人数
350 437 salary.HeadCount = headcountDict.ContainsKey(storeId) ? headcountDict[storeId] : 0;
351 438  
352   - // 2.7 计算考核指标(业绩、人头、消耗是否达标)
353   - bool performanceReached = salary.StoreTotalPerformance >= salary.StoreLifeline;
  439 + // 2.11 计算考核指标(业绩、人头、消耗是否达标)
  440 + // 业绩达标判断基于毛利
  441 + bool performanceReached = salary.GrossProfit >= salary.StoreLifeline;
354 442 bool headCountReached = salary.HeadCount >= salary.TargetHeadCount;
355 443 bool consumeReached = salary.StoreConsume >= salary.TargetConsume;
356 444  
... ... @@ -368,18 +456,13 @@ namespace NCC.Extend
368 456 salary.UnreachedIndicatorCount = unreachedCount;
369 457 salary.AssessmentDeduction = unreachedCount * 500m;
370 458  
371   - // 2.8 计算底薪
372   - salary.BaseSalary = 3500m; // 固定底薪3500元
373   - salary.ActualBaseSalary = salary.BaseSalary - salary.AssessmentDeduction; // 实际底薪 = 底薪 - 考核扣款
374   -
375   - // 2.9 计算阶梯提成
376   - CalculateCommission(salary, isNewStore);
377   -
378   - // 2.10 考勤数据
  459 + // 2.12 考勤数据
  460 + int workingDays = 0;
379 461 if (attendanceDict.ContainsKey(directorUser.Id))
380 462 {
381 463 var attendance = attendanceDict[directorUser.Id];
382   - salary.WorkingDays = (int)attendance.WorkDays;
  464 + workingDays = (int)attendance.WorkDays;
  465 + salary.WorkingDays = workingDays;
383 466 salary.LeaveDays = (int)attendance.LeaveDays;
384 467 }
385 468 else
... ... @@ -388,10 +471,44 @@ namespace NCC.Extend
388 471 salary.LeaveDays = 0;
389 472 }
390 473  
391   - // 2.11 计算应发工资
  474 + // 2.13 计算底薪(按在店天数比例计算)
  475 + decimal baseSalaryFull = 3500m; // 固定底薪3500元
  476 + decimal actualBaseSalaryFull = baseSalaryFull - salary.AssessmentDeduction; // 实际底薪 = 底薪 - 考核扣款
  477 +
  478 + if (daysInMonth > 0 && workingDays > 0)
  479 + {
  480 + salary.BaseSalary = baseSalaryFull / daysInMonth * workingDays;
  481 + salary.ActualBaseSalary = actualBaseSalaryFull / daysInMonth * workingDays;
  482 + }
  483 + else
  484 + {
  485 + salary.BaseSalary = 0;
  486 + salary.ActualBaseSalary = 0;
  487 + }
  488 +
  489 + // 2.14 计算阶梯提成(先计算门店总提成,基于毛利)
  490 + CalculateCommission(salary, isNewStore);
  491 +
  492 + // 2.15 按在店天数比例计算提成金额
  493 + if (daysInMonth > 0 && workingDays > 0)
  494 + {
  495 + // 提成金额按在店天数比例计算
  496 + salary.CommissionAmountBelowLifeline = salary.CommissionAmountBelowLifeline / daysInMonth * workingDays;
  497 + salary.CommissionAmountAboveLifeline = salary.CommissionAmountAboveLifeline / daysInMonth * workingDays;
  498 + salary.TotalCommissionAmount = salary.CommissionAmountBelowLifeline + salary.CommissionAmountAboveLifeline;
  499 + }
  500 + else
  501 + {
  502 + // 如果当月天数为0或在店天数为0,则提成为0
  503 + salary.CommissionAmountBelowLifeline = 0;
  504 + salary.CommissionAmountAboveLifeline = 0;
  505 + salary.TotalCommissionAmount = 0;
  506 + }
  507 +
  508 + // 2.16 计算应发工资
392 509 salary.GrossSalary = salary.ActualBaseSalary + salary.TotalCommissionAmount;
393 510  
394   - // 2.12 初始化扣款、补贴、奖金字段(默认值为0)
  511 + // 2.17 初始化扣款、补贴、奖金字段(默认值为0)
395 512 salary.MissingCard = 0;
396 513 salary.LateArrival = 0;
397 514 salary.LeaveDeduction = 0;
... ... @@ -412,10 +529,10 @@ namespace NCC.Extend
412 529 salary.ReturnPhoneDeposit = 0;
413 530 salary.ReturnAccommodationDeposit = 0;
414 531  
415   - // 2.13 计算实发工资
  532 + // 2.18 计算实发工资
416 533 salary.ActualSalary = salary.GrossSalary - salary.TotalDeduction + salary.TotalSubsidy + salary.Bonus;
417 534  
418   - // 2.14 初始化支付相关字段
  535 + // 2.19 初始化支付相关字段
419 536 salary.PaidAmount = 0;
420 537 salary.PendingAmount = salary.ActualSalary;
421 538 salary.LastMonthSupplement = 0;
... ... @@ -437,7 +554,7 @@ namespace NCC.Extend
437 554 }
438 555  
439 556 /// <summary>
440   - /// 计算阶梯提成
  557 + /// 计算阶梯提成(基于毛利)
441 558 /// </summary>
442 559 /// <param name="salary">工资实体</param>
443 560 /// <param name="isNewStore">是否新店</param>
... ... @@ -454,7 +571,8 @@ namespace NCC.Extend
454 571 return;
455 572 }
456 573  
457   - decimal performance = salary.StoreTotalPerformance;
  574 + // 提成计算基于毛利(StoreTotalPerformance存储的是毛利)
  575 + decimal performance = salary.StoreTotalPerformance; // 这里已经是毛利了
458 576 decimal lifeline = salary.StoreLifeline;
459 577  
460 578 // 确定提成比例(根据新店/老店和门店分类)
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
... ... @@ -183,8 +183,23 @@ namespace NCC.Extend
183 183 CreateTime = DateTime.Now
184 184 };
185 185  
186   - var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
187   - if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  186 + _db.Ado.BeginTran();
  187 + try
  188 + {
  189 + var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
  190 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  191 +
  192 + // 计算并更新产品的平均单价(加权平均成本法)
  193 + // 普通入库和采购入库都需要更新平均单价
  194 + await UpdateProductAveragePriceAsync(input.ProductId, stockInType, purchaseUnitPrice, input.Quantity);
  195 +
  196 + _db.Ado.CommitTran();
  197 + }
  198 + catch
  199 + {
  200 + _db.Ado.RollbackTran();
  201 + throw;
  202 + }
188 203 }
189 204 catch (Exception ex)
190 205 {
... ... @@ -192,6 +207,79 @@ namespace NCC.Extend
192 207 throw NCCException.Oh($"创建失败:{ex.Message}");
193 208 }
194 209 }
  210 +
  211 + /// <summary>
  212 + /// 更新产品的平均单价(加权平均成本法)
  213 + /// </summary>
  214 + /// <param name="productId">产品ID</param>
  215 + /// <param name="stockInType">入库类型(1:普通入库 2:采购入库)</param>
  216 + /// <param name="incomingUnitPrice">入库单价(采购入库时使用采购单价,普通入库时使用产品价格)</param>
  217 + /// <param name="incomingQuantity">入库数量</param>
  218 + private async Task UpdateProductAveragePriceAsync(string productId, int stockInType, decimal? incomingUnitPrice, int incomingQuantity)
  219 + {
  220 + // 获取产品信息
  221 + var product = await _db.Queryable<LqProductEntity>()
  222 + .Where(x => x.Id == productId)
  223 + .FirstAsync();
  224 +
  225 + if (product == null)
  226 + {
  227 + return;
  228 + }
  229 +
  230 + // 计算当前可用库存数量(总库存数量 - 已领取数量)
  231 + // 注意:由于入库记录已经插入,需要减去本次入库的数量,得到入库前的库存数量
  232 + var totalInventoryQuantity = await _db.Queryable<LqInventoryEntity>()
  233 + .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
  234 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  235 +
  236 + var totalUsageQuantity = await _db.Queryable<LqInventoryUsageEntity>()
  237 + .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
  238 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  239 +
  240 + // 计算入库前的可用库存数量(需要减去本次入库的数量)
  241 + var currentAvailableQuantity = (totalInventoryQuantity - incomingQuantity) - totalUsageQuantity;
  242 +
  243 + // 确定入库单价
  244 + decimal incomingPrice = 0;
  245 + if (stockInType == 2 && incomingUnitPrice.HasValue && incomingUnitPrice.Value > 0)
  246 + {
  247 + // 采购入库:使用采购单价
  248 + incomingPrice = incomingUnitPrice.Value;
  249 + }
  250 + else
  251 + {
  252 + // 普通入库:使用产品价格
  253 + incomingPrice = product.Price;
  254 + }
  255 +
  256 + // 计算新的平均单价
  257 + decimal newAveragePrice = 0;
  258 + if (currentAvailableQuantity <= 0)
  259 + {
  260 + // 如果当前没有可用库存,新平均单价就是入库单价
  261 + newAveragePrice = incomingPrice;
  262 + }
  263 + else
  264 + {
  265 + // 加权平均成本法计算
  266 + // 当前库存总金额 = 当前平均单价 × 当前可用库存数量
  267 + var currentTotalAmount = product.AveragePrice > 0 ? product.AveragePrice * currentAvailableQuantity : product.Price * currentAvailableQuantity;
  268 +
  269 + // 入库金额 = 入库单价 × 入库数量
  270 + var incomingAmount = incomingPrice * incomingQuantity;
  271 +
  272 + // 新平均单价 = (当前库存总金额 + 入库金额) / (当前可用库存数量 + 入库数量)
  273 + newAveragePrice = (currentTotalAmount + incomingAmount) / (currentAvailableQuantity + incomingQuantity);
  274 + }
  275 +
  276 + // 更新产品的平均单价
  277 + product.AveragePrice = newAveragePrice;
  278 + product.UpdateTime = DateTime.Now;
  279 + await _db.Updateable(product)
  280 + .UpdateColumns(x => new { x.AveragePrice, x.UpdateTime })
  281 + .ExecuteCommandAsync();
  282 + }
195 283 #endregion
196 284  
197 285 #region 更新库存信息
... ... @@ -307,8 +395,24 @@ namespace NCC.Extend
307 395 existingInventory.UpdateUser = _userManager.UserId;
308 396 existingInventory.UpdateTime = DateTime.Now;
309 397  
310   - var isOk = await _db.Updateable(existingInventory).ExecuteCommandAsync();
311   - if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  398 + _db.Ado.BeginTran();
  399 + try
  400 + {
  401 + var isOk = await _db.Updateable(existingInventory).ExecuteCommandAsync();
  402 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
  403 +
  404 + // 如果数量或单价发生变化,需要重新计算平均单价
  405 + // 注意:更新库存时,如果数量或单价变化,需要重新计算整个产品的平均单价
  406 + // 因为更新操作可能改变数量或单价,所以需要基于所有有效库存重新计算
  407 + await RecalculateProductAveragePriceAsync(input.ProductId);
  408 +
  409 + _db.Ado.CommitTran();
  410 + }
  411 + catch
  412 + {
  413 + _db.Ado.RollbackTran();
  414 + throw;
  415 + }
312 416 }
313 417 catch (Exception ex)
314 418 {
... ... @@ -316,6 +420,90 @@ namespace NCC.Extend
316 420 throw NCCException.Oh($"更新失败:{ex.Message}");
317 421 }
318 422 }
  423 +
  424 + /// <summary>
  425 + /// 重新计算产品的平均单价(基于所有有效库存)
  426 + /// </summary>
  427 + /// <param name="productId">产品ID</param>
  428 + private async Task RecalculateProductAveragePriceAsync(string productId)
  429 + {
  430 + // 获取产品信息
  431 + var product = await _db.Queryable<LqProductEntity>()
  432 + .Where(x => x.Id == productId)
  433 + .FirstAsync();
  434 +
  435 + if (product == null)
  436 + {
  437 + return;
  438 + }
  439 +
  440 + // 计算当前可用库存数量(总库存数量 - 已领取数量)
  441 + var totalInventoryQuantity = await _db.Queryable<LqInventoryEntity>()
  442 + .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
  443 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  444 +
  445 + var totalUsageQuantity = await _db.Queryable<LqInventoryUsageEntity>()
  446 + .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
  447 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  448 +
  449 + var currentAvailableQuantity = totalInventoryQuantity - totalUsageQuantity;
  450 +
  451 + if (currentAvailableQuantity <= 0)
  452 + {
  453 + // 如果没有可用库存,平均单价保持为产品价格
  454 + product.AveragePrice = product.Price;
  455 + product.UpdateTime = DateTime.Now;
  456 + await _db.Updateable(product)
  457 + .UpdateColumns(x => new { x.AveragePrice, x.UpdateTime })
  458 + .ExecuteCommandAsync();
  459 + return;
  460 + }
  461 +
  462 + // 获取所有有效库存记录
  463 + var inventoryList = await _db.Queryable<LqInventoryEntity>()
  464 + .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Quantity > 0)
  465 + .Select(x => new { x.Quantity, x.FinalAmount, x.PurchaseUnitPrice })
  466 + .ToListAsync();
  467 +
  468 + decimal totalAmount = 0;
  469 + int totalQuantity = 0;
  470 +
  471 + foreach (var inventory in inventoryList)
  472 + {
  473 + decimal unitPrice = 0;
  474 +
  475 + // 优先使用 F_FinalAmount(产品最终金额)计算单价
  476 + if (inventory.FinalAmount.HasValue && inventory.FinalAmount.Value > 0)
  477 + {
  478 + unitPrice = inventory.FinalAmount.Value / inventory.Quantity;
  479 + }
  480 + // 其次使用 F_PurchaseUnitPrice(采购单价)
  481 + else if (inventory.PurchaseUnitPrice.HasValue && inventory.PurchaseUnitPrice.Value > 0)
  482 + {
  483 + unitPrice = inventory.PurchaseUnitPrice.Value;
  484 + }
  485 + // 如果都没有,使用产品价格
  486 + else
  487 + {
  488 + unitPrice = product.Price;
  489 + }
  490 +
  491 + totalAmount += unitPrice * inventory.Quantity;
  492 + totalQuantity += inventory.Quantity;
  493 + }
  494 +
  495 + // 计算新的平均单价(基于总库存,但只考虑可用库存部分)
  496 + // 这里需要按比例计算:可用库存的平均单价 = 总库存的平均单价
  497 + // 因为已经领取的库存已经按照当时的平均单价计价了,所以这里直接计算总库存的平均单价即可
  498 + decimal newAveragePrice = totalQuantity > 0 ? totalAmount / totalQuantity : product.Price;
  499 +
  500 + // 更新产品的平均单价
  501 + product.AveragePrice = newAveragePrice;
  502 + product.UpdateTime = DateTime.Now;
  503 + await _db.Updateable(product)
  504 + .UpdateColumns(x => new { x.AveragePrice, x.UpdateTime })
  505 + .ExecuteCommandAsync();
  506 + }
319 507 #endregion
320 508  
321 509 #region 获取库存列表
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
... ... @@ -161,29 +161,11 @@ namespace NCC.Extend
161 161 throw NCCException.Oh("产品不存在");
162 162 }
163 163  
164   - // 计算该产品的总库存数量(所有有效库存的总和)
165   - var totalInventory = await _db.Queryable<LqInventoryEntity>()
166   - .Where(x => x.ProductId == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
167   - .SumAsync(x => (int?)x.Quantity) ?? 0;
168   -
169   - // 计算该产品的已使用数量
170   - var totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
171   - .Where(x => x.ProductId == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
172   - .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
173   -
174   - // 计算可用库存
175   - var availableInventory = totalInventory - totalUsage;
176   -
177   - // 检查库存数量是否足够
178   - if (availableInventory < input.UsageQuantity)
179   - {
180   - throw NCCException.Oh($"库存不足,当前可用库存:{availableInventory},需要数量:{input.UsageQuantity}");
181   - }
182   -
183 164 _db.Ado.BeginTran();
184 165  
185   - // 根据商品现存的库存计算平均价格(加权平均)
186   - var unitPrice = await CalculateAveragePriceFromInventoryAsync(input.ProductId, product.Price);
  166 + // 使用产品的平均单价(加权平均成本法)
  167 + // 如果产品平均单价为0或未设置,则使用产品价格
  168 + var unitPrice = product.AveragePrice > 0 ? product.AveragePrice : product.Price;
187 169 var totalAmount = unitPrice * input.UsageQuantity;
188 170  
189 171 // 创建使用记录
... ... @@ -281,70 +263,16 @@ namespace NCC.Extend
281 263 var batchId = string.IsNullOrWhiteSpace(input.BatchId) ? YitIdHelper.NextId().ToString() : input.BatchId;
282 264 var successIds = new List<string>();
283 265  
284   - // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚)
285   - var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList();
286   -
287   - // 获取所有需要检查的产品ID
288   - var productIds = productGroups.Select(x => x.Key).Distinct().ToList();
289   -
290   - // 批量查询所有产品的库存信息(总库存)
291   - var inventoryList = await _db.Queryable<LqInventoryEntity>().Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode()).GroupBy(x => x.ProductId).Select(x => new { ProductId = x.ProductId, TotalInventory = SqlFunc.AggregateSum(x.Quantity) }).ToListAsync();
292   - var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory));
293   -
294   - // 批量查询所有产品的已使用数量
295   - var usageList = await _db.Queryable<LqInventoryUsageEntity>().Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode()).GroupBy(x => x.ProductId).Select(x => new { ProductId = x.ProductId, TotalUsage = SqlFunc.AggregateSum(x.UsageQuantity) }).ToListAsync();
296   - var usageMap = usageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage));
297   -
298   - // 批量查询所有产品的名称和价格
299   - var productDict = await _db.Queryable<LqProductEntity>()
300   - .Where(x => productIds.Contains(x.Id))
301   - .Select(x => new { x.Id, x.ProductName, x.Price })
302   - .ToListAsync();
303   - var productNameMap = productDict.ToDictionary(x => x.Id, x => x.ProductName ?? "未知产品");
304   - var productPriceMap = productDict.ToDictionary(x => x.Id, x => x.Price);
305   -
306   - // 批量计算每个产品的平均价格(根据库存计算加权平均)
307   - var productAveragePriceMap = new Dictionary<string, decimal>();
308   - foreach (var productId in productIds)
309   - {
310   - var defaultPrice = productPriceMap.GetValueOrDefault(productId, 0);
311   - var averagePrice = await CalculateAveragePriceFromInventoryAsync(productId, defaultPrice);
312   - productAveragePriceMap[productId] = averagePrice;
313   - }
314   -
315   - // 检查每个产品的库存
316   - foreach (var productGroup in productGroups)
317   - {
318   - var productId = productGroup.Key;
319   - var totalRequired = productGroup.Sum(x => x.Item.UsageQuantity);
320   -
321   - // 从字典中获取库存信息
322   - var totalInventory = inventoryMap.GetValueOrDefault(productId, 0);
323   - var totalUsage = usageMap.GetValueOrDefault(productId, 0);
324   - var availableInventory = totalInventory - totalUsage;
325   -
326   - // 检查库存是否足够,如果不足则直接抛出异常
327   - if (availableInventory < totalRequired)
328   - {
329   - var productName = productNameMap.GetValueOrDefault(productId, "未知产品");
330   - throw NCCException.Oh($"产品【{productName}】(ID: {productId}) 库存不足,当前可用库存:{availableInventory},需要数量:{totalRequired}");
331   - }
332   - }
333   -
334 266 _db.Ado.BeginTran();
335 267  
336 268 try
337 269 {
338   - // 创建使用记录(自动计算单价和合计金额
  270 + // 创建使用记录(不计算价格,价格在确认领用时计算
339 271 var entitiesToInsert = new List<LqInventoryUsageEntity>();
340 272 for (int i = 0; i < input.UsageItems.Count; i++)
341 273 {
342 274 var item = input.UsageItems[i];
343 275  
344   - // 从平均价格字典获取单价(根据库存计算的加权平均价格)
345   - var unitPrice = productAveragePriceMap.GetValueOrDefault(item.ProductId, 0);
346   - var totalAmount = unitPrice * item.UsageQuantity;
347   -
348 276 var usageEntity = new LqInventoryUsageEntity
349 277 {
350 278 Id = YitIdHelper.NextId().ToString(),
... ... @@ -352,8 +280,8 @@ namespace NCC.Extend
352 280 StoreId = item.StoreId,
353 281 UsageTime = item.UsageTime,
354 282 UsageQuantity = item.UsageQuantity,
355   - UnitPrice = unitPrice,
356   - TotalAmount = totalAmount,
  283 + UnitPrice = 0, // 创建时不计算价格,在确认领用时计算
  284 + TotalAmount = 0, // 创建时不计算价格,在确认领用时计算
357 285 RelatedConsumeId = item.RelatedConsumeId,
358 286 UsageBatchId = batchId,
359 287 CreateUser = _userManager.UserId,
... ... @@ -375,8 +303,8 @@ namespace NCC.Extend
375 303 }
376 304 }
377 305  
378   - // 计算这一单所有商品的总价
379   - var batchTotalAmount = entitiesToInsert.Sum(x => x.TotalAmount);
  306 + // 申请总金额初始为0,在确认领用时计算
  307 + var batchTotalAmount = 0m;
380 308  
381 309 // 创建申请记录并提交审批(审批人ID为必填)
382 310 if (string.IsNullOrWhiteSpace(input.ApproverId))
... ... @@ -1247,15 +1175,115 @@ namespace NCC.Extend
1247 1175 // 获取当前用户信息
1248 1176 var userInfo = await _userManager.GetUserInfo();
1249 1177  
1250   - // 更新申请记录
1251   - application.IsReceived = 1;
1252   - application.ReceiveTime = DateTime.Now;
1253   - application.ReceiveUser = userInfo.userId;
1254   - application.UpdateUser = userInfo.userId;
1255   - application.UpdateTime = DateTime.Now;
  1178 + _db.Ado.BeginTran();
1256 1179  
1257   - var isOk = await _db.Updateable(application).ExecuteCommandAsync();
1258   - if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1003);
  1180 + try
  1181 + {
  1182 + // 1. 获取该批次的所有使用记录
  1183 + var usageRecords = await _db.Queryable<LqInventoryUsageEntity>()
  1184 + .Where(x => x.UsageBatchId == application.UsageBatchId && x.IsEffective == StatusEnum.有效.GetHashCode())
  1185 + .ToListAsync();
  1186 +
  1187 + if (!usageRecords.Any())
  1188 + {
  1189 + throw NCCException.Oh("该申请没有使用记录,无法确认领用");
  1190 + }
  1191 +
  1192 + // 2. 批量获取所有产品信息(包含平均单价和价格)
  1193 + var productIds = usageRecords.Select(x => x.ProductId).Distinct().ToList();
  1194 + var productDict = await _db.Queryable<LqProductEntity>()
  1195 + .Where(x => productIds.Contains(x.Id))
  1196 + .Select(x => new { x.Id, x.ProductName, x.Price, x.AveragePrice })
  1197 + .ToListAsync();
  1198 + var productMap = productDict.ToDictionary(x => x.Id, x => new { x.ProductName, x.Price, x.AveragePrice });
  1199 +
  1200 + // 3. 在确认领用前,验证所有产品的库存是否充足
  1201 + // 先获取所有已确认领用的批次ID(用于计算已使用数量)
  1202 + var receivedBatchIds = await _db.Queryable<LqInventoryUsageApplicationEntity>()
  1203 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()
  1204 + && x.IsReceived == 1
  1205 + && x.UsageBatchId != application.UsageBatchId) // 排除当前批次
  1206 + .Select(x => x.UsageBatchId)
  1207 + .ToListAsync();
  1208 +
  1209 + foreach (var usageRecord in usageRecords)
  1210 + {
  1211 + if (!productMap.ContainsKey(usageRecord.ProductId))
  1212 + {
  1213 + throw NCCException.Oh($"产品不存在:{usageRecord.ProductId}");
  1214 + }
  1215 +
  1216 + var productInfo = productMap[usageRecord.ProductId];
  1217 +
  1218 + // 计算该产品的总库存数量(所有有效库存的总和)
  1219 + var totalInventory = await _db.Queryable<LqInventoryEntity>()
  1220 + .Where(x => x.ProductId == usageRecord.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
  1221 + .SumAsync(x => (int?)x.Quantity) ?? 0;
  1222 +
  1223 + // 计算该产品的已使用数量(只计算已确认领用的批次)
  1224 + // 注意:只有已确认领用的批次才算真正领取,未确认领用的批次不应该计入已使用数量
  1225 + var totalUsage = 0;
  1226 + if (receivedBatchIds != null && receivedBatchIds.Any())
  1227 + {
  1228 + totalUsage = await _db.Queryable<LqInventoryUsageEntity>()
  1229 + .Where(x => x.ProductId == usageRecord.ProductId
  1230 + && x.IsEffective == StatusEnum.有效.GetHashCode()
  1231 + && receivedBatchIds.Contains(x.UsageBatchId)) // 只计算已确认领用的批次
  1232 + .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
  1233 + }
  1234 +
  1235 + // 计算可用库存
  1236 + var availableInventory = totalInventory - totalUsage;
  1237 +
  1238 + // 检查库存数量是否足够
  1239 + if (availableInventory < usageRecord.UsageQuantity)
  1240 + {
  1241 + throw NCCException.Oh($"产品 {productInfo.ProductName ?? usageRecord.ProductId} 库存不足,当前可用库存:{availableInventory},需要数量:{usageRecord.UsageQuantity}");
  1242 + }
  1243 + }
  1244 +
  1245 + // 4. 为每个使用记录计算价格(使用产品的平均单价)
  1246 + decimal totalAmount = 0;
  1247 + foreach (var usageRecord in usageRecords)
  1248 + {
  1249 + // 获取产品信息(包含平均单价)
  1250 + var productInfo = productMap[usageRecord.ProductId];
  1251 +
  1252 + // 使用产品的平均单价(加权平均成本法)
  1253 + // 如果平均单价为0或未设置,则使用产品价格
  1254 + var unitPrice = productInfo.AveragePrice > 0 ? productInfo.AveragePrice : productInfo.Price;
  1255 + var recordTotalAmount = unitPrice * usageRecord.UsageQuantity;
  1256 +
  1257 + // 更新使用记录的单价和总金额
  1258 + usageRecord.UnitPrice = unitPrice;
  1259 + usageRecord.TotalAmount = recordTotalAmount;
  1260 + usageRecord.UpdateUser = userInfo.userId;
  1261 + usageRecord.UpdateTime = DateTime.Now;
  1262 +
  1263 + totalAmount += recordTotalAmount;
  1264 + }
  1265 +
  1266 + // 5. 批量更新使用记录
  1267 + await _db.Updateable(usageRecords).ExecuteCommandAsync();
  1268 +
  1269 + // 6. 更新申请记录(包括总金额、领取信息)
  1270 + application.TotalAmount = totalAmount;
  1271 + application.IsReceived = 1;
  1272 + application.ReceiveTime = DateTime.Now;
  1273 + application.ReceiveUser = userInfo.userId;
  1274 + application.UpdateUser = userInfo.userId;
  1275 + application.UpdateTime = DateTime.Now;
  1276 +
  1277 + var isOk = await _db.Updateable(application).ExecuteCommandAsync();
  1278 + if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1003);
  1279 +
  1280 + _db.Ado.CommitTran();
  1281 + }
  1282 + catch
  1283 + {
  1284 + _db.Ado.RollbackTran();
  1285 + throw;
  1286 + }
1259 1287 }
1260 1288 catch (Exception ex)
1261 1289 {
... ... @@ -1460,59 +1488,6 @@ namespace NCC.Extend
1460 1488 .Where(x => x.UsageBatchId == application.UsageBatchId && x.IsEffective == StatusEnum.有效.GetHashCode())
1461 1489 .ToListAsync();
1462 1490  
1463   - // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚)
1464   - var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList();
1465   -
1466   - // 获取所有需要检查的产品ID
1467   - var productIds = productGroups.Select(x => x.Key).Distinct().ToList();
1468   -
1469   - // 批量查询所有产品的库存信息(总库存)
1470   - var inventoryList = await _db.Queryable<LqInventoryEntity>()
1471   - .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode())
1472   - .GroupBy(x => x.ProductId)
1473   - .Select(x => new { ProductId = x.ProductId, TotalInventory = SqlFunc.AggregateSum(x.Quantity) })
1474   - .ToListAsync();
1475   - var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory));
1476   -
1477   - // 批量查询所有产品的已使用数量(排除当前批次的使用记录,因为我们要替换它们)
1478   - var oldUsageRecordIds = oldUsageRecords.Select(x => x.Id).ToList();
1479   - var allUsageList = await _db.Queryable<LqInventoryUsageEntity>()
1480   - .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode())
1481   - .WhereIF(oldUsageRecordIds.Any(), x => !oldUsageRecordIds.Contains(x.Id)) // 排除当前批次的使用记录
1482   - .GroupBy(x => x.ProductId)
1483   - .Select(x => new { ProductId = x.ProductId, TotalUsage = SqlFunc.AggregateSum(x.UsageQuantity) })
1484   - .ToListAsync();
1485   - var usageMap = allUsageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage));
1486   -
1487   - // 批量查询所有产品的名称和价格
1488   - var productDict = await _db.Queryable<LqProductEntity>()
1489   - .Where(x => productIds.Contains(x.Id))
1490   - .Select(x => new { x.Id, x.ProductName, x.Price })
1491   - .ToListAsync();
1492   - var productInfoMap = productDict.ToDictionary(k => k.Id, v => new { v.ProductName, v.Price });
1493   -
1494   - // 验证每个产品的库存是否充足
1495   - foreach (var group in productGroups)
1496   - {
1497   - var productId = group.Key;
1498   - var totalNewQuantity = group.Sum(x => x.Item.UsageQuantity);
1499   -
1500   - if (!productInfoMap.ContainsKey(productId))
1501   - {
1502   - throw NCCException.Oh($"产品ID {productId} 不存在");
1503   - }
1504   -
1505   - var totalInventory = inventoryMap.GetValueOrDefault(productId, 0);
1506   - var totalUsage = usageMap.GetValueOrDefault(productId, 0);
1507   - var availableInventory = totalInventory - totalUsage;
1508   -
1509   - if (availableInventory < totalNewQuantity)
1510   - {
1511   - var productName = productInfoMap[productId].ProductName;
1512   - throw NCCException.Oh($"产品 {productName} 库存不足,当前可用库存:{availableInventory},需要数量:{totalNewQuantity}");
1513   - }
1514   - }
1515   -
1516 1491 _db.Ado.BeginTran();
1517 1492  
1518 1493 try
... ... @@ -1529,23 +1504,10 @@ namespace NCC.Extend
1529 1504 await _db.Updateable(oldUsageRecords).ExecuteCommandAsync();
1530 1505 }
1531 1506  
1532   - // 2. 批量计算新使用记录的平均价格(根据库存计算的加权平均价格)
1533   - var productAveragePriceMap = new Dictionary<string, decimal>();
1534   - foreach (var productId in productIds)
1535   - {
1536   - var product = productInfoMap[productId];
1537   - var averagePrice = await CalculateAveragePriceFromInventoryAsync(productId, product.Price);
1538   - productAveragePriceMap[productId] = averagePrice;
1539   - }
1540   -
1541   - // 3. 创建新的使用记录
  1507 + // 2. 创建新的使用记录(不计算价格,价格在确认领用时计算)
1542 1508 var entitiesToInsert = new List<LqInventoryUsageEntity>();
1543 1509 foreach (var item in input.UsageItems)
1544 1510 {
1545   - // 从平均价格字典获取单价(根据库存计算的加权平均价格)
1546   - var unitPrice = productAveragePriceMap.GetValueOrDefault(item.ProductId, 0);
1547   - var totalAmount = unitPrice * item.UsageQuantity;
1548   -
1549 1511 var usageEntity = new LqInventoryUsageEntity
1550 1512 {
1551 1513 Id = YitIdHelper.NextId().ToString(),
... ... @@ -1553,8 +1515,8 @@ namespace NCC.Extend
1553 1515 StoreId = item.StoreId,
1554 1516 UsageTime = item.UsageTime,
1555 1517 UsageQuantity = item.UsageQuantity,
1556   - UnitPrice = unitPrice,
1557   - TotalAmount = totalAmount,
  1518 + UnitPrice = 0, // 修改时不计算价格,在确认领用时计算
  1519 + TotalAmount = 0, // 修改时不计算价格,在确认领用时计算
1558 1520 RelatedConsumeId = item.RelatedConsumeId,
1559 1521 UsageBatchId = application.UsageBatchId, // 使用相同的批次ID
1560 1522 CreateUser = _userManager.UserId,
... ... @@ -1571,9 +1533,8 @@ namespace NCC.Extend
1571 1533 await _db.Insertable(entitiesToInsert).ExecuteCommandAsync();
1572 1534 }
1573 1535  
1574   - // 4. 重新计算申请总金额
1575   - var newTotalAmount = entitiesToInsert.Sum(x => x.TotalAmount);
1576   - application.TotalAmount = newTotalAmount;
  1536 + // 3. 申请总金额重置为0,在确认领用时计算
  1537 + application.TotalAmount = 0;
1577 1538 application.UpdateUser = _userManager.UserId;
1578 1539 application.UpdateTime = DateTime.Now;
1579 1540  
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs 0 → 100644
  1 +using Microsoft.AspNetCore.Authorization;
  2 +using Microsoft.AspNetCore.Mvc;
  3 +using NCC.Common.Filter;
  4 +using NCC.Common.Helper;
  5 +using NCC.Dependency;
  6 +using NCC.DynamicApiController;
  7 +using NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary;
  8 +using NCC.Extend.Entitys.lq_attendance_summary;
  9 +using NCC.Extend.Entitys.lq_hytk_hytk;
  10 +using NCC.Extend.Entitys.lq_kd_kdjlb;
  11 +using NCC.Extend.Entitys.lq_md_target;
  12 +using NCC.Extend.Entitys.lq_mdxx;
  13 +using NCC.Extend.Entitys.lq_major_project_director_salary_statistics;
  14 +using NCC.System.Entitys.Permission;
  15 +using SqlSugar;
  16 +using System;
  17 +using System.Collections.Generic;
  18 +using System.Linq;
  19 +using System.Threading.Tasks;
  20 +using Yitter.IdGenerator;
  21 +using Newtonsoft.Json;
  22 +
  23 +namespace NCC.Extend
  24 +{
  25 + /// <summary>
  26 + /// 大项目主管薪酬服务
  27 + /// </summary>
  28 + [ApiDescriptionSettings(Tag = "大项目主管薪酬服务", Name = "LqMajorProjectDirectorSalary", Order = 306)]
  29 + [Route("api/Extend/[controller]")]
  30 + public class LqMajorProjectDirectorSalaryService : IDynamicApiController, ITransient
  31 + {
  32 + private readonly ISqlSugarClient _db;
  33 +
  34 + /// <summary>
  35 + /// 初始化一个<see cref="LqMajorProjectDirectorSalaryService"/>类型的新实例
  36 + /// </summary>
  37 + public LqMajorProjectDirectorSalaryService(ISqlSugarClient db)
  38 + {
  39 + _db = db;
  40 + }
  41 +
  42 + /// <summary>
  43 + /// 获取大项目主管工资列表
  44 + /// </summary>
  45 + /// <param name="input">查询参数</param>
  46 + /// <returns>大项目主管工资分页列表</returns>
  47 + [HttpGet("major-project-director")]
  48 + public async Task<dynamic> GetMajorProjectDirectorSalaryList([FromQuery] MajorProjectDirectorSalaryInput input)
  49 + {
  50 + var monthStr = $"{input.Year}{input.Month:D2}";
  51 +
  52 + // 1. 检查当月是否已生成工资数据
  53 + var exists = await _db.Queryable<LqMajorProjectDirectorSalaryStatisticsEntity>()
  54 + .AnyAsync(x => x.StatisticsMonth == monthStr);
  55 +
  56 + // 2. 如果没有数据,则进行计算
  57 + if (!exists)
  58 + {
  59 + await CalculateMajorProjectDirectorSalary(input.Year, input.Month);
  60 + }
  61 +
  62 + // 3. 查询数据
  63 + var query = _db.Queryable<LqMajorProjectDirectorSalaryStatisticsEntity>()
  64 + .Where(x => x.StatisticsMonth == monthStr);
  65 +
  66 + if (!string.IsNullOrEmpty(input.Position))
  67 + {
  68 + query = query.Where(x => x.Position == input.Position);
  69 + }
  70 +
  71 + if (!string.IsNullOrEmpty(input.Keyword))
  72 + {
  73 + query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeAccount.Contains(input.Keyword));
  74 + }
  75 +
  76 + var list = await query.Select(x => new MajorProjectDirectorSalaryOutput
  77 + {
  78 + Id = x.Id,
  79 + StatisticsMonth = x.StatisticsMonth,
  80 + Position = x.Position,
  81 + EmployeeName = x.EmployeeName,
  82 + EmployeeId = x.EmployeeId,
  83 + EmployeeAccount = x.EmployeeAccount,
  84 + IsTerminated = x.IsTerminated,
  85 + StoreDetail = x.StoreDetail,
  86 + TotalPerformance = x.TotalPerformance,
  87 + BillingAmount = x.BillingAmount,
  88 + RefundAmount = x.RefundAmount,
  89 + BaseSalary = x.BaseSalary,
  90 + CommissionRate = x.CommissionRate,
  91 + CommissionAmount = x.CommissionAmount,
  92 + WorkingDays = x.WorkingDays,
  93 + LeaveDays = x.LeaveDays,
  94 + CalculatedGrossSalary = x.CalculatedGrossSalary,
  95 + FinalGrossSalary = x.FinalGrossSalary,
  96 + MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
  97 + MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
  98 + LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
  99 + LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
  100 + TotalSubsidy = x.TotalSubsidy,
  101 + MissingCard = x.MissingCard,
  102 + LateArrival = x.LateArrival,
  103 + LeaveDeduction = x.LeaveDeduction,
  104 + SocialInsuranceDeduction = x.SocialInsuranceDeduction,
  105 + RewardDeduction = x.RewardDeduction,
  106 + AccommodationDeduction = x.AccommodationDeduction,
  107 + StudyPeriodDeduction = x.StudyPeriodDeduction,
  108 + WorkClothesDeduction = x.WorkClothesDeduction,
  109 + TotalDeduction = x.TotalDeduction,
  110 + Bonus = x.Bonus,
  111 + ReturnPhoneDeposit = x.ReturnPhoneDeposit,
  112 + ReturnAccommodationDeposit = x.ReturnAccommodationDeposit,
  113 + ActualSalary = x.ActualSalary,
  114 + MonthlyPaymentStatus = x.MonthlyPaymentStatus,
  115 + PaidAmount = x.PaidAmount,
  116 + PendingAmount = x.PendingAmount,
  117 + LastMonthSupplement = x.LastMonthSupplement,
  118 + MonthlyTotalPayment = x.MonthlyTotalPayment,
  119 + IsLocked = x.IsLocked,
  120 + UpdateTime = x.UpdateTime
  121 + })
  122 + .ToPagedListAsync(input.currentPage, input.pageSize);
  123 +
  124 + return PageResult<MajorProjectDirectorSalaryOutput>.SqlSugarPageResult(list);
  125 + }
  126 +
  127 + /// <summary>
  128 + /// 计算大项目主管工资
  129 + /// </summary>
  130 + /// <param name="year">年份</param>
  131 + /// <param name="month">月份</param>
  132 + /// <returns></returns>
  133 + [HttpPost("calculate/major-project-director")]
  134 + public async Task CalculateMajorProjectDirectorSalary(int year, int month)
  135 + {
  136 + var startDate = new DateTime(year, month, 1);
  137 + var endDate = startDate.AddMonths(1).AddDays(-1);
  138 + var monthStr = $"{year}{month:D2}";
  139 +
  140 + // 1. 获取基础数据
  141 +
  142 + // 1.1 先从BASE_ORGANIZE表查找组织名称包含"大项目一部"或"大项目二部"的组织
  143 + var majorProjectOrganizeList = await _db.Queryable<OrganizeEntity>()
  144 + .Where(x => x.FullName != null && (x.FullName.Contains("大项目一部") || x.FullName.Contains("大项目二部"))
  145 + && x.DeleteMark == null && x.EnabledMark == 1)
  146 + .Select(x => new { x.Id, x.FullName })
  147 + .ToListAsync();
  148 +
  149 + if (!majorProjectOrganizeList.Any())
  150 + {
  151 + // 如果没有找到大项目部组织,直接返回
  152 + return;
  153 + }
  154 +
  155 + var majorProjectOrganizeIds = majorProjectOrganizeList.Select(x => x.Id).ToList();
  156 + var majorProjectOrganizeDict = majorProjectOrganizeList.ToDictionary(x => x.Id, x => x.FullName);
  157 +
  158 + // 1.2 从BASE_USER表查询岗位为"主管"且组织ID在大项目一部或大项目二部的员工
  159 + var majorProjectDirectorUserList = await _db.Queryable<UserEntity>()
  160 + .Where(x => x.Gw == "主管"
  161 + && majorProjectOrganizeIds.Contains(x.OrganizeId)
  162 + && x.DeleteMark == null && x.EnabledMark == 1)
  163 + .Select(x => new { x.Id, x.RealName, x.Account, x.Gw, x.OrganizeId, x.IsOnJob })
  164 + .ToListAsync();
  165 +
  166 + if (!majorProjectDirectorUserList.Any())
  167 + {
  168 + // 如果没有大项目主管员工,直接返回
  169 + return;
  170 + }
  171 +
  172 + // 1.3 从lq_md_target表获取管理的门店(按月份和大项目部组织ID)
  173 + var targetList = await _db.Queryable<LqMdTargetEntity>()
  174 + .Where(x => x.Month == monthStr
  175 + && majorProjectOrganizeIds.Contains(x.MajorProjectDepartment))
  176 + .Select(x => new { x.StoreId, x.MajorProjectDepartment })
  177 + .ToListAsync();
  178 +
  179 + // 1.4 按大项目主管ID分组,获取每个大项目主管管理的门店
  180 + var directorStoreDict = new Dictionary<string, List<string>>();
  181 + foreach (var directorUser in majorProjectDirectorUserList)
  182 + {
  183 + var directorId = directorUser.Id;
  184 + var directorOrganizeId = directorUser.OrganizeId;
  185 +
  186 + // 从lq_md_target表中查找该大项目主管管理的门店
  187 + var managedStores = targetList
  188 + .Where(x => x.MajorProjectDepartment == directorOrganizeId)
  189 + .Select(x => x.StoreId)
  190 + .Distinct()
  191 + .ToList();
  192 +
  193 + directorStoreDict[directorId] = managedStores;
  194 + }
  195 +
  196 + // 1.5 门店信息 (lq_mdxx)
  197 + var storeList = await _db.Queryable<LqMdxxEntity>().ToListAsync();
  198 + var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x);
  199 +
  200 + // 1.6 考勤数据 (lq_attendance_summary)
  201 + var attendanceList = await _db.Queryable<LqAttendanceSummaryEntity>()
  202 + .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
  203 + .ToListAsync();
  204 + var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
  205 +
  206 + // 1.7 获取所有管理的门店ID列表(用于后续查询)
  207 + var allManagedStoreIds = directorStoreDict.Values.SelectMany(x => x).Distinct().ToList();
  208 +
  209 + // 1.8 按大项目主管和门店分组统计(用于生成门店明细JSON和汇总数据)
  210 + var storeDetailDict = new Dictionary<string, Dictionary<string, StoreDetailItem>>();
  211 +
  212 + // 按门店统计开单和退卡金额(如果有管理的门店)
  213 + if (allManagedStoreIds.Any())
  214 + {
  215 + foreach (var storeId in allManagedStoreIds)
  216 + {
  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;
  230 +
  231 + // 该门店的净总业绩
  232 + var storeTotalPerformance = storeBillingAmount - storeRefundAmount;
  233 +
  234 + // 找到该门店归属的大项目主管
  235 + var storeTarget = targetList.FirstOrDefault(x => x.StoreId == storeId);
  236 + if (storeTarget != null)
  237 + {
  238 + var directorOrganizeId = storeTarget.MajorProjectDepartment;
  239 + var directorsOfStore = majorProjectDirectorUserList
  240 + .Where(x => x.OrganizeId == directorOrganizeId)
  241 + .Select(x => x.Id)
  242 + .ToList();
  243 +
  244 + foreach (var directorId in directorsOfStore)
  245 + {
  246 + if (!storeDetailDict.ContainsKey(directorId))
  247 + {
  248 + storeDetailDict[directorId] = new Dictionary<string, StoreDetailItem>();
  249 + }
  250 +
  251 + var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : "";
  252 + storeDetailDict[directorId][storeId] = new StoreDetailItem
  253 + {
  254 + StoreId = storeId,
  255 + StoreName = storeName,
  256 + BillingAmount = storeBillingAmount,
  257 + RefundAmount = storeRefundAmount,
  258 + TotalPerformance = storeTotalPerformance
  259 + };
  260 + }
  261 + }
  262 + }
  263 + }
  264 +
  265 + // 2. 按大项目主管聚合数据
  266 + var directorStats = new Dictionary<string, LqMajorProjectDirectorSalaryStatisticsEntity>();
  267 +
  268 + foreach (var directorUser in majorProjectDirectorUserList)
  269 + {
  270 + var directorId = directorUser.Id;
  271 +
  272 + // 获取该大项目主管管理的门店列表
  273 + var managedStores = directorStoreDict.ContainsKey(directorId) ? directorStoreDict[directorId] : new List<string>();
  274 +
  275 + // 2.1 创建工资统计对象
  276 + // 岗位使用组织名称(大项目一部/大项目二部)
  277 + var position = majorProjectOrganizeDict.ContainsKey(directorUser.OrganizeId)
  278 + ? majorProjectOrganizeDict[directorUser.OrganizeId]
  279 + : "";
  280 +
  281 + var salary = new LqMajorProjectDirectorSalaryStatisticsEntity
  282 + {
  283 + Id = YitIdHelper.NextId().ToString(),
  284 + StatisticsMonth = monthStr,
  285 + EmployeeId = directorId,
  286 + Position = position,
  287 + EmployeeName = directorUser.RealName ?? "",
  288 + EmployeeAccount = directorUser.Account ?? "",
  289 + IsTerminated = directorUser.IsOnJob == 0 ? 1 : 0,
  290 + CreateTime = DateTime.Now,
  291 + UpdateTime = DateTime.Now,
  292 + IsLocked = 0
  293 + };
  294 +
  295 + // 2.2 考勤数据
  296 + var attendance = attendanceDict.ContainsKey(directorId) ? attendanceDict[directorId] : null;
  297 + salary.WorkingDays = attendance?.WorkDays ?? 0;
  298 + salary.LeaveDays = attendance?.LeaveDays ?? 0;
  299 +
  300 + // 2.3 计算底薪(固定3500元)
  301 + salary.BaseSalary = 3500m;
  302 +
  303 + // 2.4 统计该大项目主管管理的所有门店的总业绩(开单-退卡)总和
  304 + decimal totalBillingAmount = 0m;
  305 + decimal totalRefundAmount = 0m;
  306 + var storeDetails = new List<StoreDetailItem>();
  307 +
  308 + if (managedStores.Any() && storeDetailDict.ContainsKey(directorId))
  309 + {
  310 + foreach (var storeId in managedStores)
  311 + {
  312 + if (storeDetailDict[directorId].ContainsKey(storeId))
  313 + {
  314 + var storeDetail = storeDetailDict[directorId][storeId];
  315 + totalBillingAmount += storeDetail.BillingAmount;
  316 + totalRefundAmount += storeDetail.RefundAmount;
  317 + storeDetails.Add(storeDetail);
  318 + }
  319 + }
  320 + }
  321 +
  322 + salary.BillingAmount = totalBillingAmount;
  323 + salary.RefundAmount = totalRefundAmount;
  324 + salary.TotalPerformance = totalBillingAmount - totalRefundAmount;
  325 +
  326 + // 2.5 保存门店明细(JSON格式)
  327 + salary.StoreDetail = JsonConvert.SerializeObject(storeDetails);
  328 +
  329 + // 2.6 计算提成(分段方式)
  330 + var commission = CalculateCommission(salary.TotalPerformance);
  331 + salary.CommissionAmount = commission.Amount;
  332 + salary.CommissionRate = commission.Rate;
  333 +
  334 + // 2.7 计算应发工资
  335 + salary.CalculatedGrossSalary = salary.BaseSalary + salary.CommissionAmount;
  336 + salary.FinalGrossSalary = salary.CalculatedGrossSalary;
  337 +
  338 + // 2.8 初始化其他字段
  339 + salary.MonthlyPaymentStatus = "未发放";
  340 + salary.ActualSalary = salary.FinalGrossSalary; // 默认实发工资等于应发工资
  341 +
  342 + directorStats[directorId] = salary;
  343 + }
  344 +
  345 + // 3. 保存数据
  346 + if (directorStats.Any())
  347 + {
  348 + // 先删除当月旧数据 (防止重复)
  349 + await _db.Deleteable<LqMajorProjectDirectorSalaryStatisticsEntity>()
  350 + .Where(x => x.StatisticsMonth == monthStr)
  351 + .ExecuteCommandAsync();
  352 +
  353 + await _db.Insertable(directorStats.Values.ToList()).ExecuteCommandAsync();
  354 + }
  355 + }
  356 +
  357 + /// <summary>
  358 + /// 计算提成(分段方式)
  359 + /// </summary>
  360 + /// <param name="totalPerformance">总业绩</param>
  361 + /// <returns>提成金额和比例</returns>
  362 + private (decimal Amount, decimal? Rate) CalculateCommission(decimal totalPerformance)
  363 + {
  364 + if (totalPerformance <= 0)
  365 + {
  366 + return (0m, null);
  367 + }
  368 +
  369 + decimal commissionAmount = 0m;
  370 + decimal? rate = null;
  371 +
  372 + if (totalPerformance <= 500000m)
  373 + {
  374 + // ≤ 50万:无提成
  375 + commissionAmount = 0m;
  376 + rate = null;
  377 + }
  378 + else if (totalPerformance <= 700000m)
  379 + {
  380 + // 50万 < 总业绩 ≤ 70万:1%提成
  381 + commissionAmount = totalPerformance * 0.01m;
  382 + rate = 1.00m;
  383 + }
  384 + else
  385 + {
  386 + // > 70万:1.5%提成
  387 + commissionAmount = totalPerformance * 0.015m;
  388 + rate = 1.50m;
  389 + }
  390 +
  391 + return (commissionAmount, rate);
  392 + }
  393 +
  394 + /// <summary>
  395 + /// 门店明细项(用于JSON序列化)
  396 + /// </summary>
  397 + private class StoreDetailItem
  398 + {
  399 + public string StoreId { get; set; }
  400 + public string StoreName { get; set; }
  401 + public decimal BillingAmount { get; set; }
  402 + public decimal RefundAmount { get; set; }
  403 + public decimal TotalPerformance { get; set; }
  404 + }
  405 + }
  406 +}
  407 +
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs
... ... @@ -226,6 +226,7 @@ namespace NCC.Extend
226 226 id = x.Id,
227 227 productName = x.ProductName,
228 228 price = x.Price,
  229 + averagePrice = x.AveragePrice,
229 230 productCategory = x.ProductCategory,
230 231 departmentId = x.DepartmentId,
231 232 departmentName = SqlFunc.Subqueryable<OrganizeEntity>().Where(y => y.Id == x.DepartmentId).Select(y => y.FullName),
... ... @@ -346,6 +347,7 @@ namespace NCC.Extend
346 347 id = product.Id,
347 348 productName = product.ProductName,
348 349 price = product.Price,
  350 + averagePrice = product.AveragePrice,
349 351 productCategory = product.ProductCategory,
350 352 departmentId = product.DepartmentId,
351 353 departmentName = "",
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
1   -using NCC.Common.Core.Manager;
  1 +using NCC.Common.Core.Manager;
2 2 using NCC.Common.Enum;
3 3 using NCC.Common.Extension;
4 4 using NCC.Common.Filter;
... ... @@ -906,6 +906,7 @@ namespace NCC.Extend.LqReimbursementApplication
906 906 }
907 907  
908 908 // 更新现有记录(包括空字符串、"待审批"、"退回"的情况)
  909 + // 注意:只更新审批结果和意见,不更新审批人信息(ApproverId和ApproverName在创建时已确定)
909 910 existingRecord.ApprovalResult = result;
910 911 existingRecord.ApprovalOpinion = opinion;
911 912 existingRecord.ApprovalTime = DateTime.Now;
... ... @@ -1050,6 +1051,8 @@ namespace NCC.Extend.LqReimbursementApplication
1050 1051 .Select(x => x.UserId)
1051 1052 .ToListAsync();
1052 1053  
  1054 + // 查询所有"通过"的审批记录,包括当前用户刚审批的记录
  1055 + // 注意:由于在事务中,需要确保查询包含当前刚更新的记录
1053 1056 var approvedUsers = await _db.Queryable<LqReimbursementApprovalRecordEntity>()
1054 1057 .Where(x => x.ApplicationId == id
1055 1058 && x.NodeOrder == entity.CurrentNodeOrder
... ... @@ -1058,7 +1061,13 @@ namespace NCC.Extend.LqReimbursementApplication
1058 1061 .Distinct()
1059 1062 .ToListAsync();
1060 1063  
1061   - if (approvers.Count == approvedUsers.Count)
  1064 + // 如果当前用户刚审批通过,但查询结果中还没有包含,手动添加
  1065 + if (result == "通过" && !approvedUsers.Contains(userInfo.userId))
  1066 + {
  1067 + approvedUsers.Add(userInfo.userId);
  1068 + }
  1069 +
  1070 + if (approvers.Count == approvedUsers.Count && approvers.Count > 0)
1062 1071 {
1063 1072 // 所有人都已通过
1064 1073 shouldMoveToNext = true;
... ... @@ -1506,8 +1515,8 @@ namespace NCC.Extend.LqReimbursementApplication
1506 1515 // 查询本月已审核通过的报销申请
1507 1516 var applications = await _db.Queryable<LqReimbursementApplicationEntity>()
1508 1517 .Where(x => (x.ApprovalStatus ?? x.ApproveStatus) == "已通过")
1509   - .Where(x => x.ApplicationTime.HasValue &&
1510   - x.ApplicationTime.Value.Year == queryYear &&
  1518 + .Where(x => x.ApplicationTime.HasValue &&
  1519 + x.ApplicationTime.Value.Year == queryYear &&
1511 1520 x.ApplicationTime.Value.Month == int.Parse(queryMonth))
1512 1521 .ToListAsync();
1513 1522  
... ... @@ -1543,7 +1552,7 @@ namespace NCC.Extend.LqReimbursementApplication
1543 1552 foreach (var app in applications)
1544 1553 {
1545 1554 var appPurchaseRecords = purchaseRecords.Where(x => x.ApplicationId == app.Id).ToList();
1546   -
  1555 +
1547 1556 if (appPurchaseRecords.Any())
1548 1557 {
1549 1558 // 每个购买记录作为一行
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs 0 → 100644
  1 +using Microsoft.AspNetCore.Authorization;
  2 +using Microsoft.AspNetCore.Mvc;
  3 +using NCC.Common.Filter;
  4 +using NCC.Common.Helper;
  5 +using NCC.Dependency;
  6 +using NCC.DynamicApiController;
  7 +using NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary;
  8 +using NCC.Extend.Entitys.lq_attendance_summary;
  9 +using NCC.Extend.Entitys.lq_hytk_hytk;
  10 +using NCC.Extend.Entitys.lq_hytk_mx;
  11 +using NCC.Extend.Entitys.lq_kd_kdjlb;
  12 +using NCC.Extend.Entitys.lq_kd_pxmx;
  13 +using NCC.Extend.Entitys.lq_md_general_manager_lifeline;
  14 +using NCC.Extend.Entitys.lq_mdxx;
  15 +using NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics;
  16 +using NCC.Extend.Entitys.lq_xmzl;
  17 +using NCC.System.Entitys.Permission;
  18 +using SqlSugar;
  19 +using System;
  20 +using System.Collections.Generic;
  21 +using System.Linq;
  22 +using System.Threading.Tasks;
  23 +using Yitter.IdGenerator;
  24 +using Newtonsoft.Json;
  25 +
  26 +namespace NCC.Extend
  27 +{
  28 + /// <summary>
  29 + /// 科技部总经理薪酬服务
  30 + /// </summary>
  31 + [ApiDescriptionSettings(Tag = "科技部总经理薪酬服务", Name = "LqTechGeneralManagerSalary", Order = 305)]
  32 + [Route("api/Extend/[controller]")]
  33 + public class LqTechGeneralManagerSalaryService : IDynamicApiController, ITransient
  34 + {
  35 + private readonly ISqlSugarClient _db;
  36 +
  37 + /// <summary>
  38 + /// 初始化一个<see cref="LqTechGeneralManagerSalaryService"/>类型的新实例
  39 + /// </summary>
  40 + public LqTechGeneralManagerSalaryService(ISqlSugarClient db)
  41 + {
  42 + _db = db;
  43 + }
  44 +
  45 + /// <summary>
  46 + /// 获取科技部总经理工资列表
  47 + /// </summary>
  48 + /// <param name="input">查询参数</param>
  49 + /// <returns>科技部总经理工资分页列表</returns>
  50 + [HttpGet("tech-general-manager")]
  51 + public async Task<dynamic> GetTechGeneralManagerSalaryList([FromQuery] TechGeneralManagerSalaryInput input)
  52 + {
  53 + var monthStr = $"{input.Year}{input.Month:D2}";
  54 +
  55 + // 1. 检查当月是否已生成工资数据
  56 + var exists = await _db.Queryable<LqTechGeneralManagerSalaryStatisticsEntity>()
  57 + .AnyAsync(x => x.StatisticsMonth == monthStr);
  58 +
  59 + // 2. 如果没有数据,则进行计算
  60 + if (!exists)
  61 + {
  62 + await CalculateTechGeneralManagerSalary(input.Year, input.Month);
  63 + }
  64 +
  65 + // 3. 查询数据
  66 + var query = _db.Queryable<LqTechGeneralManagerSalaryStatisticsEntity>()
  67 + .Where(x => x.StatisticsMonth == monthStr);
  68 +
  69 + if (!string.IsNullOrEmpty(input.Position))
  70 + {
  71 + query = query.Where(x => x.Position == input.Position);
  72 + }
  73 +
  74 + if (!string.IsNullOrEmpty(input.Keyword))
  75 + {
  76 + query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeAccount.Contains(input.Keyword));
  77 + }
  78 +
  79 + var list = await query.Select(x => new TechGeneralManagerSalaryOutput
  80 + {
  81 + Id = x.Id,
  82 + StatisticsMonth = x.StatisticsMonth,
  83 + Position = x.Position,
  84 + EmployeeName = x.EmployeeName,
  85 + EmployeeId = x.EmployeeId,
  86 + EmployeeAccount = x.EmployeeAccount,
  87 + IsTerminated = x.IsTerminated,
  88 + StoreDetail = x.StoreDetail,
  89 + TraceabilityAmount = x.TraceabilityAmount,
  90 + CellAmount = x.CellAmount,
  91 + BaseSalary = x.BaseSalary,
  92 + TraceabilityCommissionRate = x.TraceabilityCommissionRate,
  93 + TraceabilityCommissionAmount = x.TraceabilityCommissionAmount,
  94 + CellCommissionRate = x.CellCommissionRate,
  95 + CellCommissionAmount = x.CellCommissionAmount,
  96 + TotalCommission = x.TotalCommission,
  97 + WorkingDays = x.WorkingDays,
  98 + LeaveDays = x.LeaveDays,
  99 + CalculatedGrossSalary = x.CalculatedGrossSalary,
  100 + FinalGrossSalary = x.FinalGrossSalary,
  101 + MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
  102 + MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
  103 + LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
  104 + LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
  105 + TotalSubsidy = x.TotalSubsidy,
  106 + MissingCard = x.MissingCard,
  107 + LateArrival = x.LateArrival,
  108 + LeaveDeduction = x.LeaveDeduction,
  109 + SocialInsuranceDeduction = x.SocialInsuranceDeduction,
  110 + RewardDeduction = x.RewardDeduction,
  111 + AccommodationDeduction = x.AccommodationDeduction,
  112 + StudyPeriodDeduction = x.StudyPeriodDeduction,
  113 + WorkClothesDeduction = x.WorkClothesDeduction,
  114 + TotalDeduction = x.TotalDeduction,
  115 + Bonus = x.Bonus,
  116 + ReturnPhoneDeposit = x.ReturnPhoneDeposit,
  117 + ReturnAccommodationDeposit = x.ReturnAccommodationDeposit,
  118 + ActualSalary = x.ActualSalary,
  119 + MonthlyPaymentStatus = x.MonthlyPaymentStatus,
  120 + PaidAmount = x.PaidAmount,
  121 + PendingAmount = x.PendingAmount,
  122 + LastMonthSupplement = x.LastMonthSupplement,
  123 + MonthlyTotalPayment = x.MonthlyTotalPayment,
  124 + IsLocked = x.IsLocked,
  125 + UpdateTime = x.UpdateTime
  126 + })
  127 + .ToPagedListAsync(input.currentPage, input.pageSize);
  128 +
  129 + return PageResult<TechGeneralManagerSalaryOutput>.SqlSugarPageResult(list);
  130 + }
  131 +
  132 + /// <summary>
  133 + /// 计算科技部总经理工资
  134 + /// </summary>
  135 + /// <param name="year">年份</param>
  136 + /// <param name="month">月份</param>
  137 + /// <returns></returns>
  138 + [HttpPost("calculate/tech-general-manager")]
  139 + public async Task CalculateTechGeneralManagerSalary(int year, int month)
  140 + {
  141 + var startDate = new DateTime(year, month, 1);
  142 + var endDate = startDate.AddMonths(1).AddDays(-1);
  143 + var monthStr = $"{year}{month:D2}";
  144 +
  145 + // 1. 获取基础数据
  146 +
  147 + // 1.1 先从BASE_ORGANIZE表查找组织名称包含"科技一部"或"科技二部"的组织
  148 + var techOrganizeList = await _db.Queryable<OrganizeEntity>()
  149 + .Where(x => x.FullName != null && (x.FullName.Contains("科技一部") || x.FullName.Contains("科技二部"))
  150 + && x.DeleteMark == null && x.EnabledMark == 1)
  151 + .Select(x => new { x.Id, x.FullName })
  152 + .ToListAsync();
  153 +
  154 + if (!techOrganizeList.Any())
  155 + {
  156 + // 如果没有找到科技部组织,直接返回
  157 + return;
  158 + }
  159 +
  160 + var techOrganizeIds = techOrganizeList.Select(x => x.Id).ToList();
  161 + var techOrganizeDict = techOrganizeList.ToDictionary(x => x.Id, x => x.FullName);
  162 +
  163 + // 1.2 从BASE_USER表查询岗位为"总经理"且组织ID在科技一部或科技二部的员工
  164 + var techGeneralManagerUserList = await _db.Queryable<UserEntity>()
  165 + .Where(x => x.Gw == "总经理"
  166 + && techOrganizeIds.Contains(x.OrganizeId)
  167 + && x.DeleteMark == null && x.EnabledMark == 1)
  168 + .Select(x => new { x.Id, x.RealName, x.Account, x.Gw, x.OrganizeId, x.IsOnJob })
  169 + .ToListAsync();
  170 +
  171 + if (!techGeneralManagerUserList.Any())
  172 + {
  173 + // 如果没有科技部总经理员工,直接返回
  174 + return;
  175 + }
  176 +
  177 + if (!techGeneralManagerUserList.Any())
  178 + {
  179 + // 如果没有科技部总经理员工,直接返回
  180 + return;
  181 + }
  182 +
  183 + // 1.3 获取科技部总经理归属信息(从lq_md_general_manager_lifeline表)
  184 + // 通过门店的科技部组织ID(kjb字段)找到科技一部或科技二部管理的门店
  185 + // 然后在lifeline表中找到这些门店的记录,这些记录对应的总经理就是科技部总经理
  186 + var lifelineList = await _db.Queryable<LqMdGeneralManagerLifelineEntity, LqMdxxEntity>(
  187 + (lifeline, store) => lifeline.StoreId == store.Id)
  188 + .Where((lifeline, store) =>
  189 + lifeline.Month == monthStr
  190 + && techOrganizeIds.Contains(store.Kjb))
  191 + .Select((lifeline, store) => lifeline)
  192 + .ToListAsync();
  193 +
  194 + // 1.4 获取科技一部和科技二部管理的门店(通过门店的kjb字段)
  195 + var techManagedStoreIds = await _db.Queryable<LqMdxxEntity>()
  196 + .Where(x => techOrganizeIds.Contains(x.Kjb))
  197 + .Select(x => x.Id)
  198 + .ToListAsync();
  199 +
  200 + // 1.5 按科技部总经理ID分组,获取每个科技部总经理管理的门店
  201 + // 科技部总经理管理的门店 = 科技一部/科技二部管理的所有门店(通过门店的kjb字段确定)
  202 + var managerStoreDict = new Dictionary<string, List<string>>();
  203 + foreach (var managerUser in techGeneralManagerUserList)
  204 + {
  205 + var managerId = managerUser.Id;
  206 + var managerOrganizeId = managerUser.OrganizeId;
  207 +
  208 + // 如果该总经理属于科技一部,则管理所有科技一部的门店
  209 + // 如果该总经理属于科技二部,则管理所有科技二部的门店
  210 + var managedStores = await _db.Queryable<LqMdxxEntity>()
  211 + .Where(x => x.Kjb == managerOrganizeId)
  212 + .Select(x => x.Id)
  213 + .ToListAsync();
  214 +
  215 + managerStoreDict[managerId] = managedStores;
  216 + }
  217 +
  218 + // 1.4 门店信息 (lq_mdxx)
  219 + var storeList = await _db.Queryable<LqMdxxEntity>().ToListAsync();
  220 + var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x);
  221 +
  222 + // 1.6 考勤数据 (lq_attendance_summary)
  223 + var attendanceList = await _db.Queryable<LqAttendanceSummaryEntity>()
  224 + .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
  225 + .ToListAsync();
  226 + var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
  227 +
  228 + // 1.7 获取所有管理的门店ID列表(用于后续查询,如果没有管理的门店,则为空列表)
  229 + var allManagedStoreIds = managerStoreDict.Values.SelectMany(x => x).Distinct().ToList();
  230 +
  231 + // 1.8 按科技部总经理和门店分组统计(用于生成门店明细JSON和汇总数据)
  232 + var storeDetailDict = new Dictionary<string, Dictionary<string, StoreDetailItem>>();
  233 +
  234 + // 按门店统计溯源和Cell金额(如果有管理的门店)
  235 + if (allManagedStoreIds.Any())
  236 + {
  237 + foreach (var storeId in allManagedStoreIds)
  238 + {
  239 + // 该门店的开单溯源金额
  240 + var storeTraceabilityBilling = await _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity, LqXmzlEntity>(
  241 + (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id)
  242 + .Where((pxmx, billing, item) =>
  243 + pxmx.IsEffective == 1
  244 + && billing.IsEffective == 1
  245 + && item.IsEffective == 1
  246 + && (pxmx.BeautyType == "溯源系统" || pxmx.BeautyType == "溯源"
  247 + || item.BeautyType == "溯源系统" || item.BeautyType == "溯源")
  248 + && billing.Djmd == storeId
  249 + && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1))
  250 + .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m;
  251 +
  252 + // 该门店的退卡溯源金额
  253 + var storeTraceabilityRefund = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity, LqXmzlEntity>(
  254 + (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id)
  255 + .Where((tkmx, refund, item) =>
  256 + tkmx.IsEffective == 1
  257 + && refund.IsEffective == 1
  258 + && item.IsEffective == 1
  259 + && (tkmx.BeautyType == "溯源系统" || tkmx.BeautyType == "溯源"
  260 + || item.BeautyType == "溯源系统" || item.BeautyType == "溯源")
  261 + && refund.Md == storeId
  262 + && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1))
  263 + .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m;
  264 +
  265 + // 该门店的开单Cell金额
  266 + var storeCellBilling = await _db.Queryable<LqKdPxmxEntity, LqKdKdjlbEntity, LqXmzlEntity>(
  267 + (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id)
  268 + .Where((pxmx, billing, item) =>
  269 + pxmx.IsEffective == 1
  270 + && billing.IsEffective == 1
  271 + && item.IsEffective == 1
  272 + && (pxmx.BeautyType == "cell" || pxmx.BeautyType == "Cell"
  273 + || item.BeautyType == "cell" || item.BeautyType == "Cell")
  274 + && billing.Djmd == storeId
  275 + && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1))
  276 + .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m;
  277 +
  278 + // 该门店的退卡Cell金额
  279 + var storeCellRefund = await _db.Queryable<LqHytkMxEntity, LqHytkHytkEntity, LqXmzlEntity>(
  280 + (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id)
  281 + .Where((tkmx, refund, item) =>
  282 + tkmx.IsEffective == 1
  283 + && refund.IsEffective == 1
  284 + && item.IsEffective == 1
  285 + && (tkmx.BeautyType == "cell" || tkmx.BeautyType == "Cell"
  286 + || item.BeautyType == "cell" || item.BeautyType == "Cell")
  287 + && refund.Md == storeId
  288 + && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1))
  289 + .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m;
  290 +
  291 + // 获取该门店属于哪些科技部总经理
  292 + // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理
  293 + var store = storeDict.ContainsKey(storeId) ? storeDict[storeId] : null;
  294 + var managersOfStore = new List<string>();
  295 +
  296 + if (store != null && !string.IsNullOrEmpty(store.Kjb))
  297 + {
  298 + // 找到组织ID等于门店kjb的科技部总经理
  299 + var managers = techGeneralManagerUserList
  300 + .Where(x => x.OrganizeId == store.Kjb)
  301 + .Select(x => x.Id)
  302 + .ToList();
  303 + managersOfStore.AddRange(managers);
  304 + }
  305 +
  306 + foreach (var managerId in managersOfStore)
  307 + {
  308 + if (!storeDetailDict.ContainsKey(managerId))
  309 + {
  310 + storeDetailDict[managerId] = new Dictionary<string, StoreDetailItem>();
  311 + }
  312 +
  313 + var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : "";
  314 + storeDetailDict[managerId][storeId] = new StoreDetailItem
  315 + {
  316 + StoreId = storeId,
  317 + StoreName = storeName,
  318 + TraceabilityBillingAmount = storeTraceabilityBilling,
  319 + TraceabilityRefundAmount = storeTraceabilityRefund,
  320 + TraceabilityAmount = storeTraceabilityBilling - storeTraceabilityRefund,
  321 + CellBillingAmount = storeCellBilling,
  322 + CellRefundAmount = storeCellRefund,
  323 + CellAmount = storeCellBilling - storeCellRefund
  324 + };
  325 + }
  326 + }
  327 + }
  328 +
  329 + // 2. 按科技部总经理聚合数据
  330 + var managerStats = new Dictionary<string, LqTechGeneralManagerSalaryStatisticsEntity>();
  331 +
  332 + foreach (var managerUser in techGeneralManagerUserList)
  333 + {
  334 + var managerId = managerUser.Id;
  335 +
  336 + // 获取该科技部总经理管理的门店列表
  337 + var managedStores = managerStoreDict.ContainsKey(managerId) ? managerStoreDict[managerId] : new List<string>();
  338 +
  339 + // 2.1 创建工资统计对象
  340 + // 岗位使用组织名称(科技一部/科技二部)
  341 + var position = techOrganizeDict.ContainsKey(managerUser.OrganizeId)
  342 + ? techOrganizeDict[managerUser.OrganizeId]
  343 + : "";
  344 +
  345 + var salary = new LqTechGeneralManagerSalaryStatisticsEntity
  346 + {
  347 + Id = YitIdHelper.NextId().ToString(),
  348 + StatisticsMonth = monthStr,
  349 + EmployeeId = managerId,
  350 + Position = position,
  351 + EmployeeName = managerUser.RealName ?? "",
  352 + EmployeeAccount = managerUser.Account ?? "",
  353 + IsTerminated = managerUser.IsOnJob == 0 ? 1 : 0,
  354 + CreateTime = DateTime.Now,
  355 + UpdateTime = DateTime.Now,
  356 + IsLocked = 0
  357 + };
  358 +
  359 + // 2.2 考勤数据
  360 + var attendance = attendanceDict.ContainsKey(managerId) ? attendanceDict[managerId] : null;
  361 + salary.WorkingDays = attendance?.WorkDays ?? 0;
  362 + salary.LeaveDays = attendance?.LeaveDays ?? 0;
  363 +
  364 + // 2.3 计算底薪(固定4000元)
  365 + salary.BaseSalary = 4000m;
  366 +
  367 + // 2.4 统计该科技部总经理管理的所有门店的溯源金额和Cell金额总和
  368 + decimal totalTraceabilityAmount = 0m;
  369 + decimal totalCellAmount = 0m;
  370 + var storeDetails = new List<StoreDetailItem>();
  371 +
  372 + if (managedStores.Any() && storeDetailDict.ContainsKey(managerId))
  373 + {
  374 + foreach (var storeId in managedStores)
  375 + {
  376 + if (storeDetailDict[managerId].ContainsKey(storeId))
  377 + {
  378 + var storeDetail = storeDetailDict[managerId][storeId];
  379 + totalTraceabilityAmount += storeDetail.TraceabilityAmount;
  380 + totalCellAmount += storeDetail.CellAmount;
  381 + storeDetails.Add(storeDetail);
  382 + }
  383 + }
  384 + }
  385 +
  386 + salary.TraceabilityAmount = totalTraceabilityAmount;
  387 + salary.CellAmount = totalCellAmount;
  388 +
  389 + // 2.5 保存门店明细(JSON格式)
  390 + salary.StoreDetail = JsonConvert.SerializeObject(storeDetails);
  391 +
  392 + // 2.6 计算溯源金额提成(分段累进)
  393 + var traceabilityCommission = CalculateTraceabilityCommission(totalTraceabilityAmount);
  394 + salary.TraceabilityCommissionAmount = traceabilityCommission.Amount;
  395 + salary.TraceabilityCommissionRate = traceabilityCommission.Rate;
  396 +
  397 + // 2.7 计算Cell金额提成(分段累进)
  398 + var cellCommission = CalculateCellCommission(totalCellAmount);
  399 + salary.CellCommissionAmount = cellCommission.Amount;
  400 + salary.CellCommissionRate = cellCommission.Rate;
  401 +
  402 + // 2.8 提成合计
  403 + salary.TotalCommission = salary.TraceabilityCommissionAmount + salary.CellCommissionAmount;
  404 +
  405 + // 2.9 计算应发工资
  406 + salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission;
  407 + salary.FinalGrossSalary = salary.CalculatedGrossSalary;
  408 +
  409 + // 2.10 初始化其他字段(默认值为0)
  410 + salary.MonthlyTrainingSubsidy = 0;
  411 + salary.MonthlyTransportSubsidy = 0;
  412 + salary.LastMonthTrainingSubsidy = 0;
  413 + salary.LastMonthTransportSubsidy = 0;
  414 + salary.TotalSubsidy = 0;
  415 + salary.MissingCard = 0;
  416 + salary.LateArrival = 0;
  417 + salary.LeaveDeduction = 0;
  418 + salary.SocialInsuranceDeduction = 0;
  419 + salary.RewardDeduction = 0;
  420 + salary.AccommodationDeduction = 0;
  421 + salary.StudyPeriodDeduction = 0;
  422 + salary.WorkClothesDeduction = 0;
  423 + salary.TotalDeduction = 0;
  424 + salary.Bonus = 0;
  425 + salary.ReturnPhoneDeposit = 0;
  426 + salary.ReturnAccommodationDeposit = 0;
  427 + salary.ActualSalary = salary.FinalGrossSalary - salary.TotalDeduction + salary.TotalSubsidy + salary.Bonus;
  428 + salary.MonthlyPaymentStatus = "未发放";
  429 + salary.PaidAmount = 0;
  430 + salary.PendingAmount = salary.ActualSalary;
  431 + salary.LastMonthSupplement = 0;
  432 + salary.MonthlyTotalPayment = 0;
  433 +
  434 + managerStats[managerId] = salary;
  435 + }
  436 +
  437 + // 3. 保存数据
  438 + if (managerStats.Any())
  439 + {
  440 + // 先删除当月旧数据 (防止重复)
  441 + await _db.Deleteable<LqTechGeneralManagerSalaryStatisticsEntity>()
  442 + .Where(x => x.StatisticsMonth == monthStr)
  443 + .ExecuteCommandAsync();
  444 +
  445 + await _db.Insertable(managerStats.Values.ToList()).ExecuteCommandAsync();
  446 + }
  447 + }
  448 +
  449 + /// <summary>
  450 + /// 计算溯源金额提成(分段累进)
  451 + /// </summary>
  452 + /// <param name="traceabilityAmount">溯源金额</param>
  453 + /// <returns>提成金额和平均比例</returns>
  454 + private (decimal Amount, decimal? Rate) CalculateTraceabilityCommission(decimal traceabilityAmount)
  455 + {
  456 + if (traceabilityAmount <= 0)
  457 + {
  458 + return (0m, null);
  459 + }
  460 +
  461 + decimal commissionAmount = 0m;
  462 + decimal? averageRate = null;
  463 +
  464 + if (traceabilityAmount < 200000m)
  465 + {
  466 + // < 200,000元:1%
  467 + commissionAmount = traceabilityAmount * 0.01m;
  468 + averageRate = 1.00m;
  469 + }
  470 + else if (traceabilityAmount < 300000m)
  471 + {
  472 + // 200,000-300,000元:1.5%
  473 + commissionAmount = 200000m * 0.01m + (traceabilityAmount - 200000m) * 0.015m;
  474 + averageRate = (commissionAmount / traceabilityAmount) * 100m;
  475 + }
  476 + else if (traceabilityAmount < 500000m)
  477 + {
  478 + // 300,000-500,000元:2%
  479 + commissionAmount = 200000m * 0.01m + 100000m * 0.015m + (traceabilityAmount - 300000m) * 0.02m;
  480 + averageRate = (commissionAmount / traceabilityAmount) * 100m;
  481 + }
  482 + else
  483 + {
  484 + // ≥ 500,000元:2.5%
  485 + commissionAmount = 200000m * 0.01m + 100000m * 0.015m + 200000m * 0.02m + (traceabilityAmount - 500000m) * 0.025m;
  486 + averageRate = (commissionAmount / traceabilityAmount) * 100m;
  487 + }
  488 +
  489 + return (commissionAmount, averageRate);
  490 + }
  491 +
  492 + /// <summary>
  493 + /// 计算Cell金额提成(分段累进)
  494 + /// </summary>
  495 + /// <param name="cellAmount">Cell金额</param>
  496 + /// <returns>提成金额和平均比例</returns>
  497 + private (decimal Amount, decimal? Rate) CalculateCellCommission(decimal cellAmount)
  498 + {
  499 + if (cellAmount <= 0)
  500 + {
  501 + return (0m, null);
  502 + }
  503 +
  504 + if (cellAmount < 50000m)
  505 + {
  506 + // < 50,000元:无提成
  507 + return (0m, null);
  508 + }
  509 +
  510 + decimal commissionAmount = 0m;
  511 + decimal? averageRate = null;
  512 +
  513 + if (cellAmount < 400000m)
  514 + {
  515 + // 50,000-400,000元:1%
  516 + commissionAmount = (cellAmount - 50000m) * 0.01m;
  517 + averageRate = (commissionAmount / cellAmount) * 100m;
  518 + }
  519 + else
  520 + {
  521 + // ≥ 400,000元:1.5%
  522 + commissionAmount = 350000m * 0.01m + (cellAmount - 400000m) * 0.015m;
  523 + averageRate = (commissionAmount / cellAmount) * 100m;
  524 + }
  525 +
  526 + return (commissionAmount, averageRate);
  527 + }
  528 +
  529 + /// <summary>
  530 + /// 门店明细项(用于JSON序列化)
  531 + /// </summary>
  532 + private class StoreDetailItem
  533 + {
  534 + public string StoreId { get; set; }
  535 + public string StoreName { get; set; }
  536 + public decimal TraceabilityBillingAmount { get; set; }
  537 + public decimal TraceabilityRefundAmount { get; set; }
  538 + public decimal TraceabilityAmount { get; set; }
  539 + public decimal CellBillingAmount { get; set; }
  540 + public decimal CellRefundAmount { get; set; }
  541 + public decimal CellAmount { get; set; }
  542 + }
  543 + }
  544 +}
  545 +
... ...
sql/主任工资表新增毛利相关字段.sql 0 → 100644
  1 +-- 主任工资表新增毛利相关字段
  2 +-- 表名:lq_director_salary_statistics
  3 +-- 说明:为主任工资计算添加毛利相关字段,用于计算基于毛利的提成
  4 +
  5 +-- 1. 销售业绩(开单业绩-退款业绩)
  6 +ALTER TABLE lq_director_salary_statistics
  7 +ADD COLUMN F_SalesPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '销售业绩(开单业绩-退款业绩)' AFTER F_StoreRefundPerformance;
  8 +
  9 +-- 2. 产品物料(仓库领用金额)
  10 +ALTER TABLE lq_director_salary_statistics
  11 +ADD COLUMN F_ProductMaterial DECIMAL(18,2) DEFAULT 0.00 COMMENT '产品物料(仓库领用金额,注意11月特殊规则:11月工资算10月数据)' AFTER F_SalesPerformance;
  12 +
  13 +-- 3. 合作项目成本
  14 +ALTER TABLE lq_director_salary_statistics
  15 +ADD COLUMN F_CooperationCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '合作项目成本' AFTER F_ProductMaterial;
  16 +
  17 +-- 4. 店内支出
  18 +ALTER TABLE lq_director_salary_statistics
  19 +ADD COLUMN F_StoreExpense DECIMAL(18,2) DEFAULT 0.00 COMMENT '店内支出' AFTER F_CooperationCost;
  20 +
  21 +-- 5. 洗毛巾费用
  22 +ALTER TABLE lq_director_salary_statistics
  23 +ADD COLUMN F_LaundryCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '洗毛巾费用(只统计送出的记录,F_FlowType = 0)' AFTER F_StoreExpense;
  24 +
  25 +-- 6. 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
  26 +ALTER TABLE lq_director_salary_statistics
  27 +ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT '毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)' AFTER F_LaundryCost;
  28 +
  29 +-- 7. 修改 F_StoreTotalPerformance 字段注释(说明该字段存储的是毛利,用于提成计算)
  30 +-- 注意:此字段的值将在代码中改为存储毛利,而不是开单-退卡
  31 +ALTER TABLE lq_director_salary_statistics
  32 +MODIFY COLUMN F_StoreTotalPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '门店总业绩(毛利,用于提成计算)';
  33 +
... ...
sql/修复产品平均单价计算.sql 0 → 100644
  1 +-- ============================================
  2 +-- 修复产品平均单价计算
  3 +-- ============================================
  4 +-- 说明:重新计算所有产品的平均单价,基于当前所有有效库存
  5 +-- 执行时间:2025年
  6 +-- ============================================
  7 +
  8 +-- 重新计算所有产品的平均单价(基于所有有效库存)
  9 +UPDATE `lq_product` p
  10 +SET p.`F_AveragePrice` = (
  11 + SELECT
  12 + CASE
  13 + WHEN SUM(inv.`F_Quantity`) > 0 THEN
  14 + SUM(
  15 + CASE
  16 + WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
  17 + WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
  18 + ELSE 0
  19 + END
  20 + ) / SUM(inv.`F_Quantity`)
  21 + ELSE p.`F_Price`
  22 + END
  23 + FROM `lq_inventory` inv
  24 + WHERE inv.`F_ProductId` = p.`F_Id`
  25 + AND inv.`F_IsEffective` = 1
  26 + AND inv.`F_Quantity` > 0
  27 +)
  28 +WHERE EXISTS (
  29 + SELECT 1
  30 + FROM `lq_inventory` inv
  31 + WHERE inv.`F_ProductId` = p.`F_Id`
  32 + AND inv.`F_IsEffective` = 1
  33 + AND inv.`F_Quantity` > 0
  34 +);
  35 +
  36 +-- 对于没有库存的产品,将平均单价设置为产品价格
  37 +UPDATE `lq_product` p
  38 +SET p.`F_AveragePrice` = p.`F_Price`
  39 +WHERE p.`F_AveragePrice` = 0 OR p.`F_AveragePrice` IS NULL
  40 + OR NOT EXISTS (
  41 + SELECT 1
  42 + FROM `lq_inventory` inv
  43 + WHERE inv.`F_ProductId` = p.`F_Id`
  44 + AND inv.`F_IsEffective` = 1
  45 + AND inv.`F_Quantity` > 0
  46 + );
  47 +
  48 +-- 验证特定产品的平均单价(产品ID: 770163009904444677)
  49 +-- 应该显示:平均单价 = 200.00 (100+200+300)/3 = 600/3 = 200
  50 +SELECT
  51 + p.`F_Id` as ProductId,
  52 + p.`F_ProductName` as ProductName,
  53 + p.`F_Price` as Price,
  54 + p.`F_AveragePrice` as AveragePrice,
  55 + SUM(inv.`F_Quantity`) as TotalQuantity,
  56 + SUM(
  57 + CASE
  58 + WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
  59 + WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
  60 + ELSE 0
  61 + END
  62 + ) as TotalAmount,
  63 + CASE
  64 + WHEN SUM(inv.`F_Quantity`) > 0 THEN
  65 + SUM(
  66 + CASE
  67 + WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
  68 + WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
  69 + ELSE 0
  70 + END
  71 + ) / SUM(inv.`F_Quantity`)
  72 + ELSE p.`F_Price`
  73 + END as CalculatedAveragePrice
  74 +FROM `lq_product` p
  75 +LEFT JOIN `lq_inventory` inv ON inv.`F_ProductId` = p.`F_Id` AND inv.`F_IsEffective` = 1 AND inv.`F_Quantity` > 0
  76 +WHERE p.`F_Id` = '770163009904444677'
  77 +GROUP BY p.`F_Id`, p.`F_ProductName`, p.`F_Price`, p.`F_AveragePrice`;
... ...
sql/创建大项目主管工资统计表.sql 0 → 100644
  1 +-- ============================================
  2 +-- 创建大项目主管工资统计表
  3 +-- 功能:存储大项目主管每月的工资计算数据,包括底薪、业绩提成、扣款、补贴、奖金、支付等信息
  4 +-- 创建时间:2025年
  5 +-- ============================================
  6 +
  7 +-- 删除表(如果存在)
  8 +DROP TABLE IF EXISTS lq_major_project_director_salary_statistics;
  9 +
  10 +-- ============================================
  11 +-- 创建大项目主管工资统计表
  12 +-- ============================================
  13 +CREATE TABLE lq_major_project_director_salary_statistics (
  14 + -- 主键
  15 + F_Id VARCHAR(50) NOT NULL COMMENT '主键ID',
  16 +
  17 + -- 一、基础信息字段
  18 + F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份(YYYYMM格式)',
  19 + F_Position VARCHAR(50) NOT NULL COMMENT '核算岗位(大项目一部/大项目二部等)',
  20 + F_EmployeeName VARCHAR(100) NOT NULL COMMENT '员工姓名',
  21 + F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID',
  22 + F_EmployeeAccount VARCHAR(100) NULL COMMENT '员工账号',
  23 + F_IsTerminated INT NOT NULL DEFAULT 0 COMMENT '是否离职(0=在职,1=离职)',
  24 +
  25 + -- 二、管理的门店信息(JSON格式)
  26 + F_StoreDetail TEXT NULL COMMENT '管理的门店明细(JSON格式,记录每个门店的业绩详情)',
  27 +
  28 + -- 三、业绩相关字段
  29 + F_TotalPerformance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '总业绩(管理的所有门店的总业绩总和,开单-退卡)',
  30 + F_BillingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '开单金额(管理的所有门店的开单金额总和)',
  31 + F_RefundAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退卡金额(管理的所有门店的退卡金额总和)',
  32 +
  33 + -- 四、底薪相关字段
  34 + F_BaseSalary DECIMAL(18,2) NOT NULL DEFAULT 3500.00 COMMENT '底薪金额(固定3500元)',
  35 +
  36 + -- 五、提成相关字段
  37 + F_CommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT '提成比例(根据总业绩分段:0%/1%/1.5%)',
  38 + F_CommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '提成金额(总业绩 × 提成比例)',
  39 +
  40 + -- 六、考勤相关字段
  41 + F_WorkingDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '在店天数',
  42 + F_LeaveDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假天数',
  43 +
  44 + -- 七、工资计算字段
  45 + F_CalculatedGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '核算应发工资(底薪 + 提成金额)',
  46 + F_FinalGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '最终应发工资(等于核算应发工资)',
  47 +
  48 + -- 八、补贴相关字段
  49 + F_MonthlyTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月培训补贴',
  50 + F_MonthlyTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月交通补贴',
  51 + F_LastMonthTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月培训补贴',
  52 + F_LastMonthTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月交通补贴',
  53 + F_TotalSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补贴合计',
  54 +
  55 + -- 九、扣款相关字段
  56 + F_MissingCard DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '缺卡扣款',
  57 + F_LateArrival DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '迟到扣款',
  58 + F_LeaveDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假扣款',
  59 + F_SocialInsuranceDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣社保',
  60 + F_RewardDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣除奖励',
  61 + F_AccommodationDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣住宿费',
  62 + F_StudyPeriodDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣学习期费用',
  63 + F_WorkClothesDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣工作服费用',
  64 + F_TotalDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣款合计',
  65 +
  66 + -- 十、奖金相关字段
  67 + F_Bonus DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '发奖金',
  68 + F_ReturnPhoneDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退手机押金',
  69 + F_ReturnAccommodationDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退住宿押金',
  70 +
  71 + -- 十一、支付相关字段
  72 + F_ActualSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资(最终应发工资 - 扣款合计 + 补贴合计 + 奖金)',
  73 + F_MonthlyPaymentStatus VARCHAR(20) NOT NULL DEFAULT '未发放' COMMENT '当月是否发放(已发放/未发放/部分发放)',
  74 + F_PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '支付金额',
  75 + F_PendingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '待支付金额',
  76 + F_LastMonthSupplement DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补发上月',
  77 + F_MonthlyTotalPayment DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月支付总额',
  78 +
  79 + -- 十二、系统字段
  80 + F_IsLocked INT NOT NULL DEFAULT 0 COMMENT '是否锁定(0=未锁定,1=已锁定)',
  81 + F_CreateTime DATETIME NOT NULL COMMENT '创建时间',
  82 + F_UpdateTime DATETIME NOT NULL COMMENT '更新时间',
  83 + F_CreateUser VARCHAR(50) NULL COMMENT '创建人',
  84 + F_UpdateUser VARCHAR(50) NULL COMMENT '更新人',
  85 +
  86 + -- 主键约束
  87 + PRIMARY KEY (F_Id),
  88 +
  89 + -- 唯一索引:确保同一员工同一月份只有一条记录
  90 + UNIQUE KEY `uk_employee_month` (F_EmployeeId, F_StatisticsMonth),
  91 +
  92 + -- 普通索引
  93 + KEY `idx_statistics_month` (F_StatisticsMonth),
  94 + KEY `idx_employee_id` (F_EmployeeId),
  95 + KEY `idx_employee_account` (F_EmployeeAccount),
  96 + KEY `idx_position` (F_Position),
  97 + KEY `idx_is_terminated` (F_IsTerminated),
  98 + KEY `idx_create_time` (F_CreateTime)
  99 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大项目主管工资统计表';
  100 +
  101 +-- ============================================
  102 +-- 表结构说明
  103 +-- ============================================
  104 +/*
  105 +表名:lq_major_project_director_salary_statistics(大项目主管工资统计表)
  106 +
  107 +功能说明:
  108 +1. 存储大项目主管每月的工资计算数据
  109 +2. 包括底薪、业绩提成、扣款、补贴、奖金、支付等信息
  110 +3. 支持按员工、月份查询
  111 +4. 记录管理的门店汇总信息
  112 +
  113 +主要字段说明:
  114 +- F_BaseSalary:底薪(固定3500元)
  115 +- F_TotalPerformance:总业绩(管理的所有门店的总业绩总和,开单-退卡)
  116 +- F_CommissionRate:提成比例(根据总业绩分段:0%/1%/1.5%)
  117 +- F_CommissionAmount:提成金额(总业绩 × 提成比例)
  118 +- F_StoreDetail:门店业绩明细(JSON格式,记录每个门店的业绩详情)
  119 +
  120 +数据来源:
  121 +- 大项目主管归属:BASE_USER 表(岗位为"主管",组织ID为大项目一部或大项目二部)
  122 +- 管理的门店:lq_md_target 表的 F_MajorProjectDepartment 字段(按月份筛选)
  123 +- 开单业绩:lq_kd_kdjlb 表的 sfyj 字段(按管理的门店统计)
  124 +- 退卡业绩:lq_hytk_hytk 表的 F_ActualRefundAmount 或 tkje 字段(按管理的门店统计)
  125 +
  126 +计算公式:
  127 +- 总业绩 = 管理的所有门店的开单金额 - 退卡金额
  128 +- 提成计算逻辑:
  129 + 1. 总业绩 <= 50万:无提成(0%)
  130 + 2. 50万 < 总业绩 <= 70万:1%提成
  131 + 3. 总业绩 > 70万:1.5%提成
  132 +- 核算应发工资 = 底薪(3500) + 提成金额
  133 +- 最终应发工资 = 核算应发工资
  134 +- 实发工资 = 最终应发工资 - 扣款合计 + 补贴合计 + 奖金
  135 +
  136 +门店业绩明细JSON格式示例:
  137 +[
  138 + {
  139 + "storeId": "A001",
  140 + "storeName": "门店A",
  141 + "billingAmount": 160000.00,
  142 + "refundAmount": 10000.00,
  143 + "totalPerformance": 150000.00
  144 + },
  145 + {
  146 + "storeId": "B001",
  147 + "storeName": "门店B",
  148 + "billingAmount": 105000.00,
  149 + "refundAmount": 5000.00,
  150 + "totalPerformance": 100000.00
  151 + }
  152 +]
  153 +
  154 +索引说明:
  155 +- 主键索引:F_Id
  156 +- 唯一索引:F_EmployeeId + F_StatisticsMonth(确保同一员工同一月份只有一条记录)
  157 +- 普通索引:
  158 + - F_StatisticsMonth:按月份查询
  159 + - F_EmployeeId:按员工查询
  160 + - F_EmployeeAccount:按员工账号查询
  161 + - F_Position:按岗位查询
  162 + - F_IsTerminated:按离职状态查询
  163 + - F_CreateTime:按创建时间查询
  164 +
  165 +数据校验要求:
  166 +1. 总业绩必须 >= 0(不能为负数)
  167 +2. 提成比例必须为 0、1 或 1.5(对应不同业绩区间)
  168 +3. 底薪固定为3500元,不允许修改
  169 +*/
  170 +
... ...
sql/创建库存使用申请审批流程表.sql
... ... @@ -110,3 +110,5 @@ WHERE u.`F_UnitPrice` = 0 OR u.`F_UnitPrice` IS NULL;
110 110  
111 111  
112 112  
  113 +
  114 +
... ...
sql/创建科技部总经理工资统计表.sql 0 → 100644
  1 +-- ============================================
  2 +-- 创建科技部总经理工资统计表
  3 +-- 功能:存储科技部总经理每月的工资计算数据,包括底薪、溯源金额提成、Cell金额提成、扣款、补贴、奖金、支付等信息
  4 +-- 创建时间:2025年
  5 +-- ============================================
  6 +
  7 +-- 删除表(如果存在)
  8 +DROP TABLE IF EXISTS lq_tech_general_manager_salary_statistics;
  9 +
  10 +-- ============================================
  11 +-- 创建科技部总经理工资统计表
  12 +-- ============================================
  13 +CREATE TABLE lq_tech_general_manager_salary_statistics (
  14 + -- 主键
  15 + F_Id VARCHAR(50) NOT NULL COMMENT '主键ID',
  16 +
  17 + -- 一、基础信息字段
  18 + F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份(YYYYMM格式)',
  19 + F_Position VARCHAR(50) NOT NULL COMMENT '核算岗位(科技一部/科技二部等)',
  20 + F_EmployeeName VARCHAR(100) NOT NULL COMMENT '员工姓名',
  21 + F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID',
  22 + F_EmployeeAccount VARCHAR(100) NULL COMMENT '员工账号',
  23 + F_IsTerminated INT NOT NULL DEFAULT 0 COMMENT '是否离职(0=在职,1=离职)',
  24 +
  25 + -- 二、管理的门店信息(JSON格式)
  26 + F_StoreDetail TEXT NULL COMMENT '管理的门店明细(JSON格式,记录每个门店的溯源金额和Cell金额详情)',
  27 +
  28 + -- 三、业绩相关字段
  29 + F_TraceabilityAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额(管理的所有门店的溯源金额总和,开单-退卡)',
  30 + F_CellAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额(管理的所有门店的Cell金额总和,开单-退卡)',
  31 +
  32 + -- 四、底薪相关字段
  33 + F_BaseSalary DECIMAL(18,2) NOT NULL DEFAULT 4000.00 COMMENT '底薪金额(固定4000元)',
  34 +
  35 + -- 五、提成相关字段
  36 + F_TraceabilityCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT '溯源金额提成比例(分段计算,存储平均比例)',
  37 + F_TraceabilityCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额提成金额',
  38 + F_CellCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT 'Cell金额提成比例(分段计算,存储平均比例)',
  39 + F_CellCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额提成金额',
  40 + F_TotalCommission DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '提成合计(溯源提成+Cell提成)',
  41 +
  42 + -- 六、考勤相关字段
  43 + F_WorkingDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '在店天数',
  44 + F_LeaveDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假天数',
  45 +
  46 + -- 七、工资计算字段
  47 + F_CalculatedGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '核算应发工资(底薪 + 提成合计)',
  48 + F_FinalGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '最终应发工资(等于核算应发工资)',
  49 +
  50 + -- 八、补贴相关字段
  51 + F_MonthlyTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月培训补贴',
  52 + F_MonthlyTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月交通补贴',
  53 + F_LastMonthTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月培训补贴',
  54 + F_LastMonthTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月交通补贴',
  55 + F_TotalSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补贴合计',
  56 +
  57 + -- 九、扣款相关字段
  58 + F_MissingCard DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '缺卡扣款',
  59 + F_LateArrival DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '迟到扣款',
  60 + F_LeaveDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假扣款',
  61 + F_SocialInsuranceDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣社保',
  62 + F_RewardDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣除奖励',
  63 + F_AccommodationDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣住宿费',
  64 + F_StudyPeriodDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣学习期费用',
  65 + F_WorkClothesDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣工作服费用',
  66 + F_TotalDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣款合计',
  67 +
  68 + -- 十、奖金相关字段
  69 + F_Bonus DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '发奖金',
  70 + F_ReturnPhoneDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退手机押金',
  71 + F_ReturnAccommodationDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退住宿押金',
  72 +
  73 + -- 十一、支付相关字段
  74 + F_ActualSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资(最终应发工资 - 扣款合计 + 补贴合计 + 奖金)',
  75 + F_MonthlyPaymentStatus VARCHAR(20) NOT NULL DEFAULT '未发放' COMMENT '当月是否发放(已发放/未发放/部分发放)',
  76 + F_PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '支付金额',
  77 + F_PendingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '待支付金额',
  78 + F_LastMonthSupplement DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补发上月',
  79 + F_MonthlyTotalPayment DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月支付总额',
  80 +
  81 + -- 十二、系统字段
  82 + F_IsLocked INT NOT NULL DEFAULT 0 COMMENT '是否锁定(0=未锁定,1=已锁定)',
  83 + F_CreateTime DATETIME NOT NULL COMMENT '创建时间',
  84 + F_UpdateTime DATETIME NOT NULL COMMENT '更新时间',
  85 + F_CreateUser VARCHAR(50) NULL COMMENT '创建人',
  86 + F_UpdateUser VARCHAR(50) NULL COMMENT '更新人',
  87 +
  88 + -- 主键约束
  89 + PRIMARY KEY (F_Id),
  90 +
  91 + -- 唯一索引:确保同一员工同一月份只有一条记录
  92 + UNIQUE KEY `uk_employee_month` (F_EmployeeId, F_StatisticsMonth),
  93 +
  94 + -- 普通索引
  95 + KEY `idx_statistics_month` (F_StatisticsMonth),
  96 + KEY `idx_employee_id` (F_EmployeeId),
  97 + KEY `idx_employee_account` (F_EmployeeAccount),
  98 + KEY `idx_position` (F_Position),
  99 + KEY `idx_is_terminated` (F_IsTerminated),
  100 + KEY `idx_create_time` (F_CreateTime)
  101 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科技部总经理工资统计表';
  102 +
  103 +-- ============================================
  104 +-- 表结构说明
  105 +-- ============================================
  106 +/*
  107 +表名:lq_tech_general_manager_salary_statistics(科技部总经理工资统计表)
  108 +
  109 +功能说明:
  110 +1. 存储科技部总经理每月的工资计算数据
  111 +2. 包括底薪、溯源金额提成、Cell金额提成、扣款、补贴、奖金、支付等信息
  112 +3. 支持按员工、月份查询
  113 +4. 记录管理的门店汇总信息
  114 +
  115 +主要字段说明:
  116 +- F_BaseSalary:底薪(固定4000元)
  117 +- F_TraceabilityAmount:溯源金额(管理的所有门店的溯源金额总和,开单-退卡)
  118 +- F_CellAmount:Cell金额(管理的所有门店的Cell金额总和,开单-退卡)
  119 +- F_TraceabilityCommissionAmount:溯源金额提成金额
  120 +- F_CellCommissionAmount:Cell金额提成金额
  121 +- F_StoreDetail:管理的门店明细(JSON格式,记录每个门店的溯源金额和Cell金额详情)
  122 +
  123 +数据来源:
  124 +- 科技部总经理识别:BASE_USER 表(F_GW字段包含"科技一部"、"科技二部"等)
  125 +- 管理的门店归属:lq_md_general_manager_lifeline 表(通过F_GeneralManagerId和F_Month获取)
  126 +- 溯源金额:lq_kd_pxmx 表(F_BeautyType='溯源系统'或'溯源')和 lq_hytk_mx 表(退卡)
  127 +- Cell金额:lq_kd_pxmx 表(F_BeautyType='cell'或'Cell')和 lq_hytk_mx 表(退卡)
  128 +
  129 +计算公式:
  130 +- 溯源金额 = 管理的所有门店的溯源类型品项开单金额总和 - 退卡金额总和
  131 +- Cell金额 = 管理的所有门店的Cell类型品项开单金额总和 - 退卡金额总和
  132 +- 溯源金额提成计算(分段累进):
  133 + - < 200,000元:提成 = 溯源金额 × 1%
  134 + - 200,000-300,000元:提成 = 200,000 × 1% + (溯源金额 - 200,000) × 1.5%
  135 + - 300,000-500,000元:提成 = 200,000 × 1% + 100,000 × 1.5% + (溯源金额 - 300,000) × 2%
  136 + - ≥ 500,000元:提成 = 200,000 × 1% + 100,000 × 1.5% + 200,000 × 2% + (溯源金额 - 500,000) × 2.5%
  137 +- Cell金额提成计算(分段累进):
  138 + - < 50,000元:提成 = 0(无提成)
  139 + - 50,000-400,000元:提成 = (Cell金额 - 50,000) × 1%
  140 + - ≥ 400,000元:提成 = 350,000 × 1% + (Cell金额 - 400,000) × 1.5%
  141 +- 提成合计 = 溯源金额提成 + Cell金额提成
  142 +- 核算应发工资 = 底薪(4000) + 提成合计
  143 +- 最终应发工资 = 核算应发工资
  144 +- 实发工资 = 最终应发工资 - 扣款合计 + 补贴合计 + 奖金
  145 +
  146 +门店明细JSON格式示例:
  147 +[
  148 + {
  149 + "storeId": "A001",
  150 + "storeName": "门店A",
  151 + "traceabilityBillingAmount": 160000.00,
  152 + "traceabilityRefundAmount": 10000.00,
  153 + "traceabilityAmount": 150000.00,
  154 + "cellBillingAmount": 85000.00,
  155 + "cellRefundAmount": 5000.00,
  156 + "cellAmount": 80000.00
  157 + },
  158 + {
  159 + "storeId": "B001",
  160 + "storeName": "门店B",
  161 + "traceabilityBillingAmount": 105000.00,
  162 + "traceabilityRefundAmount": 5000.00,
  163 + "traceabilityAmount": 100000.00,
  164 + "cellBillingAmount": 125000.00,
  165 + "cellRefundAmount": 5000.00,
  166 + "cellAmount": 120000.00
  167 + }
  168 +]
  169 +
  170 +JSON字段说明:
  171 +- storeId:门店ID
  172 +- storeName:门店名称
  173 +- traceabilityBillingAmount:该门店的溯源类型品项开单金额
  174 +- traceabilityRefundAmount:该门店的溯源类型品项退卡金额
  175 +- traceabilityAmount:该门店的净溯源金额(开单-退卡)
  176 +- cellBillingAmount:该门店的Cell类型品项开单金额
  177 +- cellRefundAmount:该门店的Cell类型品项退卡金额
  178 +- cellAmount:该门店的净Cell金额(开单-退卡)
  179 +
  180 +索引说明:
  181 +- 主键索引:F_Id
  182 +- 唯一索引:F_EmployeeId + F_StatisticsMonth(确保同一员工同一月份只有一条记录)
  183 +- 普通索引:
  184 + - F_StatisticsMonth:按月份查询
  185 + - F_EmployeeId:按员工查询
  186 + - F_Position:按岗位查询(科技一部/科技二部等)
  187 + - F_CreateTime:按创建时间查询
  188 +
  189 +数据校验要求:
  190 +1. 底薪固定为4000元
  191 +2. 必须从lq_md_general_manager_lifeline表获取管理的门店
  192 +3. 必须按管理的门店筛选,只统计归属范围内的门店
  193 +4. 必须正确区分溯源和Cell类型(通过F_BeautyType字段)
  194 +5. 必须扣除退卡金额,确保数据准确性
  195 +6. 提成必须按照分段累进方式计算
  196 +7. 如果溯源金额或Cell金额为0或负数,对应提成为0
  197 +*/
  198 +
... ...
sql/更新开单扣减信息表品项分类字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 更新开单扣减信息表(lq_kd_deductinfo)的品项分类字段
  3 +-- ============================================
  4 +-- 说明:此脚本用于更新 lq_kd_deductinfo 表的 F_ItemCategory 字段
  5 +--
  6 +-- 数据来源:从关联的项目资料表(lq_xmzl)的 qt2 字段获取品项分类
  7 +--
  8 +-- 关联关系:
  9 +-- - lq_kd_deductinfo.F_ItemId = lq_xmzl.F_Id
  10 +--
  11 +-- 更新逻辑:
  12 +-- - 更新所有记录(不判断是否有效)
  13 +-- - 只更新关联的项目资料存在且qt2字段有值的记录
  14 +-- - 从 lq_xmzl.qt2 字段获取品项分类(医美/科美/生美)
  15 +-- - 如果当前 F_ItemCategory 已有值但与 lq_xmzl.qt2 不一致,也会更新为最新值
  16 +
  17 +-- ============================================
  18 +-- 更新开单扣减信息表的品项分类字段
  19 +-- ============================================
  20 +UPDATE lq_kd_deductinfo deduct
  21 +INNER JOIN lq_xmzl xmzl ON deduct.F_ItemId = xmzl.F_Id
  22 +SET deduct.F_ItemCategory = xmzl.qt2
  23 +WHERE xmzl.qt2 IS NOT NULL
  24 + AND xmzl.qt2 != ''
  25 + AND (deduct.F_ItemCategory IS NULL
  26 + OR deduct.F_ItemCategory = ''
  27 + OR deduct.F_ItemCategory != xmzl.qt2);
  28 +
  29 +-- ============================================
  30 +-- 验证更新结果
  31 +-- ============================================
  32 +-- 查看更新后的统计信息
  33 +-- SELECT
  34 +-- F_ItemCategory AS 品项分类,
  35 +-- COUNT(*) AS 记录数
  36 +-- FROM lq_kd_deductinfo
  37 +-- GROUP BY F_ItemCategory
  38 +-- ORDER BY 记录数 DESC;
  39 +
  40 +-- 查看未更新的记录数(关联的项目资料不存在或qt2为空)
  41 +-- SELECT COUNT(*) AS 未更新记录数
  42 +-- FROM lq_kd_deductinfo deduct
  43 +-- LEFT JOIN lq_xmzl xmzl ON deduct.F_ItemId = xmzl.F_Id
  44 +-- WHERE xmzl.F_Id IS NULL
  45 +-- OR xmzl.qt2 IS NULL
  46 +-- OR xmzl.qt2 = '';
  47 +
  48 +-- 查看更新前后的对比(需要先备份数据)
  49 +-- SELECT
  50 +-- deduct.F_Id,
  51 +-- deduct.F_ItemId,
  52 +-- deduct.F_ItemName,
  53 +-- deduct.F_ItemCategory AS 更新前分类,
  54 +-- xmzl.qt2 AS 更新后分类
  55 +-- FROM lq_kd_deductinfo deduct
  56 +-- INNER JOIN lq_xmzl xmzl ON deduct.F_ItemId = xmzl.F_Id
  57 +-- WHERE xmzl.qt2 IS NOT NULL
  58 +-- AND xmzl.qt2 != ''
  59 +-- AND (deduct.F_ItemCategory IS NULL
  60 +-- OR deduct.F_ItemCategory = ''
  61 +-- OR deduct.F_ItemCategory != xmzl.qt2)
  62 +-- LIMIT 100;
... ...
sql/添加产品平均单价字段.sql 0 → 100644
  1 +-- ============================================
  2 +-- 添加产品平均单价字段
  3 +-- ============================================
  4 +-- 说明:为产品表添加平均单价字段,用于维护加权平均成本
  5 +-- 执行时间:2025年
  6 +-- ============================================
  7 +
  8 +-- 1. 添加平均单价字段到产品表
  9 +ALTER TABLE `lq_product`
  10 + ADD COLUMN `F_AveragePrice` decimal(18,2) DEFAULT 0.00 COMMENT '平均单价(加权平均成本,用于出库计价)' AFTER `F_Price`,
  11 + ADD INDEX `idx_average_price` (`F_AveragePrice`);
  12 +
  13 +-- 2. 初始化历史数据的平均单价
  14 +-- 说明:对于已有库存的产品,根据当前库存计算初始平均单价
  15 +-- 计算公式:平均单价 = SUM(库存金额) / SUM(库存数量)
  16 +-- 其中,库存金额优先使用 F_FinalAmount,其次使用 F_PurchaseUnitPrice * F_Quantity
  17 +
  18 +UPDATE `lq_product` p
  19 +SET p.`F_AveragePrice` = (
  20 + SELECT
  21 + CASE
  22 + WHEN SUM(inv.`F_Quantity`) > 0 THEN
  23 + SUM(
  24 + CASE
  25 + WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
  26 + WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
  27 + ELSE 0
  28 + END
  29 + ) / SUM(inv.`F_Quantity`)
  30 + ELSE p.`F_Price`
  31 + END
  32 + FROM `lq_inventory` inv
  33 + WHERE inv.`F_ProductId` = p.`F_Id`
  34 + AND inv.`F_IsEffective` = 1
  35 + AND inv.`F_Quantity` > 0
  36 +)
  37 +WHERE EXISTS (
  38 + SELECT 1
  39 + FROM `lq_inventory` inv
  40 + WHERE inv.`F_ProductId` = p.`F_Id`
  41 + AND inv.`F_IsEffective` = 1
  42 + AND inv.`F_Quantity` > 0
  43 +);
  44 +
  45 +-- 3. 对于没有库存的产品,将平均单价设置为产品价格
  46 +UPDATE `lq_product` p
  47 +SET p.`F_AveragePrice` = p.`F_Price`
  48 +WHERE p.`F_AveragePrice` = 0 OR p.`F_AveragePrice` IS NULL;
... ...
主任工资毛利计算逻辑梳理.md 0 → 100644
  1 +# 主任工资毛利计算逻辑梳理
  2 +
  3 +## 📋 概述
  4 +
  5 +根据店长工资计算规则,主任工资也需要使用**毛利**来计算提成,而不是直接使用门店业绩(开单-退卡)。
  6 +
  7 +## 💰 毛利计算公式
  8 +
  9 +### 核心公式
  10 +
  11 +```
  12 +毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
  13 +```
  14 +
  15 +其中:
  16 +- **销售业绩** = 开单业绩 - 退款业绩
  17 +- **产品物料** = 仓库领用金额合计(注意11月特殊规则:11月工资算10月数据)
  18 +- **合作项目成本** = 合作成本表合计金额
  19 +- **店内支出** = 店内支出表合计金额
  20 +- **洗毛巾** = 送洗记录总费用(只统计送出的记录,F_FlowType = 0)
  21 +
  22 +---
  23 +
  24 +## 📊 数据来源
  25 +
  26 +### 1. 销售业绩
  27 +
  28 +**计算公式**:
  29 +```
  30 +销售业绩 = 开单业绩 - 退款业绩
  31 +```
  32 +
  33 +**数据来源**:
  34 +
  35 +1. **开单业绩**:
  36 + - 表:`lq_kd_kdjlb`(开单记录表)
  37 + - 字段:`sfyj`(实付业绩)
  38 + - 条件:
  39 + - `F_IsEffective = 1`(有效记录)
  40 + - `Djmd = @StoreId`(门店ID)
  41 + - `DATE_FORMAT(Kdrq, '%Y%m') = @Month`(月份,YYYYMM格式)
  42 +
  43 +2. **退款业绩**:
  44 + - 表:`lq_hytk_hytk`(退卡记录表)
  45 + - 字段:`F_ActualRefundAmount`(实际退款金额)
  46 + - 条件:
  47 + - `F_IsEffective = 1`(有效记录)
  48 + - `md = @StoreId`(门店ID)
  49 + - `DATE_FORMAT(tksj, '%Y%m') = @Month`(月份,YYYYMM格式)
  50 +
  51 +---
  52 +
  53 +### 2. 产品物料
  54 +
  55 +**计算公式**:
  56 +```
  57 +产品物料 = 仓库领用金额合计
  58 +```
  59 +
  60 +**数据来源**:
  61 +- 表:`lq_inventory_usage`(库存使用记录表)
  62 +- 字段:`F_TotalAmount`(合计金额)
  63 +- 条件:
  64 + - `F_IsEffective = 1`(有效记录)
  65 + - `F_StoreId = @StoreId`(门店ID)
  66 + - **特殊规则**:核算11月工资时,算的是10月份的仓库领用
  67 + - 如果计算月份是11月,则查询10月的数据
  68 + - 其他月份正常查询当月数据
  69 +
  70 +**SQL示例**:
  71 +```sql
  72 +-- 产品物料(特殊规则:11月工资算10月数据)
  73 +SET @QueryMonth = @Month;
  74 +IF @Month = '202411' THEN
  75 + SET @QueryMonth = '202410';
  76 +END IF;
  77 +
  78 +SELECT COALESCE(SUM(F_TotalAmount), 0) as MaterialCost
  79 +FROM lq_inventory_usage
  80 +WHERE F_StoreId = @StoreId
  81 + AND DATE_FORMAT(F_UsageTime, '%Y%m') = @QueryMonth
  82 + AND F_IsEffective = 1
  83 +```
  84 +
  85 +---
  86 +
  87 +### 3. 合作项目成本
  88 +
  89 +**计算公式**:
  90 +```
  91 +合作项目成本 = 合作成本表合计金额
  92 +```
  93 +
  94 +**数据来源**:
  95 +- 表:`lq_cooperation_cost`(合作成本表)
  96 +- 字段:`F_TotalAmount`(合计金额)
  97 +- 条件:
  98 + - `F_StoreId = @StoreId`(门店ID)
  99 + - `F_Year = @Year`(年份)
  100 + - `F_Month = @Month`(月份,YYYYMM格式)
  101 + - `F_IsEffective = 1`(有效记录)
  102 +
  103 +---
  104 +
  105 +### 4. 店内支出
  106 +
  107 +**计算公式**:
  108 +```
  109 +店内支出 = 店内支出表合计金额
  110 +```
  111 +
  112 +**数据来源**:
  113 +- 表:`lq_store_expense`(店内支出表)
  114 +- 字段:`F_Amount`(金额)
  115 +- 条件:
  116 + - `F_StoreId = @StoreId`(门店ID)
  117 + - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @Month`(月份,YYYYMM格式)
  118 + - `F_IsEffective = 1`(有效记录)
  119 +
  120 +---
  121 +
  122 +### 5. 洗毛巾费用
  123 +
  124 +**计算公式**:
  125 +```
  126 +洗毛巾 = 送洗记录总费用
  127 +```
  128 +
  129 +**数据来源**:
  130 +- 表:`lq_laundry_flow`(洗毛巾流水表)
  131 +- 字段:`F_TotalPrice`(总费用)
  132 +- 条件:
  133 + - `F_IsEffective = 1`(有效记录)
  134 + - `F_StoreId = @StoreId`(门店ID)
  135 + - `F_FlowType = 0`(只统计送出的记录)
  136 + - `DATE_FORMAT(F_CreateTime, '%Y%m') = @Month`(月份,YYYYMM格式)
  137 +
  138 +---
  139 +
  140 +## 🔄 修改方案
  141 +
  142 +### 1. 数据库字段调整
  143 +
  144 +#### 需要新增的字段
  145 +
  146 +在 `lq_director_salary_statistics` 表中新增以下字段:
  147 +
  148 +```sql
  149 +-- 销售业绩(开单业绩-退款业绩)
  150 +ALTER TABLE lq_director_salary_statistics
  151 +ADD COLUMN F_SalesPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '销售业绩(开单业绩-退款业绩)' AFTER F_StoreRefundPerformance;
  152 +
  153 +-- 产品物料
  154 +ALTER TABLE lq_director_salary_statistics
  155 +ADD COLUMN F_ProductMaterial DECIMAL(18,2) DEFAULT 0.00 COMMENT '产品物料(仓库领用金额)' AFTER F_SalesPerformance;
  156 +
  157 +-- 合作项目成本
  158 +ALTER TABLE lq_director_salary_statistics
  159 +ADD COLUMN F_CooperationCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '合作项目成本' AFTER F_ProductMaterial;
  160 +
  161 +-- 店内支出
  162 +ALTER TABLE lq_director_salary_statistics
  163 +ADD COLUMN F_StoreExpense DECIMAL(18,2) DEFAULT 0.00 COMMENT '店内支出' AFTER F_CooperationCost;
  164 +
  165 +-- 洗毛巾费用
  166 +ALTER TABLE lq_director_salary_statistics
  167 +ADD COLUMN F_LaundryCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '洗毛巾费用' AFTER F_StoreExpense;
  168 +
  169 +-- 毛利
  170 +ALTER TABLE lq_director_salary_statistics
  171 +ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT '毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)' AFTER F_LaundryCost;
  172 +```
  173 +
  174 +#### 字段说明调整
  175 +
  176 +**F_StoreTotalPerformance** 字段的注释需要修改:
  177 +- **修改前**:门店总业绩(门店开单业绩-门店退卡业绩)
  178 +- **修改后**:门店总业绩(毛利,用于提成计算)
  179 +
  180 +**注意**:`F_StoreTotalPerformance` 字段的值应该存储**毛利**,而不是开单-退卡。
  181 +
  182 +---
  183 +
  184 +### 2. 代码逻辑调整
  185 +
  186 +#### 计算流程
  187 +
  188 +1. **计算销售业绩**(开单-退卡)
  189 + ```csharp
  190 + salary.SalesPerformance = billing - refund;
  191 + ```
  192 +
  193 +2. **统计产品物料**(注意11月特殊规则)
  194 + ```csharp
  195 + var queryMonth = monthStr;
  196 + if (month == 11)
  197 + {
  198 + queryMonth = $"{year}10"; // 11月工资算10月数据
  199 + }
  200 + salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
  201 + ```
  202 +
  203 +3. **统计合作项目成本**
  204 + ```csharp
  205 + salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
  206 + ```
  207 +
  208 +4. **统计店内支出**
  209 + ```csharp
  210 + salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
  211 + ```
  212 +
  213 +5. **统计洗毛巾费用**(只统计送出的记录)
  214 + ```csharp
  215 + salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
  216 + ```
  217 +
  218 +6. **计算毛利**
  219 + ```csharp
  220 + salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
  221 + ```
  222 +
  223 +7. **将毛利赋值给 F_StoreTotalPerformance**(用于提成计算)
  224 + ```csharp
  225 + salary.StoreTotalPerformance = salary.GrossProfit;
  226 + ```
  227 +
  228 +8. **计算业绩完成率**(基于毛利与生命线比较)
  229 + ```csharp
  230 + if (salary.StoreLifeline > 0)
  231 + {
  232 + salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline;
  233 + }
  234 + ```
  235 +
  236 +9. **判断业绩是否达标**(基于毛利)
  237 + ```csharp
  238 + bool performanceReached = salary.GrossProfit >= salary.StoreLifeline;
  239 + ```
  240 +
  241 +10. **计算提成**(基于毛利,使用阶梯提成)
  242 + ```csharp
  243 + CalculateCommission(salary, isNewStore); // 提成计算基于毛利
  244 + ```
  245 +
  246 +---
  247 +
  248 +### 3. 提成计算调整
  249 +
  250 +**重要说明**:提成计算需要基于**毛利**,而不是销售业绩。
  251 +
  252 +当前提成计算逻辑(阶梯提成):
  253 +- ≤生命线部分:根据门店分类使用不同比例(A类2%,B类2.5%,C类3%)
  254 +- >生命线部分:根据门店分类使用不同比例(A类2.5%,B类3%,C类3.5%)
  255 +
  256 +**修改后**:
  257 +- 提成计算基于**毛利**(`salary.GrossProfit`)
  258 +- 业绩完成率判断基于**毛利**与生命线比较
  259 +- 业绩达标判断基于**毛利**是否≥生命线
  260 +
  261 +---
  262 +
  263 +## 📝 总结
  264 +
  265 +### 关键变更点
  266 +
  267 +1. **F_StoreTotalPerformance 字段含义变更**:
  268 + - 原来:开单业绩 - 退卡业绩
  269 + - 现在:毛利(销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾)
  270 +
  271 +2. **需要新增字段**:
  272 + - `F_SalesPerformance`:销售业绩(开单-退卡)
  273 + - `F_ProductMaterial`:产品物料
  274 + - `F_CooperationCost`:合作项目成本
  275 + - `F_StoreExpense`:店内支出
  276 + - `F_LaundryCost`:洗毛巾费用
  277 + - `F_GrossProfit`:毛利
  278 +
  279 +3. **计算逻辑调整**:
  280 + - 先计算销售业绩(开单-退卡)
  281 + - 统计各项成本(产品物料、合作项目成本、店内支出、洗毛巾)
  282 + - 计算毛利
  283 + - 将毛利赋值给 `F_StoreTotalPerformance`(用于提成计算)
  284 + - 基于毛利计算业绩完成率和判断业绩是否达标
  285 + - 基于毛利计算提成
  286 +
  287 +4. **数据查询**:
  288 + - 需要查询 `lq_inventory_usage`(产品物料)
  289 + - 需要查询 `lq_cooperation_cost`(合作项目成本)
  290 + - 需要查询 `lq_store_expense`(店内支出)
  291 + - 需要查询 `lq_laundry_flow`(洗毛巾费用)
  292 +
  293 +---
  294 +
  295 +## ⚠️ 注意事项
  296 +
  297 +1. **11月特殊规则**:核算11月工资时,产品物料算的是10月份的数据
  298 +2. **洗毛巾费用**:只统计送出的记录(`F_FlowType = 0`)
  299 +3. **业绩完成率**:基于毛利与生命线比较,不是基于销售业绩
  300 +4. **业绩达标判断**:基于毛利是否≥生命线
  301 +5. **提成计算**:基于毛利,不是基于销售业绩
  302 +
... ...
大项目主管工资计算规则梳理.md 0 → 100644
  1 +# 大项目主管工资计算规则梳理
  2 +
  3 +## 📋 目录
  4 +- [概述](#概述)
  5 +- [计算规则](#计算规则)
  6 +- [数据来源](#数据来源)
  7 +- [归属规则](#归属规则)
  8 +- [计算流程](#计算流程)
  9 +- [注意事项](#注意事项)
  10 +
  11 +---
  12 +
  13 +## 📋 概述
  14 +
  15 +大项目主管工资由以下几个部分组成:
  16 +1. **底薪**:固定3500元
  17 +2. **业绩提成**:根据管理的所有门店的总业绩分段提成
  18 +
  19 +**重要说明**:
  20 +- 底薪固定为3500元,不设档位,不设条件
  21 +- 大项目主管从 `BASE_USER` 表获取,岗位字段(`F_GW`)为"主管",组织ID为大项目一部或大项目二部
  22 +- 每个大项目主管管理的门店归属在 `lq_md_target` 表中(通过 `F_MajorProjectDepartment` 字段)
  23 +- 需要统计该大项目主管管理的**所有门店**的总业绩(开单-退卡)
  24 +- 提成采用分段方式计算(不是分段累进)
  25 +
  26 +---
  27 +
  28 +## 💰 计算规则
  29 +
  30 +### 1. 底薪规则
  31 +
  32 +**固定底薪**:3500元
  33 +
  34 +- 无论业绩多少,底薪固定为3500元
  35 +- 不设档位,不设条件
  36 +- 不设考核扣款
  37 +
  38 +---
  39 +
  40 +### 2. 业绩提成规则
  41 +
  42 +**提成计算方式**:根据管理的所有门店的总业绩分段计算
  43 +
  44 +| 总业绩范围 | 提成比例 | 说明 |
  45 +|-----------|---------|------|
  46 +| ≤ 50万 | 0% | 无提成 |
  47 +| > 50万 且 ≤ 70万 | 1% | 按1%计算提成 |
  48 +| > 70万 | 1.5% | 按1.5%计算提成 |
  49 +
  50 +**计算说明**:
  51 +- 提成金额 = 总业绩 × 对应提成比例
  52 +- 采用分段方式计算,不同区间按不同比例计算
  53 +- **注意**:不是分段累进,而是整个总业绩按对应比例计算
  54 +
  55 +**示例**:
  56 +- 总业绩 = 40万 → 提成 = 0(无提成)
  57 +- 总业绩 = 60万 → 提成 = 60万 × 1% = 6000元
  58 +- 总业绩 = 80万 → 提成 = 80万 × 1.5% = 12000元
  59 +
  60 +---
  61 +
  62 +## 📊 数据来源
  63 +
  64 +### 大项目主管识别
  65 +
  66 +**数据来源**:`BASE_USER` 表
  67 +
  68 +**识别条件**:
  69 +- `F_GW`(岗位字段)为"主管"
  70 +- `F_OrganizeId`(组织ID)在大项目一部或大项目二部的组织ID列表中
  71 +- `F_DeleteMark == null`(未删除)
  72 +- `F_EnabledMark == 1`(已启用)
  73 +
  74 +**获取步骤**:
  75 +1. 从 `BASE_ORGANIZE` 表查找组织名称包含"大项目一部"或"大项目二部"的组织
  76 +2. 获取这些组织的ID列表
  77 +3. 从 `BASE_USER` 表查询岗位为"主管"且组织ID在上述组织ID列表中的员工
  78 +
  79 +**说明**:
  80 +- 目前有"大项目一部"和"大项目二部",未来可能还有更多大项目部
  81 +- 岗位字段值必须是"主管"(完全匹配)
  82 +
  83 +---
  84 +
  85 +### 管理的门店归属
  86 +
  87 +**数据来源**:`lq_md_target` 表
  88 +
  89 +**关联关系**:
  90 +- 通过 `F_MajorProjectDepartment` 字段关联到 `BASE_ORGANIZE.F_Id`(大项目一部或大项目二部的组织ID)
  91 +- 通过 `F_StoreId` 字段关联到 `lq_mdxx.F_Id`
  92 +- 通过 `F_Month` 字段(YYYYMM格式)关联到统计月份
  93 +
  94 +**获取逻辑**:
  95 +1. 从 `lq_md_target` 表查询指定月份(`F_Month = @统计月份`)的记录
  96 +2. 筛选出 `F_MajorProjectDepartment = @大项目一部组织ID` 或 `F_MajorProjectDepartment = @大项目二部组织ID` 的记录
  97 +3. 获取这些记录的 `F_StoreId` 列表,即为该大项目主管管理的门店
  98 +
  99 +**重要说明**:
  100 +- 每个大项目主管可能管理多个门店
  101 +- 需要统计这些门店的总业绩(开单-退卡)总和
  102 +- 如果某个门店在 `lq_md_target` 表中没有记录,则该门店的业绩不计入该大项目主管的统计
  103 +
  104 +---
  105 +
  106 +### 总业绩统计
  107 +
  108 +**定义**:该大项目主管管理的所有门店中,开单金额总和减去退卡金额总和
  109 +
  110 +**数据来源表及字段**:
  111 +
  112 +| 业绩类型 | 数据表 | 字段 | 说明 |
  113 +|---------|--------|------|------|
  114 +| **开单金额** | `lq_kd_kdjlb` | `sfyj` | 门店开单实付金额 |
  115 +| **退卡金额** | `lq_hytk_hytk` | `F_ActualRefundAmount` 或 `tkje` | 门店退卡金额 |
  116 +
  117 +**统计逻辑**:
  118 +
  119 +1. **统计开单金额**(按管理的门店筛选):
  120 + ```sql
  121 + SELECT COALESCE(SUM(billing.sfyj), 0) as BillingAmount
  122 + FROM lq_kd_kdjlb billing
  123 + WHERE billing.F_IsEffective = 1
  124 + AND billing.djmd IN (@管理的门店ID列表)
  125 + AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
  126 + ```
  127 +
  128 +2. **统计退卡金额**(按管理的门店筛选):
  129 + ```sql
  130 + SELECT COALESCE(SUM(refund.F_ActualRefundAmount), 0) as RefundAmount
  131 + FROM lq_hytk_hytk refund
  132 + WHERE refund.F_IsEffective = 1
  133 + AND refund.djmd IN (@管理的门店ID列表)
  134 + AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
  135 + ```
  136 +
  137 +3. **计算净总业绩**:
  138 + - 净总业绩 = 开单金额 - 退卡金额
  139 +
  140 +---
  141 +
  142 +## 🔗 归属规则
  143 +
  144 +### 大项目主管与门店的归属关系
  145 +
  146 +**数据来源**:`lq_md_target` 表
  147 +
  148 +**表结构说明**:
  149 +- `F_Id`:主键ID
  150 +- `F_StoreId`:门店ID(关联 `lq_mdxx.F_Id`)
  151 +- `F_Month`:月份(YYYYMM格式)
  152 +- `F_MajorProjectDepartment`:归属大项目部(关联 `BASE_ORGANIZE.F_Id`)
  153 +
  154 +**获取管理的门店**:
  155 +1. 从 `lq_md_target` 表查询:
  156 + ```sql
  157 + SELECT DISTINCT F_StoreId
  158 + FROM lq_md_target
  159 + WHERE F_MajorProjectDepartment = @大项目一部或大项目二部组织ID
  160 + AND F_Month = @统计月份
  161 + ```
  162 +
  163 +2. 如果该大项目主管在指定月份没有管理的门店,则总业绩为0,提成为0
  164 +
  165 +**重要说明**:
  166 +- 每个大项目主管可能管理多个门店
  167 +- 需要统计这些门店的总业绩(开单-退卡)总和
  168 +- 如果某个门店在 `lq_md_target` 表中没有记录,则该门店的业绩不计入该大项目主管的统计
  169 +
  170 +---
  171 +
  172 +## 🔄 计算流程
  173 +
  174 +### 步骤1:识别大项目主管
  175 +
  176 +1. 从 `BASE_ORGANIZE` 表中筛选:
  177 + - `F_FullName LIKE '%大项目一部%'` 或 `F_FullName LIKE '%大项目二部%'`
  178 + - `F_DeleteMark == null`(未删除)
  179 + - `F_EnabledMark == 1`(已启用)
  180 +
  181 +2. 获取这些组织的ID列表
  182 +
  183 +3. 从 `BASE_USER` 表中筛选:
  184 + - `F_GW == "主管"`(岗位为"主管")
  185 + - `F_OrganizeId` 在上述组织ID列表中
  186 + - `F_DeleteMark == null`(未删除)
  187 + - `F_EnabledMark == 1`(已启用)
  188 +
  189 +---
  190 +
  191 +### 步骤2:获取管理的门店
  192 +
  193 +1. 从 `lq_md_target` 表查询指定月份(`F_Month = @统计月份`)的记录
  194 +2. 筛选出 `F_MajorProjectDepartment = @大项目一部组织ID` 或 `F_MajorProjectDepartment = @大项目二部组织ID` 的记录
  195 +3. 获取这些记录的 `F_StoreId` 列表,即为该大项目主管管理的门店
  196 +
  197 +---
  198 +
  199 +### 步骤3:统计总业绩
  200 +
  201 +1. **统计开单金额**:
  202 + - 从 `lq_kd_kdjlb` 表统计管理的门店在统计月份的开单金额(`sfyj`字段)
  203 + - 过滤条件:`F_IsEffective = 1`,`djmd IN (@管理的门店ID列表)`,`DATE_FORMAT(kdrq, '%Y%m') = @统计月份`
  204 +
  205 +2. **统计退卡金额**:
  206 + - 从 `lq_hytk_hytk` 表统计管理的门店在统计月份的退卡金额(`F_ActualRefundAmount` 或 `tkje`字段)
  207 + - 过滤条件:`F_IsEffective = 1`,`djmd IN (@管理的门店ID列表)`,`DATE_FORMAT(tkrq, '%Y%m') = @统计月份`
  208 +
  209 +3. **计算净总业绩**:
  210 + - 净总业绩 = 开单金额 - 退卡金额
  211 +
  212 +---
  213 +
  214 +### 步骤4:计算提成
  215 +
  216 +根据总业绩分段计算提成:
  217 +
  218 +- 如果总业绩 ≤ 50万:提成 = 0(无提成)
  219 +- 如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1%
  220 +- 如果总业绩 > 70万:提成 = 总业绩 × 1.5%
  221 +
  222 +---
  223 +
  224 +### 步骤5:计算应发工资
  225 +
  226 +- 应发工资 = 底薪(3500元)+ 提成金额
  227 +
  228 +---
  229 +
  230 +### 步骤6:保存数据
  231 +
  232 +将计算结果保存到 `lq_major_project_director_salary_statistics` 表中:
  233 +- 如果已存在当月数据,则更新;否则插入新数据
  234 +- 保存门店明细(JSON格式)
  235 +
  236 +---
  237 +
  238 +## ✅ 总结
  239 +
  240 +大项目主管工资计算规则相对简单明确:
  241 +1. **底薪固定**:3500元,无任何条件
  242 +2. **业绩提成**:根据管理的所有门店的总业绩分段计算,最高1.5%,低于50万无提成
  243 +
  244 +关键点:
  245 +- 必须从 `BASE_USER` 表识别大项目主管(岗位为"主管",组织ID为大项目一部或大项目二部)
  246 +- 必须从 `lq_md_target` 表获取管理的门店(通过 `F_MajorProjectDepartment` 字段)
  247 +- 必须正确统计总业绩(开单金额 - 退卡金额)
  248 +- 必须按管理的门店筛选,只统计该大项目主管管理的门店
  249 +- 采用分段方式计算提成(不是分段累进),整个总业绩按对应比例计算
  250 +
... ...
库存平均单价计算逻辑说明.md 0 → 100644
  1 +# 库存平均单价计算逻辑说明
  2 +
  3 +## 概述
  4 +
  5 +采用**加权平均成本法(Weighted Average Cost Method)**来计算库存的平均单价,用于出库计价。
  6 +
  7 +## 核心原则
  8 +
  9 +1. **入库时**:根据当前平均单价和入库单价,计算新的平均单价
  10 +2. **出库时**:使用当前平均单价,出库后平均单价不变
  11 +3. **平均单价维护**:存储在 `lq_product.F_AveragePrice` 字段中
  12 +
  13 +## 计算公式
  14 +
  15 +### 入库时计算新平均单价
  16 +
  17 +```
  18 +新平均单价 = (当前平均单价 × 当前可用库存数量 + 入库单价 × 入库数量) / (当前可用库存数量 + 入库数量)
  19 +```
  20 +
  21 +其中:
  22 +- **当前可用库存数量** = 总库存数量 - 已领取数量
  23 +- **入库单价**:
  24 + - 采购入库:使用采购单价(`F_PurchaseUnitPrice`)
  25 + - 普通入库:使用产品价格(`F_Price`)
  26 +
  27 +### 出库时使用平均单价
  28 +
  29 +```
  30 +出库单价 = 产品平均单价(F_AveragePrice)
  31 +出库总金额 = 出库单价 × 出库数量
  32 +```
  33 +
  34 +**注意**:出库后,平均单价不变(因为已经领取的库存已经按照当时的平均单价计价了)
  35 +
  36 +## 示例说明
  37 +
  38 +### 示例1:基本流程
  39 +
  40 +**初始状态**:
  41 +- 商品A:当前平均单价 = 150
  42 +- 库存A:数量1,单价100
  43 +- 库存B:数量1,单价200
  44 +
  45 +**步骤1:领取1个**
  46 +- 使用平均单价:150
  47 +- 领取后剩余:1个库存
  48 +- 平均单价不变:150
  49 +
  50 +**步骤2:再次入库1个,单价50**
  51 +- 当前可用库存数量:1
  52 +- 当前库存总金额:150 × 1 = 150
  53 +- 入库金额:50 × 1 = 50
  54 +- **新平均单价** = (150 + 50) / (1 + 1) = **100**
  55 +
  56 +### 示例2:多次入库
  57 +
  58 +**初始状态**:
  59 +- 商品B:当前平均单价 = 100,当前可用库存 = 10
  60 +
  61 +**步骤1:采购入库5个,单价120**
  62 +- 当前库存总金额:100 × 10 = 1000
  63 +- 入库金额:120 × 5 = 600
  64 +- **新平均单价** = (1000 + 600) / (10 + 5) = **106.67**
  65 +
  66 +**步骤2:领取3个**
  67 +- 使用平均单价:106.67
  68 +- 领取后剩余:12个库存
  69 +- 平均单价不变:106.67
  70 +
  71 +**步骤3:再次采购入库8个,单价110**
  72 +- 当前可用库存数量:12
  73 +- 当前库存总金额:106.67 × 12 = 1280.04
  74 +- 入库金额:110 × 8 = 880
  75 +- **新平均单价** = (1280.04 + 880) / (12 + 8) = **108.00**
  76 +
  77 +## 数据库字段
  78 +
  79 +### 产品表(lq_product)
  80 +
  81 +- `F_AveragePrice`:平均单价(加权平均成本,用于出库计价)
  82 +
  83 +### 库存表(lq_inventory)
  84 +
  85 +- `F_PurchaseUnitPrice`:采购单价(采购入库时使用)
  86 +- `F_FinalAmount`:产品最终金额(优先用于计算单价)
  87 +- `F_Quantity`:库存数量
  88 +
  89 +### 库存使用记录表(lq_inventory_usage)
  90 +
  91 +- `F_UnitPrice`:使用时的单价(从产品平均单价获取)
  92 +- `F_TotalAmount`:使用总金额(单价 × 数量)
  93 +
  94 +## 实现逻辑
  95 +
  96 +### 1. 入库时(LqInventoryService.CreateAsync / UpdateAsync)
  97 +
  98 +```csharp
  99 +// 计算当前可用库存数量
  100 +var currentAvailableQuantity = totalInventoryQuantity - totalUsageQuantity;
  101 +
  102 +// 确定入库单价
  103 +decimal incomingPrice = stockInType == 2 && purchaseUnitPrice.HasValue
  104 + ? purchaseUnitPrice.Value // 采购入库
  105 + : product.Price; // 普通入库
  106 +
  107 +// 计算新平均单价
  108 +if (currentAvailableQuantity <= 0)
  109 +{
  110 + newAveragePrice = incomingPrice; // 没有库存时,直接使用入库单价
  111 +}
  112 +else
  113 +{
  114 + var currentTotalAmount = product.AveragePrice * currentAvailableQuantity;
  115 + var incomingAmount = incomingPrice * incomingQuantity;
  116 + newAveragePrice = (currentTotalAmount + incomingAmount) / (currentAvailableQuantity + incomingQuantity);
  117 +}
  118 +
  119 +// 更新产品平均单价
  120 +product.AveragePrice = newAveragePrice;
  121 +```
  122 +
  123 +### 2. 出库时(LqInventoryUsageService.CreateAsync / ConfirmReceiveAsync)
  124 +
  125 +```csharp
  126 +// 使用产品的平均单价
  127 +var unitPrice = product.AveragePrice > 0 ? product.AveragePrice : product.Price;
  128 +var totalAmount = unitPrice * usageQuantity;
  129 +
  130 +// 创建使用记录(平均单价不变)
  131 +usageEntity.UnitPrice = unitPrice;
  132 +usageEntity.TotalAmount = totalAmount;
  133 +```
  134 +
  135 +## 初始化历史数据
  136 +
  137 +对于已有库存的产品,根据当前库存计算初始平均单价:
  138 +
  139 +```sql
  140 +平均单价 = SUM(库存金额) / SUM(库存数量)
  141 +```
  142 +
  143 +其中,库存金额优先使用 `F_FinalAmount`,其次使用 `F_PurchaseUnitPrice × F_Quantity`。
  144 +
  145 +## 注意事项
  146 +
  147 +1. **平均单价维护**:每次入库时自动更新,出库时不变
  148 +2. **可用库存计算**:需要考虑已领取的数量
  149 +3. **普通入库**:如果没有采购单价,使用产品价格作为入库单价
  150 +4. **历史数据初始化**:首次添加字段时,需要根据现有库存计算初始平均单价
  151 +5. **数据一致性**:确保入库和出库操作在事务中执行,保证平均单价的一致性
... ...
科技部总经理工资计算规则梳理.md 0 → 100644
  1 +# 科技部总经理工资计算规则梳理
  2 +
  3 +## 📋 目录
  4 +- [概述](#概述)
  5 +- [计算规则](#计算规则)
  6 +- [数据来源](#数据来源)
  7 +- [归属规则](#归属规则)
  8 +- [计算流程](#计算流程)
  9 +- [注意事项](#注意事项)
  10 +
  11 +---
  12 +
  13 +## 📋 概述
  14 +
  15 +科技部总经理工资由以下几个部分组成:
  16 +1. **底薪**:固定4000元
  17 +2. **溯源金额提成**:根据管理的所有门店的溯源金额总和分段提成
  18 +3. **Cell金额提成**:根据管理的所有门店的Cell金额总和分段提成
  19 +
  20 +**重要说明**:
  21 +- 底薪固定为4000元,不设档位,不设条件
  22 +- 科技部总经理从 `BASE_USER` 表获取,岗位字段(`F_GW`)为"科技一部"、"科技二部"等(未来可能还有更多)
  23 +- 每个科技部总经理管理的门店归属在 `lq_md_general_manager_lifeline` 表中
  24 +- 需要统计该科技部总经理管理的**所有门店**的溯源金额和Cell金额总和
  25 +- 溯源金额和Cell金额分别计算提成,互不影响
  26 +- 提成采用分段累进方式计算
  27 +
  28 +---
  29 +
  30 +## 💰 计算规则
  31 +
  32 +### 1. 底薪规则
  33 +
  34 +**固定底薪**:4000元
  35 +
  36 +- 无论业绩多少,底薪固定为4000元
  37 +- 不设档位,不设条件
  38 +- 不设考核扣款
  39 +
  40 +---
  41 +
  42 +### 2. 溯源金额提成规则
  43 +
  44 +**提成计算方式**:根据管理的所有门店的溯源金额总和分段累进计算
  45 +
  46 +| 溯源金额范围 | 提成比例 |
  47 +|------------|---------|
  48 +| < 200,000元 | 1% |
  49 +| 200,000元 - 300,000元 | 1.5% |
  50 +| 300,000元 - 500,000元 | 2% |
  51 +| ≥ 500,000元 | 2.5% |
  52 +
  53 +**计算说明**:
  54 +- 提成金额 = 溯源金额 × 对应提成比例
  55 +- 采用分段累进方式计算,不同区间按不同比例计算
  56 +- 例如:溯源金额为350,000元
  57 + - 0-200,000元部分:200,000 × 1% = 2,000元
  58 + - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
  59 + - 300,000-350,000元部分:50,000 × 2% = 1,000元
  60 + - 总提成 = 2,000 + 1,500 + 1,000 = 4,500元
  61 +
  62 +---
  63 +
  64 +### 3. Cell金额提成规则
  65 +
  66 +**提成计算方式**:根据管理的所有门店的Cell金额总和分段累进计算
  67 +
  68 +| Cell金额范围 | 提成比例 |
  69 +|------------|---------|
  70 +| < 50,000元 | 0% (无提成) |
  71 +| 50,000元 - 400,000元 | 1% |
  72 +| ≥ 400,000元 | 1.5% |
  73 +
  74 +**计算说明**:
  75 +- 如果Cell金额 < 50,000元,无提成
  76 +- 如果Cell金额 ≥ 50,000元,按分段累进方式计算
  77 +- 例如:Cell金额为450,000元
  78 + - 0-50,000元部分:无提成
  79 + - 50,000-400,000元部分:350,000 × 1% = 3,500元
  80 + - 400,000-450,000元部分:50,000 × 1.5% = 750元
  81 + - 总提成 = 3,500 + 750 = 4,250元
  82 +
  83 +---
  84 +
  85 +## 📊 数据来源
  86 +
  87 +### 科技部总经理识别
  88 +
  89 +**数据来源**:`BASE_USER` 表
  90 +
  91 +**识别条件**:
  92 +- `F_GW`(岗位字段)包含"科技一部"、"科技二部"等(如:`F_GW LIKE '%科技一部%'` 或 `F_GW LIKE '%科技二部%'`)
  93 +- `F_DeleteMark == null`(未删除)
  94 +- `F_EnabledMark == 1`(已启用)
  95 +
  96 +**说明**:
  97 +- 目前有"科技一部"和"科技二部",未来可能还有更多科技部
  98 +- 岗位字段值可能是"科技一部"、"科技二部"等完整名称
  99 +
  100 +---
  101 +
  102 +### 管理的门店归属
  103 +
  104 +**数据来源**:`lq_md_general_manager_lifeline` 表
  105 +
  106 +**关联关系**:
  107 +- 通过 `F_GeneralManagerId` 字段关联到 `BASE_USER.F_Id`
  108 +- 通过 `F_StoreId` 字段关联到 `lq_mdxx.F_Id`
  109 +- 通过 `F_Month` 字段(YYYYMM格式)关联到统计月份
  110 +
  111 +**获取逻辑**:
  112 +1. 从 `lq_md_general_manager_lifeline` 表查询指定月份(`F_Month = @统计月份`)的记录
  113 +2. 筛选出 `F_GeneralManagerId = @科技部总经理ID` 的记录
  114 +3. 获取这些记录的 `F_StoreId` 列表,即为该科技部总经理管理的门店
  115 +
  116 +**重要说明**:
  117 +- 每个科技部总经理可能管理多个门店
  118 +- 需要统计这些门店的溯源金额和Cell金额总和
  119 +
  120 +---
  121 +
  122 +### 溯源金额统计
  123 +
  124 +**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "溯源系统" 或 "溯源" 的品项明细的实付金额总和(开单金额 - 退卡金额)
  125 +
  126 +**数据来源表及字段**:
  127 +
  128 +| 数据表 | 字段 | 说明 |
  129 +|--------|------|------|
  130 +| `lq_kd_pxmx` | `F_ActualPrice` | 品项明细实付金额 |
  131 +| `lq_kd_pxmx` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) |
  132 +| `lq_kd_kdjlb` | `kdrq` | 开单日期(用于按月统计) |
  133 +| `lq_kd_kdjlb` | `djmd` | 单据门店ID(用于筛选管理的门店) |
  134 +| `lq_xmzl` | `F_BeautyType` | 品项的科美类型(如果明细表没有,从品项表获取) |
  135 +| `lq_hytk_mx` | `tkje` | 退卡明细退款金额 |
  136 +| `lq_hytk_hytk` | `tkrq` | 退卡日期(用于按月统计) |
  137 +| `lq_hytk_hytk` | `djmd` | 单据门店ID(用于筛选管理的门店) |
  138 +
  139 +**统计逻辑**:
  140 +
  141 +1. **统计开单溯源金额**(按管理的门店筛选):
  142 + ```sql
  143 + SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as TraceabilityAmount
  144 + FROM lq_kd_pxmx pxmx
  145 + INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  146 + INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  147 + WHERE pxmx.F_IsEffective = 1
  148 + AND billing.F_IsEffective = 1
  149 + AND item.F_IsEffective = 1
  150 + AND (pxmx.F_BeautyType = '溯源系统' OR pxmx.F_BeautyType = '溯源'
  151 + OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源')
  152 + AND billing.djmd IN (@管理的门店ID列表)
  153 + AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
  154 + ```
  155 +
  156 +2. **统计退卡溯源金额**(按管理的门店筛选):
  157 + ```sql
  158 + SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundTraceabilityAmount
  159 + FROM lq_hytk_mx tkmx
  160 + INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id
  161 + INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id
  162 + WHERE tkmx.F_IsEffective = 1
  163 + AND refund.F_IsEffective = 1
  164 + AND item.F_IsEffective = 1
  165 + AND (tkmx.F_BeautyType = '溯源系统' OR tkmx.F_BeautyType = '溯源'
  166 + OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源')
  167 + AND refund.djmd IN (@管理的门店ID列表)
  168 + AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
  169 + ```
  170 +
  171 +3. **计算净溯源金额**:
  172 + - 净溯源金额 = 开单溯源金额 - 退卡溯源金额
  173 +
  174 +---
  175 +
  176 +### Cell金额统计
  177 +
  178 +**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "cell" 或 "Cell" 的品项明细的实付金额总和(开单金额 - 退卡金额)
  179 +
  180 +**数据来源表及字段**:同溯源金额统计
  181 +
  182 +**统计逻辑**:
  183 +
  184 +1. **统计开单Cell金额**(按管理的门店筛选):
  185 + ```sql
  186 + SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as CellAmount
  187 + FROM lq_kd_pxmx pxmx
  188 + INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
  189 + INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
  190 + WHERE pxmx.F_IsEffective = 1
  191 + AND billing.F_IsEffective = 1
  192 + AND item.F_IsEffective = 1
  193 + AND (pxmx.F_BeautyType = 'cell' OR pxmx.F_BeautyType = 'Cell'
  194 + OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell')
  195 + AND billing.djmd IN (@管理的门店ID列表)
  196 + AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
  197 + ```
  198 +
  199 +2. **统计退卡Cell金额**(按管理的门店筛选):
  200 + ```sql
  201 + SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundCellAmount
  202 + FROM lq_hytk_mx tkmx
  203 + INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id
  204 + INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id
  205 + WHERE tkmx.F_IsEffective = 1
  206 + AND refund.F_IsEffective = 1
  207 + AND item.F_IsEffective = 1
  208 + AND (tkmx.F_BeautyType = 'cell' OR tkmx.F_BeautyType = 'Cell'
  209 + OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell')
  210 + AND refund.djmd IN (@管理的门店ID列表)
  211 + AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
  212 + ```
  213 +
  214 +3. **计算净Cell金额**:
  215 + - 净Cell金额 = 开单Cell金额 - 退卡Cell金额
  216 +
  217 +---
  218 +
  219 +## 🔗 归属规则
  220 +
  221 +### 科技部总经理与门店的归属关系
  222 +
  223 +**数据来源**:`lq_md_general_manager_lifeline` 表
  224 +
  225 +**表结构说明**:
  226 +- `F_Id`:主键ID
  227 +- `F_StoreId`:门店ID(关联 `lq_mdxx.F_Id`)
  228 +- `F_Month`:月份(YYYYMM格式)
  229 +- `F_GeneralManagerId`:总经理用户ID(关联 `BASE_USER.F_Id`)
  230 +- `F_ManagerType`:经理类型(0=经理,1=总经理)
  231 +
  232 +**获取管理的门店**:
  233 +1. 从 `lq_md_general_manager_lifeline` 表查询:
  234 + ```sql
  235 + SELECT DISTINCT F_StoreId
  236 + FROM lq_md_general_manager_lifeline
  237 + WHERE F_GeneralManagerId = @科技部总经理ID
  238 + AND F_Month = @统计月份
  239 + ```
  240 +
  241 +2. 如果该科技部总经理在指定月份没有管理的门店,则溯源金额和Cell金额为0,提成为0
  242 +
  243 +**重要说明**:
  244 +- 每个科技部总经理可能管理多个门店
  245 +- 需要统计这些门店的溯源金额和Cell金额总和
  246 +- 如果某个门店在 `lq_md_general_manager_lifeline` 表中没有记录,则该门店的业绩不计入该科技部总经理的统计
  247 +
  248 +---
  249 +
  250 +## 🔄 计算流程
  251 +
  252 +### 步骤1:识别科技部总经理
  253 +
  254 +从 `BASE_USER` 表中筛选:
  255 +- `F_GW LIKE '%科技一部%'` 或 `F_GW LIKE '%科技二部%'` 等(根据实际岗位名称)
  256 +- `F_DeleteMark == null`(未删除)
  257 +- `F_EnabledMark == 1`(已启用)
  258 +
  259 +**注意**:未来可能还有"科技三部"等,需要灵活处理岗位识别逻辑
  260 +
  261 +---
  262 +
  263 +### 步骤2:获取管理的门店
  264 +
  265 +1. 从 `lq_md_general_manager_lifeline` 表查询该科技部总经理在指定月份管理的门店:
  266 + ```sql
  267 + SELECT DISTINCT F_StoreId
  268 + FROM lq_md_general_manager_lifeline
  269 + WHERE F_GeneralManagerId = @科技部总经理ID
  270 + AND F_Month = @统计月份
  271 + ```
  272 +
  273 +2. 如果查询结果为空,说明该科技部总经理在该月份没有管理的门店,溯源金额和Cell金额为0,提成为0
  274 +
  275 +---
  276 +
  277 +### 步骤3:统计溯源金额(所有管理的门店总和)
  278 +
  279 +1. **统计开单溯源金额**(按管理的门店筛选):
  280 + - 从 `lq_kd_pxmx` 表统计
  281 + - 关联 `lq_kd_kdjlb` 表获取开单日期和门店ID
  282 + - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
  283 + - 筛选条件:
  284 + - `lq_kd_pxmx.F_IsEffective = 1`(有效记录)
  285 + - `lq_kd_kdjlb.F_IsEffective = 1`(有效开单)
  286 + - `lq_xmzl.F_IsEffective = 1`(有效品项)
  287 + - `F_BeautyType = '溯源系统'` 或 `'溯源'`
  288 + - `lq_kd_kdjlb.djmd IN (@管理的门店ID列表)`
  289 + - 开单日期在统计月份范围内
  290 + - 汇总:`SUM(lq_kd_pxmx.F_ActualPrice)`
  291 +
  292 +2. **统计退卡溯源金额**(按管理的门店筛选):
  293 + - 从 `lq_hytk_mx` 表统计
  294 + - 关联 `lq_hytk_hytk` 表获取退卡日期和门店ID
  295 + - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
  296 + - 筛选条件:
  297 + - `lq_hytk_mx.F_IsEffective = 1`(有效记录)
  298 + - `lq_hytk_hytk.F_IsEffective = 1`(有效退卡)
  299 + - `lq_xmzl.F_IsEffective = 1`(有效品项)
  300 + - `F_BeautyType = '溯源系统'` 或 `'溯源'`
  301 + - `lq_hytk_hytk.djmd IN (@管理的门店ID列表)`
  302 + - 退卡日期在统计月份范围内
  303 + - 汇总:`SUM(lq_hytk_mx.tkje)`
  304 +
  305 +3. **计算净溯源金额**:
  306 + - 净溯源金额 = 开单溯源金额 - 退卡溯源金额
  307 +
  308 +---
  309 +
  310 +### 步骤4:统计Cell金额(所有管理的门店总和)
  311 +
  312 +1. **统计开单Cell金额**(按管理的门店筛选):
  313 + - 从 `lq_kd_pxmx` 表统计
  314 + - 关联 `lq_kd_kdjlb` 表获取开单日期和门店ID
  315 + - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
  316 + - 筛选条件:
  317 + - `lq_kd_pxmx.F_IsEffective = 1`(有效记录)
  318 + - `lq_kd_kdjlb.F_IsEffective = 1`(有效开单)
  319 + - `lq_xmzl.F_IsEffective = 1`(有效品项)
  320 + - `F_BeautyType = 'cell'` 或 `'Cell'`
  321 + - `lq_kd_kdjlb.djmd IN (@管理的门店ID列表)`
  322 + - 开单日期在统计月份范围内
  323 + - 汇总:`SUM(lq_kd_pxmx.F_ActualPrice)`
  324 +
  325 +2. **统计退卡Cell金额**(按管理的门店筛选):
  326 + - 从 `lq_hytk_mx` 表统计
  327 + - 关联 `lq_hytk_hytk` 表获取退卡日期和门店ID
  328 + - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
  329 + - 筛选条件:
  330 + - `lq_hytk_mx.F_IsEffective = 1`(有效记录)
  331 + - `lq_hytk_hytk.F_IsEffective = 1`(有效退卡)
  332 + - `lq_xmzl.F_IsEffective = 1`(有效品项)
  333 + - `F_BeautyType = 'cell'` 或 `'Cell'`
  334 + - `lq_hytk_hytk.djmd IN (@管理的门店ID列表)`
  335 + - 退卡日期在统计月份范围内
  336 + - 汇总:`SUM(lq_hytk_mx.tkje)`
  337 +
  338 +3. **计算净Cell金额**:
  339 + - 净Cell金额 = 开单Cell金额 - 退卡Cell金额
  340 +
  341 +---
  342 +
  343 +### 步骤5:工资计算
  344 +
  345 +#### 5.1 计算底薪
  346 +- 底薪 = 4000元(固定)
  347 +
  348 +#### 5.2 计算溯源金额提成
  349 +
  350 +根据净溯源金额范围确定提成比例,采用分段累进方式:
  351 +
  352 +- **如果净溯源金额 < 200,000元**:
  353 + - 提成金额 = 净溯源金额 × 1%
  354 +
  355 +- **如果 200,000元 ≤ 净溯源金额 < 300,000元**:
  356 + - 0-200,000元部分:200,000 × 1% = 2,000元
  357 + - 200,000元以上部分:(净溯源金额 - 200,000) × 1.5%
  358 + - 总提成 = 2,000 + (净溯源金额 - 200,000) × 1.5%
  359 +
  360 +- **如果 300,000元 ≤ 净溯源金额 < 500,000元**:
  361 + - 0-200,000元部分:200,000 × 1% = 2,000元
  362 + - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
  363 + - 300,000元以上部分:(净溯源金额 - 300,000) × 2%
  364 + - 总提成 = 2,000 + 1,500 + (净溯源金额 - 300,000) × 2%
  365 +
  366 +- **如果净溯源金额 ≥ 500,000元**:
  367 + - 0-200,000元部分:200,000 × 1% = 2,000元
  368 + - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
  369 + - 300,000-500,000元部分:200,000 × 2% = 4,000元
  370 + - 500,000元以上部分:(净溯源金额 - 500,000) × 2.5%
  371 + - 总提成 = 2,000 + 1,500 + 4,000 + (净溯源金额 - 500,000) × 2.5%
  372 +
  373 +#### 5.3 计算Cell金额提成
  374 +
  375 +根据净Cell金额范围确定提成比例,采用分段累进方式:
  376 +
  377 +- **如果净Cell金额 < 50,000元**:
  378 + - 提成金额 = 0(无提成)
  379 +
  380 +- **如果 50,000元 ≤ 净Cell金额 < 400,000元**:
  381 + - 0-50,000元部分:无提成
  382 + - 50,000元以上部分:(净Cell金额 - 50,000) × 1%
  383 + - 总提成 = (净Cell金额 - 50,000) × 1%
  384 +
  385 +- **如果净Cell金额 ≥ 400,000元**:
  386 + - 0-50,000元部分:无提成
  387 + - 50,000-400,000元部分:350,000 × 1% = 3,500元
  388 + - 400,000元以上部分:(净Cell金额 - 400,000) × 1.5%
  389 + - 总提成 = 3,500 + (净Cell金额 - 400,000) × 1.5%
  390 +
  391 +#### 5.4 计算最终工资
  392 +
  393 +- **应发工资** = 底薪 + 溯源金额提成 + Cell金额提成
  394 +
  395 +---
  396 +
  397 +## 📝 注意事项
  398 +
  399 +1. **数据一致性**:
  400 + - 溯源金额和Cell金额的统计逻辑必须与其他统计接口保持一致
  401 + - 必须扣除退卡金额,确保数据准确性
  402 + - 必须按管理的门店筛选,只统计该科技部总经理管理的门店
  403 +
  404 +2. **数据校验**:
  405 + - `F_BeautyType` 字段值可能不统一("溯源系统"、"溯源"、"cell"、"Cell"),需要兼容处理
  406 + - 优先使用明细表的 `F_BeautyType`,如果为空则使用品项表的 `F_BeautyType`
  407 + - 如果科技部总经理在指定月份没有管理的门店,溯源金额和Cell金额为0,提成为0
  408 +
  409 +3. **归属关系**:
  410 + - 必须从 `lq_md_general_manager_lifeline` 表获取管理的门店
  411 + - 如果某个门店不在该科技部总经理的管理范围内,该门店的业绩不计入统计
  412 + - 需要确保 `lq_md_general_manager_lifeline` 表的数据准确性
  413 +
  414 +4. **边界情况**:
  415 + - 如果溯源金额或Cell金额为0或负数,对应提成为0
  416 + - 如果溯源金额或Cell金额计算后为负数(退卡金额大于开单金额),对应提成为0
  417 + - 如果科技部总经理在指定月份没有管理的门店,所有金额为0,提成为0
  418 +
  419 +5. **计算精度**:
  420 + - 涉及金额计算时,建议保留2位小数
  421 + - 提成金额四舍五入到分
  422 +
  423 +6. **性能优化**:
  424 + - 建议使用数据库聚合查询,避免在应用层进行大量数据计算
  425 + - 可以先获取管理的门店列表,然后在SQL中使用 `IN` 子句筛选
  426 + - 可以考虑创建统计视图或物化视图,提高查询性能
  427 +
  428 +7. **岗位识别**:
  429 + - 目前有"科技一部"和"科技二部",未来可能还有更多
  430 + - 建议使用模糊匹配(`LIKE '%科技一部%'`)或维护一个岗位列表
  431 + - 需要确保岗位识别逻辑能够适应未来的扩展
  432 +
  433 +---
  434 +
  435 +## 📊 计算示例
  436 +
  437 +### 示例1:基础计算
  438 +
  439 +**假设数据**:
  440 +- 科技部总经理:科技一部(ID: 123456)
  441 +- 管理的门店:门店A(ID: A001)、门店B(ID: B001)
  442 +- 底薪:4000元
  443 +- 门店A溯源金额:150,000元,Cell金额:80,000元
  444 +- 门店B溯源金额:100,000元,Cell金额:120,000元
  445 +- 总溯源金额:250,000元
  446 +- 总Cell金额:200,000元
  447 +
  448 +**计算过程**:
  449 +
  450 +1. **溯源金额提成**:
  451 + - 0-200,000元部分:200,000 × 1% = 2,000元
  452 + - 200,000-250,000元部分:50,000 × 1.5% = 750元
  453 + - 溯源金额提成 = 2,000 + 750 = 2,750元
  454 +
  455 +2. **Cell金额提成**:
  456 + - 0-50,000元部分:无提成
  457 + - 50,000-200,000元部分:150,000 × 1% = 1,500元
  458 + - Cell金额提成 = 1,500元
  459 +
  460 +3. **应发工资**:
  461 + - 应发工资 = 4,000 + 2,750 + 1,500 = 8,250元
  462 +
  463 +---
  464 +
  465 +### 示例2:高业绩计算
  466 +
  467 +**假设数据**:
  468 +- 科技部总经理:科技二部(ID: 789012)
  469 +- 管理的门店:门店C(ID: C001)、门店D(ID: D001)、门店E(ID: E001)
  470 +- 底薪:4000元
  471 +- 门店C溯源金额:200,000元,Cell金额:150,000元
  472 +- 门店D溯源金额:250,000元,Cell金额:200,000元
  473 +- 门店E溯源金额:150,000元,Cell金额:100,000元
  474 +- 总溯源金额:600,000元
  475 +- 总Cell金额:450,000元
  476 +
  477 +**计算过程**:
  478 +
  479 +1. **溯源金额提成**:
  480 + - 0-200,000元部分:200,000 × 1% = 2,000元
  481 + - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
  482 + - 300,000-500,000元部分:200,000 × 2% = 4,000元
  483 + - 500,000-600,000元部分:100,000 × 2.5% = 2,500元
  484 + - 溯源金额提成 = 2,000 + 1,500 + 4,000 + 2,500 = 10,000元
  485 +
  486 +2. **Cell金额提成**:
  487 + - 0-50,000元部分:无提成
  488 + - 50,000-400,000元部分:350,000 × 1% = 3,500元
  489 + - 400,000-450,000元部分:50,000 × 1.5% = 750元
  490 + - Cell金额提成 = 3,500 + 750 = 4,250元
  491 +
  492 +3. **应发工资**:
  493 + - 应发工资 = 4,000 + 10,000 + 4,250 = 18,250元
  494 +
  495 +---
  496 +
  497 +## 🔍 数据表结构参考
  498 +
  499 +### 科技部总经理工资统计表
  500 +
  501 +参考其他岗位的工资表结构(如:`lq_business_unit_manager_salary_statistics`、`lq_tech_teacher_salary_statistics`),建议创建如下表结构:
  502 +
  503 +```sql
  504 +-- ============================================
  505 +-- 创建科技部总经理工资统计表
  506 +-- 功能:存储科技部总经理每月的工资计算数据,包括底薪、溯源金额提成、Cell金额提成、扣款、补贴、奖金、支付等信息
  507 +-- 创建时间:2025年
  508 +-- ============================================
  509 +
  510 +DROP TABLE IF EXISTS lq_tech_general_manager_salary_statistics;
  511 +
  512 +CREATE TABLE lq_tech_general_manager_salary_statistics (
  513 + -- 主键
  514 + F_Id VARCHAR(50) NOT NULL COMMENT '主键ID',
  515 +
  516 + -- 一、基础信息字段
  517 + F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份(YYYYMM格式)',
  518 + F_Position VARCHAR(50) NOT NULL COMMENT '核算岗位(科技一部/科技二部等)',
  519 + F_EmployeeName VARCHAR(100) NOT NULL COMMENT '员工姓名',
  520 + F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID',
  521 + F_EmployeeAccount VARCHAR(100) NULL COMMENT '员工账号',
  522 + F_IsTerminated INT NOT NULL DEFAULT 0 COMMENT '是否离职(0=在职,1=离职)',
  523 +
  524 + -- 二、管理的门店信息(JSON格式)
  525 + F_StoreDetail TEXT NULL COMMENT '管理的门店明细(JSON格式,记录每个门店的溯源金额和Cell金额详情)',
  526 +
  527 + -- 三、业绩相关字段
  528 + F_TraceabilityAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额(管理的所有门店的溯源金额总和,开单-退卡)',
  529 + F_CellAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额(管理的所有门店的Cell金额总和,开单-退卡)',
  530 +
  531 + -- 四、底薪相关字段
  532 + F_BaseSalary DECIMAL(18,2) NOT NULL DEFAULT 4000.00 COMMENT '底薪金额(固定4000元)',
  533 +
  534 + -- 五、提成相关字段
  535 + F_TraceabilityCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT '溯源金额提成比例(分段计算,存储平均比例)',
  536 + F_TraceabilityCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额提成金额',
  537 + F_CellCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT 'Cell金额提成比例(分段计算,存储平均比例)',
  538 + F_CellCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额提成金额',
  539 + F_TotalCommission DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '提成合计(溯源提成+Cell提成)',
  540 +
  541 + -- 六、考勤相关字段
  542 + F_WorkingDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '在店天数',
  543 + F_LeaveDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假天数',
  544 +
  545 + -- 七、工资计算字段
  546 + F_CalculatedGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '核算应发工资(底薪 + 提成合计)',
  547 + F_FinalGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '最终应发工资(等于核算应发工资)',
  548 +
  549 + -- 八、补贴相关字段
  550 + F_MonthlyTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月培训补贴',
  551 + F_MonthlyTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月交通补贴',
  552 + F_LastMonthTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月培训补贴',
  553 + F_LastMonthTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月交通补贴',
  554 + F_TotalSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补贴合计',
  555 +
  556 + -- 九、扣款相关字段
  557 + F_MissingCard DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '缺卡扣款',
  558 + F_LateArrival DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '迟到扣款',
  559 + F_LeaveDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假扣款',
  560 + F_SocialInsuranceDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣社保',
  561 + F_RewardDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣除奖励',
  562 + F_AccommodationDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣住宿费',
  563 + F_StudyPeriodDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣学习期费用',
  564 + F_WorkClothesDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣工作服费用',
  565 + F_TotalDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣款合计',
  566 +
  567 + -- 十、奖金相关字段
  568 + F_Bonus DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '发奖金',
  569 + F_ReturnPhoneDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退手机押金',
  570 + F_ReturnAccommodationDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退住宿押金',
  571 +
  572 + -- 十一、支付相关字段
  573 + F_ActualSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资(最终应发工资 - 扣款合计 + 补贴合计 + 奖金)',
  574 + F_MonthlyPaymentStatus VARCHAR(20) NOT NULL DEFAULT '未发放' COMMENT '当月是否发放(已发放/未发放/部分发放)',
  575 + F_PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '支付金额',
  576 + F_PendingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '待支付金额',
  577 + F_LastMonthSupplement DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补发上月',
  578 + F_MonthlyTotalPayment DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月支付总额',
  579 +
  580 + -- 十二、系统字段
  581 + F_IsLocked INT NOT NULL DEFAULT 0 COMMENT '是否锁定(0=未锁定,1=已锁定)',
  582 + F_CreateTime DATETIME NOT NULL COMMENT '创建时间',
  583 + F_UpdateTime DATETIME NOT NULL COMMENT '更新时间',
  584 + F_CreateUser VARCHAR(50) NULL COMMENT '创建人',
  585 + F_UpdateUser VARCHAR(50) NULL COMMENT '更新人',
  586 +
  587 + -- 主键约束
  588 + PRIMARY KEY (F_Id),
  589 +
  590 + -- 唯一索引:确保同一员工同一月份只有一条记录
  591 + UNIQUE KEY `uk_employee_month` (F_EmployeeId, F_StatisticsMonth),
  592 +
  593 + -- 普通索引
  594 + KEY `idx_statistics_month` (F_StatisticsMonth),
  595 + KEY `idx_employee_id` (F_EmployeeId),
  596 + KEY `idx_employee_account` (F_EmployeeAccount),
  597 + KEY `idx_position` (F_Position),
  598 + KEY `idx_is_terminated` (F_IsTerminated),
  599 + KEY `idx_create_time` (F_CreateTime)
  600 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科技部总经理工资统计表';
  601 +```
  602 +
  603 +**门店明细JSON格式示例**:
  604 +```json
  605 +[
  606 + {
  607 + "storeId": "A001",
  608 + "storeName": "门店A",
  609 + "traceabilityBillingAmount": 160000.00,
  610 + "traceabilityRefundAmount": 10000.00,
  611 + "traceabilityAmount": 150000.00,
  612 + "cellBillingAmount": 85000.00,
  613 + "cellRefundAmount": 5000.00,
  614 + "cellAmount": 80000.00
  615 + },
  616 + {
  617 + "storeId": "B001",
  618 + "storeName": "门店B",
  619 + "traceabilityBillingAmount": 105000.00,
  620 + "traceabilityRefundAmount": 5000.00,
  621 + "traceabilityAmount": 100000.00,
  622 + "cellBillingAmount": 125000.00,
  623 + "cellRefundAmount": 5000.00,
  624 + "cellAmount": 120000.00
  625 + }
  626 +]
  627 +```
  628 +
  629 +---
  630 +
  631 +## ✅ 总结
  632 +
  633 +科技部总经理工资计算规则相对简单明确:
  634 +1. **底薪固定**:4000元,无任何条件
  635 +2. **溯源提成**:根据管理的所有门店的溯源金额总和分段累进,最高2.5%
  636 +3. **Cell提成**:根据管理的所有门店的Cell金额总和分段累进,最高1.5%,低于5万无提成
  637 +
  638 +关键点:
  639 +- 必须从 `BASE_USER` 表识别科技部总经理(岗位为"科技一部"、"科技二部"等)
  640 +- 必须从 `lq_md_general_manager_lifeline` 表获取管理的门店
  641 +- 必须正确区分溯源和Cell类型(通过 `F_BeautyType` 字段)
  642 +- 必须扣除退卡金额,确保数据准确性
  643 +- 必须按管理的门店筛选,只统计该科技部总经理管理的门店
  644 +- 采用分段累进方式计算提成,确保计算准确
... ...