Merge remote-tracking branch 'origin/main'

This commit is contained in:
Suriya
2026-08-03 17:48:21 +05:30
9 changed files with 326 additions and 98 deletions

View File

@@ -1785,29 +1785,52 @@ func (r *orderRepository) resolveOfflineCustomer(tx *gorm.DB, ctx *offlineLocati
// instead of a race: two uploads of the same file arriving together would
// otherwise both read "not yet imported" and both commit.
func (r *orderRepository) UploadOfflineSales(input models.OfflineSalesUpload) (*models.OfflineSalesUploadResponse, error) {
if input.Tenantid <= 0 {
return nil, errors.New("tenantid is required")
}
if len(input.Bills) == 0 {
return nil, errors.New("no sales rows found in the upload")
}
ctx, err := r.resolveOfflineLocationContext(input.Tenantid, input.Locationid)
if err != nil {
return nil, err
// When the caller pins the upload to one branch, that branch is resolved
// (and authorised) up front so an outlet the merchant does not own fails
// the whole request rather than each bill in turn.
if input.Locationid > 0 {
if _, err := r.resolveOfflineLocationContext(input.Tenantid, input.Locationid); err != nil {
return nil, err
}
}
products, err := r.loadOfflineProducts(input.Tenantid, input.Locationid)
if err != nil {
return nil, err
}
if len(products) == 0 {
return nil, fmt.Errorf("outlet '%s' has no products stocked against it", ctx.Locationname)
// Branch context and catalogue are resolved once per branch and reused. A
// workbook covering six outlets would otherwise re-run both queries for
// every bill in it.
contexts := make(map[int]*offlineLocationContext)
catalogues := make(map[int]map[int]offlineProduct)
resolve := func(locationID int) (*offlineLocationContext, map[int]offlineProduct, error) {
if ctx, ok := contexts[locationID]; ok {
return ctx, catalogues[locationID], nil
}
ctx, err := r.resolveOfflineLocationContext(input.Tenantid, locationID)
if err != nil {
return nil, nil, err
}
products, err := r.loadOfflineProducts(input.Tenantid, locationID)
if err != nil {
return nil, nil, err
}
if len(products) == 0 {
return nil, nil, fmt.Errorf("outlet '%s' has no products stocked against it", ctx.Locationname)
}
contexts[locationID] = ctx
catalogues[locationID] = products
return ctx, products, nil
}
resp := &models.OfflineSalesUploadResponse{Results: make([]models.OfflineSaleResult, 0, len(input.Bills))}
for _, bill := range input.Bills {
result := r.importOfflineBill(ctx, products, input.Userid, bill)
record := func(result models.OfflineSaleResult) {
resp.Results = append(resp.Results, result)
switch result.Status {
case models.OfflineSaleImported:
resp.Imported++
@@ -1819,6 +1842,49 @@ func (r *orderRepository) UploadOfflineSales(input models.OfflineSalesUpload) (*
}
}
for _, bill := range input.Bills {
billLocation := bill.Locationid
if billLocation <= 0 {
billLocation = input.Locationid
}
if billLocation <= 0 {
record(models.OfflineSaleResult{
Billno: strings.TrimSpace(bill.Billno),
Status: models.OfflineSaleFailed,
Message: "no locationid on these rows — the sheet must say which branch the sale belongs to",
})
continue
}
// A pinned upload refuses bills for anywhere else. This is what keeps a
// store user inside their own branch: editing the locationid column in
// the spreadsheet changes nothing, because the pin is set from their
// session and not from the file.
if input.Locationid > 0 && billLocation != input.Locationid {
record(models.OfflineSaleResult{
Locationid: billLocation,
Billno: strings.TrimSpace(bill.Billno),
Status: models.OfflineSaleFailed,
Message: fmt.Sprintf("this upload is limited to outlet %d, but these rows are for outlet %d", input.Locationid, billLocation),
})
continue
}
ctx, products, err := resolve(billLocation)
if err != nil {
record(models.OfflineSaleResult{
Locationid: billLocation,
Billno: strings.TrimSpace(bill.Billno),
Status: models.OfflineSaleFailed,
Message: err.Error(),
})
continue
}
record(r.importOfflineBill(ctx, products, input.Userid, bill))
}
return resp, nil
}
@@ -1836,9 +1902,11 @@ func (r *orderRepository) importOfflineBill(
fail := func(format string, args ...any) models.OfflineSaleResult {
return models.OfflineSaleResult{
Billno: billLabel,
Status: models.OfflineSaleFailed,
Message: fmt.Sprintf(format, args...),
Locationid: ctx.Locationid,
Locationname: ctx.Locationname,
Billno: billLabel,
Status: models.OfflineSaleFailed,
Message: fmt.Sprintf(format, args...),
}
}
@@ -1968,9 +2036,11 @@ func (r *orderRepository) importOfflineBill(
if already > 0 {
tx.Rollback()
return models.OfflineSaleResult{
Billno: billLabel,
Status: models.OfflineSaleDuplicate,
Message: fmt.Sprintf("bill %s was already imported for this outlet; stock was not deducted again", billLabel),
Locationid: ctx.Locationid,
Locationname: ctx.Locationname,
Billno: billLabel,
Status: models.OfflineSaleDuplicate,
Message: fmt.Sprintf("bill %s was already imported for %s; stock was not deducted again", billLabel, ctx.Locationname),
}
}
@@ -2021,13 +2091,15 @@ func (r *orderRepository) importOfflineBill(
}
return models.OfflineSaleResult{
Locationid: ctx.Locationid,
Locationname: ctx.Locationname,
Billno: billLabel,
Status: models.OfflineSaleImported,
Orderid: created.Orderid,
Orderheaderid: created.Orderheaderid,
Itemcount: len(items),
Amount: orderAmount,
Message: fmt.Sprintf("imported as order %s", created.Orderid),
Message: fmt.Sprintf("imported as order %s at %s", created.Orderid, ctx.Locationname),
}
}