mirror of
https://github.com/frappe/books.git
synced 2025-01-24 15:48:25 +00:00
feat: erpnext sync
This commit is contained in:
parent
d304e9cd38
commit
603f2d972f
@ -44,6 +44,8 @@ import {
|
|||||||
ValidationMap,
|
ValidationMap,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { validateOptions, validateRequired } from './validationFunction';
|
import { validateOptions, validateRequired } from './validationFunction';
|
||||||
|
import { getShouldDocSyncToERPNext } from 'src/utils/erpnextSync';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
|
||||||
export class Doc extends Observable<DocValue | Doc[]> {
|
export class Doc extends Observable<DocValue | Doc[]> {
|
||||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
@ -247,6 +249,22 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get shouldDocSyncToERPNext() {
|
||||||
|
const syncEnabled = !!this.fyo.singles.ERPNextSyncSettings?.isEnabled;
|
||||||
|
if (!syncEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.schemaName || !this.fyo.singles.ERPNextSyncSettings) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getShouldDocSyncToERPNext(
|
||||||
|
this.fyo.singles.ERPNextSyncSettings,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_setValuesWithoutChecks(data: DocValueMap, convertToDocValue: boolean) {
|
_setValuesWithoutChecks(data: DocValueMap, convertToDocValue: boolean) {
|
||||||
for (const field of this.schema.fields) {
|
for (const field of this.schema.fields) {
|
||||||
const { fieldname, fieldtype } = field;
|
const { fieldname, fieldtype } = field;
|
||||||
@ -912,6 +930,28 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
this._notInserted = false;
|
this._notInserted = false;
|
||||||
await this.trigger('afterSync');
|
await this.trigger('afterSync');
|
||||||
this.fyo.doc.observer.trigger(`sync:${this.schemaName}`, this.name);
|
this.fyo.doc.observer.trigger(`sync:${this.schemaName}`, this.name);
|
||||||
|
|
||||||
|
if (this._addDocToSyncQueue && !!this.shouldDocSyncToERPNext) {
|
||||||
|
const isDocExistsInQueue = await this.fyo.db.getAll(
|
||||||
|
ModelNameEnum.ERPNextSyncQueue,
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
referenceType: this.schemaName,
|
||||||
|
documentName: this.name as string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDocExistsInQueue.length) {
|
||||||
|
this.fyo.doc
|
||||||
|
.getNewDoc(ModelNameEnum.ERPNextSyncQueue, {
|
||||||
|
referenceType: this.schemaName,
|
||||||
|
documentName: this.name,
|
||||||
|
})
|
||||||
|
.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._syncing = false;
|
this._syncing = false;
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
10
main/api.ts
Normal file
10
main/api.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import fetch, { RequestInit } from 'node-fetch';
|
||||||
|
|
||||||
|
export async function sendAPIRequest(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit | undefined
|
||||||
|
) {
|
||||||
|
return (await fetch(endpoint, options)).json() as unknown as {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
}[];
|
||||||
|
}
|
@ -180,6 +180,18 @@ const ipc = {
|
|||||||
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, body);
|
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, body);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async sendAPIRequest(endpoint: string, options: RequestInit | undefined) {
|
||||||
|
return (await ipcRenderer.invoke(
|
||||||
|
IPC_ACTIONS.SEND_API_REQUEST,
|
||||||
|
endpoint,
|
||||||
|
options
|
||||||
|
)) as Promise<
|
||||||
|
{
|
||||||
|
[key: string]: string | number | boolean | Date | object | object[];
|
||||||
|
}[]
|
||||||
|
>;
|
||||||
|
},
|
||||||
|
|
||||||
registerMainProcessErrorListener(listener: IPCRendererListener) {
|
registerMainProcessErrorListener(listener: IPCRendererListener) {
|
||||||
ipcRenderer.on(IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR, listener);
|
ipcRenderer.on(IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR, listener);
|
||||||
},
|
},
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
setAndGetCleanedConfigFiles,
|
setAndGetCleanedConfigFiles,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { saveHtmlAsPdf } from './saveHtmlAsPdf';
|
import { saveHtmlAsPdf } from './saveHtmlAsPdf';
|
||||||
|
import { sendAPIRequest } from './api';
|
||||||
|
|
||||||
export default function registerIpcMainActionListeners(main: Main) {
|
export default function registerIpcMainActionListeners(main: Main) {
|
||||||
ipcMain.handle(IPC_ACTIONS.CHECK_DB_ACCESS, async (_, filePath: string) => {
|
ipcMain.handle(IPC_ACTIONS.CHECK_DB_ACCESS, async (_, filePath: string) => {
|
||||||
@ -209,6 +210,13 @@ export default function registerIpcMainActionListeners(main: Main) {
|
|||||||
return getTemplates();
|
return getTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
IPC_ACTIONS.SEND_API_REQUEST,
|
||||||
|
async (e, endpoint: string, options: RequestInit | undefined) => {
|
||||||
|
return sendAPIRequest(endpoint, options);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database Related Actions
|
* Database Related Actions
|
||||||
*/
|
*/
|
||||||
|
@ -1257,6 +1257,30 @@ export function removeFreeItems(sinvDoc: SalesInvoice) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updatePricingRule(sinvDoc: SalesInvoice) {
|
||||||
|
const applicablePricingRuleNames = await getPricingRule(sinvDoc);
|
||||||
|
|
||||||
|
if (!applicablePricingRuleNames || !applicablePricingRuleNames.length) {
|
||||||
|
sinvDoc.pricingRuleDetail = undefined;
|
||||||
|
sinvDoc.isPricingRuleApplied = false;
|
||||||
|
removeFreeItems(sinvDoc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedPricingRuleCount = sinvDoc?.items?.filter(
|
||||||
|
(val) => val.isFreeItem
|
||||||
|
).length;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
void (async () => {
|
||||||
|
if (appliedPricingRuleCount !== applicablePricingRuleNames?.length) {
|
||||||
|
await sinvDoc.appendPricingRuleDetail(applicablePricingRuleNames);
|
||||||
|
await sinvDoc.applyProductDiscount();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
export function getPricingRulesConflicts(
|
export function getPricingRulesConflicts(
|
||||||
pricingRules: PricingRule[]
|
pricingRules: PricingRule[]
|
||||||
): undefined | boolean {
|
): undefined | boolean {
|
||||||
|
@ -61,7 +61,9 @@ export enum ModelNameEnum {
|
|||||||
POSSettings = 'POSSettings',
|
POSSettings = 'POSSettings',
|
||||||
POSShift = 'POSShift',
|
POSShift = 'POSShift',
|
||||||
|
|
||||||
ERPNextSyncSettings= 'ERPNextSyncSettings'
|
ERPNextSyncSettings= 'ERPNextSyncSettings',
|
||||||
|
ERPNextSyncQueue = 'ERPNextSyncQueue',
|
||||||
|
FetchFromERPNextQueue = 'FetchFromERPNextQueue',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModelName = keyof typeof ModelNameEnum;
|
export type ModelName = keyof typeof ModelNameEnum;
|
||||||
|
@ -70,6 +70,7 @@ import { Shortcuts } from './utils/shortcuts';
|
|||||||
import { routeTo } from './utils/ui';
|
import { routeTo } from './utils/ui';
|
||||||
import { useKeys } from './utils/vueUtils';
|
import { useKeys } from './utils/vueUtils';
|
||||||
import { setDarkMode } from 'src/utils/theme';
|
import { setDarkMode } from 'src/utils/theme';
|
||||||
|
import { initERPNSync, updateERPNSyncSettings } from './utils/erpnextSync';
|
||||||
|
|
||||||
enum Screen {
|
enum Screen {
|
||||||
Desk = 'Desk',
|
Desk = 'Desk',
|
||||||
@ -224,6 +225,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
await initializeInstance(filePath, false, countryCode, fyo);
|
await initializeInstance(filePath, false, countryCode, fyo);
|
||||||
await updatePrintTemplates(fyo);
|
await updatePrintTemplates(fyo);
|
||||||
|
await updateERPNSyncSettings(fyo);
|
||||||
|
initERPNSync(fyo);
|
||||||
await this.setDesk(filePath);
|
await this.setDesk(filePath);
|
||||||
},
|
},
|
||||||
async handleConnectionFailed(error: Error, actionSymbol: symbol) {
|
async handleConnectionFailed(error: Error, actionSymbol: symbol) {
|
||||||
|
6
src/utils/api.ts
Normal file
6
src/utils/api.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export async function sendAPIRequest(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit | undefined
|
||||||
|
) {
|
||||||
|
return await ipc.sendAPIRequest(endpoint, options);
|
||||||
|
}
|
631
src/utils/erpnextSync.ts
Normal file
631
src/utils/erpnextSync.ts
Normal file
@ -0,0 +1,631 @@
|
|||||||
|
import { Fyo } from 'fyo';
|
||||||
|
import { sendAPIRequest } from './api';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { ERPNextSyncSettings } from 'models/baseModels/ERPNextSyncSettings/ERPNextSyncSettings';
|
||||||
|
import { DocValueMap } from 'fyo/core/types';
|
||||||
|
import { Doc } from 'fyo/model/doc';
|
||||||
|
import { ERPNextSyncQueue } from 'models/baseModels/ERPNextSyncQueue/ERPNextSyncQueue';
|
||||||
|
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||||
|
|
||||||
|
export async function updateERPNSyncSettings(fyo: Fyo) {
|
||||||
|
const syncSettingsDoc = (await fyo.doc.getDoc(
|
||||||
|
ModelNameEnum.ERPNextSyncSettings
|
||||||
|
)) as ERPNextSyncSettings;
|
||||||
|
|
||||||
|
const endpoint = syncSettingsDoc.endpoint;
|
||||||
|
const authToken = syncSettingsDoc.authToken;
|
||||||
|
|
||||||
|
if (!endpoint || !authToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getERPNSyncSettings(endpoint, authToken);
|
||||||
|
if (!res || !res.message || !res.message.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncSettingsDoc.setMultiple(parseSyncSettingsData(res));
|
||||||
|
await syncSettingsDoc.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getERPNSyncSettings(
|
||||||
|
endpoint: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ERPNextSyncSettingsAPIResponse | undefined> {
|
||||||
|
try {
|
||||||
|
return (await sendAPIRequest(
|
||||||
|
`${endpoint}/api/method/books_integration.api.sync_settings`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)) as unknown as ERPNextSyncSettingsAPIResponse;
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initERPNSync(fyo: Fyo) {
|
||||||
|
const isSyncEnabled = fyo.singles.ERPNextSyncSettings?.isEnabled;
|
||||||
|
if (!isSyncEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncInterval = fyo.singles.ERPNextSyncSettings?.dataSyncInterval;
|
||||||
|
|
||||||
|
if (!syncInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
setInterval(async () => {
|
||||||
|
await syncFetchFromERPNextQueue(fyo);
|
||||||
|
await syncDocumentsFromERPNext(fyo);
|
||||||
|
await syncDocumentsToERPNext(fyo);
|
||||||
|
}, syncInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncDocumentsFromERPNext(fyo: Fyo) {
|
||||||
|
const isEnabled = fyo.singles.ERPNextSyncSettings?.isEnabled;
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = fyo.singles.ERPNextSyncSettings?.authToken;
|
||||||
|
const endpoint = fyo.singles.ERPNextSyncSettings?.endpoint;
|
||||||
|
|
||||||
|
if (!token || !endpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsToSync = await getDocsFromERPNext(endpoint, token);
|
||||||
|
|
||||||
|
if (!docsToSync || !docsToSync.message.success || !docsToSync.message.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const doc of docsToSync.message.data) {
|
||||||
|
if (!(getDocTypeName(doc) in ModelNameEnum)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ((doc.fbooksDocName as string) || (doc.name as string)) {
|
||||||
|
const isDocExists = await fyo.db.exists(
|
||||||
|
getDocTypeName(doc),
|
||||||
|
(doc.fbooksDocName as string) || (doc.name as string)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDocExists) {
|
||||||
|
const existingDoc = await fyo.doc.getDoc(
|
||||||
|
getDocTypeName(doc),
|
||||||
|
(doc.fbooksDocName as string) || (doc.name as string)
|
||||||
|
);
|
||||||
|
|
||||||
|
await existingDoc.setMultiple(doc);
|
||||||
|
await performPreSync(fyo, doc, existingDoc);
|
||||||
|
existingDoc._addDocToSyncQueue = false;
|
||||||
|
|
||||||
|
await existingDoc.sync();
|
||||||
|
|
||||||
|
if (doc.submitted) {
|
||||||
|
await existingDoc.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.cancelled) {
|
||||||
|
await existingDoc.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newDoc = fyo.doc.getNewDoc(getDocTypeName(doc), doc);
|
||||||
|
|
||||||
|
await performPreSync(fyo, doc, newDoc);
|
||||||
|
newDoc._addDocToSyncQueue = false;
|
||||||
|
|
||||||
|
await newDoc.sync();
|
||||||
|
|
||||||
|
if (doc.submitted) {
|
||||||
|
await newDoc.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.cancelled) {
|
||||||
|
await newDoc.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
await afterDocSync(
|
||||||
|
endpoint,
|
||||||
|
token,
|
||||||
|
doc,
|
||||||
|
doc.name as string,
|
||||||
|
newDoc.name as string
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performPreSync(fyo: Fyo, doc: DocValueMap) {
|
||||||
|
switch (doc.doctype) {
|
||||||
|
case ModelNameEnum.Item:
|
||||||
|
const isUnitExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.UOM,
|
||||||
|
doc.unit as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const isUnitExistsInQueue = (
|
||||||
|
await fyo.db.getAll(ModelNameEnum.FetchFromERPNextQueue, {
|
||||||
|
filters: {
|
||||||
|
referenceType: ModelNameEnum.UOM,
|
||||||
|
documentName: doc.unit as string,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (!isUnitExists && !isUnitExistsInQueue) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.UOM,
|
||||||
|
documentName: doc.unit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.uomConversions) {
|
||||||
|
for (const row of doc.uomConversions as DocValueMap[]) {
|
||||||
|
const isUnitExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.UOM,
|
||||||
|
row.uom as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUnitExists && !isUnitExistsInQueue) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.UOM,
|
||||||
|
documentName: row.uom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case ModelNameEnum.Party:
|
||||||
|
const isAddressExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.Address,
|
||||||
|
doc.addressName as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAddressExists) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.Address,
|
||||||
|
documentName: doc.addressName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
case ModelNameEnum.SalesInvoice:
|
||||||
|
return await preSyncSalesInvoice(fyo, doc as SalesInvoice);
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preSyncSalesInvoice(fyo: Fyo, doc: SalesInvoice) {
|
||||||
|
const isPartyExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.Party,
|
||||||
|
doc.party as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPartyExists) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.Party,
|
||||||
|
documentName: doc.party,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.items) {
|
||||||
|
for (const item of doc.items) {
|
||||||
|
const isUnitExists = await fyo.db.exists(ModelNameEnum.UOM, item.unit);
|
||||||
|
if (!isUnitExists) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.UOM,
|
||||||
|
documentName: item.unit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isItemExists = await fyo.db.exists(ModelNameEnum.Item, item.item);
|
||||||
|
if (!isItemExists) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.Item,
|
||||||
|
documentName: item.item,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.batch) {
|
||||||
|
const isBatchExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.Batch,
|
||||||
|
item.batch
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isBatchExists) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.Batch,
|
||||||
|
documentName: item.batch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.priceList) {
|
||||||
|
const isPriceListExists = await fyo.db.exists(
|
||||||
|
ModelNameEnum.PriceList,
|
||||||
|
doc.priceList
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isPriceListExists) {
|
||||||
|
await addToFetchFromERPNextQueue(fyo, {
|
||||||
|
referenceType: ModelNameEnum.PriceList,
|
||||||
|
documentName: doc.priceList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToFetchFromERPNextQueue(fyo: Fyo, data: DocValueMap) {
|
||||||
|
await fyo.doc.getNewDoc(ModelNameEnum.FetchFromERPNextQueue, data).sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncDocumentsToERPNext(fyo: Fyo) {
|
||||||
|
const isEnabled = fyo.singles.ERPNextSyncSettings?.isEnabled;
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = fyo.singles.ERPNextSyncSettings?.authToken as string;
|
||||||
|
const endpoint = fyo.singles.ERPNextSyncSettings?.endpoint as string;
|
||||||
|
|
||||||
|
if (!token || !endpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsToSync = [];
|
||||||
|
const syncQueueItems = (await fyo.db.getAll(ModelNameEnum.ERPNextSyncQueue, {
|
||||||
|
fields: ['referenceType', 'documentName'],
|
||||||
|
order: 'desc',
|
||||||
|
})) as ERPNextSyncQueue[];
|
||||||
|
|
||||||
|
if (!syncQueueItems.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const doc of syncQueueItems) {
|
||||||
|
const referenceDoc = await fyo.doc.getDoc(
|
||||||
|
doc.referenceType as ModelNameEnum,
|
||||||
|
doc.documentName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!referenceDoc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
docsToSync.push({
|
||||||
|
doctype: getDocTypeName(referenceDoc),
|
||||||
|
...referenceDoc.getValidDict(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!docsToSync.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = (await sendAPIRequest(
|
||||||
|
`${endpoint}/api/method/books_integration.api.insert_docs`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ payload: docsToSync }),
|
||||||
|
}
|
||||||
|
)) as unknown as InsertDocsAPIResponse;
|
||||||
|
|
||||||
|
if (res.message.success) {
|
||||||
|
if (!res.message.success_log.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const doc of res.message.success_log) {
|
||||||
|
const filteredLogDoc = await fyo.db.getAll(
|
||||||
|
ModelNameEnum.ERPNextSyncQueue,
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
referenceType: getDocTypeName(doc),
|
||||||
|
documentName: doc.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!filteredLogDoc.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logDoc = await fyo.doc.getDoc(
|
||||||
|
ModelNameEnum.ERPNextSyncQueue,
|
||||||
|
filteredLogDoc[0].name as string
|
||||||
|
);
|
||||||
|
|
||||||
|
await logDoc.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncFetchFromERPNextQueue(fyo: Fyo) {
|
||||||
|
const docsInQueue = await fyo.db.getAll(ModelNameEnum.FetchFromERPNextQueue, {
|
||||||
|
fields: ['referenceType', 'documentName'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!docsInQueue.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = fyo.singles.ERPNextSyncSettings?.authToken as string;
|
||||||
|
const endpoint = fyo.singles.ERPNextSyncSettings?.endpoint as string;
|
||||||
|
|
||||||
|
if (!token || !endpoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = (await sendAPIRequest(
|
||||||
|
`${endpoint}/api/method/books_integration.api.sync_queue`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ records: docsInQueue }),
|
||||||
|
}
|
||||||
|
)) as unknown as ERPNSyncDocsResponse;
|
||||||
|
|
||||||
|
if (!res.message.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.message.success_log) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of res.message.success_log) {
|
||||||
|
const isDocExisitsInQueue = await fyo.db.getAll(
|
||||||
|
ModelNameEnum.FetchFromERPNextQueue,
|
||||||
|
{
|
||||||
|
filters: {
|
||||||
|
referenceType: row.doctype_name as string,
|
||||||
|
documentName: row.document_name as string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDocExisitsInQueue.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingDoc = await fyo.doc.getDoc(
|
||||||
|
ModelNameEnum.FetchFromERPNextQueue,
|
||||||
|
isDocExisitsInQueue[0].name as string
|
||||||
|
);
|
||||||
|
await existingDoc.delete();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDocsFromERPNext(
|
||||||
|
endpoint: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ERPNSyncDocsResponse | undefined> {
|
||||||
|
try {
|
||||||
|
return (await sendAPIRequest(
|
||||||
|
`${endpoint}/api/method/books_integration.api.sync_queue`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)) as unknown as ERPNSyncDocsResponse;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function afterDocSync(
|
||||||
|
endpoint: string,
|
||||||
|
token: string,
|
||||||
|
doc: Doc | DocValueMap,
|
||||||
|
erpnDocName: string,
|
||||||
|
fbooksDocName: string
|
||||||
|
) {
|
||||||
|
const res = await ipc.sendAPIRequest(
|
||||||
|
`${endpoint}/api/method/books_integration.api.perform_aftersync`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
doctype: getDocTypeName(doc),
|
||||||
|
nameInERPNext: erpnDocName,
|
||||||
|
nameInFBooks: fbooksDocName,
|
||||||
|
doc,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShouldDocSyncToERPNext(
|
||||||
|
syncSettings: ERPNextSyncSettings,
|
||||||
|
doc: Doc
|
||||||
|
): boolean {
|
||||||
|
switch (doc.schemaName) {
|
||||||
|
case ModelNameEnum.Payment:
|
||||||
|
const isSalesPayment = doc.referenceType === ModelNameEnum.SalesInvoice;
|
||||||
|
return (
|
||||||
|
isSalesPayment && syncSettings.sinvPaymentType !== 'ERPNext to FBooks'
|
||||||
|
);
|
||||||
|
|
||||||
|
case ModelNameEnum.Party:
|
||||||
|
const isCustomer = doc.role !== 'Supplier';
|
||||||
|
|
||||||
|
if (isCustomer) {
|
||||||
|
return (
|
||||||
|
!!syncSettings.syncCustomer &&
|
||||||
|
syncSettings.customerSyncType !== 'ERPNext to FBooks'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!syncSettings.syncSupplier &&
|
||||||
|
syncSettings.supplierSyncType !== 'ERPNext to FBooks'
|
||||||
|
);
|
||||||
|
|
||||||
|
case ModelNameEnum.PriceListItem:
|
||||||
|
const isPriceListSyncEnabled = !!syncSettings.syncPriceList;
|
||||||
|
|
||||||
|
return (
|
||||||
|
isPriceListSyncEnabled &&
|
||||||
|
syncSettings.supplierSyncType !== 'ERPNext to FBooks'
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
const schemaName =
|
||||||
|
doc.schemaName[0].toLowerCase() + doc.schemaName.substring(1);
|
||||||
|
|
||||||
|
if (!syncSettings[`${schemaName}SyncType`]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncSettings[`${schemaName}SyncType`] !== 'ERPNext to FBooks';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDocTypeName(doc: DocValueMap | Doc): string {
|
||||||
|
const doctype =
|
||||||
|
doc.schemaName ?? doc.referenceType ?? (doc.doctype as string);
|
||||||
|
|
||||||
|
if (['Supplier', 'Customer'].includes(doctype as string)) {
|
||||||
|
return ModelNameEnum.Party;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doctype === 'Party') {
|
||||||
|
if (doc.role && doc.role !== 'Both') {
|
||||||
|
return doc.role as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doctype as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InsertDocsAPIResponse {
|
||||||
|
message: {
|
||||||
|
success: boolean;
|
||||||
|
success_log: { name: string; doctype: string }[];
|
||||||
|
failed_log: { name: string; doctype: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ERPNSyncDocsResponse {
|
||||||
|
message: {
|
||||||
|
success: boolean;
|
||||||
|
data: DocValueMap[];
|
||||||
|
success_log?: DocValueMap[];
|
||||||
|
failed_log?: DocValueMap[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ERPNextSyncSettingsAPIResponse {
|
||||||
|
message: {
|
||||||
|
success: boolean;
|
||||||
|
app_version: string;
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
modified: string;
|
||||||
|
modified_by: string;
|
||||||
|
docstatus: boolean;
|
||||||
|
idx: string;
|
||||||
|
enable_sync: boolean;
|
||||||
|
sync_dependant_masters: boolean;
|
||||||
|
sync_interval: number;
|
||||||
|
sync_item: boolean;
|
||||||
|
item_sync_type: string;
|
||||||
|
sync_customer: boolean;
|
||||||
|
customer_sync_type: string;
|
||||||
|
sync_supplier: boolean;
|
||||||
|
supplier_sync_type: string;
|
||||||
|
sync_sales_invoice: boolean;
|
||||||
|
sales_invoice_sync_type: string;
|
||||||
|
sync_sales_payment: boolean;
|
||||||
|
sales_payment_sync_type: string;
|
||||||
|
sync_stock: boolean;
|
||||||
|
stock_sync_type: string;
|
||||||
|
sync_price_list: boolean;
|
||||||
|
price_list_sync_type: string;
|
||||||
|
sync_serial_number: boolean;
|
||||||
|
serial_number_sync_type: string;
|
||||||
|
sync_batches: boolean;
|
||||||
|
batch_sync_type: string;
|
||||||
|
sync_delivery_note: boolean;
|
||||||
|
delivery_note_sync_type: string;
|
||||||
|
doctype: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSyncSettingsData(
|
||||||
|
res: ERPNextSyncSettingsAPIResponse
|
||||||
|
): DocValueMap {
|
||||||
|
return {
|
||||||
|
integrationAppVersion: res.message.app_version,
|
||||||
|
isEnabled: !!res.message.data.enable_sync,
|
||||||
|
dataSyncInterval: res.message.data.sync_interval,
|
||||||
|
|
||||||
|
syncItem: res.message.data.sync_item,
|
||||||
|
itemSyncType: res.message.data.item_sync_type,
|
||||||
|
|
||||||
|
syncCustomer: res.message.data.sync_customer,
|
||||||
|
customerSyncType: res.message.data.customer_sync_type,
|
||||||
|
|
||||||
|
syncSupplier: res.message.data.sync_supplier,
|
||||||
|
supplierSyncType: res.message.data.supplier_sync_type,
|
||||||
|
|
||||||
|
syncSalesInvoice: res.message.data.sync_sales_invoice,
|
||||||
|
salesInvoiceSyncType: res.message.data.sales_invoice_sync_type,
|
||||||
|
|
||||||
|
syncSalesInvoicePayment: res.message.data.sync_sales_payment,
|
||||||
|
sinvPaymentSyncType: res.message.data.sales_payment_sync_type,
|
||||||
|
|
||||||
|
syncStockMovement: res.message.data.sync_stock,
|
||||||
|
stockMovementSyncType: res.message.data.stock_sync_type,
|
||||||
|
|
||||||
|
syncPriceList: res.message.data.sync_price_list,
|
||||||
|
priceListSyncType: res.message.data.price_list_sync_type,
|
||||||
|
|
||||||
|
syncSerialNumber: res.message.data.sync_serial_number,
|
||||||
|
serialNumberSyncType: res.message.data.serial_number_sync_type,
|
||||||
|
|
||||||
|
syncBatch: res.message.data.sync_batches,
|
||||||
|
batchSyncType: res.message.data.batch_sync_type,
|
||||||
|
|
||||||
|
syncShipment: res.message.data.sync_delivery_note,
|
||||||
|
shipmentSyncType: res.message.data.delivery_note_sync_type,
|
||||||
|
};
|
||||||
|
}
|
@ -33,6 +33,7 @@ export enum IPC_ACTIONS {
|
|||||||
GET_TEMPLATES = 'get-templates',
|
GET_TEMPLATES = 'get-templates',
|
||||||
DELETE_FILE = 'delete-file',
|
DELETE_FILE = 'delete-file',
|
||||||
GET_DB_DEFAULT_PATH = 'get-db-default-path',
|
GET_DB_DEFAULT_PATH = 'get-db-default-path',
|
||||||
|
SEND_API_REQUEST = 'send-api-request',
|
||||||
// Database messages
|
// Database messages
|
||||||
DB_CREATE = 'db-create',
|
DB_CREATE = 'db-create',
|
||||||
DB_CONNECT = 'db-connect',
|
DB_CONNECT = 'db-connect',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user