From ae5b1a80eb50e085728b33b75122901a9957324e Mon Sep 17 00:00:00 2001 From: chenlin Date: Fri, 8 Nov 2024 18:58:41 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E9=9B=B6=E5=94=AE=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E5=AF=BC=E5=87=BAexcel=E4=BC=98=E5=8C=96=EF=BC=8C=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E5=95=86=E5=93=81=E5=88=86=E7=B1=BB=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E9=A1=B9=EF=BC=9B=202=E3=80=81=E9=87=87=E8=B4=AD=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=8F=8D=E5=AE=A1=E6=A0=B8=E5=90=8E=E5=BA=93=E5=AD=98?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=94=B9=E4=B8=BA9=EF=BC=9B=203=E3=80=81?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=BB=8F=E8=90=A5=E6=97=A5=E6=8A=A5=E8=A1=A8?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/apis/erpordermanage/erp_order.go | 33 ++ app/admin/models/category.go | 21 + app/admin/models/commodity.go | 1 + app/admin/models/erp_order.go | 511 ++++++++++++++++++--- app/admin/models/purchase.go | 4 +- app/admin/models/store.go | 32 ++ app/admin/router/erpordermanage.go | 1 + docs/docs.go | 143 ++++++ docs/swagger.json | 143 ++++++ docs/swagger.yaml | 100 ++++ 10 files changed, 925 insertions(+), 64 deletions(-) diff --git a/app/admin/apis/erpordermanage/erp_order.go b/app/admin/apis/erpordermanage/erp_order.go index 182452d..4efa29d 100644 --- a/app/admin/apis/erpordermanage/erp_order.go +++ b/app/admin/apis/erpordermanage/erp_order.go @@ -590,3 +590,36 @@ func ErpOrderShowAllData(c *gin.Context) { app.OK(c, nil, "") return } + +// ErpOrderDailyReport 经营日报表 +// @Summary 经营日报表 +// @Tags 零售报表 +// @Produce json +// @Accept json +// @Param request body models.ErpOrderDailyReportReq true "经营日报表模型" +// @Success 200 {object} models.ErpOrderDailyReportResp +// @Router /api/v1/erp_order/daily_report [post] +func ErpOrderDailyReport(c *gin.Context) { + var req = new(model.ErpOrderDailyReportReq) + if err := c.ShouldBindJSON(&req); err != nil { + logger.Error("ShouldBindJSON err:", logger.Field("err", err)) + app.Error(c, http.StatusBadRequest, err, "参数错误:"+err.Error()) + return + } + + err := tools.Validate(req) //必填参数校验 + if err != nil { + app.Error(c, http.StatusBadRequest, err, err.Error()) + return + } + + resp, err := model.QueryOrderDailyReport(req, c) + if err != nil { + logger.Error("QueryOrderDailyReport err:", logger.Field("err", err)) + app.Error(c, http.StatusInternalServerError, err, "查询失败:"+err.Error()) + return + } + + app.OK(c, resp, "") + return +} diff --git a/app/admin/models/category.go b/app/admin/models/category.go index 2c08073..d25332f 100644 --- a/app/admin/models/category.go +++ b/app/admin/models/category.go @@ -326,3 +326,24 @@ func GetCategoryLevelsFromMap(categoryID uint32, categoryMap map[uint32]Category return levels } + +// GetAllFirstCategories 查询所有1级分类 +func GetAllFirstCategories() ([]Category, error) { + var categories []Category + err := orm.Eloquent.Model(&Category{}).Where("pid = 0 and display = 1").Find(&categories).Error + if err != nil { + return nil, err + } + + return categories, nil +} + +// getCategoryNameByID 获取分类名称 +func getCategoryNameByCategoryID(categoryID uint32) string { + // 假设有一个GetCategoryByID函数返回分类信息 + category, err := GetCategoryById(categoryID) + if err != nil { + return "未知分类" + } + return category.Name +} diff --git a/app/admin/models/commodity.go b/app/admin/models/commodity.go index 7dcd2e6..e558512 100644 --- a/app/admin/models/commodity.go +++ b/app/admin/models/commodity.go @@ -36,6 +36,7 @@ const ( SystemOut = 5 // 系统出库 CheckOut = 6 // 盘点出库 OnSale = 7 // 销售锁定中 + PurchaseCancel = 8 // 采购订单反审核 ) // ErpStock 库存列表 diff --git a/app/admin/models/erp_order.go b/app/admin/models/erp_order.go index 4e04079..0707350 100644 --- a/app/admin/models/erp_order.go +++ b/app/admin/models/erp_order.go @@ -3781,70 +3781,54 @@ func retailDetailExport(list []ErpOrder, sumData RetailDetailTotalData, c *gin.C flag10, _ := checkRoleMenu(c, DetailStorePerMenu) nEndCount := 0 - title := []interface{}{"订单编号", "订单类型", "用户ID", "客户手机号", "日期", "审核时间", "店铺", "银联流水号", "销售员1", "销售员2", "商品分类", "", "", "商品名称", - "供应商", "是否串码", "商品串码", "是否赠送", "销售数量", "指导零售价", "零售价", "零售优惠", "会员优惠", "实际零售价/退货价"} - - title2 := []interface{}{"订单编号", "订单类型", "用户ID", "客户手机号", "日期", "审核时间", "店铺", "银联流水号", "销售员1", "销售员2", "一级分类", "二级分类", "三级分类", "商品名称", + title := []interface{}{"订单编号", "订单类型", "用户ID", "客户手机号", "日期", "审核时间", "店铺", "银联流水号", "销售员1", "销售员2", "一级分类", "二级分类", "三级分类", "商品名称", "供应商", "是否串码", "商品串码", "是否赠送", "销售数量", "指导零售价", "零售价", "零售优惠", "会员优惠", "实际零售价/退货价"} if flag1 { // 采购单价 title = append(title, "采购单价") - title2 = append(title2, "采购单价") nEndCount += 1 } if flag2 { // 员工成本价 title = append(title, "员工成本价") - title2 = append(title2, "员工成本价") nEndCount += 1 } if flag3 { // 销售毛利 title = append(title, "销售毛利") - title2 = append(title2, "销售毛利") nEndCount += 1 } if flag4 { // 员工毛利 title = append(title, "员工毛利") - title2 = append(title2, "员工毛利") nEndCount += 1 } title = append(title, "订单总指导零售价", "订单总优惠", "订单实收", "【扫码付", "现金收款", "pos机收款", "商场积分抵扣", "其他付款方式】") - title2 = append(title2, "订单总指导零售价", "订单总优惠", "订单实收", "【扫码付", "现金收款", "pos机收款", - "商场积分抵扣", "其他付款方式】") if flag5 { // 订单总销售毛利 title = append(title, "订单总销售毛利") - title2 = append(title2, "订单总销售毛利") nEndCount += 1 } if flag6 { // 订单总员工毛利 title = append(title, "订单总员工毛利") - title2 = append(title2, "订单总员工毛利") nEndCount += 1 } if flag7 { // 销售毛利提成 title = append(title, "销售毛利提成") - title2 = append(title2, "销售毛利提成") nEndCount += 1 } if flag8 { // 员工毛利提成 title = append(title, "员工毛利提成") - title2 = append(title2, "员工毛利提成") nEndCount += 1 } if flag9 { // 销售员提成 title = append(title, "销售员提成") - title2 = append(title2, "销售员提成") nEndCount += 1 } if flag10 { // 门店提成 title = append(title, "门店提成") - title2 = append(title2, "门店提成") nEndCount += 1 } title = append(title, "备注") - title2 = append(title2, "备注") for i, _ := range title { cell, _ := excelize.CoordinatesToCellName(1+i, 1) @@ -3854,14 +3838,6 @@ func retailDetailExport(list []ErpOrder, sumData RetailDetailTotalData, c *gin.C } } - for i, _ := range title2 { - cell, _ := excelize.CoordinatesToCellName(1+i, 2) - err := file.SetCellValue(fSheet, cell, title2[i]) - if err != nil { - logger.Error("file set value err:", logger.Field("err", err)) - } - } - categoryMap, err := GetAllCategories() if err != nil { // 处理错误 @@ -4246,7 +4222,7 @@ func retailDetailExport(list []ErpOrder, sumData RetailDetailTotalData, c *gin.C } for j, _ := range row { - cell, _ := excelize.CoordinatesToCellName(1+j, nExcelStartRow+3) + cell, _ := excelize.CoordinatesToCellName(1+j, nExcelStartRow+2) err = file.SetCellValue(fSheet, cell, row[j]) if err != nil { logger.Error("file set value err:", logger.Field("err", err)) @@ -4434,7 +4410,7 @@ func retailDetailExport(list []ErpOrder, sumData RetailDetailTotalData, c *gin.C end = append(end, "") for i, _ := range end { - cell, _ := excelize.CoordinatesToCellName(1+i, nExcelStartRow+3) + cell, _ := excelize.CoordinatesToCellName(1+i, nExcelStartRow+2) err := file.SetCellValue(fSheet, cell, end[i]) if err != nil { logger.Error("file set value err:", logger.Field("err", err)) @@ -4451,27 +4427,27 @@ func retailDetailExport(list []ErpOrder, sumData RetailDetailTotalData, c *gin.C var endRow string switch nEndCount { case 1: - endRow = fmt.Sprintf("AH"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AH"+"%d", nExcelStartRow+2) case 2: - endRow = fmt.Sprintf("AI"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AI"+"%d", nExcelStartRow+2) case 3: - endRow = fmt.Sprintf("AJ"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AJ"+"%d", nExcelStartRow+2) case 4: - endRow = fmt.Sprintf("AK"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AK"+"%d", nExcelStartRow+2) case 5: - endRow = fmt.Sprintf("AL"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AL"+"%d", nExcelStartRow+2) case 6: - endRow = fmt.Sprintf("AM"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AM"+"%d", nExcelStartRow+2) case 7: - endRow = fmt.Sprintf("AN"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AN"+"%d", nExcelStartRow+2) case 8: - endRow = fmt.Sprintf("AO"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AO"+"%d", nExcelStartRow+2) case 9: - endRow = fmt.Sprintf("AP"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AP"+"%d", nExcelStartRow+2) case 10: - endRow = fmt.Sprintf("AQ"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AQ"+"%d", nExcelStartRow+2) default: - endRow = fmt.Sprintf("AG"+"%d", nExcelStartRow+3) + endRow = fmt.Sprintf("AG"+"%d", nExcelStartRow+2) } //endRow := fmt.Sprintf("AL%d", nExcelStartRow+2) // 应用样式到整个表格 @@ -4486,34 +4462,14 @@ func retailDetailExport(list []ErpOrder, sumData RetailDetailTotalData, c *gin.C {"type":"bottom","color":"000000","style":1}]}`) //需要自动换行的列 - endRow2 := fmt.Sprintf("AI%d", nExcelStartRow+3) - endRow3 := fmt.Sprintf("AJ%d", nExcelStartRow+3) - endRow4 := fmt.Sprintf("AK%d", nExcelStartRow+3) + endRow2 := fmt.Sprintf("AI%d", nExcelStartRow+2) + endRow3 := fmt.Sprintf("AJ%d", nExcelStartRow+2) + endRow4 := fmt.Sprintf("AK%d", nExcelStartRow+2) _ = file.SetCellStyle("Sheet1", "A1", "AN1", style1) _ = file.SetCellStyle("Sheet1", "AL2", endRow2, style1) _ = file.SetCellStyle("Sheet1", "AJ2", endRow3, style1) _ = file.SetCellStyle("Sheet1", "AK2", endRow4, style1) - // 合并单元格 - // 定义要排除的列 - excludeCols := map[string]bool{"K": true, "L": true, "M": true} - // 合并 J1-L1 - _ = file.MergeCell("Sheet1", "K1", "M1") - // 根据 nEndCount 动态合并列 - for i := 0; i < len(title); i++ { - // 获取当前列名 - currentCol := getExcelColumnName(i) - // 跳过 J、K、L 列(已通过 J1-L1 合并处理) - if excludeCols[currentCol] { - continue - } - - // 合并当前列的第 1 行和第 2 行 - startCell := fmt.Sprintf("%s1", currentCol) - endCell := fmt.Sprintf("%s2", currentCol) - _ = file.MergeCell("Sheet1", startCell, endCell) - } - // 设置单元格大小 file.SetColWidth("Sheet1", "A", "A", 15) file.SetColWidth("Sheet1", "D", "D", 15) @@ -5587,11 +5543,26 @@ func QueryReceiptData(req *ErpOrderDeleteReq, c *gin.Context) (*ErpOrderReceiptD resp.IsShowImei = 1 } + var categoryList []uint32 + if sysUser.CooperativeBusinessId == 1 { // 迪为特殊处理 + // 获取任天堂游戏的子分类 + categoryList, _ = GetSubcategoryIds(4) + } + var totalCouponAmount float64 commodityMap := make(map[string]TableData, 0) for i, item := range order.Commodities { var tableData TableData - tableData.Name = item.ErpCommodityName + if Contains(categoryList, item.ErpCategoryId) { + category, err := GetCategoryById(4) + if err != nil { + tableData.Name = "任天堂游戏" + } else { + tableData.Name = category.Name + } + } else { + tableData.Name = item.ErpCommodityName + } tableData.SL = uint32(item.Count) tableData.DJ = item.RetailPrice tableData.JE = float64(item.Count) * item.RetailPrice @@ -6507,3 +6478,419 @@ func updatePayWayData(gdb *gorm.DB, orderId uint32, req *ErpOrderCreateReq) erro return nil } + +// ErpOrderDailyReportReq 经营日报表入参 +type ErpOrderDailyReportReq struct { + StartTime string `json:"start_time"` // 开始时间 + EndTime string `json:"end_time"` // 结束时间 + PageIndex int `json:"pageIndex"` // 页码 + PageSize int `json:"pageSize"` // 页面条数 +} + +// ErpOrderDailyReportResp 经营日报表出参 +type ErpOrderDailyReportResp struct { + Total int `json:"total"` // 总条数(总订单数) + PageIndex int `json:"pageIndex"` // 页码 + PageSize int `json:"pageSize"` // 每页展示条数 + TotalAmount float64 `json:"total_amount"` // 合计总金额 + TotalSalesProfit float64 `json:"total_sales_profit"` // 合计总毛利(销售毛利;店员看员工毛利) + List []DailyReport `json:"list"` // 零售明细 +} + +// DailyReport 经营日报表 +type DailyReport struct { + StoreId uint32 `json:"store_id"` // 门店ID + StoreName string `json:"store_name"` // 门店名称 + StoreAmount float64 `json:"store_amount"` // 门店销售金额 + StoreSalesProfit float64 `json:"store_sales_profit"` // 门店销售毛利(销售毛利;店员看员工毛利) + CategorySales []CategorySalesData `json:"category_sales"` // 各分类的销售数据 +} + +// CategorySalesData 门店分类销售数据 +type CategorySalesData struct { + CategoryID uint32 `json:"category_id"` // 一级分类ID + CategoryName string `json:"category_name"` // 一级分类名称 + SaleTitle string `json:"sale_title"` // 金额标题 (例如: "主机金额") + TotalSaleAmount float64 `json:"total_sale_amount"` // 销售金额 + CostTitle string `json:"cost_title"` // 成本标题 (例如: "主机成本") + TotalCostAmount float64 `json:"total_cost_amount"` // 销售成本 + ProfitTitle string `json:"profit_title"` // 毛利标题 (例如: "主机毛利") + TotalProfitAmount float64 `json:"total_profit_amount"` // 销售利润 +} + +// QueryOrderDailyReport 生成经营日报表 +func QueryOrderDailyReport(req *ErpOrderDailyReportReq, c *gin.Context) (*ErpOrderDailyReportResp, error) { + page := req.PageIndex - 1 + if page < 0 { + page = 0 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + + resp := &ErpOrderDailyReportResp{ + PageIndex: page + 1, + PageSize: req.PageSize, + } + + // 查询用户所属门店 + var storeList []uint32 + sysUser, err := GetSysUserByCtx(c) + if err != nil { + return nil, err + } + if !(tools.GetRoleName(c) == "admin" || tools.GetRoleName(c) == "系统管理员") { + // 返回sysUser未过期的门店id列表 + storeList = GetValidStoreIDs(sysUser.StoreData) + } else { + storeList, err = GetAllStoreIdsByCooperativeBusinessId(sysUser.CooperativeBusinessId) + if err != nil { + return nil, errors.New("用户未绑定门店") + } + } + if len(storeList) == 0 { + return nil, errors.New("用户未绑定门店") + } + + storeMap, err := GetAllStoreData() + if err != nil { + return nil, err + } + + // 查询所有门店和分类的零售信息 + var sumData []RetailDetailSumData + sumData, err = getSumDataByStoreAndCategory(req) + if err != nil { + return nil, err + } + + var nTotalAmount, nTotalSalesProfit float64 + var storeDataList []DailyReport + for _, store := range storeList { + var categoryDataList []CategorySalesData + var nStoreAmount, nStoreSalesProfit float64 + + // 判断门店名称中是否包含"哈曼" + displayCategories := getDisplayCategories(storeMap[store].Name) + + for _, item := range sumData { + nTotalAmount += item.Amount + nTotalSalesProfit += item.SalesProfit + + // 仅当门店ID匹配且分类ID在展示分类中时,才添加分类数据 + if item.StoreId == store && containsCategory(displayCategories, item.ErpCategoryId) { + nStoreAmount += item.Amount + nStoreSalesProfit += item.TotalSalesProfit + + // 处理单个分类数据 + categoryData := processCategoryData(item) + categoryDataList = append(categoryDataList, categoryData) + } + } + + // 如果分类数据不足4个,补充缺失的分类,金额为0 + categoryDataList = appendMissingCategories(categoryDataList, displayCategories) + + // 排序分类数据列表 + sort.Slice(categoryDataList, func(i, j int) bool { + return categoryDataList[i].CategoryID < categoryDataList[j].CategoryID + }) + + // 构建门店数据 + storeData := DailyReport{ + StoreId: store, + StoreName: storeMap[store].Name, + StoreAmount: tools.RoundToTwoDecimalPlaces(nStoreAmount), + StoreSalesProfit: tools.RoundToTwoDecimalPlaces(nStoreSalesProfit), + CategorySales: categoryDataList, + } + storeDataList = append(storeDataList, storeData) + } + + sort.Slice(storeDataList, func(i, j int) bool { + return storeDataList[i].StoreAmount > storeDataList[j].StoreAmount + }) + + // 计算分页所需的切片索引 + startIndex := page * req.PageSize + endIndex := (page + 1) * req.PageSize + if endIndex > len(storeDataList) { + endIndex = len(storeDataList) + } + resp.List = storeDataList[startIndex:endIndex] + resp.Total = len(storeDataList) + resp.PageIndex = req.PageIndex + resp.PageSize = req.PageSize + resp.TotalAmount = tools.RoundToTwoDecimalPlaces(nTotalAmount) + resp.TotalSalesProfit = tools.RoundToTwoDecimalPlaces(nTotalSalesProfit) + + return resp, nil +} + +// 获取分类展示列表 +func getDisplayCategories(storeName string) []uint32 { + if strings.Contains(storeName, "哈曼") { + return []uint32{27, 34} // 如果包含"哈曼",只显示分类27和34 + } + return []uint32{1, 4, 7, 13} // 否则显示分类1、4、7、13 +} + +// 处理单个分类数据 +func processCategoryData(item RetailDetailSumData) CategorySalesData { + var categoryName string + switch item.ErpCategoryId { + case 1: + categoryName = "主机" + case 4: + categoryName = "游戏" + case 7: + categoryName = "配件" + case 13: + categoryName = "周边" + default: + categoryName = item.ErpCategoryName + } + + return CategorySalesData{ + CategoryID: item.ErpCategoryId, + CategoryName: item.ErpCategoryName, + SaleTitle: categoryName + "销售金额", + TotalSaleAmount: tools.RoundToTwoDecimalPlaces(item.Amount), + CostTitle: categoryName + "销售成本", + TotalCostAmount: tools.RoundToTwoDecimalPlaces(item.WholesalePrice), + ProfitTitle: categoryName + "销售利润", + TotalProfitAmount: tools.RoundToTwoDecimalPlaces(item.SalesProfit), + } +} + +// 补充缺失的分类数据 +func appendMissingCategories(categoryDataList []CategorySalesData, displayCategories []uint32) []CategorySalesData { + for _, categoryId := range displayCategories { + if !containsCategorySalesData(categoryDataList, categoryId) { + var categoryName string + switch categoryId { + case 1: + categoryName = "主机" + case 4: + categoryName = "游戏" + case 7: + categoryName = "配件" + case 13: + categoryName = "周边" + case 27: + categoryName = "哈曼卡顿" + case 34: + categoryName = "JBL" + } + + categoryDataList = append(categoryDataList, CategorySalesData{ + CategoryID: categoryId, + CategoryName: categoryName, + SaleTitle: categoryName + "销售金额", + TotalSaleAmount: 0, // 补充分类金额为0 + CostTitle: categoryName + "销售成本", + TotalCostAmount: 0, // 补充分类成本为0 + ProfitTitle: categoryName + "销售利润", + TotalProfitAmount: 0, // 补充分类利润为0 + }) + } + } + return categoryDataList +} + +// RetailDetailSumData 零售明细相关金额汇总 +type RetailDetailSumData struct { + StoreId uint32 `json:"store_id" gorm:"index"` // 门店id + StoreName string `json:"store_name"` // 门店名称 + ErpCategoryId uint32 `json:"erp_category_id" gorm:"index"` // 分类id + ErpCategoryName string `json:"erp_category_name"` // 分类名称 + Count int32 `json:"count"` // 销售数量 + RetailPrice float64 `json:"retail_price"` // 指导零售价 + SalePrice float64 `json:"sale_price"` // 零售价 + SaleDiscount float64 `json:"sale_discount"` // 零售优惠 + MemberDiscount float64 `json:"member_discount"` // 会员优惠 + VmDiscount float64 `json:"vm_discount"` // 会员积分抵扣 + CouponDiscount float64 `json:"coupon_discount"` // 优惠券抵扣 + Amount float64 `json:"amount"` // 实际零售价 + WholesalePrice float64 `json:"wholesale_price"` // 采购单价 + StaffPrice float64 `json:"staff_price"` // 员工成本价 + SalesProfit float64 `json:"sales_profit"` // 销售毛利 + StaffProfit float64 `json:"staff_profit"` // 员工毛利 + TotalRetailPrice float64 `json:"total_retail_price"` // 订单总指导零售价 + TotalDiscount float64 `json:"total_discount"` // 订单总优惠:订单所有商品零售优惠+会员优惠+会员积分抵扣之和 + TotalAmount float64 `json:"total_amount"` // 订单实收金额 + TotalCashierData + TotalSalesProfit float64 `json:"total_sales_profit"` // 订单总销售毛利 + TotalStaffProfit float64 `json:"total_staff_profit"` // 订单总员工毛利 + TotalPerData + StorePer float64 `json:"store_per"` // 门店提成 +} + +// 查询零售和退货订单的汇总并直接做减法,增加时间筛选条件 +func getSumDataByStoreAndCategory(req *ErpOrderDailyReportReq) ([]RetailDetailSumData, error) { + var result []RetailDetailSumData + + // 解析开始时间 + startTimeCondition := "" + if req.StartTime != "" { + parse, err := time.Parse(QueryTimeFormat, req.StartTime) + if err != nil { + logger.Errorf("开始时间解析错误: %v", err) + return result, err + } + startTimeCondition = fmt.Sprintf("AND eo.audit_time > '%s'", parse.Format(QueryTimeFormat)) + } + + // 解析结束时间 + endTimeCondition := "" + if req.EndTime != "" { + parse, err := time.Parse(QueryTimeFormat, req.EndTime) + if err != nil { + logger.Errorf("结束时间解析错误: %v", err) + return result, err + } + endTimeCondition = fmt.Sprintf("AND eo.audit_time < '%s'", parse.Format(QueryTimeFormat)) + } + + // 获取所有1级分类 + firstCategories, err := GetAllFirstCategories() + if err != nil { + logger.Error("获取一级分类错误", logger.Field("err", err)) + return result, err + } + + // 获取所有一级分类对应的子分类IDs + categorySubIds := make(map[uint32][]uint32) + for _, category := range firstCategories { + subCategoryIDs, err := GetSubcategoryIds(category.ID) + if err != nil { + logger.Error("获取子分类ID错误", logger.Field("err", err), logger.Field("categoryID", category.ID)) + return result, err + } + categorySubIds[category.ID] = subCategoryIDs + } + + err = orm.Eloquent.Raw(fmt.Sprintf(` + SELECT + retail.store_id, + retail.store_name, + retail.erp_category_id, + retail.erp_category_name, + COALESCE(retail.count, 0) - COALESCE(rejected.count, 0) AS count, + COALESCE(retail.retail_price, 0) - COALESCE(rejected.retail_price, 0) AS retail_price, + COALESCE(retail.sale_price, 0) - COALESCE(rejected.sale_price, 0) AS sale_price, + COALESCE(retail.sale_discount, 0) - COALESCE(rejected.sale_discount, 0) AS sale_discount, + COALESCE(retail.member_discount, 0) - COALESCE(rejected.member_discount, 0) AS member_discount, + COALESCE(retail.amount, 0) - COALESCE(rejected.amount, 0) AS amount, + COALESCE(retail.wholesale_price, 0) - COALESCE(rejected.wholesale_price, 0) AS wholesale_price, + COALESCE(retail.staff_price, 0) - COALESCE(rejected.staff_price, 0) AS staff_price, + COALESCE(retail.sales_profit, 0) - COALESCE(rejected.sales_profit, 0) AS sales_profit, + COALESCE(retail.staff_profit, 0) - COALESCE(rejected.staff_profit, 0) AS staff_profit, + COALESCE(retail.total_amount, 0) - COALESCE(rejected.total_amount, 0) AS total_amount, + COALESCE(retail.total_sales_profit, 0) - COALESCE(rejected.total_sales_profit, 0) AS total_sales_profit, + COALESCE(retail.total_staff_profit, 0) - COALESCE(rejected.total_staff_profit, 0) AS total_staff_profit + FROM ( + SELECT eo.store_id, eo.store_name, oc.erp_category_id, oc.erp_category_name, + SUM(oc.count) AS count, + SUM(oc.retail_price) AS retail_price, + SUM(oc.sale_price) AS sale_price, + SUM(oc.sale_discount) AS sale_discount, + SUM(oc.member_discount) AS member_discount, + SUM(oc.amount) AS amount, + SUM(oc.wholesale_price) AS wholesale_price, + (SUM(oc.wholesale_price) + SUM(oc.staff_cost_price)) AS staff_price, + (SUM(oc.amount) - SUM(oc.wholesale_price)) AS sales_profit, + (SUM(oc.amount) - SUM(oc.wholesale_price) - SUM(oc.staff_cost_price)) AS staff_profit, + SUM(eo.total_amount) as total_amount, + SUM(eo.total_sales_profit) as total_sales_profit, + SUM(eo.total_staff_profit) as total_staff_profit + FROM erp_order eo + JOIN erp_order_commodity oc ON eo.id = oc.erp_order_id + WHERE eo.retail_type = ? %s %s + GROUP BY eo.store_id, eo.store_name, oc.erp_category_id, oc.erp_category_name + ) AS retail + LEFT JOIN ( + SELECT eo.store_id, eo.store_name, oc.erp_category_id, oc.erp_category_name, + SUM(oc.count) AS count, + SUM(oc.retail_price) AS retail_price, + SUM(oc.sale_price) AS sale_price, + SUM(oc.sale_discount) AS sale_discount, + SUM(oc.member_discount) AS member_discount, + SUM(oc.rejected_amount) AS amount, + SUM(oc.wholesale_price) AS wholesale_price, + (SUM(oc.wholesale_price) + SUM(oc.staff_cost_price)) AS staff_price, + SUM(oc.sales_profit) AS sales_profit, + SUM(oc.staff_profit) AS staff_profit, + SUM(eo.total_amount) as total_amount, + SUM(eo.total_sales_profit) as total_sales_profit, + SUM(eo.total_staff_profit) as total_staff_profit + FROM erp_order eo + JOIN erp_order_commodity oc ON eo.id = oc.erp_order_id + WHERE eo.retail_type = ? %s %s + GROUP BY eo.store_id, eo.store_name, oc.erp_category_id, oc.erp_category_name + ) AS rejected ON retail.store_id = rejected.store_id AND retail.erp_category_id = rejected.erp_category_id + `, startTimeCondition, endTimeCondition, startTimeCondition, endTimeCondition), RetailTypeSale, RetailTypeRejected).Scan(&result).Error + + if err != nil { + logger.Error("query data with subtraction err:", logger.Field("err", err)) + return result, err + } + + // 合并子分类数据(根据门店ID和父分类合并,并累加金额) + mergedResult := make(map[string]RetailDetailSumData) + + for i := range result { + for parentID, subCategories := range categorySubIds { + if containsCategory(subCategories, result[i].ErpCategoryId) { + key := fmt.Sprintf("%d-%d", result[i].StoreId, parentID) + + if existing, exists := mergedResult[key]; exists { + existing.Count += result[i].Count + existing.RetailPrice += result[i].RetailPrice + existing.SalePrice += result[i].SalePrice + existing.SaleDiscount += result[i].SaleDiscount + existing.MemberDiscount += result[i].MemberDiscount + existing.Amount += result[i].Amount + existing.WholesalePrice += result[i].WholesalePrice + existing.StaffPrice += result[i].StaffPrice + existing.SalesProfit += result[i].SalesProfit + existing.StaffProfit += result[i].StaffProfit + mergedResult[key] = existing + } else { + result[i].ErpCategoryId = parentID + //result[i].ErpCategoryName = getCategoryNameByCategoryID(parentID) + mergedResult[key] = result[i] + } + break + } + } + } + + // 转换合并结果为切片 + finalResult := make([]RetailDetailSumData, 0, len(mergedResult)) + for _, v := range mergedResult { + finalResult = append(finalResult, v) + } + + return finalResult, nil +} + +// containsCategory 检查分类ID是否在子分类IDs列表中 +func containsCategory(subCategoryIds []uint32, categoryId uint32) bool { + for _, id := range subCategoryIds { + if id == categoryId { + return true + } + } + return false +} + +// 辅助函数,用于检查分类是否已经存在于categoryDataList中 +func containsCategorySalesData(categoryDataList []CategorySalesData, categoryId uint32) bool { + for _, category := range categoryDataList { + if category.CategoryID == categoryId { + return true + } + } + return false +} diff --git a/app/admin/models/purchase.go b/app/admin/models/purchase.go index 423d154..b5f626f 100644 --- a/app/admin/models/purchase.go +++ b/app/admin/models/purchase.go @@ -1463,7 +1463,7 @@ func InventoryErpPurchaseUpdateRejectStock(gdb *gorm.DB, list []ErpPurchaseInven err = gdb.Table("erp_stock_commodity").Where("imei = ?", list[i].IMEI). Updates(&map[string]interface{}{ - "state": PurchaseReturn, + "state": PurchaseCancel, "stock_sn": list[i].SerialNumber, "updated_at": time.Now(), }).Error // 状态更新为采购退货 @@ -1526,7 +1526,7 @@ func InventoryErpPurchaseUpdateRejectStock(gdb *gorm.DB, list []ErpPurchaseInven return fmt.Errorf("商品[%s]采购退货数量超出实际库存数量,请先零售退货", list[i].ErpCommodityName) } err = gdb.Table("erp_stock_commodity").Where("id = ?", currentID).Updates(&map[string]interface{}{ - "state": PurchaseReturn, + "state": PurchaseCancel, "stock_sn": list[i].SerialNumber, "updated_at": time.Now(), }).Error // 状态更新为采购退货 diff --git a/app/admin/models/store.go b/app/admin/models/store.go index c203a9e..30e5b24 100644 --- a/app/admin/models/store.go +++ b/app/admin/models/store.go @@ -304,3 +304,35 @@ func GetStoreIdsByCooperativeBusinessId(id uint32) ([]uint32, error) { return ids, nil } + +func GetAllStoreIdsByCooperativeBusinessId(id uint32) ([]uint32, error) { + ids := make([]uint32, 0) + var stores []Store + err := orm.Eloquent.Table("store").Where("cooperative_business_id = ? and is_online = 1", id).Find(&stores).Error + if err != nil { + logger.Errorf("err:", logger.Field("err", err)) + return ids, err + } + + for i, _ := range stores { + ids = append(ids, stores[i].ID) + } + + return ids, nil +} + +func GetAllStoreData() (map[uint32]Store, error) { + storeMap := make(map[uint32]Store) + var stores []Store + err := orm.Eloquent.Table("store").Find(&stores).Error + if err != nil { + logger.Errorf("err:", logger.Field("err", err)) + return storeMap, err + } + + for _, store := range stores { + storeMap[store.ID] = store + } + + return storeMap, nil +} diff --git a/app/admin/router/erpordermanage.go b/app/admin/router/erpordermanage.go index 6c18457..455c0fc 100644 --- a/app/admin/router/erpordermanage.go +++ b/app/admin/router/erpordermanage.go @@ -23,4 +23,5 @@ func registerErpOrderManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJW r.POST("retail_detail", erpordermanage.ErpOrderRetailDetail) // 查询零售明细 r.POST("receipt_data", erpordermanage.ErpOrderReceiptData) // 查询小票数据 r.POST("show_all_data", erpordermanage.ErpOrderShowAllData) // 展示所有订单 + r.POST("daily_report", erpordermanage.ErpOrderDailyReport) // 经营日报表 } diff --git a/docs/docs.go b/docs/docs.go index 94da3dc..b38525d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1844,6 +1844,39 @@ const docTemplate = `{ } } }, + "/api/v1/erp_order/daily_report": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "零售订单" + ], + "summary": "经营日报表", + "parameters": [ + { + "description": "经营日报表模型", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ErpOrderDailyReportReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ErpOrderDailyReportResp" + } + } + } + } + }, "/api/v1/erp_order/delete": { "post": { "consumes": [ @@ -7000,6 +7033,43 @@ const docTemplate = `{ } } }, + "models.CategorySalesData": { + "type": "object", + "properties": { + "category_id": { + "description": "一级分类ID", + "type": "integer" + }, + "category_name": { + "description": "一级分类名称", + "type": "string" + }, + "cost_title": { + "description": "成本标题 (例如: \"主机成本\")", + "type": "string" + }, + "profit_title": { + "description": "毛利标题 (例如: \"主机毛利\")", + "type": "string" + }, + "sale_title": { + "description": "金额标题 (例如: \"主机金额\")", + "type": "string" + }, + "total_cost_amount": { + "description": "销售成本", + "type": "number" + }, + "total_profit_amount": { + "description": "销售利润", + "type": "number" + }, + "total_sale_amount": { + "description": "销售金额", + "type": "number" + } + } + }, "models.CommodityCreateRequest": { "type": "object", "required": [ @@ -7705,6 +7775,26 @@ const docTemplate = `{ } } }, + "models.DailyReport": { + "type": "object", + "properties": { + "category_sales": { + "description": "各分类的销售数据", + "type": "array", + "items": { + "$ref": "#/definitions/models.CategorySalesData" + } + }, + "store_id": { + "description": "门店ID", + "type": "integer" + }, + "store_name": { + "description": "门店名称", + "type": "string" + } + } + }, "models.DecisionReportData": { "type": "object", "properties": { @@ -9561,6 +9651,59 @@ const docTemplate = `{ } } }, + "models.ErpOrderDailyReportReq": { + "type": "object", + "properties": { + "end_time": { + "description": "结束时间", + "type": "string" + }, + "pageIndex": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "页面条数", + "type": "integer" + }, + "start_time": { + "description": "开始时间", + "type": "string" + } + } + }, + "models.ErpOrderDailyReportResp": { + "type": "object", + "properties": { + "list": { + "description": "零售明细", + "type": "array", + "items": { + "$ref": "#/definitions/models.DailyReport" + } + }, + "pageIndex": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页展示条数", + "type": "integer" + }, + "total": { + "description": "总条数(总订单数)", + "type": "integer" + }, + "total_amount": { + "description": "合计总金额", + "type": "number" + }, + "total_sales_profit": { + "description": "合计总毛利(销售毛利;店员看员工毛利)", + "type": "number" + } + } + }, "models.ErpOrderDeleteReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 325a364..83294da 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1833,6 +1833,39 @@ } } }, + "/api/v1/erp_order/daily_report": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "零售订单" + ], + "summary": "经营日报表", + "parameters": [ + { + "description": "经营日报表模型", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ErpOrderDailyReportReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ErpOrderDailyReportResp" + } + } + } + } + }, "/api/v1/erp_order/delete": { "post": { "consumes": [ @@ -6989,6 +7022,43 @@ } } }, + "models.CategorySalesData": { + "type": "object", + "properties": { + "category_id": { + "description": "一级分类ID", + "type": "integer" + }, + "category_name": { + "description": "一级分类名称", + "type": "string" + }, + "cost_title": { + "description": "成本标题 (例如: \"主机成本\")", + "type": "string" + }, + "profit_title": { + "description": "毛利标题 (例如: \"主机毛利\")", + "type": "string" + }, + "sale_title": { + "description": "金额标题 (例如: \"主机金额\")", + "type": "string" + }, + "total_cost_amount": { + "description": "销售成本", + "type": "number" + }, + "total_profit_amount": { + "description": "销售利润", + "type": "number" + }, + "total_sale_amount": { + "description": "销售金额", + "type": "number" + } + } + }, "models.CommodityCreateRequest": { "type": "object", "required": [ @@ -7694,6 +7764,26 @@ } } }, + "models.DailyReport": { + "type": "object", + "properties": { + "category_sales": { + "description": "各分类的销售数据", + "type": "array", + "items": { + "$ref": "#/definitions/models.CategorySalesData" + } + }, + "store_id": { + "description": "门店ID", + "type": "integer" + }, + "store_name": { + "description": "门店名称", + "type": "string" + } + } + }, "models.DecisionReportData": { "type": "object", "properties": { @@ -9550,6 +9640,59 @@ } } }, + "models.ErpOrderDailyReportReq": { + "type": "object", + "properties": { + "end_time": { + "description": "结束时间", + "type": "string" + }, + "pageIndex": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "页面条数", + "type": "integer" + }, + "start_time": { + "description": "开始时间", + "type": "string" + } + } + }, + "models.ErpOrderDailyReportResp": { + "type": "object", + "properties": { + "list": { + "description": "零售明细", + "type": "array", + "items": { + "$ref": "#/definitions/models.DailyReport" + } + }, + "pageIndex": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页展示条数", + "type": "integer" + }, + "total": { + "description": "总条数(总订单数)", + "type": "integer" + }, + "total_amount": { + "description": "合计总金额", + "type": "number" + }, + "total_sales_profit": { + "description": "合计总毛利(销售毛利;店员看员工毛利)", + "type": "number" + } + } + }, "models.ErpOrderDeleteReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cfd4c02..5f86cd7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -517,6 +517,33 @@ definitions: description: 更新时间 type: string type: object + models.CategorySalesData: + properties: + category_id: + description: 一级分类ID + type: integer + category_name: + description: 一级分类名称 + type: string + cost_title: + description: '成本标题 (例如: "主机成本")' + type: string + profit_title: + description: '毛利标题 (例如: "主机毛利")' + type: string + sale_title: + description: '金额标题 (例如: "主机金额")' + type: string + total_cost_amount: + description: 销售成本 + type: number + total_profit_amount: + description: 销售利润 + type: number + total_sale_amount: + description: 销售金额 + type: number + type: object models.CommodityCreateRequest: properties: brokerage_1: @@ -1034,6 +1061,20 @@ definitions: description: 备注 type: string type: object + models.DailyReport: + properties: + category_sales: + description: 各分类的销售数据 + items: + $ref: '#/definitions/models.CategorySalesData' + type: array + store_id: + description: 门店ID + type: integer + store_name: + description: 门店名称 + type: string + type: object models.DecisionReportData: properties: allot_in: @@ -2397,6 +2438,44 @@ definitions: - store_name - total_count type: object + models.ErpOrderDailyReportReq: + properties: + end_time: + description: 结束时间 + type: string + pageIndex: + description: 页码 + type: integer + pageSize: + description: 页面条数 + type: integer + start_time: + description: 开始时间 + type: string + type: object + models.ErpOrderDailyReportResp: + properties: + list: + description: 零售明细 + items: + $ref: '#/definitions/models.DailyReport' + type: array + pageIndex: + description: 页码 + type: integer + pageSize: + description: 每页展示条数 + type: integer + total: + description: 总条数(总订单数) + type: integer + total_amount: + description: 合计总金额 + type: number + total_sales_profit: + description: 合计总毛利(销售毛利;店员看员工毛利) + type: number + type: object models.ErpOrderDeleteReq: properties: bill_sn: @@ -9560,6 +9639,27 @@ paths: summary: 新建零售订单 tags: - 零售订单 + /api/v1/erp_order/daily_report: + post: + consumes: + - application/json + parameters: + - description: 经营日报表模型 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.ErpOrderDailyReportReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ErpOrderDailyReportResp' + summary: 经营日报表 + tags: + - 零售订单 /api/v1/erp_order/delete: post: consumes: