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

feat: schema changes for invoice return

This commit is contained in:
akshayitzme 2023-09-23 16:42:21 +05:30
parent 3199121432
commit 8cac1ef45f
4 changed files with 196 additions and 7 deletions

View File

@ -16,6 +16,7 @@ export class AccountingSettings extends Doc {
enableInventory?: boolean;
enablePriceList?: boolean;
enableFormCustomization?: boolean;
enableInvoiceReturns?: boolean;
static filters: FiltersMap = {
writeOffAccount: () => ({
@ -47,6 +48,9 @@ export class AccountingSettings extends Doc {
enableInventory: () => {
return !!this.enableInventory;
},
enableInvoiceReturns: () => {
return !!this.enableInvoiceReturns;
},
};
override hidden: HiddenMap = {

View File

@ -25,6 +25,8 @@ 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';
export abstract class Invoice extends Transactional {
_taxes: Record<string, Tax> = {};
@ -52,6 +54,9 @@ export abstract class Invoice extends Transactional {
makeAutoPayment?: boolean;
makeAutoStockTransfer?: boolean;
isReturned?: boolean;
returnAgainst?: string;
get isSales() {
return this.schemaName === 'SalesInvoice';
}
@ -118,6 +123,10 @@ export abstract class Invoice extends Transactional {
return null;
}
get isReturn(): boolean {
return !!this.returnAgainst;
}
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
super(schema, data, fyo);
this._setGetCurrencies();
@ -159,12 +168,15 @@ export abstract class Invoice extends Transactional {
await stockTransfer?.submit();
await this.load();
}
await this._updateIsItemsReturned();
}
async afterCancel() {
await super.afterCancel();
await this._cancelPayments();
await this._updatePartyOutStanding();
await this._updateIsItemsReturned();
}
async _cancelPayments() {
@ -368,6 +380,134 @@ export abstract class Invoice extends Transactional {
return discountAmount;
}
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) {
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) {
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 () => {
@ -518,6 +658,8 @@ export abstract class Invoice extends Transactional {
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
backReference: () => !this.backReference,
priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList,
returnAgainst: () =>
(this.isSubmitted || this.isCancelled) && !this.returnAgainst,
};
static defaults: DefaultMap = {
@ -595,12 +737,29 @@ export abstract class Invoice extends Transactional {
return null;
}
const accountField = this.isSales ? 'account' : 'paymentAccount';
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;
}
}
const data = {
party: this.party,
date: new Date().toISOString().slice(0, 10),
paymentType: this.isSales ? 'Receive' : 'Pay',
amount: this.outstandingAmount,
paymentType,
amount: this.outstandingAmount?.abs(),
[accountField]: this.account,
for: [
{
@ -720,6 +879,7 @@ export abstract class Invoice extends Transactional {
async beforeCancel(): Promise<void> {
await super.beforeCancel();
await this._validateStockTransferCancelled();
await this._validateHasLinkedReturnInvoices();
}
async beforeDelete(): Promise<void> {

View File

@ -300,7 +300,7 @@ export class Payment extends Transactional {
)) as Invoice;
outstandingAmount = outstandingAmount.add(
referenceDoc.outstandingAmount ?? 0
referenceDoc.outstandingAmount?.abs() ?? 0
);
}
@ -438,6 +438,15 @@ export class Payment extends Transactional {
'referenceName'
)) as Invoice | null;
if (
refDoc &&
refDoc.schema.name === ModelNameEnum.SalesInvoice &&
refDoc.isReturned
) {
const accountsMap = await this._getAccountsMap();
return accountsMap[AccountTypeEnum.Cash]?.[0];
}
return refDoc?.account ?? null;
}
@ -534,7 +543,7 @@ export class Payment extends Transactional {
return;
}
const amount = this.getSum('for', 'amount', false);
const amount = (this.getSum('for', 'amount', false) as Money).abs();
if ((value as Money).gt(amount)) {
throw new ValidationError(

View File

@ -12,14 +12,26 @@ export class SalesInvoice extends Invoice {
async getPosting() {
const exchangeRate = this.exchangeRate ?? 1;
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
await posting.debit(this.account!, this.baseGrandTotal!);
if (this.isReturn) {
await posting.credit(this.account!, this.baseGrandTotal!);
} else {
await posting.debit(this.account!, this.baseGrandTotal!);
}
for (const item of this.items!) {
if (this.isReturn) {
await posting.debit(item.account!, item.amount!.mul(exchangeRate));
continue;
}
await posting.credit(item.account!, item.amount!.mul(exchangeRate));
}
if (this.taxes) {
for (const tax of this.taxes) {
if (this.isReturn) {
await posting.debit(tax.account!, tax.amount!.mul(exchangeRate));
continue;
}
await posting.credit(tax.account!, tax.amount!.mul(exchangeRate));
}
}
@ -28,7 +40,11 @@ export class SalesInvoice extends Invoice {
const discountAccount = this.fyo.singles.AccountingSettings
?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) {
await posting.debit(discountAccount, discountAmount.mul(exchangeRate));
if (this.isReturn) {
await posting.credit(discountAccount, discountAmount.mul(exchangeRate));
} else {
await posting.debit(discountAccount, discountAmount.mul(exchangeRate));
}
}
await posting.makeRoundOffEntry();