diff --git a/app/admin/apis/erpordermanage/erp_order.go b/app/admin/apis/erpordermanage/erp_order.go index c01d385..5f22cc1 100644 --- a/app/admin/apis/erpordermanage/erp_order.go +++ b/app/admin/apis/erpordermanage/erp_order.go @@ -3,12 +3,14 @@ package erpordermanage import ( "encoding/json" "errors" + "fmt" "github.com/gin-gonic/gin" model "go-admin/app/admin/models" orm "go-admin/common/global" "go-admin/logger" "go-admin/tools" "go-admin/tools/app" + "io" "net/http" "time" ) @@ -656,3 +658,54 @@ func ErpOrderSaleDetail(c *gin.Context) { app.OK(c, resp, "") return } + +// ErpOrderBatchImport 批量导入零售数据 +// @Summary 批量导入零售数据 +// @Tags 零售订单 +// @Produce json +// @Accept json +// @Param file body string true "上传excel文件" +// @Success 200 {object} models.ErpOrderBatchImportResp +// @Router /api/v1/erp_order/import [post] +func ErpOrderBatchImport(c *gin.Context) { + file, header, err := c.Request.FormFile("file") + if err != nil { + logger.Error("form file err:", logger.Field("err", err)) + app.Error(c, http.StatusInternalServerError, err, "预览失败") + return + } + + readAll, err := io.ReadAll(file) + if err != nil { + logger.Error("read all err:", logger.Field("err", err)) + app.Error(c, http.StatusInternalServerError, err, "预览失败") + return + } + + fmt.Println("header:", header.Filename) + _, colsMap, err := model.FileExcelImport(readAll, nil, 4) + if err != nil { + logger.Errorf("file excel reader err:", err) + app.Error(c, http.StatusInternalServerError, err, err.Error()) + return + } + + fmt.Println("colsMap:", colsMap) + if len(colsMap) != 0 { + colsMap = colsMap[1:] + } + + var orderCommodities []model.ErpOrderCommodity + orderCommodities, err = model.ImportOrderData(colsMap) + if err != nil { + app.Error(c, http.StatusInternalServerError, err, err.Error()) + return + } + + resp := model.ErpOrderBatchImportResp{ + Commodities: orderCommodities, + } + + app.OK(c, resp, "导入成功") + return +} diff --git a/app/admin/models/erp_order.go b/app/admin/models/erp_order.go index 426bb64..1a60afe 100644 --- a/app/admin/models/erp_order.go +++ b/app/admin/models/erp_order.go @@ -439,6 +439,10 @@ type TableData struct { IMEI string `json:"imei"` // 串码 } +type ErpOrderBatchImportResp struct { + Commodities []ErpOrderCommodity `json:"commodities"` // 零售订单商品信息 +} + // Contains 判断id是否在list中 func Contains(list []uint32, id uint32) bool { for _, item := range list { diff --git a/app/admin/models/file.go b/app/admin/models/file.go index 953775c..c62adc8 100644 --- a/app/admin/models/file.go +++ b/app/admin/models/file.go @@ -56,6 +56,17 @@ type StockExcel struct { Count string `json:"count" binding:"required"` // 数量 } +type OrderExcel struct { + SerialNum string `json:"serial_num"` // 商品编号 + Name string `json:"name"` // 商品名称 + IMEI string `json:"imei"` // 商品串码 + Count string `json:"count" binding:"required"` // 销售数量 + StoreName string `json:"store_name" binding:"required"` // 所属门店 + SalePrice string `json:"sale_price"` // 零售价 + PresentType string `json:"present_type"` // 是否赠送 + Remark string `json:"remark"` // 备注 +} + // 获取struct的tag标签,匹配excel导入数据 func getJSONTagNames(s interface{}) []string { valueType := reflect.TypeOf(s) @@ -208,6 +219,14 @@ func FileExcelImport(d []byte, cols []string, nType int) ([]byte, []map[string]i return nil, nil, err } cols = getJSONTagNames(StockExcel{}) + case 4: + if sheetList[0] != "导零售" { + return nil, nil, errors.New("格式错误,不是零售模版excel") + } + if err := checkOrderExcel(sheetCols); err != nil { + return nil, nil, err + } + cols = getJSONTagNames(OrderExcel{}) default: return nil, nil, errors.New("格式错误,不是商品分类或资料模版excel") } @@ -1335,3 +1354,335 @@ func IsExistingCommodity(commodityId uint32) bool { return count > 0 } + +// 校验导入零售excel格式 +// 必填项:商品名称、销售数量 +// 商品名称和串码是否对应 +// 串码商品数量只能为1 +// 是否有库存 +func checkOrderExcel(sheetCols [][]string) error { + if len(sheetCols) != 8 { + return errors.New("模版错误,请检查文件") + } + + maxLength := findMaxLength(sheetCols) + nLow, nMax := determineLowAndMax(sheetCols) + + if nMax < maxLength { + return errors.New("第" + strconv.Itoa(nMax+1) + "行商品名称和商品编号不能同时为空") + } + + // 判断是否有重复串码 + if duplicateName, nFlag := hasDuplicateIMEI(sheetCols[2]); nFlag { + return fmt.Errorf("商品串码不允许重复,请检查:[%v]", duplicateName) + } + + //先遍历第1列 + for i := 1; i < nLow; i++ { + if sheetCols[0][i] == "" && sheetCols[1][i] == "" { // 商品名称和编号不能都为空 + return errors.New("第" + strconv.Itoa(i+1) + "行商品名称和商品编号不能同时为空") + } + } + + for i := 1; i < nMax; i++ { + if i < len(sheetCols[1]) { + if !IsExistingProduct(sheetCols[1][i]) { + return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + } + + // 商品编号必须为纯数字 + if i < len(sheetCols[0]) { + if sheetCols[0][i] != "" { + if _, err := strconv.Atoi(sheetCols[0][i]); err != nil { + return errors.New("第" + strconv.Itoa(i+1) + "行商品编号必须为纯数字") + } + } + + if !isExistingProductCode(sheetCols[0][i]) { + return errors.New("第" + strconv.Itoa(i+1) + "行商品编号不存在") + } + } + + // 所属门店不能为空 + if i < len(sheetCols[4]) { + if sheetCols[4][i] == "" { + return errors.New("第" + strconv.Itoa(i+1) + "行所属门店不能为空") + } + if !isExistingStore(sheetCols[4][i]) { + return errors.New("第" + strconv.Itoa(i+1) + "行门店不存在") + } + } + + // 数量必须为正整数 + if i < len(sheetCols[3]) { + if quantity, err := strconv.Atoi(sheetCols[3][i]); err != nil || quantity < 1 { + return errors.New("第" + strconv.Itoa(i+1) + "行数量必须是大于等于1的整数") + } + } + + // 串码商品数量只能为1 + var nCount int + if i < len(sheetCols[3]) { + if i < len(sheetCols[3]) { + nCount, _ = strconv.Atoi(sheetCols[3][i]) + } + + // 串码类商品数量只能为1 + if sheetCols[2][i] != "" && nCount != 1 { + return errors.New("第" + strconv.Itoa(i+1) + "行串码类商品数量只能为1") + } + } + + // 判断商品是否赠送 + if i < len(sheetCols[6]) { + if sheetCols[6][i] != "是" && sheetCols[6][i] != "" { + return errors.New("第" + strconv.Itoa(i+1) + "行商品是否赠送填写有误") + } + } + + // 查询商品是否有库存 + if sheetCols[2][i] != "" { // 串码商品 + var imeiStockCommodity ErpStockCommodity + err := orm.Eloquent.Table("erp_stock_commodity").Where("imei = ? and state = ?", + sheetCols[2][i], InStock). + Find(&imeiStockCommodity).Error + if err != nil { + return err + } + if imeiStockCommodity.ID == 0 { + return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + // 门店对比 + if imeiStockCommodity.StoreName != sheetCols[4][i] { + return errors.New("第" + strconv.Itoa(i+1) + "行商品" + + "[" + imeiStockCommodity.ErpCommodityName + "]非所选门店库存,请检查") + } + // 零售价对比 + var floatVal float64 + if i < len(sheetCols[5]) { + if sheetCols[5][i] != "" { + floatVal, err = strconv.ParseFloat(sheetCols[5][i], 64) + if err != nil { + return errors.New("第" + strconv.Itoa(i+1) + "行零售价有误") + } + strMin := strconv.FormatFloat(imeiStockCommodity.MinRetailPrice, 'f', 2, 64) + if floatVal < imeiStockCommodity.MinRetailPrice { + return errors.New("第" + strconv.Itoa(i+1) + "行零售价有误,不能低于最低零售价[" + strMin + "]") + } + } + } + } else { // 非串码商品 + var count int64 + var err error + if sheetCols[0][i] != "" { // 商品编号不为空 + err = orm.Eloquent.Table("erp_stock_commodity"). + Where("commodity_serial_number = ? and store_name = ? and state = ? and imei_type = ?", + sheetCols[0][i], sheetCols[4][i], InStock, NoIMEICommodity). + Count(&count).Error + } else { + err = orm.Eloquent.Table("erp_stock_commodity"). + Where("erp_commodity_name = ? and store_name = ? and state = ? and imei_type = ?", + sheetCols[1][i], sheetCols[4][i], InStock, NoIMEICommodity). + Count(&count).Error + } + if err != nil { + return errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + + if count < int64(nCount) { + // 获取商品名称 + return errors.New("第" + strconv.Itoa(i+1) + "行商品" + "[" + sheetCols[1][i] + "]库存不足") + } + } + } + + return nil +} + +// 将读取的excel数据转换成OrderExcel struct +func transOrderData(colsMap []map[string]interface{}) ([]OrderExcel, error) { + var stockInfos []OrderExcel + + // 遍历 colsMap 进行类型断言和转换 + for _, col := range colsMap { + var stockInfo OrderExcel + + // 将 col 转换为 JSON 字符串 + jsonData, err := json.Marshal(col) + if err != nil { + return nil, fmt.Errorf("failed to marshal data to JSON: %v", err) + } + + // 将 JSON 字符串反序列化为 CommodityExcel + if err := json.Unmarshal(jsonData, &stockInfo); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON to StockExcel: %v", err) + } + + // 将处理后的数据追加到 commodities 中 + stockInfos = append(stockInfos, stockInfo) + } + + return stockInfos, nil +} + +func ImportOrderData(colsMap []map[string]interface{}) ([]ErpOrderCommodity, error) { + list, err := transOrderData(colsMap) + if err != nil { + return nil, err + } + + var orderCommodities []ErpOrderCommodity + for i, _ := range list { + var orderCommodity ErpOrderCommodity + + // 商品数量 + var nCount int + nCount, _ = strconv.Atoi(list[i].Count) + + // 判断商品是否赠送 + var nPresentType uint32 + nPresentType = 1 + if i < len(list[i].PresentType) { + if list[i].PresentType == "是" { + nPresentType = 2 + } + } + + // 查询商品是否有库存 + if list[i].IMEI != "" { // 串码商品 + var imeiStockCommodity ErpStockCommodity + err = orm.Eloquent.Table("erp_stock_commodity").Where("imei = ? and state = ?", + list[i].IMEI, InStock). + Find(&imeiStockCommodity).Error + if err != nil { + return nil, err + } + if imeiStockCommodity.ID == 0 { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + // 门店对比 + if imeiStockCommodity.StoreName != list[i].StoreName { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行商品" + + "[" + imeiStockCommodity.ErpCommodityName + "]非所选门店库存,请检查") + } + // 零售价对比 + var floatVal float64 + if list[i].SalePrice != "" { + floatVal, err = strconv.ParseFloat(list[i].SalePrice, 64) + if err != nil { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行零售价有误") + } + strMin := strconv.FormatFloat(imeiStockCommodity.MinRetailPrice, 'f', 2, 64) + if floatVal < imeiStockCommodity.MinRetailPrice { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行零售价有误,不能低于最低零售价[" + strMin + "]") + } + } else { + floatVal = imeiStockCommodity.RetailPrice + } + + orderCommodity.ErpCategoryId = imeiStockCommodity.ErpCategoryId + orderCommodity.ErpCategoryName = imeiStockCommodity.ErpCategoryName + orderCommodity.ErpCommodityId = imeiStockCommodity.ErpCommodityId + orderCommodity.ErpCommodityName = imeiStockCommodity.ErpCommodityName + orderCommodity.ErpSupplierId = imeiStockCommodity.ErpSupplierId + orderCommodity.ErpSupplierName = imeiStockCommodity.ErpSupplierName + orderCommodity.IMEIType = imeiStockCommodity.IMEIType + orderCommodity.IMEI = imeiStockCommodity.IMEI + orderCommodity.PresentType = nPresentType + orderCommodity.RetailPrice = imeiStockCommodity.RetailPrice + orderCommodity.SalePrice = floatVal + orderCommodity.ReceivedAmount = floatVal + orderCommodity.SaleDiscount = floatVal - imeiStockCommodity.RetailPrice + orderCommodity.WholesalePrice = imeiStockCommodity.WholesalePrice + orderCommodity.StaffCostPrice = imeiStockCommodity.StaffCostPrice + orderCommodity.MemberDiscount = imeiStockCommodity.MemberDiscount + orderCommodity.SalesProfit = floatVal - imeiStockCommodity.WholesalePrice + orderCommodity.StaffProfit = floatVal - imeiStockCommodity.StaffCostPrice + orderCommodity.Count = int32(nCount) + orderCommodity.Remark = list[i].Remark + + } else { // 非串码商品 + var count int64 + var imeiStockCommodities []ErpStockCommodity + if list[i].SerialNum != "" { // 商品编号不为空 + err = orm.Eloquent.Table("erp_stock_commodity"). + Where("commodity_serial_number = ? and store_name = ? and state = ? and imei_type = ?", + list[i].SerialNum, list[i].StoreName, InStock, NoIMEICommodity). + Count(&count).Error + } else { + err = orm.Eloquent.Table("erp_stock_commodity"). + Where("erp_commodity_name = ? and store_name = ? and state = ? and imei_type = ?", + list[i].Name, list[i].StoreName, InStock, NoIMEICommodity). + Count(&count).Error + } + if err != nil { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + + if count < int64(nCount) { + // 获取商品名称 + return nil, errors.New("第" + strconv.Itoa(i+1) + "行商品" + "[" + list[i].Name + "]库存不足") + } + + if list[i].SerialNum != "" { // 商品编号不为空 + err = orm.Eloquent.Table("erp_stock_commodity"). + Where("commodity_serial_number = ? and store_name = ? and state = ? and imei_type = ?", + list[i].SerialNum, list[i].StoreName, InStock, NoIMEICommodity). + Find(&imeiStockCommodities).Order("first_stock_time DESC").Error + } else { + err = orm.Eloquent.Table("erp_stock_commodity"). + Where("erp_commodity_name = ? and store_name = ? and state = ? and imei_type = ?", + list[i].Name, list[i].StoreName, InStock, NoIMEICommodity). + Find(&imeiStockCommodities).Order("first_stock_time DESC").Error + } + if err != nil { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + + if len(imeiStockCommodities) == 0 { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行商品不存在") + } + + // 零售价对比 + var floatVal float64 + if list[i].SalePrice != "" { + floatVal, err = strconv.ParseFloat(list[i].SalePrice, 64) + if err != nil { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行零售价有误") + } + strMin := strconv.FormatFloat(imeiStockCommodities[0].MinRetailPrice, 'f', 2, 64) + if floatVal < imeiStockCommodities[0].MinRetailPrice { + return nil, errors.New("第" + strconv.Itoa(i+1) + "行零售价有误,不能低于最低零售价[" + strMin + "]") + } + } else { + floatVal = imeiStockCommodities[0].RetailPrice + } + + orderCommodity.ErpCategoryId = imeiStockCommodities[0].ErpCategoryId + orderCommodity.ErpCategoryName = imeiStockCommodities[0].ErpCategoryName + orderCommodity.ErpCommodityId = imeiStockCommodities[0].ErpCommodityId + orderCommodity.ErpCommodityName = imeiStockCommodities[0].ErpCommodityName + orderCommodity.ErpSupplierId = imeiStockCommodities[0].ErpSupplierId + orderCommodity.ErpSupplierName = imeiStockCommodities[0].ErpSupplierName + orderCommodity.IMEIType = imeiStockCommodities[0].IMEIType + orderCommodity.IMEI = imeiStockCommodities[0].IMEI + orderCommodity.PresentType = nPresentType + orderCommodity.RetailPrice = imeiStockCommodities[0].RetailPrice + orderCommodity.SalePrice = floatVal + orderCommodity.ReceivedAmount = floatVal + orderCommodity.SaleDiscount = floatVal - imeiStockCommodities[0].RetailPrice + orderCommodity.WholesalePrice = imeiStockCommodities[0].WholesalePrice + orderCommodity.StaffCostPrice = imeiStockCommodities[0].StaffCostPrice + orderCommodity.MemberDiscount = imeiStockCommodities[0].MemberDiscount + orderCommodity.SalesProfit = floatVal - imeiStockCommodities[0].WholesalePrice + orderCommodity.StaffProfit = floatVal - imeiStockCommodities[0].StaffCostPrice + orderCommodity.Count = int32(nCount) + orderCommodity.Remark = list[i].Remark + } + + orderCommodities = append(orderCommodities, orderCommodity) + } + + return orderCommodities, nil +} diff --git a/app/admin/router/erpordermanage.go b/app/admin/router/erpordermanage.go index f1fb4dc..93508ed 100644 --- a/app/admin/router/erpordermanage.go +++ b/app/admin/router/erpordermanage.go @@ -25,4 +25,5 @@ func registerErpOrderManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJW r.POST("show_all_data", erpordermanage.ErpOrderShowAllData) // 展示所有订单 r.POST("daily_report", erpordermanage.ErpOrderDailyReport) // 经营日报表 r.POST("sale_detail", erpordermanage.ErpOrderSaleDetail) // 查询销售明细 + r.POST("import", erpordermanage.ErpOrderBatchImport) // 批量导入零售数据 } diff --git a/docs/docs.go b/docs/docs.go index b9751c2..ef1dab1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1943,6 +1943,39 @@ const docTemplate = `{ } } }, + "/api/v1/erp_order/import": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "零售订单" + ], + "summary": "批量导入零售数据", + "parameters": [ + { + "description": "上传excel文件", + "name": "file", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ErpOrderBatchImportResp" + } + } + } + } + }, "/api/v1/erp_order/list": { "post": { "consumes": [ @@ -9455,6 +9488,18 @@ const docTemplate = `{ } } }, + "models.ErpOrderBatchImportResp": { + "type": "object", + "properties": { + "commodities": { + "description": "零售订单商品信息", + "type": "array", + "items": { + "$ref": "#/definitions/models.ErpOrderCommodity" + } + } + } + }, "models.ErpOrderCashier": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index f8dd47a..6a3e8d7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1932,6 +1932,39 @@ } } }, + "/api/v1/erp_order/import": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "零售订单" + ], + "summary": "批量导入零售数据", + "parameters": [ + { + "description": "上传excel文件", + "name": "file", + "in": "body", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ErpOrderBatchImportResp" + } + } + } + } + }, "/api/v1/erp_order/list": { "post": { "consumes": [ @@ -9444,6 +9477,18 @@ } } }, + "models.ErpOrderBatchImportResp": { + "type": "object", + "properties": { + "commodities": { + "description": "零售订单商品信息", + "type": "array", + "items": { + "$ref": "#/definitions/models.ErpOrderCommodity" + } + } + } + }, "models.ErpOrderCashier": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e1b7e6f..1a66916 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2268,6 +2268,14 @@ definitions: - bill_sn - state type: object + models.ErpOrderBatchImportResp: + properties: + commodities: + description: 零售订单商品信息 + items: + $ref: '#/definitions/models.ErpOrderCommodity' + type: array + type: object models.ErpOrderCashier: properties: amount: @@ -9779,6 +9787,27 @@ paths: summary: 编辑零售订单 tags: - 零售订单 + /api/v1/erp_order/import: + post: + consumes: + - application/json + parameters: + - description: 上传excel文件 + in: body + name: file + required: true + schema: + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.ErpOrderBatchImportResp' + summary: 批量导入零售数据 + tags: + - 零售订单 /api/v1/erp_order/list: post: consumes: