feat(web): supply line retail override column

В таблице строк приёмки добавлена колонка «Розничная (карточка)».
- Значение по умолчанию — текущая дефолтная розничная цена товара
  (берётся из ProductDto.prices при подборе или из SupplyLineDto.
  currentRetailPrice при загрузке существующего документа).
- Любая ручная правка ставит retailPriceManuallyOverridden=true и
  записывает retailPriceOverride. При проведении документа этот
  override применяется к Product.Prices[default] вместо автонаценки.
- В payload PUT/POST шлём retailPriceManuallyOverridden и
  retailPriceOverride (либо null если override снят).
- types.SupplyLineDto расширен полями currentRetailPrice /
  retailPriceManuallyOverridden / retailPriceOverride.
- В addLineFromProduct unitPrice fallbacks теперь учитывают cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-25 21:12:12 +05:00
parent a5f0fb83d8
commit 7fe30bd98d
2 changed files with 36 additions and 3 deletions

View file

@ -94,6 +94,8 @@ export interface SupplyLineDto {
id: string | null; productId: string; id: string | null; productId: string;
productName: string | null; productArticle: string | null; unitName: string | null; productName: string | null; productArticle: string | null; unitName: string | null;
quantity: number; unitPrice: number; lineTotal: number; sortOrder: number; quantity: number; unitPrice: number; lineTotal: number; sortOrder: number;
retailPriceManuallyOverridden: boolean; retailPriceOverride: number | null;
currentRetailPrice: number | null;
} }
export interface SupplyDto { export interface SupplyDto {

View file

@ -18,6 +18,11 @@ interface LineRow {
unitName: string | null unitName: string | null
quantity: number quantity: number
unitPrice: number unitPrice: number
// Розничная цена с карточки товара на момент загрузки документа
// (read-only baseline). Используется как placeholder для ручного override.
currentRetailPrice: number | null
retailPriceManuallyOverridden: boolean
retailPriceOverride: number | null
} }
interface Form { interface Form {
@ -81,6 +86,9 @@ export function SupplyEditPage() {
unitName: l.unitName, unitName: l.unitName,
quantity: l.quantity, quantity: l.quantity,
unitPrice: l.unitPrice, unitPrice: l.unitPrice,
currentRetailPrice: l.currentRetailPrice ?? null,
retailPriceManuallyOverridden: l.retailPriceManuallyOverridden ?? false,
retailPriceOverride: l.retailPriceOverride ?? null,
})), })),
}) })
} }
@ -121,7 +129,13 @@ export function SupplyEditPage() {
supplierInvoiceNumber: form.supplierInvoiceNumber || null, supplierInvoiceNumber: form.supplierInvoiceNumber || null,
supplierInvoiceDate: form.supplierInvoiceDate ? new Date(form.supplierInvoiceDate).toISOString() : null, supplierInvoiceDate: form.supplierInvoiceDate ? new Date(form.supplierInvoiceDate).toISOString() : null,
notes: form.notes || null, notes: form.notes || null,
lines: form.lines.map((l) => ({ productId: l.productId, quantity: l.quantity, unitPrice: l.unitPrice })), lines: form.lines.map((l) => ({
productId: l.productId,
quantity: l.quantity,
unitPrice: l.unitPrice,
retailPriceManuallyOverridden: l.retailPriceManuallyOverridden,
retailPriceOverride: l.retailPriceManuallyOverridden ? l.retailPriceOverride : null,
})),
} }
if (isNew) { if (isNew) {
return (await api.post<SupplyDto>('/api/purchases/supplies', payload)).data return (await api.post<SupplyDto>('/api/purchases/supplies', payload)).data
@ -167,6 +181,7 @@ export function SupplyEditPage() {
const onSubmit = (e: FormEvent) => { e.preventDefault(); save.mutate() } const onSubmit = (e: FormEvent) => { e.preventDefault(); save.mutate() }
const addLineFromProduct = (p: Product) => { const addLineFromProduct = (p: Product) => {
const defaultRetail = p.prices?.[0]?.amount ?? null
setForm({ setForm({
...form, ...form,
lines: [...form.lines, { lines: [...form.lines, {
@ -175,7 +190,10 @@ export function SupplyEditPage() {
productArticle: p.article, productArticle: p.article,
unitName: p.unitName, unitName: p.unitName,
quantity: 1, quantity: 1,
unitPrice: p.referencePrice ?? 0, unitPrice: p.referencePrice ?? p.cost ?? 0,
currentRetailPrice: defaultRetail,
retailPriceManuallyOverridden: false,
retailPriceOverride: null,
}], }],
}) })
} }
@ -299,6 +317,7 @@ export function SupplyEditPage() {
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[90px]">Ед.</th> <th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[90px]">Ед.</th>
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Количество</th> <th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Количество</th>
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Цена</th> <th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[140px] text-right">Цена</th>
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[160px] text-right">Розничная (карточка)</th>
<th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[160px] text-right">Сумма</th> <th className="py-2 px-3 font-medium text-xs uppercase tracking-wide text-slate-500 w-[160px] text-right">Сумма</th>
<th className="py-2 pl-3 w-[40px]"></th> <th className="py-2 pl-3 w-[40px]"></th>
</tr> </tr>
@ -322,6 +341,18 @@ export function SupplyEditPage() {
onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })} onChange={(n) => updateLine(i, { unitPrice: n ?? 0 })}
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code} currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol} currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
/>
</td>
<td className="py-2 px-3">
<MoneyInput disabled={isPosted}
value={l.retailPriceManuallyOverridden ? l.retailPriceOverride : l.currentRetailPrice}
onChange={(n) => updateLine(i, {
retailPriceManuallyOverridden: true,
retailPriceOverride: n,
})}
currencyCode={currencies.data?.find((c) => c.id === form.currencyId)?.code}
currencySymbol={currencies.data?.find((c) => c.id === form.currencyId)?.symbol}
placeholder={l.currentRetailPrice != null ? String(l.currentRetailPrice) : '—'}
/> />
</td> </td>
<td className="py-2 px-3 text-right font-mono font-semibold"> <td className="py-2 px-3 text-right font-mono font-semibold">
@ -339,7 +370,7 @@ export function SupplyEditPage() {
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td colSpan={4} className="py-3 pr-3 text-right text-sm font-semibold text-slate-600 dark:text-slate-300"> <td colSpan={5} className="py-3 pr-3 text-right text-sm font-semibold text-slate-600 dark:text-slate-300">
Итого: Итого:
</td> </td>
<td className="py-3 px-3 text-right font-mono text-lg font-bold"> <td className="py-3 px-3 text-right font-mono text-lg font-bold">