From ab2d2f8975a16e46e82e62054196c2833fc285cc Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 22 Nov 2022 14:42:49 +0530 Subject: [PATCH] incr: add make stock transfer --- models/baseModels/Invoice/Invoice.ts | 135 +++++++++++++++++- models/baseModels/InvoiceItem/InvoiceItem.ts | 20 ++- .../PurchaseInvoice/PurchaseInvoice.ts | 10 +- .../baseModels/SalesInvoice/SalesInvoice.ts | 10 +- models/helpers.ts | 86 +++++------ models/inventory/StockTransfer.ts | 81 +++++++++++ schemas/app/Invoice.json | 6 + schemas/app/InvoiceItem.json | 6 + schemas/app/inventory/PurchaseReceipt.json | 7 + schemas/app/inventory/Shipment.json | 7 + schemas/app/inventory/StockTransfer.json | 2 +- src/pages/GeneralForm.vue | 7 + src/pages/InvoiceForm.vue | 17 ++- src/utils/types.ts | 6 +- src/utils/ui.ts | 18 ++- 15 files changed, 346 insertions(+), 72 deletions(-) diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index d897d86f..8ea493cd 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -2,14 +2,23 @@ import { Fyo } from 'fyo'; import { DocValue, DocValueMap } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; import { - CurrenciesMap, DefaultMap, + Action, + CurrenciesMap, + DefaultMap, FiltersMap, FormulaMap, - HiddenMap + HiddenMap, } from 'fyo/model/types'; import { DEFAULT_CURRENCY } from 'fyo/utils/consts'; import { ValidationError } from 'fyo/utils/errors'; -import { getExchangeRate, getNumberSeries } from 'models/helpers'; +import { + getExchangeRate, + getInvoiceActions, + getNumberSeries, +} from 'models/helpers'; +import { InventorySettings } from 'models/inventory/InventorySettings'; +import { StockTransfer } from 'models/inventory/StockTransfer'; +import { getStockTransfer } from 'models/inventory/tests/helpers'; import { Transactional } from 'models/Transactional/Transactional'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; @@ -17,6 +26,7 @@ import { FieldTypeEnum, Schema } from 'schemas/types'; import { getIsNullOrUndef, safeParseFloat } from 'utils'; import { Defaults } from '../Defaults/Defaults'; import { InvoiceItem } from '../InvoiceItem/InvoiceItem'; +import { Item } from '../Item/Item'; import { Party } from '../Party/Party'; import { Payment } from '../Payment/Payment'; import { Tax } from '../Tax/Tax'; @@ -39,6 +49,7 @@ export abstract class Invoice extends Transactional { discountAmount?: Money; discountPercent?: number; discountAfterTax?: boolean; + stockNotTransferred?: number; submitted?: boolean; cancelled?: boolean; @@ -341,8 +352,25 @@ export abstract class Invoice extends Transactional { return this.baseGrandTotal!; }, }, + stockNotTransferred: { + formula: async () => { + if (this.submitted) { + return; + } + + return this.getStockNotTransferred(); + }, + dependsOn: ['items'], + }, }; + getStockNotTransferred() { + return (this.items ?? []).reduce( + (acc, item) => (item.stockNotTransferred ?? 0) + acc, + 0 + ); + } + getItemDiscountedAmounts() { let itemDiscountedAmounts = this.fyo.pesa(0); for (const item of this.items ?? []) { @@ -412,4 +440,105 @@ export abstract class Invoice extends Transactional { this.getCurrencies[fieldname] ??= this._getCurrency.bind(this); } } + + static getActions(fyo: Fyo): Action[] { + return getInvoiceActions(fyo); + } + + getPayment(): Payment | null { + if (!this.isSubmitted) { + return null; + } + + const outstandingAmount = this.outstandingAmount; + if (!outstandingAmount) { + return null; + } + + if (this.outstandingAmount?.isZero()) { + return null; + } + + const accountField = this.isSales ? 'account' : 'paymentAccount'; + const data = { + party: this.party, + date: new Date().toISOString().slice(0, 10), + paymentType: this.isSales ? 'Receive' : 'Pay', + amount: this.outstandingAmount, + [accountField]: this.account, + for: [ + { + referenceType: this.schemaName, + referenceName: this.name, + amount: this.outstandingAmount, + }, + ], + }; + + return this.fyo.doc.getNewDoc(ModelNameEnum.Payment, data) as Payment; + } + + async getStockTransfer(): Promise { + if (!this.isSubmitted) { + return null; + } + + if (!this.stockNotTransferred) { + return null; + } + + const schemaName = this.isSales + ? ModelNameEnum.Shipment + : ModelNameEnum.PurchaseReceipt; + + const defaults = (this.fyo.singles.Defaults as Defaults) ?? {}; + let terms; + if (this.isSales) { + terms = defaults.shipmentTerms ?? ''; + } else { + terms = defaults.purchaseReceiptTerms ?? ''; + } + + const data = { + party: this.party, + date: new Date().toISOString(), + terms, + backReference: this.name, + }; + console.log(data.backReference); + + const location = + (this.fyo.singles.InventorySettings as InventorySettings) + .defaultLocation ?? null; + + const transfer = this.fyo.doc.getNewDoc(schemaName, data) as StockTransfer; + for (const row of this.items ?? []) { + if (!row.item) { + continue; + } + + const itemDoc = (await row.loadAndGetLink('item')) as Item; + const item = row.item; + const quantity = row.stockNotTransferred; + const trackItem = itemDoc.trackItem; + const rate = row.rate; + + if (!quantity || !trackItem) { + continue; + } + + await transfer.append('items', { + item, + quantity, + location, + rate, + }); + } + + if (!transfer.items?.length) { + return null; + } + + return transfer; + } } diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index ea3a1982..9675aa57 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -6,7 +6,7 @@ import { FiltersMap, FormulaMap, HiddenMap, - ValidationMap + ValidationMap, } from 'fyo/model/types'; import { DEFAULT_CURRENCY } from 'fyo/utils/consts'; import { ValidationError } from 'fyo/utils/errors'; @@ -14,14 +14,17 @@ import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { FieldTypeEnum, Schema } from 'schemas/types'; import { Invoice } from '../Invoice/Invoice'; +import { Item } from '../Item/Item'; export abstract class InvoiceItem extends Doc { + item?: string; account?: string; amount?: Money; parentdoc?: Invoice; rate?: Money; quantity?: number; tax?: string; + stockNotTransferred?: number; setItemDiscountAmount?: boolean; itemDiscountAmount?: Money; @@ -260,6 +263,21 @@ export abstract class InvoiceItem extends Doc { 'item', ], }, + stockNotTransferred: { + formula: async () => { + if (this.parentdoc?.isSubmitted) { + return; + } + + const item = (await this.loadAndGetLink('item')) as Item; + if (!item.trackItem) { + return 0; + } + + return this.quantity; + }, + dependsOn: ['item', 'quantity'], + }, }; validations: ValidationMap = { diff --git a/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts b/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts index 38f447c9..c9132250 100644 --- a/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts +++ b/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts @@ -1,8 +1,6 @@ -import { Fyo } from 'fyo'; -import { Action, ListViewSettings } from 'fyo/model/types'; +import { ListViewSettings } from 'fyo/model/types'; import { LedgerPosting } from 'models/Transactional/LedgerPosting'; -import { ModelNameEnum } from 'models/types'; -import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers'; +import { getTransactionStatusColumn } from '../../helpers'; import { Invoice } from '../Invoice/Invoice'; import { PurchaseInvoiceItem } from '../PurchaseInvoiceItem/PurchaseInvoiceItem'; @@ -35,10 +33,6 @@ export class PurchaseInvoice extends Invoice { return posting; } - static getActions(fyo: Fyo): Action[] { - return getInvoiceActions(ModelNameEnum.PurchaseInvoice, fyo); - } - static getListViewSettings(): ListViewSettings { return { formRoute: (name) => `/edit/PurchaseInvoice/${name}`, diff --git a/models/baseModels/SalesInvoice/SalesInvoice.ts b/models/baseModels/SalesInvoice/SalesInvoice.ts index 5fd7b0d0..55b50896 100644 --- a/models/baseModels/SalesInvoice/SalesInvoice.ts +++ b/models/baseModels/SalesInvoice/SalesInvoice.ts @@ -1,8 +1,6 @@ -import { Fyo } from 'fyo'; -import { Action, ListViewSettings } from 'fyo/model/types'; +import { ListViewSettings } from 'fyo/model/types'; import { LedgerPosting } from 'models/Transactional/LedgerPosting'; -import { ModelNameEnum } from 'models/types'; -import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers'; +import { getTransactionStatusColumn } from '../../helpers'; import { Invoice } from '../Invoice/Invoice'; import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem'; @@ -35,10 +33,6 @@ export class SalesInvoice extends Invoice { return posting; } - static getActions(fyo: Fyo): Action[] { - return getInvoiceActions(ModelNameEnum.SalesInvoice, fyo); - } - static getListViewSettings(): ListViewSettings { return { formRoute: (name) => `/edit/SalesInvoice/${name}`, diff --git a/models/helpers.ts b/models/helpers.ts index 7fe46ed3..514c6f13 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -13,58 +13,62 @@ import { Defaults, numberSeriesDefaultsMap, } from './baseModels/Defaults/Defaults'; +import { Invoice } from './baseModels/Invoice/Invoice'; import { InvoiceStatus, ModelNameEnum } from './types'; -export function getInvoiceActions( - schemaName: ModelNameEnum.PurchaseInvoice | ModelNameEnum.SalesInvoice, - fyo: Fyo -): Action[] { +export function getInvoiceActions(fyo: Fyo): Action[] { return [ - { - label: fyo.t`Make Payment`, - condition: (doc: Doc) => - doc.isSubmitted && !(doc.outstandingAmount as Money).isZero(), - action: async function makePayment(doc: Doc) { - const payment = fyo.doc.getNewDoc('Payment'); - payment.once('afterSync', async () => { - await payment.submit(); - }); - - const isSales = schemaName === 'SalesInvoice'; - const party = doc.party as string; - const paymentType = isSales ? 'Receive' : 'Pay'; - const hideAccountField = isSales ? 'account' : 'paymentAccount'; - - const { openQuickEdit } = await import('src/utils/ui'); - await openQuickEdit({ - schemaName: 'Payment', - name: payment.name as string, - hideFields: ['party', 'paymentType', 'for'], - defaults: { - party, - [hideAccountField]: doc.account, - date: new Date().toISOString().slice(0, 10), - paymentType, - for: [ - { - referenceType: doc.schemaName, - referenceName: doc.name, - amount: doc.outstandingAmount, - }, - ], - }, - }); - }, - }, + getMakePaymentAction(fyo), + getMakeStockTransferAction(fyo), getLedgerLinkAction(fyo), ]; } +export function getMakeStockTransferAction(fyo: Fyo): Action { + return { + label: fyo.t`Make Stock Transfer`, + condition: (doc: Doc) => doc.isSubmitted && !!doc.stockNotTransferred, + action: async (doc: Doc) => { + const transfer = await (doc as Invoice).getStockTransfer(); + if (!transfer) { + return; + } + + const { routeTo } = await import('src/utils/ui'); + const path = `/edit/${transfer.schemaName}/${transfer.name}`; + await routeTo(path); + }, + }; +} + +export function getMakePaymentAction(fyo: Fyo): Action { + return { + label: fyo.t`Make Payment`, + condition: (doc: Doc) => + doc.isSubmitted && !(doc.outstandingAmount as Money).isZero(), + action: async (doc: Doc) => { + const payment = (doc as Invoice).getPayment(); + if (!payment) { + return; + } + + payment.once('afterSync', async () => { + await payment.submit(); + }); + + const { openQuickEdit } = await import('src/utils/ui'); + await openQuickEdit({ + doc: payment, + hideFields: ['party', 'paymentType', 'for'], + }); + }, + }; +} + export function getLedgerLinkAction( fyo: Fyo, isStock: boolean = false ): Action { - let label = fyo.t`Ledger Entries`; let reportClassName = 'GeneralLedger'; diff --git a/models/inventory/StockTransfer.ts b/models/inventory/StockTransfer.ts index a0cbe88d..755874e5 100644 --- a/models/inventory/StockTransfer.ts +++ b/models/inventory/StockTransfer.ts @@ -4,10 +4,12 @@ import { Doc } from 'fyo/model/doc'; import { Action, DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types'; import { ValidationError } from 'fyo/utils/errors'; import { Defaults } from 'models/baseModels/Defaults/Defaults'; +import { Invoice } from 'models/baseModels/Invoice/Invoice'; import { getLedgerLinkAction, getNumberSeries } from 'models/helpers'; import { LedgerPosting } from 'models/Transactional/LedgerPosting'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; +import { getMapFromList } from 'utils/index'; import { StockTransferItem } from './StockTransferItem'; import { Transfer } from './Transfer'; @@ -18,6 +20,7 @@ export abstract class StockTransfer extends Transfer { terms?: string; attachment?: Attachment; grandTotal?: Money; + backReference?: string; items?: StockTransferItem[]; get isSales() { @@ -137,4 +140,82 @@ export abstract class StockTransfer extends Transfer { static getActions(fyo: Fyo): Action[] { return [getLedgerLinkAction(fyo, false), getLedgerLinkAction(fyo, true)]; } + + async afterSubmit() { + await super.afterSubmit(); + await this._updateBackReference(); + } + + async afterCancel(): Promise { + await super.afterCancel(); + await this._updateBackReference(); + } + + async _updateBackReference() { + if (!this.isCancelled && !this.isSubmitted) { + return; + } + + if (!this.backReference) { + return; + } + + const schemaName = this.isSales + ? ModelNameEnum.SalesInvoice + : ModelNameEnum.PurchaseInvoice; + + const invoice = (await this.fyo.doc.getDoc( + schemaName, + this.backReference + )) as Invoice; + const transferMap = this._getTransferMap(); + + for (const row of invoice.items ?? []) { + const item = row.item!; + const quantity = row.quantity!; + const notTransferred = (row.stockNotTransferred as number) ?? 0; + + const transferred = transferMap[item]; + if (!transferred || !notTransferred) { + continue; + } + + if (this.isCancelled) { + await row.set( + 'stockNotTransferred', + Math.min(notTransferred + transferred, quantity) + ); + transferMap[item] = Math.max( + transferred + notTransferred - quantity, + 0 + ); + } else { + await row.set( + 'stockNotTransferred', + Math.max(notTransferred - transferred, 0) + ); + transferMap[item] = Math.max(transferred - notTransferred, 0); + } + } + + const notTransferred = invoice.getStockNotTransferred(); + await invoice.setAndSync('stockNotTransferred', notTransferred); + } + + _getTransferMap() { + return (this.items ?? []).reduce((acc, item) => { + if (!item.item) { + return acc; + } + + if (!item.quantity) { + return acc; + } + + acc[item.item] ??= 0; + acc[item.item] += item.quantity; + + return acc; + }, {} as Record); + } } diff --git a/schemas/app/Invoice.json b/schemas/app/Invoice.json index a82f9e0f..dd713c94 100644 --- a/schemas/app/Invoice.json +++ b/schemas/app/Invoice.json @@ -131,6 +131,12 @@ "placeholder": "Add attachment", "label": "Attachment", "fieldtype": "Attachment" + }, + { + "fieldname": "stockNotTransferred", + "label": "Stock Not Transferred", + "fieldtype": "Float", + "readOnly": true } ], "keywordFields": ["name", "party"] diff --git a/schemas/app/InvoiceItem.json b/schemas/app/InvoiceItem.json index e2130a4f..2d95b90f 100644 --- a/schemas/app/InvoiceItem.json +++ b/schemas/app/InvoiceItem.json @@ -87,6 +87,12 @@ "label": "HSN/SAC", "fieldtype": "Int", "placeholder": "HSN/SAC Code" + }, + { + "fieldname": "stockNotTransferred", + "label": "Stock Not Transferred", + "fieldtype": "Float", + "readOnly": true } ], "tableFields": ["item", "tax", "quantity", "rate", "amount"], diff --git a/schemas/app/inventory/PurchaseReceipt.json b/schemas/app/inventory/PurchaseReceipt.json index d3a23981..33fcebfa 100644 --- a/schemas/app/inventory/PurchaseReceipt.json +++ b/schemas/app/inventory/PurchaseReceipt.json @@ -21,6 +21,13 @@ "create": true, "required": true, "default": "PREC-" + }, + { + "fieldname": "backReference", + "label": "Back Reference", + "fieldtype": "Link", + "target": "PurchaseInvoice", + "readOnly": true } ], "keywordFields": ["name", "party"] diff --git a/schemas/app/inventory/Shipment.json b/schemas/app/inventory/Shipment.json index 21902aef..10df12dd 100644 --- a/schemas/app/inventory/Shipment.json +++ b/schemas/app/inventory/Shipment.json @@ -21,6 +21,13 @@ "create": true, "required": true, "default": "SHPM-" + }, + { + "fieldname": "backReference", + "label": "Back Reference", + "fieldtype": "Link", + "target": "SalesInvoice", + "readOnly": true } ], "keywordFields": ["name", "party"] diff --git a/schemas/app/inventory/StockTransfer.json b/schemas/app/inventory/StockTransfer.json index 2cb6baac..c2fa869a 100644 --- a/schemas/app/inventory/StockTransfer.json +++ b/schemas/app/inventory/StockTransfer.json @@ -16,7 +16,7 @@ { "fieldname": "date", "label": "Date", - "fieldtype": "Date", + "fieldtype": "Datetime", "required": true }, { diff --git a/src/pages/GeneralForm.vue b/src/pages/GeneralForm.vue index 5f472c81..f74bce10 100644 --- a/src/pages/GeneralForm.vue +++ b/src/pages/GeneralForm.vue @@ -55,6 +55,13 @@ @change="(value) => doc.set('numberSeries', value)" :read-only="!doc.notInserted || doc?.submitted" /> + fyo.getField(doc.schemaName, f) + ); + } + this.quickEditFields = fields; }, actions() { diff --git a/src/utils/types.ts b/src/utils/types.ts index fb19c4de..8772c440 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,3 +1,4 @@ +import { Doc } from "fyo/model/doc"; import { FieldTypeEnum } from "schemas/types"; export interface MessageDialogButton { @@ -23,8 +24,9 @@ export type WindowAction = 'close' | 'minimize' | 'maximize' | 'unmaximize'; export type SettingsTab = 'Invoice' | 'General' | 'System'; export interface QuickEditOptions { - schemaName: string; - name: string; + doc?: Doc; + schemaName?: string; + name?: string; hideFields?: string[]; showFields?: string[]; defaults?: Record; diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 5e9bc5f3..78fe67cb 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -7,7 +7,7 @@ import { t } from 'fyo'; import { Doc } from 'fyo/model/doc'; import { Action } from 'fyo/model/types'; import { getActions } from 'fyo/utils'; -import { getDbError, LinkValidationError } from 'fyo/utils/errors'; +import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors'; import { ModelNameEnum } from 'models/types'; import { handleErrorWithDialog } from 'src/errorHandling'; import { fyo } from 'src/initFyo'; @@ -26,12 +26,22 @@ import { export const docsPath = ref(''); export async function openQuickEdit({ + doc, schemaName, name, hideFields = [], showFields = [], defaults = {}, }: QuickEditOptions) { + if (doc) { + schemaName = doc.schemaName; + name = doc.name; + } + + if (!doc && (!schemaName || !name)) { + throw new ValueError(t`Schema Name or Name not passed to Open Quick Edit`); + } + const currentRoute = router.currentRoute.value; const query = currentRoute.query; let method: 'push' | 'replace' = 'push'; @@ -74,6 +84,9 @@ export async function openQuickEdit({ }); } +// @ts-ignore +window.openqe = openQuickEdit; + export async function showMessageDialog({ message, detail, @@ -108,9 +121,6 @@ export async function showToast(options: ToastOptions) { replaceAndAppendMount(toast, 'toast-target'); } -// @ts-ignore -window.st = showToast; - function replaceAndAppendMount(app: App, replaceId: string) { const fragment = document.createDocumentFragment(); const target = document.getElementById(replaceId);