2
0
mirror of https://github.com/frappe/books.git synced 2025-01-03 15:17:30 +00:00

incr: add make stock transfer

This commit is contained in:
18alantom 2022-11-22 14:42:49 +05:30
parent f9a95a7c9b
commit ab2d2f8975
15 changed files with 346 additions and 72 deletions

View File

@ -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<StockTransfer | null> {
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;
}
}

View File

@ -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 = {

View File

@ -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}`,

View File

@ -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}`,

View File

@ -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 [
{
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 function makePayment(doc: Doc) {
const payment = fyo.doc.getNewDoc('Payment');
action: async (doc: Doc) => {
const payment = (doc as Invoice).getPayment();
if (!payment) {
return;
}
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,
doc: payment,
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,
},
],
},
});
},
},
getLedgerLinkAction(fyo),
];
};
}
export function getLedgerLinkAction(
fyo: Fyo,
isStock: boolean = false
): Action {
let label = fyo.t`Ledger Entries`;
let reportClassName = 'GeneralLedger';

View File

@ -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<void> {
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<string, number>);
}
}

View File

@ -131,6 +131,12 @@
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment"
},
{
"fieldname": "stockNotTransferred",
"label": "Stock Not Transferred",
"fieldtype": "Float",
"readOnly": true
}
],
"keywordFields": ["name", "party"]

View File

@ -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"],

View File

@ -21,6 +21,13 @@
"create": true,
"required": true,
"default": "PREC-"
},
{
"fieldname": "backReference",
"label": "Back Reference",
"fieldtype": "Link",
"target": "PurchaseInvoice",
"readOnly": true
}
],
"keywordFields": ["name", "party"]

View File

@ -21,6 +21,13 @@
"create": true,
"required": true,
"default": "SHPM-"
},
{
"fieldname": "backReference",
"label": "Back Reference",
"fieldtype": "Link",
"target": "SalesInvoice",
"readOnly": true
}
],
"keywordFields": ["name", "party"]

View File

@ -16,7 +16,7 @@
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"fieldtype": "Datetime",
"required": true
},
{

View File

@ -55,6 +55,13 @@
@change="(value) => doc.set('numberSeries', value)"
:read-only="!doc.notInserted || doc?.submitted"
/>
<FormControl
v-if="doc.backReference"
:border="true"
:df="getField('backReference')"
:value="doc.backReference"
:read-only="true"
/>
<FormControl
v-if="doc.attachment || !(doc.isSubmitted || doc.isCancelled)"
:border="true"

View File

@ -289,10 +289,10 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import {
docsPath,
getActionsForDocument,
routeTo,
showMessageDialog
docsPath,
getActionsForDocument,
routeTo,
showMessageDialog,
} from 'src/utils/ui';
import { nextTick } from 'vue';
import { handleErrorWithDialog } from '../errorHandling';
@ -425,6 +425,15 @@ export default {
}
this.quickEditDoc = doc;
if (
doc?.schemaName?.includes('InvoiceItem') &&
doc?.stockNotTransferred
) {
fields = [...doc.schema.quickEditFields, 'stockNotTransferred'].map(
(f) => fyo.getField(doc.schemaName, f)
);
}
this.quickEditFields = fields;
},
actions() {

View File

@ -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<string, unknown>;

View File

@ -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<Element>, replaceId: string) {
const fragment = document.createDocumentFragment();
const target = document.getElementById(replaceId);