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` 字段) +- 必须扣除退卡金额,确保数据准确性 +- 必须按管理的门店筛选,只统计该科技部总经理管理的门店 +- 采用分段累进方式计算提成,确保计算准确