diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryInput.cs
new file mode 100644
index 0000000..97eeb9e
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryInput.cs
@@ -0,0 +1,32 @@
+using NCC.Common.Filter;
+using System;
+
+namespace NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary
+{
+ ///
+ /// 大项目主管工资查询参数
+ ///
+ public class MajorProjectDirectorSalaryInput : PageInputBase
+ {
+ ///
+ /// 年份
+ ///
+ public int Year { get; set; }
+
+ ///
+ /// 月份
+ ///
+ public int Month { get; set; }
+
+ ///
+ /// 岗位(大项目一部/大项目二部等,不传则查询全部)
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 员工姓名/账号(可选,用于模糊搜索)
+ ///
+ public string Keyword { get; set; }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryOutput.cs
new file mode 100644
index 0000000..789e39a
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMajorProjectDirectorSalary/MajorProjectDirectorSalaryOutput.cs
@@ -0,0 +1,226 @@
+using System;
+
+namespace NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary
+{
+ ///
+ /// 大项目主管工资输出
+ ///
+ public class MajorProjectDirectorSalaryOutput
+ {
+ ///
+ /// 主键ID
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// 统计月份
+ ///
+ public string StatisticsMonth { get; set; }
+
+ ///
+ /// 核算岗位(大项目一部/大项目二部等)
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 员工姓名
+ ///
+ public string EmployeeName { get; set; }
+
+ ///
+ /// 员工ID
+ ///
+ public string EmployeeId { get; set; }
+
+ ///
+ /// 员工账号
+ ///
+ public string EmployeeAccount { get; set; }
+
+ ///
+ /// 是否离职
+ ///
+ public int IsTerminated { get; set; }
+
+ ///
+ /// 管理的门店明细(JSON格式)
+ ///
+ public string StoreDetail { get; set; }
+
+ ///
+ /// 总业绩
+ ///
+ public decimal TotalPerformance { get; set; }
+
+ ///
+ /// 开单金额
+ ///
+ public decimal BillingAmount { get; set; }
+
+ ///
+ /// 退卡金额
+ ///
+ public decimal RefundAmount { get; set; }
+
+ ///
+ /// 底薪
+ ///
+ public decimal BaseSalary { get; set; }
+
+ ///
+ /// 提成比例
+ ///
+ public decimal? CommissionRate { get; set; }
+
+ ///
+ /// 提成金额
+ ///
+ public decimal CommissionAmount { get; set; }
+
+ ///
+ /// 在店天数
+ ///
+ public decimal WorkingDays { get; set; }
+
+ ///
+ /// 请假天数
+ ///
+ public decimal LeaveDays { get; set; }
+
+ ///
+ /// 核算应发工资
+ ///
+ public decimal CalculatedGrossSalary { get; set; }
+
+ ///
+ /// 最终应发工资
+ ///
+ public decimal FinalGrossSalary { get; set; }
+
+ ///
+ /// 当月培训补贴
+ ///
+ public decimal MonthlyTrainingSubsidy { get; set; }
+
+ ///
+ /// 当月交通补贴
+ ///
+ public decimal MonthlyTransportSubsidy { get; set; }
+
+ ///
+ /// 上月培训补贴
+ ///
+ public decimal LastMonthTrainingSubsidy { get; set; }
+
+ ///
+ /// 上月交通补贴
+ ///
+ public decimal LastMonthTransportSubsidy { get; set; }
+
+ ///
+ /// 补贴合计
+ ///
+ public decimal TotalSubsidy { get; set; }
+
+ ///
+ /// 缺卡扣款
+ ///
+ public decimal MissingCard { get; set; }
+
+ ///
+ /// 迟到扣款
+ ///
+ public decimal LateArrival { get; set; }
+
+ ///
+ /// 请假扣款
+ ///
+ public decimal LeaveDeduction { get; set; }
+
+ ///
+ /// 扣社保
+ ///
+ public decimal SocialInsuranceDeduction { get; set; }
+
+ ///
+ /// 扣除奖励
+ ///
+ public decimal RewardDeduction { get; set; }
+
+ ///
+ /// 扣住宿费
+ ///
+ public decimal AccommodationDeduction { get; set; }
+
+ ///
+ /// 扣学习期费用
+ ///
+ public decimal StudyPeriodDeduction { get; set; }
+
+ ///
+ /// 扣工作服费用
+ ///
+ public decimal WorkClothesDeduction { get; set; }
+
+ ///
+ /// 扣款合计
+ ///
+ public decimal TotalDeduction { get; set; }
+
+ ///
+ /// 发奖金
+ ///
+ public decimal Bonus { get; set; }
+
+ ///
+ /// 退手机押金
+ ///
+ public decimal ReturnPhoneDeposit { get; set; }
+
+ ///
+ /// 退住宿押金
+ ///
+ public decimal ReturnAccommodationDeposit { get; set; }
+
+ ///
+ /// 实发工资
+ ///
+ public decimal ActualSalary { get; set; }
+
+ ///
+ /// 当月是否发放
+ ///
+ public string MonthlyPaymentStatus { get; set; }
+
+ ///
+ /// 支付金额
+ ///
+ public decimal PaidAmount { get; set; }
+
+ ///
+ /// 待支付金额
+ ///
+ public decimal PendingAmount { get; set; }
+
+ ///
+ /// 补发上月
+ ///
+ public decimal LastMonthSupplement { get; set; }
+
+ ///
+ /// 当月支付总额
+ ///
+ public decimal MonthlyTotalPayment { get; set; }
+
+ ///
+ /// 是否锁定
+ ///
+ public int IsLocked { get; set; }
+
+ ///
+ /// 更新时间
+ ///
+ public DateTime UpdateTime { get; set; }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInfoOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInfoOutput.cs
index c813659..b034662 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInfoOutput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductInfoOutput.cs
@@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqProduct
public decimal price { get; set; }
///
+ /// 平均单价(加权平均成本,用于出库计价)
+ ///
+ public decimal averagePrice { get; set; }
+
+ ///
/// 产品类别
///
public string productCategory { get; set; }
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListOutput.cs
index ed76fd6..2d584f7 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListOutput.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqProduct/LqProductListOutput.cs
@@ -23,6 +23,11 @@ namespace NCC.Extend.Entitys.Dto.LqProduct
public decimal price { get; set; }
///
+ /// 平均单价(加权平均成本,用于出库计价)
+ ///
+ public decimal averagePrice { get; set; }
+
+ ///
/// 产品类别
///
public string productCategory { get; set; }
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryInput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryInput.cs
new file mode 100644
index 0000000..888f44d
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryInput.cs
@@ -0,0 +1,32 @@
+using NCC.Common.Filter;
+using System;
+
+namespace NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary
+{
+ ///
+ /// 科技部总经理工资查询参数
+ ///
+ public class TechGeneralManagerSalaryInput : PageInputBase
+ {
+ ///
+ /// 年份
+ ///
+ public int Year { get; set; }
+
+ ///
+ /// 月份
+ ///
+ public int Month { get; set; }
+
+ ///
+ /// 岗位(科技一部/科技二部等,不传则查询全部)
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 员工姓名/账号(可选,用于模糊搜索)
+ ///
+ public string Keyword { get; set; }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryOutput.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryOutput.cs
new file mode 100644
index 0000000..1249885
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqTechGeneralManagerSalary/TechGeneralManagerSalaryOutput.cs
@@ -0,0 +1,236 @@
+using System;
+
+namespace NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary
+{
+ ///
+ /// 科技部总经理工资输出
+ ///
+ public class TechGeneralManagerSalaryOutput
+ {
+ ///
+ /// 主键ID
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// 统计月份
+ ///
+ public string StatisticsMonth { get; set; }
+
+ ///
+ /// 核算岗位(科技一部/科技二部等)
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 员工姓名
+ ///
+ public string EmployeeName { get; set; }
+
+ ///
+ /// 员工ID
+ ///
+ public string EmployeeId { get; set; }
+
+ ///
+ /// 员工账号
+ ///
+ public string EmployeeAccount { get; set; }
+
+ ///
+ /// 是否离职
+ ///
+ public int IsTerminated { get; set; }
+
+ ///
+ /// 管理的门店明细(JSON格式)
+ ///
+ public string StoreDetail { get; set; }
+
+ ///
+ /// 溯源金额
+ ///
+ public decimal TraceabilityAmount { get; set; }
+
+ ///
+ /// Cell金额
+ ///
+ public decimal CellAmount { get; set; }
+
+ ///
+ /// 底薪
+ ///
+ public decimal BaseSalary { get; set; }
+
+ ///
+ /// 溯源金额提成比例
+ ///
+ public decimal? TraceabilityCommissionRate { get; set; }
+
+ ///
+ /// 溯源金额提成金额
+ ///
+ public decimal TraceabilityCommissionAmount { get; set; }
+
+ ///
+ /// Cell金额提成比例
+ ///
+ public decimal? CellCommissionRate { get; set; }
+
+ ///
+ /// Cell金额提成金额
+ ///
+ public decimal CellCommissionAmount { get; set; }
+
+ ///
+ /// 提成合计
+ ///
+ public decimal TotalCommission { get; set; }
+
+ ///
+ /// 在店天数
+ ///
+ public decimal WorkingDays { get; set; }
+
+ ///
+ /// 请假天数
+ ///
+ public decimal LeaveDays { get; set; }
+
+ ///
+ /// 核算应发工资
+ ///
+ public decimal CalculatedGrossSalary { get; set; }
+
+ ///
+ /// 最终应发工资
+ ///
+ public decimal FinalGrossSalary { get; set; }
+
+ ///
+ /// 当月培训补贴
+ ///
+ public decimal MonthlyTrainingSubsidy { get; set; }
+
+ ///
+ /// 当月交通补贴
+ ///
+ public decimal MonthlyTransportSubsidy { get; set; }
+
+ ///
+ /// 上月培训补贴
+ ///
+ public decimal LastMonthTrainingSubsidy { get; set; }
+
+ ///
+ /// 上月交通补贴
+ ///
+ public decimal LastMonthTransportSubsidy { get; set; }
+
+ ///
+ /// 补贴合计
+ ///
+ public decimal TotalSubsidy { get; set; }
+
+ ///
+ /// 缺卡扣款
+ ///
+ public decimal MissingCard { get; set; }
+
+ ///
+ /// 迟到扣款
+ ///
+ public decimal LateArrival { get; set; }
+
+ ///
+ /// 请假扣款
+ ///
+ public decimal LeaveDeduction { get; set; }
+
+ ///
+ /// 扣社保
+ ///
+ public decimal SocialInsuranceDeduction { get; set; }
+
+ ///
+ /// 扣除奖励
+ ///
+ public decimal RewardDeduction { get; set; }
+
+ ///
+ /// 扣住宿费
+ ///
+ public decimal AccommodationDeduction { get; set; }
+
+ ///
+ /// 扣学习期费用
+ ///
+ public decimal StudyPeriodDeduction { get; set; }
+
+ ///
+ /// 扣工作服费用
+ ///
+ public decimal WorkClothesDeduction { get; set; }
+
+ ///
+ /// 扣款合计
+ ///
+ public decimal TotalDeduction { get; set; }
+
+ ///
+ /// 发奖金
+ ///
+ public decimal Bonus { get; set; }
+
+ ///
+ /// 退手机押金
+ ///
+ public decimal ReturnPhoneDeposit { get; set; }
+
+ ///
+ /// 退住宿押金
+ ///
+ public decimal ReturnAccommodationDeposit { get; set; }
+
+ ///
+ /// 实发工资
+ ///
+ public decimal ActualSalary { get; set; }
+
+ ///
+ /// 当月是否发放
+ ///
+ public string MonthlyPaymentStatus { get; set; }
+
+ ///
+ /// 支付金额
+ ///
+ public decimal PaidAmount { get; set; }
+
+ ///
+ /// 待支付金额
+ ///
+ public decimal PendingAmount { get; set; }
+
+ ///
+ /// 补发上月
+ ///
+ public decimal LastMonthSupplement { get; set; }
+
+ ///
+ /// 当月支付总额
+ ///
+ public decimal MonthlyTotalPayment { get; set; }
+
+ ///
+ /// 是否锁定
+ ///
+ public int IsLocked { get; set; }
+
+ ///
+ /// 更新时间
+ ///
+ public DateTime UpdateTime { get; set; }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_director_salary_statistics/LqDirectorSalaryStatisticsEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_director_salary_statistics/LqDirectorSalaryStatisticsEntity.cs
index 3f48dab..782cff8 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_director_salary_statistics/LqDirectorSalaryStatisticsEntity.cs
+++ b/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
public decimal StoreRefundPerformance { get; set; }
///
+ /// 销售业绩(开单业绩-退款业绩)
+ ///
+ [SugarColumn(ColumnName = "F_SalesPerformance")]
+ public decimal SalesPerformance { get; set; }
+
+ ///
+ /// 产品物料(仓库领用金额)
+ ///
+ [SugarColumn(ColumnName = "F_ProductMaterial")]
+ public decimal ProductMaterial { get; set; }
+
+ ///
+ /// 合作项目成本
+ ///
+ [SugarColumn(ColumnName = "F_CooperationCost")]
+ public decimal CooperationCost { get; set; }
+
+ ///
+ /// 店内支出
+ ///
+ [SugarColumn(ColumnName = "F_StoreExpense")]
+ public decimal StoreExpense { get; set; }
+
+ ///
+ /// 洗毛巾费用
+ ///
+ [SugarColumn(ColumnName = "F_LaundryCost")]
+ public decimal LaundryCost { get; set; }
+
+ ///
+ /// 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
+ ///
+ [SugarColumn(ColumnName = "F_GrossProfit")]
+ public decimal GrossProfit { get; set; }
+
+ ///
/// 门店生命线
///
[SugarColumn(ColumnName = "F_StoreLifeline")]
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_application_node/LqInventoryUsageApplicationNodeEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_application_node/LqInventoryUsageApplicationNodeEntity.cs
index 09c438e..4dd5414 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_application_node/LqInventoryUsageApplicationNodeEntity.cs
+++ b/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
+
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_approval_record/LqInventoryUsageApprovalRecordEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_approval_record/LqInventoryUsageApprovalRecordEntity.cs
index ffcc6ad..6ffedd6 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_inventory_usage_approval_record/LqInventoryUsageApprovalRecordEntity.cs
+++ b/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
+
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_major_project_director_salary_statistics/LqMajorProjectDirectorSalaryStatisticsEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_major_project_director_salary_statistics/LqMajorProjectDirectorSalaryStatisticsEntity.cs
new file mode 100644
index 0000000..a742b34
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_major_project_director_salary_statistics/LqMajorProjectDirectorSalaryStatisticsEntity.cs
@@ -0,0 +1,291 @@
+using System;
+using NCC.Common.Const;
+using SqlSugar;
+
+namespace NCC.Extend.Entitys.lq_major_project_director_salary_statistics
+{
+ ///
+ /// 大项目主管工资统计表
+ ///
+ [SugarTable("lq_major_project_director_salary_statistics")]
+ [Tenant(ClaimConst.TENANT_ID)]
+ public class LqMajorProjectDirectorSalaryStatisticsEntity
+ {
+ ///
+ /// 主键ID
+ ///
+ [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
+ public string Id { get; set; }
+
+ ///
+ /// 统计月份(YYYYMM)
+ ///
+ [SugarColumn(ColumnName = "F_StatisticsMonth", Length = 6)]
+ public string StatisticsMonth { get; set; }
+
+ ///
+ /// 核算岗位(大项目一部/大项目二部等)
+ ///
+ [SugarColumn(ColumnName = "F_Position")]
+ public string Position { get; set; }
+
+ ///
+ /// 员工姓名
+ ///
+ [SugarColumn(ColumnName = "F_EmployeeName")]
+ public string EmployeeName { get; set; }
+
+ ///
+ /// 员工ID
+ ///
+ [SugarColumn(ColumnName = "F_EmployeeId")]
+ public string EmployeeId { get; set; }
+
+ ///
+ /// 员工账号
+ ///
+ [SugarColumn(ColumnName = "F_EmployeeAccount")]
+ public string EmployeeAccount { get; set; }
+
+ ///
+ /// 是否离职(0=在职,1=离职)
+ ///
+ [SugarColumn(ColumnName = "F_IsTerminated")]
+ public int IsTerminated { get; set; }
+
+ ///
+ /// 管理的门店明细(JSON格式)
+ ///
+ [SugarColumn(ColumnName = "F_StoreDetail", ColumnDataType = "TEXT")]
+ public string StoreDetail { get; set; }
+
+ ///
+ /// 总业绩(管理的所有门店的总业绩总和,开单-退卡)
+ ///
+ [SugarColumn(ColumnName = "F_TotalPerformance")]
+ public decimal TotalPerformance { get; set; }
+
+ ///
+ /// 开单金额(管理的所有门店的开单金额总和)
+ ///
+ [SugarColumn(ColumnName = "F_BillingAmount")]
+ public decimal BillingAmount { get; set; }
+
+ ///
+ /// 退卡金额(管理的所有门店的退卡金额总和)
+ ///
+ [SugarColumn(ColumnName = "F_RefundAmount")]
+ public decimal RefundAmount { get; set; }
+
+ ///
+ /// 底薪金额(固定3500元)
+ ///
+ [SugarColumn(ColumnName = "F_BaseSalary")]
+ public decimal BaseSalary { get; set; }
+
+ ///
+ /// 提成比例(根据总业绩分段:0%/1%/1.5%)
+ ///
+ [SugarColumn(ColumnName = "F_CommissionRate")]
+ public decimal? CommissionRate { get; set; }
+
+ ///
+ /// 提成金额(总业绩 × 提成比例)
+ ///
+ [SugarColumn(ColumnName = "F_CommissionAmount")]
+ public decimal CommissionAmount { get; set; }
+
+ ///
+ /// 在店天数
+ ///
+ [SugarColumn(ColumnName = "F_WorkingDays")]
+ public decimal WorkingDays { get; set; }
+
+ ///
+ /// 请假天数
+ ///
+ [SugarColumn(ColumnName = "F_LeaveDays")]
+ public decimal LeaveDays { get; set; }
+
+ ///
+ /// 核算应发工资(底薪 + 提成金额)
+ ///
+ [SugarColumn(ColumnName = "F_CalculatedGrossSalary")]
+ public decimal CalculatedGrossSalary { get; set; }
+
+ ///
+ /// 最终应发工资
+ ///
+ [SugarColumn(ColumnName = "F_FinalGrossSalary")]
+ public decimal FinalGrossSalary { get; set; }
+
+ ///
+ /// 当月培训补贴
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyTrainingSubsidy")]
+ public decimal MonthlyTrainingSubsidy { get; set; }
+
+ ///
+ /// 当月交通补贴
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyTransportSubsidy")]
+ public decimal MonthlyTransportSubsidy { get; set; }
+
+ ///
+ /// 上月培训补贴
+ ///
+ [SugarColumn(ColumnName = "F_LastMonthTrainingSubsidy")]
+ public decimal LastMonthTrainingSubsidy { get; set; }
+
+ ///
+ /// 上月交通补贴
+ ///
+ [SugarColumn(ColumnName = "F_LastMonthTransportSubsidy")]
+ public decimal LastMonthTransportSubsidy { get; set; }
+
+ ///
+ /// 补贴合计
+ ///
+ [SugarColumn(ColumnName = "F_TotalSubsidy")]
+ public decimal TotalSubsidy { get; set; }
+
+ ///
+ /// 缺卡扣款
+ ///
+ [SugarColumn(ColumnName = "F_MissingCard")]
+ public decimal MissingCard { get; set; }
+
+ ///
+ /// 迟到扣款
+ ///
+ [SugarColumn(ColumnName = "F_LateArrival")]
+ public decimal LateArrival { get; set; }
+
+ ///
+ /// 请假扣款
+ ///
+ [SugarColumn(ColumnName = "F_LeaveDeduction")]
+ public decimal LeaveDeduction { get; set; }
+
+ ///
+ /// 扣社保
+ ///
+ [SugarColumn(ColumnName = "F_SocialInsuranceDeduction")]
+ public decimal SocialInsuranceDeduction { get; set; }
+
+ ///
+ /// 扣除奖励
+ ///
+ [SugarColumn(ColumnName = "F_RewardDeduction")]
+ public decimal RewardDeduction { get; set; }
+
+ ///
+ /// 扣住宿费
+ ///
+ [SugarColumn(ColumnName = "F_AccommodationDeduction")]
+ public decimal AccommodationDeduction { get; set; }
+
+ ///
+ /// 扣学习期费用
+ ///
+ [SugarColumn(ColumnName = "F_StudyPeriodDeduction")]
+ public decimal StudyPeriodDeduction { get; set; }
+
+ ///
+ /// 扣工作服费用
+ ///
+ [SugarColumn(ColumnName = "F_WorkClothesDeduction")]
+ public decimal WorkClothesDeduction { get; set; }
+
+ ///
+ /// 扣款合计
+ ///
+ [SugarColumn(ColumnName = "F_TotalDeduction")]
+ public decimal TotalDeduction { get; set; }
+
+ ///
+ /// 发奖金
+ ///
+ [SugarColumn(ColumnName = "F_Bonus")]
+ public decimal Bonus { get; set; }
+
+ ///
+ /// 退手机押金
+ ///
+ [SugarColumn(ColumnName = "F_ReturnPhoneDeposit")]
+ public decimal ReturnPhoneDeposit { get; set; }
+
+ ///
+ /// 退住宿押金
+ ///
+ [SugarColumn(ColumnName = "F_ReturnAccommodationDeposit")]
+ public decimal ReturnAccommodationDeposit { get; set; }
+
+ ///
+ /// 实发工资
+ ///
+ [SugarColumn(ColumnName = "F_ActualSalary")]
+ public decimal ActualSalary { get; set; }
+
+ ///
+ /// 当月是否发放
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyPaymentStatus")]
+ public string MonthlyPaymentStatus { get; set; }
+
+ ///
+ /// 支付金额
+ ///
+ [SugarColumn(ColumnName = "F_PaidAmount")]
+ public decimal PaidAmount { get; set; }
+
+ ///
+ /// 待支付金额
+ ///
+ [SugarColumn(ColumnName = "F_PendingAmount")]
+ public decimal PendingAmount { get; set; }
+
+ ///
+ /// 补发上月
+ ///
+ [SugarColumn(ColumnName = "F_LastMonthSupplement")]
+ public decimal LastMonthSupplement { get; set; }
+
+ ///
+ /// 当月支付总额
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyTotalPayment")]
+ public decimal MonthlyTotalPayment { get; set; }
+
+ ///
+ /// 是否锁定(0=未锁定,1=已锁定)
+ ///
+ [SugarColumn(ColumnName = "F_IsLocked")]
+ public int IsLocked { get; set; }
+
+ ///
+ /// 创建时间
+ ///
+ [SugarColumn(ColumnName = "F_CreateTime")]
+ public DateTime CreateTime { get; set; }
+
+ ///
+ /// 更新时间
+ ///
+ [SugarColumn(ColumnName = "F_UpdateTime")]
+ public DateTime UpdateTime { get; set; }
+
+ ///
+ /// 创建人
+ ///
+ [SugarColumn(ColumnName = "F_CreateUser")]
+ public string CreateUser { get; set; }
+
+ ///
+ /// 更新人
+ ///
+ [SugarColumn(ColumnName = "F_UpdateUser")]
+ public string UpdateUser { get; set; }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_product/LqProductEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_product/LqProductEntity.cs
index 32e7e10..b8de161 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_product/LqProductEntity.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_product/LqProductEntity.cs
@@ -30,6 +30,12 @@ namespace NCC.Extend.Entitys.lq_product
public decimal Price { get; set; }
///
+ /// 平均单价(加权平均成本,用于出库计价)
+ ///
+ [SugarColumn(ColumnName = "F_AveragePrice")]
+ public decimal AveragePrice { get; set; }
+
+ ///
/// 产品类别
///
[SugarColumn(ColumnName = "F_ProductCategory")]
diff --git a/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_tech_general_manager_salary_statistics/LqTechGeneralManagerSalaryStatisticsEntity.cs b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_tech_general_manager_salary_statistics/LqTechGeneralManagerSalaryStatisticsEntity.cs
new file mode 100644
index 0000000..241016f
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_tech_general_manager_salary_statistics/LqTechGeneralManagerSalaryStatisticsEntity.cs
@@ -0,0 +1,303 @@
+using System;
+using NCC.Common.Const;
+using SqlSugar;
+
+namespace NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics
+{
+ ///
+ /// 科技部总经理工资统计表
+ ///
+ [SugarTable("lq_tech_general_manager_salary_statistics")]
+ [Tenant(ClaimConst.TENANT_ID)]
+ public class LqTechGeneralManagerSalaryStatisticsEntity
+ {
+ ///
+ /// 主键ID
+ ///
+ [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
+ public string Id { get; set; }
+
+ ///
+ /// 统计月份(YYYYMM)
+ ///
+ [SugarColumn(ColumnName = "F_StatisticsMonth", Length = 6)]
+ public string StatisticsMonth { get; set; }
+
+ ///
+ /// 核算岗位(科技一部/科技二部等)
+ ///
+ [SugarColumn(ColumnName = "F_Position")]
+ public string Position { get; set; }
+
+ ///
+ /// 员工姓名
+ ///
+ [SugarColumn(ColumnName = "F_EmployeeName")]
+ public string EmployeeName { get; set; }
+
+ ///
+ /// 员工ID
+ ///
+ [SugarColumn(ColumnName = "F_EmployeeId")]
+ public string EmployeeId { get; set; }
+
+ ///
+ /// 员工账号
+ ///
+ [SugarColumn(ColumnName = "F_EmployeeAccount")]
+ public string EmployeeAccount { get; set; }
+
+ ///
+ /// 是否离职(0=在职,1=离职)
+ ///
+ [SugarColumn(ColumnName = "F_IsTerminated")]
+ public int IsTerminated { get; set; }
+
+ ///
+ /// 管理的门店明细(JSON格式)
+ ///
+ [SugarColumn(ColumnName = "F_StoreDetail", ColumnDataType = "TEXT")]
+ public string StoreDetail { get; set; }
+
+ ///
+ /// 溯源金额(管理的所有门店的溯源金额总和,开单-退卡)
+ ///
+ [SugarColumn(ColumnName = "F_TraceabilityAmount")]
+ public decimal TraceabilityAmount { get; set; }
+
+ ///
+ /// Cell金额(管理的所有门店的Cell金额总和,开单-退卡)
+ ///
+ [SugarColumn(ColumnName = "F_CellAmount")]
+ public decimal CellAmount { get; set; }
+
+ ///
+ /// 底薪金额(固定4000元)
+ ///
+ [SugarColumn(ColumnName = "F_BaseSalary")]
+ public decimal BaseSalary { get; set; }
+
+ ///
+ /// 溯源金额提成比例(分段计算,存储平均比例)
+ ///
+ [SugarColumn(ColumnName = "F_TraceabilityCommissionRate")]
+ public decimal? TraceabilityCommissionRate { get; set; }
+
+ ///
+ /// 溯源金额提成金额
+ ///
+ [SugarColumn(ColumnName = "F_TraceabilityCommissionAmount")]
+ public decimal TraceabilityCommissionAmount { get; set; }
+
+ ///
+ /// Cell金额提成比例(分段计算,存储平均比例)
+ ///
+ [SugarColumn(ColumnName = "F_CellCommissionRate")]
+ public decimal? CellCommissionRate { get; set; }
+
+ ///
+ /// Cell金额提成金额
+ ///
+ [SugarColumn(ColumnName = "F_CellCommissionAmount")]
+ public decimal CellCommissionAmount { get; set; }
+
+ ///
+ /// 提成合计(溯源提成+Cell提成)
+ ///
+ [SugarColumn(ColumnName = "F_TotalCommission")]
+ public decimal TotalCommission { get; set; }
+
+ ///
+ /// 在店天数
+ ///
+ [SugarColumn(ColumnName = "F_WorkingDays")]
+ public decimal WorkingDays { get; set; }
+
+ ///
+ /// 请假天数
+ ///
+ [SugarColumn(ColumnName = "F_LeaveDays")]
+ public decimal LeaveDays { get; set; }
+
+ ///
+ /// 核算应发工资(底薪 + 提成合计)
+ ///
+ [SugarColumn(ColumnName = "F_CalculatedGrossSalary")]
+ public decimal CalculatedGrossSalary { get; set; }
+
+ ///
+ /// 最终应发工资
+ ///
+ [SugarColumn(ColumnName = "F_FinalGrossSalary")]
+ public decimal FinalGrossSalary { get; set; }
+
+ ///
+ /// 当月培训补贴
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyTrainingSubsidy")]
+ public decimal MonthlyTrainingSubsidy { get; set; }
+
+ ///
+ /// 当月交通补贴
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyTransportSubsidy")]
+ public decimal MonthlyTransportSubsidy { get; set; }
+
+ ///
+ /// 上月培训补贴
+ ///
+ [SugarColumn(ColumnName = "F_LastMonthTrainingSubsidy")]
+ public decimal LastMonthTrainingSubsidy { get; set; }
+
+ ///
+ /// 上月交通补贴
+ ///
+ [SugarColumn(ColumnName = "F_LastMonthTransportSubsidy")]
+ public decimal LastMonthTransportSubsidy { get; set; }
+
+ ///
+ /// 补贴合计
+ ///
+ [SugarColumn(ColumnName = "F_TotalSubsidy")]
+ public decimal TotalSubsidy { get; set; }
+
+ ///
+ /// 缺卡扣款
+ ///
+ [SugarColumn(ColumnName = "F_MissingCard")]
+ public decimal MissingCard { get; set; }
+
+ ///
+ /// 迟到扣款
+ ///
+ [SugarColumn(ColumnName = "F_LateArrival")]
+ public decimal LateArrival { get; set; }
+
+ ///
+ /// 请假扣款
+ ///
+ [SugarColumn(ColumnName = "F_LeaveDeduction")]
+ public decimal LeaveDeduction { get; set; }
+
+ ///
+ /// 扣社保
+ ///
+ [SugarColumn(ColumnName = "F_SocialInsuranceDeduction")]
+ public decimal SocialInsuranceDeduction { get; set; }
+
+ ///
+ /// 扣除奖励
+ ///
+ [SugarColumn(ColumnName = "F_RewardDeduction")]
+ public decimal RewardDeduction { get; set; }
+
+ ///
+ /// 扣住宿费
+ ///
+ [SugarColumn(ColumnName = "F_AccommodationDeduction")]
+ public decimal AccommodationDeduction { get; set; }
+
+ ///
+ /// 扣学习期费用
+ ///
+ [SugarColumn(ColumnName = "F_StudyPeriodDeduction")]
+ public decimal StudyPeriodDeduction { get; set; }
+
+ ///
+ /// 扣工作服费用
+ ///
+ [SugarColumn(ColumnName = "F_WorkClothesDeduction")]
+ public decimal WorkClothesDeduction { get; set; }
+
+ ///
+ /// 扣款合计
+ ///
+ [SugarColumn(ColumnName = "F_TotalDeduction")]
+ public decimal TotalDeduction { get; set; }
+
+ ///
+ /// 发奖金
+ ///
+ [SugarColumn(ColumnName = "F_Bonus")]
+ public decimal Bonus { get; set; }
+
+ ///
+ /// 退手机押金
+ ///
+ [SugarColumn(ColumnName = "F_ReturnPhoneDeposit")]
+ public decimal ReturnPhoneDeposit { get; set; }
+
+ ///
+ /// 退住宿押金
+ ///
+ [SugarColumn(ColumnName = "F_ReturnAccommodationDeposit")]
+ public decimal ReturnAccommodationDeposit { get; set; }
+
+ ///
+ /// 实发工资
+ ///
+ [SugarColumn(ColumnName = "F_ActualSalary")]
+ public decimal ActualSalary { get; set; }
+
+ ///
+ /// 当月是否发放
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyPaymentStatus")]
+ public string MonthlyPaymentStatus { get; set; }
+
+ ///
+ /// 支付金额
+ ///
+ [SugarColumn(ColumnName = "F_PaidAmount")]
+ public decimal PaidAmount { get; set; }
+
+ ///
+ /// 待支付金额
+ ///
+ [SugarColumn(ColumnName = "F_PendingAmount")]
+ public decimal PendingAmount { get; set; }
+
+ ///
+ /// 补发上月
+ ///
+ [SugarColumn(ColumnName = "F_LastMonthSupplement")]
+ public decimal LastMonthSupplement { get; set; }
+
+ ///
+ /// 当月支付总额
+ ///
+ [SugarColumn(ColumnName = "F_MonthlyTotalPayment")]
+ public decimal MonthlyTotalPayment { get; set; }
+
+ ///
+ /// 是否锁定(0=未锁定,1=已锁定)
+ ///
+ [SugarColumn(ColumnName = "F_IsLocked")]
+ public int IsLocked { get; set; }
+
+ ///
+ /// 创建时间
+ ///
+ [SugarColumn(ColumnName = "F_CreateTime")]
+ public DateTime CreateTime { get; set; }
+
+ ///
+ /// 更新时间
+ ///
+ [SugarColumn(ColumnName = "F_UpdateTime")]
+ public DateTime UpdateTime { get; set; }
+
+ ///
+ /// 创建人
+ ///
+ [SugarColumn(ColumnName = "F_CreateUser")]
+ public string CreateUser { get; set; }
+
+ ///
+ /// 更新人
+ ///
+ [SugarColumn(ColumnName = "F_UpdateUser")]
+ public string UpdateUser { get; set; }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
index f701278..ffa460c 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqAssistantSalaryService.cs
@@ -311,54 +311,69 @@ namespace NCC.Extend
salary.PerformanceCompletionRate = 0;
}
- // 2.5 计算门店总提成(先计算门店级别的提成)
+ // 2.5 计算提成比例(固定比例,不随在店天数变化)
// 判断岗位类型:店助 或 店助主任
bool isDirector = salary.Position == "店助主任";
- decimal storeTotalCommission = 0;
decimal commissionRate = 0;
// 如果门店生命线未设置(<=0),则没有提成
if (salary.StoreLifeline <= 0)
{
commissionRate = 0;
- storeTotalCommission = 0;
}
else
{
- // 根据岗位类型计算提成
+ // 计算业绩完成率
+ decimal performanceRatio = salary.StoreTotalPerformance / salary.StoreLifeline;
+
+ // 根据岗位类型确定提成比例
if (isDirector)
{
- // 店助主任:使用阶梯提成模式
+ // 店助主任:使用固定比例(按规则文档,业绩≥100%时使用阶梯提成,但为保持比例固定,使用平均比例)
// 业绩 < 70%:0%
- // 70% ≤ 业绩 < 100%:0.4%(阶梯)
- // 超过生命线部分:1.6%(阶梯)
- storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
- // 店助主任的提成比例用于显示,计算平均比例
- if (salary.StoreTotalPerformance > 0)
+ // 70% ≤ 业绩 < 100%:0.4%
+ // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例
+ if (performanceRatio < 0.7m)
+ {
+ commissionRate = 0;
+ }
+ else if (performanceRatio < 1.0m)
{
- commissionRate = storeTotalCommission / salary.StoreTotalPerformance;
+ commissionRate = 0.004m; // 0.4%
}
else
{
- commissionRate = 0;
+ // 业绩 ≥ 100%:使用阶梯提成计算总提成,然后计算平均比例
+ // ≤生命线部分:0.6%,>生命线部分:1%
+ decimal storeTotalCommission = CalculateDirectorCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
+ if (salary.StoreTotalPerformance > 0)
+ {
+ commissionRate = storeTotalCommission / salary.StoreTotalPerformance;
+ }
+ else
+ {
+ commissionRate = 0;
+ }
}
}
else
{
- // 店助:使用阶梯提成模式
+ // 店助:使用固定比例
// 业绩 < 70%:0%
- // 70% ≤ 业绩 < 100%:0.4%(阶梯)
- // 业绩 ≥ 100%:0.6%(阶梯)
- storeTotalCommission = CalculateAssistantCommission(salary.StoreTotalPerformance, salary.StoreLifeline);
- // 店助的提成比例用于显示,计算平均比例
- if (salary.StoreTotalPerformance > 0)
+ // 70% ≤ 业绩 < 100%:0.4%
+ // 业绩 ≥ 100%:0.6%
+ if (performanceRatio < 0.7m)
{
- commissionRate = storeTotalCommission / salary.StoreTotalPerformance;
+ commissionRate = 0;
+ }
+ else if (performanceRatio < 1.0m)
+ {
+ commissionRate = 0.004m; // 0.4%
}
else
{
- commissionRate = 0;
+ commissionRate = 0.006m; // 0.6%
}
}
}
@@ -408,13 +423,7 @@ namespace NCC.Extend
}
}
- // 2.8 计算底薪(店助和店助主任底薪规则相同,都根据门店分类确定)
- salary.BaseSalary = CalculateBaseSalary(salary.StoreCategory.Value);
-
- // 2.9 固定奖励(手机管理费)
- salary.PhoneManagementFee = 150m;
-
- // 2.10 考勤数据
+ // 2.8 考勤数据
int workingDays = 0;
if (attendanceDict.ContainsKey(assistantUser.Id))
{
@@ -429,13 +438,35 @@ namespace NCC.Extend
salary.LeaveDays = 0;
}
+ // 2.9 计算底薪(店助和店助主任底薪规则相同,都根据门店分类确定,按在店天数比例计算)
+ decimal baseSalaryFull = CalculateBaseSalary(salary.StoreCategory.Value);
+ if (daysInMonth > 0 && workingDays > 0)
+ {
+ salary.BaseSalary = baseSalaryFull / daysInMonth * workingDays;
+ }
+ else
+ {
+ salary.BaseSalary = 0;
+ }
+
+ // 2.10 计算手机管理费(按在店天数比例计算)
+ decimal phoneManagementFeeFull = 150m;
+ if (daysInMonth > 0 && workingDays > 0)
+ {
+ salary.PhoneManagementFee = phoneManagementFeeFull / daysInMonth * workingDays;
+ }
+ else
+ {
+ salary.PhoneManagementFee = 0;
+ }
+
// 2.11 按在店天数比例计算店助的提成和奖励
- // 逻辑:门店总提成 / 当月天数 * 在店天数 = 店助提成
- // 门店总奖励 / 当月天数 * 在店天数 = 店助奖励
+ // 逻辑:提成金额 = 门店业绩 × 提成比例 / 当月天数 × 在店天数
+ // 阶段奖励 = 门店总奖励 / 当月天数 × 在店天数
if (daysInMonth > 0 && workingDays > 0)
{
- // 按比例计算提成
- salary.CommissionAmount = storeTotalCommission / daysInMonth * workingDays;
+ // 按比例计算提成:门店业绩 × 提成比例 / 当月天数 × 在店天数
+ salary.CommissionAmount = salary.StoreTotalPerformance * commissionRate / daysInMonth * workingDays;
// 按比例计算奖励
salary.StageRewardAmount = storeTotalStageReward / daysInMonth * workingDays;
@@ -615,14 +646,17 @@ namespace NCC.Extend
// 门店业绩 ≥ 门店生命线 × 100% → 阶梯提成
// 70%以下部分:0%
// 70%-100%部分:0.4%
- // 超过生命线部分:1.6%
+ // ≤生命线部分:0.6%,>生命线部分:1%
decimal stage70 = storeLifeline * 0.7m;
decimal stage100 = storeLifeline;
decimal performance70To100 = stage100 - stage70;
decimal performanceAbove100 = storePerformance - stage100;
+ // 70%-100%部分:0.4%
decimal commission70To100 = performance70To100 * 0.004m;
- decimal commissionAbove100 = performanceAbove100 * 0.016m;
+ // ≤生命线部分(0-70%):0%,70%-100%部分:0.4%,已计算
+ // >生命线部分:1%
+ decimal commissionAbove100 = performanceAbove100 * 0.01m; // 1%
return commission70To100 + commissionAbove100;
}
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
index ef82589..e1cad62 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDailyReportService.cs
@@ -866,7 +866,7 @@ namespace NCC.Extend
/// - ManagerId: 经理用户ID
/// - ManagerName: 经理姓名
/// - StoreId/StoreName: 门店信息
- /// - Target1/2/3: 目标业绩一/二/三阶段(来自门店总经理生命线设置表lq_md_general_manager_lifeline的F_Lifeline1/F_Lifeline2/F_Lifeline3字段,根据开始时间所在月份获取,如果未查询到则为0)
+ /// - Target1/2/3: 目标业绩一/二/三阶段(来自门店目标表lq_md_target的F_StoreTarget字段,根据开始时间所在月份获取,如果未查询到则为0,三个阶段都使用同一个门店目标值)
/// - CompletedPerformance: 完成业绩(指定时间范围内的开单业绩总和)
/// - CompletionRate1/2/3: 完成率各阶段(百分比)
///
@@ -889,40 +889,42 @@ namespace NCC.Extend
var managerFilter = "";
if (!string.IsNullOrWhiteSpace(input.ManagerId))
{
- managerFilter = $"AND target.F_GeneralManagerId = '{input.ManagerId}'";
+ managerFilter = $"AND gm.F_GeneralManagerId = '{input.ManagerId}'";
}
// 构建经理类型过滤条件
if (input.ManagerType.HasValue)
{
- managerFilter += $" AND target.F_ManagerType = {input.ManagerType.Value}";
+ managerFilter += $" AND gm.F_ManagerType = {input.ManagerType.Value}";
}
// SQL查询:获取经理在各门店的目标业绩和完成业绩
+ // 目标业绩从门店目标表(lq_md_target)的F_StoreTarget字段获取
var sql = $@"
SELECT
- target.F_GeneralManagerId as ManagerId,
+ gm.F_GeneralManagerId as ManagerId,
u.F_RealName as ManagerName,
- target.F_StoreId as StoreId,
+ gm.F_StoreId as StoreId,
store.dm as StoreName,
- target.F_Lifeline1 as Target1,
- target.F_Lifeline2 as Target2,
- target.F_Lifeline3 as Target3,
+ COALESCE(md_target.F_StoreTarget, 0) as Target1,
+ COALESCE(md_target.F_StoreTarget, 0) as Target2,
+ COALESCE(md_target.F_StoreTarget, 0) as Target3,
-- 完成业绩
COALESCE((
SELECT SUM(billing.sfyj)
FROM lq_kd_kdjlb billing
- WHERE billing.djmd = target.F_StoreId
+ WHERE billing.djmd = gm.F_StoreId
AND billing.F_IsEffective = 1
- AND DATE(billing.kdrq) >= '{startDate:yyyy-MM-dd}'
- AND DATE(billing.kdrq) <= '{endDate:yyyy-MM-dd}'
+ AND DATE(billing.kdrq) >= '{startDate.ToString("yyyy-MM-dd")}'
+ AND DATE(billing.kdrq) <= '{endDate.ToString("yyyy-MM-dd")}'
), 0) as CompletedPerformance
- FROM lq_md_general_manager_lifeline target
- INNER JOIN BASE_USER u ON target.F_GeneralManagerId = u.F_Id
- INNER JOIN lq_mdxx store ON target.F_StoreId = store.F_Id
- WHERE target.F_Month = '{month}'
+ FROM lq_md_general_manager_lifeline gm
+ INNER JOIN BASE_USER u ON gm.F_GeneralManagerId = u.F_Id
+ INNER JOIN lq_mdxx store ON gm.F_StoreId = store.F_Id
+ LEFT JOIN lq_md_target md_target ON gm.F_StoreId = md_target.F_StoreId AND md_target.F_Month = '{month}'
+ WHERE gm.F_Month = '{month}'
{managerFilter}
- ORDER BY target.F_GeneralManagerId, store.dm";
+ ORDER BY gm.F_GeneralManagerId, store.dm";
var result = await _db.Ado.SqlQueryAsync(sql);
@@ -981,7 +983,7 @@ namespace NCC.Extend
/// 返回说明:
/// - ManagerId: 经理用户ID
/// - ManagerName: 经理姓名
- /// - TotalTarget1/2/3: 总目标业绩一/二/三阶段(来自门店总经理生命线设置表lq_md_general_manager_lifeline的F_Lifeline1/F_Lifeline2/F_Lifeline3字段,根据开始时间所在月份获取并汇总)
+ /// - TotalTarget1/2/3: 总目标业绩一/二/三阶段(来自门店目标表lq_md_target的F_StoreTarget字段,根据开始时间所在月份获取并汇总所有管理门店的门店目标,三个阶段都使用同一个汇总值)
/// - TotalCompletedPerformance: 总完成业绩(指定时间范围内的开单业绩总和)
/// - TotalCompletionRate1/2/3: 总完成率各阶段
/// - StoreCount: 管理门店数量(根据门店总经理生命线设置表中归属该经理的门店数统计)
@@ -1005,39 +1007,41 @@ namespace NCC.Extend
var managerFilter = "";
if (!string.IsNullOrWhiteSpace(input.ManagerId))
{
- managerFilter = $"AND target.F_GeneralManagerId = '{input.ManagerId}'";
+ managerFilter = $"AND gm.F_GeneralManagerId = '{input.ManagerId}'";
}
// 构建经理类型过滤条件
if (input.ManagerType.HasValue)
{
- managerFilter += $" AND target.F_ManagerType = {input.ManagerType.Value}";
+ managerFilter += $" AND gm.F_ManagerType = {input.ManagerType.Value}";
}
// SQL查询:获取经理汇总业绩(基于lq_md_general_manager_lifeline表中的经理和门店关系)
+ // 目标业绩从门店目标表(lq_md_target)的F_StoreTarget字段获取并汇总
var sql = $@"
SELECT
- target.F_GeneralManagerId as ManagerId,
+ gm.F_GeneralManagerId as ManagerId,
u.F_RealName as ManagerName,
- SUM(target.F_Lifeline1) as TotalTarget1,
- COALESCE(SUM(target.F_Lifeline2), 0) as TotalTarget2,
- COALESCE(SUM(target.F_Lifeline3), 0) as TotalTarget3,
- COUNT(DISTINCT target.F_StoreId) as StoreCount,
+ COALESCE(SUM(md_target.F_StoreTarget), 0) as TotalTarget1,
+ COALESCE(SUM(md_target.F_StoreTarget), 0) as TotalTarget2,
+ COALESCE(SUM(md_target.F_StoreTarget), 0) as TotalTarget3,
+ COUNT(DISTINCT gm.F_StoreId) as StoreCount,
-- 总完成业绩(基于lq_md_general_manager_lifeline表中的门店关系计算)
SUM(COALESCE((
SELECT SUM(billing.sfyj)
FROM lq_kd_kdjlb billing
- WHERE billing.djmd = target.F_StoreId
+ WHERE billing.djmd = gm.F_StoreId
AND billing.F_IsEffective = 1
- AND DATE(billing.kdrq) >= '{startDate:yyyy-MM-dd}'
- AND DATE(billing.kdrq) <= '{endDate:yyyy-MM-dd}'
+ AND DATE(billing.kdrq) >= '{startDate.ToString("yyyy-MM-dd")}'
+ AND DATE(billing.kdrq) <= '{endDate.ToString("yyyy-MM-dd")}'
), 0)) as TotalCompletedPerformance
- FROM lq_md_general_manager_lifeline target
- INNER JOIN BASE_USER u ON target.F_GeneralManagerId = u.F_Id
- INNER JOIN lq_mdxx store ON target.F_StoreId = store.F_Id
- WHERE target.F_Month = '{month}'
+ FROM lq_md_general_manager_lifeline gm
+ INNER JOIN BASE_USER u ON gm.F_GeneralManagerId = u.F_Id
+ INNER JOIN lq_mdxx store ON gm.F_StoreId = store.F_Id
+ LEFT JOIN lq_md_target md_target ON gm.F_StoreId = md_target.F_StoreId AND md_target.F_Month = '{month}'
+ WHERE gm.F_Month = '{month}'
{managerFilter}
- GROUP BY target.F_GeneralManagerId, u.F_RealName
+ GROUP BY gm.F_GeneralManagerId, u.F_RealName
ORDER BY TotalCompletedPerformance DESC";
var result = await _db.Ado.SqlQueryAsync(sql);
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
index e3d4eb1..f136964 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqDirectorSalaryService.cs
@@ -5,13 +5,17 @@ using NCC.Common.Helper;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.LqDirectorSalary;
+using NCC.Extend.Entitys.Enum;
using NCC.Extend.Entitys.lq_attendance_summary;
+using NCC.Extend.Entitys.lq_cooperation_cost;
using NCC.Extend.Entitys.lq_director_salary_statistics;
using NCC.Extend.Entitys.lq_hytk_hytk;
using NCC.Extend.Entitys.lq_kd_kdjlb;
+using NCC.Extend.Entitys.lq_laundry_flow;
using NCC.Extend.Entitys.lq_md_target;
using NCC.Extend.Entitys.lq_md_xdbhsj;
using NCC.Extend.Entitys.lq_mdxx;
+using NCC.Extend.Entitys.lq_store_expense;
using NCC.Extend.Entitys.lq_xh_hyhk;
using NCC.Extend.Entitys.lq_xh_jksyj;
using NCC.System.Entitys.Permission;
@@ -140,6 +144,7 @@ namespace NCC.Extend
var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1).AddDays(-1);
var monthStr = $"{year}{month:D2}";
+ var daysInMonth = DateTime.DaysInMonth(year, month); // 当月天数
// 1. 获取基础数据
@@ -244,6 +249,68 @@ namespace NCC.Extend
.ToListAsync();
var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
+ // 1.9 产品物料统计(仓库领用金额,注意11月特殊规则)
+ var queryMonth = monthStr;
+ if (month == 11)
+ {
+ // 11月工资算10月数据
+ queryMonth = $"{year}10";
+ }
+ var productMaterialSql = $@"
+ SELECT
+ F_StoreId as StoreId,
+ COALESCE(SUM(F_TotalAmount), 0) as MaterialAmount
+ FROM lq_inventory_usage
+ WHERE F_IsEffective = 1
+ AND DATE_FORMAT(F_UsageTime, '%Y%m') = @queryMonth
+ GROUP BY F_StoreId";
+
+ var productMaterialData = await _db.Ado.SqlQueryAsync(productMaterialSql, new { queryMonth });
+ var productMaterialDict = productMaterialData
+ .Where(x => x.StoreId != null)
+ .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0));
+
+ // 1.10 合作项目成本统计
+ var cooperationCostList = await _db.Queryable()
+ .Where(x => x.Year == year && x.Month == monthStr && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .Select(x => new { x.StoreId, x.TotalAmount })
+ .ToListAsync();
+ var cooperationCostDict = cooperationCostList
+ .Where(x => !string.IsNullOrEmpty(x.StoreId))
+ .GroupBy(x => x.StoreId)
+ .ToDictionary(g => g.Key, g => g.Sum(x => x.TotalAmount));
+
+ // 1.11 店内支出统计
+ var storeExpenseSql = $@"
+ SELECT
+ F_StoreId as StoreId,
+ COALESCE(SUM(F_Amount), 0) as ExpenseAmount
+ FROM lq_store_expense
+ WHERE F_IsEffective = 1
+ AND DATE_FORMAT(F_ExpenseDate, '%Y%m') = @monthStr
+ GROUP BY F_StoreId";
+
+ var storeExpenseData = await _db.Ado.SqlQueryAsync(storeExpenseSql, new { monthStr });
+ var storeExpenseDict = storeExpenseData
+ .Where(x => x.StoreId != null)
+ .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0));
+
+ // 1.12 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0)
+ var laundryCostSql = $@"
+ SELECT
+ F_StoreId as StoreId,
+ COALESCE(SUM(F_TotalPrice), 0) as LaundryAmount
+ FROM lq_laundry_flow
+ WHERE F_IsEffective = 1
+ AND F_FlowType = 0
+ AND DATE_FORMAT(F_CreateTime, '%Y%m') = @monthStr
+ GROUP BY F_StoreId";
+
+ var laundryCostData = await _db.Ado.SqlQueryAsync(laundryCostSql, new { monthStr });
+ var laundryCostDict = laundryCostData
+ .Where(x => x.StoreId != null)
+ .ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.LaundryAmount ?? 0));
+
// 2. 计算每个主任的工资
var directorSalaryList = new List();
@@ -326,31 +393,52 @@ namespace NCC.Extend
throw new Exception($"门店【{salary.StoreName ?? storeId}】的目标消耗未设置,无法计算主任工资(老店需要考核消耗)");
}
- // 2.4 计算门店业绩
+ // 2.4 计算销售业绩(开单业绩-退款业绩)
decimal billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
decimal refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0;
salary.StoreBillingPerformance = billing;
salary.StoreRefundPerformance = refund;
- salary.StoreTotalPerformance = billing - refund;
+ salary.SalesPerformance = billing - refund;
+
+ // 2.5 统计各项成本
+ // 产品物料(注意11月特殊规则已在查询时处理)
+ salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
+
+ // 合作项目成本
+ salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
+
+ // 店内支出
+ salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
+
+ // 洗毛巾费用
+ salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
+
+ // 2.6 计算毛利
+ // 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
+ salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
+
+ // 2.7 将毛利赋值给StoreTotalPerformance(用于提成计算)
+ salary.StoreTotalPerformance = salary.GrossProfit;
- // 计算业绩完成率
+ // 2.8 计算业绩完成率(基于毛利与生命线比较)
if (salary.StoreLifeline > 0)
{
- salary.PerformanceCompletionRate = salary.StoreTotalPerformance / salary.StoreLifeline;
+ salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline;
}
else
{
salary.PerformanceCompletionRate = 0;
}
- // 2.5 统计门店消耗金额
+ // 2.9 统计门店消耗金额
salary.StoreConsume = storeConsumeDict.ContainsKey(storeId) ? storeConsumeDict[storeId] : 0;
- // 2.6 统计进店消耗人数
+ // 2.10 统计进店消耗人数
salary.HeadCount = headcountDict.ContainsKey(storeId) ? headcountDict[storeId] : 0;
- // 2.7 计算考核指标(业绩、人头、消耗是否达标)
- bool performanceReached = salary.StoreTotalPerformance >= salary.StoreLifeline;
+ // 2.11 计算考核指标(业绩、人头、消耗是否达标)
+ // 业绩达标判断基于毛利
+ bool performanceReached = salary.GrossProfit >= salary.StoreLifeline;
bool headCountReached = salary.HeadCount >= salary.TargetHeadCount;
bool consumeReached = salary.StoreConsume >= salary.TargetConsume;
@@ -368,18 +456,13 @@ namespace NCC.Extend
salary.UnreachedIndicatorCount = unreachedCount;
salary.AssessmentDeduction = unreachedCount * 500m;
- // 2.8 计算底薪
- salary.BaseSalary = 3500m; // 固定底薪3500元
- salary.ActualBaseSalary = salary.BaseSalary - salary.AssessmentDeduction; // 实际底薪 = 底薪 - 考核扣款
-
- // 2.9 计算阶梯提成
- CalculateCommission(salary, isNewStore);
-
- // 2.10 考勤数据
+ // 2.12 考勤数据
+ int workingDays = 0;
if (attendanceDict.ContainsKey(directorUser.Id))
{
var attendance = attendanceDict[directorUser.Id];
- salary.WorkingDays = (int)attendance.WorkDays;
+ workingDays = (int)attendance.WorkDays;
+ salary.WorkingDays = workingDays;
salary.LeaveDays = (int)attendance.LeaveDays;
}
else
@@ -388,10 +471,44 @@ namespace NCC.Extend
salary.LeaveDays = 0;
}
- // 2.11 计算应发工资
+ // 2.13 计算底薪(按在店天数比例计算)
+ decimal baseSalaryFull = 3500m; // 固定底薪3500元
+ decimal actualBaseSalaryFull = baseSalaryFull - salary.AssessmentDeduction; // 实际底薪 = 底薪 - 考核扣款
+
+ if (daysInMonth > 0 && workingDays > 0)
+ {
+ salary.BaseSalary = baseSalaryFull / daysInMonth * workingDays;
+ salary.ActualBaseSalary = actualBaseSalaryFull / daysInMonth * workingDays;
+ }
+ else
+ {
+ salary.BaseSalary = 0;
+ salary.ActualBaseSalary = 0;
+ }
+
+ // 2.14 计算阶梯提成(先计算门店总提成,基于毛利)
+ CalculateCommission(salary, isNewStore);
+
+ // 2.15 按在店天数比例计算提成金额
+ if (daysInMonth > 0 && workingDays > 0)
+ {
+ // 提成金额按在店天数比例计算
+ salary.CommissionAmountBelowLifeline = salary.CommissionAmountBelowLifeline / daysInMonth * workingDays;
+ salary.CommissionAmountAboveLifeline = salary.CommissionAmountAboveLifeline / daysInMonth * workingDays;
+ salary.TotalCommissionAmount = salary.CommissionAmountBelowLifeline + salary.CommissionAmountAboveLifeline;
+ }
+ else
+ {
+ // 如果当月天数为0或在店天数为0,则提成为0
+ salary.CommissionAmountBelowLifeline = 0;
+ salary.CommissionAmountAboveLifeline = 0;
+ salary.TotalCommissionAmount = 0;
+ }
+
+ // 2.16 计算应发工资
salary.GrossSalary = salary.ActualBaseSalary + salary.TotalCommissionAmount;
- // 2.12 初始化扣款、补贴、奖金字段(默认值为0)
+ // 2.17 初始化扣款、补贴、奖金字段(默认值为0)
salary.MissingCard = 0;
salary.LateArrival = 0;
salary.LeaveDeduction = 0;
@@ -412,10 +529,10 @@ namespace NCC.Extend
salary.ReturnPhoneDeposit = 0;
salary.ReturnAccommodationDeposit = 0;
- // 2.13 计算实发工资
+ // 2.18 计算实发工资
salary.ActualSalary = salary.GrossSalary - salary.TotalDeduction + salary.TotalSubsidy + salary.Bonus;
- // 2.14 初始化支付相关字段
+ // 2.19 初始化支付相关字段
salary.PaidAmount = 0;
salary.PendingAmount = salary.ActualSalary;
salary.LastMonthSupplement = 0;
@@ -437,7 +554,7 @@ namespace NCC.Extend
}
///
- /// 计算阶梯提成
+ /// 计算阶梯提成(基于毛利)
///
/// 工资实体
/// 是否新店
@@ -454,7 +571,8 @@ namespace NCC.Extend
return;
}
- decimal performance = salary.StoreTotalPerformance;
+ // 提成计算基于毛利(StoreTotalPerformance存储的是毛利)
+ decimal performance = salary.StoreTotalPerformance; // 这里已经是毛利了
decimal lifeline = salary.StoreLifeline;
// 确定提成比例(根据新店/老店和门店分类)
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
index ec56504..bf5d3d2 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryService.cs
@@ -183,8 +183,23 @@ namespace NCC.Extend
CreateTime = DateTime.Now
};
- var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
- if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
+ _db.Ado.BeginTran();
+ try
+ {
+ var isOk = await _db.Insertable(inventoryEntity).ExecuteCommandAsync();
+ if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
+
+ // 计算并更新产品的平均单价(加权平均成本法)
+ // 普通入库和采购入库都需要更新平均单价
+ await UpdateProductAveragePriceAsync(input.ProductId, stockInType, purchaseUnitPrice, input.Quantity);
+
+ _db.Ado.CommitTran();
+ }
+ catch
+ {
+ _db.Ado.RollbackTran();
+ throw;
+ }
}
catch (Exception ex)
{
@@ -192,6 +207,79 @@ namespace NCC.Extend
throw NCCException.Oh($"创建失败:{ex.Message}");
}
}
+
+ ///
+ /// 更新产品的平均单价(加权平均成本法)
+ ///
+ /// 产品ID
+ /// 入库类型(1:普通入库 2:采购入库)
+ /// 入库单价(采购入库时使用采购单价,普通入库时使用产品价格)
+ /// 入库数量
+ private async Task UpdateProductAveragePriceAsync(string productId, int stockInType, decimal? incomingUnitPrice, int incomingQuantity)
+ {
+ // 获取产品信息
+ var product = await _db.Queryable()
+ .Where(x => x.Id == productId)
+ .FirstAsync();
+
+ if (product == null)
+ {
+ return;
+ }
+
+ // 计算当前可用库存数量(总库存数量 - 已领取数量)
+ // 注意:由于入库记录已经插入,需要减去本次入库的数量,得到入库前的库存数量
+ var totalInventoryQuantity = await _db.Queryable()
+ .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .SumAsync(x => (int?)x.Quantity) ?? 0;
+
+ var totalUsageQuantity = await _db.Queryable()
+ .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
+
+ // 计算入库前的可用库存数量(需要减去本次入库的数量)
+ var currentAvailableQuantity = (totalInventoryQuantity - incomingQuantity) - totalUsageQuantity;
+
+ // 确定入库单价
+ decimal incomingPrice = 0;
+ if (stockInType == 2 && incomingUnitPrice.HasValue && incomingUnitPrice.Value > 0)
+ {
+ // 采购入库:使用采购单价
+ incomingPrice = incomingUnitPrice.Value;
+ }
+ else
+ {
+ // 普通入库:使用产品价格
+ incomingPrice = product.Price;
+ }
+
+ // 计算新的平均单价
+ decimal newAveragePrice = 0;
+ if (currentAvailableQuantity <= 0)
+ {
+ // 如果当前没有可用库存,新平均单价就是入库单价
+ newAveragePrice = incomingPrice;
+ }
+ else
+ {
+ // 加权平均成本法计算
+ // 当前库存总金额 = 当前平均单价 × 当前可用库存数量
+ var currentTotalAmount = product.AveragePrice > 0 ? product.AveragePrice * currentAvailableQuantity : product.Price * currentAvailableQuantity;
+
+ // 入库金额 = 入库单价 × 入库数量
+ var incomingAmount = incomingPrice * incomingQuantity;
+
+ // 新平均单价 = (当前库存总金额 + 入库金额) / (当前可用库存数量 + 入库数量)
+ newAveragePrice = (currentTotalAmount + incomingAmount) / (currentAvailableQuantity + incomingQuantity);
+ }
+
+ // 更新产品的平均单价
+ product.AveragePrice = newAveragePrice;
+ product.UpdateTime = DateTime.Now;
+ await _db.Updateable(product)
+ .UpdateColumns(x => new { x.AveragePrice, x.UpdateTime })
+ .ExecuteCommandAsync();
+ }
#endregion
#region 更新库存信息
@@ -307,8 +395,24 @@ namespace NCC.Extend
existingInventory.UpdateUser = _userManager.UserId;
existingInventory.UpdateTime = DateTime.Now;
- var isOk = await _db.Updateable(existingInventory).ExecuteCommandAsync();
- if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
+ _db.Ado.BeginTran();
+ try
+ {
+ var isOk = await _db.Updateable(existingInventory).ExecuteCommandAsync();
+ if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1000);
+
+ // 如果数量或单价发生变化,需要重新计算平均单价
+ // 注意:更新库存时,如果数量或单价变化,需要重新计算整个产品的平均单价
+ // 因为更新操作可能改变数量或单价,所以需要基于所有有效库存重新计算
+ await RecalculateProductAveragePriceAsync(input.ProductId);
+
+ _db.Ado.CommitTran();
+ }
+ catch
+ {
+ _db.Ado.RollbackTran();
+ throw;
+ }
}
catch (Exception ex)
{
@@ -316,6 +420,90 @@ namespace NCC.Extend
throw NCCException.Oh($"更新失败:{ex.Message}");
}
}
+
+ ///
+ /// 重新计算产品的平均单价(基于所有有效库存)
+ ///
+ /// 产品ID
+ private async Task RecalculateProductAveragePriceAsync(string productId)
+ {
+ // 获取产品信息
+ var product = await _db.Queryable()
+ .Where(x => x.Id == productId)
+ .FirstAsync();
+
+ if (product == null)
+ {
+ return;
+ }
+
+ // 计算当前可用库存数量(总库存数量 - 已领取数量)
+ var totalInventoryQuantity = await _db.Queryable()
+ .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .SumAsync(x => (int?)x.Quantity) ?? 0;
+
+ var totalUsageQuantity = await _db.Queryable()
+ .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
+
+ var currentAvailableQuantity = totalInventoryQuantity - totalUsageQuantity;
+
+ if (currentAvailableQuantity <= 0)
+ {
+ // 如果没有可用库存,平均单价保持为产品价格
+ product.AveragePrice = product.Price;
+ product.UpdateTime = DateTime.Now;
+ await _db.Updateable(product)
+ .UpdateColumns(x => new { x.AveragePrice, x.UpdateTime })
+ .ExecuteCommandAsync();
+ return;
+ }
+
+ // 获取所有有效库存记录
+ var inventoryList = await _db.Queryable()
+ .Where(x => x.ProductId == productId && x.IsEffective == StatusEnum.有效.GetHashCode() && x.Quantity > 0)
+ .Select(x => new { x.Quantity, x.FinalAmount, x.PurchaseUnitPrice })
+ .ToListAsync();
+
+ decimal totalAmount = 0;
+ int totalQuantity = 0;
+
+ foreach (var inventory in inventoryList)
+ {
+ decimal unitPrice = 0;
+
+ // 优先使用 F_FinalAmount(产品最终金额)计算单价
+ if (inventory.FinalAmount.HasValue && inventory.FinalAmount.Value > 0)
+ {
+ unitPrice = inventory.FinalAmount.Value / inventory.Quantity;
+ }
+ // 其次使用 F_PurchaseUnitPrice(采购单价)
+ else if (inventory.PurchaseUnitPrice.HasValue && inventory.PurchaseUnitPrice.Value > 0)
+ {
+ unitPrice = inventory.PurchaseUnitPrice.Value;
+ }
+ // 如果都没有,使用产品价格
+ else
+ {
+ unitPrice = product.Price;
+ }
+
+ totalAmount += unitPrice * inventory.Quantity;
+ totalQuantity += inventory.Quantity;
+ }
+
+ // 计算新的平均单价(基于总库存,但只考虑可用库存部分)
+ // 这里需要按比例计算:可用库存的平均单价 = 总库存的平均单价
+ // 因为已经领取的库存已经按照当时的平均单价计价了,所以这里直接计算总库存的平均单价即可
+ decimal newAveragePrice = totalQuantity > 0 ? totalAmount / totalQuantity : product.Price;
+
+ // 更新产品的平均单价
+ product.AveragePrice = newAveragePrice;
+ product.UpdateTime = DateTime.Now;
+ await _db.Updateable(product)
+ .UpdateColumns(x => new { x.AveragePrice, x.UpdateTime })
+ .ExecuteCommandAsync();
+ }
#endregion
#region 获取库存列表
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
index 40c5ea0..6f7d9d1 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqInventoryUsageService.cs
@@ -161,29 +161,11 @@ namespace NCC.Extend
throw NCCException.Oh("产品不存在");
}
- // 计算该产品的总库存数量(所有有效库存的总和)
- var totalInventory = await _db.Queryable()
- .Where(x => x.ProductId == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
- .SumAsync(x => (int?)x.Quantity) ?? 0;
-
- // 计算该产品的已使用数量
- var totalUsage = await _db.Queryable()
- .Where(x => x.ProductId == input.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
- .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
-
- // 计算可用库存
- var availableInventory = totalInventory - totalUsage;
-
- // 检查库存数量是否足够
- if (availableInventory < input.UsageQuantity)
- {
- throw NCCException.Oh($"库存不足,当前可用库存:{availableInventory},需要数量:{input.UsageQuantity}");
- }
-
_db.Ado.BeginTran();
- // 根据商品现存的库存计算平均价格(加权平均)
- var unitPrice = await CalculateAveragePriceFromInventoryAsync(input.ProductId, product.Price);
+ // 使用产品的平均单价(加权平均成本法)
+ // 如果产品平均单价为0或未设置,则使用产品价格
+ var unitPrice = product.AveragePrice > 0 ? product.AveragePrice : product.Price;
var totalAmount = unitPrice * input.UsageQuantity;
// 创建使用记录
@@ -281,70 +263,16 @@ namespace NCC.Extend
var batchId = string.IsNullOrWhiteSpace(input.BatchId) ? YitIdHelper.NextId().ToString() : input.BatchId;
var successIds = new List();
- // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚)
- var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList();
-
- // 获取所有需要检查的产品ID
- var productIds = productGroups.Select(x => x.Key).Distinct().ToList();
-
- // 批量查询所有产品的库存信息(总库存)
- var inventoryList = await _db.Queryable().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();
- var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory));
-
- // 批量查询所有产品的已使用数量
- var usageList = await _db.Queryable().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();
- var usageMap = usageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage));
-
- // 批量查询所有产品的名称和价格
- var productDict = await _db.Queryable()
- .Where(x => productIds.Contains(x.Id))
- .Select(x => new { x.Id, x.ProductName, x.Price })
- .ToListAsync();
- var productNameMap = productDict.ToDictionary(x => x.Id, x => x.ProductName ?? "未知产品");
- var productPriceMap = productDict.ToDictionary(x => x.Id, x => x.Price);
-
- // 批量计算每个产品的平均价格(根据库存计算加权平均)
- var productAveragePriceMap = new Dictionary();
- foreach (var productId in productIds)
- {
- var defaultPrice = productPriceMap.GetValueOrDefault(productId, 0);
- var averagePrice = await CalculateAveragePriceFromInventoryAsync(productId, defaultPrice);
- productAveragePriceMap[productId] = averagePrice;
- }
-
- // 检查每个产品的库存
- foreach (var productGroup in productGroups)
- {
- var productId = productGroup.Key;
- var totalRequired = productGroup.Sum(x => x.Item.UsageQuantity);
-
- // 从字典中获取库存信息
- var totalInventory = inventoryMap.GetValueOrDefault(productId, 0);
- var totalUsage = usageMap.GetValueOrDefault(productId, 0);
- var availableInventory = totalInventory - totalUsage;
-
- // 检查库存是否足够,如果不足则直接抛出异常
- if (availableInventory < totalRequired)
- {
- var productName = productNameMap.GetValueOrDefault(productId, "未知产品");
- throw NCCException.Oh($"产品【{productName}】(ID: {productId}) 库存不足,当前可用库存:{availableInventory},需要数量:{totalRequired}");
- }
- }
-
_db.Ado.BeginTran();
try
{
- // 创建使用记录(自动计算单价和合计金额)
+ // 创建使用记录(不计算价格,价格在确认领用时计算)
var entitiesToInsert = new List();
for (int i = 0; i < input.UsageItems.Count; i++)
{
var item = input.UsageItems[i];
- // 从平均价格字典获取单价(根据库存计算的加权平均价格)
- var unitPrice = productAveragePriceMap.GetValueOrDefault(item.ProductId, 0);
- var totalAmount = unitPrice * item.UsageQuantity;
-
var usageEntity = new LqInventoryUsageEntity
{
Id = YitIdHelper.NextId().ToString(),
@@ -352,8 +280,8 @@ namespace NCC.Extend
StoreId = item.StoreId,
UsageTime = item.UsageTime,
UsageQuantity = item.UsageQuantity,
- UnitPrice = unitPrice,
- TotalAmount = totalAmount,
+ UnitPrice = 0, // 创建时不计算价格,在确认领用时计算
+ TotalAmount = 0, // 创建时不计算价格,在确认领用时计算
RelatedConsumeId = item.RelatedConsumeId,
UsageBatchId = batchId,
CreateUser = _userManager.UserId,
@@ -375,8 +303,8 @@ namespace NCC.Extend
}
}
- // 计算这一单所有商品的总价
- var batchTotalAmount = entitiesToInsert.Sum(x => x.TotalAmount);
+ // 申请总金额初始为0,在确认领用时计算
+ var batchTotalAmount = 0m;
// 创建申请记录并提交审批(审批人ID为必填)
if (string.IsNullOrWhiteSpace(input.ApproverId))
@@ -1247,15 +1175,115 @@ namespace NCC.Extend
// 获取当前用户信息
var userInfo = await _userManager.GetUserInfo();
- // 更新申请记录
- application.IsReceived = 1;
- application.ReceiveTime = DateTime.Now;
- application.ReceiveUser = userInfo.userId;
- application.UpdateUser = userInfo.userId;
- application.UpdateTime = DateTime.Now;
+ _db.Ado.BeginTran();
- var isOk = await _db.Updateable(application).ExecuteCommandAsync();
- if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1003);
+ try
+ {
+ // 1. 获取该批次的所有使用记录
+ var usageRecords = await _db.Queryable()
+ .Where(x => x.UsageBatchId == application.UsageBatchId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .ToListAsync();
+
+ if (!usageRecords.Any())
+ {
+ throw NCCException.Oh("该申请没有使用记录,无法确认领用");
+ }
+
+ // 2. 批量获取所有产品信息(包含平均单价和价格)
+ var productIds = usageRecords.Select(x => x.ProductId).Distinct().ToList();
+ var productDict = await _db.Queryable()
+ .Where(x => productIds.Contains(x.Id))
+ .Select(x => new { x.Id, x.ProductName, x.Price, x.AveragePrice })
+ .ToListAsync();
+ var productMap = productDict.ToDictionary(x => x.Id, x => new { x.ProductName, x.Price, x.AveragePrice });
+
+ // 3. 在确认领用前,验证所有产品的库存是否充足
+ // 先获取所有已确认领用的批次ID(用于计算已使用数量)
+ var receivedBatchIds = await _db.Queryable()
+ .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()
+ && x.IsReceived == 1
+ && x.UsageBatchId != application.UsageBatchId) // 排除当前批次
+ .Select(x => x.UsageBatchId)
+ .ToListAsync();
+
+ foreach (var usageRecord in usageRecords)
+ {
+ if (!productMap.ContainsKey(usageRecord.ProductId))
+ {
+ throw NCCException.Oh($"产品不存在:{usageRecord.ProductId}");
+ }
+
+ var productInfo = productMap[usageRecord.ProductId];
+
+ // 计算该产品的总库存数量(所有有效库存的总和)
+ var totalInventory = await _db.Queryable()
+ .Where(x => x.ProductId == usageRecord.ProductId && x.IsEffective == StatusEnum.有效.GetHashCode())
+ .SumAsync(x => (int?)x.Quantity) ?? 0;
+
+ // 计算该产品的已使用数量(只计算已确认领用的批次)
+ // 注意:只有已确认领用的批次才算真正领取,未确认领用的批次不应该计入已使用数量
+ var totalUsage = 0;
+ if (receivedBatchIds != null && receivedBatchIds.Any())
+ {
+ totalUsage = await _db.Queryable()
+ .Where(x => x.ProductId == usageRecord.ProductId
+ && x.IsEffective == StatusEnum.有效.GetHashCode()
+ && receivedBatchIds.Contains(x.UsageBatchId)) // 只计算已确认领用的批次
+ .SumAsync(x => (int?)x.UsageQuantity) ?? 0;
+ }
+
+ // 计算可用库存
+ var availableInventory = totalInventory - totalUsage;
+
+ // 检查库存数量是否足够
+ if (availableInventory < usageRecord.UsageQuantity)
+ {
+ throw NCCException.Oh($"产品 {productInfo.ProductName ?? usageRecord.ProductId} 库存不足,当前可用库存:{availableInventory},需要数量:{usageRecord.UsageQuantity}");
+ }
+ }
+
+ // 4. 为每个使用记录计算价格(使用产品的平均单价)
+ decimal totalAmount = 0;
+ foreach (var usageRecord in usageRecords)
+ {
+ // 获取产品信息(包含平均单价)
+ var productInfo = productMap[usageRecord.ProductId];
+
+ // 使用产品的平均单价(加权平均成本法)
+ // 如果平均单价为0或未设置,则使用产品价格
+ var unitPrice = productInfo.AveragePrice > 0 ? productInfo.AveragePrice : productInfo.Price;
+ var recordTotalAmount = unitPrice * usageRecord.UsageQuantity;
+
+ // 更新使用记录的单价和总金额
+ usageRecord.UnitPrice = unitPrice;
+ usageRecord.TotalAmount = recordTotalAmount;
+ usageRecord.UpdateUser = userInfo.userId;
+ usageRecord.UpdateTime = DateTime.Now;
+
+ totalAmount += recordTotalAmount;
+ }
+
+ // 5. 批量更新使用记录
+ await _db.Updateable(usageRecords).ExecuteCommandAsync();
+
+ // 6. 更新申请记录(包括总金额、领取信息)
+ application.TotalAmount = totalAmount;
+ application.IsReceived = 1;
+ application.ReceiveTime = DateTime.Now;
+ application.ReceiveUser = userInfo.userId;
+ application.UpdateUser = userInfo.userId;
+ application.UpdateTime = DateTime.Now;
+
+ var isOk = await _db.Updateable(application).ExecuteCommandAsync();
+ if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1003);
+
+ _db.Ado.CommitTran();
+ }
+ catch
+ {
+ _db.Ado.RollbackTran();
+ throw;
+ }
}
catch (Exception ex)
{
@@ -1460,59 +1488,6 @@ namespace NCC.Extend
.Where(x => x.UsageBatchId == application.UsageBatchId && x.IsEffective == StatusEnum.有效.GetHashCode())
.ToListAsync();
- // 按产品ID分组,批量验证库存(在事务外先检查,避免不必要的回滚)
- var productGroups = input.UsageItems.Select((item, index) => new { Item = item, Index = index }).GroupBy(x => x.Item.ProductId).ToList();
-
- // 获取所有需要检查的产品ID
- var productIds = productGroups.Select(x => x.Key).Distinct().ToList();
-
- // 批量查询所有产品的库存信息(总库存)
- var inventoryList = await _db.Queryable()
- .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();
- var inventoryMap = inventoryList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalInventory));
-
- // 批量查询所有产品的已使用数量(排除当前批次的使用记录,因为我们要替换它们)
- var oldUsageRecordIds = oldUsageRecords.Select(x => x.Id).ToList();
- var allUsageList = await _db.Queryable()
- .Where(x => productIds.Contains(x.ProductId) && x.IsEffective == StatusEnum.有效.GetHashCode())
- .WhereIF(oldUsageRecordIds.Any(), x => !oldUsageRecordIds.Contains(x.Id)) // 排除当前批次的使用记录
- .GroupBy(x => x.ProductId)
- .Select(x => new { ProductId = x.ProductId, TotalUsage = SqlFunc.AggregateSum(x.UsageQuantity) })
- .ToListAsync();
- var usageMap = allUsageList.ToDictionary(x => x.ProductId, x => Convert.ToInt32(x.TotalUsage));
-
- // 批量查询所有产品的名称和价格
- var productDict = await _db.Queryable()
- .Where(x => productIds.Contains(x.Id))
- .Select(x => new { x.Id, x.ProductName, x.Price })
- .ToListAsync();
- var productInfoMap = productDict.ToDictionary(k => k.Id, v => new { v.ProductName, v.Price });
-
- // 验证每个产品的库存是否充足
- foreach (var group in productGroups)
- {
- var productId = group.Key;
- var totalNewQuantity = group.Sum(x => x.Item.UsageQuantity);
-
- if (!productInfoMap.ContainsKey(productId))
- {
- throw NCCException.Oh($"产品ID {productId} 不存在");
- }
-
- var totalInventory = inventoryMap.GetValueOrDefault(productId, 0);
- var totalUsage = usageMap.GetValueOrDefault(productId, 0);
- var availableInventory = totalInventory - totalUsage;
-
- if (availableInventory < totalNewQuantity)
- {
- var productName = productInfoMap[productId].ProductName;
- throw NCCException.Oh($"产品 {productName} 库存不足,当前可用库存:{availableInventory},需要数量:{totalNewQuantity}");
- }
- }
-
_db.Ado.BeginTran();
try
@@ -1529,23 +1504,10 @@ namespace NCC.Extend
await _db.Updateable(oldUsageRecords).ExecuteCommandAsync();
}
- // 2. 批量计算新使用记录的平均价格(根据库存计算的加权平均价格)
- var productAveragePriceMap = new Dictionary();
- foreach (var productId in productIds)
- {
- var product = productInfoMap[productId];
- var averagePrice = await CalculateAveragePriceFromInventoryAsync(productId, product.Price);
- productAveragePriceMap[productId] = averagePrice;
- }
-
- // 3. 创建新的使用记录
+ // 2. 创建新的使用记录(不计算价格,价格在确认领用时计算)
var entitiesToInsert = new List();
foreach (var item in input.UsageItems)
{
- // 从平均价格字典获取单价(根据库存计算的加权平均价格)
- var unitPrice = productAveragePriceMap.GetValueOrDefault(item.ProductId, 0);
- var totalAmount = unitPrice * item.UsageQuantity;
-
var usageEntity = new LqInventoryUsageEntity
{
Id = YitIdHelper.NextId().ToString(),
@@ -1553,8 +1515,8 @@ namespace NCC.Extend
StoreId = item.StoreId,
UsageTime = item.UsageTime,
UsageQuantity = item.UsageQuantity,
- UnitPrice = unitPrice,
- TotalAmount = totalAmount,
+ UnitPrice = 0, // 修改时不计算价格,在确认领用时计算
+ TotalAmount = 0, // 修改时不计算价格,在确认领用时计算
RelatedConsumeId = item.RelatedConsumeId,
UsageBatchId = application.UsageBatchId, // 使用相同的批次ID
CreateUser = _userManager.UserId,
@@ -1571,9 +1533,8 @@ namespace NCC.Extend
await _db.Insertable(entitiesToInsert).ExecuteCommandAsync();
}
- // 4. 重新计算申请总金额
- var newTotalAmount = entitiesToInsert.Sum(x => x.TotalAmount);
- application.TotalAmount = newTotalAmount;
+ // 3. 申请总金额重置为0,在确认领用时计算
+ application.TotalAmount = 0;
application.UpdateUser = _userManager.UserId;
application.UpdateTime = DateTime.Now;
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs
new file mode 100644
index 0000000..345ed02
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqMajorProjectDirectorSalaryService.cs
@@ -0,0 +1,407 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using NCC.Common.Filter;
+using NCC.Common.Helper;
+using NCC.Dependency;
+using NCC.DynamicApiController;
+using NCC.Extend.Entitys.Dto.LqMajorProjectDirectorSalary;
+using NCC.Extend.Entitys.lq_attendance_summary;
+using NCC.Extend.Entitys.lq_hytk_hytk;
+using NCC.Extend.Entitys.lq_kd_kdjlb;
+using NCC.Extend.Entitys.lq_md_target;
+using NCC.Extend.Entitys.lq_mdxx;
+using NCC.Extend.Entitys.lq_major_project_director_salary_statistics;
+using NCC.System.Entitys.Permission;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Yitter.IdGenerator;
+using Newtonsoft.Json;
+
+namespace NCC.Extend
+{
+ ///
+ /// 大项目主管薪酬服务
+ ///
+ [ApiDescriptionSettings(Tag = "大项目主管薪酬服务", Name = "LqMajorProjectDirectorSalary", Order = 306)]
+ [Route("api/Extend/[controller]")]
+ public class LqMajorProjectDirectorSalaryService : IDynamicApiController, ITransient
+ {
+ private readonly ISqlSugarClient _db;
+
+ ///
+ /// 初始化一个类型的新实例
+ ///
+ public LqMajorProjectDirectorSalaryService(ISqlSugarClient db)
+ {
+ _db = db;
+ }
+
+ ///
+ /// 获取大项目主管工资列表
+ ///
+ /// 查询参数
+ /// 大项目主管工资分页列表
+ [HttpGet("major-project-director")]
+ public async Task GetMajorProjectDirectorSalaryList([FromQuery] MajorProjectDirectorSalaryInput input)
+ {
+ var monthStr = $"{input.Year}{input.Month:D2}";
+
+ // 1. 检查当月是否已生成工资数据
+ var exists = await _db.Queryable()
+ .AnyAsync(x => x.StatisticsMonth == monthStr);
+
+ // 2. 如果没有数据,则进行计算
+ if (!exists)
+ {
+ await CalculateMajorProjectDirectorSalary(input.Year, input.Month);
+ }
+
+ // 3. 查询数据
+ var query = _db.Queryable()
+ .Where(x => x.StatisticsMonth == monthStr);
+
+ if (!string.IsNullOrEmpty(input.Position))
+ {
+ query = query.Where(x => x.Position == input.Position);
+ }
+
+ if (!string.IsNullOrEmpty(input.Keyword))
+ {
+ query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeAccount.Contains(input.Keyword));
+ }
+
+ var list = await query.Select(x => new MajorProjectDirectorSalaryOutput
+ {
+ Id = x.Id,
+ StatisticsMonth = x.StatisticsMonth,
+ Position = x.Position,
+ EmployeeName = x.EmployeeName,
+ EmployeeId = x.EmployeeId,
+ EmployeeAccount = x.EmployeeAccount,
+ IsTerminated = x.IsTerminated,
+ StoreDetail = x.StoreDetail,
+ TotalPerformance = x.TotalPerformance,
+ BillingAmount = x.BillingAmount,
+ RefundAmount = x.RefundAmount,
+ BaseSalary = x.BaseSalary,
+ CommissionRate = x.CommissionRate,
+ CommissionAmount = x.CommissionAmount,
+ WorkingDays = x.WorkingDays,
+ LeaveDays = x.LeaveDays,
+ CalculatedGrossSalary = x.CalculatedGrossSalary,
+ FinalGrossSalary = x.FinalGrossSalary,
+ MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
+ MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
+ LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
+ LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
+ TotalSubsidy = x.TotalSubsidy,
+ MissingCard = x.MissingCard,
+ LateArrival = x.LateArrival,
+ LeaveDeduction = x.LeaveDeduction,
+ SocialInsuranceDeduction = x.SocialInsuranceDeduction,
+ RewardDeduction = x.RewardDeduction,
+ AccommodationDeduction = x.AccommodationDeduction,
+ StudyPeriodDeduction = x.StudyPeriodDeduction,
+ WorkClothesDeduction = x.WorkClothesDeduction,
+ TotalDeduction = x.TotalDeduction,
+ Bonus = x.Bonus,
+ ReturnPhoneDeposit = x.ReturnPhoneDeposit,
+ ReturnAccommodationDeposit = x.ReturnAccommodationDeposit,
+ ActualSalary = x.ActualSalary,
+ MonthlyPaymentStatus = x.MonthlyPaymentStatus,
+ PaidAmount = x.PaidAmount,
+ PendingAmount = x.PendingAmount,
+ LastMonthSupplement = x.LastMonthSupplement,
+ MonthlyTotalPayment = x.MonthlyTotalPayment,
+ IsLocked = x.IsLocked,
+ UpdateTime = x.UpdateTime
+ })
+ .ToPagedListAsync(input.currentPage, input.pageSize);
+
+ return PageResult.SqlSugarPageResult(list);
+ }
+
+ ///
+ /// 计算大项目主管工资
+ ///
+ /// 年份
+ /// 月份
+ ///
+ [HttpPost("calculate/major-project-director")]
+ public async Task CalculateMajorProjectDirectorSalary(int year, int month)
+ {
+ var startDate = new DateTime(year, month, 1);
+ var endDate = startDate.AddMonths(1).AddDays(-1);
+ var monthStr = $"{year}{month:D2}";
+
+ // 1. 获取基础数据
+
+ // 1.1 先从BASE_ORGANIZE表查找组织名称包含"大项目一部"或"大项目二部"的组织
+ var majorProjectOrganizeList = await _db.Queryable()
+ .Where(x => x.FullName != null && (x.FullName.Contains("大项目一部") || x.FullName.Contains("大项目二部"))
+ && x.DeleteMark == null && x.EnabledMark == 1)
+ .Select(x => new { x.Id, x.FullName })
+ .ToListAsync();
+
+ if (!majorProjectOrganizeList.Any())
+ {
+ // 如果没有找到大项目部组织,直接返回
+ return;
+ }
+
+ var majorProjectOrganizeIds = majorProjectOrganizeList.Select(x => x.Id).ToList();
+ var majorProjectOrganizeDict = majorProjectOrganizeList.ToDictionary(x => x.Id, x => x.FullName);
+
+ // 1.2 从BASE_USER表查询岗位为"主管"且组织ID在大项目一部或大项目二部的员工
+ var majorProjectDirectorUserList = await _db.Queryable()
+ .Where(x => x.Gw == "主管"
+ && majorProjectOrganizeIds.Contains(x.OrganizeId)
+ && x.DeleteMark == null && x.EnabledMark == 1)
+ .Select(x => new { x.Id, x.RealName, x.Account, x.Gw, x.OrganizeId, x.IsOnJob })
+ .ToListAsync();
+
+ if (!majorProjectDirectorUserList.Any())
+ {
+ // 如果没有大项目主管员工,直接返回
+ return;
+ }
+
+ // 1.3 从lq_md_target表获取管理的门店(按月份和大项目部组织ID)
+ var targetList = await _db.Queryable()
+ .Where(x => x.Month == monthStr
+ && majorProjectOrganizeIds.Contains(x.MajorProjectDepartment))
+ .Select(x => new { x.StoreId, x.MajorProjectDepartment })
+ .ToListAsync();
+
+ // 1.4 按大项目主管ID分组,获取每个大项目主管管理的门店
+ var directorStoreDict = new Dictionary>();
+ foreach (var directorUser in majorProjectDirectorUserList)
+ {
+ var directorId = directorUser.Id;
+ var directorOrganizeId = directorUser.OrganizeId;
+
+ // 从lq_md_target表中查找该大项目主管管理的门店
+ var managedStores = targetList
+ .Where(x => x.MajorProjectDepartment == directorOrganizeId)
+ .Select(x => x.StoreId)
+ .Distinct()
+ .ToList();
+
+ directorStoreDict[directorId] = managedStores;
+ }
+
+ // 1.5 门店信息 (lq_mdxx)
+ var storeList = await _db.Queryable().ToListAsync();
+ var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x);
+
+ // 1.6 考勤数据 (lq_attendance_summary)
+ var attendanceList = await _db.Queryable()
+ .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
+ .ToListAsync();
+ var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
+
+ // 1.7 获取所有管理的门店ID列表(用于后续查询)
+ var allManagedStoreIds = directorStoreDict.Values.SelectMany(x => x).Distinct().ToList();
+
+ // 1.8 按大项目主管和门店分组统计(用于生成门店明细JSON和汇总数据)
+ var storeDetailDict = new Dictionary>();
+
+ // 按门店统计开单和退卡金额(如果有管理的门店)
+ if (allManagedStoreIds.Any())
+ {
+ foreach (var storeId in allManagedStoreIds)
+ {
+ // 该门店的开单金额
+ var storeBillingAmount = await _db.Queryable()
+ .Where(x => x.IsEffective == 1
+ && x.Djmd == storeId
+ && x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1))
+ .SumAsync(x => (decimal?)x.Sfyj) ?? 0m;
+
+ // 该门店的退卡金额(优先使用ActualRefundAmount,如果没有则使用Tkje)
+ var storeRefundAmount = await _db.Queryable()
+ .Where(x => x.IsEffective == 1
+ && x.Md == storeId
+ && x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1))
+ .SumAsync(x => (decimal?)(x.ActualRefundAmount ?? x.Tkje ?? 0)) ?? 0m;
+
+ // 该门店的净总业绩
+ var storeTotalPerformance = storeBillingAmount - storeRefundAmount;
+
+ // 找到该门店归属的大项目主管
+ var storeTarget = targetList.FirstOrDefault(x => x.StoreId == storeId);
+ if (storeTarget != null)
+ {
+ var directorOrganizeId = storeTarget.MajorProjectDepartment;
+ var directorsOfStore = majorProjectDirectorUserList
+ .Where(x => x.OrganizeId == directorOrganizeId)
+ .Select(x => x.Id)
+ .ToList();
+
+ foreach (var directorId in directorsOfStore)
+ {
+ if (!storeDetailDict.ContainsKey(directorId))
+ {
+ storeDetailDict[directorId] = new Dictionary();
+ }
+
+ var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : "";
+ storeDetailDict[directorId][storeId] = new StoreDetailItem
+ {
+ StoreId = storeId,
+ StoreName = storeName,
+ BillingAmount = storeBillingAmount,
+ RefundAmount = storeRefundAmount,
+ TotalPerformance = storeTotalPerformance
+ };
+ }
+ }
+ }
+ }
+
+ // 2. 按大项目主管聚合数据
+ var directorStats = new Dictionary();
+
+ foreach (var directorUser in majorProjectDirectorUserList)
+ {
+ var directorId = directorUser.Id;
+
+ // 获取该大项目主管管理的门店列表
+ var managedStores = directorStoreDict.ContainsKey(directorId) ? directorStoreDict[directorId] : new List();
+
+ // 2.1 创建工资统计对象
+ // 岗位使用组织名称(大项目一部/大项目二部)
+ var position = majorProjectOrganizeDict.ContainsKey(directorUser.OrganizeId)
+ ? majorProjectOrganizeDict[directorUser.OrganizeId]
+ : "";
+
+ var salary = new LqMajorProjectDirectorSalaryStatisticsEntity
+ {
+ Id = YitIdHelper.NextId().ToString(),
+ StatisticsMonth = monthStr,
+ EmployeeId = directorId,
+ Position = position,
+ EmployeeName = directorUser.RealName ?? "",
+ EmployeeAccount = directorUser.Account ?? "",
+ IsTerminated = directorUser.IsOnJob == 0 ? 1 : 0,
+ CreateTime = DateTime.Now,
+ UpdateTime = DateTime.Now,
+ IsLocked = 0
+ };
+
+ // 2.2 考勤数据
+ var attendance = attendanceDict.ContainsKey(directorId) ? attendanceDict[directorId] : null;
+ salary.WorkingDays = attendance?.WorkDays ?? 0;
+ salary.LeaveDays = attendance?.LeaveDays ?? 0;
+
+ // 2.3 计算底薪(固定3500元)
+ salary.BaseSalary = 3500m;
+
+ // 2.4 统计该大项目主管管理的所有门店的总业绩(开单-退卡)总和
+ decimal totalBillingAmount = 0m;
+ decimal totalRefundAmount = 0m;
+ var storeDetails = new List();
+
+ if (managedStores.Any() && storeDetailDict.ContainsKey(directorId))
+ {
+ foreach (var storeId in managedStores)
+ {
+ if (storeDetailDict[directorId].ContainsKey(storeId))
+ {
+ var storeDetail = storeDetailDict[directorId][storeId];
+ totalBillingAmount += storeDetail.BillingAmount;
+ totalRefundAmount += storeDetail.RefundAmount;
+ storeDetails.Add(storeDetail);
+ }
+ }
+ }
+
+ salary.BillingAmount = totalBillingAmount;
+ salary.RefundAmount = totalRefundAmount;
+ salary.TotalPerformance = totalBillingAmount - totalRefundAmount;
+
+ // 2.5 保存门店明细(JSON格式)
+ salary.StoreDetail = JsonConvert.SerializeObject(storeDetails);
+
+ // 2.6 计算提成(分段方式)
+ var commission = CalculateCommission(salary.TotalPerformance);
+ salary.CommissionAmount = commission.Amount;
+ salary.CommissionRate = commission.Rate;
+
+ // 2.7 计算应发工资
+ salary.CalculatedGrossSalary = salary.BaseSalary + salary.CommissionAmount;
+ salary.FinalGrossSalary = salary.CalculatedGrossSalary;
+
+ // 2.8 初始化其他字段
+ salary.MonthlyPaymentStatus = "未发放";
+ salary.ActualSalary = salary.FinalGrossSalary; // 默认实发工资等于应发工资
+
+ directorStats[directorId] = salary;
+ }
+
+ // 3. 保存数据
+ if (directorStats.Any())
+ {
+ // 先删除当月旧数据 (防止重复)
+ await _db.Deleteable()
+ .Where(x => x.StatisticsMonth == monthStr)
+ .ExecuteCommandAsync();
+
+ await _db.Insertable(directorStats.Values.ToList()).ExecuteCommandAsync();
+ }
+ }
+
+ ///
+ /// 计算提成(分段方式)
+ ///
+ /// 总业绩
+ /// 提成金额和比例
+ private (decimal Amount, decimal? Rate) CalculateCommission(decimal totalPerformance)
+ {
+ if (totalPerformance <= 0)
+ {
+ return (0m, null);
+ }
+
+ decimal commissionAmount = 0m;
+ decimal? rate = null;
+
+ if (totalPerformance <= 500000m)
+ {
+ // ≤ 50万:无提成
+ commissionAmount = 0m;
+ rate = null;
+ }
+ else if (totalPerformance <= 700000m)
+ {
+ // 50万 < 总业绩 ≤ 70万:1%提成
+ commissionAmount = totalPerformance * 0.01m;
+ rate = 1.00m;
+ }
+ else
+ {
+ // > 70万:1.5%提成
+ commissionAmount = totalPerformance * 0.015m;
+ rate = 1.50m;
+ }
+
+ return (commissionAmount, rate);
+ }
+
+ ///
+ /// 门店明细项(用于JSON序列化)
+ ///
+ private class StoreDetailItem
+ {
+ public string StoreId { get; set; }
+ public string StoreName { get; set; }
+ public decimal BillingAmount { get; set; }
+ public decimal RefundAmount { get; set; }
+ public decimal TotalPerformance { get; set; }
+ }
+ }
+}
+
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs
index 7f1a9ee..1b93389 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqProductService.cs
@@ -226,6 +226,7 @@ namespace NCC.Extend
id = x.Id,
productName = x.ProductName,
price = x.Price,
+ averagePrice = x.AveragePrice,
productCategory = x.ProductCategory,
departmentId = x.DepartmentId,
departmentName = SqlFunc.Subqueryable().Where(y => y.Id == x.DepartmentId).Select(y => y.FullName),
@@ -346,6 +347,7 @@ namespace NCC.Extend
id = product.Id,
productName = product.ProductName,
price = product.Price,
+ averagePrice = product.AveragePrice,
productCategory = product.ProductCategory,
departmentId = product.DepartmentId,
departmentName = "",
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
index 5ce9e32..7b51811 100644
--- a/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqReimbursementApplicationService.cs
@@ -1,4 +1,4 @@
-using NCC.Common.Core.Manager;
+using NCC.Common.Core.Manager;
using NCC.Common.Enum;
using NCC.Common.Extension;
using NCC.Common.Filter;
@@ -906,6 +906,7 @@ namespace NCC.Extend.LqReimbursementApplication
}
// 更新现有记录(包括空字符串、"待审批"、"退回"的情况)
+ // 注意:只更新审批结果和意见,不更新审批人信息(ApproverId和ApproverName在创建时已确定)
existingRecord.ApprovalResult = result;
existingRecord.ApprovalOpinion = opinion;
existingRecord.ApprovalTime = DateTime.Now;
@@ -1050,6 +1051,8 @@ namespace NCC.Extend.LqReimbursementApplication
.Select(x => x.UserId)
.ToListAsync();
+ // 查询所有"通过"的审批记录,包括当前用户刚审批的记录
+ // 注意:由于在事务中,需要确保查询包含当前刚更新的记录
var approvedUsers = await _db.Queryable()
.Where(x => x.ApplicationId == id
&& x.NodeOrder == entity.CurrentNodeOrder
@@ -1058,7 +1061,13 @@ namespace NCC.Extend.LqReimbursementApplication
.Distinct()
.ToListAsync();
- if (approvers.Count == approvedUsers.Count)
+ // 如果当前用户刚审批通过,但查询结果中还没有包含,手动添加
+ if (result == "通过" && !approvedUsers.Contains(userInfo.userId))
+ {
+ approvedUsers.Add(userInfo.userId);
+ }
+
+ if (approvers.Count == approvedUsers.Count && approvers.Count > 0)
{
// 所有人都已通过
shouldMoveToNext = true;
@@ -1506,8 +1515,8 @@ namespace NCC.Extend.LqReimbursementApplication
// 查询本月已审核通过的报销申请
var applications = await _db.Queryable()
.Where(x => (x.ApprovalStatus ?? x.ApproveStatus) == "已通过")
- .Where(x => x.ApplicationTime.HasValue &&
- x.ApplicationTime.Value.Year == queryYear &&
+ .Where(x => x.ApplicationTime.HasValue &&
+ x.ApplicationTime.Value.Year == queryYear &&
x.ApplicationTime.Value.Month == int.Parse(queryMonth))
.ToListAsync();
@@ -1543,7 +1552,7 @@ namespace NCC.Extend.LqReimbursementApplication
foreach (var app in applications)
{
var appPurchaseRecords = purchaseRecords.Where(x => x.ApplicationId == app.Id).ToList();
-
+
if (appPurchaseRecords.Any())
{
// 每个购买记录作为一行
diff --git a/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs b/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs
new file mode 100644
index 0000000..1cc7903
--- /dev/null
+++ b/netcore/src/Modularity/Extend/NCC.Extend/LqTechGeneralManagerSalaryService.cs
@@ -0,0 +1,545 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using NCC.Common.Filter;
+using NCC.Common.Helper;
+using NCC.Dependency;
+using NCC.DynamicApiController;
+using NCC.Extend.Entitys.Dto.LqTechGeneralManagerSalary;
+using NCC.Extend.Entitys.lq_attendance_summary;
+using NCC.Extend.Entitys.lq_hytk_hytk;
+using NCC.Extend.Entitys.lq_hytk_mx;
+using NCC.Extend.Entitys.lq_kd_kdjlb;
+using NCC.Extend.Entitys.lq_kd_pxmx;
+using NCC.Extend.Entitys.lq_md_general_manager_lifeline;
+using NCC.Extend.Entitys.lq_mdxx;
+using NCC.Extend.Entitys.lq_tech_general_manager_salary_statistics;
+using NCC.Extend.Entitys.lq_xmzl;
+using NCC.System.Entitys.Permission;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Yitter.IdGenerator;
+using Newtonsoft.Json;
+
+namespace NCC.Extend
+{
+ ///
+ /// 科技部总经理薪酬服务
+ ///
+ [ApiDescriptionSettings(Tag = "科技部总经理薪酬服务", Name = "LqTechGeneralManagerSalary", Order = 305)]
+ [Route("api/Extend/[controller]")]
+ public class LqTechGeneralManagerSalaryService : IDynamicApiController, ITransient
+ {
+ private readonly ISqlSugarClient _db;
+
+ ///
+ /// 初始化一个类型的新实例
+ ///
+ public LqTechGeneralManagerSalaryService(ISqlSugarClient db)
+ {
+ _db = db;
+ }
+
+ ///
+ /// 获取科技部总经理工资列表
+ ///
+ /// 查询参数
+ /// 科技部总经理工资分页列表
+ [HttpGet("tech-general-manager")]
+ public async Task GetTechGeneralManagerSalaryList([FromQuery] TechGeneralManagerSalaryInput input)
+ {
+ var monthStr = $"{input.Year}{input.Month:D2}";
+
+ // 1. 检查当月是否已生成工资数据
+ var exists = await _db.Queryable()
+ .AnyAsync(x => x.StatisticsMonth == monthStr);
+
+ // 2. 如果没有数据,则进行计算
+ if (!exists)
+ {
+ await CalculateTechGeneralManagerSalary(input.Year, input.Month);
+ }
+
+ // 3. 查询数据
+ var query = _db.Queryable()
+ .Where(x => x.StatisticsMonth == monthStr);
+
+ if (!string.IsNullOrEmpty(input.Position))
+ {
+ query = query.Where(x => x.Position == input.Position);
+ }
+
+ if (!string.IsNullOrEmpty(input.Keyword))
+ {
+ query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeAccount.Contains(input.Keyword));
+ }
+
+ var list = await query.Select(x => new TechGeneralManagerSalaryOutput
+ {
+ Id = x.Id,
+ StatisticsMonth = x.StatisticsMonth,
+ Position = x.Position,
+ EmployeeName = x.EmployeeName,
+ EmployeeId = x.EmployeeId,
+ EmployeeAccount = x.EmployeeAccount,
+ IsTerminated = x.IsTerminated,
+ StoreDetail = x.StoreDetail,
+ TraceabilityAmount = x.TraceabilityAmount,
+ CellAmount = x.CellAmount,
+ BaseSalary = x.BaseSalary,
+ TraceabilityCommissionRate = x.TraceabilityCommissionRate,
+ TraceabilityCommissionAmount = x.TraceabilityCommissionAmount,
+ CellCommissionRate = x.CellCommissionRate,
+ CellCommissionAmount = x.CellCommissionAmount,
+ TotalCommission = x.TotalCommission,
+ WorkingDays = x.WorkingDays,
+ LeaveDays = x.LeaveDays,
+ CalculatedGrossSalary = x.CalculatedGrossSalary,
+ FinalGrossSalary = x.FinalGrossSalary,
+ MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
+ MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
+ LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
+ LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
+ TotalSubsidy = x.TotalSubsidy,
+ MissingCard = x.MissingCard,
+ LateArrival = x.LateArrival,
+ LeaveDeduction = x.LeaveDeduction,
+ SocialInsuranceDeduction = x.SocialInsuranceDeduction,
+ RewardDeduction = x.RewardDeduction,
+ AccommodationDeduction = x.AccommodationDeduction,
+ StudyPeriodDeduction = x.StudyPeriodDeduction,
+ WorkClothesDeduction = x.WorkClothesDeduction,
+ TotalDeduction = x.TotalDeduction,
+ Bonus = x.Bonus,
+ ReturnPhoneDeposit = x.ReturnPhoneDeposit,
+ ReturnAccommodationDeposit = x.ReturnAccommodationDeposit,
+ ActualSalary = x.ActualSalary,
+ MonthlyPaymentStatus = x.MonthlyPaymentStatus,
+ PaidAmount = x.PaidAmount,
+ PendingAmount = x.PendingAmount,
+ LastMonthSupplement = x.LastMonthSupplement,
+ MonthlyTotalPayment = x.MonthlyTotalPayment,
+ IsLocked = x.IsLocked,
+ UpdateTime = x.UpdateTime
+ })
+ .ToPagedListAsync(input.currentPage, input.pageSize);
+
+ return PageResult.SqlSugarPageResult(list);
+ }
+
+ ///
+ /// 计算科技部总经理工资
+ ///
+ /// 年份
+ /// 月份
+ ///
+ [HttpPost("calculate/tech-general-manager")]
+ public async Task CalculateTechGeneralManagerSalary(int year, int month)
+ {
+ var startDate = new DateTime(year, month, 1);
+ var endDate = startDate.AddMonths(1).AddDays(-1);
+ var monthStr = $"{year}{month:D2}";
+
+ // 1. 获取基础数据
+
+ // 1.1 先从BASE_ORGANIZE表查找组织名称包含"科技一部"或"科技二部"的组织
+ var techOrganizeList = await _db.Queryable()
+ .Where(x => x.FullName != null && (x.FullName.Contains("科技一部") || x.FullName.Contains("科技二部"))
+ && x.DeleteMark == null && x.EnabledMark == 1)
+ .Select(x => new { x.Id, x.FullName })
+ .ToListAsync();
+
+ if (!techOrganizeList.Any())
+ {
+ // 如果没有找到科技部组织,直接返回
+ return;
+ }
+
+ var techOrganizeIds = techOrganizeList.Select(x => x.Id).ToList();
+ var techOrganizeDict = techOrganizeList.ToDictionary(x => x.Id, x => x.FullName);
+
+ // 1.2 从BASE_USER表查询岗位为"总经理"且组织ID在科技一部或科技二部的员工
+ var techGeneralManagerUserList = await _db.Queryable()
+ .Where(x => x.Gw == "总经理"
+ && techOrganizeIds.Contains(x.OrganizeId)
+ && x.DeleteMark == null && x.EnabledMark == 1)
+ .Select(x => new { x.Id, x.RealName, x.Account, x.Gw, x.OrganizeId, x.IsOnJob })
+ .ToListAsync();
+
+ if (!techGeneralManagerUserList.Any())
+ {
+ // 如果没有科技部总经理员工,直接返回
+ return;
+ }
+
+ if (!techGeneralManagerUserList.Any())
+ {
+ // 如果没有科技部总经理员工,直接返回
+ return;
+ }
+
+ // 1.3 获取科技部总经理归属信息(从lq_md_general_manager_lifeline表)
+ // 通过门店的科技部组织ID(kjb字段)找到科技一部或科技二部管理的门店
+ // 然后在lifeline表中找到这些门店的记录,这些记录对应的总经理就是科技部总经理
+ var lifelineList = await _db.Queryable(
+ (lifeline, store) => lifeline.StoreId == store.Id)
+ .Where((lifeline, store) =>
+ lifeline.Month == monthStr
+ && techOrganizeIds.Contains(store.Kjb))
+ .Select((lifeline, store) => lifeline)
+ .ToListAsync();
+
+ // 1.4 获取科技一部和科技二部管理的门店(通过门店的kjb字段)
+ var techManagedStoreIds = await _db.Queryable()
+ .Where(x => techOrganizeIds.Contains(x.Kjb))
+ .Select(x => x.Id)
+ .ToListAsync();
+
+ // 1.5 按科技部总经理ID分组,获取每个科技部总经理管理的门店
+ // 科技部总经理管理的门店 = 科技一部/科技二部管理的所有门店(通过门店的kjb字段确定)
+ var managerStoreDict = new Dictionary>();
+ foreach (var managerUser in techGeneralManagerUserList)
+ {
+ var managerId = managerUser.Id;
+ var managerOrganizeId = managerUser.OrganizeId;
+
+ // 如果该总经理属于科技一部,则管理所有科技一部的门店
+ // 如果该总经理属于科技二部,则管理所有科技二部的门店
+ var managedStores = await _db.Queryable()
+ .Where(x => x.Kjb == managerOrganizeId)
+ .Select(x => x.Id)
+ .ToListAsync();
+
+ managerStoreDict[managerId] = managedStores;
+ }
+
+ // 1.4 门店信息 (lq_mdxx)
+ var storeList = await _db.Queryable().ToListAsync();
+ var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x);
+
+ // 1.6 考勤数据 (lq_attendance_summary)
+ var attendanceList = await _db.Queryable()
+ .Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
+ .ToListAsync();
+ var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
+
+ // 1.7 获取所有管理的门店ID列表(用于后续查询,如果没有管理的门店,则为空列表)
+ var allManagedStoreIds = managerStoreDict.Values.SelectMany(x => x).Distinct().ToList();
+
+ // 1.8 按科技部总经理和门店分组统计(用于生成门店明细JSON和汇总数据)
+ var storeDetailDict = new Dictionary>();
+
+ // 按门店统计溯源和Cell金额(如果有管理的门店)
+ if (allManagedStoreIds.Any())
+ {
+ foreach (var storeId in allManagedStoreIds)
+ {
+ // 该门店的开单溯源金额
+ var storeTraceabilityBilling = await _db.Queryable(
+ (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id)
+ .Where((pxmx, billing, item) =>
+ pxmx.IsEffective == 1
+ && billing.IsEffective == 1
+ && item.IsEffective == 1
+ && (pxmx.BeautyType == "溯源系统" || pxmx.BeautyType == "溯源"
+ || item.BeautyType == "溯源系统" || item.BeautyType == "溯源")
+ && billing.Djmd == storeId
+ && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1))
+ .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m;
+
+ // 该门店的退卡溯源金额
+ var storeTraceabilityRefund = await _db.Queryable(
+ (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id)
+ .Where((tkmx, refund, item) =>
+ tkmx.IsEffective == 1
+ && refund.IsEffective == 1
+ && item.IsEffective == 1
+ && (tkmx.BeautyType == "溯源系统" || tkmx.BeautyType == "溯源"
+ || item.BeautyType == "溯源系统" || item.BeautyType == "溯源")
+ && refund.Md == storeId
+ && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1))
+ .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m;
+
+ // 该门店的开单Cell金额
+ var storeCellBilling = await _db.Queryable(
+ (pxmx, billing, item) => pxmx.Glkdbh == billing.Id && pxmx.Px == item.Id)
+ .Where((pxmx, billing, item) =>
+ pxmx.IsEffective == 1
+ && billing.IsEffective == 1
+ && item.IsEffective == 1
+ && (pxmx.BeautyType == "cell" || pxmx.BeautyType == "Cell"
+ || item.BeautyType == "cell" || item.BeautyType == "Cell")
+ && billing.Djmd == storeId
+ && billing.Kdrq >= startDate && billing.Kdrq <= endDate.AddDays(1))
+ .SumAsync((pxmx, billing, item) => (decimal?)pxmx.ActualPrice) ?? 0m;
+
+ // 该门店的退卡Cell金额
+ var storeCellRefund = await _db.Queryable(
+ (tkmx, refund, item) => tkmx.RefundInfoId == refund.Id && tkmx.Px == item.Id)
+ .Where((tkmx, refund, item) =>
+ tkmx.IsEffective == 1
+ && refund.IsEffective == 1
+ && item.IsEffective == 1
+ && (tkmx.BeautyType == "cell" || tkmx.BeautyType == "Cell"
+ || item.BeautyType == "cell" || item.BeautyType == "Cell")
+ && refund.Md == storeId
+ && refund.Tksj >= startDate && refund.Tksj <= endDate.AddDays(1))
+ .SumAsync((tkmx, refund, item) => (decimal?)tkmx.Tkje) ?? 0m;
+
+ // 获取该门店属于哪些科技部总经理
+ // 通过门店的kjb字段确定:如果门店的kjb等于科技一部的组织ID,则该门店属于科技一部总经理
+ var store = storeDict.ContainsKey(storeId) ? storeDict[storeId] : null;
+ var managersOfStore = new List();
+
+ if (store != null && !string.IsNullOrEmpty(store.Kjb))
+ {
+ // 找到组织ID等于门店kjb的科技部总经理
+ var managers = techGeneralManagerUserList
+ .Where(x => x.OrganizeId == store.Kjb)
+ .Select(x => x.Id)
+ .ToList();
+ managersOfStore.AddRange(managers);
+ }
+
+ foreach (var managerId in managersOfStore)
+ {
+ if (!storeDetailDict.ContainsKey(managerId))
+ {
+ storeDetailDict[managerId] = new Dictionary();
+ }
+
+ var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : "";
+ storeDetailDict[managerId][storeId] = new StoreDetailItem
+ {
+ StoreId = storeId,
+ StoreName = storeName,
+ TraceabilityBillingAmount = storeTraceabilityBilling,
+ TraceabilityRefundAmount = storeTraceabilityRefund,
+ TraceabilityAmount = storeTraceabilityBilling - storeTraceabilityRefund,
+ CellBillingAmount = storeCellBilling,
+ CellRefundAmount = storeCellRefund,
+ CellAmount = storeCellBilling - storeCellRefund
+ };
+ }
+ }
+ }
+
+ // 2. 按科技部总经理聚合数据
+ var managerStats = new Dictionary();
+
+ foreach (var managerUser in techGeneralManagerUserList)
+ {
+ var managerId = managerUser.Id;
+
+ // 获取该科技部总经理管理的门店列表
+ var managedStores = managerStoreDict.ContainsKey(managerId) ? managerStoreDict[managerId] : new List();
+
+ // 2.1 创建工资统计对象
+ // 岗位使用组织名称(科技一部/科技二部)
+ var position = techOrganizeDict.ContainsKey(managerUser.OrganizeId)
+ ? techOrganizeDict[managerUser.OrganizeId]
+ : "";
+
+ var salary = new LqTechGeneralManagerSalaryStatisticsEntity
+ {
+ Id = YitIdHelper.NextId().ToString(),
+ StatisticsMonth = monthStr,
+ EmployeeId = managerId,
+ Position = position,
+ EmployeeName = managerUser.RealName ?? "",
+ EmployeeAccount = managerUser.Account ?? "",
+ IsTerminated = managerUser.IsOnJob == 0 ? 1 : 0,
+ CreateTime = DateTime.Now,
+ UpdateTime = DateTime.Now,
+ IsLocked = 0
+ };
+
+ // 2.2 考勤数据
+ var attendance = attendanceDict.ContainsKey(managerId) ? attendanceDict[managerId] : null;
+ salary.WorkingDays = attendance?.WorkDays ?? 0;
+ salary.LeaveDays = attendance?.LeaveDays ?? 0;
+
+ // 2.3 计算底薪(固定4000元)
+ salary.BaseSalary = 4000m;
+
+ // 2.4 统计该科技部总经理管理的所有门店的溯源金额和Cell金额总和
+ decimal totalTraceabilityAmount = 0m;
+ decimal totalCellAmount = 0m;
+ var storeDetails = new List();
+
+ if (managedStores.Any() && storeDetailDict.ContainsKey(managerId))
+ {
+ foreach (var storeId in managedStores)
+ {
+ if (storeDetailDict[managerId].ContainsKey(storeId))
+ {
+ var storeDetail = storeDetailDict[managerId][storeId];
+ totalTraceabilityAmount += storeDetail.TraceabilityAmount;
+ totalCellAmount += storeDetail.CellAmount;
+ storeDetails.Add(storeDetail);
+ }
+ }
+ }
+
+ salary.TraceabilityAmount = totalTraceabilityAmount;
+ salary.CellAmount = totalCellAmount;
+
+ // 2.5 保存门店明细(JSON格式)
+ salary.StoreDetail = JsonConvert.SerializeObject(storeDetails);
+
+ // 2.6 计算溯源金额提成(分段累进)
+ var traceabilityCommission = CalculateTraceabilityCommission(totalTraceabilityAmount);
+ salary.TraceabilityCommissionAmount = traceabilityCommission.Amount;
+ salary.TraceabilityCommissionRate = traceabilityCommission.Rate;
+
+ // 2.7 计算Cell金额提成(分段累进)
+ var cellCommission = CalculateCellCommission(totalCellAmount);
+ salary.CellCommissionAmount = cellCommission.Amount;
+ salary.CellCommissionRate = cellCommission.Rate;
+
+ // 2.8 提成合计
+ salary.TotalCommission = salary.TraceabilityCommissionAmount + salary.CellCommissionAmount;
+
+ // 2.9 计算应发工资
+ salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission;
+ salary.FinalGrossSalary = salary.CalculatedGrossSalary;
+
+ // 2.10 初始化其他字段(默认值为0)
+ salary.MonthlyTrainingSubsidy = 0;
+ salary.MonthlyTransportSubsidy = 0;
+ salary.LastMonthTrainingSubsidy = 0;
+ salary.LastMonthTransportSubsidy = 0;
+ salary.TotalSubsidy = 0;
+ salary.MissingCard = 0;
+ salary.LateArrival = 0;
+ salary.LeaveDeduction = 0;
+ salary.SocialInsuranceDeduction = 0;
+ salary.RewardDeduction = 0;
+ salary.AccommodationDeduction = 0;
+ salary.StudyPeriodDeduction = 0;
+ salary.WorkClothesDeduction = 0;
+ salary.TotalDeduction = 0;
+ salary.Bonus = 0;
+ salary.ReturnPhoneDeposit = 0;
+ salary.ReturnAccommodationDeposit = 0;
+ salary.ActualSalary = salary.FinalGrossSalary - salary.TotalDeduction + salary.TotalSubsidy + salary.Bonus;
+ salary.MonthlyPaymentStatus = "未发放";
+ salary.PaidAmount = 0;
+ salary.PendingAmount = salary.ActualSalary;
+ salary.LastMonthSupplement = 0;
+ salary.MonthlyTotalPayment = 0;
+
+ managerStats[managerId] = salary;
+ }
+
+ // 3. 保存数据
+ if (managerStats.Any())
+ {
+ // 先删除当月旧数据 (防止重复)
+ await _db.Deleteable()
+ .Where(x => x.StatisticsMonth == monthStr)
+ .ExecuteCommandAsync();
+
+ await _db.Insertable(managerStats.Values.ToList()).ExecuteCommandAsync();
+ }
+ }
+
+ ///
+ /// 计算溯源金额提成(分段累进)
+ ///
+ /// 溯源金额
+ /// 提成金额和平均比例
+ private (decimal Amount, decimal? Rate) CalculateTraceabilityCommission(decimal traceabilityAmount)
+ {
+ if (traceabilityAmount <= 0)
+ {
+ return (0m, null);
+ }
+
+ decimal commissionAmount = 0m;
+ decimal? averageRate = null;
+
+ if (traceabilityAmount < 200000m)
+ {
+ // < 200,000元:1%
+ commissionAmount = traceabilityAmount * 0.01m;
+ averageRate = 1.00m;
+ }
+ else if (traceabilityAmount < 300000m)
+ {
+ // 200,000-300,000元:1.5%
+ commissionAmount = 200000m * 0.01m + (traceabilityAmount - 200000m) * 0.015m;
+ averageRate = (commissionAmount / traceabilityAmount) * 100m;
+ }
+ else if (traceabilityAmount < 500000m)
+ {
+ // 300,000-500,000元:2%
+ commissionAmount = 200000m * 0.01m + 100000m * 0.015m + (traceabilityAmount - 300000m) * 0.02m;
+ averageRate = (commissionAmount / traceabilityAmount) * 100m;
+ }
+ else
+ {
+ // ≥ 500,000元:2.5%
+ commissionAmount = 200000m * 0.01m + 100000m * 0.015m + 200000m * 0.02m + (traceabilityAmount - 500000m) * 0.025m;
+ averageRate = (commissionAmount / traceabilityAmount) * 100m;
+ }
+
+ return (commissionAmount, averageRate);
+ }
+
+ ///
+ /// 计算Cell金额提成(分段累进)
+ ///
+ /// Cell金额
+ /// 提成金额和平均比例
+ private (decimal Amount, decimal? Rate) CalculateCellCommission(decimal cellAmount)
+ {
+ if (cellAmount <= 0)
+ {
+ return (0m, null);
+ }
+
+ if (cellAmount < 50000m)
+ {
+ // < 50,000元:无提成
+ return (0m, null);
+ }
+
+ decimal commissionAmount = 0m;
+ decimal? averageRate = null;
+
+ if (cellAmount < 400000m)
+ {
+ // 50,000-400,000元:1%
+ commissionAmount = (cellAmount - 50000m) * 0.01m;
+ averageRate = (commissionAmount / cellAmount) * 100m;
+ }
+ else
+ {
+ // ≥ 400,000元:1.5%
+ commissionAmount = 350000m * 0.01m + (cellAmount - 400000m) * 0.015m;
+ averageRate = (commissionAmount / cellAmount) * 100m;
+ }
+
+ return (commissionAmount, averageRate);
+ }
+
+ ///
+ /// 门店明细项(用于JSON序列化)
+ ///
+ private class StoreDetailItem
+ {
+ public string StoreId { get; set; }
+ public string StoreName { get; set; }
+ public decimal TraceabilityBillingAmount { get; set; }
+ public decimal TraceabilityRefundAmount { get; set; }
+ public decimal TraceabilityAmount { get; set; }
+ public decimal CellBillingAmount { get; set; }
+ public decimal CellRefundAmount { get; set; }
+ public decimal CellAmount { get; set; }
+ }
+ }
+}
+
diff --git a/sql/主任工资表新增毛利相关字段.sql b/sql/主任工资表新增毛利相关字段.sql
new file mode 100644
index 0000000..84e6c8e
--- /dev/null
+++ b/sql/主任工资表新增毛利相关字段.sql
@@ -0,0 +1,33 @@
+-- 主任工资表新增毛利相关字段
+-- 表名:lq_director_salary_statistics
+-- 说明:为主任工资计算添加毛利相关字段,用于计算基于毛利的提成
+
+-- 1. 销售业绩(开单业绩-退款业绩)
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_SalesPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '销售业绩(开单业绩-退款业绩)' AFTER F_StoreRefundPerformance;
+
+-- 2. 产品物料(仓库领用金额)
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_ProductMaterial DECIMAL(18,2) DEFAULT 0.00 COMMENT '产品物料(仓库领用金额,注意11月特殊规则:11月工资算10月数据)' AFTER F_SalesPerformance;
+
+-- 3. 合作项目成本
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_CooperationCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '合作项目成本' AFTER F_ProductMaterial;
+
+-- 4. 店内支出
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_StoreExpense DECIMAL(18,2) DEFAULT 0.00 COMMENT '店内支出' AFTER F_CooperationCost;
+
+-- 5. 洗毛巾费用
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_LaundryCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '洗毛巾费用(只统计送出的记录,F_FlowType = 0)' AFTER F_StoreExpense;
+
+-- 6. 毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT '毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)' AFTER F_LaundryCost;
+
+-- 7. 修改 F_StoreTotalPerformance 字段注释(说明该字段存储的是毛利,用于提成计算)
+-- 注意:此字段的值将在代码中改为存储毛利,而不是开单-退卡
+ALTER TABLE lq_director_salary_statistics
+MODIFY COLUMN F_StoreTotalPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '门店总业绩(毛利,用于提成计算)';
+
diff --git a/sql/修复产品平均单价计算.sql b/sql/修复产品平均单价计算.sql
new file mode 100644
index 0000000..09f66b8
--- /dev/null
+++ b/sql/修复产品平均单价计算.sql
@@ -0,0 +1,77 @@
+-- ============================================
+-- 修复产品平均单价计算
+-- ============================================
+-- 说明:重新计算所有产品的平均单价,基于当前所有有效库存
+-- 执行时间:2025年
+-- ============================================
+
+-- 重新计算所有产品的平均单价(基于所有有效库存)
+UPDATE `lq_product` p
+SET p.`F_AveragePrice` = (
+ SELECT
+ CASE
+ WHEN SUM(inv.`F_Quantity`) > 0 THEN
+ SUM(
+ CASE
+ WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
+ WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
+ ELSE 0
+ END
+ ) / SUM(inv.`F_Quantity`)
+ ELSE p.`F_Price`
+ END
+ FROM `lq_inventory` inv
+ WHERE inv.`F_ProductId` = p.`F_Id`
+ AND inv.`F_IsEffective` = 1
+ AND inv.`F_Quantity` > 0
+)
+WHERE EXISTS (
+ SELECT 1
+ FROM `lq_inventory` inv
+ WHERE inv.`F_ProductId` = p.`F_Id`
+ AND inv.`F_IsEffective` = 1
+ AND inv.`F_Quantity` > 0
+);
+
+-- 对于没有库存的产品,将平均单价设置为产品价格
+UPDATE `lq_product` p
+SET p.`F_AveragePrice` = p.`F_Price`
+WHERE p.`F_AveragePrice` = 0 OR p.`F_AveragePrice` IS NULL
+ OR NOT EXISTS (
+ SELECT 1
+ FROM `lq_inventory` inv
+ WHERE inv.`F_ProductId` = p.`F_Id`
+ AND inv.`F_IsEffective` = 1
+ AND inv.`F_Quantity` > 0
+ );
+
+-- 验证特定产品的平均单价(产品ID: 770163009904444677)
+-- 应该显示:平均单价 = 200.00 (100+200+300)/3 = 600/3 = 200
+SELECT
+ p.`F_Id` as ProductId,
+ p.`F_ProductName` as ProductName,
+ p.`F_Price` as Price,
+ p.`F_AveragePrice` as AveragePrice,
+ SUM(inv.`F_Quantity`) as TotalQuantity,
+ SUM(
+ CASE
+ WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
+ WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
+ ELSE 0
+ END
+ ) as TotalAmount,
+ CASE
+ WHEN SUM(inv.`F_Quantity`) > 0 THEN
+ SUM(
+ CASE
+ WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
+ WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
+ ELSE 0
+ END
+ ) / SUM(inv.`F_Quantity`)
+ ELSE p.`F_Price`
+ END as CalculatedAveragePrice
+FROM `lq_product` p
+LEFT JOIN `lq_inventory` inv ON inv.`F_ProductId` = p.`F_Id` AND inv.`F_IsEffective` = 1 AND inv.`F_Quantity` > 0
+WHERE p.`F_Id` = '770163009904444677'
+GROUP BY p.`F_Id`, p.`F_ProductName`, p.`F_Price`, p.`F_AveragePrice`;
diff --git a/sql/创建大项目主管工资统计表.sql b/sql/创建大项目主管工资统计表.sql
new file mode 100644
index 0000000..9728095
--- /dev/null
+++ b/sql/创建大项目主管工资统计表.sql
@@ -0,0 +1,170 @@
+-- ============================================
+-- 创建大项目主管工资统计表
+-- 功能:存储大项目主管每月的工资计算数据,包括底薪、业绩提成、扣款、补贴、奖金、支付等信息
+-- 创建时间:2025年
+-- ============================================
+
+-- 删除表(如果存在)
+DROP TABLE IF EXISTS lq_major_project_director_salary_statistics;
+
+-- ============================================
+-- 创建大项目主管工资统计表
+-- ============================================
+CREATE TABLE lq_major_project_director_salary_statistics (
+ -- 主键
+ F_Id VARCHAR(50) NOT NULL COMMENT '主键ID',
+
+ -- 一、基础信息字段
+ F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份(YYYYMM格式)',
+ F_Position VARCHAR(50) NOT NULL COMMENT '核算岗位(大项目一部/大项目二部等)',
+ F_EmployeeName VARCHAR(100) NOT NULL COMMENT '员工姓名',
+ F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID',
+ F_EmployeeAccount VARCHAR(100) NULL COMMENT '员工账号',
+ F_IsTerminated INT NOT NULL DEFAULT 0 COMMENT '是否离职(0=在职,1=离职)',
+
+ -- 二、管理的门店信息(JSON格式)
+ F_StoreDetail TEXT NULL COMMENT '管理的门店明细(JSON格式,记录每个门店的业绩详情)',
+
+ -- 三、业绩相关字段
+ F_TotalPerformance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '总业绩(管理的所有门店的总业绩总和,开单-退卡)',
+ F_BillingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '开单金额(管理的所有门店的开单金额总和)',
+ F_RefundAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退卡金额(管理的所有门店的退卡金额总和)',
+
+ -- 四、底薪相关字段
+ F_BaseSalary DECIMAL(18,2) NOT NULL DEFAULT 3500.00 COMMENT '底薪金额(固定3500元)',
+
+ -- 五、提成相关字段
+ F_CommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT '提成比例(根据总业绩分段:0%/1%/1.5%)',
+ F_CommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '提成金额(总业绩 × 提成比例)',
+
+ -- 六、考勤相关字段
+ F_WorkingDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '在店天数',
+ F_LeaveDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假天数',
+
+ -- 七、工资计算字段
+ F_CalculatedGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '核算应发工资(底薪 + 提成金额)',
+ F_FinalGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '最终应发工资(等于核算应发工资)',
+
+ -- 八、补贴相关字段
+ F_MonthlyTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月培训补贴',
+ F_MonthlyTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月交通补贴',
+ F_LastMonthTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月培训补贴',
+ F_LastMonthTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月交通补贴',
+ F_TotalSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补贴合计',
+
+ -- 九、扣款相关字段
+ F_MissingCard DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '缺卡扣款',
+ F_LateArrival DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '迟到扣款',
+ F_LeaveDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假扣款',
+ F_SocialInsuranceDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣社保',
+ F_RewardDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣除奖励',
+ F_AccommodationDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣住宿费',
+ F_StudyPeriodDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣学习期费用',
+ F_WorkClothesDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣工作服费用',
+ F_TotalDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣款合计',
+
+ -- 十、奖金相关字段
+ F_Bonus DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '发奖金',
+ F_ReturnPhoneDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退手机押金',
+ F_ReturnAccommodationDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退住宿押金',
+
+ -- 十一、支付相关字段
+ F_ActualSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资(最终应发工资 - 扣款合计 + 补贴合计 + 奖金)',
+ F_MonthlyPaymentStatus VARCHAR(20) NOT NULL DEFAULT '未发放' COMMENT '当月是否发放(已发放/未发放/部分发放)',
+ F_PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '支付金额',
+ F_PendingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '待支付金额',
+ F_LastMonthSupplement DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补发上月',
+ F_MonthlyTotalPayment DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月支付总额',
+
+ -- 十二、系统字段
+ F_IsLocked INT NOT NULL DEFAULT 0 COMMENT '是否锁定(0=未锁定,1=已锁定)',
+ F_CreateTime DATETIME NOT NULL COMMENT '创建时间',
+ F_UpdateTime DATETIME NOT NULL COMMENT '更新时间',
+ F_CreateUser VARCHAR(50) NULL COMMENT '创建人',
+ F_UpdateUser VARCHAR(50) NULL COMMENT '更新人',
+
+ -- 主键约束
+ PRIMARY KEY (F_Id),
+
+ -- 唯一索引:确保同一员工同一月份只有一条记录
+ UNIQUE KEY `uk_employee_month` (F_EmployeeId, F_StatisticsMonth),
+
+ -- 普通索引
+ KEY `idx_statistics_month` (F_StatisticsMonth),
+ KEY `idx_employee_id` (F_EmployeeId),
+ KEY `idx_employee_account` (F_EmployeeAccount),
+ KEY `idx_position` (F_Position),
+ KEY `idx_is_terminated` (F_IsTerminated),
+ KEY `idx_create_time` (F_CreateTime)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大项目主管工资统计表';
+
+-- ============================================
+-- 表结构说明
+-- ============================================
+/*
+表名:lq_major_project_director_salary_statistics(大项目主管工资统计表)
+
+功能说明:
+1. 存储大项目主管每月的工资计算数据
+2. 包括底薪、业绩提成、扣款、补贴、奖金、支付等信息
+3. 支持按员工、月份查询
+4. 记录管理的门店汇总信息
+
+主要字段说明:
+- F_BaseSalary:底薪(固定3500元)
+- F_TotalPerformance:总业绩(管理的所有门店的总业绩总和,开单-退卡)
+- F_CommissionRate:提成比例(根据总业绩分段:0%/1%/1.5%)
+- F_CommissionAmount:提成金额(总业绩 × 提成比例)
+- F_StoreDetail:门店业绩明细(JSON格式,记录每个门店的业绩详情)
+
+数据来源:
+- 大项目主管归属:BASE_USER 表(岗位为"主管",组织ID为大项目一部或大项目二部)
+- 管理的门店:lq_md_target 表的 F_MajorProjectDepartment 字段(按月份筛选)
+- 开单业绩:lq_kd_kdjlb 表的 sfyj 字段(按管理的门店统计)
+- 退卡业绩:lq_hytk_hytk 表的 F_ActualRefundAmount 或 tkje 字段(按管理的门店统计)
+
+计算公式:
+- 总业绩 = 管理的所有门店的开单金额 - 退卡金额
+- 提成计算逻辑:
+ 1. 总业绩 <= 50万:无提成(0%)
+ 2. 50万 < 总业绩 <= 70万:1%提成
+ 3. 总业绩 > 70万:1.5%提成
+- 核算应发工资 = 底薪(3500) + 提成金额
+- 最终应发工资 = 核算应发工资
+- 实发工资 = 最终应发工资 - 扣款合计 + 补贴合计 + 奖金
+
+门店业绩明细JSON格式示例:
+[
+ {
+ "storeId": "A001",
+ "storeName": "门店A",
+ "billingAmount": 160000.00,
+ "refundAmount": 10000.00,
+ "totalPerformance": 150000.00
+ },
+ {
+ "storeId": "B001",
+ "storeName": "门店B",
+ "billingAmount": 105000.00,
+ "refundAmount": 5000.00,
+ "totalPerformance": 100000.00
+ }
+]
+
+索引说明:
+- 主键索引:F_Id
+- 唯一索引:F_EmployeeId + F_StatisticsMonth(确保同一员工同一月份只有一条记录)
+- 普通索引:
+ - F_StatisticsMonth:按月份查询
+ - F_EmployeeId:按员工查询
+ - F_EmployeeAccount:按员工账号查询
+ - F_Position:按岗位查询
+ - F_IsTerminated:按离职状态查询
+ - F_CreateTime:按创建时间查询
+
+数据校验要求:
+1. 总业绩必须 >= 0(不能为负数)
+2. 提成比例必须为 0、1 或 1.5(对应不同业绩区间)
+3. 底薪固定为3500元,不允许修改
+*/
+
diff --git a/sql/创建库存使用申请审批流程表.sql b/sql/创建库存使用申请审批流程表.sql
index 60d040a..0eb3960 100644
--- a/sql/创建库存使用申请审批流程表.sql
+++ b/sql/创建库存使用申请审批流程表.sql
@@ -110,3 +110,5 @@ WHERE u.`F_UnitPrice` = 0 OR u.`F_UnitPrice` IS NULL;
+
+
diff --git a/sql/创建科技部总经理工资统计表.sql b/sql/创建科技部总经理工资统计表.sql
new file mode 100644
index 0000000..dea1053
--- /dev/null
+++ b/sql/创建科技部总经理工资统计表.sql
@@ -0,0 +1,198 @@
+-- ============================================
+-- 创建科技部总经理工资统计表
+-- 功能:存储科技部总经理每月的工资计算数据,包括底薪、溯源金额提成、Cell金额提成、扣款、补贴、奖金、支付等信息
+-- 创建时间:2025年
+-- ============================================
+
+-- 删除表(如果存在)
+DROP TABLE IF EXISTS lq_tech_general_manager_salary_statistics;
+
+-- ============================================
+-- 创建科技部总经理工资统计表
+-- ============================================
+CREATE TABLE lq_tech_general_manager_salary_statistics (
+ -- 主键
+ F_Id VARCHAR(50) NOT NULL COMMENT '主键ID',
+
+ -- 一、基础信息字段
+ F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份(YYYYMM格式)',
+ F_Position VARCHAR(50) NOT NULL COMMENT '核算岗位(科技一部/科技二部等)',
+ F_EmployeeName VARCHAR(100) NOT NULL COMMENT '员工姓名',
+ F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID',
+ F_EmployeeAccount VARCHAR(100) NULL COMMENT '员工账号',
+ F_IsTerminated INT NOT NULL DEFAULT 0 COMMENT '是否离职(0=在职,1=离职)',
+
+ -- 二、管理的门店信息(JSON格式)
+ F_StoreDetail TEXT NULL COMMENT '管理的门店明细(JSON格式,记录每个门店的溯源金额和Cell金额详情)',
+
+ -- 三、业绩相关字段
+ F_TraceabilityAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额(管理的所有门店的溯源金额总和,开单-退卡)',
+ F_CellAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额(管理的所有门店的Cell金额总和,开单-退卡)',
+
+ -- 四、底薪相关字段
+ F_BaseSalary DECIMAL(18,2) NOT NULL DEFAULT 4000.00 COMMENT '底薪金额(固定4000元)',
+
+ -- 五、提成相关字段
+ F_TraceabilityCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT '溯源金额提成比例(分段计算,存储平均比例)',
+ F_TraceabilityCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额提成金额',
+ F_CellCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT 'Cell金额提成比例(分段计算,存储平均比例)',
+ F_CellCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额提成金额',
+ F_TotalCommission DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '提成合计(溯源提成+Cell提成)',
+
+ -- 六、考勤相关字段
+ F_WorkingDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '在店天数',
+ F_LeaveDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假天数',
+
+ -- 七、工资计算字段
+ F_CalculatedGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '核算应发工资(底薪 + 提成合计)',
+ F_FinalGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '最终应发工资(等于核算应发工资)',
+
+ -- 八、补贴相关字段
+ F_MonthlyTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月培训补贴',
+ F_MonthlyTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月交通补贴',
+ F_LastMonthTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月培训补贴',
+ F_LastMonthTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月交通补贴',
+ F_TotalSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补贴合计',
+
+ -- 九、扣款相关字段
+ F_MissingCard DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '缺卡扣款',
+ F_LateArrival DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '迟到扣款',
+ F_LeaveDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假扣款',
+ F_SocialInsuranceDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣社保',
+ F_RewardDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣除奖励',
+ F_AccommodationDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣住宿费',
+ F_StudyPeriodDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣学习期费用',
+ F_WorkClothesDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣工作服费用',
+ F_TotalDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣款合计',
+
+ -- 十、奖金相关字段
+ F_Bonus DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '发奖金',
+ F_ReturnPhoneDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退手机押金',
+ F_ReturnAccommodationDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退住宿押金',
+
+ -- 十一、支付相关字段
+ F_ActualSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资(最终应发工资 - 扣款合计 + 补贴合计 + 奖金)',
+ F_MonthlyPaymentStatus VARCHAR(20) NOT NULL DEFAULT '未发放' COMMENT '当月是否发放(已发放/未发放/部分发放)',
+ F_PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '支付金额',
+ F_PendingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '待支付金额',
+ F_LastMonthSupplement DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补发上月',
+ F_MonthlyTotalPayment DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月支付总额',
+
+ -- 十二、系统字段
+ F_IsLocked INT NOT NULL DEFAULT 0 COMMENT '是否锁定(0=未锁定,1=已锁定)',
+ F_CreateTime DATETIME NOT NULL COMMENT '创建时间',
+ F_UpdateTime DATETIME NOT NULL COMMENT '更新时间',
+ F_CreateUser VARCHAR(50) NULL COMMENT '创建人',
+ F_UpdateUser VARCHAR(50) NULL COMMENT '更新人',
+
+ -- 主键约束
+ PRIMARY KEY (F_Id),
+
+ -- 唯一索引:确保同一员工同一月份只有一条记录
+ UNIQUE KEY `uk_employee_month` (F_EmployeeId, F_StatisticsMonth),
+
+ -- 普通索引
+ KEY `idx_statistics_month` (F_StatisticsMonth),
+ KEY `idx_employee_id` (F_EmployeeId),
+ KEY `idx_employee_account` (F_EmployeeAccount),
+ KEY `idx_position` (F_Position),
+ KEY `idx_is_terminated` (F_IsTerminated),
+ KEY `idx_create_time` (F_CreateTime)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科技部总经理工资统计表';
+
+-- ============================================
+-- 表结构说明
+-- ============================================
+/*
+表名:lq_tech_general_manager_salary_statistics(科技部总经理工资统计表)
+
+功能说明:
+1. 存储科技部总经理每月的工资计算数据
+2. 包括底薪、溯源金额提成、Cell金额提成、扣款、补贴、奖金、支付等信息
+3. 支持按员工、月份查询
+4. 记录管理的门店汇总信息
+
+主要字段说明:
+- F_BaseSalary:底薪(固定4000元)
+- F_TraceabilityAmount:溯源金额(管理的所有门店的溯源金额总和,开单-退卡)
+- F_CellAmount:Cell金额(管理的所有门店的Cell金额总和,开单-退卡)
+- F_TraceabilityCommissionAmount:溯源金额提成金额
+- F_CellCommissionAmount:Cell金额提成金额
+- F_StoreDetail:管理的门店明细(JSON格式,记录每个门店的溯源金额和Cell金额详情)
+
+数据来源:
+- 科技部总经理识别:BASE_USER 表(F_GW字段包含"科技一部"、"科技二部"等)
+- 管理的门店归属:lq_md_general_manager_lifeline 表(通过F_GeneralManagerId和F_Month获取)
+- 溯源金额:lq_kd_pxmx 表(F_BeautyType='溯源系统'或'溯源')和 lq_hytk_mx 表(退卡)
+- Cell金额:lq_kd_pxmx 表(F_BeautyType='cell'或'Cell')和 lq_hytk_mx 表(退卡)
+
+计算公式:
+- 溯源金额 = 管理的所有门店的溯源类型品项开单金额总和 - 退卡金额总和
+- Cell金额 = 管理的所有门店的Cell类型品项开单金额总和 - 退卡金额总和
+- 溯源金额提成计算(分段累进):
+ - < 200,000元:提成 = 溯源金额 × 1%
+ - 200,000-300,000元:提成 = 200,000 × 1% + (溯源金额 - 200,000) × 1.5%
+ - 300,000-500,000元:提成 = 200,000 × 1% + 100,000 × 1.5% + (溯源金额 - 300,000) × 2%
+ - ≥ 500,000元:提成 = 200,000 × 1% + 100,000 × 1.5% + 200,000 × 2% + (溯源金额 - 500,000) × 2.5%
+- Cell金额提成计算(分段累进):
+ - < 50,000元:提成 = 0(无提成)
+ - 50,000-400,000元:提成 = (Cell金额 - 50,000) × 1%
+ - ≥ 400,000元:提成 = 350,000 × 1% + (Cell金额 - 400,000) × 1.5%
+- 提成合计 = 溯源金额提成 + Cell金额提成
+- 核算应发工资 = 底薪(4000) + 提成合计
+- 最终应发工资 = 核算应发工资
+- 实发工资 = 最终应发工资 - 扣款合计 + 补贴合计 + 奖金
+
+门店明细JSON格式示例:
+[
+ {
+ "storeId": "A001",
+ "storeName": "门店A",
+ "traceabilityBillingAmount": 160000.00,
+ "traceabilityRefundAmount": 10000.00,
+ "traceabilityAmount": 150000.00,
+ "cellBillingAmount": 85000.00,
+ "cellRefundAmount": 5000.00,
+ "cellAmount": 80000.00
+ },
+ {
+ "storeId": "B001",
+ "storeName": "门店B",
+ "traceabilityBillingAmount": 105000.00,
+ "traceabilityRefundAmount": 5000.00,
+ "traceabilityAmount": 100000.00,
+ "cellBillingAmount": 125000.00,
+ "cellRefundAmount": 5000.00,
+ "cellAmount": 120000.00
+ }
+]
+
+JSON字段说明:
+- storeId:门店ID
+- storeName:门店名称
+- traceabilityBillingAmount:该门店的溯源类型品项开单金额
+- traceabilityRefundAmount:该门店的溯源类型品项退卡金额
+- traceabilityAmount:该门店的净溯源金额(开单-退卡)
+- cellBillingAmount:该门店的Cell类型品项开单金额
+- cellRefundAmount:该门店的Cell类型品项退卡金额
+- cellAmount:该门店的净Cell金额(开单-退卡)
+
+索引说明:
+- 主键索引:F_Id
+- 唯一索引:F_EmployeeId + F_StatisticsMonth(确保同一员工同一月份只有一条记录)
+- 普通索引:
+ - F_StatisticsMonth:按月份查询
+ - F_EmployeeId:按员工查询
+ - F_Position:按岗位查询(科技一部/科技二部等)
+ - F_CreateTime:按创建时间查询
+
+数据校验要求:
+1. 底薪固定为4000元
+2. 必须从lq_md_general_manager_lifeline表获取管理的门店
+3. 必须按管理的门店筛选,只统计归属范围内的门店
+4. 必须正确区分溯源和Cell类型(通过F_BeautyType字段)
+5. 必须扣除退卡金额,确保数据准确性
+6. 提成必须按照分段累进方式计算
+7. 如果溯源金额或Cell金额为0或负数,对应提成为0
+*/
+
diff --git a/sql/更新开单扣减信息表品项分类字段.sql b/sql/更新开单扣减信息表品项分类字段.sql
new file mode 100644
index 0000000..1e6aef1
--- /dev/null
+++ b/sql/更新开单扣减信息表品项分类字段.sql
@@ -0,0 +1,62 @@
+-- ============================================
+-- 更新开单扣减信息表(lq_kd_deductinfo)的品项分类字段
+-- ============================================
+-- 说明:此脚本用于更新 lq_kd_deductinfo 表的 F_ItemCategory 字段
+--
+-- 数据来源:从关联的项目资料表(lq_xmzl)的 qt2 字段获取品项分类
+--
+-- 关联关系:
+-- - lq_kd_deductinfo.F_ItemId = lq_xmzl.F_Id
+--
+-- 更新逻辑:
+-- - 更新所有记录(不判断是否有效)
+-- - 只更新关联的项目资料存在且qt2字段有值的记录
+-- - 从 lq_xmzl.qt2 字段获取品项分类(医美/科美/生美)
+-- - 如果当前 F_ItemCategory 已有值但与 lq_xmzl.qt2 不一致,也会更新为最新值
+
+-- ============================================
+-- 更新开单扣减信息表的品项分类字段
+-- ============================================
+UPDATE lq_kd_deductinfo deduct
+INNER JOIN lq_xmzl xmzl ON deduct.F_ItemId = xmzl.F_Id
+SET deduct.F_ItemCategory = xmzl.qt2
+WHERE xmzl.qt2 IS NOT NULL
+ AND xmzl.qt2 != ''
+ AND (deduct.F_ItemCategory IS NULL
+ OR deduct.F_ItemCategory = ''
+ OR deduct.F_ItemCategory != xmzl.qt2);
+
+-- ============================================
+-- 验证更新结果
+-- ============================================
+-- 查看更新后的统计信息
+-- SELECT
+-- F_ItemCategory AS 品项分类,
+-- COUNT(*) AS 记录数
+-- FROM lq_kd_deductinfo
+-- GROUP BY F_ItemCategory
+-- ORDER BY 记录数 DESC;
+
+-- 查看未更新的记录数(关联的项目资料不存在或qt2为空)
+-- SELECT COUNT(*) AS 未更新记录数
+-- FROM lq_kd_deductinfo deduct
+-- LEFT JOIN lq_xmzl xmzl ON deduct.F_ItemId = xmzl.F_Id
+-- WHERE xmzl.F_Id IS NULL
+-- OR xmzl.qt2 IS NULL
+-- OR xmzl.qt2 = '';
+
+-- 查看更新前后的对比(需要先备份数据)
+-- SELECT
+-- deduct.F_Id,
+-- deduct.F_ItemId,
+-- deduct.F_ItemName,
+-- deduct.F_ItemCategory AS 更新前分类,
+-- xmzl.qt2 AS 更新后分类
+-- FROM lq_kd_deductinfo deduct
+-- INNER JOIN lq_xmzl xmzl ON deduct.F_ItemId = xmzl.F_Id
+-- WHERE xmzl.qt2 IS NOT NULL
+-- AND xmzl.qt2 != ''
+-- AND (deduct.F_ItemCategory IS NULL
+-- OR deduct.F_ItemCategory = ''
+-- OR deduct.F_ItemCategory != xmzl.qt2)
+-- LIMIT 100;
diff --git a/sql/添加产品平均单价字段.sql b/sql/添加产品平均单价字段.sql
new file mode 100644
index 0000000..ef65497
--- /dev/null
+++ b/sql/添加产品平均单价字段.sql
@@ -0,0 +1,48 @@
+-- ============================================
+-- 添加产品平均单价字段
+-- ============================================
+-- 说明:为产品表添加平均单价字段,用于维护加权平均成本
+-- 执行时间:2025年
+-- ============================================
+
+-- 1. 添加平均单价字段到产品表
+ALTER TABLE `lq_product`
+ ADD COLUMN `F_AveragePrice` decimal(18,2) DEFAULT 0.00 COMMENT '平均单价(加权平均成本,用于出库计价)' AFTER `F_Price`,
+ ADD INDEX `idx_average_price` (`F_AveragePrice`);
+
+-- 2. 初始化历史数据的平均单价
+-- 说明:对于已有库存的产品,根据当前库存计算初始平均单价
+-- 计算公式:平均单价 = SUM(库存金额) / SUM(库存数量)
+-- 其中,库存金额优先使用 F_FinalAmount,其次使用 F_PurchaseUnitPrice * F_Quantity
+
+UPDATE `lq_product` p
+SET p.`F_AveragePrice` = (
+ SELECT
+ CASE
+ WHEN SUM(inv.`F_Quantity`) > 0 THEN
+ SUM(
+ CASE
+ WHEN inv.`F_FinalAmount` IS NOT NULL AND inv.`F_FinalAmount` > 0 THEN inv.`F_FinalAmount`
+ WHEN inv.`F_PurchaseUnitPrice` IS NOT NULL AND inv.`F_PurchaseUnitPrice` > 0 THEN inv.`F_PurchaseUnitPrice` * inv.`F_Quantity`
+ ELSE 0
+ END
+ ) / SUM(inv.`F_Quantity`)
+ ELSE p.`F_Price`
+ END
+ FROM `lq_inventory` inv
+ WHERE inv.`F_ProductId` = p.`F_Id`
+ AND inv.`F_IsEffective` = 1
+ AND inv.`F_Quantity` > 0
+)
+WHERE EXISTS (
+ SELECT 1
+ FROM `lq_inventory` inv
+ WHERE inv.`F_ProductId` = p.`F_Id`
+ AND inv.`F_IsEffective` = 1
+ AND inv.`F_Quantity` > 0
+);
+
+-- 3. 对于没有库存的产品,将平均单价设置为产品价格
+UPDATE `lq_product` p
+SET p.`F_AveragePrice` = p.`F_Price`
+WHERE p.`F_AveragePrice` = 0 OR p.`F_AveragePrice` IS NULL;
diff --git a/主任工资毛利计算逻辑梳理.md b/主任工资毛利计算逻辑梳理.md
new file mode 100644
index 0000000..e90b3b1
--- /dev/null
+++ b/主任工资毛利计算逻辑梳理.md
@@ -0,0 +1,302 @@
+# 主任工资毛利计算逻辑梳理
+
+## 📋 概述
+
+根据店长工资计算规则,主任工资也需要使用**毛利**来计算提成,而不是直接使用门店业绩(开单-退卡)。
+
+## 💰 毛利计算公式
+
+### 核心公式
+
+```
+毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
+```
+
+其中:
+- **销售业绩** = 开单业绩 - 退款业绩
+- **产品物料** = 仓库领用金额合计(注意11月特殊规则:11月工资算10月数据)
+- **合作项目成本** = 合作成本表合计金额
+- **店内支出** = 店内支出表合计金额
+- **洗毛巾** = 送洗记录总费用(只统计送出的记录,F_FlowType = 0)
+
+---
+
+## 📊 数据来源
+
+### 1. 销售业绩
+
+**计算公式**:
+```
+销售业绩 = 开单业绩 - 退款业绩
+```
+
+**数据来源**:
+
+1. **开单业绩**:
+ - 表:`lq_kd_kdjlb`(开单记录表)
+ - 字段:`sfyj`(实付业绩)
+ - 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `Djmd = @StoreId`(门店ID)
+ - `DATE_FORMAT(Kdrq, '%Y%m') = @Month`(月份,YYYYMM格式)
+
+2. **退款业绩**:
+ - 表:`lq_hytk_hytk`(退卡记录表)
+ - 字段:`F_ActualRefundAmount`(实际退款金额)
+ - 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `md = @StoreId`(门店ID)
+ - `DATE_FORMAT(tksj, '%Y%m') = @Month`(月份,YYYYMM格式)
+
+---
+
+### 2. 产品物料
+
+**计算公式**:
+```
+产品物料 = 仓库领用金额合计
+```
+
+**数据来源**:
+- 表:`lq_inventory_usage`(库存使用记录表)
+- 字段:`F_TotalAmount`(合计金额)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `F_StoreId = @StoreId`(门店ID)
+ - **特殊规则**:核算11月工资时,算的是10月份的仓库领用
+ - 如果计算月份是11月,则查询10月的数据
+ - 其他月份正常查询当月数据
+
+**SQL示例**:
+```sql
+-- 产品物料(特殊规则:11月工资算10月数据)
+SET @QueryMonth = @Month;
+IF @Month = '202411' THEN
+ SET @QueryMonth = '202410';
+END IF;
+
+SELECT COALESCE(SUM(F_TotalAmount), 0) as MaterialCost
+FROM lq_inventory_usage
+WHERE F_StoreId = @StoreId
+ AND DATE_FORMAT(F_UsageTime, '%Y%m') = @QueryMonth
+ AND F_IsEffective = 1
+```
+
+---
+
+### 3. 合作项目成本
+
+**计算公式**:
+```
+合作项目成本 = 合作成本表合计金额
+```
+
+**数据来源**:
+- 表:`lq_cooperation_cost`(合作成本表)
+- 字段:`F_TotalAmount`(合计金额)
+- 条件:
+ - `F_StoreId = @StoreId`(门店ID)
+ - `F_Year = @Year`(年份)
+ - `F_Month = @Month`(月份,YYYYMM格式)
+ - `F_IsEffective = 1`(有效记录)
+
+---
+
+### 4. 店内支出
+
+**计算公式**:
+```
+店内支出 = 店内支出表合计金额
+```
+
+**数据来源**:
+- 表:`lq_store_expense`(店内支出表)
+- 字段:`F_Amount`(金额)
+- 条件:
+ - `F_StoreId = @StoreId`(门店ID)
+ - `DATE_FORMAT(F_ExpenseDate, '%Y%m') = @Month`(月份,YYYYMM格式)
+ - `F_IsEffective = 1`(有效记录)
+
+---
+
+### 5. 洗毛巾费用
+
+**计算公式**:
+```
+洗毛巾 = 送洗记录总费用
+```
+
+**数据来源**:
+- 表:`lq_laundry_flow`(洗毛巾流水表)
+- 字段:`F_TotalPrice`(总费用)
+- 条件:
+ - `F_IsEffective = 1`(有效记录)
+ - `F_StoreId = @StoreId`(门店ID)
+ - `F_FlowType = 0`(只统计送出的记录)
+ - `DATE_FORMAT(F_CreateTime, '%Y%m') = @Month`(月份,YYYYMM格式)
+
+---
+
+## 🔄 修改方案
+
+### 1. 数据库字段调整
+
+#### 需要新增的字段
+
+在 `lq_director_salary_statistics` 表中新增以下字段:
+
+```sql
+-- 销售业绩(开单业绩-退款业绩)
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_SalesPerformance DECIMAL(18,2) DEFAULT 0.00 COMMENT '销售业绩(开单业绩-退款业绩)' AFTER F_StoreRefundPerformance;
+
+-- 产品物料
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_ProductMaterial DECIMAL(18,2) DEFAULT 0.00 COMMENT '产品物料(仓库领用金额)' AFTER F_SalesPerformance;
+
+-- 合作项目成本
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_CooperationCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '合作项目成本' AFTER F_ProductMaterial;
+
+-- 店内支出
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_StoreExpense DECIMAL(18,2) DEFAULT 0.00 COMMENT '店内支出' AFTER F_CooperationCost;
+
+-- 洗毛巾费用
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_LaundryCost DECIMAL(18,2) DEFAULT 0.00 COMMENT '洗毛巾费用' AFTER F_StoreExpense;
+
+-- 毛利
+ALTER TABLE lq_director_salary_statistics
+ADD COLUMN F_GrossProfit DECIMAL(18,2) DEFAULT 0.00 COMMENT '毛利(销售业绩-产品物料-合作项目成本-店内支出-洗毛巾)' AFTER F_LaundryCost;
+```
+
+#### 字段说明调整
+
+**F_StoreTotalPerformance** 字段的注释需要修改:
+- **修改前**:门店总业绩(门店开单业绩-门店退卡业绩)
+- **修改后**:门店总业绩(毛利,用于提成计算)
+
+**注意**:`F_StoreTotalPerformance` 字段的值应该存储**毛利**,而不是开单-退卡。
+
+---
+
+### 2. 代码逻辑调整
+
+#### 计算流程
+
+1. **计算销售业绩**(开单-退卡)
+ ```csharp
+ salary.SalesPerformance = billing - refund;
+ ```
+
+2. **统计产品物料**(注意11月特殊规则)
+ ```csharp
+ var queryMonth = monthStr;
+ if (month == 11)
+ {
+ queryMonth = $"{year}10"; // 11月工资算10月数据
+ }
+ salary.ProductMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
+ ```
+
+3. **统计合作项目成本**
+ ```csharp
+ salary.CooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
+ ```
+
+4. **统计店内支出**
+ ```csharp
+ salary.StoreExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
+ ```
+
+5. **统计洗毛巾费用**(只统计送出的记录)
+ ```csharp
+ salary.LaundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
+ ```
+
+6. **计算毛利**
+ ```csharp
+ salary.GrossProfit = salary.SalesPerformance - salary.ProductMaterial - salary.CooperationCost - salary.StoreExpense - salary.LaundryCost;
+ ```
+
+7. **将毛利赋值给 F_StoreTotalPerformance**(用于提成计算)
+ ```csharp
+ salary.StoreTotalPerformance = salary.GrossProfit;
+ ```
+
+8. **计算业绩完成率**(基于毛利与生命线比较)
+ ```csharp
+ if (salary.StoreLifeline > 0)
+ {
+ salary.PerformanceCompletionRate = salary.GrossProfit / salary.StoreLifeline;
+ }
+ ```
+
+9. **判断业绩是否达标**(基于毛利)
+ ```csharp
+ bool performanceReached = salary.GrossProfit >= salary.StoreLifeline;
+ ```
+
+10. **计算提成**(基于毛利,使用阶梯提成)
+ ```csharp
+ CalculateCommission(salary, isNewStore); // 提成计算基于毛利
+ ```
+
+---
+
+### 3. 提成计算调整
+
+**重要说明**:提成计算需要基于**毛利**,而不是销售业绩。
+
+当前提成计算逻辑(阶梯提成):
+- ≤生命线部分:根据门店分类使用不同比例(A类2%,B类2.5%,C类3%)
+- >生命线部分:根据门店分类使用不同比例(A类2.5%,B类3%,C类3.5%)
+
+**修改后**:
+- 提成计算基于**毛利**(`salary.GrossProfit`)
+- 业绩完成率判断基于**毛利**与生命线比较
+- 业绩达标判断基于**毛利**是否≥生命线
+
+---
+
+## 📝 总结
+
+### 关键变更点
+
+1. **F_StoreTotalPerformance 字段含义变更**:
+ - 原来:开单业绩 - 退卡业绩
+ - 现在:毛利(销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾)
+
+2. **需要新增字段**:
+ - `F_SalesPerformance`:销售业绩(开单-退卡)
+ - `F_ProductMaterial`:产品物料
+ - `F_CooperationCost`:合作项目成本
+ - `F_StoreExpense`:店内支出
+ - `F_LaundryCost`:洗毛巾费用
+ - `F_GrossProfit`:毛利
+
+3. **计算逻辑调整**:
+ - 先计算销售业绩(开单-退卡)
+ - 统计各项成本(产品物料、合作项目成本、店内支出、洗毛巾)
+ - 计算毛利
+ - 将毛利赋值给 `F_StoreTotalPerformance`(用于提成计算)
+ - 基于毛利计算业绩完成率和判断业绩是否达标
+ - 基于毛利计算提成
+
+4. **数据查询**:
+ - 需要查询 `lq_inventory_usage`(产品物料)
+ - 需要查询 `lq_cooperation_cost`(合作项目成本)
+ - 需要查询 `lq_store_expense`(店内支出)
+ - 需要查询 `lq_laundry_flow`(洗毛巾费用)
+
+---
+
+## ⚠️ 注意事项
+
+1. **11月特殊规则**:核算11月工资时,产品物料算的是10月份的数据
+2. **洗毛巾费用**:只统计送出的记录(`F_FlowType = 0`)
+3. **业绩完成率**:基于毛利与生命线比较,不是基于销售业绩
+4. **业绩达标判断**:基于毛利是否≥生命线
+5. **提成计算**:基于毛利,不是基于销售业绩
+
diff --git a/大项目主管工资计算规则梳理.md b/大项目主管工资计算规则梳理.md
new file mode 100644
index 0000000..8b7d337
--- /dev/null
+++ b/大项目主管工资计算规则梳理.md
@@ -0,0 +1,250 @@
+# 大项目主管工资计算规则梳理
+
+## 📋 目录
+- [概述](#概述)
+- [计算规则](#计算规则)
+- [数据来源](#数据来源)
+- [归属规则](#归属规则)
+- [计算流程](#计算流程)
+- [注意事项](#注意事项)
+
+---
+
+## 📋 概述
+
+大项目主管工资由以下几个部分组成:
+1. **底薪**:固定3500元
+2. **业绩提成**:根据管理的所有门店的总业绩分段提成
+
+**重要说明**:
+- 底薪固定为3500元,不设档位,不设条件
+- 大项目主管从 `BASE_USER` 表获取,岗位字段(`F_GW`)为"主管",组织ID为大项目一部或大项目二部
+- 每个大项目主管管理的门店归属在 `lq_md_target` 表中(通过 `F_MajorProjectDepartment` 字段)
+- 需要统计该大项目主管管理的**所有门店**的总业绩(开单-退卡)
+- 提成采用分段方式计算(不是分段累进)
+
+---
+
+## 💰 计算规则
+
+### 1. 底薪规则
+
+**固定底薪**:3500元
+
+- 无论业绩多少,底薪固定为3500元
+- 不设档位,不设条件
+- 不设考核扣款
+
+---
+
+### 2. 业绩提成规则
+
+**提成计算方式**:根据管理的所有门店的总业绩分段计算
+
+| 总业绩范围 | 提成比例 | 说明 |
+|-----------|---------|------|
+| ≤ 50万 | 0% | 无提成 |
+| > 50万 且 ≤ 70万 | 1% | 按1%计算提成 |
+| > 70万 | 1.5% | 按1.5%计算提成 |
+
+**计算说明**:
+- 提成金额 = 总业绩 × 对应提成比例
+- 采用分段方式计算,不同区间按不同比例计算
+- **注意**:不是分段累进,而是整个总业绩按对应比例计算
+
+**示例**:
+- 总业绩 = 40万 → 提成 = 0(无提成)
+- 总业绩 = 60万 → 提成 = 60万 × 1% = 6000元
+- 总业绩 = 80万 → 提成 = 80万 × 1.5% = 12000元
+
+---
+
+## 📊 数据来源
+
+### 大项目主管识别
+
+**数据来源**:`BASE_USER` 表
+
+**识别条件**:
+- `F_GW`(岗位字段)为"主管"
+- `F_OrganizeId`(组织ID)在大项目一部或大项目二部的组织ID列表中
+- `F_DeleteMark == null`(未删除)
+- `F_EnabledMark == 1`(已启用)
+
+**获取步骤**:
+1. 从 `BASE_ORGANIZE` 表查找组织名称包含"大项目一部"或"大项目二部"的组织
+2. 获取这些组织的ID列表
+3. 从 `BASE_USER` 表查询岗位为"主管"且组织ID在上述组织ID列表中的员工
+
+**说明**:
+- 目前有"大项目一部"和"大项目二部",未来可能还有更多大项目部
+- 岗位字段值必须是"主管"(完全匹配)
+
+---
+
+### 管理的门店归属
+
+**数据来源**:`lq_md_target` 表
+
+**关联关系**:
+- 通过 `F_MajorProjectDepartment` 字段关联到 `BASE_ORGANIZE.F_Id`(大项目一部或大项目二部的组织ID)
+- 通过 `F_StoreId` 字段关联到 `lq_mdxx.F_Id`
+- 通过 `F_Month` 字段(YYYYMM格式)关联到统计月份
+
+**获取逻辑**:
+1. 从 `lq_md_target` 表查询指定月份(`F_Month = @统计月份`)的记录
+2. 筛选出 `F_MajorProjectDepartment = @大项目一部组织ID` 或 `F_MajorProjectDepartment = @大项目二部组织ID` 的记录
+3. 获取这些记录的 `F_StoreId` 列表,即为该大项目主管管理的门店
+
+**重要说明**:
+- 每个大项目主管可能管理多个门店
+- 需要统计这些门店的总业绩(开单-退卡)总和
+- 如果某个门店在 `lq_md_target` 表中没有记录,则该门店的业绩不计入该大项目主管的统计
+
+---
+
+### 总业绩统计
+
+**定义**:该大项目主管管理的所有门店中,开单金额总和减去退卡金额总和
+
+**数据来源表及字段**:
+
+| 业绩类型 | 数据表 | 字段 | 说明 |
+|---------|--------|------|------|
+| **开单金额** | `lq_kd_kdjlb` | `sfyj` | 门店开单实付金额 |
+| **退卡金额** | `lq_hytk_hytk` | `F_ActualRefundAmount` 或 `tkje` | 门店退卡金额 |
+
+**统计逻辑**:
+
+1. **统计开单金额**(按管理的门店筛选):
+ ```sql
+ SELECT COALESCE(SUM(billing.sfyj), 0) as BillingAmount
+ FROM lq_kd_kdjlb billing
+ WHERE billing.F_IsEffective = 1
+ AND billing.djmd IN (@管理的门店ID列表)
+ AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
+ ```
+
+2. **统计退卡金额**(按管理的门店筛选):
+ ```sql
+ SELECT COALESCE(SUM(refund.F_ActualRefundAmount), 0) as RefundAmount
+ FROM lq_hytk_hytk refund
+ WHERE refund.F_IsEffective = 1
+ AND refund.djmd IN (@管理的门店ID列表)
+ AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
+ ```
+
+3. **计算净总业绩**:
+ - 净总业绩 = 开单金额 - 退卡金额
+
+---
+
+## 🔗 归属规则
+
+### 大项目主管与门店的归属关系
+
+**数据来源**:`lq_md_target` 表
+
+**表结构说明**:
+- `F_Id`:主键ID
+- `F_StoreId`:门店ID(关联 `lq_mdxx.F_Id`)
+- `F_Month`:月份(YYYYMM格式)
+- `F_MajorProjectDepartment`:归属大项目部(关联 `BASE_ORGANIZE.F_Id`)
+
+**获取管理的门店**:
+1. 从 `lq_md_target` 表查询:
+ ```sql
+ SELECT DISTINCT F_StoreId
+ FROM lq_md_target
+ WHERE F_MajorProjectDepartment = @大项目一部或大项目二部组织ID
+ AND F_Month = @统计月份
+ ```
+
+2. 如果该大项目主管在指定月份没有管理的门店,则总业绩为0,提成为0
+
+**重要说明**:
+- 每个大项目主管可能管理多个门店
+- 需要统计这些门店的总业绩(开单-退卡)总和
+- 如果某个门店在 `lq_md_target` 表中没有记录,则该门店的业绩不计入该大项目主管的统计
+
+---
+
+## 🔄 计算流程
+
+### 步骤1:识别大项目主管
+
+1. 从 `BASE_ORGANIZE` 表中筛选:
+ - `F_FullName LIKE '%大项目一部%'` 或 `F_FullName LIKE '%大项目二部%'`
+ - `F_DeleteMark == null`(未删除)
+ - `F_EnabledMark == 1`(已启用)
+
+2. 获取这些组织的ID列表
+
+3. 从 `BASE_USER` 表中筛选:
+ - `F_GW == "主管"`(岗位为"主管")
+ - `F_OrganizeId` 在上述组织ID列表中
+ - `F_DeleteMark == null`(未删除)
+ - `F_EnabledMark == 1`(已启用)
+
+---
+
+### 步骤2:获取管理的门店
+
+1. 从 `lq_md_target` 表查询指定月份(`F_Month = @统计月份`)的记录
+2. 筛选出 `F_MajorProjectDepartment = @大项目一部组织ID` 或 `F_MajorProjectDepartment = @大项目二部组织ID` 的记录
+3. 获取这些记录的 `F_StoreId` 列表,即为该大项目主管管理的门店
+
+---
+
+### 步骤3:统计总业绩
+
+1. **统计开单金额**:
+ - 从 `lq_kd_kdjlb` 表统计管理的门店在统计月份的开单金额(`sfyj`字段)
+ - 过滤条件:`F_IsEffective = 1`,`djmd IN (@管理的门店ID列表)`,`DATE_FORMAT(kdrq, '%Y%m') = @统计月份`
+
+2. **统计退卡金额**:
+ - 从 `lq_hytk_hytk` 表统计管理的门店在统计月份的退卡金额(`F_ActualRefundAmount` 或 `tkje`字段)
+ - 过滤条件:`F_IsEffective = 1`,`djmd IN (@管理的门店ID列表)`,`DATE_FORMAT(tkrq, '%Y%m') = @统计月份`
+
+3. **计算净总业绩**:
+ - 净总业绩 = 开单金额 - 退卡金额
+
+---
+
+### 步骤4:计算提成
+
+根据总业绩分段计算提成:
+
+- 如果总业绩 ≤ 50万:提成 = 0(无提成)
+- 如果 50万 < 总业绩 ≤ 70万:提成 = 总业绩 × 1%
+- 如果总业绩 > 70万:提成 = 总业绩 × 1.5%
+
+---
+
+### 步骤5:计算应发工资
+
+- 应发工资 = 底薪(3500元)+ 提成金额
+
+---
+
+### 步骤6:保存数据
+
+将计算结果保存到 `lq_major_project_director_salary_statistics` 表中:
+- 如果已存在当月数据,则更新;否则插入新数据
+- 保存门店明细(JSON格式)
+
+---
+
+## ✅ 总结
+
+大项目主管工资计算规则相对简单明确:
+1. **底薪固定**:3500元,无任何条件
+2. **业绩提成**:根据管理的所有门店的总业绩分段计算,最高1.5%,低于50万无提成
+
+关键点:
+- 必须从 `BASE_USER` 表识别大项目主管(岗位为"主管",组织ID为大项目一部或大项目二部)
+- 必须从 `lq_md_target` 表获取管理的门店(通过 `F_MajorProjectDepartment` 字段)
+- 必须正确统计总业绩(开单金额 - 退卡金额)
+- 必须按管理的门店筛选,只统计该大项目主管管理的门店
+- 采用分段方式计算提成(不是分段累进),整个总业绩按对应比例计算
+
diff --git a/库存平均单价计算逻辑说明.md b/库存平均单价计算逻辑说明.md
new file mode 100644
index 0000000..84f3aaf
--- /dev/null
+++ b/库存平均单价计算逻辑说明.md
@@ -0,0 +1,151 @@
+# 库存平均单价计算逻辑说明
+
+## 概述
+
+采用**加权平均成本法(Weighted Average Cost Method)**来计算库存的平均单价,用于出库计价。
+
+## 核心原则
+
+1. **入库时**:根据当前平均单价和入库单价,计算新的平均单价
+2. **出库时**:使用当前平均单价,出库后平均单价不变
+3. **平均单价维护**:存储在 `lq_product.F_AveragePrice` 字段中
+
+## 计算公式
+
+### 入库时计算新平均单价
+
+```
+新平均单价 = (当前平均单价 × 当前可用库存数量 + 入库单价 × 入库数量) / (当前可用库存数量 + 入库数量)
+```
+
+其中:
+- **当前可用库存数量** = 总库存数量 - 已领取数量
+- **入库单价**:
+ - 采购入库:使用采购单价(`F_PurchaseUnitPrice`)
+ - 普通入库:使用产品价格(`F_Price`)
+
+### 出库时使用平均单价
+
+```
+出库单价 = 产品平均单价(F_AveragePrice)
+出库总金额 = 出库单价 × 出库数量
+```
+
+**注意**:出库后,平均单价不变(因为已经领取的库存已经按照当时的平均单价计价了)
+
+## 示例说明
+
+### 示例1:基本流程
+
+**初始状态**:
+- 商品A:当前平均单价 = 150
+- 库存A:数量1,单价100
+- 库存B:数量1,单价200
+
+**步骤1:领取1个**
+- 使用平均单价:150
+- 领取后剩余:1个库存
+- 平均单价不变:150
+
+**步骤2:再次入库1个,单价50**
+- 当前可用库存数量:1
+- 当前库存总金额:150 × 1 = 150
+- 入库金额:50 × 1 = 50
+- **新平均单价** = (150 + 50) / (1 + 1) = **100**
+
+### 示例2:多次入库
+
+**初始状态**:
+- 商品B:当前平均单价 = 100,当前可用库存 = 10
+
+**步骤1:采购入库5个,单价120**
+- 当前库存总金额:100 × 10 = 1000
+- 入库金额:120 × 5 = 600
+- **新平均单价** = (1000 + 600) / (10 + 5) = **106.67**
+
+**步骤2:领取3个**
+- 使用平均单价:106.67
+- 领取后剩余:12个库存
+- 平均单价不变:106.67
+
+**步骤3:再次采购入库8个,单价110**
+- 当前可用库存数量:12
+- 当前库存总金额:106.67 × 12 = 1280.04
+- 入库金额:110 × 8 = 880
+- **新平均单价** = (1280.04 + 880) / (12 + 8) = **108.00**
+
+## 数据库字段
+
+### 产品表(lq_product)
+
+- `F_AveragePrice`:平均单价(加权平均成本,用于出库计价)
+
+### 库存表(lq_inventory)
+
+- `F_PurchaseUnitPrice`:采购单价(采购入库时使用)
+- `F_FinalAmount`:产品最终金额(优先用于计算单价)
+- `F_Quantity`:库存数量
+
+### 库存使用记录表(lq_inventory_usage)
+
+- `F_UnitPrice`:使用时的单价(从产品平均单价获取)
+- `F_TotalAmount`:使用总金额(单价 × 数量)
+
+## 实现逻辑
+
+### 1. 入库时(LqInventoryService.CreateAsync / UpdateAsync)
+
+```csharp
+// 计算当前可用库存数量
+var currentAvailableQuantity = totalInventoryQuantity - totalUsageQuantity;
+
+// 确定入库单价
+decimal incomingPrice = stockInType == 2 && purchaseUnitPrice.HasValue
+ ? purchaseUnitPrice.Value // 采购入库
+ : product.Price; // 普通入库
+
+// 计算新平均单价
+if (currentAvailableQuantity <= 0)
+{
+ newAveragePrice = incomingPrice; // 没有库存时,直接使用入库单价
+}
+else
+{
+ var currentTotalAmount = product.AveragePrice * currentAvailableQuantity;
+ var incomingAmount = incomingPrice * incomingQuantity;
+ newAveragePrice = (currentTotalAmount + incomingAmount) / (currentAvailableQuantity + incomingQuantity);
+}
+
+// 更新产品平均单价
+product.AveragePrice = newAveragePrice;
+```
+
+### 2. 出库时(LqInventoryUsageService.CreateAsync / ConfirmReceiveAsync)
+
+```csharp
+// 使用产品的平均单价
+var unitPrice = product.AveragePrice > 0 ? product.AveragePrice : product.Price;
+var totalAmount = unitPrice * usageQuantity;
+
+// 创建使用记录(平均单价不变)
+usageEntity.UnitPrice = unitPrice;
+usageEntity.TotalAmount = totalAmount;
+```
+
+## 初始化历史数据
+
+对于已有库存的产品,根据当前库存计算初始平均单价:
+
+```sql
+平均单价 = SUM(库存金额) / SUM(库存数量)
+```
+
+其中,库存金额优先使用 `F_FinalAmount`,其次使用 `F_PurchaseUnitPrice × F_Quantity`。
+
+## 注意事项
+
+1. **平均单价维护**:每次入库时自动更新,出库时不变
+2. **可用库存计算**:需要考虑已领取的数量
+3. **普通入库**:如果没有采购单价,使用产品价格作为入库单价
+4. **历史数据初始化**:首次添加字段时,需要根据现有库存计算初始平均单价
+5. **数据一致性**:确保入库和出库操作在事务中执行,保证平均单价的一致性
diff --git a/科技部总经理工资计算规则梳理.md b/科技部总经理工资计算规则梳理.md
new file mode 100644
index 0000000..f598d1c
--- /dev/null
+++ b/科技部总经理工资计算规则梳理.md
@@ -0,0 +1,644 @@
+# 科技部总经理工资计算规则梳理
+
+## 📋 目录
+- [概述](#概述)
+- [计算规则](#计算规则)
+- [数据来源](#数据来源)
+- [归属规则](#归属规则)
+- [计算流程](#计算流程)
+- [注意事项](#注意事项)
+
+---
+
+## 📋 概述
+
+科技部总经理工资由以下几个部分组成:
+1. **底薪**:固定4000元
+2. **溯源金额提成**:根据管理的所有门店的溯源金额总和分段提成
+3. **Cell金额提成**:根据管理的所有门店的Cell金额总和分段提成
+
+**重要说明**:
+- 底薪固定为4000元,不设档位,不设条件
+- 科技部总经理从 `BASE_USER` 表获取,岗位字段(`F_GW`)为"科技一部"、"科技二部"等(未来可能还有更多)
+- 每个科技部总经理管理的门店归属在 `lq_md_general_manager_lifeline` 表中
+- 需要统计该科技部总经理管理的**所有门店**的溯源金额和Cell金额总和
+- 溯源金额和Cell金额分别计算提成,互不影响
+- 提成采用分段累进方式计算
+
+---
+
+## 💰 计算规则
+
+### 1. 底薪规则
+
+**固定底薪**:4000元
+
+- 无论业绩多少,底薪固定为4000元
+- 不设档位,不设条件
+- 不设考核扣款
+
+---
+
+### 2. 溯源金额提成规则
+
+**提成计算方式**:根据管理的所有门店的溯源金额总和分段累进计算
+
+| 溯源金额范围 | 提成比例 |
+|------------|---------|
+| < 200,000元 | 1% |
+| 200,000元 - 300,000元 | 1.5% |
+| 300,000元 - 500,000元 | 2% |
+| ≥ 500,000元 | 2.5% |
+
+**计算说明**:
+- 提成金额 = 溯源金额 × 对应提成比例
+- 采用分段累进方式计算,不同区间按不同比例计算
+- 例如:溯源金额为350,000元
+ - 0-200,000元部分:200,000 × 1% = 2,000元
+ - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
+ - 300,000-350,000元部分:50,000 × 2% = 1,000元
+ - 总提成 = 2,000 + 1,500 + 1,000 = 4,500元
+
+---
+
+### 3. Cell金额提成规则
+
+**提成计算方式**:根据管理的所有门店的Cell金额总和分段累进计算
+
+| Cell金额范围 | 提成比例 |
+|------------|---------|
+| < 50,000元 | 0% (无提成) |
+| 50,000元 - 400,000元 | 1% |
+| ≥ 400,000元 | 1.5% |
+
+**计算说明**:
+- 如果Cell金额 < 50,000元,无提成
+- 如果Cell金额 ≥ 50,000元,按分段累进方式计算
+- 例如:Cell金额为450,000元
+ - 0-50,000元部分:无提成
+ - 50,000-400,000元部分:350,000 × 1% = 3,500元
+ - 400,000-450,000元部分:50,000 × 1.5% = 750元
+ - 总提成 = 3,500 + 750 = 4,250元
+
+---
+
+## 📊 数据来源
+
+### 科技部总经理识别
+
+**数据来源**:`BASE_USER` 表
+
+**识别条件**:
+- `F_GW`(岗位字段)包含"科技一部"、"科技二部"等(如:`F_GW LIKE '%科技一部%'` 或 `F_GW LIKE '%科技二部%'`)
+- `F_DeleteMark == null`(未删除)
+- `F_EnabledMark == 1`(已启用)
+
+**说明**:
+- 目前有"科技一部"和"科技二部",未来可能还有更多科技部
+- 岗位字段值可能是"科技一部"、"科技二部"等完整名称
+
+---
+
+### 管理的门店归属
+
+**数据来源**:`lq_md_general_manager_lifeline` 表
+
+**关联关系**:
+- 通过 `F_GeneralManagerId` 字段关联到 `BASE_USER.F_Id`
+- 通过 `F_StoreId` 字段关联到 `lq_mdxx.F_Id`
+- 通过 `F_Month` 字段(YYYYMM格式)关联到统计月份
+
+**获取逻辑**:
+1. 从 `lq_md_general_manager_lifeline` 表查询指定月份(`F_Month = @统计月份`)的记录
+2. 筛选出 `F_GeneralManagerId = @科技部总经理ID` 的记录
+3. 获取这些记录的 `F_StoreId` 列表,即为该科技部总经理管理的门店
+
+**重要说明**:
+- 每个科技部总经理可能管理多个门店
+- 需要统计这些门店的溯源金额和Cell金额总和
+
+---
+
+### 溯源金额统计
+
+**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "溯源系统" 或 "溯源" 的品项明细的实付金额总和(开单金额 - 退卡金额)
+
+**数据来源表及字段**:
+
+| 数据表 | 字段 | 说明 |
+|--------|------|------|
+| `lq_kd_pxmx` | `F_ActualPrice` | 品项明细实付金额 |
+| `lq_kd_pxmx` | `F_BeautyType` | 科美类型(用于区分溯源和Cell) |
+| `lq_kd_kdjlb` | `kdrq` | 开单日期(用于按月统计) |
+| `lq_kd_kdjlb` | `djmd` | 单据门店ID(用于筛选管理的门店) |
+| `lq_xmzl` | `F_BeautyType` | 品项的科美类型(如果明细表没有,从品项表获取) |
+| `lq_hytk_mx` | `tkje` | 退卡明细退款金额 |
+| `lq_hytk_hytk` | `tkrq` | 退卡日期(用于按月统计) |
+| `lq_hytk_hytk` | `djmd` | 单据门店ID(用于筛选管理的门店) |
+
+**统计逻辑**:
+
+1. **统计开单溯源金额**(按管理的门店筛选):
+ ```sql
+ SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as TraceabilityAmount
+ FROM lq_kd_pxmx pxmx
+ INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
+ INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
+ WHERE pxmx.F_IsEffective = 1
+ AND billing.F_IsEffective = 1
+ AND item.F_IsEffective = 1
+ AND (pxmx.F_BeautyType = '溯源系统' OR pxmx.F_BeautyType = '溯源'
+ OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源')
+ AND billing.djmd IN (@管理的门店ID列表)
+ AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
+ ```
+
+2. **统计退卡溯源金额**(按管理的门店筛选):
+ ```sql
+ SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundTraceabilityAmount
+ FROM lq_hytk_mx tkmx
+ INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id
+ INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id
+ WHERE tkmx.F_IsEffective = 1
+ AND refund.F_IsEffective = 1
+ AND item.F_IsEffective = 1
+ AND (tkmx.F_BeautyType = '溯源系统' OR tkmx.F_BeautyType = '溯源'
+ OR item.F_BeautyType = '溯源系统' OR item.F_BeautyType = '溯源')
+ AND refund.djmd IN (@管理的门店ID列表)
+ AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
+ ```
+
+3. **计算净溯源金额**:
+ - 净溯源金额 = 开单溯源金额 - 退卡溯源金额
+
+---
+
+### Cell金额统计
+
+**定义**:该科技部总经理管理的所有门店中,品项的 `F_BeautyType` 为 "cell" 或 "Cell" 的品项明细的实付金额总和(开单金额 - 退卡金额)
+
+**数据来源表及字段**:同溯源金额统计
+
+**统计逻辑**:
+
+1. **统计开单Cell金额**(按管理的门店筛选):
+ ```sql
+ SELECT COALESCE(SUM(pxmx.F_ActualPrice), 0) as CellAmount
+ FROM lq_kd_pxmx pxmx
+ INNER JOIN lq_kd_kdjlb billing ON pxmx.glkdbh = billing.F_Id
+ INNER JOIN lq_xmzl item ON pxmx.px = item.F_Id
+ WHERE pxmx.F_IsEffective = 1
+ AND billing.F_IsEffective = 1
+ AND item.F_IsEffective = 1
+ AND (pxmx.F_BeautyType = 'cell' OR pxmx.F_BeautyType = 'Cell'
+ OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell')
+ AND billing.djmd IN (@管理的门店ID列表)
+ AND DATE_FORMAT(billing.kdrq, '%Y%m') = @统计月份
+ ```
+
+2. **统计退卡Cell金额**(按管理的门店筛选):
+ ```sql
+ SELECT COALESCE(SUM(tkmx.tkje), 0) as RefundCellAmount
+ FROM lq_hytk_mx tkmx
+ INNER JOIN lq_hytk_hytk refund ON tkmx.glhytkbh = refund.F_Id
+ INNER JOIN lq_xmzl item ON tkmx.px = item.F_Id
+ WHERE tkmx.F_IsEffective = 1
+ AND refund.F_IsEffective = 1
+ AND item.F_IsEffective = 1
+ AND (tkmx.F_BeautyType = 'cell' OR tkmx.F_BeautyType = 'Cell'
+ OR item.F_BeautyType = 'cell' OR item.F_BeautyType = 'Cell')
+ AND refund.djmd IN (@管理的门店ID列表)
+ AND DATE_FORMAT(refund.tkrq, '%Y%m') = @统计月份
+ ```
+
+3. **计算净Cell金额**:
+ - 净Cell金额 = 开单Cell金额 - 退卡Cell金额
+
+---
+
+## 🔗 归属规则
+
+### 科技部总经理与门店的归属关系
+
+**数据来源**:`lq_md_general_manager_lifeline` 表
+
+**表结构说明**:
+- `F_Id`:主键ID
+- `F_StoreId`:门店ID(关联 `lq_mdxx.F_Id`)
+- `F_Month`:月份(YYYYMM格式)
+- `F_GeneralManagerId`:总经理用户ID(关联 `BASE_USER.F_Id`)
+- `F_ManagerType`:经理类型(0=经理,1=总经理)
+
+**获取管理的门店**:
+1. 从 `lq_md_general_manager_lifeline` 表查询:
+ ```sql
+ SELECT DISTINCT F_StoreId
+ FROM lq_md_general_manager_lifeline
+ WHERE F_GeneralManagerId = @科技部总经理ID
+ AND F_Month = @统计月份
+ ```
+
+2. 如果该科技部总经理在指定月份没有管理的门店,则溯源金额和Cell金额为0,提成为0
+
+**重要说明**:
+- 每个科技部总经理可能管理多个门店
+- 需要统计这些门店的溯源金额和Cell金额总和
+- 如果某个门店在 `lq_md_general_manager_lifeline` 表中没有记录,则该门店的业绩不计入该科技部总经理的统计
+
+---
+
+## 🔄 计算流程
+
+### 步骤1:识别科技部总经理
+
+从 `BASE_USER` 表中筛选:
+- `F_GW LIKE '%科技一部%'` 或 `F_GW LIKE '%科技二部%'` 等(根据实际岗位名称)
+- `F_DeleteMark == null`(未删除)
+- `F_EnabledMark == 1`(已启用)
+
+**注意**:未来可能还有"科技三部"等,需要灵活处理岗位识别逻辑
+
+---
+
+### 步骤2:获取管理的门店
+
+1. 从 `lq_md_general_manager_lifeline` 表查询该科技部总经理在指定月份管理的门店:
+ ```sql
+ SELECT DISTINCT F_StoreId
+ FROM lq_md_general_manager_lifeline
+ WHERE F_GeneralManagerId = @科技部总经理ID
+ AND F_Month = @统计月份
+ ```
+
+2. 如果查询结果为空,说明该科技部总经理在该月份没有管理的门店,溯源金额和Cell金额为0,提成为0
+
+---
+
+### 步骤3:统计溯源金额(所有管理的门店总和)
+
+1. **统计开单溯源金额**(按管理的门店筛选):
+ - 从 `lq_kd_pxmx` 表统计
+ - 关联 `lq_kd_kdjlb` 表获取开单日期和门店ID
+ - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
+ - 筛选条件:
+ - `lq_kd_pxmx.F_IsEffective = 1`(有效记录)
+ - `lq_kd_kdjlb.F_IsEffective = 1`(有效开单)
+ - `lq_xmzl.F_IsEffective = 1`(有效品项)
+ - `F_BeautyType = '溯源系统'` 或 `'溯源'`
+ - `lq_kd_kdjlb.djmd IN (@管理的门店ID列表)`
+ - 开单日期在统计月份范围内
+ - 汇总:`SUM(lq_kd_pxmx.F_ActualPrice)`
+
+2. **统计退卡溯源金额**(按管理的门店筛选):
+ - 从 `lq_hytk_mx` 表统计
+ - 关联 `lq_hytk_hytk` 表获取退卡日期和门店ID
+ - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
+ - 筛选条件:
+ - `lq_hytk_mx.F_IsEffective = 1`(有效记录)
+ - `lq_hytk_hytk.F_IsEffective = 1`(有效退卡)
+ - `lq_xmzl.F_IsEffective = 1`(有效品项)
+ - `F_BeautyType = '溯源系统'` 或 `'溯源'`
+ - `lq_hytk_hytk.djmd IN (@管理的门店ID列表)`
+ - 退卡日期在统计月份范围内
+ - 汇总:`SUM(lq_hytk_mx.tkje)`
+
+3. **计算净溯源金额**:
+ - 净溯源金额 = 开单溯源金额 - 退卡溯源金额
+
+---
+
+### 步骤4:统计Cell金额(所有管理的门店总和)
+
+1. **统计开单Cell金额**(按管理的门店筛选):
+ - 从 `lq_kd_pxmx` 表统计
+ - 关联 `lq_kd_kdjlb` 表获取开单日期和门店ID
+ - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
+ - 筛选条件:
+ - `lq_kd_pxmx.F_IsEffective = 1`(有效记录)
+ - `lq_kd_kdjlb.F_IsEffective = 1`(有效开单)
+ - `lq_xmzl.F_IsEffective = 1`(有效品项)
+ - `F_BeautyType = 'cell'` 或 `'Cell'`
+ - `lq_kd_kdjlb.djmd IN (@管理的门店ID列表)`
+ - 开单日期在统计月份范围内
+ - 汇总:`SUM(lq_kd_pxmx.F_ActualPrice)`
+
+2. **统计退卡Cell金额**(按管理的门店筛选):
+ - 从 `lq_hytk_mx` 表统计
+ - 关联 `lq_hytk_hytk` 表获取退卡日期和门店ID
+ - 关联 `lq_xmzl` 表获取品项的 `F_BeautyType`
+ - 筛选条件:
+ - `lq_hytk_mx.F_IsEffective = 1`(有效记录)
+ - `lq_hytk_hytk.F_IsEffective = 1`(有效退卡)
+ - `lq_xmzl.F_IsEffective = 1`(有效品项)
+ - `F_BeautyType = 'cell'` 或 `'Cell'`
+ - `lq_hytk_hytk.djmd IN (@管理的门店ID列表)`
+ - 退卡日期在统计月份范围内
+ - 汇总:`SUM(lq_hytk_mx.tkje)`
+
+3. **计算净Cell金额**:
+ - 净Cell金额 = 开单Cell金额 - 退卡Cell金额
+
+---
+
+### 步骤5:工资计算
+
+#### 5.1 计算底薪
+- 底薪 = 4000元(固定)
+
+#### 5.2 计算溯源金额提成
+
+根据净溯源金额范围确定提成比例,采用分段累进方式:
+
+- **如果净溯源金额 < 200,000元**:
+ - 提成金额 = 净溯源金额 × 1%
+
+- **如果 200,000元 ≤ 净溯源金额 < 300,000元**:
+ - 0-200,000元部分:200,000 × 1% = 2,000元
+ - 200,000元以上部分:(净溯源金额 - 200,000) × 1.5%
+ - 总提成 = 2,000 + (净溯源金额 - 200,000) × 1.5%
+
+- **如果 300,000元 ≤ 净溯源金额 < 500,000元**:
+ - 0-200,000元部分:200,000 × 1% = 2,000元
+ - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
+ - 300,000元以上部分:(净溯源金额 - 300,000) × 2%
+ - 总提成 = 2,000 + 1,500 + (净溯源金额 - 300,000) × 2%
+
+- **如果净溯源金额 ≥ 500,000元**:
+ - 0-200,000元部分:200,000 × 1% = 2,000元
+ - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
+ - 300,000-500,000元部分:200,000 × 2% = 4,000元
+ - 500,000元以上部分:(净溯源金额 - 500,000) × 2.5%
+ - 总提成 = 2,000 + 1,500 + 4,000 + (净溯源金额 - 500,000) × 2.5%
+
+#### 5.3 计算Cell金额提成
+
+根据净Cell金额范围确定提成比例,采用分段累进方式:
+
+- **如果净Cell金额 < 50,000元**:
+ - 提成金额 = 0(无提成)
+
+- **如果 50,000元 ≤ 净Cell金额 < 400,000元**:
+ - 0-50,000元部分:无提成
+ - 50,000元以上部分:(净Cell金额 - 50,000) × 1%
+ - 总提成 = (净Cell金额 - 50,000) × 1%
+
+- **如果净Cell金额 ≥ 400,000元**:
+ - 0-50,000元部分:无提成
+ - 50,000-400,000元部分:350,000 × 1% = 3,500元
+ - 400,000元以上部分:(净Cell金额 - 400,000) × 1.5%
+ - 总提成 = 3,500 + (净Cell金额 - 400,000) × 1.5%
+
+#### 5.4 计算最终工资
+
+- **应发工资** = 底薪 + 溯源金额提成 + Cell金额提成
+
+---
+
+## 📝 注意事项
+
+1. **数据一致性**:
+ - 溯源金额和Cell金额的统计逻辑必须与其他统计接口保持一致
+ - 必须扣除退卡金额,确保数据准确性
+ - 必须按管理的门店筛选,只统计该科技部总经理管理的门店
+
+2. **数据校验**:
+ - `F_BeautyType` 字段值可能不统一("溯源系统"、"溯源"、"cell"、"Cell"),需要兼容处理
+ - 优先使用明细表的 `F_BeautyType`,如果为空则使用品项表的 `F_BeautyType`
+ - 如果科技部总经理在指定月份没有管理的门店,溯源金额和Cell金额为0,提成为0
+
+3. **归属关系**:
+ - 必须从 `lq_md_general_manager_lifeline` 表获取管理的门店
+ - 如果某个门店不在该科技部总经理的管理范围内,该门店的业绩不计入统计
+ - 需要确保 `lq_md_general_manager_lifeline` 表的数据准确性
+
+4. **边界情况**:
+ - 如果溯源金额或Cell金额为0或负数,对应提成为0
+ - 如果溯源金额或Cell金额计算后为负数(退卡金额大于开单金额),对应提成为0
+ - 如果科技部总经理在指定月份没有管理的门店,所有金额为0,提成为0
+
+5. **计算精度**:
+ - 涉及金额计算时,建议保留2位小数
+ - 提成金额四舍五入到分
+
+6. **性能优化**:
+ - 建议使用数据库聚合查询,避免在应用层进行大量数据计算
+ - 可以先获取管理的门店列表,然后在SQL中使用 `IN` 子句筛选
+ - 可以考虑创建统计视图或物化视图,提高查询性能
+
+7. **岗位识别**:
+ - 目前有"科技一部"和"科技二部",未来可能还有更多
+ - 建议使用模糊匹配(`LIKE '%科技一部%'`)或维护一个岗位列表
+ - 需要确保岗位识别逻辑能够适应未来的扩展
+
+---
+
+## 📊 计算示例
+
+### 示例1:基础计算
+
+**假设数据**:
+- 科技部总经理:科技一部(ID: 123456)
+- 管理的门店:门店A(ID: A001)、门店B(ID: B001)
+- 底薪:4000元
+- 门店A溯源金额:150,000元,Cell金额:80,000元
+- 门店B溯源金额:100,000元,Cell金额:120,000元
+- 总溯源金额:250,000元
+- 总Cell金额:200,000元
+
+**计算过程**:
+
+1. **溯源金额提成**:
+ - 0-200,000元部分:200,000 × 1% = 2,000元
+ - 200,000-250,000元部分:50,000 × 1.5% = 750元
+ - 溯源金额提成 = 2,000 + 750 = 2,750元
+
+2. **Cell金额提成**:
+ - 0-50,000元部分:无提成
+ - 50,000-200,000元部分:150,000 × 1% = 1,500元
+ - Cell金额提成 = 1,500元
+
+3. **应发工资**:
+ - 应发工资 = 4,000 + 2,750 + 1,500 = 8,250元
+
+---
+
+### 示例2:高业绩计算
+
+**假设数据**:
+- 科技部总经理:科技二部(ID: 789012)
+- 管理的门店:门店C(ID: C001)、门店D(ID: D001)、门店E(ID: E001)
+- 底薪:4000元
+- 门店C溯源金额:200,000元,Cell金额:150,000元
+- 门店D溯源金额:250,000元,Cell金额:200,000元
+- 门店E溯源金额:150,000元,Cell金额:100,000元
+- 总溯源金额:600,000元
+- 总Cell金额:450,000元
+
+**计算过程**:
+
+1. **溯源金额提成**:
+ - 0-200,000元部分:200,000 × 1% = 2,000元
+ - 200,000-300,000元部分:100,000 × 1.5% = 1,500元
+ - 300,000-500,000元部分:200,000 × 2% = 4,000元
+ - 500,000-600,000元部分:100,000 × 2.5% = 2,500元
+ - 溯源金额提成 = 2,000 + 1,500 + 4,000 + 2,500 = 10,000元
+
+2. **Cell金额提成**:
+ - 0-50,000元部分:无提成
+ - 50,000-400,000元部分:350,000 × 1% = 3,500元
+ - 400,000-450,000元部分:50,000 × 1.5% = 750元
+ - Cell金额提成 = 3,500 + 750 = 4,250元
+
+3. **应发工资**:
+ - 应发工资 = 4,000 + 10,000 + 4,250 = 18,250元
+
+---
+
+## 🔍 数据表结构参考
+
+### 科技部总经理工资统计表
+
+参考其他岗位的工资表结构(如:`lq_business_unit_manager_salary_statistics`、`lq_tech_teacher_salary_statistics`),建议创建如下表结构:
+
+```sql
+-- ============================================
+-- 创建科技部总经理工资统计表
+-- 功能:存储科技部总经理每月的工资计算数据,包括底薪、溯源金额提成、Cell金额提成、扣款、补贴、奖金、支付等信息
+-- 创建时间:2025年
+-- ============================================
+
+DROP TABLE IF EXISTS lq_tech_general_manager_salary_statistics;
+
+CREATE TABLE lq_tech_general_manager_salary_statistics (
+ -- 主键
+ F_Id VARCHAR(50) NOT NULL COMMENT '主键ID',
+
+ -- 一、基础信息字段
+ F_StatisticsMonth VARCHAR(6) NOT NULL COMMENT '统计月份(YYYYMM格式)',
+ F_Position VARCHAR(50) NOT NULL COMMENT '核算岗位(科技一部/科技二部等)',
+ F_EmployeeName VARCHAR(100) NOT NULL COMMENT '员工姓名',
+ F_EmployeeId VARCHAR(50) NOT NULL COMMENT '员工ID',
+ F_EmployeeAccount VARCHAR(100) NULL COMMENT '员工账号',
+ F_IsTerminated INT NOT NULL DEFAULT 0 COMMENT '是否离职(0=在职,1=离职)',
+
+ -- 二、管理的门店信息(JSON格式)
+ F_StoreDetail TEXT NULL COMMENT '管理的门店明细(JSON格式,记录每个门店的溯源金额和Cell金额详情)',
+
+ -- 三、业绩相关字段
+ F_TraceabilityAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额(管理的所有门店的溯源金额总和,开单-退卡)',
+ F_CellAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额(管理的所有门店的Cell金额总和,开单-退卡)',
+
+ -- 四、底薪相关字段
+ F_BaseSalary DECIMAL(18,2) NOT NULL DEFAULT 4000.00 COMMENT '底薪金额(固定4000元)',
+
+ -- 五、提成相关字段
+ F_TraceabilityCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT '溯源金额提成比例(分段计算,存储平均比例)',
+ F_TraceabilityCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '溯源金额提成金额',
+ F_CellCommissionRate DECIMAL(18,4) DEFAULT NULL COMMENT 'Cell金额提成比例(分段计算,存储平均比例)',
+ F_CellCommissionAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 'Cell金额提成金额',
+ F_TotalCommission DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '提成合计(溯源提成+Cell提成)',
+
+ -- 六、考勤相关字段
+ F_WorkingDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '在店天数',
+ F_LeaveDays DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假天数',
+
+ -- 七、工资计算字段
+ F_CalculatedGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '核算应发工资(底薪 + 提成合计)',
+ F_FinalGrossSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '最终应发工资(等于核算应发工资)',
+
+ -- 八、补贴相关字段
+ F_MonthlyTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月培训补贴',
+ F_MonthlyTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月交通补贴',
+ F_LastMonthTrainingSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月培训补贴',
+ F_LastMonthTransportSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '上月交通补贴',
+ F_TotalSubsidy DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补贴合计',
+
+ -- 九、扣款相关字段
+ F_MissingCard DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '缺卡扣款',
+ F_LateArrival DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '迟到扣款',
+ F_LeaveDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '请假扣款',
+ F_SocialInsuranceDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣社保',
+ F_RewardDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣除奖励',
+ F_AccommodationDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣住宿费',
+ F_StudyPeriodDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣学习期费用',
+ F_WorkClothesDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣工作服费用',
+ F_TotalDeduction DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '扣款合计',
+
+ -- 十、奖金相关字段
+ F_Bonus DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '发奖金',
+ F_ReturnPhoneDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退手机押金',
+ F_ReturnAccommodationDeposit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '退住宿押金',
+
+ -- 十一、支付相关字段
+ F_ActualSalary DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '实发工资(最终应发工资 - 扣款合计 + 补贴合计 + 奖金)',
+ F_MonthlyPaymentStatus VARCHAR(20) NOT NULL DEFAULT '未发放' COMMENT '当月是否发放(已发放/未发放/部分发放)',
+ F_PaidAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '支付金额',
+ F_PendingAmount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '待支付金额',
+ F_LastMonthSupplement DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '补发上月',
+ F_MonthlyTotalPayment DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '当月支付总额',
+
+ -- 十二、系统字段
+ F_IsLocked INT NOT NULL DEFAULT 0 COMMENT '是否锁定(0=未锁定,1=已锁定)',
+ F_CreateTime DATETIME NOT NULL COMMENT '创建时间',
+ F_UpdateTime DATETIME NOT NULL COMMENT '更新时间',
+ F_CreateUser VARCHAR(50) NULL COMMENT '创建人',
+ F_UpdateUser VARCHAR(50) NULL COMMENT '更新人',
+
+ -- 主键约束
+ PRIMARY KEY (F_Id),
+
+ -- 唯一索引:确保同一员工同一月份只有一条记录
+ UNIQUE KEY `uk_employee_month` (F_EmployeeId, F_StatisticsMonth),
+
+ -- 普通索引
+ KEY `idx_statistics_month` (F_StatisticsMonth),
+ KEY `idx_employee_id` (F_EmployeeId),
+ KEY `idx_employee_account` (F_EmployeeAccount),
+ KEY `idx_position` (F_Position),
+ KEY `idx_is_terminated` (F_IsTerminated),
+ KEY `idx_create_time` (F_CreateTime)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科技部总经理工资统计表';
+```
+
+**门店明细JSON格式示例**:
+```json
+[
+ {
+ "storeId": "A001",
+ "storeName": "门店A",
+ "traceabilityBillingAmount": 160000.00,
+ "traceabilityRefundAmount": 10000.00,
+ "traceabilityAmount": 150000.00,
+ "cellBillingAmount": 85000.00,
+ "cellRefundAmount": 5000.00,
+ "cellAmount": 80000.00
+ },
+ {
+ "storeId": "B001",
+ "storeName": "门店B",
+ "traceabilityBillingAmount": 105000.00,
+ "traceabilityRefundAmount": 5000.00,
+ "traceabilityAmount": 100000.00,
+ "cellBillingAmount": 125000.00,
+ "cellRefundAmount": 5000.00,
+ "cellAmount": 120000.00
+ }
+]
+```
+
+---
+
+## ✅ 总结
+
+科技部总经理工资计算规则相对简单明确:
+1. **底薪固定**:4000元,无任何条件
+2. **溯源提成**:根据管理的所有门店的溯源金额总和分段累进,最高2.5%
+3. **Cell提成**:根据管理的所有门店的Cell金额总和分段累进,最高1.5%,低于5万无提成
+
+关键点:
+- 必须从 `BASE_USER` 表识别科技部总经理(岗位为"科技一部"、"科技二部"等)
+- 必须从 `lq_md_general_manager_lifeline` 表获取管理的门店
+- 必须正确区分溯源和Cell类型(通过 `F_BeautyType` 字段)
+- 必须扣除退卡金额,确保数据准确性
+- 必须按管理的门店筛选,只统计该科技部总经理管理的门店
+- 采用分段累进方式计算提成,确保计算准确