The offline-sales import required one workbook per outlet and a store picked in the UI. A merchant running several branches had to download, fill and upload a file per branch, and the picker defaulted to the tenant's first outlet — so an admin who never touched it silently credited the wrong store, which no validation could catch because the file and the selection agreed with each other. One workbook now covers every branch. getsaletemplate takes locationid=0 (the default) to span the tenant, stamping tenantid, locationid and the store name onto every row, and that row's locationid is what decides which branch a sale is deducted from. The INNER JOIN on tenantlocations confines it to outlets the tenant owns, so a template can never disclose another merchant's catalogue. uploadofflinesales accordingly takes locationid on each bill. The locationid on the request itself becomes a scope constraint rather than a destination: left at 0 the bills go where their rows say, and set to a branch it pins the upload there and refuses anything else. That is what holds a store user to their own store — the pin comes from their session, so editing the locationid column in the spreadsheet changes nothing. Every branch referenced is checked against the tenant regardless. 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. Duplicate detection is now per branch. Bill numbers only have to be unique within a store, since counter books at different outlets routinely restart numbering at 1, and treating a shared number as a repeat would have silently dropped a real sale. Verified against tenant 1087, whose two branches both stock product 6998 at 100 units: a single upload of two bills moved 1097 to 97 and 1135 to 95 independently; the same bill number at both branches imported as two separate orders; an upload pinned to 1097 imported its own bill and refused the 1135 one; a row naming another tenant's outlet was refused; and re-uploading the file deducted nothing. All five test orders were cancelled afterwards and both branches confirmed back at 100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
393 lines
17 KiB
Go
393 lines
17 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
type ProductSubCategory struct {
|
|
Subcatid int `json:"subcatid"`
|
|
Categoryid int `json:"categoryid,omitempty"`
|
|
Tenantid int `json:"tenantid,omitempty"`
|
|
Subcatname string `json:"subcatname,omitempty"`
|
|
Image string `json:"image,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Sortorder int `json:"sortorder,omitempty"`
|
|
Createdby int `json:"createdby,omitempty"`
|
|
Created time.Time `json:"created,omitempty"`
|
|
Updated time.Time `json:"updated,omitempty"`
|
|
}
|
|
|
|
type Productcount struct {
|
|
Total int `json:"total"`
|
|
Available int `json:"available"`
|
|
Outofstock int `json:"outofstock"`
|
|
}
|
|
|
|
type ProductCategory struct {
|
|
Categoryid int `json:"categoryid"`
|
|
Moduleid int `json:"moduleid"`
|
|
Tenantid int `json:"tenantid,omitempty"`
|
|
Categorytypeid int `json:"categorytypeid,omitempty"`
|
|
Categoryname string `json:"categoryname,omitempty"`
|
|
Image string `json:"image,omitempty"`
|
|
Catalougecategoryid int `json:"catalougecategoryid,omitempty"`
|
|
Sortorder int `json:"sortorder,omitempty"`
|
|
Status string `json:"status"`
|
|
Createdby int `json:"createdby,omitempty"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
}
|
|
|
|
type Productvariant struct {
|
|
Variantid int `json:"variantid" gorm:"Primary_Key"`
|
|
Tenantid int `json:"tenantid"`
|
|
Variantname string `json:"variantname"`
|
|
Categoryid int `json:"categoryid" gorm:"default:0"`
|
|
Categoryname string `json:"categoryname" gorm:"-"`
|
|
Subcategoryid int `json:"subcategoryid"`
|
|
Status string `json:"status" gorm:"default:active"`
|
|
}
|
|
|
|
type Products struct {
|
|
Productid int `json:"productid" gorm:"primaryKey;autoIncrement"`
|
|
AppLocationid int `json:"applocationid" gorm:"column:applocationid"`
|
|
Productlocationid int `json:"productlocationid" gorm:"->"`
|
|
Tenantid int `json:"tenantid,omitempty"`
|
|
Categoryid int `json:"categoryid"`
|
|
Categoryname string `json:"categoryname" gorm:"->"`
|
|
Subcategoryid int `json:"subcategoryid,omitempty"`
|
|
Subcategoryname string `json:"Subcategoryname" gorm:"->"`
|
|
Catalogueid int `json:"catalogueid,omitempty"`
|
|
Addonid int `json:"addonid,omitempty"`
|
|
Discountid int `json:"discountid"`
|
|
Discountvalue float64 `json:"discountvalue"`
|
|
Pricingid int `json:"pricingid,omitempty"`
|
|
Productname string `json:"productname,omitempty"`
|
|
Productimage string `json:"productimage,omitempty"`
|
|
Productdesc string `json:"productdesc,omitempty"`
|
|
Productsku string `json:"productsku,omitempty"`
|
|
Brandid int `json:"brandid,omitempty"`
|
|
Productbrand string `json:"productbrand,omitempty"`
|
|
Productunit string `json:"productunit,omitempty"`
|
|
Unitvalue string `json:"unitvalue,omitempty"`
|
|
Toppicks string `json:"toppicks,omitempty"`
|
|
Productcost float64 `json:"productcost,omitempty"`
|
|
Taxamount float64 `json:"taxamount,omitempty"`
|
|
Taxpercent float64 `json:"taxpercent,omitempty"`
|
|
Producttax int `json:"producttax" gorm:"default:0"`
|
|
Productstock int `json:"productstock" gorm:"default:0"`
|
|
Productcombo int `json:"productcombo" gorm:"default:0"`
|
|
Variants int `json:"variants" gorm:"default:0"`
|
|
Quantity int `json:"quantity"`
|
|
Retailprice float64 `json:"retailprice,omitempty"`
|
|
Diffprice float64 `json:"diffprice,omitempty"`
|
|
Diffpercent float64 `json:"diffpercent,omitempty"`
|
|
Othercost float64 `json:"othercost,omitempty"`
|
|
Approve int `json:"approve"`
|
|
Productstatus string `json:"productstatus" `
|
|
// Populated only by queries scoped to a specific location (e.g.
|
|
// GetProductByVariant when locationid is passed): Productstock becomes
|
|
// the live SUM(in)-SUM(out) balance from productstocks — the same
|
|
// formula CreateOrder's stock check uses — and Locationstatus mirrors
|
|
// productlocations.status ("outofstock"/"available") for that store.
|
|
// Left at zero values for callers that don't scope to a location.
|
|
Locationstatus string `json:"locationstatus,omitempty" gorm:"->"`
|
|
// Status string `json:"status" gorm:"default:InActive"`
|
|
// Status string `json:"status" gorm:"-"`
|
|
}
|
|
|
|
type Locationproducts struct {
|
|
Productid int `json:"productid"`
|
|
AppLocationid int `json:"applocationid" gorm:"column:applocationid"`
|
|
Productlocationid int `json:"productlocationid" gorm:"->"`
|
|
Tenantid int `json:"tenantid,omitempty"`
|
|
Categoryid int `json:"categoryid"`
|
|
Categoryname string `json:"categoryname" gorm:"->"`
|
|
Subcategoryid int `json:"subcategoryid,omitempty"`
|
|
Subcategoryname string `json:"Subcategoryname" gorm:"->"`
|
|
Catalogueid int `json:"catalogueid,omitempty"`
|
|
Addonid int `json:"addonid,omitempty"`
|
|
Discountid int `json:"discountid,omitempty"`
|
|
Pricingid int `json:"pricingid,omitempty"`
|
|
Productname string `json:"productname,omitempty"`
|
|
Productimage string `json:"productimage,omitempty"`
|
|
Productdesc string `json:"productdesc,omitempty"`
|
|
Productsku string `json:"productsku,omitempty"`
|
|
Brandid int `json:"brandid,omitempty"`
|
|
Productbrand string `json:"productbrand,omitempty"`
|
|
Productunit string `json:"productunit,omitempty"`
|
|
Unitvalue string `json:"unitvalue,omitempty"`
|
|
Toppicks string `json:"toppicks,omitempty"`
|
|
Productcost float64 `json:"productcost,omitempty"`
|
|
Taxamount float64 `json:"taxamount,omitempty"`
|
|
Taxpercent float64 `json:"taxpercent,omitempty"`
|
|
Producttax int `json:"producttax" gorm:"default:0"`
|
|
Productstock int `json:"productstock" gorm:"default:0"`
|
|
Productcombo int `json:"productcombo" gorm:"default:0"`
|
|
Variants int `json:"variants" gorm:"default:0"`
|
|
Quantity int `json:"quantity"`
|
|
// Price is the per-store selling price from productlocations.price — the one
|
|
// CreateProductLocation upserts. Read-only here: it comes from the joined
|
|
// productlocations row, not from products. Without it a store could set a
|
|
// price and never read it back, so the UI always showed the master price.
|
|
Price float64 `json:"price" gorm:"->"`
|
|
Retailprice float64 `json:"retailprice,omitempty"`
|
|
Diffprice float64 `json:"diffprice,omitempty"`
|
|
Diffpercent float64 `json:"diffpercent,omitempty"`
|
|
Othercost float64 `json:"othercost,omitempty"`
|
|
Approve int `json:"approve" gorm:"default:0"`
|
|
// Productstatus string `json:"productstatus" gorm:"default:available"`
|
|
Status string `json:"status" gorm:"default:outofstock"`
|
|
}
|
|
|
|
type Productstocks struct {
|
|
Productstockid int `json:"productstockid" gorm:"Primary_Key"`
|
|
Locationid int `json:"locationid"`
|
|
Tenantid int `json:"tenantid"`
|
|
Stockdate string `json:"stockdate"`
|
|
Productid int `json:"productid"`
|
|
Quantity int `json:"quantity"`
|
|
Stocktype string `json:"stocktype"`
|
|
Status string `json:"status"`
|
|
AppLocationid int `json:"applocationid"`
|
|
Categoryid int `json:"categoryid"`
|
|
Categoryname string `json:"categoryname" gorm:"->"`
|
|
Subcategoryid int `json:"subcategoryid,omitempty"`
|
|
Subcategoryname string `json:"Subcategoryname" `
|
|
Productname string `json:"productname,omitempty"`
|
|
Productimage string `json:"productimage,omitempty"`
|
|
Productdesc *string `json:"productdesc,omitempty"`
|
|
Productsku *string `json:"productsku,omitempty"`
|
|
Brandid *int `json:"brandid,omitempty"`
|
|
Productbrand *string `json:"productbrand,omitempty"`
|
|
Productunit *string `json:"productunit,omitempty"`
|
|
Unitvalue *string `json:"unitvalue,omitempty"`
|
|
Toppicks *string `json:"toppicks,omitempty"`
|
|
Productcost *float64 `json:"productcost,omitempty"`
|
|
Taxamount *float64 `json:"taxamount,omitempty"`
|
|
Taxpercent *float64 `json:"taxpercent,omitempty"`
|
|
Producttax *int `json:"producttax,omitempty"`
|
|
Productstock *int `json:"productstock,omitempty"`
|
|
Productcombo *int `json:"productcombo,omitempty"`
|
|
Variants int `json:"variants"`
|
|
Retailprice float64 `json:"retailprice,omitempty"`
|
|
Diffprice float64 `json:"diffprice,omitempty"`
|
|
Diffpercent float64 `json:"diffpercent,omitempty"`
|
|
Othercost float64 `json:"othercost,omitempty"`
|
|
Approve *int `json:"approve"`
|
|
}
|
|
|
|
type Productstock struct {
|
|
Productstockid int `json:"productstockid" gorm:"Primary_Key"`
|
|
Tenantid int `json:"tenantid"`
|
|
Stockdate time.Time `json:"stockdate"`
|
|
Locationid int `json:"locationid"`
|
|
Productid int `json:"productid"`
|
|
Quantity int `json:"quantity"`
|
|
Stocktype string `json:"stocktype"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type Productstockstatement struct {
|
|
Productid int `json:"productid"`
|
|
Productname string `json:"productname"`
|
|
Productimage string `json:"productimage"`
|
|
Categoryid int `json:"categoryid"`
|
|
Subcategoryid int `json:"subcategoryid"`
|
|
Productunit string `json:"productunit"`
|
|
Unitvalue string `json:"unitvalue"`
|
|
Productcost float32 `json:"productcost"`
|
|
Taxpercent float32 `json:"taxpercent"`
|
|
Taxamount float32 `json:"taxamount"`
|
|
Retailprice float32 `json:"retailprice"`
|
|
Tenantid int `json:"tenantid"`
|
|
Locationid int `json:"locationid"`
|
|
Opening int `json:"opening"`
|
|
Credit int `json:"credit"`
|
|
Debit int `json:"debit"`
|
|
Closing int `json:"closing"`
|
|
}
|
|
|
|
type ProductSummary struct {
|
|
Subcategoryid int `json:"subcategoryid"`
|
|
Subcategroyname string `json:"subcategroyname"`
|
|
Image string `json:"image"`
|
|
Productcount int `json:"productcount"`
|
|
}
|
|
|
|
type Tenantproducts struct {
|
|
Tenant TenantInfo `json:"tenant"`
|
|
Products []Products `json:"products"`
|
|
// Locationproducts []Locationproducts `json:"locationproducts"`
|
|
}
|
|
|
|
type TenantInfo struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Tenantname string `json:"tenantname"`
|
|
Userfcmtoken string `json:"userfcmtoken"`
|
|
Address string `json:"address"`
|
|
Licenseno string `json:"licenseno"`
|
|
Primaryemail string `json:"primaryemail"`
|
|
Primarycontact string `json:"primarycontact"`
|
|
Pickuplocationid int `json:"pickuplocationid"`
|
|
Applocationid int `json:"applocationid"`
|
|
Suburb string `json:"suburb"`
|
|
City string `json:"city"`
|
|
Latitude string `json:"latitude"`
|
|
Longitude string `json:"longitude"`
|
|
Postcode string `json:"postcode"`
|
|
Tenantimage string `json:"tenantimage"`
|
|
Locationid int `json:"locationid"`
|
|
Locationname string `json:"locationname"`
|
|
Subcategoryid int `json:"subcategoryid"`
|
|
Categoryid int `json:"categoryid"`
|
|
Registrationno string `json:"registrationno"`
|
|
Orderscount int `json:"orderscount"`
|
|
// Products []Products `json:"products" gorm:"-"`
|
|
ProductSubcategory []ProductSubcategory `json:"productsubcategory" gorm:"-"`
|
|
}
|
|
|
|
type SubcategoryProductResponse struct {
|
|
SubcategoryID int `json:"subcategoryid"`
|
|
SubcategoryName string `json:"subcategoryname"`
|
|
Image string `json:"image"`
|
|
Products []Products `json:"products"`
|
|
}
|
|
|
|
type ProductFilter struct {
|
|
CategoryID int
|
|
TenantID int
|
|
AppLocationID int
|
|
ProductID int
|
|
Keyword string
|
|
LocationID int
|
|
}
|
|
|
|
type Subcategory struct {
|
|
Subcategoryid int `json:"subcategoryid" gorm:"column:subcatid"`
|
|
Subcategoryname string `json:"subcategoryname" gorm:"column:subcatname"`
|
|
Categoryid int `json:"categoryid" gorm:"column:categoryid"`
|
|
Image string `json:"image" gorm:"column:image"`
|
|
}
|
|
|
|
// TenantCategory is a categoryid actually in use by a tenant's own products,
|
|
// with a best-effort name. Used instead of the global productcategories list
|
|
// for the import category picker, since that master table is missing rows
|
|
// for categoryids that are nonetheless in real use (e.g. categoryid 2).
|
|
type TenantCategory struct {
|
|
Categoryid int `json:"categoryid"`
|
|
Categoryname string `json:"categoryname"`
|
|
}
|
|
|
|
// ImportedCatalogueRef identifies a catalogue product a tenant has already
|
|
// imported. Brand is always included, even when a caller filtered by a
|
|
// single brand, because a bare catalogueid is ambiguous across brand tables.
|
|
type ImportedCatalogueRef struct {
|
|
Brand string `json:"brand"`
|
|
Catalogueid int64 `json:"catalogueid"`
|
|
}
|
|
|
|
// ImportCatalogueProductRequest is the payload for importing a product from
|
|
// the global catalogue (CatalogueDB) into a tenant's own store catalogue.
|
|
// Brand+Catalogueid is the bridge key back to CatalogueDB: a catalogue row's
|
|
// bare id is only unique within its brand table, so both are required.
|
|
type ImportCatalogueProductRequest struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Locationid int `json:"locationid"`
|
|
Brand string `json:"brand"`
|
|
Catalogueid int64 `json:"catalogueid"`
|
|
Categoryid int `json:"categoryid"`
|
|
Subcategoryid int `json:"subcategoryid"`
|
|
Quantity int `json:"quantity"`
|
|
Stocktype string `json:"stocktype"`
|
|
Status string `json:"status"`
|
|
Retailprice float64 `json:"retailprice"`
|
|
Productcost float64 `json:"productcost"`
|
|
Taxpercent float64 `json:"taxpercent"`
|
|
}
|
|
|
|
type Productlocations struct {
|
|
Productlocationid int `json:"productlocationid" gorm:"Primary_Key"`
|
|
Tenantid int `json:"tenantid"`
|
|
Locationid int `json:"locationid"`
|
|
Productid int `json:"productid"`
|
|
Catlougeid int `json:"catlougeid"`
|
|
Minquantity int `json:"minquantity" gorm:"default:0"`
|
|
Maxquantity int `json:"maxquantity" gorm:"default:0"`
|
|
Price float32 `json:"price" gorm:"default:0.0"`
|
|
Quantity int `json:"quantity" gorm:"<-:false"`
|
|
Stocktype string `json:"stocktype" gorm:"<-:false"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// ProductLocationRef identifies a single (tenant, location, product) row in
|
|
// productlocations — used to reactivate it after new stock arrives, the
|
|
// per-location counterpart to CreateOrder's outofstock flag.
|
|
type ProductLocationRef struct {
|
|
Tenantid int
|
|
Locationid int
|
|
Productid int
|
|
}
|
|
|
|
// SaleTemplateRow is one line of the downloadable offline-sales spreadsheet: a
|
|
// product stocked at one branch, with the numbers the person at the till needs
|
|
// to see before typing a sold quantity against it.
|
|
//
|
|
// Tenantid and Locationid ride on every row because one workbook covers every
|
|
// branch a merchant runs. The row's own Locationid decides which branch's stock
|
|
// its sale comes out of — a tenant-level import would be wrong, since the same
|
|
// product is held separately at each outlet.
|
|
//
|
|
// Productid is the only field that identifies the product. It cannot be
|
|
// productsku: across the live catalogue 6,245 products share just 93 distinct
|
|
// sku values (one tenant has 463 products all carrying sku "1"), and 154 are
|
|
// blank, so a sku is not a key. Productname is nearly unique per tenant but not
|
|
// reliably ("rice" appears 6 times for one tenant), so it travels as a
|
|
// human-readable confirmation only and is never matched on. That is why the
|
|
// spreadsheet has to be generated from this endpoint rather than typed from
|
|
// scratch — productid and locationid are filled in for the user.
|
|
type SaleTemplateRow struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Locationid int `json:"locationid"`
|
|
Locationname string `json:"locationname"`
|
|
Productid int `json:"productid"`
|
|
Productname string `json:"productname"`
|
|
Productunit string `json:"productunit"`
|
|
Unitvalue string `json:"unitvalue"`
|
|
Categoryname string `json:"categoryname"`
|
|
Currentstock int `json:"currentstock"`
|
|
Price float64 `json:"price"`
|
|
Taxpercent float64 `json:"taxpercent"`
|
|
}
|
|
|
|
// SaleTemplateLocation is one branch covered by the workbook, so the sheet can
|
|
// list what it spans and the UI can summarise it without walking every row.
|
|
type SaleTemplateLocation struct {
|
|
Locationid int `json:"locationid"`
|
|
Locationname string `json:"locationname"`
|
|
Productcount int `json:"productcount"`
|
|
}
|
|
|
|
// SaleTemplate is the payload the web app turns into an .xlsx workbook.
|
|
//
|
|
// Locationid is 0 when the template spans every branch of the tenant, which is
|
|
// the normal case for an owner or admin. A store user gets a template for their
|
|
// own branch only, and it is then the single entry in Locations.
|
|
type SaleTemplate struct {
|
|
Tenantid int `json:"tenantid"`
|
|
Locationid int `json:"locationid"`
|
|
Locations []SaleTemplateLocation `json:"locations"`
|
|
Products []SaleTemplateRow `json:"products"`
|
|
}
|
|
|
|
type ProductSubcategory struct {
|
|
Subcatid int `json:"subcatid"`
|
|
Categoryid int `json:"categoryid"`
|
|
Tenantid int `json:"tenantid"`
|
|
Subcatname string `json:"subcatname"`
|
|
Status string `json:"status"`
|
|
Sortorder int `json:"sortorder"`
|
|
Createdby int `json:"createdby"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
Image string `json:"image"`
|
|
}
|