Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: lead #929

Merged
merged 4 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
>
> Frappe Books is looking for a maintainer, please view [#775](https://github.com/frappe/books/issues/775) for more info.


<div align="center" markdown="1">

<img src="https://user-images.githubusercontent.com/29507195/207267672-d422db6c-d89a-4bbe-9822-468a55c15053.png" alt="Frappe Books logo" width="384"/>
Expand Down
10 changes: 10 additions & 0 deletions fyo/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,13 @@ export type DocStatus =
| 'NotSaved'
| 'Submitted'
| 'Cancelled';

export type LeadStatus =
| ''
| 'Open'
| 'Replied'
| 'Interested'
| 'Opportunity'
| 'Converted'
| 'Quotation'
| 'DonotContact'
4 changes: 4 additions & 0 deletions models/baseModels/AccountingSettings/AccountingSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class AccountingSettings extends Doc {
enableDiscounting?: boolean;
enableInventory?: boolean;
enablePriceList?: boolean;
enableLead?: boolean;
enableFormCustomization?: boolean;
enableInvoiceReturns?: boolean;

Expand Down Expand Up @@ -48,6 +49,9 @@ export class AccountingSettings extends Doc {
enableInventory: () => {
return !!this.enableInventory;
},
enableLead: () => {
return !!this.enableLead;
},
enableInvoiceReturns: () => {
return !!this.enableInvoiceReturns;
},
Expand Down
4 changes: 4 additions & 0 deletions models/baseModels/Invoice/Invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ export abstract class Invoice extends Transactional {
async afterSubmit() {
await super.afterSubmit();

if (this.schemaName === ModelNameEnum.SalesQuote) {
return;
}

// update outstanding amounts
await this.fyo.db.update(this.schemaName, {
name: this.name as string,
Expand Down
51 changes: 51 additions & 0 deletions models/baseModels/Lead/Lead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Fyo } from 'fyo';
import { Doc } from 'fyo/model/doc';
import {
Action,
LeadStatus,
ListViewSettings,
ValidationMap,
} from 'fyo/model/types';
import { getLeadActions, getLeadStatusColumn } from 'models/helpers';
import {
validateEmail,
validatePhoneNumber,
} from 'fyo/model/validationFunction';
import { ModelNameEnum } from 'models/types';

export class Lead extends Doc {
status?: LeadStatus;

validations: ValidationMap = {
email: validateEmail,
mobile: validatePhoneNumber,
};

createCustomer() {
return this.fyo.doc.getNewDoc(ModelNameEnum.Party, {
...this.getValidDict(),
fromLead: this.name,
phone: this.mobile as string,
role: 'Customer',
});
}

createSalesQuote() {
const data: { party: string | undefined; referenceType: string } = {
party: this.name,
referenceType: ModelNameEnum.Lead,
};

return this.fyo.doc.getNewDoc(ModelNameEnum.SalesQuote, data);
}

static getActions(fyo: Fyo): Action[] {
return getLeadActions(fyo);
}

static getListViewSettings(): ListViewSettings {
return {
columns: ['name', getLeadStatusColumn(), 'email', 'mobile'],
};
}
}
22 changes: 22 additions & 0 deletions models/baseModels/Party/Party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import {
} from 'fyo/model/validationFunction';
import { Money } from 'pesa';
import { PartyRole } from './types';
import { ModelNameEnum } from 'models/types';

export class Party extends Doc {
role?: PartyRole;
party?: string;
fromLead?: string;
defaultAccount?: string;
outstandingAmount?: Money;
async updateOutstandingAmount() {
Expand Down Expand Up @@ -125,6 +128,25 @@ export class Party extends Doc {
};
}

async afterDelete() {
await super.afterDelete();
if (!this.fromLead) {
return;
}
const leadData = await this.fyo.doc.getDoc(ModelNameEnum.Lead, this.name);
await leadData.setAndSync('status', 'Interested');
}

async afterSync() {
await super.afterSync();
if (!this.fromLead) {
return;
}

const leadData = await this.fyo.doc.getDoc(ModelNameEnum.Lead, this.name);
await leadData.setAndSync('status', 'Converted');
}

static getActions(fyo: Fyo): Action[] {
return [
{
Expand Down
24 changes: 23 additions & 1 deletion models/baseModels/SalesQuote/SalesQuote.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Fyo } from 'fyo';
import { DocValueMap } from 'fyo/core/types';
import { Action, ListViewSettings } from 'fyo/model/types';
import { Action, FiltersMap, ListViewSettings } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
import { getQuoteActions, getTransactionStatusColumn } from '../../helpers';
import { Invoice } from '../Invoice/Invoice';
import { SalesQuoteItem } from '../SalesQuoteItem/SalesQuoteItem';
import { Defaults } from '../Defaults/Defaults';
import { Doc } from 'fyo/model/doc';
import { Party } from '../Party/Party';

export class SalesQuote extends Invoice {
items?: SalesQuoteItem[];
party?: string;
name?: string;
referenceType?:
| ModelNameEnum.SalesInvoice
| ModelNameEnum.PurchaseInvoice
| ModelNameEnum.Lead;

// This is an inherited method and it must keep the async from the parent
// class
Expand Down Expand Up @@ -48,6 +56,20 @@ export class SalesQuote extends Invoice {
return invoice;
}

static filters: FiltersMap = {
numberSeries: (doc: Doc) => ({ referenceType: doc.schemaName }),
};

async afterSubmit(): Promise<void> {
await super.afterSubmit();

if (this.referenceType == ModelNameEnum.Lead) {
const partyDoc = (await this.loadAndGetLink('party')) as Party;

await partyDoc.setAndSync('status', 'Quotation');
}
}

static getListViewSettings(): ListViewSettings {
return {
columns: [
Expand Down
121 changes: 121 additions & 0 deletions models/baseModels/tests/testLead.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import test from 'tape';
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
import { ModelNameEnum } from 'models/types';
import { Lead } from '../Lead/Lead';
import { Party } from '../Party/Party';

const fyo = getTestFyo();
setupTestFyo(fyo, __filename);

const leadData = {
name: 'name2',
status: 'Open',
email: '[email protected]',
mobile: '1234567890',
};

const itemData: { name: string; rate: number } = {
name: 'Pen',
rate: 100,
};

test('create test docs for Lead', async (t) => {
await fyo.doc.getNewDoc(ModelNameEnum.Item, itemData).sync();

t.ok(
fyo.db.exists(ModelNameEnum.Item, itemData.name),
`dummy item ${itemData.name} exists`
);
});

test('create a Lead doc', async (t) => {
await fyo.doc.getNewDoc(ModelNameEnum.Lead, leadData).sync();

t.ok(
fyo.db.exists(ModelNameEnum.Lead, leadData.name),
`${leadData.name} exists`
);
});

test('create Customer from Lead', async (t) => {
const leadDoc = (await fyo.doc.getDoc(ModelNameEnum.Lead, 'name2')) as Lead;

const newCustomer = leadDoc.createCustomer();

t.equals(
leadDoc.status,
'Open',
'status must be Open before Customer is created'
);

await newCustomer.sync();

t.equals(
leadDoc.status,
'Converted',
'status should change to Converted after Customer is created'
);

t.ok(
await fyo.db.exists(ModelNameEnum.Party, newCustomer.name),
'Customer created from Lead'
);
});

test('create SalesQuote', async (t) => {
const leadDoc = (await fyo.doc.getDoc(ModelNameEnum.Lead, 'name2')) as Lead;

const newSalesQuote = leadDoc.createSalesQuote();

newSalesQuote.items = [];
newSalesQuote.append('items', {
item: itemData.name,
quantity: 1,
rate: itemData.rate,
});

t.equals(
leadDoc.status,
'Converted',
'status must be Open before SQUOT is created'
);

await newSalesQuote.sync();
await newSalesQuote.submit();

t.equals(
leadDoc.status,
'Quotation',
'status should change to Quotation after SQUOT submission'
);

t.ok(
await fyo.db.exists(ModelNameEnum.SalesQuote, newSalesQuote.name),
'SalesQuote Created from Lead'
);
});

test('delete Customer then lead status changes to Interested', async (t) => {
const partyDoc = (await fyo.doc.getDoc(
ModelNameEnum.Party,
'name2'
)) as Party;

await partyDoc.delete();

t.equals(
await fyo.db.exists(ModelNameEnum.Party, 'name2'),
false,
'Customer deleted'
);

const leadDoc = (await fyo.doc.getDoc(ModelNameEnum.Lead, 'name2')) as Lead;

t.equals(
leadDoc.status,
'Interested',
'status should change to Interested after Customer is deleted'
);
});

closeTestFyo(fyo, __filename);
Loading
Loading