2
0
mirror of https://github.com/frappe/books.git synced 2024-12-24 11:55:46 +00:00
books/models/baseModels/Invoice/Invoice.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1261 lines
32 KiB
TypeScript
Raw Normal View History

2022-09-29 11:04:35 +00:00
import { Fyo } from 'fyo';
2023-05-04 10:45:12 +00:00
import { DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
2022-09-29 11:04:35 +00:00
import {
2022-11-22 09:12:49 +00:00
CurrenciesMap,
DefaultMap,
2022-09-29 11:04:35 +00:00
FiltersMap,
FormulaMap,
HiddenMap,
2022-09-29 11:04:35 +00:00
} from 'fyo/model/types';
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors';
2023-05-04 10:45:12 +00:00
import { Transactional } from 'models/Transactional/Transactional';
2024-01-30 12:55:50 +00:00
import {
addItem,
canApplyPricingRule,
filterPricingRules,
getExchangeRate,
getNumberSeries,
getPricingRulesConflicts,
2024-02-01 11:33:59 +00:00
roundFreeItemQty,
2024-01-30 12:55:50 +00:00
} from 'models/helpers';
2022-11-22 09:12:49 +00:00
import { StockTransfer } from 'models/inventory/StockTransfer';
2023-05-04 10:45:12 +00:00
import { validateBatch } from 'models/inventory/helpers';
import { ModelNameEnum } from 'models/types';
2022-05-23 05:30:54 +00:00
import { Money } from 'pesa';
2022-09-29 11:04:35 +00:00
import { FieldTypeEnum, Schema } from 'schemas/types';
import { getIsNullOrUndef, joinMapLists, safeParseFloat } from 'utils';
2022-10-12 09:29:43 +00:00
import { Defaults } from '../Defaults/Defaults';
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
2022-11-22 09:12:49 +00:00
import { Item } from '../Item/Item';
2022-04-14 08:01:33 +00:00
import { Party } from '../Party/Party';
import { Payment } from '../Payment/Payment';
import { Tax } from '../Tax/Tax';
import { TaxSummary } from '../TaxSummary/TaxSummary';
import { ReturnDocItem } from 'models/inventory/types';
import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types';
2024-01-30 12:55:50 +00:00
import { PricingRule } from '../PricingRule/PricingRule';
import { ApplicablePricingRules } from './types';
import { PricingRuleDetail } from '../PricingRuleDetail/PricingRuleDetail';
2022-04-14 08:01:33 +00:00
export type TaxDetail = {
account: string;
payment_account?: string;
rate: number;
};
export type InvoiceTaxItem = {
details: TaxDetail;
exchangeRate?: number;
fullAmount: Money;
taxAmount: Money;
};
export abstract class Invoice extends Transactional {
2022-04-14 08:01:33 +00:00
_taxes: Record<string, Tax> = {};
taxes?: TaxSummary[];
2022-04-14 08:01:33 +00:00
items?: InvoiceItem[];
party?: string;
account?: string;
currency?: string;
2023-06-06 08:59:08 +00:00
priceList?: string;
netTotal?: Money;
2022-05-17 12:12:57 +00:00
grandTotal?: Money;
baseGrandTotal?: Money;
2022-04-20 06:38:47 +00:00
outstandingAmount?: Money;
exchangeRate?: number;
setDiscountAmount?: boolean;
discountAmount?: Money;
discountPercent?: number;
discountAfterTax?: boolean;
2022-11-22 09:12:49 +00:00
stockNotTransferred?: number;
backReference?: string;
2022-04-20 06:38:47 +00:00
submitted?: boolean;
cancelled?: boolean;
makeAutoPayment?: boolean;
2023-06-06 07:44:26 +00:00
makeAutoStockTransfer?: boolean;
2022-04-20 06:38:47 +00:00
isReturned?: boolean;
returnAgainst?: string;
2024-01-30 12:55:50 +00:00
pricingRuleDetail?: PricingRuleDetail[];
get isSales() {
2023-12-22 08:50:35 +00:00
return (
this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote'
);
}
get isQuote() {
return this.schemaName == 'SalesQuote';
}
2022-04-14 08:01:33 +00:00
get enableDiscounting() {
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
}
get isMultiCurrency() {
if (!this.currency) {
return false;
}
return this.fyo.singles.SystemSettings!.currency !== this.currency;
}
get companyCurrency() {
return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY;
}
get stockTransferSchemaName() {
return this.isSales
? ModelNameEnum.Shipment
: ModelNameEnum.PurchaseReceipt;
}
get hasLinkedTransfers() {
if (!this.submitted) {
return false;
}
return this.getStockTransferred() > 0;
}
get hasLinkedPayments() {
if (!this.submitted) {
return false;
}
return !this.baseGrandTotal?.eq(this.outstandingAmount!);
}
get autoPaymentAccount(): string | null {
const fieldname = this.isSales
? 'salesPaymentAccount'
: 'purchasePaymentAccount';
const value = this.fyo.singles.Defaults?.[fieldname];
if (typeof value === 'string' && value.length) {
return value;
}
return null;
}
2023-06-06 07:44:26 +00:00
get autoStockTransferLocation(): string | null {
const fieldname = this.isSales
? 'shipmentLocation'
: 'purchaseReceiptLocation';
2023-06-06 07:44:26 +00:00
const value = this.fyo.singles.Defaults?.[fieldname];
if (typeof value === 'string' && value.length) {
return value;
}
return null;
}
get isReturn(): boolean {
return !!this.returnAgainst;
}
2022-09-29 11:04:35 +00:00
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
super(schema, data, fyo);
this._setGetCurrencies();
}
async validate() {
await super.validate();
if (
this.enableDiscounting &&
!this.fyo.singles?.AccountingSettings?.discountAccount
) {
throw new ValidationError(this.fyo.t`Discount Account is not set.`);
}
2023-05-04 10:45:12 +00:00
await validateBatch(this);
2024-02-01 11:33:59 +00:00
await this._validatePricingRule();
}
2022-04-14 08:01:33 +00:00
async afterSubmit() {
await super.afterSubmit();
2022-04-14 08:01:33 +00:00
2024-08-09 05:11:54 +00:00
if (this.schemaName === ModelNameEnum.SalesQuote) {
return;
}
2022-04-14 08:01:33 +00:00
// update outstanding amounts
await this.fyo.db.update(this.schemaName, {
2022-04-14 08:01:33 +00:00
name: this.name as string,
outstandingAmount: this.baseGrandTotal!,
2022-04-14 08:01:33 +00:00
});
const party = (await this.fyo.doc.getDoc('Party', this.party)) as Party;
2022-04-14 08:01:33 +00:00
await party.updateOutstandingAmount();
if (this.makeAutoPayment && this.autoPaymentAccount) {
const payment = this.getPayment();
await payment?.sync();
await payment?.submit();
await this.load();
}
2023-06-06 07:44:26 +00:00
if (this.makeAutoStockTransfer && this.autoStockTransferLocation) {
const stockTransfer = await this.getStockTransfer(true);
await stockTransfer?.sync();
await stockTransfer?.submit();
await this.load();
}
await this._updateIsItemsReturned();
2022-04-14 08:01:33 +00:00
}
async afterCancel() {
await super.afterCancel();
await this._cancelPayments();
await this._updatePartyOutStanding();
await this._updateIsItemsReturned();
}
async _cancelPayments() {
const paymentIds = await this.getPaymentIds();
for (const paymentId of paymentIds) {
const paymentDoc = (await this.fyo.doc.getDoc(
2022-04-14 08:01:33 +00:00
'Payment',
paymentId
2022-04-14 08:01:33 +00:00
)) as Payment;
await paymentDoc.cancel();
}
}
async _updatePartyOutStanding() {
const partyDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.Party,
this.party
)) as Party;
await partyDoc.updateOutstandingAmount();
}
async afterDelete() {
await super.afterDelete();
const paymentIds = await this.getPaymentIds();
for (const name of paymentIds) {
const paymentDoc = await this.fyo.doc.getDoc(ModelNameEnum.Payment, name);
await paymentDoc.delete();
}
}
async getPaymentIds() {
const payments = (await this.fyo.db.getAll('PaymentFor', {
fields: ['parent'],
filters: { referenceType: this.schemaName, referenceName: this.name! },
orderBy: 'name',
})) as { parent: string }[];
2022-04-14 08:01:33 +00:00
if (payments.length != 0) {
return [...new Set(payments.map(({ parent }) => parent))];
2022-04-14 08:01:33 +00:00
}
return [];
2022-04-14 08:01:33 +00:00
}
async getExchangeRate() {
if (!this.currency) {
return 1.0;
}
2022-04-14 08:01:33 +00:00
const currency = await this.fyo.getValue(
ModelNameEnum.SystemSettings,
'currency'
);
if (this.currency === currency) {
2022-04-14 08:01:33 +00:00
return 1.0;
}
const exchangeRate = await getExchangeRate({
fromCurrency: this.currency,
toCurrency: currency as string,
2022-04-14 08:01:33 +00:00
});
return safeParseFloat(exchangeRate.toFixed(2));
2022-04-14 08:01:33 +00:00
}
async getTaxItems(): Promise<InvoiceTaxItem[]> {
const taxItems: InvoiceTaxItem[] = [];
for (const item of this.items ?? []) {
if (!item.tax) {
2022-04-14 08:01:33 +00:00
continue;
}
const tax = await this.getTax(item.tax);
for (const details of (tax.details ?? []) as TaxDetail[]) {
let amount = item.amount!;
if (
this.enableDiscounting &&
!this.discountAfterTax &&
!item.itemDiscountedTotal?.isZero()
) {
amount = item.itemDiscountedTotal!;
}
const taxItem: InvoiceTaxItem = {
details,
exchangeRate: this.exchangeRate ?? 1,
fullAmount: amount,
taxAmount: amount.mul(details.rate / 100),
};
taxItems.push(taxItem);
2022-04-14 08:01:33 +00:00
}
}
return taxItems;
}
async getTaxSummary() {
const taxes: Record<
string,
{
account: string;
rate: number;
amount: Money;
}
> = {};
for (const { details, taxAmount } of await this.getTaxItems()) {
const account = details.account;
taxes[account] ??= {
account,
rate: details.rate,
amount: this.fyo.pesa(0),
};
taxes[account].amount = taxes[account].amount.add(taxAmount);
}
type Summary = typeof taxes[string] & { idx: number };
const taxArr: Summary[] = [];
let idx = 0;
for (const account in taxes) {
const tax = taxes[account];
if (tax.amount.isZero()) {
continue;
}
taxArr.push({
...tax,
idx,
});
idx += 1;
}
return taxArr;
2022-04-14 08:01:33 +00:00
}
async getTax(tax: string) {
if (!this._taxes[tax]) {
this._taxes[tax] = await this.fyo.doc.getDoc('Tax', tax);
2022-04-14 08:01:33 +00:00
}
return this._taxes[tax];
}
2022-07-15 07:52:19 +00:00
getTotalDiscount() {
if (!this.enableDiscounting) {
return this.fyo.pesa(0);
}
const itemDiscountAmount = this.getItemDiscountAmount();
const invoiceDiscountAmount = this.getInvoiceDiscountAmount();
return itemDiscountAmount.add(invoiceDiscountAmount);
}
getGrandTotal() {
2022-07-15 07:52:19 +00:00
const totalDiscount = this.getTotalDiscount();
2022-04-14 08:01:33 +00:00
return ((this.taxes ?? []) as Doc[])
.map((doc) => doc.amount as Money)
.reduce((a, b) => a.add(b), this.netTotal!)
.sub(totalDiscount);
}
getInvoiceDiscountAmount() {
if (!this.enableDiscounting) {
return this.fyo.pesa(0);
}
if (this.setDiscountAmount) {
return this.discountAmount ?? this.fyo.pesa(0);
}
let totalItemAmounts = this.fyo.pesa(0);
for (const item of this.items ?? []) {
if (this.discountAfterTax) {
totalItemAmounts = totalItemAmounts.add(item.itemTaxedTotal!);
} else {
totalItemAmounts = totalItemAmounts.add(item.itemDiscountedTotal!);
}
}
return totalItemAmounts.percent(this.discountPercent ?? 0);
}
getItemDiscountAmount() {
if (!this.enableDiscounting) {
return this.fyo.pesa(0);
}
if (!this?.items?.length) {
return this.fyo.pesa(0);
}
let discountAmount = this.fyo.pesa(0);
for (const item of this.items) {
if (item.setItemDiscountAmount) {
discountAmount = discountAmount.add(
item.itemDiscountAmount ?? this.fyo.pesa(0)
);
} else if (!this.discountAfterTax) {
discountAmount = discountAmount.add(
(item.amount ?? this.fyo.pesa(0)).mul(
(item.itemDiscountPercent ?? 0) / 100
)
);
} else if (this.discountAfterTax) {
discountAmount = discountAmount.add(
(item.itemTaxedTotal ?? this.fyo.pesa(0)).mul(
(item.itemDiscountPercent ?? 0) / 100
)
);
}
}
return discountAmount;
2022-04-14 08:01:33 +00:00
}
async getReturnDoc(): Promise<Invoice | undefined> {
if (!this.name) {
return;
}
const docData = this.getValidDict(true, true);
const docItems = docData.items as DocValueMap[];
if (!docItems) {
return;
}
let returnDocItems: DocValueMap[] = [];
const returnBalanceItemsQty = await this.fyo.db.getReturnBalanceItemsQty(
this.schemaName,
this.name
);
for (const item of docItems) {
if (!returnBalanceItemsQty) {
returnDocItems = docItems;
returnDocItems.map((row) => {
row.name = undefined;
(row.quantity as number) *= -1;
return row;
});
break;
}
const isItemExist = !!returnDocItems.filter(
(balanceItem) => balanceItem.item === item.item
).length;
if (isItemExist) {
continue;
}
const returnedItem: ReturnDocItem | undefined =
returnBalanceItemsQty[item.item as string];
let quantity = returnedItem.quantity;
let serialNumber: string | undefined =
returnedItem.serialNumbers?.join('\n');
if (
item.batch &&
returnedItem.batches &&
returnedItem.batches[item.batch as string]
) {
quantity = returnedItem.batches[item.batch as string].quantity;
if (returnedItem.batches[item.batch as string].serialNumbers) {
serialNumber =
returnedItem.batches[item.batch as string].serialNumbers?.join(
'\n'
);
}
}
returnDocItems.push({
...item,
serialNumber,
name: undefined,
quantity: quantity,
});
}
const returnDocData = {
...docData,
name: undefined,
date: new Date(),
items: returnDocItems,
returnAgainst: docData.name,
} as DocValueMap;
const newReturnDoc = this.fyo.doc.getNewDoc(
this.schema.name,
returnDocData
) as Invoice;
await newReturnDoc.runFormulas();
return newReturnDoc;
}
async _updateIsItemsReturned() {
if (!this.isReturn || !this.returnAgainst || this.isQuote) {
return;
}
const returnInvoices = await this.fyo.db.getAll(this.schema.name, {
filters: {
submitted: true,
cancelled: false,
returnAgainst: this.returnAgainst,
},
});
const isReturned = !!returnInvoices.length;
const invoiceDoc = await this.fyo.doc.getDoc(
this.schemaName,
this.returnAgainst
);
await invoiceDoc.setAndSync({ isReturned });
await invoiceDoc.submit();
}
async _validateHasLinkedReturnInvoices() {
if (!this.name || this.isReturn || this.isQuote) {
return;
}
const returnInvoices = await this.fyo.db.getAll(this.schemaName, {
filters: {
returnAgainst: this.name,
},
});
if (!returnInvoices.length) {
return;
}
const names = returnInvoices.map(({ name }) => name).join(', ');
throw new ValidationError(
this.fyo
.t`Cannot cancel ${this.name} because of the following ${this.schema.label}: ${names}`
);
}
formulas: FormulaMap = {
account: {
formula: async () => {
return (await this.fyo.getValue(
'Party',
this.party!,
'defaultAccount'
)) as string;
},
dependsOn: ['party'],
},
currency: {
formula: async () => {
const currency = (await this.fyo.getValue(
'Party',
this.party!,
'currency'
)) as string;
if (!getIsNullOrUndef(currency)) {
return currency;
}
return this.fyo.singles.SystemSettings!.currency as string;
},
dependsOn: ['party'],
},
exchangeRate: {
formula: async () => {
if (
this.currency ===
(this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY)
) {
return 1;
}
if (this.exchangeRate && this.exchangeRate !== 1) {
return this.exchangeRate;
}
return await this.getExchangeRate();
},
dependsOn: ['party', 'currency'],
},
netTotal: { formula: () => this.getSum('items', 'amount', false) },
taxes: { formula: async () => await this.getTaxSummary() },
grandTotal: { formula: () => this.getGrandTotal() },
baseGrandTotal: {
formula: () => (this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
dependsOn: ['grandTotal', 'exchangeRate'],
},
outstandingAmount: {
formula: () => {
if (this.submitted) {
return;
}
return this.baseGrandTotal!;
},
},
2022-11-22 09:12:49 +00:00
stockNotTransferred: {
formula: () => {
2022-11-22 09:12:49 +00:00
if (this.submitted) {
return;
}
return this.getStockNotTransferred();
},
dependsOn: ['items'],
},
makeAutoPayment: {
formula: () => !!this.autoPaymentAccount,
dependsOn: [],
},
makeAutoStockTransfer: {
formula: () =>
!!this.fyo.singles.AccountingSettings?.enableInventory &&
!!this.autoStockTransferLocation,
dependsOn: [],
},
2024-01-30 12:55:50 +00:00
isPricingRuleApplied: {
formula: async () => {
2024-02-01 11:33:59 +00:00
if (!this.fyo.singles.AccountingSettings?.enablePricingRule) {
return false;
}
2024-01-30 12:55:50 +00:00
const pricingRule = await this.getPricingRule();
2024-08-19 07:30:25 +00:00
if (!pricingRule) {
return false;
2024-01-30 12:55:50 +00:00
}
2024-08-19 07:30:25 +00:00
await this.appendPricingRuleDetail(pricingRule);
return !!pricingRule;
2024-01-30 12:55:50 +00:00
},
dependsOn: ['items'],
},
};
getStockTransferred() {
return (this.items ?? []).reduce(
(acc, item) =>
(item.quantity ?? 0) - (item.stockNotTransferred ?? 0) + acc,
0
);
}
getTotalQuantity() {
return (this.items ?? []).reduce(
(acc, item) => acc + (item.quantity ?? 0),
0
);
}
2022-11-22 09:12:49 +00:00
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 ?? []) {
itemDiscountedAmounts = itemDiscountedAmounts.add(
item.itemDiscountedTotal ?? item.amount!
);
}
return itemDiscountedAmounts;
}
hidden: HiddenMap = {
makeAutoPayment: () => {
if (this.submitted) {
return true;
}
2023-06-06 07:44:26 +00:00
return !this.autoPaymentAccount;
},
makeAutoStockTransfer: () => {
if (this.submitted) {
return true;
}
2023-06-06 07:44:26 +00:00
if (!this.fyo.singles.AccountingSettings?.enableInventory) {
return true;
}
return !this.autoStockTransferLocation;
},
setDiscountAmount: () => true || !this.enableDiscounting,
discountAmount: () =>
true || !(this.enableDiscounting && !!this.setDiscountAmount),
discountPercent: () =>
true || !(this.enableDiscounting && !this.setDiscountAmount),
discountAfterTax: () => !this.enableDiscounting,
taxes: () => !this.taxes?.length,
baseGrandTotal: () =>
this.exchangeRate === 1 || this.baseGrandTotal!.isZero(),
grandTotal: () => !this.taxes?.length,
stockNotTransferred: () => !this.stockNotTransferred,
outstandingAmount: () =>
!!this.outstandingAmount?.isZero() || !this.isSubmitted,
terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)),
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
backReference: () => !this.backReference,
quote: () => !this.quote,
priceList: () =>
!this.fyo.singles.AccountingSettings?.enablePriceList ||
(!this.canEdit && !this.priceList),
returnAgainst: () =>
(this.isSubmitted || this.isCancelled) && !this.returnAgainst,
pricingRuleDetail: () =>
!this.fyo.singles.AccountingSettings?.enablePricingRule ||
!this.pricingRuleDetail?.length,
};
2022-04-25 06:33:31 +00:00
static defaults: DefaultMap = {
makeAutoPayment: (doc) =>
doc instanceof Invoice && !!doc.autoPaymentAccount,
makeAutoStockTransfer: (doc) =>
!!doc.fyo.singles.AccountingSettings?.enableInventory &&
doc instanceof Invoice &&
!!doc.autoStockTransferLocation,
2022-10-12 09:29:43 +00:00
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
terms: (doc) => {
const defaults = doc.fyo.singles.Defaults;
2022-10-12 09:29:43 +00:00
if (doc.schemaName === ModelNameEnum.SalesInvoice) {
return defaults?.salesInvoiceTerms ?? '';
}
return defaults?.purchaseInvoiceTerms ?? '';
},
date: () => new Date(),
};
static filters: FiltersMap = {
party: (doc: Doc) => ({
role: ['in', [doc.isSales ? 'Customer' : 'Supplier', 'Both']],
}),
account: (doc: Doc) => ({
isGroup: false,
accountType: doc.isSales ? 'Receivable' : 'Payable',
}),
numberSeries: (doc: Doc) => ({ referenceType: doc.schemaName }),
2023-06-06 08:59:08 +00:00
priceList: (doc: Doc) => ({
2023-06-07 04:45:26 +00:00
isEnabled: true,
...(doc.isSales ? { isSales: true } : { isPurchase: true }),
2023-06-06 08:59:08 +00:00
}),
};
2022-05-18 16:55:24 +00:00
static createFilters: FiltersMap = {
party: (doc: Doc) => ({
role: doc.isSales ? 'Customer' : 'Supplier',
}),
};
2022-09-29 11:04:35 +00:00
getCurrencies: CurrenciesMap = {
baseGrandTotal: () => this.companyCurrency,
outstandingAmount: () => this.companyCurrency,
};
2022-09-29 11:04:35 +00:00
_getCurrency() {
if (this.exchangeRate === 1) {
return this.companyCurrency;
2022-09-29 11:04:35 +00:00
}
return this.currency ?? DEFAULT_CURRENCY;
}
_setGetCurrencies() {
const currencyFields = this.schema.fields.filter(
({ fieldtype }) => fieldtype === FieldTypeEnum.Currency
);
for (const { fieldname } of currencyFields) {
this.getCurrencies[fieldname] ??= this._getCurrency.bind(this);
2022-09-29 11:04:35 +00:00
}
}
2022-11-22 09:12:49 +00:00
getPayment(): Payment | null {
if (!this.isSubmitted) {
return null;
}
const outstandingAmount = this.outstandingAmount;
if (!outstandingAmount) {
return null;
}
if (this.outstandingAmount?.isZero()) {
return null;
}
let accountField: AccountFieldEnum = AccountFieldEnum.Account;
let paymentType: PaymentTypeEnum = PaymentTypeEnum.Receive;
if (this.isSales && this.isReturn) {
accountField = AccountFieldEnum.PaymentAccount;
paymentType = PaymentTypeEnum.Pay;
}
if (!this.isSales) {
accountField = AccountFieldEnum.PaymentAccount;
paymentType = PaymentTypeEnum.Pay;
if (this.isReturn) {
accountField = AccountFieldEnum.Account;
paymentType = PaymentTypeEnum.Receive;
}
}
2022-11-22 09:12:49 +00:00
const data = {
party: this.party,
date: new Date().toISOString().slice(0, 10),
paymentType,
amount: this.outstandingAmount?.abs(),
2022-11-22 09:12:49 +00:00
[accountField]: this.account,
for: [
{
referenceType: this.schemaName,
referenceName: this.name,
amount: this.outstandingAmount,
},
],
};
if (this.makeAutoPayment && this.autoPaymentAccount) {
const autoPaymentAccount = this.isSales ? 'paymentAccount' : 'account';
data[autoPaymentAccount] = this.autoPaymentAccount;
}
2022-11-22 09:12:49 +00:00
return this.fyo.doc.getNewDoc(ModelNameEnum.Payment, data) as Payment;
}
async getStockTransfer(isAuto = false): Promise<StockTransfer | null> {
2022-11-22 09:12:49 +00:00
if (!this.isSubmitted) {
return null;
}
if (!this.stockNotTransferred) {
return null;
}
const schemaName = this.stockTransferSchemaName;
2022-11-22 09:12:49 +00:00
const defaults = (this.fyo.singles.Defaults as Defaults) ?? {};
let terms;
let numberSeries;
2022-11-22 09:12:49 +00:00
if (this.isSales) {
terms = defaults.shipmentTerms ?? '';
numberSeries = defaults.shipmentNumberSeries ?? undefined;
2022-11-22 09:12:49 +00:00
} else {
terms = defaults.purchaseReceiptTerms ?? '';
numberSeries = defaults.purchaseReceiptNumberSeries ?? undefined;
2022-11-22 09:12:49 +00:00
}
const data = {
party: this.party,
date: new Date().toISOString(),
terms,
numberSeries,
2022-11-22 09:12:49 +00:00
backReference: this.name,
};
2023-06-06 07:44:26 +00:00
let location = this.autoStockTransferLocation;
if (!location) {
location = this.fyo.singles.InventorySettings?.defaultLocation ?? null;
}
if (isAuto && !location) {
return null;
}
2022-11-22 09:12:49 +00:00
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;
2023-06-06 07:44:26 +00:00
if (isAuto && (itemDoc.hasBatch || itemDoc.hasSerialNumber)) {
continue;
}
2022-11-22 09:12:49 +00:00
const item = row.item;
const quantity = row.stockNotTransferred;
const trackItem = itemDoc.trackItem;
2023-02-28 06:01:04 +00:00
const batch = row.batch || null;
const description = row.description;
const hsnCode = row.hsnCode;
let rate = row.rate as Money;
if (this.exchangeRate && this.exchangeRate > 1) {
rate = rate.mul(this.exchangeRate);
}
2022-11-22 09:12:49 +00:00
if (!quantity || !trackItem) {
continue;
}
2023-06-06 07:44:26 +00:00
if (isAuto) {
const stock =
(await this.fyo.db.getStockQuantity(
item,
location!,
undefined,
data.date
)) ?? 0;
if (stock < quantity) {
2023-06-06 07:44:26 +00:00
continue;
}
}
2022-11-22 09:12:49 +00:00
await transfer.append('items', {
item,
quantity,
location,
rate,
2023-02-28 06:01:04 +00:00
batch,
description,
hsnCode,
2022-11-22 09:12:49 +00:00
});
}
if (!transfer.items?.length) {
return null;
}
return transfer;
}
2024-01-30 12:55:50 +00:00
async beforeSync(): Promise<void> {
await super.beforeSync();
if (this.pricingRuleDetail?.length) {
await this.applyProductDiscount();
2024-08-19 07:30:25 +00:00
} else {
this.clearFreeItems();
2024-01-30 12:55:50 +00:00
}
}
async beforeCancel(): Promise<void> {
await super.beforeCancel();
await this._validateStockTransferCancelled();
await this._validateHasLinkedReturnInvoices();
}
async beforeDelete(): Promise<void> {
await super.beforeCancel();
await this._validateStockTransferCancelled();
await this._deleteCancelledStockTransfers();
}
async _deleteCancelledStockTransfers() {
const schemaName = this.stockTransferSchemaName;
const transfers = await this._getLinkedStockTransferNames(true);
for (const { name } of transfers) {
const st = await this.fyo.doc.getDoc(schemaName, name);
await st.delete();
}
}
async _validateStockTransferCancelled() {
const schemaName = this.stockTransferSchemaName;
const transfers = await this._getLinkedStockTransferNames(false);
if (!transfers?.length) {
return;
}
const names = transfers.map(({ name }) => name).join(', ');
const label = this.fyo.schemaMap[schemaName]?.label ?? schemaName;
throw new ValidationError(
this.fyo.t`Cannot cancel ${this.schema.label} ${this
.name!} because of the following ${label}: ${names}`
);
}
async _getLinkedStockTransferNames(cancelled: boolean) {
const name = this.name;
if (!name) {
throw new ValidationError(`Name not found for ${this.schema.label}`);
}
const schemaName = this.stockTransferSchemaName;
const transfers = (await this.fyo.db.getAllRaw(schemaName, {
fields: ['name'],
filters: { backReference: this.name!, cancelled },
})) as { name: string }[];
return transfers;
}
async getLinkedPayments() {
if (!this.hasLinkedPayments) {
return [];
}
const paymentFors = (await this.fyo.db.getAllRaw('PaymentFor', {
fields: ['parent', 'amount'],
filters: { referenceName: this.name!, referenceType: this.schemaName },
})) as { parent: string; amount: string }[];
const payments = (await this.fyo.db.getAllRaw('Payment', {
fields: ['name', 'date', 'submitted', 'cancelled'],
filters: { name: ['in', paymentFors.map((p) => p.parent)] },
})) as {
name: string;
date: string;
submitted: number;
cancelled: number;
}[];
return joinMapLists(payments, paymentFors, 'name', 'parent')
.map((j) => ({
name: j.name,
date: new Date(j.date),
submitted: !!j.submitted,
cancelled: !!j.cancelled,
amount: this.fyo.pesa(j.amount),
}))
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
}
async getLinkedStockTransfers() {
if (!this.hasLinkedTransfers) {
return [];
}
const schemaName = this.stockTransferSchemaName;
const transfers = (await this.fyo.db.getAllRaw(schemaName, {
fields: ['name', 'date', 'submitted', 'cancelled'],
filters: { backReference: this.name! },
})) as {
name: string;
date: string;
submitted: number;
cancelled: number;
}[];
const itemSchemaName = schemaName + 'Item';
const transferItems = (await this.fyo.db.getAllRaw(itemSchemaName, {
fields: ['parent', 'quantity', 'location', 'amount'],
filters: {
parent: ['in', transfers.map((t) => t.name)],
item: ['in', this.items!.map((i) => i.item!)],
},
})) as {
parent: string;
quantity: number;
location: string;
amount: string;
}[];
return joinMapLists(transfers, transferItems, 'name', 'parent')
.map((j) => ({
name: j.name,
date: new Date(j.date),
submitted: !!j.submitted,
cancelled: !!j.cancelled,
amount: this.fyo.pesa(j.amount),
location: j.location,
quantity: j.quantity,
}))
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
}
async addItem(name: string) {
return await addItem(name, this);
}
2024-01-30 12:55:50 +00:00
2024-02-01 11:33:59 +00:00
async appendPricingRuleDetail(
applicablePricingRule: ApplicablePricingRules[]
) {
await this.set('pricingRuleDetail', null);
for (const doc of applicablePricingRule) {
await this.append('pricingRuleDetail', {
referenceName: doc.pricingRule.name,
referenceItem: doc.applyOnItem,
});
}
}
2024-08-19 07:30:25 +00:00
clearFreeItems() {
if (this.pricingRuleDetail?.length || !this.items) {
2024-02-01 11:33:59 +00:00
return;
}
for (const item of this.items) {
if (item.isFreeItem) {
2024-08-19 07:30:25 +00:00
this.items = this.items?.filter(
(invoiceItem) => invoiceItem.name !== item.name
);
2024-02-01 11:33:59 +00:00
}
2024-08-19 07:30:25 +00:00
}
}
2024-02-01 11:33:59 +00:00
2024-08-19 07:30:25 +00:00
async applyProductDiscount() {
if (!this.items) {
return;
}
this.items = this.items.filter((item) => !item.isFreeItem);
for (const item of this.items) {
const pricingRuleDetailForItem = this.pricingRuleDetail?.filter(
2024-02-01 11:33:59 +00:00
(doc) => doc.referenceItem === item.item
);
if (!pricingRuleDetailForItem?.length) {
2024-09-02 06:16:15 +00:00
continue;
2024-08-19 07:30:25 +00:00
}
2024-02-01 11:33:59 +00:00
const pricingRuleDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.PricingRule,
pricingRuleDetailForItem[0].referenceName
)) as PricingRule;
if (pricingRuleDoc.discountType === 'Price Discount') {
continue;
}
const appliedItems = pricingRuleDoc.appliedItems?.map((doc) => doc.item);
if (!appliedItems?.includes(item.item)) {
continue;
}
const canApplyPRLOnItem = canApplyPricingRule(
pricingRuleDoc,
this.date as Date,
item.quantity as number,
item.amount as Money
);
if (!canApplyPRLOnItem) {
continue;
}
let freeItemQty = pricingRuleDoc.freeItemQuantity as number;
if (pricingRuleDoc.isRecursive) {
freeItemQty =
(item.quantity as number) / (pricingRuleDoc.recurseEvery as number);
}
if (pricingRuleDoc.roundFreeItemQty) {
freeItemQty = roundFreeItemQty(
freeItemQty,
pricingRuleDoc.roundingMethod as 'round' | 'floor' | 'ceil'
);
}
await this.append('items', {
item: pricingRuleDoc.freeItem as string,
quantity: freeItemQty,
isFreeItem: true,
pricingRule: pricingRuleDoc.title,
2024-02-01 11:33:59 +00:00
rate: pricingRuleDoc.freeItemRate,
unit: pricingRuleDoc.freeItemUnit,
});
}
}
2024-01-30 12:55:50 +00:00
async getPricingRule(): Promise<ApplicablePricingRules[] | undefined> {
if (!this.isSales || !this.items) {
return;
}
const pricingRules: ApplicablePricingRules[] = [];
for (const item of this.items) {
if (item.isFreeItem) {
continue;
}
const pricingRuleDocNames = (
await this.fyo.db.getAll(ModelNameEnum.PricingRuleItem, {
fields: ['parent'],
filters: {
item: item.item as string,
unit: item.unit as string,
},
})
).map((doc) => doc.parent) as string[];
const pricingRuleDocsForItem = (await this.fyo.db.getAll(
ModelNameEnum.PricingRule,
{
fields: ['*'],
filters: {
name: ['in', pricingRuleDocNames],
isEnabled: true,
},
orderBy: 'priority',
order: 'desc',
}
)) as PricingRule[];
const filtered = filterPricingRules(
pricingRuleDocsForItem,
this.date as Date,
item.quantity as number,
item.amount as Money
);
if (!filtered.length) {
continue;
}
2024-09-09 04:08:02 +00:00
for (const filteredDoc of filtered) {
const isPricingRuleHasConflicts = await getPricingRulesConflicts(
filteredDoc,
filteredDoc.priority as number
);
2024-01-30 12:55:50 +00:00
2024-09-09 04:08:02 +00:00
if (isPricingRuleHasConflicts) {
continue;
}
2024-01-30 12:55:50 +00:00
2024-09-09 04:08:02 +00:00
pricingRules.push({
applyOnItem: item.item as string,
pricingRule: filtered[0],
});
}
2024-01-30 12:55:50 +00:00
}
return pricingRules;
}
2024-02-01 11:33:59 +00:00
async _validatePricingRule() {
if (!this.fyo.singles.AccountingSettings?.enablePricingRule) {
2024-01-30 12:55:50 +00:00
return;
}
2024-02-01 11:33:59 +00:00
if (!this.items) {
2024-01-30 12:55:50 +00:00
return;
}
2024-02-01 11:33:59 +00:00
await this.getPricingRule();
2024-01-30 12:55:50 +00:00
}
2022-04-14 08:01:33 +00:00
}