Sales import Working
This commit is contained in:
1 parent
7be26ad9da
commit
7656c7e877
11 files changed
+190
-77
No files matched your search
Vendored
+3
-1
@@ -8,7 +8,7 @@
|
||||
],
|
||||
"python.venvPath": "${workspaceFolder}/barker",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
// Quality-of-life
|
||||
// Quality-of-life
|
||||
/* --- Pylance: keep IntelliSense, disable type checking --- */
|
||||
// "python.analysis.typeCheckingMode": "off",
|
||||
// "python.analysis.diagnosticMode": "openFilesOnly",
|
||||
@@ -40,6 +40,7 @@
|
||||
"**/.mypy_cache": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/.venv": true,
|
||||
".idea": true,
|
||||
"**/node_modules": true,
|
||||
"typings": true,
|
||||
@@ -56,6 +57,7 @@
|
||||
"**/.mypy_cache": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
"**/.venv": true,
|
||||
".idea": true,
|
||||
"**/node_modules": true,
|
||||
"**/poetry.lock": true,
|
||||
|
||||
Vendored
+1
-1
@@ -4,7 +4,7 @@
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"script": "start -- --port 4200",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/overlord"
|
||||
|
||||
@@ -27,6 +27,7 @@ PERMISSION_ID = "28514cc1-4004-441d-98c5-26f1df917fd2"
|
||||
ROLE_PERMISSION_ID = "7bbaf067-b174-409b-8223-b3b4a309dfd8"
|
||||
ROLE_OWNER_ID = "52e08c0c-048a-784f-be10-6e129ad4b5d4"
|
||||
COST_CENTRE_PRODUCTION_ID = "0715924d-373e-4765-a46e-dd00f7e1aede"
|
||||
COST_CENTRE_SALE_ID = "a61fc66c-8ce1-4d1d-8dde-98e33e1afc4f"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -75,6 +76,7 @@ def upgrade() -> None:
|
||||
sa.column("is_fixture", sa.Boolean),
|
||||
)
|
||||
op.execute(cost_centres.insert().values(id=COST_CENTRE_PRODUCTION_ID, name="Production", is_fixture=True))
|
||||
op.execute(cost_centres.insert().values(id=COST_CENTRE_SALE_ID, name="Sale", is_fixture=True))
|
||||
|
||||
permissions = sa.table(
|
||||
"permissions",
|
||||
@@ -102,6 +104,7 @@ def downgrade() -> None:
|
||||
)
|
||||
op.execute(sa.text("DELETE FROM permissions WHERE id = :permission_id").bindparams(permission_id=PERMISSION_ID))
|
||||
op.execute(sa.text("DELETE FROM cost_centres WHERE id = :id").bindparams(id=COST_CENTRE_PRODUCTION_ID))
|
||||
op.execute(sa.text("DELETE FROM cost_centres WHERE id = :id").bindparams(id=COST_CENTRE_SALE_ID))
|
||||
op.drop_index(op.f("ix_barker_products_brewman_product_id"), table_name="barker_products")
|
||||
op.drop_index(op.f("ix_barker_products_barker_product_id"), table_name="barker_products")
|
||||
op.drop_table("barker_products")
|
||||
@@ -55,6 +55,10 @@ class CostCentre:
|
||||
def cost_centre_production(cls) -> uuid.UUID:
|
||||
return uuid.UUID("0715924d-373e-4765-a46e-dd00f7e1aede")
|
||||
|
||||
@classmethod
|
||||
def cost_centre_sale(cls) -> uuid.UUID:
|
||||
return uuid.UUID("a61fc66c-8ce1-4d1d-8dde-98e33e1afc4f")
|
||||
|
||||
@classmethod
|
||||
def overall(cls) -> CostCentreLink:
|
||||
return CostCentreLink(id_=uuid.UUID("36f59436-522a-0746-ae94-e0f746bf6c0d"), name="Overall")
|
||||
@@ -6,7 +6,7 @@ from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, Numeric, Unicode, Uuid, func, text
|
||||
from sqlalchemy import Date, FetchedValue, ForeignKey, Numeric, Unicode, Uuid, func, text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -24,7 +24,6 @@ class SkuVersion:
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
|
||||
)
|
||||
# product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False) # should i remove this?
|
||||
# DB column is "product_id", but ORM attribute is private: "_product_id"
|
||||
_product_id: Mapped[uuid.UUID] = mapped_column(
|
||||
"product_id",
|
||||
@@ -33,6 +32,8 @@ class SkuVersion:
|
||||
nullable=False,
|
||||
repr=False, # hides in dataclass repr
|
||||
init=False, # not part of __init__ signature (dataclass)
|
||||
server_default=FetchedValue(),
|
||||
server_onupdate=FetchedValue(),
|
||||
)
|
||||
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
|
||||
units: Mapped[str] = mapped_column(Unicode, nullable=False)
|
||||
@@ -66,16 +67,12 @@ class SkuVersion:
|
||||
product_yield: Decimal = Decimal("1.0"),
|
||||
cost_price: Decimal = Decimal("0.0"),
|
||||
sale_price: Decimal = Decimal("0.0"),
|
||||
# product_id: uuid.UUID | None = None,
|
||||
# product: Product | None = None,
|
||||
sku_id: uuid.UUID | None = None,
|
||||
sku: StockKeepingUnit | None = None,
|
||||
id_: uuid.UUID | None = None,
|
||||
valid_from: date | None = None,
|
||||
valid_till: date | None = None,
|
||||
) -> None:
|
||||
# if product_id is not None:
|
||||
# self.product_id = product_id
|
||||
if sku_id is not None:
|
||||
self.sku_id = sku_id
|
||||
self.units = units
|
||||
@@ -85,8 +82,6 @@ class SkuVersion:
|
||||
self.sale_price = sale_price
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
# if product is not None:
|
||||
# self.product = product
|
||||
if sku is not None:
|
||||
self.sku = sku
|
||||
self.valid_from = valid_from
|
||||
|
||||
@@ -91,6 +91,7 @@ def build_report(
|
||||
.where(
|
||||
StockKeepingUnit.product_id == product_id,
|
||||
Journal.cost_centre_id != CostCentre.cost_centre_purchase(),
|
||||
Journal.cost_centre_id != CostCentre.cost_centre_sale(),
|
||||
Voucher.date_ >= start_date,
|
||||
Voucher.date_ <= finish_date,
|
||||
)
|
||||
@@ -102,9 +103,13 @@ def build_report(
|
||||
|
||||
for voucher, inventory, journal, stockKeepingUnit in query:
|
||||
journal_debit = journal.debit * -1
|
||||
if voucher.voucher_type == VoucherType.SALE:
|
||||
print(journal_debit, " pre")
|
||||
journal_debit = -1 * journal_debit
|
||||
print(journal_debit)
|
||||
name = (
|
||||
journal.cost_centre.name
|
||||
if voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK]
|
||||
if voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK, VoucherType.SALE]
|
||||
else journal.account.name
|
||||
)
|
||||
debit_q = inventory.quantity if journal_debit == 1 else None
|
||||
@@ -131,7 +136,8 @@ def build_report(
|
||||
],
|
||||
type_=voucher.voucher_type.name.replace("_", " ").title(),
|
||||
narration=voucher.narration,
|
||||
posted=voucher.posted or voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK],
|
||||
posted=voucher.posted
|
||||
or voucher.voucher_type in [VoucherType.ISSUE, VoucherType.CLOSING_STOCK, VoucherType.SALE],
|
||||
debit_quantity=debit_q,
|
||||
debit_amount=debit_a,
|
||||
debit_unit=debit_u,
|
||||
@@ -162,8 +168,6 @@ def opening_balance(
|
||||
.join(Batch.sku)
|
||||
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
|
||||
.where(
|
||||
Voucher.id == Inventory.voucher_id,
|
||||
Voucher.id == Journal.voucher_id,
|
||||
StockKeepingUnit.product_id == product_id,
|
||||
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
||||
Voucher.date_ < start_date,
|
||||
|
||||
@@ -160,7 +160,10 @@ def _pg_normalize(column: Any) -> Any:
|
||||
"""Return the SQLAlchemy expression to normalize a string in PostgreSQL, matching _normalize."""
|
||||
return func.btrim(
|
||||
func.regexp_replace(
|
||||
func.regexp_replace(func.lower(func.trim(column)), r"[^\w\s-]", "", "g"), r"[\s_-]+", "-", "g"
|
||||
func.regexp_replace(func.lower(func.trim(column)), r"[^[:alnum:]_[:space:]-]", "", "g"),
|
||||
r"[[:space:]_-]+",
|
||||
"-",
|
||||
"g",
|
||||
),
|
||||
"-",
|
||||
)
|
||||
@@ -264,6 +267,7 @@ def _create_sku(db: Session, product_id: uuid.UUID, line: BarkerSaleLine, date_:
|
||||
valid_till=None,
|
||||
)
|
||||
)
|
||||
db.flush()
|
||||
return sku.id
|
||||
|
||||
|
||||
@@ -297,6 +301,7 @@ def _create_product(db: Session, line: BarkerSaleLine, date_: date) -> uuid.UUID
|
||||
valid_till=None,
|
||||
)
|
||||
)
|
||||
db.flush()
|
||||
_create_sku(db, product.id, line, date_)
|
||||
return product.id
|
||||
|
||||
@@ -339,6 +344,21 @@ def _update_name_if_needed(db: Session, product_id: uuid.UUID, new_name: str, da
|
||||
).scalar_one_or_none()
|
||||
|
||||
if active_pv is not None and _normalize(active_pv.name) != wanted:
|
||||
clashing_rows = db.execute(
|
||||
select(ProductVersion.product_id, ProductVersion.name).where(
|
||||
_pg_normalize(ProductVersion.name) == wanted,
|
||||
ProductVersion.product_id != product_id,
|
||||
(ProductVersion.valid_from == None) | (ProductVersion.valid_from <= date_), # noqa: E711
|
||||
(ProductVersion.valid_till == None) | (ProductVersion.valid_till >= date_), # noqa: E711
|
||||
)
|
||||
).all()
|
||||
if clashing_rows:
|
||||
conflicting_names = ", ".join(f"'{p_name}'" for (_, p_name) in clashing_rows)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Cannot rename product '{active_pv.name}' to '{new_name}': an active product with that name already exists ({conflicting_names}).",
|
||||
)
|
||||
|
||||
if active_pv.valid_from == date_:
|
||||
active_pv.name = new_name
|
||||
active_pv.handle = ProductVersion.slugify(new_name)
|
||||
@@ -387,6 +407,26 @@ def _update_units_if_needed(db: Session, sku_id: uuid.UUID, new_units: str, date
|
||||
).scalar_one_or_none()
|
||||
|
||||
if active_sv is not None and _normalize(active_sv.units) != wanted:
|
||||
clashing_sv = (
|
||||
db.execute(
|
||||
select(SkuVersion.units).where(
|
||||
SkuVersion._product_id == active_sv._product_id,
|
||||
SkuVersion.sku_id != sku_id,
|
||||
_pg_normalize(SkuVersion.units) == wanted,
|
||||
(SkuVersion.valid_from == None) | (SkuVersion.valid_from <= date_), # noqa: E711
|
||||
(SkuVersion.valid_till == None) | (SkuVersion.valid_till >= date_), # noqa: E711
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
if clashing_sv:
|
||||
conflicting_units = ", ".join(f"'{u}'" for u in clashing_sv)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Cannot rename SKU units '{active_sv.units}' to '{new_units}': an active SKU with those units already exists ({conflicting_units}).",
|
||||
)
|
||||
|
||||
if active_sv.valid_from == date_:
|
||||
active_sv.units = new_units
|
||||
db.flush()
|
||||
|
||||
@@ -37,7 +37,8 @@ from .products import ALL_PURCHASES_ID, ProvisionedProduct, existing_binding, re
|
||||
|
||||
|
||||
NARRATION_PREFIX = "Sales import from Barker for"
|
||||
PRODUCTION_ID = CostCentre.cost_centre_production()
|
||||
# PRODUCTION_ID = CostCentre.cost_centre_production()
|
||||
SALE_ID = CostCentre.cost_centre_sale()
|
||||
Q2 = Decimal("0.01")
|
||||
Q5 = Decimal("0.00001")
|
||||
|
||||
@@ -316,12 +317,15 @@ def create_voucher(db: Session, plan: PlannedVoucher, user_id: uuid.UUID) -> Non
|
||||
voucher.inventories.append(item)
|
||||
db.add(item)
|
||||
amount = sum((item.amount for item in voucher.inventories), Decimal(0))
|
||||
voucher.journals.append(
|
||||
Journal(debit=-1, account_id=ALL_PURCHASES_ID, amount=round(amount, 2), cost_centre_id=plan.cost_centre_id)
|
||||
)
|
||||
voucher.journals.append(
|
||||
Journal(debit=1, account_id=ALL_PURCHASES_ID, amount=round(amount, 2), cost_centre_id=PRODUCTION_ID)
|
||||
credit_journal = Journal(
|
||||
debit=-1, account_id=ALL_PURCHASES_ID, amount=round(amount, 2), cost_centre_id=plan.cost_centre_id
|
||||
)
|
||||
voucher.journals.append(credit_journal)
|
||||
db.add(credit_journal)
|
||||
debit_journal = Journal(debit=1, account_id=ALL_PURCHASES_ID, amount=round(amount, 2), cost_centre_id=SALE_ID)
|
||||
voucher.journals.append(debit_journal)
|
||||
db.add(debit_journal)
|
||||
db.flush()
|
||||
check_journals_are_valid(voucher)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="two-col">
|
||||
<div class="col">
|
||||
<h3>Permissions</h3>
|
||||
@for (p of item().permissions; track p; let i = $index) {
|
||||
@for (p of item().permissions; track p.id; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-checkbox
|
||||
[formField]="form.permissions[i].permission"
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div class="col">
|
||||
<h3>Includes Roles</h3>
|
||||
@for (r of item().includedRoles; track r; let i = $index) {
|
||||
@for (r of item().includedRoles; track r.id; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-checkbox [formField]="form.includedRoles[i].role" class="flex-auto">{{ r.name }}</mat-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Component, inject, afterNextRender, effect, input, computed, linkedSignal } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
untracked,
|
||||
afterNextRender,
|
||||
} from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
@@ -12,14 +21,31 @@ import { Router } from '@angular/router';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { Role } from '../role';
|
||||
import { Permission } from '../permission';
|
||||
import { Role, RoleItem } from '../role';
|
||||
import { RoleService } from '../role.service';
|
||||
|
||||
export interface RoleDetailFormData {
|
||||
name: string;
|
||||
permissions: {
|
||||
id: string;
|
||||
name: string;
|
||||
permission: boolean;
|
||||
direct: boolean;
|
||||
disabled: boolean;
|
||||
}[];
|
||||
includedRoles: {
|
||||
id: string;
|
||||
name: string;
|
||||
role: boolean;
|
||||
permissionIds: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-role-detail',
|
||||
templateUrl: './role-detail.component.html',
|
||||
styleUrls: ['./role-detail.component.css'],
|
||||
|
||||
imports: [
|
||||
FormField,
|
||||
FormRoot,
|
||||
@@ -44,63 +70,93 @@ export class RoleDetailComponent {
|
||||
|
||||
model = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal) => ({
|
||||
name: itemVal.name ?? '',
|
||||
permissions: itemVal.permissions.map((x) => ({
|
||||
permission: x.enabled,
|
||||
disabled: false,
|
||||
})),
|
||||
includedRoles: itemVal.includedRoles.map((x) => ({
|
||||
role: x.enabled,
|
||||
})),
|
||||
}),
|
||||
computation: (itemVal): RoleDetailFormData => {
|
||||
const implied = new Set<string>();
|
||||
(itemVal.includedRoles || []).forEach((r) => {
|
||||
if (r.enabled) {
|
||||
(r.permissionIds || []).forEach((pid) => implied.add(pid));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: itemVal.name ?? '',
|
||||
permissions: (itemVal.permissions || []).map((p) => {
|
||||
const isImplied = implied.has(p.id as string);
|
||||
const isDirect = p.enabled === true;
|
||||
return {
|
||||
id: p.id ?? '',
|
||||
name: p.name,
|
||||
permission: isImplied || isDirect,
|
||||
direct: isDirect,
|
||||
disabled: isImplied,
|
||||
};
|
||||
}),
|
||||
includedRoles: (itemVal.includedRoles || []).map((r) => ({
|
||||
id: r.id ?? '',
|
||||
name: r.name,
|
||||
role: r.enabled === true,
|
||||
permissionIds: r.permissionIds || [],
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
private enabledIncludedRoleIds = computed(() => {
|
||||
return this.model()
|
||||
.includedRoles.filter((r) => r.role)
|
||||
.map((r) => r.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
});
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.form.name().focusBoundControl();
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
// Track changes to includedRoles explicitly
|
||||
this.model().includedRoles.map((x) => x.role);
|
||||
this.applyIncludedRolePermissionLocks();
|
||||
// Recompute permission locks whenever the set of enabled included roles changes
|
||||
this.enabledIncludedRoleIds();
|
||||
untracked(() => {
|
||||
this.applyIncludedRolePermissionLocks();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private applyIncludedRolePermissionLocks(): void {
|
||||
this.model.update((currentModel) => {
|
||||
const implied = new Set<string>();
|
||||
const currentItem = this.item();
|
||||
|
||||
currentItem.includedRoles.forEach((roleOpt, idx) => {
|
||||
const checked = currentModel.includedRoles[idx]?.role;
|
||||
if (checked) {
|
||||
(roleOpt.permissionIds || []).forEach((pid) => implied.add(pid));
|
||||
currentModel.includedRoles.forEach((role) => {
|
||||
if (role.role) {
|
||||
(role.permissionIds || []).forEach((pid) => implied.add(pid));
|
||||
}
|
||||
});
|
||||
|
||||
const newPerms = [...currentModel.permissions];
|
||||
let changed = false;
|
||||
const newPerms = currentModel.permissions.map((p) => {
|
||||
// If not disabled, direct state tracks the user's checkbox value
|
||||
const currentDirect = p.disabled ? p.direct : p.permission;
|
||||
const isImplied = implied.has(p.id);
|
||||
|
||||
currentItem.permissions.forEach((perm, idx) => {
|
||||
const isImplied = implied.has(perm.id as string);
|
||||
const isDirect = perm.enabled === true;
|
||||
const newPermission = isImplied ? true : currentDirect;
|
||||
const newDisabled = isImplied;
|
||||
|
||||
if (!newPerms[idx]) return;
|
||||
|
||||
if (isImplied) {
|
||||
if (!newPerms[idx].permission || !newPerms[idx].disabled) {
|
||||
newPerms[idx] = { permission: true, disabled: true };
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
if (newPerms[idx].permission !== isDirect || newPerms[idx].disabled) {
|
||||
newPerms[idx] = { permission: isDirect, disabled: false };
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
p.direct !== currentDirect ||
|
||||
p.permission !== newPermission ||
|
||||
p.disabled !== newDisabled
|
||||
) {
|
||||
changed = true;
|
||||
return {
|
||||
...p,
|
||||
direct: currentDirect,
|
||||
permission: newPermission,
|
||||
disabled: newDisabled,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
@@ -149,21 +205,26 @@ export class RoleDetailComponent {
|
||||
|
||||
getItem(): Role {
|
||||
const formModel = this.model();
|
||||
const currentItem = this.item();
|
||||
currentItem.name = formModel.name ?? '';
|
||||
const permArray = formModel.permissions;
|
||||
|
||||
currentItem.permissions.forEach((p, index) => {
|
||||
if (permArray[index]?.disabled) {
|
||||
return;
|
||||
}
|
||||
p.enabled = permArray[index]?.permission ?? false;
|
||||
return new Role({
|
||||
...this.item(),
|
||||
name: formModel.name.trim(),
|
||||
permissions: formModel.permissions.map(
|
||||
(p) =>
|
||||
new Permission({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
enabled: p.disabled ? p.direct : p.permission,
|
||||
}),
|
||||
),
|
||||
includedRoles: formModel.includedRoles.map(
|
||||
(r) =>
|
||||
new RoleItem({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
enabled: r.role,
|
||||
permissionIds: r.permissionIds,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const includeArray = formModel.includedRoles;
|
||||
currentItem.includedRoles.forEach((r, index) => {
|
||||
r.enabled = includeArray[index]?.role ?? false;
|
||||
});
|
||||
return currentItem;
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,6 @@ export class SalesImportComponent {
|
||||
}
|
||||
|
||||
trackLine(_index: number, line: SaleLine): string {
|
||||
return `${line.skuId ?? line.productName}-${line.rate}-${line.isHappyHour}`;
|
||||
return `${line.skuId ?? line.name}-${line.rate}-${line.isHappyHour}`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user