price fix for app
This commit is contained in:
@@ -68,7 +68,19 @@ export function PublishDialog({ products, tenantid, branchCount, onClose }: Publ
|
|||||||
* the selection.
|
* the selection.
|
||||||
*/
|
*/
|
||||||
const needPrice = needingPrice(products);
|
const needPrice = needingPrice(products);
|
||||||
const askForPrice = needPrice.length > 0;
|
/**
|
||||||
|
* Whether to show the price field.
|
||||||
|
*
|
||||||
|
* Always for ONE product, even when it already has a price — that is the
|
||||||
|
* "Change price" case, and a dialog that hides the field because the value
|
||||||
|
* exists is a dialog that cannot change it. That was the bug: the field was
|
||||||
|
* shown only when something LACKED a price, so a priced product opened a form
|
||||||
|
* with nothing to edit and re-published the figure it already had.
|
||||||
|
*
|
||||||
|
* For a batch, only when something lacks one: a spreadsheet import carries a
|
||||||
|
* price on every row, and asking again is asking a question already answered.
|
||||||
|
*/
|
||||||
|
const askForPrice = needPrice.length > 0 || !isBulk;
|
||||||
|
|
||||||
const priceValue = Number(price);
|
const priceValue = Number(price);
|
||||||
// The backend refuses `price <= 0` with "set a price before publishing — a
|
// The backend refuses `price <= 0` with "set a price before publishing — a
|
||||||
|
|||||||
@@ -697,6 +697,16 @@ function StockPanel() {
|
|||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Said once, above the table. "Opening" reads to a shopkeeper as
|
||||||
|
"the stock I first loaded", and it is not — it is the balance
|
||||||
|
carried in from before today, so a delivery received this
|
||||||
|
morning shows under In and not Opening. The arithmetic is
|
||||||
|
right; only the word was ambiguous. */}
|
||||||
|
<Text type="body" size="xsm" color="secondary" style={{ padding: '0 16px 10px' }}>
|
||||||
|
Today’s ledger. <strong>Opening</strong> is what was on the shelf before today —
|
||||||
|
stock received today counts under <strong>In</strong> and becomes tomorrow’s opening.
|
||||||
|
</Text>
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -741,6 +751,7 @@ function StockPanel() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ may carry only the master. `effectivePrice` already resolves that, and publish
|
|||||||
has to use the same resolution or it would "fill in" a price that exists.
|
has to use the same resolution or it would "fill in" a price that exists.
|
||||||
*/
|
*/
|
||||||
test('the master price counts as having a price', () => {
|
test('the master price counts as having a price', () => {
|
||||||
const plan = planPublish([product(1, 0, 175)], { price: 99, taxpercent: 0 });
|
// Two products, because this is the BATCH rule: with one, the typed figure
|
||||||
|
// is an edit of that product and wins by design. The behaviour under test —
|
||||||
|
// that retailprice counts as "has a price" — is unchanged.
|
||||||
|
const plan = planPublish([product(1, 0, 175), product(2, 0)], { price: 99, taxpercent: 0 });
|
||||||
assert.equal(plan[0]?.price, 175);
|
assert.equal(plan[0]?.price, 175);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,10 +56,49 @@ test('tax follows the same rule as price', () => {
|
|||||||
|
|
||||||
// Zero is a real tax rate and must survive, not fall through to the fallback.
|
// Zero is a real tax rate and must survive, not fall through to the fallback.
|
||||||
test('a tax rate of zero is kept, not treated as missing', () => {
|
test('a tax rate of zero is kept, not treated as missing', () => {
|
||||||
const plan = planPublish([product(1, 100, 0, 0)], { price: 0, taxpercent: 18 });
|
// Zero-rated goods are real — unpackaged staples are 0% GST — so 0 must not
|
||||||
|
// be read as "no tax set". Batch context, as above.
|
||||||
|
const plan = planPublish([product(1, 100, 0, 0), product(2, 100)], { price: 0, taxpercent: 18 });
|
||||||
assert.equal(plan[0]?.taxpercent, 0);
|
assert.equal(plan[0]?.taxpercent, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('only the unpriced products are counted as needing one', () => {
|
test('only the unpriced products are counted as needing one', () => {
|
||||||
assert.equal(needingPrice([product(1, 395), product(2, 0), product(3, 0, 88)]).length, 1);
|
assert.equal(needingPrice([product(1, 395), product(2, 0), product(3, 0, 88)]).length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Changing a price that already exists.
|
||||||
|
|
||||||
|
`own > 0 ? own : typed` protected a bulk release — seventeen imported products
|
||||||
|
must not all take one typed figure — but it also made "Change price" impossible:
|
||||||
|
the dialog sent the product's existing price straight back, so a price could be
|
||||||
|
set once and never corrected.
|
||||||
|
|
||||||
|
One product is an edit of that product. Several is a release of a batch. The two
|
||||||
|
need opposite defaults, and these tests pin both.
|
||||||
|
*/
|
||||||
|
|
||||||
|
test('one product takes the typed price, even when it already has one', () => {
|
||||||
|
const [line] = planPublish([product(7, 395)], { price: 250, taxpercent: 5 });
|
||||||
|
assert.equal(line?.price, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('one product takes the typed tax too', () => {
|
||||||
|
const [line] = planPublish([product(7, 395)], { price: 250, taxpercent: 12 });
|
||||||
|
assert.equal(line?.taxpercent, 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a batch still protects the prices it already has', () => {
|
||||||
|
// The failure this guards: ₹395 rice, ₹30 crisps and ₹105 cleaner released
|
||||||
|
// together, all set to whichever figure prefilled from the first.
|
||||||
|
const lines = planPublish(
|
||||||
|
[product(1, 395), product(2, 30), product(3, 105)],
|
||||||
|
{ price: 999, taxpercent: 5 },
|
||||||
|
);
|
||||||
|
assert.deepEqual(lines.map((line) => line.price), [395, 30, 105]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a batch still fills in the ones with no price', () => {
|
||||||
|
const lines = planPublish([product(1, 395), product(2, 0)], { price: 60, taxpercent: 5 });
|
||||||
|
assert.deepEqual(lines.map((line) => line.price), [395, 60]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ import { effectivePrice } from './productState';
|
|||||||
* with one typed figure sets all seventeen to it and loses the originals, with
|
* with one typed figure sets all seventeen to it and loses the originals, with
|
||||||
* nothing to restore them from.
|
* nothing to restore them from.
|
||||||
*
|
*
|
||||||
* The rule: a product that HAS a price is released at its own. The typed figure
|
* The rule for a BATCH: a product that has a price is released at its own, and
|
||||||
* fills in only for one that has none, which after a spreadsheet import is
|
* the typed figure fills in only for one that has none — which after a
|
||||||
* usually nothing at all.
|
* spreadsheet import is usually nothing at all.
|
||||||
|
*
|
||||||
|
* The rule for ONE product is the opposite, and it has to be: "Change price"
|
||||||
|
* opens on a single product, and a form whose typed value is discarded because
|
||||||
|
* the field already had a value is not a form. That was the bug — a price could
|
||||||
|
* be set once and never corrected, because `own > 0` sent the old figure back
|
||||||
|
* every time.
|
||||||
*/
|
*/
|
||||||
export interface PublishLine {
|
export interface PublishLine {
|
||||||
productid: number;
|
productid: number;
|
||||||
@@ -22,15 +28,19 @@ export interface PublishLine {
|
|||||||
|
|
||||||
export function planPublish(
|
export function planPublish(
|
||||||
products: readonly Product[],
|
products: readonly Product[],
|
||||||
fallback: { price: number; taxpercent: number },
|
typed: { price: number; taxpercent: number },
|
||||||
): PublishLine[] {
|
): PublishLine[] {
|
||||||
|
// One product is an edit of that product: the merchant is looking at its
|
||||||
|
// price, has typed another, and means it.
|
||||||
|
const isSingle = products.length === 1;
|
||||||
|
|
||||||
return products.map((product) => {
|
return products.map((product) => {
|
||||||
const own = effectivePrice(product);
|
const own = effectivePrice(product);
|
||||||
const ownTax = Number(product.taxpercent ?? NaN);
|
const ownTax = Number(product.taxpercent ?? NaN);
|
||||||
return {
|
return {
|
||||||
productid: product.productid,
|
productid: product.productid,
|
||||||
price: own > 0 ? own : fallback.price,
|
price: isSingle || own <= 0 ? typed.price : own,
|
||||||
taxpercent: Number.isFinite(ownTax) ? ownTax : fallback.taxpercent,
|
taxpercent: isSingle || !Number.isFinite(ownTax) ? typed.taxpercent : ownTax,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -528,6 +528,12 @@ function StockPanel({ rows, isLoading }: { rows: StockStatementRow[]; isLoading:
|
|||||||
<Text type="body" size="sm" color="secondary">
|
<Text type="body" size="sm" color="secondary">
|
||||||
{count(rows.length)} line{rows.length === 1 ? '' : 's'}
|
{count(rows.length)} line{rows.length === 1 ? '' : 's'}
|
||||||
</Text>
|
</Text>
|
||||||
|
{/* See the same note on the admin ledger: "Opening" is the balance
|
||||||
|
carried in from before today, not the stock first loaded. */}
|
||||||
|
<Text type="body" size="xsm" color="secondary">
|
||||||
|
Today’s ledger. <strong>Opening</strong> is what was on the shelf before today — stock
|
||||||
|
received today counts under <strong>In</strong> and becomes tomorrow’s opening.
|
||||||
|
</Text>
|
||||||
<Card padding={0} elevation="low">
|
<Card padding={0} elevation="low">
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table
|
<table
|
||||||
|
|||||||
Reference in New Issue
Block a user