Same 30-per-sheet, 1" x 2-5/8" grid as 5160 (rounded corners are the only difference), so unlike 61526 this one isn't flagged approximate.
167 lines
5.0 KiB
Go
167 lines
5.0 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
|
|
"inventree/internal/assets"
|
|
"inventree/internal/models"
|
|
)
|
|
|
|
// LabelTemplate describes a sheet of adhesive labels: cell size, grid, and
|
|
// page margins, all in inches, used to lay out printable asset tags.
|
|
type LabelTemplate struct {
|
|
Key string
|
|
DisplayName string
|
|
LabelWidthIn float64
|
|
LabelHeightIn float64
|
|
Cols int
|
|
Rows int
|
|
MarginTopIn float64
|
|
MarginLeftIn float64
|
|
ColGapIn float64
|
|
RowGapIn float64
|
|
QRSizeIn float64
|
|
// Approximate is true for templates whose margins/grid aren't from a
|
|
// verified manufacturer spec sheet - print one test page on plain
|
|
// paper and hold it up to a real label sheet before committing a
|
|
// full (often expensive/waterproof) sheet to the printer.
|
|
Approximate bool
|
|
}
|
|
|
|
var labelTemplates = map[string]LabelTemplate{
|
|
"5160": {
|
|
Key: "5160", DisplayName: `Avery 5160 - 1" x 2-5/8" (30/sheet)`,
|
|
LabelWidthIn: 2.625, LabelHeightIn: 1, Cols: 3, Rows: 10,
|
|
MarginTopIn: 0.5, MarginLeftIn: 0.1875, ColGapIn: 0.125, RowGapIn: 0,
|
|
QRSizeIn: 0.85,
|
|
},
|
|
"5260": {
|
|
// Same 30-per-sheet grid as 5160 (1" x 2-5/8"); 5260 is just the
|
|
// rounded-corner version of the same physical layout.
|
|
Key: "5260", DisplayName: `Avery 5260 - 1" x 2-5/8" (30/sheet, rounded corners)`,
|
|
LabelWidthIn: 2.625, LabelHeightIn: 1, Cols: 3, Rows: 10,
|
|
MarginTopIn: 0.5, MarginLeftIn: 0.1875, ColGapIn: 0.125, RowGapIn: 0,
|
|
QRSizeIn: 0.85,
|
|
},
|
|
"61526": {
|
|
Key: "61526", DisplayName: `Asset Tag Labels 61526 - 3/4" x 2" (~48/sheet)`,
|
|
LabelWidthIn: 2, LabelHeightIn: 0.75, Cols: 4, Rows: 12,
|
|
MarginTopIn: 1, MarginLeftIn: 0.25, ColGapIn: 0, RowGapIn: 0,
|
|
QRSizeIn: 0.62, Approximate: true,
|
|
},
|
|
}
|
|
|
|
const defaultLabelTemplate = "5160"
|
|
|
|
func (t LabelTemplate) css() template.CSS {
|
|
return template.CSS(fmt.Sprintf(`
|
|
.label-sheet {
|
|
width: 8.5in;
|
|
box-sizing: border-box;
|
|
padding: %.4fin %.4fin;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
grid-template-columns: repeat(%d, %.4fin);
|
|
column-gap: %.4fin;
|
|
row-gap: %.4fin;
|
|
justify-content: center;
|
|
}
|
|
.label {
|
|
width: %.4fin;
|
|
height: %.4fin;
|
|
box-sizing: border-box;
|
|
padding: 0.06in 0.08in;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.08in;
|
|
overflow: hidden;
|
|
page-break-inside: avoid;
|
|
}
|
|
.label img { width: %.4fin; height: %.4fin; flex-shrink: 0; }
|
|
`,
|
|
t.MarginTopIn, t.MarginLeftIn, t.Cols, t.LabelWidthIn, t.ColGapIn, t.RowGapIn,
|
|
t.LabelWidthIn, t.LabelHeightIn, t.QRSizeIn, t.QRSizeIn))
|
|
}
|
|
|
|
type labelItem struct {
|
|
Asset models.Asset
|
|
// template.URL marks this as pre-vetted safe (it's server-generated,
|
|
// never user input), so html/template's URL filter doesn't replace it
|
|
// with the "#ZgotmplZ" safety placeholder the way it would a plain
|
|
// string containing a data: URI.
|
|
QRData template.URL
|
|
}
|
|
|
|
type labelsPageData struct {
|
|
User *models.User
|
|
Labels []labelItem
|
|
SheetCSS template.CSS
|
|
Templates map[string]LabelTemplate
|
|
TemplateKeys []string
|
|
Selected LabelTemplate
|
|
AssetID string
|
|
FolderID string
|
|
Recursive bool
|
|
}
|
|
|
|
// handleLabelsPage renders a print-ready sheet of QR-coded asset labels:
|
|
// either a single asset (?asset_id=) or every asset in a folder
|
|
// (?folder_id=&recursive=1), or the whole inventory with neither param.
|
|
// Each QR code encodes an absolute /scan/<tag> URL so a phone camera (or a
|
|
// USB barcode scanner focused on a browser address bar) opens straight to
|
|
// that asset.
|
|
func (a *App) handleLabelsPage(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
origin := requestOrigin(r)
|
|
|
|
tmpl, ok := labelTemplates[r.URL.Query().Get("template")]
|
|
if !ok {
|
|
tmpl = labelTemplates[defaultLabelTemplate]
|
|
}
|
|
|
|
var list []models.Asset
|
|
assetIDParam := r.URL.Query().Get("asset_id")
|
|
folderIDParam := r.URL.Query().Get("folder_id")
|
|
recursive := r.URL.Query().Get("recursive") == "1"
|
|
|
|
if assetID, err := queryOptionalID(r, "asset_id"); err == nil && assetID != nil {
|
|
asset, err := a.Assets.Get(ctx, *assetID)
|
|
if err != nil {
|
|
writeErr(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
list = []models.Asset{*asset}
|
|
} else {
|
|
filter := assets.ListFilter{Page: 1, PageSize: 2000}
|
|
if folderID, err := queryOptionalID(r, "folder_id"); err == nil && folderID != nil {
|
|
filter.FolderID = folderID
|
|
filter.FolderSet = true
|
|
filter.IncludeSub = recursive
|
|
}
|
|
batch, _, err := a.Assets.List(ctx, filter)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
list = batch
|
|
}
|
|
|
|
labels := make([]labelItem, 0, len(list))
|
|
for _, as := range list {
|
|
qr, err := qrDataURI(origin+"/scan/"+as.AssetTag, 220)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
labels = append(labels, labelItem{Asset: as, QRData: template.URL(qr)})
|
|
}
|
|
|
|
a.Render(w, http.StatusOK, "labels", labelsPageData{
|
|
User: currentUser(r), Labels: labels, SheetCSS: tmpl.css(),
|
|
Templates: labelTemplates, TemplateKeys: []string{"5160", "5260", "61526"},
|
|
Selected: tmpl, AssetID: assetIDParam, FolderID: folderIDParam, Recursive: recursive,
|
|
})
|
|
}
|