2
0
mirror of https://github.com/frappe/books.git synced 2025-01-11 02:36:14 +00:00

Merge pull request #97 from thefalconx33/master

This commit is contained in:
Saqib 2019-09-03 15:33:46 +05:30 committed by GitHub
commit 15b3b1c662
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
149 changed files with 10050 additions and 15713 deletions

BIN
.github/preview.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,7 +1,22 @@
# Frappe Accounting
<div align="center">
<h1>
Frappe Accounting
</h1>
<h3>
Simple app for personal and small businesses accounting
</h3>
<h5>
it's pronounced - <em>fra-pay</em>
</h5>
</div>
Simple JS based app for personal and small businesses accounting
<p align="center">
<a href="https://frappe.io/accounting">
<img src=".github/preview.gif">
</a>
</p>
Frappe Accounting is built on [FrappeJS](https://github.com/frappe/frappejs) Framework a Full-Stack VueJS based meta-data driven web framework. Under the hood it uses Electron bundles.
### Installation
@ -16,11 +31,19 @@ apt-get install build-essential python git
apt-get install libgconf-2-4
```
MacOS
```bash
xcode-select --install
```
You will also need [Xcode App](https://apps.apple.com/in/app/xcode/id497799835?mt=12) from App Store
#### Step 1
Install [Node.js](https://nodejs.org/en/) LTS (version 8.11.1)
Install [Node.js](https://nodejs.org/en/) (version 12.6.0)
> Tip: The best way to install and manage Node is to install [nvm](https://github.com/creationix/nvm)
> Tip: The best way to install and manage Node is to install [nvm](https://github.com/nvm-sh/nvm#usage)
#### Step 2
@ -32,7 +55,7 @@ npm install -g yarn
#### Step 3
Clone this repo
Clone this repo
```bash
git clone https://github.com/frappe/accounting.git
@ -51,6 +74,11 @@ yarn
# Start the electron app
yarn electron
```
### Troubleshooting
Frappe Technologies
- If you are facing node-gyp errors then you may need to:
1. Install Xcode App from App Store.
2. Use node v12.6.0
3. Delete yarn.lock/package-lock.json

View File

@ -5,16 +5,40 @@ module.exports = class LedgerPosting {
Object.assign(this, arguments[0]);
this.entries = [];
this.entryMap = {};
// To change balance while entering ledger entries
this.accountEntries = [];
}
debit(account, amount, referenceType, referenceName) {
async debit(account, amount, referenceType, referenceName) {
const entry = this.getEntry(account, referenceType, referenceName);
amount = frappe.parseNumber(amount);
entry.debit += amount;
await this.setAccountBalanceChange(account, 'debit', amount);
}
credit(account, amount, referenceType, referenceName) {
async credit(account, amount, referenceType, referenceName) {
const entry = this.getEntry(account, referenceType, referenceName);
amount = frappe.parseNumber(amount);
entry.credit += amount;
await this.setAccountBalanceChange(account, 'credit', amount);
}
async setAccountBalanceChange(accountName, type, amount) {
const debitAccounts = ['Asset', 'Expense'];
const { rootType } = await frappe.getDoc('Account', accountName);
if (debitAccounts.indexOf(rootType) === -1) {
const change = type == 'credit' ? amount : -1 * amount;
this.accountEntries.push({
name: accountName,
balanceChange: change
});
} else {
const change = type == 'debit' ? amount : -1 * amount;
this.accountEntries.push({
name: accountName,
balanceChange: change
});
}
}
getEntry(account, referenceType, referenceName) {
@ -50,6 +74,9 @@ module.exports = class LedgerPosting {
entry.debit = entry.credit;
entry.credit = temp;
}
for (let entry of this.accountEntries) {
entry.balanceChange = -1 * entry.balanceChange;
}
await this.insertEntries();
}
@ -58,7 +85,6 @@ module.exports = class LedgerPosting {
let credit = 0;
let debitAccounts = [];
let creditAccounts = [];
for (let entry of this.entries) {
debit += entry.debit;
credit += entry.credit;
@ -68,9 +94,20 @@ module.exports = class LedgerPosting {
creditAccounts.push(entry.account);
}
}
debit = Math.floor(debit * 100) / 100;
credit = Math.floor(credit * 100) / 100;
if (debit !== credit) {
throw new frappe.errors.ValidationError(frappe._('Debit {0} must be equal to Credit {1}', [debit, credit]));
frappe.call({
method: 'show-dialog',
args: {
title: 'Invalid Entry',
message: frappe._('Debit {0} must be equal to Credit {1}', [
debit,
credit
])
}
});
throw new Error();
}
}
@ -82,5 +119,10 @@ module.exports = class LedgerPosting {
Object.assign(entryDoc, entry);
await entryDoc.insert();
}
for (let entry of this.accountEntries) {
let entryDoc = await frappe.getDoc('Account', entry.name);
entryDoc.balance += entry.balanceChange;
await entryDoc.update();
}
}
};

View File

@ -271,7 +271,7 @@
"currency_fraction_units": 100,
"currency_name": "Belize Dollar",
"currency_symbol": "$",
"date_format": "mm-dd-yyyy",
"date_format": "MM-dd-yyyy",
"number_format": "#,###.##",
"timezones": [
"America/Belize"
@ -356,7 +356,7 @@
"currency_fraction": "Centavo",
"currency_fraction_units": 100,
"currency_symbol": "R$",
"date_format": "dd/mm/yyyy",
"date_format": "dd/MM/yyyy",
"number_format": "#.###,##",
"timezones": [
"America/Araguaina",
@ -463,7 +463,7 @@
"currency_fraction_units": 100,
"currency_name": "Canadian Dollar",
"currency_symbol": "$",
"date_format": "mm-dd-yyyy",
"date_format": "MM-dd-yyyy",
"number_format": "#,###.##",
"timezones": [
"America/Atikokan",
@ -563,7 +563,7 @@
"code": "cn",
"currency": "CNY",
"currency_name": "Yuan Renminbi",
"date_format": "yyyy-mm-dd",
"date_format": "yyyy-MM-dd",
"number_format": "#,###.##",
"timezones": [
"Asia/Chongqing",
@ -884,7 +884,7 @@
"currency_fraction_units": 100,
"currency_symbol": "\u20ac",
"number_format": "# ###,##",
"date_format": "dd/mm/yyyy",
"date_format": "dd/MM/yyyy",
"timezones": [
"Europe/Paris"
]
@ -1136,7 +1136,7 @@
"currency_fraction_units": 100,
"currency_name": "Forint",
"currency_symbol": "Ft",
"date_format": "yyyy-mm-dd",
"date_format": "yyyy-MM-dd",
"number_format": "#.###",
"timezones": [
"Europe/Budapest"
@ -1161,6 +1161,7 @@
"currency_fraction_units": 100,
"currency_name": "Indian Rupee",
"currency_symbol": "\u20b9",
"date_format": "dd/MM/yyyy",
"number_format": "#,##,###.##",
"timezones": [
"Asia/Kolkata"
@ -1247,7 +1248,7 @@
"currency_fraction_units": 100,
"currency_symbol": "\u20ac",
"number_format": "#.###,##",
"date_format": "dd/mm/yyyy",
"date_format": "dd/MM/yyyy",
"timezones": [
"Europe/Rome"
],
@ -1471,7 +1472,7 @@
"currency_fraction_units": 100,
"currency_name": "Lithuanian Litas",
"currency_symbol": "Lt",
"date_format": "yyyy-mm-dd",
"date_format": "yyyy-MM-dd",
"number_format": "# ###,##",
"timezones": [
"Europe/Vilnius"
@ -1674,7 +1675,7 @@
"currency_fraction_units": 100,
"currency_name": "Tugrik",
"currency_symbol": "\u20ae",
"date_format": "yyyy-mm-dd",
"date_format": "yyyy-MM-dd",
"number_format": "#,###.##",
"timezones": [
"Asia/Choibalsan",
@ -1911,7 +1912,7 @@
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"currency_symbol": "$",
"date_format": "mm-dd-yyyy",
"date_format": "MM-dd-yyyy",
"number_format": "#,###.##",
"timezones": [
"Pacific/Palau"
@ -1974,7 +1975,7 @@
"currency_fraction_units": 100,
"currency_name": "Philippine Peso",
"currency_symbol": "\u20b1",
"date_format": "mm-dd-yyyy",
"date_format": "MM-dd-yyyy",
"number_format": "#,###.##",
"timezones": [
"Asia/Manila"
@ -2278,7 +2279,7 @@
"currency_fraction_units": 100,
"currency_name": "Rand",
"currency_symbol": "R",
"date_format": "yyyy-mm-dd",
"date_format": "yyyy-MM-dd",
"number_format": "# ###.##",
"timezones": [
"Africa/Johannesburg"
@ -2398,7 +2399,7 @@
"Taiwan": {
"code": "tw",
"currency": "TWD",
"date_format": "yyyy-mm-dd",
"date_format": "yyyy-MM-dd",
"number_format": "#,###.##"
},
"Tajikistan": {
@ -2591,7 +2592,7 @@
"smallest_currency_fraction_value": 0.05,
"currency_name": "US Dollar",
"currency_symbol": "$",
"date_format": "mm-dd-yyyy",
"date_format": "MM-dd-yyyy",
"number_format": "#,###.##",
"timezones": [
"America/Adak",

View File

@ -1,7 +1,7 @@
module.exports = {
doctype: "PrintFormat",
name: "Standard Invoice Format",
for: "Invoice",
for: "SalesInvoice",
template: `
<h1>{{ doc.name }}</h1>
<div class="row py-4">

View File

@ -1,99 +1,107 @@
module.exports = {
"name": "Account",
"doctype": "DocType",
"documentClass": require("./AccountDocument.js"),
"isSingle": 0,
"isTree": 1,
"keywordFields": [
"name",
"rootType",
"accountType"
],
"fields": [
{
"fieldname": "name",
"label": "Account Name",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "parentAccount",
"label": "Parent Account",
"fieldtype": "Link",
"target": "Account"
},
{
"fieldname": "rootType",
"label": "Root Type",
"fieldtype": "Select",
"options": [
"Asset",
"Liability",
"Equity",
"Income",
"Expense"
]
},
{
"fieldname": "accountType",
"label": "Account Type",
"fieldtype": "Select",
"options": [
"Accumulated Depreciation",
"Bank",
"Cash",
"Chargeable",
"Cost of Goods Sold",
"Depreciation",
"Equity",
"Expense Account",
"Expenses Included In Valuation",
"Fixed Asset",
"Income Account",
"Payable",
"Receivable",
"Round Off",
"Stock",
"Stock Adjustment",
"Stock Received But Not Billed",
"Tax",
"Temporary"
]
},
{
"fieldname": "balance",
"label": "Balance",
"fieldtype": "Currency",
"default": '0',
"disabled": true
},
{
"fieldname": "isGroup",
"label": "Is Group",
"fieldtype": "Check"
}
],
events: {
validate: (doc) => {
}
name: 'Account',
label: 'Account',
doctype: 'DocType',
documentClass: require('./AccountDocument.js'),
isSingle: 0,
isTree: 1,
keywordFields: ['name', 'rootType', 'accountType'],
fields: [
{
fieldname: 'name',
label: 'Account Name',
fieldtype: 'Data',
required: 1
},
listSettings: {
getFields(list) {
return ['name', 'accountType', 'rootType'];
},
getRowHTML(list, data) {
return `<div class="col-11">${list.getNameHTML(data)} (${data.rootType})</div>`;
}
{
fieldname: 'rootType',
label: 'Root Type',
fieldtype: 'Select',
options: [
'Select...',
'Asset',
'Liability',
'Equity',
'Income',
'Expense'
],
required: 1
},
treeSettings: {
parentField: 'parentAccount',
async getRootLabel() {
let accountingSettings = await frappe.getSingle('AccountingSettings');
return accountingSettings.companyName;
}
{
fieldname: 'parentAccount',
label: 'Parent Account',
fieldtype: 'Link',
target: 'Account',
getFilters: (query, doc) => {
const filter = {
isGroup: 1
};
doc.rootType ? (filter.rootType = doc.rootType) : '';
return filter;
}
},
{
fieldname: 'accountType',
label: 'Account Type',
fieldtype: 'Select',
required: 1,
options: [
'',
'Accumulated Depreciation',
'Bank',
'Cash',
'Chargeable',
'Cost of Goods Sold',
'Depreciation',
'Equity',
'Expense Account',
'Expenses Included In Valuation',
'Fixed Asset',
'Income Account',
'Payable',
'Receivable',
'Round Off',
'Stock',
'Stock Adjustment',
'Stock Received But Not Billed',
'Tax',
'Temporary'
]
},
{
fieldname: 'balance',
label: 'Balance',
fieldtype: 'Currency',
default: '0',
disabled: true
},
{
fieldname: 'isGroup',
label: 'Is Group',
fieldtype: 'Check'
}
}
],
events: {
validate: doc => {}
},
listSettings: {
getFields(list) {
return ['name', 'accountType', 'rootType'];
},
getRowHTML(list, data) {
return `<div class="col-11">${list.getNameHTML(data)} (${
data.rootType
})</div>`;
}
},
treeSettings: {
parentField: 'parentAccount',
async getRootLabel() {
let accountingSettings = await frappe.getSingle('AccountingSettings');
return accountingSettings.companyName;
}
}
};

View File

@ -0,0 +1,7 @@
import { _ } from 'frappejs/utils';
export default {
doctype: 'Account',
title: _('Account'),
columns: ['name', 'parentAccount', 'rootType']
};

View File

@ -3,67 +3,72 @@ const path = require('path');
const fs = require('fs');
const countries = require('../../../fixtures/countryInfo.json');
const standardCOA = require('../../../fixtures/verified/standardCOA.json');
const accountFields = ['accountType', 'rootType', 'isGroup', 'account_type', 'root_type', 'is_group'];
const accountFields = [
'accountType',
'rootType',
'isGroup',
'account_type',
'root_type',
'is_group'
];
async function importAccounts(children, parent, rootType, rootAccount) {
for (let accountName in children) {
const child = children[accountName];
for (let accountName in children) {
const child = children[accountName];
if (rootAccount) {
rootType = child.rootType || child.root_type;
}
if (!accountFields.includes(accountName)) {
let isGroup = identifyIsGroup(child);
const doc = frappe.newDoc({
doctype: 'Account',
name: accountName,
parentAccount: parent,
isGroup,
rootType,
balance: 0,
accountType: child.accountType
})
await doc.insert()
await importAccounts(child, accountName, rootType)
}
if (rootAccount) {
rootType = child.rootType || child.root_type;
}
if (!accountFields.includes(accountName)) {
let isGroup = identifyIsGroup(child);
const doc = frappe.newDoc({
doctype: 'Account',
name: accountName,
parentAccount: parent,
isGroup,
rootType,
balance: 0,
accountType: child.accountType || child.account_type
});
await doc.insert();
await importAccounts(child, accountName, rootType);
}
}
}
function identifyIsGroup(child) {
if (child.isGroup || child.is_group) {
return child.isGroup || child.is_group;
}
if (child.isGroup || child.is_group) {
return child.isGroup || child.is_group;
}
const keys = Object.keys(child);
const children = keys.filter(key => !accountFields.includes(key))
const keys = Object.keys(child);
const children = keys.filter(key => !accountFields.includes(key));
if (children.length) {
return 1;
}
if (children.length) {
return 1;
}
return 0;
return 0;
}
async function getCountryCOA(){
const doc = await frappe.getDoc('AccountingSettings');
const conCode = countries[doc.country].code;
async function getCountryCOA() {
const doc = await frappe.getDoc('AccountingSettings');
const conCode = countries[doc.country].code;
const countryCOA = path.resolve(path.join('./fixtures/verified/', conCode + '.json'));
if(fs.existsSync(countryCOA)){
const jsonText = fs.readFileSync(countryCOA, 'utf-8');
const json = JSON.parse(jsonText);
return json.tree;
} else {
return standardCOA;
}
try {
const countryCoa = require('../../../fixtures/verified/' +
conCode +
'.json');
return countryCoa.tree;
} catch (e) {
return standardCOA;
}
}
module.exports = async function importCharts() {
const chart = await getCountryCOA();
await importAccounts(chart, '', '', true)
}
const chart = await getCountryCOA();
await importAccounts(chart, '', '', true);
};

View File

@ -1,70 +1,89 @@
const countryList = Object.keys(require('../../../fixtures/countryInfo.json')).sort();
const countryList = Object.keys(
require('../../../fixtures/countryInfo.json')
).sort();
module.exports = {
name: "AccountingSettings",
label: "Accounting Settings",
naming: "name", // {random|autoincrement}
isSingle: 1,
isChild: 0,
isSubmittable: 0,
settings: null,
keywordFields: [],
fields: [{
label: "Company Name",
fieldname: "companyName",
fieldtype: "Data",
required: 1,
disabled: 0
},
name: 'AccountingSettings',
label: 'Accounting Settings',
naming: 'name', // {random|autoincrement}
isSingle: 1,
isChild: 0,
isSubmittable: 0,
settings: null,
keywordFields: [],
fields: [
{
label: 'Company Name',
fieldname: 'companyName',
fieldtype: 'Data',
required: 1,
disabled: 0
},
{
label: "Writeoff Account",
fieldname: "writeOffAccount",
fieldtype: "Account"
},
{
label: 'Writeoff Account',
fieldname: 'writeOffAccount',
fieldtype: 'Account'
},
{
"fieldname": "country",
"label": "Country",
"fieldtype": "Autocomplete",
"required": 1,
getList: () => countryList
},
{
fieldname: 'country',
label: 'Country',
fieldtype: 'Autocomplete',
required: 1,
getList: () => countryList
},
{
"fieldname": "fullname",
"label": "Name",
"fieldtype": "Data",
"required": 1
},
{
fieldname: 'currency',
label: 'Country Currency',
fieldtype: 'Data',
required: 0
},
{
"fieldname": "email",
"label": "Email",
"fieldtype": "Data",
"required": 1
},
{
fieldname: 'numberFormat',
fieldtype: 'Data'
},
{
"fieldname": "bankName",
"label": "Bank Name",
"fieldtype": "Data",
"required": 1
},
{
fieldname: 'symbol',
fieldtype: 'Data'
},
{
"fieldname": "fiscalYearStart",
"label": "Fiscal Year Start Date",
"fieldtype": "Date",
"required": 1
},
{
fieldname: 'fullname',
label: 'Name',
fieldtype: 'Data',
required: 1
},
{
"fieldname": "fiscalYearEnd",
"label": "Fiscal Year End Date",
"fieldtype": "Date",
"required": 1
},
{
fieldname: 'email',
label: 'Email',
fieldtype: 'Data',
required: 1
},
]
}
{
fieldname: 'bankName',
label: 'Bank Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'fiscalYearStart',
label: 'Fiscal Year Start Date',
fieldtype: 'Date',
required: 1
},
{
fieldname: 'fiscalYearEnd',
label: 'Fiscal Year End Date',
fieldtype: 'Date',
required: 1
}
]
};

View File

@ -1,124 +1,141 @@
module.exports = {
"name": "Address",
"doctype": "DocType",
"isSingle": 0,
"keywordFields": [
"name"
],
pageSettings: {
hideTitle: true
name: 'Address',
doctype: 'DocType',
isSingle: 0,
keywordFields: ['name'],
pageSettings: {
hideTitle: true
},
fields: [
{
fieldname: 'name',
label: 'Address Title',
fieldtype: 'Data',
defaultValue: 'Work',
required: 1
},
// "naming": "autoincrement",
"fields": [
{
"fieldname": "name",
"label": "Address Title",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "addressType",
"label": "Address Type",
"fieldtype": "Select",
"options": [
"Billing", "Shipping", "Office",
"Personal", "Plant", "Postal",
"Shop", "Subsidary", "Warehouse",
"Current", "Permanent", "Other"
]
},
{
"fieldname": "addressLine1",
"label": "Address Line 1",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "addressLine2",
"label": "Address Line 2",
"fieldtype": "Data"
},
{
"fieldname": "city",
"label": "City / Town",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "state",
"label": "State",
"fieldtype": "Data"
},
{
"fieldname": "country",
"label": "Country",
"fieldtype": "Data",
"required": 1
},
{
"fieldname": "postalCode",
"label": "Postal Code",
"fieldtype": "Data"
},
{
"fieldname": "emailAddress",
"label": "Email Address",
"fieldtype": "Data"
},
{
"fieldname": "phone",
"label": "Phone",
"fieldtype": "Data"
},
{
"fieldname": "fax",
"label": "Fax",
"fieldtype": "Data"
},
{
"fieldname": "isPreferredBilling",
"label": "Preferred Billing Address",
"fieldtype": "Check"
},
{
"fieldname": "isShippingBilling",
"label": "Preferred Shipping Address",
"fieldtype": "Check"
}
],
// events: {
// validate: (doc) => {
// }
// },
listSettings: {
getFields(list) {
return ['name', 'addressType'];
},
getRowHTML(list, data) {
return `<div class="col-11">${list.getNameHTML(data)} (${data.addressType})</div>`;
}
{
fieldname: 'addressType',
label: 'Address Type',
fieldtype: 'Select',
options: [
'Billing',
'Shipping',
'Office',
'Personal',
'Plant',
'Postal',
'Shop',
'Subsidary',
'Warehouse',
'Current',
'Permanent',
'Other'
]
},
{
fieldname: 'addressLine1',
label: 'Address Line 1',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'addressLine2',
label: 'Address Line 2',
fieldtype: 'Data'
},
{
fieldname: 'city',
label: 'City / Town',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'state',
label: 'State',
fieldtype: 'Data'
},
{
fieldname: 'country',
label: 'Country',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'postalCode',
label: 'Postal Code',
fieldtype: 'Data'
},
{
fieldname: 'emailAddress',
label: 'Email Address',
fieldtype: 'Data'
},
{
fieldname: 'phone',
label: 'Phone',
fieldtype: 'Data'
},
{
fieldname: 'fax',
label: 'Fax',
fieldtype: 'Data'
},
{
fieldname: 'isPreferredBilling',
label: 'Preferred Billing Address',
fieldtype: 'Check'
},
{
fieldname: 'isShippingBilling',
label: 'Preferred Shipping Address',
fieldtype: 'Check'
}
],
layout: [
// section 1
// events: {
// validate: (doc) => {
// }
// },
listSettings: {
getFields(list) {
return ['name', 'addressType'];
},
getRowHTML(list, data) {
return `<div class="col-11">${list.getNameHTML(data)} (${
data.addressType
})</div>`;
}
},
layout: [
// section 1
{
columns: [
{
columns: [
{
fields: [
"name", "addressType", "addressLine1",
"addressLine2", "city", "country", "state",
"postalCode"
]
},
{
fields: [
"emailAddress", "phone", "fax", "isPreferredBilling", "isShippingBilling"
]
}
]
fields: [
'name',
'addressType',
'addressLine1',
'addressLine2',
'city',
'country',
'state',
'postalCode'
]
},
{
fields: [
'emailAddress',
'phone',
'fax',
'isPreferredBilling',
'isShippingBilling'
]
}
]
}
]
}
]
};

View File

@ -1,138 +0,0 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
module.exports = {
"name": "Bill",
"doctype": "DocType",
"documentClass": require('./BillDocument'),
"isSingle": 0,
"isChild": 0,
"isSubmittable": 1,
"keywordFields": ["name", "supplier"],
"settings": "BillSettings",
"showTitle": true,
"fields": [
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date"
},
{
"fieldname": "supplier",
"label": "Supplier",
"fieldtype": "Link",
"target": "Party",
"required": 1,
getFilters: (query, control) => {
return {
keywords: ["like", query],
supplier: 1
}
}
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
getFilters: (query, control) => {
return {
keywords: ["like", query],
isGroup: 0,
accountType: "Payable"
}
}
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"childtype": "BillItem",
"required": true
},
{
"fieldname": "netTotal",
"label": "Net Total",
"fieldtype": "Currency",
formula: (doc) => doc.getSum('items', 'amount'),
"disabled": true
},
{
"fieldname": "taxes",
"label": "Taxes",
"fieldtype": "Table",
"childtype": "TaxSummary",
"disabled": true,
template: (doc, row) => {
return `<div class='row'>
<div class='col-6'><!-- empty left side --></div>
<div class='col-6'>${(doc.taxes || []).map(row => {
return `<div class='row'>
<div class='col-6'>${row.account} (${row.rate}%)</div>
<div class='col-6 text-right'>
${frappe.format(row.amount, 'Currency')}
</div>
</div>`
}).join('')}
</div></div>`;
}
},
{
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
formula: (doc) => doc.getGrandTotal(),
"disabled": true
},
{
"fieldname": "terms",
"label": "Terms",
"fieldtype": "Text"
}
],
layout: [
// section 1
{
columns: [
{ fields: [ "supplier", "account" ] },
{ fields: [ "date" ] }
]
},
// section 2
{ fields: [ "items" ] },
// section 3
{ fields: [ "netTotal", "taxes", "grandTotal" ] },
// section 4
{ fields: [ "terms" ] },
],
links: [
{
label: 'Make Payment',
condition: form => form.doc.submitted,
action: async form => {
const payment = await frappe.getNewDoc('Payment');
payment.party = form.doc.supplier,
payment.account = form.doc.account,
payment.for = [{referenceType: form.doc.doctype, referenceName: form.doc.name, amount: form.doc.grandTotal}]
const formModal = await frappe.desk.showFormModal('Payment', payment.name);
}
}
],
listSettings: {
getFields(list) {
return ['name', 'supplier', 'grandTotal', 'submitted'];
},
getRowHTML(list, data) {
return `<div class="col-3">${list.getNameHTML(data)}</div>
<div class="col-4 text-muted">${data.supplier}</div>
<div class="col-4 text-muted text-right">${frappe.format(data.grandTotal, "Currency")}</div>`;
}
}
}

View File

@ -1,4 +0,0 @@
const InvoiceDocument = require('../Invoice/InvoiceDocument');
const frappe = require('frappejs');
module.exports = class Bill extends InvoiceDocument { }

View File

@ -1,29 +0,0 @@
const frappe = require('frappejs');
const Bill = require('./BillDocument');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class BillServer extends Bill {
getPosting() {
let entries = new LedgerPosting({reference: this, party: this.supplier});
entries.credit(this.account, this.grandTotal);
for (let item of this.items) {
entries.debit(item.account, item.amount);
}
if (this.taxes) {
for (let tax of this.taxes) {
entries.debit(tax.account, tax.amount);
}
}
return entries;
}
async afterSubmit() {
await this.getPosting().post();
}
async afterRevert() {
await this.getPosting().postReverse();
}
}

View File

@ -1,67 +0,0 @@
module.exports = {
name: "BillItem",
doctype: "DocType",
isSingle: 0,
isChild: 1,
keywordFields: [],
layout: 'ratio',
fields: [
{
"fieldname": "item",
"label": "Item",
"fieldtype": "Link",
"target": "Item",
"required": 1,
width: 2
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text",
formula: (row, doc) => doc.getFrom('Item', row.item, 'description'),
hidden: 1
},
{
"fieldname": "quantity",
"label": "Quantity",
"fieldtype": "Float",
"required": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency",
"required": 1,
formula: (row, doc) => doc.getFrom('Item', row.item, 'rate')
},
{
fieldname: "account",
label: "Account",
hidden: 1,
fieldtype: "Link",
target: "Account",
formula: (row, doc) => doc.getFrom('Item', row.item, 'expenseAccount')
},
{
"fieldname": "tax",
"label": "Tax",
"fieldtype": "Link",
"target": "Tax",
formula: (row, doc) => doc.getFrom('Item', row.item, 'tax')
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"disabled": 1,
formula: (row, doc) => row.quantity * row.rate
},
{
"fieldname": "taxAmount",
"label": "Tax Amount",
"hidden": 1,
"fieldtype": "Text",
formula: (row, doc) => doc.getRowTax(row)
}
]
}

View File

@ -1,18 +0,0 @@
module.exports = {
"name": "BillSettings",
"label": "Bill Settings",
"doctype": "DocType",
"isSingle": 1,
"isChild": 0,
"keywordFields": [],
"fields": [
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"required": 1,
"default": "BILL"
}
]
}

View File

@ -0,0 +1,16 @@
module.exports = {
name: 'Color',
doctype: 'DocType',
fields: [
{
fieldname: 'name',
fieldtype: 'Data',
label: 'Color'
},
{
fieldname: 'hexvalue',
fieldtype: 'Data',
label: 'Hex Value'
}
]
};

View File

@ -0,0 +1,53 @@
module.exports = {
name: 'Currency',
label: 'Currency',
doctype: 'DocType',
isSingle: 0,
keywordFields: ['name', 'symbol'],
fields: [
{
fieldname: 'name',
label: 'Currency Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'fraction',
label: 'Fraction',
fieldtype: 'Data'
},
{
fieldname: 'fractionUnits',
label: 'Fraction Units',
fieldtype: 'Int'
},
{
label: 'Smallest Currency Fraction Value',
fieldname: 'smallestValue',
fieldtype: 'Currency'
},
{
label: 'Symbol',
fieldname: 'symbol',
fieldtype: 'Data'
},
{
fieldname: 'numberFormat',
fieldtype: 'Select',
label: 'Number Format',
options: [
'',
'#,###.##',
'#.###,##',
'# ###.##',
'# ###,##',
"#'###.##",
'#, ###.##',
'#,##,###.##',
'#,###.###',
'#.###',
'#,###'
]
}
]
};

View File

@ -0,0 +1,26 @@
module.exports = {
name: 'DashboardChart',
doctype: 'DocType',
isChild: 1,
fields: [
{
fieldname: 'account',
fieldtype: 'Link',
label: 'Account',
target: 'Account'
},
{
fieldname: 'type',
fieldtype: 'Select',
options: ['Bar', 'Line', 'Percentage'],
label: 'Chart Type',
defaultValue: 'Bar'
},
{
fieldname: 'color',
fieldtype: 'Link',
label: 'Color',
target: 'Color'
}
]
};

View File

@ -0,0 +1,14 @@
module.exports = {
name: 'Dashboard',
label: 'Dashboard Settings',
doctype: 'DocType',
isSingle: 1,
fields: [
{
fieldname: 'charts',
fieldtype: 'Table',
label: 'Charts',
childtype: 'DashboardChart'
}
]
};

View File

@ -0,0 +1,77 @@
const frappe = require('frappejs');
module.exports = {
name: 'GSTR3B',
label: 'GSTR 3B',
doctype: 'DocType',
documentClass: require('./GSTR3BDocument.js'),
print: {
printFormat: 'GSTR3B Print Format'
},
keywordFields: ['name', 'month', 'year'],
fields: [
{
fieldname: 'year',
label: 'Year',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'month',
label: 'Month',
fieldtype: 'Select',
options: [
'',
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
required: 1
},
{
fieldname: 'jsonData',
label: 'JSON Data',
fieldtype: 'Code',
formula: doc => doc.getJson(),
required: 1,
disabled: 1,
rows: 15
}
],
layout: [
{
columns: [{ fields: ['year', 'month', 'jsonData'] }]
}
],
links: [
{
label: 'Print PDF',
condition: form => !form.doc._notInserted,
action: async form => {
form.$router.push({
path: `/print/GSTR3B/${form.doc.name}`
});
}
},
{
label: 'Delete',
condition: form => !form.doc._notInserted,
action: async form => {
const doc = await frappe.getDoc('GSTR3B', form.doc.name);
await doc.delete();
form.$router.push({
path: `/list/GSTR3B`
});
}
}
]
};

View File

@ -0,0 +1,150 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
const { format } = require('./GSTR3BFormat');
module.exports = class GSTR3B extends BaseDocument {
async getData() {
const monthIndex = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
].indexOf(this.month);
const month = monthIndex + 1 > 9 ? monthIndex + 1 : `0${monthIndex + 1}`;
const lastDate = new Date(this.year, monthIndex + 1, 0).getDate();
const filters = {
date: [
'>=',
`${this.year}-${month}-01`,
'<=',
`${this.year}-${month}-${lastDate}`
]
};
const salesInvoices = frappe.db.getAll({
doctype: 'SalesInvoice',
filters,
fields: ['*']
});
const purchaseInvoices = frappe.db.getAll({
doctype: 'PurchaseInvoice',
filters,
fields: ['*']
});
const [gstr1Data, gstr2Data] = await Promise.all([
salesInvoices,
purchaseInvoices
]);
let gstr3bData = [[], []];
for (let ledgerEntry of gstr1Data) {
ledgerEntry.doctype = 'SalesInvoice';
gstr3bData[0].push(await this.makeGSTRow(ledgerEntry));
}
for (let ledgerEntry of gstr2Data) {
ledgerEntry.doctype = 'PurchaseInvoice';
gstr3bData[1].push(await this.makeGSTRow(ledgerEntry));
}
return gstr3bData;
}
async makeGSTRow(ledgerEntry) {
let row = {};
ledgerEntry = await frappe.getDoc(ledgerEntry.doctype, ledgerEntry.name);
let party = await frappe.getDoc(
'Party',
ledgerEntry.customer || ledgerEntry.supplier
);
if (party.address) {
let addressDetails = await frappe.getDoc('Address', party.address);
row.place = addressDetails.state || '';
}
row.gstin = party.gstin;
row.partyName = ledgerEntry.customer || ledgerEntry.supplier;
row.invNo = ledgerEntry.name;
row.invDate = ledgerEntry.date;
row.rate = 0;
row.inState = true;
row.reverseCharge = !party.gstin ? 'Y' : 'N';
ledgerEntry.taxes.forEach(tax => {
row.rate += tax.rate;
const taxAmt = (tax.rate * ledgerEntry.netTotal) / 100;
if (tax.account === 'IGST') row.igstAmt = taxAmt;
if (tax.account === 'IGST') row.inState = false;
if (tax.account === 'CGST') row.cgstAmt = taxAmt;
if (tax.account === 'SGST') row.sgstAmt = taxAmt;
if (tax.account === 'Nil Rated') row.nilRated = true;
if (tax.account === 'Exempt') row.exempt = true;
if (tax.account === 'Non GST') row.nonGST = true;
});
row.invAmt = ledgerEntry.grandTotal;
row.taxVal = ledgerEntry.netTotal;
return row;
}
async createJson(data) {
let jsonData = JSON.parse(JSON.stringify(format));
for (let ledgerEntry of data[0]) {
if (ledgerEntry.rate > 0) {
jsonData['sup_details']['osup_det']['samt'] += ledgerEntry.sgstAmt || 0;
jsonData['sup_details']['osup_det']['camt'] += ledgerEntry.cgstAmt || 0;
jsonData['sup_details']['osup_det']['iamt'] += ledgerEntry.igstAmt || 0;
jsonData['sup_details']['osup_det']['txval'] += ledgerEntry.taxVal;
}
if (ledgerEntry.rate === 0) {
jsonData['sup_details']['osup_zero']['txval'] += ledgerEntry.taxVal;
}
if (ledgerEntry.nilRated || ledgerEntry.exempt) {
jsonData['sup_details']['osup_nil_exmp']['txval'] += ledgerEntry.taxVal;
}
if (ledgerEntry.nonGST) {
jsonData['sup_details']['osup_nongst']['txval'] += ledgerEntry.taxVal;
}
if (!ledgerEntry.inState && !ledgerEntry.gstin) {
jsonData['inter_sup']['unreg_details'].push({
pos: ledgerEntry.place,
txval: ledgerEntry.taxVal,
iAmt: ledgerEntry.igstAmt || 0
});
}
}
for (let ledgerEntry of data[1]) {
if (ledgerEntry.reverseCharge === 'Y') {
jsonData['sup_details']['isup_rev']['samt'] += ledgerEntry.sgstAmt || 0;
jsonData['sup_details']['isup_rev']['camt'] += ledgerEntry.cgstAmt || 0;
jsonData['sup_details']['isup_rev']['iamt'] += ledgerEntry.igstAmt || 0;
jsonData['sup_details']['isup_rev']['txval'] += ledgerEntry.taxVal;
}
if (ledgerEntry.nilRated || ledgerEntry.exempt) {
jsonData['inward_sup']['isup_details'][0][
ledgerEntry.inState ? 'intra' : 'inter'
] += ledgerEntry.taxVal;
}
if (ledgerEntry.nonGST) {
jsonData['inward_sup']['isup_details'][0][
ledgerEntry.inState ? 'intra' : 'inter'
] += ledgerEntry.taxVal;
}
}
return jsonData;
}
async getJson() {
if (this.year && this.month) {
const data = await this.getData();
const json = await this.createJson(data);
return JSON.stringify(json, undefined, 2);
}
}
};

View File

@ -0,0 +1,390 @@
const format = {
gstin: '',
ret_period: '',
inward_sup: {
isup_details: [
{
ty: 'GST',
intra: 0,
inter: 0
},
{
ty: 'NONGST',
inter: 0,
intra: 0
}
]
},
sup_details: {
osup_zero: {
csamt: 0,
txval: 0,
iamt: 0
},
osup_nil_exmp: {
txval: 0
},
osup_det: {
samt: 0,
csamt: 0,
txval: 0,
camt: 0,
iamt: 0
},
isup_rev: {
samt: 0,
csamt: 0,
txval: 0,
camt: 0,
iamt: 0
},
osup_nongst: {
txval: 0
}
},
inter_sup: {
unreg_details: [],
comp_details: [],
uin_details: []
},
itc_elg: {
itc_avl: [
{
csamt: 0,
samt: 0,
ty: 'IMPG',
camt: 0,
iamt: 0
},
{
csamt: 0,
samt: 0,
ty: 'IMPS',
camt: 0,
iamt: 0
},
{
samt: 0,
csamt: 0,
ty: 'ISRC',
camt: 0,
iamt: 0
},
{
ty: 'ISD',
iamt: 0,
camt: 0,
samt: 0,
csamt: 0
},
{
samt: 0,
csamt: 0,
ty: 'OTH',
camt: 0,
iamt: 0
}
],
itc_rev: [
{
ty: 'RUL',
iamt: 0,
camt: 0,
samt: 0,
csamt: 0
},
{
ty: 'OTH',
iamt: 0,
camt: 0,
samt: 0,
csamt: 0
}
],
itc_net: {
samt: 0,
csamt: 0,
camt: 0,
iamt: 0
},
itc_inelg: [
{
ty: 'RUL',
iamt: 0,
camt: 0,
samt: 0,
csamt: 0
},
{
ty: 'OTH',
iamt: 0,
camt: 0,
samt: 0,
csamt: 0
}
]
}
};
function generateHTML(data) {
let template = `
<div class="p-5 m-5" style="font-size: 14px !important">
<div>
<h3 class="text-center">GSTR3B-Form</h3>
<h5>GSTIN: &nbsp ${data.gstin}</h5>
<h5>Period: &nbsp ${data.ret_period}</h5>
</div>
<h5>3.1&nbsp&nbspDetails of Outward Supplies and inward supplies liable to reverse charge</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Nature Of Supplies</th>
<th>Total Taxable value</th>
<th>Integrated Tax</th>
<th>Central Tax</th>
<th>State/UT Tax</th>
<th>Cess</th>
</tr>
</thead>
<tbody>
<tr>
<td>(a) Outward taxable supplies(other than zero rated, nil rated and exempted</td>
<td class="right">${data.sup_details.osup_det.txval}</td>
<td class="right">${data.sup_details.osup_det.iamt}</td>
<td class="right">${data.sup_details.osup_det.camt}</td>
<td class="right">${data.sup_details.osup_det.samt}</td>
<td class="right">${data.sup_details.osup_det.csamt}</td>
</tr>
<tr>
<td>(b) Outward taxable supplies(zero rated)</td>
<td class="right">${data.sup_details.osup_zero.txval}</td>
<td class="right">${data.sup_details.osup_zero.iamt}</td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
<td class="right">${data.sup_details.osup_zero.csamt}</td>
</tr>
<tr>
<td>(b) Other outward supplies(Nil rated,Exempted)</td>
<td class="right">${data.sup_details.osup_nil_exmp.txval}</td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
<tr>
<td>(d) Inward Supplies(liable to reverse charge</td>
<td class="right">${data.sup_details.isup_rev.txval}</td>
<td class="right">${data.sup_details.isup_rev.iamt}</td>
<td class="right">${data.sup_details.isup_rev.camt}</td>
<td class="right">${data.sup_details.isup_rev.samt}</td>
<td class="right">${data.sup_details.isup_rev.csamt}</td>
</tr>
<tr>
<td>(e) Non-GST outward supplies</td>
<td class="right">${data.sup_details.osup_nongst.txval}</td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
<td style="background-color:#d9d9d9;"></td>
</tr>
</tbody>
</table>
<h5>
3.2&nbsp&nbspOf the supplies shown in 3.1 (a) above, details of inter-State supplies made to unregisterd
persons, composition taxable persons and UIN holders
</h5>
<table class="table table-bordered">
<thead>
<tr>
<th></th>
<th>Place Of Supply (State/UT)</th>
<th>Total Taxable Value</th>
<th>Amount of Integrated Tax</th>
</tr>
</thead>
<tbody>
<tr>
<td>Supplies made to Unregistered Persons</td>
<td class="right">`;
for (let row of data.inter_sup.unreg_details) {
if (row) template += row.pos + '<br>';
}
template += '</td><td class="right">';
for (let row of data.inter_sup.unreg_details) {
if (row) template += row.txval + '<br>';
}
template += '</td><td class="right">';
for (let row of data.inter_sup.unreg_details) {
if (row) template += row.iamt + '<br>';
}
template +=
'</td></tr><tr><td>Supplies made to Composition Taxable Persons</td><td class="right">';
for (let row of data.inter_sup.comp_details) {
if (row) template += row.pos + '<br>';
}
template += '</td><td class="right">';
for (let row of data.inter_sup.comp_details) {
if (row) template += row.txval + '<br>';
}
template += '</td><td class="right">';
for (let row of data.inter_sup.comp_details) {
if (row) template += row.iamt + '<br>';
}
template +=
'</td></tr><tr><td>Supplies made to UIN holders</td><td class="right">';
for (let row of data.inter_sup.uin_details) {
if (row) template += row.pos + '<br>';
}
template += '</td><td class="right">';
for (let row of data.inter_sup.uin_details) {
if (row) template += row.txval + '<br>';
}
template += '</td><td class="right">';
for (let row of data.inter_sup.uin_details) {
if (row) template += row.iamt + '<br>';
}
template += `</td>
</tr>
</tbody>
</table>
<h5>4. &nbsp Eligible ITC</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Details</th>
<th>Integrated Tax</th>
<th>Central Tax</th>
<th>State/UT tax</th>
<th>Cess</th>
</tr>
</thead>
<tbody>
<tr>
<td><b>(A) ITC Available (whether in full op part)</b></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>&nbsp (1) Import of goods </td>
<td class="right">${data.itc_elg.itc_avl[0].iamt}</td>
<td class="right">${data.itc_elg.itc_avl[0].camt}</td>
<td class="right">${data.itc_elg.itc_avl[0].samt}</td>
<td class="right">${data.itc_elg.itc_avl[0].csamt}</td>
</tr>
<tr>
<td>&nbsp (2) Import of services</td>
<td class="right">${data.itc_elg.itc_avl[1].iamt}</td>
<td class="right">${data.itc_elg.itc_avl[1].camt}</td>
<td class="right">${data.itc_elg.itc_avl[1].samt}</td>
<td class="right">${data.itc_elg.itc_avl[1].csamt}</td>
</tr>
<tr>
<td>&nbsp (3) Inward supplies liable to reverse charge (other than 1 & 2 above)</td>
<td class="right">${data.itc_elg.itc_avl[2].iamt}</td>
<td class="right">${data.itc_elg.itc_avl[2].camt}</td>
<td class="right">${data.itc_elg.itc_avl[2].samt}</td>
<td class="right">${data.itc_elg.itc_avl[2].csamt}</td>
</tr>
<tr>
<td>&nbsp (4) Inward supplies from ISD</td>
<td class="right">${data.itc_elg.itc_avl[3].iamt}</td>
<td class="right">${data.itc_elg.itc_avl[3].camt}</td>
<td class="right">${data.itc_elg.itc_avl[3].samt}</td>
<td class="right">${data.itc_elg.itc_avl[3].csamt}</td>
</tr>
<tr>
<td>&nbsp (5) All other ITC</td>
<td class="right">${data.itc_elg.itc_avl[4].iamt}</td>
<td class="right">${data.itc_elg.itc_avl[4].camt}</td>
<td class="right">${data.itc_elg.itc_avl[4].samt}</td>
<td class="right">${data.itc_elg.itc_avl[4].csamt}</td>
</tr>
<tr>
<td><b>(B) ITC Reversed</b></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>&nbsp (1) As per rules 42 & 43 of CGST Rules</td>
<td class="right">${data.itc_elg.itc_rev[0].iamt}</td>
<td class="right">${data.itc_elg.itc_rev[0].camt}</td>
<td class="right">${data.itc_elg.itc_rev[0].samt}</td>
<td class="right">${data.itc_elg.itc_rev[0].csamt}</td>
</tr>
<tr>
<td>&nbsp (2) Others</td>
<td class="right">${data.itc_elg.itc_rev[1].iamt}</td>
<td class="right">${data.itc_elg.itc_rev[1].camt}</td>
<td class="right">${data.itc_elg.itc_rev[1].samt}</td>
<td class="right">${data.itc_elg.itc_rev[1].csamt}</td>
</tr>
<tr>
<td><b>(C) Net ITC Available(A) - (B)</b></td>
<td class="right">${data.itc_elg.itc_net.iamt}</td>
<td class="right">${data.itc_elg.itc_net.camt}</td>
<td class="right">${data.itc_elg.itc_net.samt}</td>
<td class="right">${data.itc_elg.itc_net.csamt}</td>
</tr>
<tr>
<td><b>(D) Ineligible ITC</b></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>&nbsp (1) As per section 17(5)</td>
<td class="right">${data.itc_elg.itc_inelg[0].iamt}</td>
<td class="right">${data.itc_elg.itc_inelg[0].camt}</td>
<td class="right">${data.itc_elg.itc_inelg[0].samt}</td>
<td class="right">${data.itc_elg.itc_inelg[0].csamt}</td>
</tr>
<tr>
<td>&nbsp (2) Others</td>
<td class="right">${data.itc_elg.itc_inelg[1].iamt}</td>
<td class="right">${data.itc_elg.itc_inelg[1].camt}</td>
<td class="right">${data.itc_elg.itc_inelg[1].samt}</td>
<td class="right">${data.itc_elg.itc_inelg[1].csamt}</td>
</tr>
</tbody>
</table>
<h5>5. &nbsp&nbsp Values of exempt, nil rated and non-GST inward supplies</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Nature of Supplies</th>
<th>Inter-State Supplies</th>
<th>Intra-State Supplies</th>
</tr>
</thead>
<tbody>
<tr>
<td>From a supplier under composition scheme, Exempt and Nil rated</td>
<td class="right">${data.inward_sup.isup_details[0].inter}</td>
<td class="right">${data.inward_sup.isup_details[0].intra}</td>
</tr>
<tr>
<td>Non GST Inward Supplies</td>
<td class="right">${data.inward_sup.isup_details[1].inter}</td>
<td class="right">${data.inward_sup.isup_details[1].intra}</td>
</tr>
</tbody>
</table>
</div>`;
return template;
}
module.exports = {
format
};

View File

@ -0,0 +1,7 @@
import { _ } from 'frappejs/utils';
export default {
doctype: 'GSTR3B',
title: _('GSTR 3B Report'),
columns: ['year', 'month']
};

View File

@ -0,0 +1,303 @@
<template>
<div>
<div class="row no-gutters">
<div class="col-8 mx-auto text-right mt-4">
<f-button primary @click="$emit('makePDF', $refs.printComponent.innerHTML)">{{ _('PDF') }}</f-button>
</div>
</div>
<div ref="printComponent" class="col-8 bg-white mt-4 mx-auto border shadow">
<div class="print-format" style="padding: 3.5rem; font-size: 8pt !important;">
<div>
<h3 class="text-center">GSTR3B-Form</h3>
<h5>GSTIN: &nbsp; {{ jsonData.gstin }}</h5>
<h5>Period: &nbsp; {{ jsonData.ret_period }}</h5>
</div>
<h5>3.1&nbsp;&nbsp;Details of Outward Supplies and inward supplies liable to reverse charge</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Nature Of Supplies</th>
<th>Total Taxable value</th>
<th>Integrated Tax</th>
<th>Central Tax</th>
<th>State/UT Tax</th>
<th>Cess</th>
</tr>
</thead>
<tbody>
<tr>
<td>(a) Outward taxable supplies(other than zero rated, nil rated and exempted</td>
<td class="right">{{ jsonData.sup_details.osup_det.txval }}</td>
<td class="right">{{ jsonData.sup_details.osup_det.iamt }}</td>
<td class="right">{{ jsonData.sup_details.osup_det.camt }}</td>
<td class="right">{{ jsonData.sup_details.osup_det.samt }}</td>
<td class="right">{{ jsonData.sup_details.osup_det.csamt }}</td>
</tr>
<tr>
<td>(b) Outward taxable supplies(zero rated)</td>
<td class="right">{{ jsonData.sup_details.osup_zero.txval }}</td>
<td class="right">{{ jsonData.sup_details.osup_zero.iamt }}</td>
<td class="disabled"></td>
<td class="disabled"></td>
<td class="right">{{ jsonData.sup_details.osup_zero.csamt }}</td>
</tr>
<tr>
<td>(b) Other outward supplies(Nil rated,Exempted)</td>
<td class="right">{{ jsonData.sup_details.osup_nil_exmp.txval }}</td>
<td class="disabled"></td>
<td class="disabled"></td>
<td class="disabled"></td>
<td class="disabled"></td>
</tr>
<tr>
<td>(d) Inward Supplies(liable to reverse charge</td>
<td class="right">{{ jsonData.sup_details.isup_rev.txval }}</td>
<td class="right">{{ jsonData.sup_details.isup_rev.iamt }}</td>
<td class="right">{{ jsonData.sup_details.isup_rev.camt }}</td>
<td class="right">{{ jsonData.sup_details.isup_rev.samt }}</td>
<td class="right">{{ jsonData.sup_details.isup_rev.csamt }}</td>
</tr>
<tr>
<td>(e) Non-GST outward supplies</td>
<td class="right">{{ jsonData.sup_details.osup_nongst.txval }}</td>
<td class="disabled"></td>
<td class="disabled"></td>
<td class="disabled"></td>
<td class="disabled"></td>
</tr>
</tbody>
</table>
<h5>3.2&nbsp;&nbsp;Of the supplies shown in 3.1 (a) above, details of inter-State supplies made to unregisterd persons, composition taxable persons and UIN holders</h5>
<table class="table table-bordered">
<thead>
<tr>
<th></th>
<th>Place Of Supply (State/UT)</th>
<th>Total Taxable Value</th>
<th>Amount of Integrated Tax</th>
</tr>
</thead>
<tbody>
<tr>
<td>Supplies made to Unregistered Persons</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.unreg_details" :key="i">
<p>{{ row.pos }}</p>
</div>
</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.unreg_details" :key="i">
<p>{{ row.txval }}</p>
</div>
</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.unreg_details" :key="i">
<p>{{ row.iamt }}</p>
</div>
</td>
</tr>
<tr>
<td>Suppliies made to Composition Taxable Persons</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.comp_details" :key="i">
<p>{{ row.pos }}</p>
</div>
</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.comp_details" :key="i">
<p>{{ row.txval }}</p>
</div>
</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.comp_details" :key="i">
<p>{{ row.iamt }}</p>
</div>
</td>
</tr>
<tr>
<td>Supplies made to UIN holders</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.uin_details" :key="i">
<p>{{ row.pos }}</p>
</div>
</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.uin_details" :key="i">
<p>{{ row.txval }}</p>
</div>
</td>
<td class="right">
<div v-for="(row, i) in jsonData.inter_sup.uin_details" :key="i">
<p>{{ row.iamt }}</p>
</div>
</td>
</tr>
</tbody>
</table>
<h5>4. &nbsp; Eligible ITC</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Details</th>
<th>Integrated Tax</th>
<th>Central Tax</th>
<th>State/UT tax</th>
<th>Cess</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<b>(A) ITC Available (whether in full op part)</b>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>&nbsp; (1) Import of goods</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[0].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[0].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[0].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[0].csamt }}</td>
</tr>
<tr>
<td>&nbsp; (2) Import of services</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[1].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[1].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[1].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[1].csamt }}</td>
</tr>
<tr>
<td>&nbsp; (3) Inward supplies liable to reverse charge (other than 1 & 2 above)</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[2].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[2].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[2].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[2].csamt }}</td>
</tr>
<tr>
<td>&nbsp; (4) Inward supplies from ISD</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[3].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[3].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[3].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[3].csamt }}</td>
</tr>
<tr>
<td>&nbsp; (5) All other ITC</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[4].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[4].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[4].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_avl[4].csamt }}</td>
</tr>
<tr>
<td>
<b>(B) ITC Reversed</b>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>&nbsp; (1) As per rules 42 & 43 of CGST Rules</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[0].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[0].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[0].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[0].csamt }}</td>
</tr>
<tr>
<td>&nbsp; (2) Others</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[1].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[1].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[1].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_rev[1].csamt }}</td>
</tr>
<tr>
<td>
<b>(C) Net ITC Available(A) - (B)</b>
</td>
<td class="right">{{ jsonData.itc_elg.itc_net.iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_net.camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_net.samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_net.csamt }}</td>
</tr>
<tr>
<td>
<b>(D) Ineligible ITC</b>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>&nbsp; (1) As per section 17(5)</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[0].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[0].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[0].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[0].csamt }}</td>
</tr>
<tr>
<td>&nbsp; (2) Others</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[1].iamt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[1].camt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[1].samt }}</td>
<td class="right">{{ jsonData.itc_elg.itc_inelg[1].csamt }}</td>
</tr>
</tbody>
</table>
<h5>5. &nbsp;&nbsp; Values of exempt, nil rated and non-GST inward supplies</h5>
<table class="table table-bordered">
<thead>
<tr>
<th>Nature of Supplies</th>
<th>Inter-State Supplies</th>
<th>Intra-State Supplies</th>
</tr>
</thead>
<tbody>
<tr>
<td>From a supplier under composition scheme, Exempt and Nil rated</td>
<td class="right">{{ jsonData.inward_sup.isup_details[0].inter }}</td>
<td class="right">{{ jsonData.inward_sup.isup_details[0].intra }}</td>
</tr>
<tr>
<td>Non GST Inward Supplies</td>
<td class="right">{{ jsonData.inward_sup.isup_details[1].inter }}</td>
<td class="right">{{ jsonData.inward_sup.isup_details[1].intra }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'GSTR3BPrintView',
props: ['doc'],
computed: {
jsonData() {
return JSON.parse(this.doc.jsonData);
}
}
};
</script>
<style>
.print-format {
}
.disabled {
background-color: #d9d9d9;
}
.right {
text-align: right;
}
</style>

View File

@ -0,0 +1,19 @@
const GSTR3B = require('./GSTR3BDocument');
module.exports = class GSTR3BServer extends GSTR3B {
async validate() {
if (this.month.length === 0 || this.year.length != 4) {
frappe.call({
method: 'show-dialog',
args: {
title: 'Invalid Entry',
message: `Month or Year is not valid`
}
});
throw new Error();
}
}
async beforeInsert() {
this.name = `${this.doctype} Report ${this.month} ${this.year}`;
}
};

View File

@ -1,156 +0,0 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
module.exports = {
name: 'Invoice',
doctype: 'DocType',
documentClass: require('./InvoiceDocument.js'),
print: {
printFormat: 'Standard Invoice Format'
},
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: ['name', 'customer'],
settings: 'InvoiceSettings',
showTitle: true,
fields: [
{
fieldname: 'date',
label: 'Date',
fieldtype: 'Date',
// default: (new Date()).toISOString()
},
{
fieldname: 'customer',
label: 'Customer',
fieldtype: 'Link',
target: 'Party',
required: 1,
getFilters: (query) => {
return {
customer: 1
};
}
},
{
fieldname: 'account',
label: 'Account',
fieldtype: 'Link',
target: 'Account',
formula: (doc) => doc.getFrom('Party', doc.customer , 'defaultAccount'),
getFilters: (query, control) => {
return {
isGroup: 0,
accountType: 'Receivable'
};
}
},
{
fieldname: 'items',
label: 'Items',
fieldtype: 'Table',
childtype: 'InvoiceItem',
required: true
},
{
fieldname: 'netTotal',
label: 'Net Total',
fieldtype: 'Currency',
formula: (doc) => doc.getSum('items', 'amount'),
disabled: true,
readOnly: 1
},
{
fieldname: 'taxes',
label: 'Taxes',
fieldtype: 'Table',
childtype: 'TaxSummary',
readOnly: 1,
template: (doc, row) => {
return `<div class='row'>
<div class='col-6'></div>
<div class='col-6'>
<div class='row' v-for='row in value'>
<div class='col-6'>{{row.account}} ({{row.rate}}%)</div>
<div class='col-6 text-right'>
{{frappe.format(row.amount, 'Currency')}}
</div>
</div>
</div>
</div>`;
}
},
{
fieldname: 'grandTotal',
label: 'Grand Total',
fieldtype: 'Currency',
formula: (doc) => doc.getGrandTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'outstandingAmount',
label: 'Outstanding Amount',
fieldtype: 'Currency',
hidden: 1
},
{
fieldname: 'terms',
label: 'Terms',
fieldtype: 'Text'
}
],
layout: [
// section 1
{
columns: [
{ fields: ['customer', 'account'] },
{ fields: ['date'] }
]
},
// section 2
{
columns: [
{ fields: ['items'] }
]
},
// section 3
{
columns: [
{ fields: ['netTotal', 'taxes', 'grandTotal'] }
]
},
// section 4
{
columns: [
{ fields: ['terms'] }
]
}
],
links: [
utils.ledgerLink,
{
label: 'Make Payment',
condition: form => form.doc.submitted && form.doc.outstandingAmount !== 0.0,
action: async form => {
const payment = await frappe.getNewDoc('Payment');
payment.party = form.doc.customer;
payment.account = form.doc.account;
payment.for = [{ referenceType: form.doc.doctype, referenceName: form.doc.name, amount: form.doc.grandTotal }];
payment.on('afterInsert', async () => {
form.$formModal.close();
const _payment = await frappe.getDoc('Payment', payment.name);
await _payment.submit();
})
await form.$formModal.open(payment);
}
}
]
};

View File

@ -1,72 +0,0 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
module.exports = class Invoice extends BaseDocument {
async getRowTax(row) {
if (row.tax) {
let tax = await this.getTax(row.tax);
let taxAmount = [];
for (let d of (tax.details || [])) {
taxAmount.push({account: d.account, rate: d.rate, amount: row.amount * d.rate / 100});
}
return JSON.stringify(taxAmount);
} else {
return '';
}
}
async getTax(tax) {
if (!this._taxes) this._taxes = {};
if (!this._taxes[tax]) this._taxes[tax] = await frappe.getDoc('Tax', tax);
return this._taxes[tax];
}
makeTaxSummary() {
if (!this.taxes) this.taxes = [];
// reset tax amount
this.taxes.map(d => { d.amount = 0; d.rate = 0; });
// calculate taxes
for (let row of this.items) {
if (row.taxAmount) {
let taxAmount = JSON.parse(row.taxAmount);
for (let rowTaxDetail of taxAmount) {
let found = false;
// check if added in summary
for (let taxDetail of this.taxes) {
if (taxDetail.account === rowTaxDetail.account) {
taxDetail.rate = rowTaxDetail.rate;
taxDetail.amount += rowTaxDetail.amount;
found = true;
}
}
// add new row
if (!found) {
this.taxes.push({
account: rowTaxDetail.account,
rate: rowTaxDetail.rate,
amount: rowTaxDetail.amount
});
}
}
}
}
// clear no taxes
this.taxes = this.taxes.filter(d => d.amount);
}
getGrandTotal() {
this.makeTaxSummary();
let grandTotal = this.netTotal;
if (this.taxes) {
for (let row of this.taxes) {
grandTotal += row.amount;
}
}
return grandTotal;
}
}

View File

@ -1,61 +0,0 @@
<template>
<component :themeColor="color" :font="fontFamily" :is="invoiceTemplate" v-if="doc" :doc="doc"/>
</template>
<script>
import InvoiceTemplate1 from '@/../models/doctype/Invoice/Templates/InvoiceTemplate1';
import InvoiceTemplate2 from '@/../models/doctype/Invoice/Templates/InvoiceTemplate2';
import InvoiceTemplate3 from '@/../models/doctype/Invoice/Templates/InvoiceTemplate3';
const invoiceTemplates = {
'Basic I': InvoiceTemplate1,
'Basic II': InvoiceTemplate2,
'Modern': InvoiceTemplate3
};
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'template', 'font'],
data() {
return {
color: undefined,
fontFamily: undefined,
invoiceTemplate: undefined
};
},
watch: {
themeColor: async function() {
await this.loadInvoice();
},
font: async function() {
await this.loadInvoice();
},
template: async function() {
await this.loadInvoice();
}
},
async created() {
await this.loadInvoice();
},
methods: {
async loadInvoice() {
this.color = this.themeColor !== undefined ? this.themeColor : await this.getColor();
this.fontFamily = this.font !== undefined ? this.font : await this.getFont();
let template = this.template !== undefined ? this.template : await this.getTemplate();
let templateFile = invoiceTemplates[template];
this.invoiceTemplate = templateFile;
},
async getTemplate() {
let invoiceSettings = await frappe.getDoc('InvoiceSettings');
return invoiceSettings.template;
},
async getColor() {
let invoiceSettings = await frappe.getDoc('InvoiceSettings');
return invoiceSettings.themeColor;
},
async getFont() {
let invoiceSettings = await frappe.getDoc('InvoiceSettings');
return invoiceSettings.font;
}
}
};
</script>

View File

@ -1,28 +0,0 @@
const Invoice = require('./InvoiceDocument');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class InvoiceServer extends Invoice {
getPosting() {
let entries = new LedgerPosting({ reference: this, party: this.customer });
entries.debit(this.account, this.grandTotal);
for (let item of this.items) {
entries.credit(item.account, item.amount);
}
if (this.taxes) {
for (let tax of this.taxes) {
entries.credit(tax.account, tax.amount);
}
}
return entries;
}
async afterSubmit() {
await this.getPosting().post();
}
async afterRevert() {
await this.getPosting().postReverse();
}
};

View File

@ -1,108 +0,0 @@
<template>
<div :style="$.font" style="font-family: sans-serif;">
<div class="row no-gutters pl-5 pr-5 mt-5">
<div :style="$.regularFontSize" class="col-6">
<company-address />
</div>
<div :style="$.regularFontSize" class="col-6 text-right">
<h2 :style="$.headerFontColor">INVOICE</h2>
<p :style="$.paraStyle"><strong>{{ doc.name }}</strong></p>
<p :style="$.paraStyle">{{ frappe.format(doc.date, 'Date') }}</p>
</div>
</div>
<div class="row pl-5 mt-5">
<div :style="$.regularFontSize" class="col-6 mt-1">
<customer-address :customer="doc.customer" />
</div>
</div>
<div :style="$.regularFontSize" class="row pl-5 pr-5 mt-5">
<div class="col-12">
<table class="table p-0">
<thead>
<tr :style="$.showBorderBottom">
<th :style="$.hideBorderTop" class="text-left pl-0" style="width: 10%">{{ _("NO") }}</th>
<th :style="$.hideBorderTop" class="text-left" style="width: 60%">{{ _("ITEM") }}</th>
<th :style="$.hideBorderTop" class="text-left pl-0" style="width: 20%">{{ _("RATE") }}</th>
<th :style="$.hideBorderTop" class="text-left" style="width: 15%">{{ _("QTY") }}</th>
<th :style="$.hideBorderTop" class="text-right pr-1" style="width: 20%">{{ _("AMOUNT") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in doc.items" :key="row.idx">
<td class="text-left pl-1">{{ row.idx + 1 }}</td>
<td class="text-left">{{ row.item }}</td>
<td class="text-left pl-0">{{ frappe.format(row.rate, 'Currency') }}</td>
<td class="text-left">{{ row.quantity }}</td>
<td class="text-right pr-1">{{ frappe.format(row.amount, 'Currency') }}</td>
</tr>
<tr>
<td colspan="2" class="text-left pl-1"></td>
<td colspan="2" :style="$.bold" class="text-left pl-0">SUBTOTAL</td>
<td :style="$.bold" class="text-right pr-1">{{ frappe.format(doc.netTotal, 'Currency') }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td colspan="2" :style="$.bold" class="text-left pl-0">{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.bold" class="text-right pr-1">{{ frappe.format(tax.amount, 'Currency') }}</td>
</tr>
<tr>
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td colspan="2" :style="[$.bold, $.mediumFontSize, $.showBorderTop]" class="text-left pl-0">TOTAL</td>
<td :style="[$.bold, $.mediumFontSize, $.showBorderTop]" class="text-right pr-1" style="color: green;">
{{ frappe.format(doc.grandTotal, 'Currency') }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row pl-5 pr-5 mt-5">
<div :style="$.regularFontSize" class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showBorderBottom]" ><td :style="$.hideBorderTop" class="pl-0">NOTICE</td></tr>
<tr><td class="pl-0">{{ doc.terms }}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoiceTemplate1',
components: {
CompanyAddress,
CustomerAddress
},
props: ['doc', 'themeColor', 'font'],
data() {
return {
$: Styles
}
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.headerFontColor.color = this.themeColor;
this.$.showBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.showBorderTop.borderTop = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
}
</script>

View File

@ -1,117 +0,0 @@
<template>
<div :style="[$.regularFontSize, $.font]" style="font-family: sans-serif;">
<div class="row no-gutters p-5" :style="$.headerColor">
<div class="col-8 text-left">
<h1>INVOICE</h1>
</div>
<div class="col-4 text-right">
<company-address />
</div>
</div>
<div class="row p-5 mt-4">
<div class="col-4">
<customer-address :customer="doc.customer" />
</div>
<div class="col-4">
<p :style="[$.bold, $.mediumFontSize]">Invoice Number</p>
<p :style="$.paraStyle">{{ doc.name }}</p><br>
<p :style="[$.bold, $.mediumFontSize]">Date</p>
<p :style="$.paraStyle">{{doc.date}}</p>
</div>
<div class="col-4 text-right">
<p :style="[$.bold, $.mediumFontSize]">Invoice Total</p>
<h2 :style="$.fontColor">{{ frappe.format(doc.grandTotal, 'Currency') }}</h2>
</div>
</div>
<div class="row pl-5 pr-5 mt-3">
<div class="col-12">
<table class="table table-borderless p-0">
<thead>
<tr :style="[$.showBorderTop, $.fontColor]">
<th class="text-left pl-0" style="width: 10%">{{ _("NO") }}</th>
<th class="text-left" style="width: 60%">{{ _("ITEM") }}</th>
<th class="text-left pl-0" style="width: 20%">{{ _("RATE") }}</th>
<th class="text-left" style="width: 15%">{{ _("QTY") }}</th>
<th class="text-right pr-1" style="width: 20%">{{ _("AMOUNT") }}</th>
</tr>
</thead>
<tbody>
<tr :style="$.showBorderBottom" v-for="row in doc.items" :key="row.idx">
<td class="text-left pl-1">{{ row.idx + 1 }}</td>
<td class="text-left">{{ row.item }}</td>
<td class="text-left pl-0">{{ frappe.format(row.rate, 'Currency') }}</td>
<td class="text-left">{{ row.quantity }}</td>
<td class="text-right pr-1">{{ frappe.format(row.amount, 'Currency') }}</td>
</tr>
<tr><td colspan="5" style="padding: 4%"></td></tr>
<tr>
<td colspan="2" class="text-left pl-1"></td>
<td colspan="2" :style="[$.bold, $.fontColor]" class="text-left pl-0">SUBTOTAL</td>
<td :style="$.bold" class="text-right pr-1">{{ frappe.format(doc.netTotal, 'Currency') }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td colspan="2" :style="[$.bold, $.fontColor]" class="text-left pl-0">{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.bold" class="text-right pr-1">{{ frappe.format(tax.amount, 'Currency') }}</td>
</tr>
<tr>
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td colspan="2" :style="[$.bold, $.fontColor, $.mediumFontSize]" class="text-left pl-0">TOTAL</td>
<td :style="[$.bold, $.mediumFontSize]" class="text-right pr-1">{{ frappe.format(doc.grandTotal, 'Currency') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row pl-5 pr-5 mt-5">
<div class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showNoticeBorderBottom]" ><td :style="$.hideBorderTop" class="pl-0">NOTICE</td></tr>
<tr><td class="pl-0">{{ doc.terms }}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'font'],
components: {
CompanyAddress,
CustomerAddress
},
data() {
return {
$: Styles,
}
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.fontColor.color = this.themeColor;
this.$.headerColor.backgroundColor = this.themeColor;
this.$.showBorderBottom.borderBottom = '0.1rem solid #e0e0d1';
this.$.showNoticeBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.showBorderTop.borderTop = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
}
</script>

View File

@ -1,113 +0,0 @@
<template>
<div :style="[$.regularFontSize, $.font]" style="font-family: sans-serif;">
<div class="row no-gutters mt-5">
<div class="col-6" :style="$.bgColor"></div>
<div class="col-4 text-center" style="vertical-align: middle">
<h1>INVOICE</h1>
</div>
<div class="col-2" :style="$.bgColor"></div>
</div>
<div class="row no-gutters mt-5">
<div class="col-6 text-left pl-5">
<company-address />
</div>
<div class="col-6 pr-5 text-right">
<p :style="[$.bold, $.paraStyle, $.mediumFontSize]">{{ doc.name }}</p>
<p :style="$.paraStyle">{{ frappe.format(doc.date, 'Date') }}</p>
</div>
</div>
<div class="row no-gutters mt-5">
<div class="col-6 text-left pl-5">
<customer-address :customer="doc.customer" />
</div>
<div class="col-6"></div>
</div>
<div class="row mt-5 no-gutters">
<div class="col-12">
<table class="table">
<tbody>
<tr>
<td :style="[$.bold, $.showBorderRight, $.tablePadding]" style="width: 15" class="pl-5">{{ _("NO") }}</td>
<td :style="[$.bold, $.showBorderRight, $.tablePadding]" style="width: 40%">{{ _("ITEM") }}</td>
<td class="text-left" :style="[$.bold, $.showBorderRight, $.tablePadding]" style="width: 20%">{{ _("RATE") }}</td>
<td :style="[$.bold, $.showBorderRight, $.tablePadding]" style="width: 10%">{{ _("QTY") }}</td>
<td class="text-right pr-5" :style="[$.bold, $.tablePadding]" style="width: 20%">{{ _("AMOUNT") }}</td>
</tr>
<tr v-for="row in doc.items" :key="row.idx">
<td :style="$.tablePadding" class="pl-5 pr-5">{{ row.idx + 1 }}</td>
<td :style="$.tablePadding">{{ row.item }}</td>
<td :style="$.tablePadding" class="text-left">{{ frappe.format(row.rate, 'Currency') }}</td>
<td :style="$.tablePadding">{{ row.quantity }}</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ frappe.format(row.amount, 'Currency') }}</td>
</tr>
<tr><td colspan="5" style="padding: 4%"></td></tr>
<tr>
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="$.tablePadding" colspan="2">SUBTOTAL</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ frappe.format(doc.netTotal, 'Currency') }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="$.tablePadding" med colspan="2">{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ frappe.format(tax.amount, 'Currency') }}</td>
</tr>
<tr>
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="[$.bold, $.tablePadding, $.mediumFontSize]" colspan="2">TOTAL</td>
<td :style="[$.bold, $.tablePadding, $.mediumFontSize]" class="text-right pr-5">{{ frappe.format(doc.grandTotal, 'Currency') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row mt-5">
<div :style="$.regularFontSize" class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showBorderBottom]" >
<td :style="$.hideBorderTop" class="pl-5">NOTICE</td>
</tr>
<tr><td class="pl-5">{{ doc.terms }}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'font'],
components: {
CompanyAddress,
CustomerAddress
},
data() {
return {
$: Styles,
}
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.bgColor.backgroundColor = this.themeColor;
this.$.showBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
}
</script>

View File

@ -1,42 +0,0 @@
module.exports = {
name: "InvoiceSettings",
label: "Invoice Settings",
doctype: "DocType",
isSingle: 1,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: "numberSeries",
label: "Number Series",
fieldtype: "Link",
target: "NumberSeries",
required: 1,
default: "INV"
},
{
fieldname: "template",
label: "Template",
fieldtype: "Select",
options: ["Basic I", "Basic II", "Modern"],
required: 1,
default: "Basic I"
},
{
fieldname: "font",
label: "Font",
fieldtype: "Select",
options: ["Montserrat", "Open Sans", "Oxygen", "Merriweather"],
required: 1,
default: "Montserrat"
},
{
fieldname: "themeColor",
label: "Theme Color",
fieldtype: "Data",
required: 1,
default: "#000000",
hidden: 1
}
]
}

View File

@ -2,11 +2,9 @@ module.exports = {
name: 'Item',
doctype: 'DocType',
isSingle: 0,
keywordFields: [
'name',
'description'
],
fields: [{
keywordFields: ['name', 'description'],
fields: [
{
fieldname: 'name',
label: 'Item Name',
fieldtype: 'Data',
@ -22,26 +20,40 @@ module.exports = {
label: 'Unit',
fieldtype: 'Select',
default: 'No',
options: [
'No',
'Kg',
'Gram',
'Hour',
'Day'
]
options: ['No', 'Kg', 'Gram', 'Hour', 'Day']
},
{
fieldname: 'incomeAccount',
label: 'Income Account',
fieldtype: 'Link',
target: 'Account',
required: 1
required: 1,
getFilters: query => {
return {
isGroup: 0,
accountType: 'Income Account'
};
}
},
{
fieldname: 'expenseAccount',
label: 'Expense Account',
fieldtype: 'Link',
target: 'Account'
target: 'Account',
required: 1,
getFilters: query => {
return {
isGroup: 0,
accountType: [
'in',
[
'Cost of Goods Sold',
'Expense Account',
'Stock Received But Not Billed'
]
]
};
}
},
{
fieldname: 'tax',
@ -58,7 +70,8 @@ module.exports = {
layout: [
// section 1
{
columns: [{
columns: [
{
fields: ['name', 'unit']
},
{
@ -69,15 +82,18 @@ module.exports = {
// section 2
{
columns: [{
fields: ['description']
}]
columns: [
{
fields: ['description']
}
]
},
// section 3
{
title: 'Accounting',
columns: [{
columns: [
{
fields: ['incomeAccount', 'expenseAccount']
},
{
@ -85,5 +101,49 @@ module.exports = {
}
]
}
],
links: [
{
label: 'New Sales Invoice',
condition: form => !form.doc._notInserted,
action: async form => {
const invoice = await frappe.getNewDoc('SalesInvoice');
invoice.items = [
{
item: form.doc.name,
rate: form.doc.rate,
tax: form.doc.tax
}
];
invoice.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/SalesInvoice/${invoice.name}`
});
});
await form.$formModal.open(invoice);
}
},
{
label: 'New Purchase Invoice',
condition: form => !form.doc._notInserted,
action: async form => {
const invoice = await frappe.getNewDoc('PurchaseInvoice');
invoice.items = [
{
item: form.doc.name,
rate: form.doc.rate,
tax: form.doc.tax
}
];
invoice.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/PurchaseInvoice/${invoice.name}`
});
});
await form.$formModal.open(invoice);
}
}
]
};

View File

@ -2,88 +2,80 @@ const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
module.exports = {
name: "JournalEntry",
doctype: "DocType",
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: ["name"],
showTitle: true,
settings: "JournalEntrySettings",
fields: [
{
fieldname: "date",
label: "Date",
fieldtype: "Date"
},
{
fieldname: "entryType",
label: "Entry Type",
fieldtype: "Select",
options: [
"Journal Entry",
"Bank Entry",
"Cash Entry",
"Credit Card Entry",
"Debit Note",
"Credit Note",
"Contra Entry",
"Excise Entry",
"Write Off Entry",
"Opening Entry",
"Depreciation Entry"
],
required: 1
},
{
fieldname: "accounts",
label: "Account Entries",
fieldtype: "Table",
childtype: "JournalEntryAccount",
required: true
},
{
fieldname: "referenceNumber",
label: "Reference Number",
fieldtype: "Data",
},
{
fieldname: "referenceDate",
label: "Reference Date",
fieldtype: "Date",
},
{
fieldname: "userRemark",
label: "User Remark",
fieldtype: "Text",
}
],
layout: [
// section 1
{
columns: [
{ fields: [ "date" ] },
{ fields: [ "entryType" ] },
]
},
// section 2
{
columns: [
{ fields: ["accounts"]},
]
},
// section 3
{
columns: [
{ fields: [ "referenceNumber"] },
{ fields: [ "referenceDate"] }
]
},
// section 4
{
columns: [{ fields: [ "userRemark" ] }],
}
]
}
label: 'Journal Entry',
name: 'JournalEntry',
doctype: 'DocType',
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: ['name'],
showTitle: true,
settings: 'JournalEntrySettings',
fields: [
{
fieldname: 'entryType',
label: 'Entry Type',
fieldtype: 'Select',
options: [
'Select...',
'Journal Entry',
'Bank Entry',
'Cash Entry',
'Credit Card Entry',
'Debit Note',
'Credit Note',
'Contra Entry',
'Excise Entry',
'Write Off Entry',
'Opening Entry',
'Depreciation Entry'
],
required: 1
},
{
fieldname: 'date',
label: 'Date',
fieldtype: 'Date'
},
{
fieldname: 'accounts',
label: 'Account Entries',
fieldtype: 'Table',
childtype: 'JournalEntryAccount',
required: true
},
{
fieldname: 'referenceNumber',
label: 'Reference Number',
fieldtype: 'Data'
},
{
fieldname: 'referenceDate',
label: 'Reference Date',
fieldtype: 'Date'
},
{
fieldname: 'userRemark',
label: 'User Remark',
fieldtype: 'Text'
}
],
layout: [
// section 1
{
columns: [{ fields: ['entryType'] }, { fields: ['date'] }]
},
// section 2
{
columns: [{ fields: ['accounts'] }]
},
// section 3
{
columns: [{ fields: ['referenceNumber'] }, { fields: ['referenceDate'] }]
},
// section 4
{
columns: [{ fields: ['userRemark'] }]
}
]
};

View File

@ -0,0 +1,7 @@
import { _ } from 'frappejs/utils';
export default {
doctype: 'JournalEntry',
title: _('Journal Entry'),
columns: ['date', 'entryType']
};

View File

@ -1,33 +1,34 @@
module.exports = {
name: "JournalEntryAccount",
doctype: "DocType",
isSingle: 0,
isChild: 1,
keywordFields: [],
layout: 'ratio',
fields: [
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"required": 1,
getFilters: (query, control) => {
return {
keywords: ["like", query],
isGroup: 0
}
}
},
{
"fieldname": "debit",
"label": "Debit",
"fieldtype": "Currency"
},
{
"fieldname": "credit",
"label": "Credit",
"fieldtype": "Currency"
}
]
}
name: 'JournalEntryAccount',
doctype: 'DocType',
isSingle: 0,
isChild: 1,
keywordFields: [],
layout: 'ratio',
fields: [
{
fieldname: 'account',
label: 'Account',
fieldtype: 'Link',
target: 'Account',
required: 1,
getFilters: (query, control) => {
if (query)
return {
keywords: ['like', query],
isGroup: 0
};
}
},
{
fieldname: 'debit',
label: 'Debit',
fieldtype: 'Currency'
},
{
fieldname: 'credit',
label: 'Credit',
fieldtype: 'Currency'
}
]
};

View File

@ -3,10 +3,8 @@ import { _ } from 'frappejs/utils';
export default {
doctype: 'Party',
title: _('Customer'),
columns: [
'name'
],
columns: ['name', 'defaultAccount', 'address'],
filters: {
customer: 1
}
}
};

View File

@ -1,54 +1,127 @@
module.exports = {
"name": "Party",
"doctype": "DocType",
"isSingle": 0,
"istable": 0,
"keywordFields": [
"name"
],
"fields": [{
"fieldname": "name",
"label": "Name",
"fieldtype": "Data",
"required": 1
},
{
fieldname: "address",
label: "Address",
fieldtype: "Link",
target: "Address"
},
{
fieldname: 'defaultAccount',
label: 'Default Account',
fieldtype: 'Link',
target: 'Account',
getFilters: (query, control) => {
return {
isGroup: 0,
accountType: 'Receivable'
};
}
},
{
"fieldname": "customer",
"label": "Customer",
"fieldtype": "Check"
},
{
"fieldname": "supplier",
"label": "Supplier",
"fieldtype": "Check"
}
],
name: 'Party',
label: 'Party',
doctype: 'DocType',
isSingle: 0,
keywordFields: ['name'],
fields: [
{
fieldname: 'name',
label: 'Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'address',
label: 'Address',
fieldtype: 'Link',
target: 'Address'
},
{
fieldname: 'defaultAccount',
label: 'Default Account',
fieldtype: 'Link',
target: 'Account',
getFilters: (query, doc) => {
return {
isGroup: 0,
accountType: doc.customer === 1 ? 'Receivable' : 'Payable'
};
}
},
{
fieldname: 'currency',
label: 'Currency',
fieldtype: 'Link',
target: 'Currency',
formula: async doc => {
const { currency } = await frappe.getSingle('AccountingSettings');
return currency;
}
},
{
fieldname: 'customer',
label: 'Customer',
fieldtype: 'Check'
},
{
fieldname: 'supplier',
label: 'Supplier',
fieldtype: 'Check'
}
],
links: [{
label: 'Invoices',
condition: (form) => form.doc.customer,
action: form => {
form.$router.push({
path: `/report/sales-register?&customer=${form.doc.name}`
});
}
}]
}
getFormTitle(doc) {
if (doc.customer) return 'Customer';
return 'Supplier';
},
getListTitle(doc) {
if (doc.customer) return 'Customer';
return 'Supplier';
},
links: [
{
label: 'New Sales Invoice',
condition: form => form.doc.customer,
action: async form => {
const invoice = await frappe.getNewDoc('SalesInvoice');
invoice.customer = form.doc.name;
invoice.account = form.doc.defaultAccount;
invoice.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/SalesInvoice/${invoice.name}`
});
});
await form.$formModal.open(invoice);
}
},
{
label: 'Sales Invoices',
condition: form => form.doc.customer,
action: form => {
form.$router.push({
path: `/list/SalesInvoice?customer=${form.doc.name}`
});
}
},
{
label: 'New Purchase Invoice',
condition: form => form.doc.supplier,
action: async form => {
const invoice = await frappe.getNewDoc('PurchaseInvoice');
invoice.supplier = form.doc.name;
invoice.account = form.doc.defaultAccount;
invoice.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/PurchaseInvoice/${invoice.name}`
});
});
await form.$formModal.open(invoice);
}
},
{
label: 'Purchase Invoices',
condition: form => form.doc.supplier,
action: form => {
form.$router.push({
path: `/list/PurchaseInvoice?supplier=${form.doc.name}`
});
}
},
{
label: 'Delete',
condition: form => form.doc.customer,
action: async form => {
const party = await frappe.getDoc('Party', form.doc.name);
await party.delete();
form.$router.push({
path: `/list/Customer`
});
}
}
]
};

View File

@ -0,0 +1,7 @@
import { _ } from 'frappejs/utils';
export default {
doctype: 'Party',
title: filters => (filters.customer ? 'Customer' : 'Supplier'),
columns: ['name', 'defaultAccount', 'address']
};

View File

@ -0,0 +1,21 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
module.exports = class PartyServer extends BaseDocument {
beforeInsert() {
if (this.customer && this.supplier) {
frappe.call({
method: 'show-dialog',
args: {
title: 'Invalid Entry',
message: 'Select a single party type.'
}
});
throw new Error();
}
if (this.gstin && ['Unregistered', 'Consumer'].includes(this.gstType)) {
this.gstin = '';
}
}
};

View File

@ -1,12 +1,21 @@
const party = require('./Party')
const party = require('./Party');
party.fields.splice(3, 0, { //insert at 3rd position
party.fields.splice(3, 0, {
//insert at 3rd position
fieldname: 'gstin',
label: 'GSTIN No.',
fieldtype: 'Data',
hidden: 0
})
party.fields.join()
const newParty = party
hidden: form => {
return form.gstType === 'Registered Regular' ? 0 : 1;
}
});
party.fields.splice(4, 0, {
fieldname: 'gstType',
label: 'GST Registration Type',
fieldtype: 'Select',
options: ['Unregistered', 'Registered Regular', 'Consumer']
});
party.fields.join();
const newParty = party;
module.exports = newParty
module.exports = newParty;

View File

@ -0,0 +1,10 @@
import { _ } from 'frappejs/utils';
export default {
doctype: 'Party',
title: _('Supplier'),
columns: ['name', 'defaultAccount', 'address'],
filters: {
supplier: 1
}
};

View File

@ -1,110 +1,165 @@
const utils = require('../../../accounting/utils');
module.exports = {
name: "Payment",
label: "Payment",
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: [],
settings: "PaymentSettings",
fields: [{
"fieldname": "date",
"label": "Posting Date",
"fieldtype": "Date",
// default: (new Date()).toISOString()
},
{
fieldname: "party",
label: "Party",
fieldtype: "Link",
target: "Party",
required: 1
},
{
fieldname: "account",
label: "Account",
fieldtype: "Link",
target: "Account",
required: 1
},
{
fieldname: "paymentAccount",
label: "Payment Account",
fieldtype: "Link",
target: "Account",
required: 1
},
{
fieldname: "referenceId",
label: "Ref. / Cheque No.",
fieldtype: "Data",
default: "ABC",
required: 1 // TODO: UNIQUE
},
{
fieldname: "referenceDate",
label: "Ref. Date",
fieldtype: "Date",
},
{
fieldname: "clearanceDate",
label: "Clearance Date",
fieldtype: "Date",
},
{
fieldname: "amount",
label: "Amount",
fieldtype: "Currency",
required: 1,
disabled: true,
formula: (doc) => doc.getSum('for', 'amount')
},
{
fieldname: "writeoff",
label: "Write Off / Refund",
fieldtype: "Currency",
},
{
fieldname: "for",
label: "Payment For",
fieldtype: "Table",
childtype: "PaymentFor",
required: 1
name: 'Payment',
label: 'Payment',
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: [],
settings: 'PaymentSettings',
fields: [
{
fieldname: 'party',
label: 'Party',
fieldtype: 'Link',
target: 'Party',
required: 1
},
{
fieldname: 'date',
label: 'Posting Date',
fieldtype: 'Date',
defaultValue: new Date().toISOString()
},
{
fieldname: 'account',
label: 'From Account',
fieldtype: 'Link',
target: 'Account',
required: 1,
getFilters: (query, doc) => {
if (doc.paymentType === 'Pay') {
if (doc.paymentMethod === 'Cash') {
return { accountType: 'Cash', isGroup: 0 };
} else {
return { accountType: ['in', ['Bank', 'Cash']], isGroup: 0 };
}
}
],
layout: [{
columns: [{
fields: ['date', 'party']
},
{
fields: ['account', 'paymentAccount']
},
]
},
{
columns: [{
fields: ['referenceId']
}, {
fields: ['referenceDate']
},{
fields: ['clearanceDate']
}]
},
{
columns: [{
fields: ['for']
}]
},
{
columns: [{
fields: ['amount', 'writeoff']
}]
}
},
{
fieldname: 'paymentType',
label: 'Payment Type',
fieldtype: 'Select',
options: ['', 'Receive', 'Pay'],
required: 1
},
{
fieldname: 'paymentAccount',
label: 'To Account',
fieldtype: 'Link',
target: 'Account',
required: 1,
getFilters: (query, doc) => {
if (doc.paymentType === 'Receive') {
if (doc.paymentMethod === 'Cash') {
return { accountType: 'Cash', isGroup: 0 };
} else {
return { accountType: ['in', ['Bank', 'Cash']], isGroup: 0 };
}
}
],
}
},
{
fieldname: 'paymentMethod',
label: 'Payment Method',
fieldtype: 'Select',
options: ['', 'Cash', 'Cheque', 'Transfer'],
required: 1
},
{
fieldname: 'referenceId',
label: 'Ref. / Cheque No.',
fieldtype: 'Data',
required: 1 // TODO: UNIQUE
},
{
fieldname: 'referenceDate',
label: 'Ref. Date',
fieldtype: 'Date'
},
{
fieldname: 'clearanceDate',
label: 'Clearance Date',
fieldtype: 'Date',
hidden: doc => {
return doc.paymentMethod === 'Cash' ? 1 : 0;
}
},
{
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Currency',
required: 1,
disabled: true,
formula: doc => {
return frappe.format(doc.getSum('for', 'amount'), 'Currency');
}
},
{
fieldname: 'writeoff',
label: 'Write Off / Refund',
fieldtype: 'Currency'
},
{
fieldname: 'for',
label: 'Payment For',
fieldtype: 'Table',
childtype: 'PaymentFor',
required: 1
}
],
links: [
utils.ledgerLink
]
}
layout: [
{
columns: [
{
fields: ['party', 'account']
},
{
fields: ['date', 'paymentAccount']
}
]
},
{
columns: [
{
fields: ['paymentMethod']
},
{
fields: ['paymentType']
},
{
fields: ['referenceId']
}
]
},
{
columns: [
{
fields: ['referenceDate']
},
{
fields: ['clearanceDate']
}
]
},
{
columns: [
{
fields: ['for']
}
]
},
{
columns: [
{
fields: ['amount', 'writeoff']
}
]
}
],
links: [utils.ledgerLink]
};

View File

@ -7,24 +7,33 @@ export default {
columns: [
'party',
{
label: 'Payment',
label: 'Status',
fieldname: 'status',
fieldtype: 'Select',
size: 'small',
options: ['Status...', 'Reconciled', 'Not Reconciled'],
getValue(doc) {
if (doc.submitted === 1 && doc.clearanceDate !== null) {
if (
doc.submitted === 1 &&
(doc.clearanceDate !== null || doc.paymentMethod === 'Cash')
) {
return 'Reconciled';
}
return 'Not Reconciled';
},
getIndicator(doc) {
if (doc.submitted === 1 && doc.clearanceDate !== null) {
if (
doc.submitted === 1 &&
(doc.clearanceDate !== null || doc.paymentMethod === 'Cash')
) {
return indicators.GREEN;
}
return indicators.ORANGE;
}
},
'account',
'amount',
'paymentType',
'date',
'clearanceDate',
'name'
'amount'
]
}
};

View File

@ -3,28 +3,83 @@ const frappe = require('frappejs');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class PaymentServer extends BaseDocument {
getPosting() {
let entries = new LedgerPosting({reference: this, party: this.party});
entries.debit(this.paymentAccount, this.amount);
for (let row of this.for) {
entries.credit(this.account, row.amount, row.referenceType, row.referenceName);
}
return entries;
async change({ changed }) {
if (changed === 'for') {
this.amount = 0;
for (let paymentReference of this.for) {
this.amount += frappe.parseNumber(paymentReference.amount);
}
this.amount = frappe.format(this.amount, 'Currency');
}
}
async afterSubmit() {
await this.getPosting().post();
for (let row of this.for) {
if (row.referenceType === 'Invoice') {
await frappe.db.setValue('Invoice', row.referenceName, 'outstandingAmount', 0.0);
async getPosting() {
let entries = new LedgerPosting({ reference: this, party: this.party });
await entries.debit(this.paymentAccount, this.amount);
for (let row of this.for) {
await entries.credit(this.account, row.amount);
}
return entries;
}
async beforeSubmit() {
if (this.for.length > 0)
for (let row of this.for) {
if (['SalesInvoice', 'PurchaseInvoice'].includes(row.referenceType)) {
let { outstandingAmount, grandTotal } = await frappe.getDoc(
row.referenceType,
row.referenceName
);
if (outstandingAmount === null) {
outstandingAmount = grandTotal;
}
if (
0 >= frappe.parseNumber(this.amount) ||
frappe.parseNumber(this.amount) >
frappe.parseNumber(outstandingAmount)
) {
frappe.call({
method: 'show-dialog',
args: {
title: 'Invalid Payment Entry',
message: `Payment amount (${
this.amount
}) should be greater than 0 and less than Outstanding amount (${outstandingAmount})`
}
});
throw new Error();
} else {
await frappe.db.setValue(
row.referenceType,
row.referenceName,
'outstandingAmount',
frappe.parseNumber(outstandingAmount) -
frappe.parseNumber(this.amount)
);
}
}
}
else {
frappe.call({
method: 'show-dialog',
args: {
title: 'Invalid Payment Entry',
message: `No reference for the payment.`
}
});
throw new Error();
}
}
async afterRevert() {
await this.getPosting().postReverse();
}
}
async afterSubmit() {
const entries = await this.getPosting();
await entries.post();
}
async afterRevert() {
const entries = await this.getPosting();
await entries.postReverse();
// Maybe revert outstanding amount of invoice too?
}
};

View File

@ -1,28 +1,30 @@
module.exports = {
name: "PaymentFor",
label: "Payment For",
isSingle: 0,
isChild: 1,
keywordFields: [],
fields: [
{
fieldname: "referenceType",
label: "Reference Type",
fieldtype: "Data",
required: 1
},
{
fieldname: "referenceName",
label: "Reference Name",
fieldtype: "DynamicLink",
references: "referenceType",
required: 1
},
{
fieldname: "amount",
label: "Amount",
fieldtype: "Currency",
required: 1
},
]
}
name: 'PaymentFor',
label: 'Payment For',
isSingle: 0,
isChild: 1,
keywordFields: [],
fields: [
{
fieldname: 'referenceType',
label: 'Reference Type',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'referenceName',
label: 'Reference Name',
fieldtype: 'DynamicLink',
references: 'referenceType',
required: 1
},
{
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Currency',
formula: (row, doc) =>
doc.getFrom(row.referenceType, row.referenceName, 'grandTotal'),
required: 1
}
]
};

View File

@ -0,0 +1,204 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
module.exports = {
name: 'PurchaseInvoice',
doctype: 'DocType',
label: 'Purchase Invoice',
documentClass: require('./PurchaseInvoiceDocument'),
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: ['name', 'supplier'],
settings: 'PurchaseInvoiceSettings',
showTitle: true,
fields: [
{
fieldname: 'date',
label: 'Date',
fieldtype: 'Date',
defaultValue: new Date().toISOString()
},
{
fieldname: 'supplier',
label: 'Supplier',
fieldtype: 'Link',
target: 'Party',
required: 1,
getFilters: (query, control) => {
if (!query) return { supplier: 1 };
return {
keywords: ['like', query],
supplier: 1
};
}
},
{
fieldname: 'account',
label: 'Account',
fieldtype: 'Link',
target: 'Account',
formula: doc => doc.getFrom('Party', doc.supplier, 'defaultAccount'),
getFilters: (query, control) => {
if (!query) return { isGroup: 0, accountType: 'Payable' };
return {
keywords: ['like', query],
isGroup: 0,
accountType: 'Payable'
};
}
},
{
fieldname: 'currency',
label: 'Customer Currency',
fieldtype: 'Link',
target: 'Currency',
hidden: 1,
formula: doc => doc.getFrom('Party', doc.supplier, 'currency')
},
{
fieldname: 'exchangeRate',
label: 'Exchange Rate',
fieldtype: 'Float',
placeholder: '1 USD = [?] INR',
hidden: doc => !doc.isForeignTransaction()
},
{
fieldname: 'items',
label: 'Items',
fieldtype: 'Table',
childtype: 'PurchaseInvoiceItem',
required: true
},
{
fieldname: 'baseNetTotal',
label: 'Net Total (INR)',
fieldtype: 'Currency',
formula: async doc => await doc.getBaseNetTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'netTotal',
label: 'Net Total (USD)',
fieldtype: 'Currency',
hidden: doc => !doc.isForeignTransaction(),
formula: async doc =>
await doc.formatIntoCustomerCurrency(doc.getSum('items', 'amount')),
disabled: true,
readOnly: 1
},
{
fieldname: 'taxes',
label: 'Taxes',
fieldtype: 'Table',
childtype: 'TaxSummary',
readOnly: 1,
template: (doc, row) => {
return `<div class='row'>
<div class='col-6'></div>
<div class='col-6'>
<div class='row' v-for='row in value'>
<div class='col-6'>{{ row.account }} ({{row.rate}}%)</div>
<div class='col-6 text-right'>
{{ row.amount }}
</div>
</div>
</div>
</div>`;
}
},
{
fieldname: 'baseGrandTotal',
label: 'Grand Total (INR)',
fieldtype: 'Currency',
formula: async doc => await doc.getBaseGrandTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'grandTotal',
label: 'Grand Total (USD)',
fieldtype: 'Currency',
hidden: doc => !doc.isForeignTransaction(),
formula: async doc => await doc.getGrandTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'terms',
label: 'Terms',
fieldtype: 'Text'
},
{
fieldname: 'outstandingAmount',
label: 'Outstanding Amount',
fieldtype: 'Currency',
hidden: 1
}
],
layout: [
// section 1
{
columns: [
{ fields: ['supplier', 'account'] },
{ fields: ['date', 'exchangeRate'] }
]
},
// section 2
{
columns: [{ fields: ['items'] }]
},
// section 3
{
columns: [
{
fields: [
'baseNetTotal',
'netTotal',
'taxes',
'baseGrandTotal',
'grandTotal'
]
}
]
},
// section 4
{
columns: [{ fields: ['terms'] }]
}
],
links: [
utils.ledgerLink,
{
label: 'Make Payment',
condition: form =>
form.doc.submitted && form.doc.outstandingAmount != 0.0,
action: async form => {
const payment = await frappe.getNewDoc('Payment');
payment.paymentType = 'Pay';
payment.party = form.doc.supplier;
payment.paymentAccount = form.doc.account;
payment.for = [
{
referenceType: form.doc.doctype,
referenceName: form.doc.name,
amount: form.doc.outstandingAmount
}
];
payment.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/Payment/${payment.name}`
});
});
await form.$formModal.open(payment);
}
}
]
};

View File

@ -0,0 +1,4 @@
const SalesInvoiceDocument = require('../SalesInvoice/SalesInvoiceDocument');
const frappe = require('frappejs');
module.exports = class PurchaseInvoice extends SalesInvoiceDocument {};

View File

@ -0,0 +1,32 @@
import { _ } from 'frappejs/utils';
import indicators from 'frappejs/ui/constants/indicators';
export default {
doctype: 'PurchaseInvoice',
title: _('Purchase Invoice'),
columns: [
'supplier',
{
label: 'Status',
fieldname: 'status',
fieldtype: 'Select',
size: 'small',
options: ['Status...', 'Paid', 'Pending'],
getValue(doc) {
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
return 'Paid';
}
return 'Pending';
},
getIndicator(doc) {
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
return indicators.GREEN;
}
return indicators.ORANGE;
}
},
'date',
'grandTotal',
'outstandingAmount'
]
};

View File

@ -0,0 +1,72 @@
const frappe = require('frappejs');
const PurchaseInvoice = require('./PurchaseInvoiceDocument');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class PurchaseInvoiceServer extends PurchaseInvoice {
async getPosting() {
let entries = new LedgerPosting({ reference: this, party: this.supplier });
await entries.credit(this.account, this.baseGrandTotal);
for (let item of this.items) {
const baseItemAmount = frappe.format(
frappe.parseNumber(item.amount) * this.exchangeRate,
'Currency'
);
await entries.debit(item.account, baseItemAmount);
}
if (this.taxes) {
for (let tax of this.taxes) {
const baseTaxAmount = frappe.format(
frappe.parseNumber(tax.amount) * this.exchangeRate,
'Currency'
);
await entries.debit(tax.account, baseTaxAmount);
}
}
return entries;
}
async getPayments() {
let payments = await frappe.db.getAll({
doctype: 'PaymentFor',
fields: ['parent'],
filters: { referenceName: this.name },
orderBy: 'name'
});
if (payments.length != 0) {
return payments;
}
return [];
}
async beforeInsert() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async afterSubmit() {
const entries = await this.getPosting();
await entries.post();
await frappe.db.setValue(
'PurchaseInvoice',
this.name,
'outstandingAmount',
this.baseGrandTotal
);
}
async afterRevert() {
let paymentRefList = await this.getPayments();
for (let paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent;
const payment = await frappe.getDoc('Payment', paymentReference);
const paymentEntries = await payment.getPosting();
await paymentEntries.postReverse();
// To set the payment status as unsubmitted.
payment.revert();
}
const entries = await this.getPosting();
await entries.postReverse();
}
};

View File

@ -0,0 +1,77 @@
module.exports = {
name: 'PurchaseInvoiceItem',
label: 'Purchase Invoice Item',
doctype: 'DocType',
isSingle: 0,
isChild: 1,
keywordFields: [],
layout: 'ratio',
fields: [
{
fieldname: 'item',
label: 'Item',
fieldtype: 'Link',
target: 'Item',
required: 1,
width: 2
},
{
fieldname: 'description',
label: 'Description',
fieldtype: 'Text',
formula: (row, doc) => doc.getFrom('Item', row.item, 'description'),
hidden: 1
},
{
fieldname: 'quantity',
label: 'Quantity',
fieldtype: 'Float',
required: 1
},
{
fieldname: 'rate',
label: 'Rate',
fieldtype: 'Currency',
required: 1,
formula: (row, doc) => doc.getFrom('Item', row.item, 'rate')
},
{
fieldname: 'account',
label: 'Account',
fieldtype: 'Link',
target: 'Account',
required: 1,
formula: (row, doc) => doc.getFrom('Item', row.item, 'expenseAccount')
},
{
fieldname: 'tax',
label: 'Tax',
fieldtype: 'Link',
target: 'Tax',
formula: (row, doc) => {
if (row.tax) return row.tax;
return doc.getFrom('Item', row.item, 'tax');
}
},
{
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Currency',
readOnly: 1,
disabled: true,
formula: async (row, doc) => {
return await doc.formatIntoCustomerCurrency(
row.quantity * frappe.parseNumber(row.rate)
);
}
},
{
fieldname: 'taxAmount',
label: 'Tax Amount',
hidden: 1,
readOnly: 1,
fieldtype: 'Text',
formula: (row, doc) => doc.getRowTax(row)
}
]
};

View File

@ -0,0 +1,18 @@
module.exports = {
name: 'PurchaseInvoiceSettings',
label: 'Purchase Invoice Settings',
doctype: 'DocType',
isSingle: 1,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: 'numberSeries',
label: 'Number Series',
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
default: 'PINV'
}
]
};

View File

@ -1,16 +1,20 @@
const model = require('frappejs/model');
const Bill = require('../Bill/Bill');
const PurchaseInvoice = require('../PurchaseInvoice/PurchaseInvoice');
module.exports = model.extend(Bill, {
name: "PurchaseOrder",
label: "Purchase Order",
settings: "PurchaseOrderSettings",
module.exports = model.extend(
PurchaseInvoice,
{
name: 'PurchaseOrder',
label: 'Purchase Order',
settings: 'PurchaseOrderSettings',
fields: [
{
"fieldname": "items",
"childtype": "PurchaseOrderItem"
}
{
fieldname: 'items',
childtype: 'PurchaseOrderItem'
}
]
}, {
},
{
skipFields: ['account']
});
}
);

View File

@ -1,6 +1,6 @@
const model = require('frappejs/model');
const BillItem = require('../BillItem/BillItem');
const PurchaseInvoiceItem = require('../PurchaseInvoiceItem/PurchaseInvoiceItem');
module.exports = model.extend(BillItem, {
module.exports = model.extend(PurchaseInvoiceItem, {
name: "PurchaseOrderItem"
});

View File

@ -1,7 +1,7 @@
const model = require('frappejs/model');
const BillSettings = require('../BillSettings/BillSettings');
const PurchaseInvoiceSettings = require('../PurchaseInvoiceSettings/PurchaseInvoiceSettings');
module.exports = model.extend(BillSettings, {
module.exports = model.extend(PurchaseInvoiceSettings, {
"name": "PurchaseOrderSettings",
"label": "Purchase Order Settings",
"fields": [

View File

@ -1,7 +1,7 @@
const model = require('frappejs/model');
const Invoice = require('../Invoice/Invoice');
const SalesInvoice = require('../SalesInvoice/SalesInvoice');
const Quotation = model.extend(Invoice, {
const Quotation = model.extend(SalesInvoice, {
name: "Quotation",
label: "Quotation",
settings: "QuotationSettings",

View File

@ -1,3 +1,3 @@
const InvoiceDocument = require('../Invoice/InvoiceDocument');
const SalesInvoiceDocument = require('../SalesInvoice/SalesInvoiceDocument');
module.exports = class Quotation extends InvoiceDocument { }
module.exports = class Quotation extends SalesInvoiceDocument {};

View File

@ -1,6 +1,6 @@
const model = require('frappejs/model');
const InvoiceItem = require('../InvoiceItem/InvoiceItem');
const SalesInvoiceItem = require('../SalesInvoiceItem/SalesInvoiceItem');
module.exports = model.extend(InvoiceItem, {
module.exports = model.extend(SalesInvoiceItem, {
name: "QuotationItem"
});

View File

@ -1,7 +1,7 @@
const model = require('frappejs/model');
const InvoiceSettings = require('../InvoiceSettings/InvoiceSettings');
const SalesInvoiceSettings = require('../SalesInvoiceSettings/SalesInvoiceSettings');
module.exports = model.extend(InvoiceSettings, {
module.exports = model.extend(SalesInvoiceSettings, {
"name": "QuotationSettings",
"label": "Quotation Settings",
"fields": [

View File

@ -0,0 +1,211 @@
const frappe = require('frappejs');
const utils = require('../../../accounting/utils');
module.exports = {
name: 'SalesInvoice',
label: 'Sales Invoice',
doctype: 'DocType',
documentClass: require('./SalesInvoiceDocument'),
print: {
printFormat: 'Standard Invoice Format'
},
isSingle: 0,
isChild: 0,
isSubmittable: 1,
keywordFields: ['name', 'customer'],
settings: 'SalesInvoiceSettings',
showTitle: true,
fields: [
{
fieldname: 'date',
label: 'Date',
fieldtype: 'Date',
defaultValue: new Date().toISOString()
},
{
fieldname: 'customer',
label: 'Customer',
fieldtype: 'Link',
target: 'Party',
required: 1,
getFilters: query => {
if (query)
return {
keywords: ['like', query],
customer: 1
};
return {
customer: 1
};
}
},
{
fieldname: 'account',
label: 'Account',
fieldtype: 'Link',
target: 'Account',
formula: doc => doc.getFrom('Party', doc.customer, 'defaultAccount'),
getFilters: (query, control) => {
if (!query) return { isGroup: 0, accountType: 'Receivable' };
return {
keywords: ['like', query],
isGroup: 0,
accountType: 'Receivable'
};
}
},
{
fieldname: 'currency',
label: 'Customer Currency',
fieldtype: 'Link',
target: 'Currency',
hidden: 1,
formula: doc => doc.getFrom('Party', doc.customer, 'currency')
},
{
fieldname: 'exchangeRate',
label: 'Exchange Rate',
fieldtype: 'Float',
placeholder: '1 USD = [?] INR',
hidden: doc => !doc.isForeignTransaction()
},
{
fieldname: 'items',
label: 'Items',
fieldtype: 'Table',
childtype: 'SalesInvoiceItem',
required: true
},
{
fieldname: 'baseNetTotal',
label: 'Net Total (INR)',
fieldtype: 'Currency',
formula: async doc => await doc.getBaseNetTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'netTotal',
label: 'Net Total (USD)',
fieldtype: 'Currency',
hidden: doc => !doc.isForeignTransaction(),
formula: async doc =>
await doc.formatIntoCustomerCurrency(doc.getSum('items', 'amount')),
disabled: true,
readOnly: 1
},
{
fieldname: 'taxes',
label: 'Taxes',
fieldtype: 'Table',
childtype: 'TaxSummary',
readOnly: 1,
template: (doc, row) => {
return `<div class='row'>
<div class='col-6'></div>
<div class='col-6'>
<div class='row' v-for='row in value'>
<div class='col-6'>{{ row.account }} ({{row.rate}}%)</div>
<div class='col-6 text-right'>
{{ row.amount }}
</div>
</div>
</div>
</div>`;
}
},
{
fieldname: 'baseGrandTotal',
label: 'Grand Total (INR)',
fieldtype: 'Currency',
formula: async doc => await doc.getBaseGrandTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'grandTotal',
label: 'Grand Total (USD)',
fieldtype: 'Currency',
hidden: doc => !doc.isForeignTransaction(),
formula: async doc => await doc.getGrandTotal(),
disabled: true,
readOnly: 1
},
{
fieldname: 'outstandingAmount',
label: 'Outstanding Amount',
fieldtype: 'Currency',
hidden: 1
},
{
fieldname: 'terms',
label: 'Terms',
fieldtype: 'Text'
}
],
layout: [
// section 1
{
columns: [
{ fields: ['customer', 'account'] },
{ fields: ['date', 'exchangeRate'] }
]
},
// section 2
{
columns: [{ fields: ['items'] }]
},
// section 3
{
columns: [
{
fields: [
'baseNetTotal',
'netTotal',
'taxes',
'baseGrandTotal',
'grandTotal'
]
}
]
},
// section 4
{
columns: [{ fields: ['terms'] }]
}
],
links: [
utils.ledgerLink,
{
label: 'Make Payment',
condition: form =>
form.doc.submitted && form.doc.outstandingAmount != 0.0,
action: async form => {
const payment = await frappe.getNewDoc('Payment');
payment.paymentType = 'Receive';
payment.party = form.doc.customer;
payment.account = form.doc.account;
payment.for = [
{
referenceType: form.doc.doctype,
referenceName: form.doc.name,
amount: form.doc.outstandingAmount
}
];
payment.on('afterInsert', async () => {
form.$formModal.close();
form.$router.push({
path: `/edit/Payment/${payment.name}`
});
});
await form.$formModal.open(payment);
}
}
]
};

View File

@ -0,0 +1,182 @@
const BaseDocument = require('frappejs/model/document');
const frappe = require('frappejs');
module.exports = class SalesInvoice extends BaseDocument {
async change({ changed }) {
if (changed === 'items' || changed === 'exchangeRate') {
const companyCurrency = frappe.AccountingSettings.currency;
if (this.currency.length && this.currency !== companyCurrency) {
for (let item of this.items) {
if (item.rate && this.exchangeRate) {
const itemRate = await this.getFrom('Item', item.item, 'rate');
item.rate = frappe.parseNumber(itemRate) / this.exchangeRate;
if (item.quantity) {
item.amount = item.rate * item.quantity;
}
item.amount = await this.formatIntoCustomerCurrency(item.amount);
item.rate = await this.formatIntoCustomerCurrency(item.rate);
}
}
this.netTotal = await this.formatIntoCustomerCurrency(this.netTotal);
this.grandTotal = await this.formatIntoCustomerCurrency(
this.grandTotal
);
}
}
if (changed === 'customer' || changed === 'supplier') {
this.currency = await this.getFrom('Party', this[changed], 'currency');
this.exchangeRate = await this.getExchangeRate();
}
}
async formatIntoCustomerCurrency(value) {
const companyCurrency = frappe.AccountingSettings.currency;
if (this.currency.length && this.currency !== companyCurrency) {
const { numberFormat, symbol } = await this.getCustomerCurrencyInfo();
return frappe.format(value, {
fieldtype: 'Currency',
currencyInfo: { numberFormat, symbol }
});
} else {
return frappe.format(value, 'Currency');
}
}
isForeignTransaction() {
return this.currency
? this.currency !== frappe.AccountingSettings.currency
? 1
: 0
: 0;
}
async getCustomerCurrencyInfo() {
if (this.numberFormat || this.symbol) {
return { numberFormat: this.numberFormat, symbol: this.symbol };
}
const { numberFormat, symbol } = await frappe.getDoc(
'Currency',
this.currency
);
this.numberFormat = numberFormat;
this.symbol = symbol;
return { numberFormat, symbol };
}
async getExchangeRate() {
const companyCurrency = frappe.AccountingSettings.currency;
return this.currency === companyCurrency ? 1.0 : undefined;
}
async getBaseNetTotal() {
if (this.isForeignTransaction()) {
return frappe.format(
this.getSum('items', 'amount') * (this.exchangeRate || 0),
'Currency'
);
} else {
return await this.formatIntoCustomerCurrency(
this.getSum('items', 'amount')
);
}
}
async getRowTax(row) {
if (row.tax) {
let tax = await this.getTax(row.tax);
let taxAmount = [];
for (let d of tax.details || []) {
const amt = (frappe.parseNumber(row.amount) * d.rate) / 100;
taxAmount.push({
account: d.account,
rate: d.rate,
amount: await this.formatIntoCustomerCurrency(amt)
});
}
return JSON.stringify(taxAmount);
} else {
return '';
}
}
async getTax(tax) {
if (!this._taxes) this._taxes = {};
if (!this._taxes[tax]) this._taxes[tax] = await frappe.getDoc('Tax', tax);
return this._taxes[tax];
}
async makeTaxSummary() {
if (!this.taxes) this.taxes = [];
// reset tax amount
this.taxes.map(d => {
d.amount = 0;
d.rate = 0;
});
// calculate taxes
for (let row of this.items) {
if (row.taxAmount) {
let taxAmount = JSON.parse(row.taxAmount);
for (let rowTaxDetail of taxAmount) {
let found = false;
// check if added in summary
for (let taxDetail of this.taxes) {
if (taxDetail.account === rowTaxDetail.account) {
taxDetail.rate = rowTaxDetail.rate;
taxDetail.amount =
frappe.parseNumber(taxDetail.amount) +
frappe.parseNumber(rowTaxDetail.amount);
taxDetail.amount = await this.formatIntoCustomerCurrency(
taxDetail.amount
);
found = true;
}
}
// add new row
if (!found) {
this.taxes.push({
account: rowTaxDetail.account,
rate: rowTaxDetail.rate,
amount: rowTaxDetail.amount
});
}
}
}
}
// clear no taxes
this.taxes = this.taxes.filter(d => d.amount);
}
async getGrandTotal() {
await this.makeTaxSummary();
let grandTotal = frappe.parseNumber(this.netTotal);
if (this.taxes) {
for (let row of this.taxes) {
grandTotal += frappe.parseNumber(row.amount);
}
}
grandTotal = Math.floor(grandTotal * 100) / 100;
return await this.formatIntoCustomerCurrency(grandTotal);
}
async getBaseGrandTotal() {
await this.makeTaxSummary();
let baseGrandTotal = frappe.parseNumber(this.baseNetTotal);
if (this.taxes) {
for (let row of this.taxes) {
baseGrandTotal += frappe.parseNumber(row.amount) * this.exchangeRate;
}
}
baseGrandTotal = Math.floor(baseGrandTotal * 100) / 100;
return frappe.format(baseGrandTotal, 'Currency');
}
};

View File

@ -2,12 +2,16 @@ import { _ } from 'frappejs/utils';
import indicators from 'frappejs/ui/constants/indicators';
export default {
doctype: 'Invoice',
title: _('Invoice'),
doctype: 'SalesInvoice',
title: _('Sales Invoice'),
columns: [
'customer',
{
label: 'Status',
fieldname: 'status',
fieldtype: 'Select',
size: 'small',
options: ['Status..', 'Paid', 'Pending'],
getValue(doc) {
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
return 'Paid';
@ -21,13 +25,8 @@ export default {
return indicators.ORANGE;
}
},
'grandTotal',
'date',
{
label: 'INV #',
getValue(doc) {
return doc.name;
}
}
'grandTotal',
'outstandingAmount'
]
}
};

View File

@ -0,0 +1,102 @@
<template>
<div>
<div class="row no-gutters">
<div v-if="showInvoiceCustomizer" class="col-3 mt-4 mx-auto"></div>
<div class="col-8 mx-auto text-right mt-4">
<f-button primary @click="$emit('send', $refs.printComponent.innerHTML)">{{ _('Send') }}</f-button>
<f-button secondary @click="toggleCustomizer">{{ _('Customize') }}</f-button>
<f-button secondary @click="$emit('makePDF', $refs.printComponent.innerHTML)">{{ _('PDF') }}</f-button>
</div>
</div>
<div class="row no-gutters">
<div v-if="showInvoiceCustomizer" class="col-3 mt-4 mx-auto">
<invoice-customizer
class="border"
style="position: fixed"
@closeInvoiceCustomizer="toggleCustomizer"
@changeColor="changeColor($event)"
@changeTemplate="changeTemplate($event)"
@changeFont="changeFont($event)"
@updateTemplateView="updateTemplateView"
/>
</div>
<div class="col-8 bg-white mt-4 mx-auto border shadow" ref="printComponent">
<component
:themeColor="themeColor"
:font="font"
:is="template"
v-if="doc"
:doc="doc"
:key="usedForReRender"
/>
</div>
</div>
</div>
</template>
<script>
import InvoiceTemplate1 from '@/../models/doctype/SalesInvoice/Templates/InvoiceTemplate1';
import InvoiceTemplate2 from '@/../models/doctype/SalesInvoice/Templates/InvoiceTemplate2';
import InvoiceTemplate3 from '@/../models/doctype/SalesInvoice/Templates/InvoiceTemplate3';
import InvoiceCustomizer from '@/components/InvoiceCustomizer';
const invoiceTemplates = {
'Basic I': InvoiceTemplate1,
'Basic II': InvoiceTemplate2,
Modern: InvoiceTemplate3
};
export default {
name: 'InvoicePrint',
props: ['doc'],
components: {
InvoiceCustomizer
},
data() {
return {
showInvoiceCustomizer: false,
themeColor: undefined,
template: undefined,
font: undefined,
usedForReRender: 0
};
},
async created() {
await this.loadInvoice();
},
methods: {
async loadInvoice() {
await this.getTemplate();
await this.getColor();
await this.getFont();
},
async getTemplate() {
let invoiceSettings = await frappe.getDoc('SalesInvoiceSettings');
this.template = invoiceTemplates[invoiceSettings.template];
},
async getColor() {
let invoiceSettings = await frappe.getDoc('SalesInvoiceSettings');
this.themeColor = invoiceSettings.themeColor;
},
async getFont() {
let invoiceSettings = await frappe.getDoc('SalesInvoiceSettings');
this.font = invoiceSettings.font;
},
async toggleCustomizer() {
await this.loadInvoice();
this.showInvoiceCustomizer = !this.showInvoiceCustomizer;
},
changeColor(color) {
this.themeColor = color;
},
changeTemplate(template) {
this.template = invoiceTemplates[template];
},
changeFont(font) {
this.font = font;
},
updateTemplateView() {
this.usedForReRender += 1;
}
}
};
</script>

View File

@ -0,0 +1,84 @@
const SalesInvoice = require('./SalesInvoiceDocument');
const LedgerPosting = require('../../../accounting/ledgerPosting');
module.exports = class SalesInvoiceServer extends SalesInvoice {
async getPosting() {
let entries = new LedgerPosting({ reference: this, party: this.customer });
await entries.debit(this.account, this.baseGrandTotal);
if (this.isForeignTransaction()) {
for (let item of this.items) {
const baseItemAmount = frappe.format(
frappe.parseNumber(item.amount) * this.exchangeRate,
'Currency'
);
await entries.credit(item.account, baseItemAmount);
}
if (this.taxes) {
for (let tax of this.taxes) {
const baseTaxAmount = frappe.format(
frappe.parseNumber(tax.amount) * this.exchangeRate,
'Currency'
);
await entries.credit(tax.account, baseTaxAmount);
}
}
return entries;
}
for (let item of this.items) {
await entries.credit(item.account, item.amount);
}
if (this.taxes) {
for (let tax of this.taxes) {
await entries.credit(tax.account, tax.amount);
}
}
return entries;
}
async getPayments() {
let payments = await frappe.db.getAll({
doctype: 'PaymentFor',
fields: ['parent'],
filters: { referenceName: this.name },
orderBy: 'name'
});
if (payments.length != 0) {
return payments;
}
return [];
}
async beforeInsert() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async afterSubmit() {
const entries = await this.getPosting();
await entries.post();
await frappe.db.setValue(
'SalesInvoice',
this.name,
'outstandingAmount',
this.baseGrandTotal
);
}
async afterRevert() {
let paymentRefList = await this.getPayments();
for (let paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent;
const payment = await frappe.getDoc('Payment', paymentReference);
const paymentEntries = await payment.getPosting();
await paymentEntries.postReverse();
// To set the payment status as unsubmitted.
payment.revert();
}
const entries = await this.getPosting();
await entries.postReverse();
}
};

View File

@ -0,0 +1,128 @@
<template>
<div :style="$.font" style="font-family: sans-serif;">
<div class="row no-gutters pl-5 pr-5 mt-5">
<div :style="$.regularFontSize" class="col-6">
<company-address />
</div>
<div :style="$.regularFontSize" class="col-6 text-right">
<h2 :style="$.headerFontColor">INVOICE</h2>
<p :style="$.paraStyle">
<strong>{{ doc.name }}</strong>
</p>
<p :style="$.paraStyle">{{ frappe.format(doc.date, 'Date') }}</p>
</div>
</div>
<div class="row pl-5 mt-5">
<div :style="$.regularFontSize" class="col-6 mt-1">
<customer-address :customer="doc.customer" />
</div>
</div>
<div :style="$.regularFontSize" class="row pl-5 pr-5 mt-5">
<div class="col-12">
<table class="table p-0">
<thead>
<tr :style="$.showBorderBottom">
<th :style="$.hideBorderTop" class="text-left pl-0" style="width: 10%">{{ _("NO") }}</th>
<th :style="$.hideBorderTop" class="text-left" style="width: 50%">{{ _("ITEM") }}</th>
<th :style="$.hideBorderTop" class="text-left pl-0" style="width: 15%">{{ _("RATE") }}</th>
<th :style="$.hideBorderTop" class="text-left" style="width: 10%">{{ _("QTY") }}</th>
<th
:style="$.hideBorderTop"
class="text-right pr-1"
style="width: 30%"
>{{ _("AMOUNT") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in doc.items" :key="row.idx">
<td class="text-left pl-1">{{ row.idx + 1 }}</td>
<td class="text-left">{{ row.item }}</td>
<td class="text-left pl-0">{{ row.rate }}</td>
<td class="text-left">{{ row.quantity }}</td>
<td class="text-right pr-1">{{ row.amount }}</td>
</tr>
<tr>
<td colspan="2" class="text-left pl-1"></td>
<td colspan="2" :style="$.bold" class="text-left pl-0">SUBTOTAL</td>
<td :style="$.bold" class="text-right pr-1">{{ doc.netTotal }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="$.bold"
class="text-left pl-0"
>{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.bold" class="text-right pr-1">{{ tax.amount }}</td>
</tr>
<tr>
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="[$.bold, $.mediumFontSize, $.showBorderTop]"
class="text-left pl-0"
>TOTAL</td>
<td
:style="[$.bold, $.mediumFontSize, $.showBorderTop]"
class="text-right pr-1"
style="color: green;"
>{{ doc.grandTotal }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row pl-5 pr-5 mt-5">
<div :style="$.regularFontSize" class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showBorderBottom]">
<td :style="$.hideBorderTop" class="pl-0">NOTICE</td>
</tr>
<tr>
<td class="pl-0">{{ doc.terms }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoiceTemplate1',
components: {
CompanyAddress,
CustomerAddress
},
props: ['doc', 'themeColor', 'font'],
data() {
return {
$: Styles
};
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.headerFontColor.color = this.themeColor;
this.$.showBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.showBorderTop.borderTop = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
};
</script>

View File

@ -0,0 +1,132 @@
<template>
<div :style="[$.regularFontSize, $.font]" style="font-family: sans-serif;">
<div class="row no-gutters p-5" :style="$.headerColor">
<div class="col-8 text-left">
<h1>INVOICE</h1>
</div>
<div class="col-4 text-right">
<company-address />
</div>
</div>
<div class="row p-5 mt-4">
<div class="col-4">
<customer-address :customer="doc.customer" />
</div>
<div class="col-4">
<p :style="[$.bold, $.mediumFontSize]">Invoice Number</p>
<p :style="$.paraStyle">{{ doc.name }}</p>
<br />
<p :style="[$.bold, $.mediumFontSize]">Date</p>
<p :style="$.paraStyle">{{doc.date}}</p>
</div>
<div class="col-4 text-right">
<p :style="[$.bold, $.mediumFontSize]">Invoice Total</p>
<h2 :style="$.fontColor">{{ doc.grandTotal }}</h2>
</div>
</div>
<div class="row pl-5 pr-5 mt-3">
<div class="col-12">
<table class="table table-borderless p-0">
<thead>
<tr :style="[$.showBorderTop, $.fontColor]">
<th class="text-left pl-0" style="width: 10%">{{ _("NO") }}</th>
<th class="text-left" style="width: 50%">{{ _("ITEM") }}</th>
<th class="text-left pl-0" style="width: 15%">{{ _("RATE") }}</th>
<th class="text-left" style="width: 10%">{{ _("QTY") }}</th>
<th class="text-right pr-1" style="width: 30%">{{ _("AMOUNT") }}</th>
</tr>
</thead>
<tbody>
<tr :style="$.showBorderBottom" v-for="row in doc.items" :key="row.idx">
<td class="text-left pl-1">{{ row.idx + 1 }}</td>
<td class="text-left">{{ row.item }}</td>
<td class="text-left pl-0">{{ row.rate }}</td>
<td class="text-left">{{ row.quantity }}</td>
<td class="text-right pr-1">{{ row.amount }}</td>
</tr>
<tr>
<td colspan="5" style="padding: 4%"></td>
</tr>
<tr>
<td colspan="2" class="text-left pl-1"></td>
<td colspan="2" :style="[$.bold, $.fontColor]" class="text-left pl-0">SUBTOTAL</td>
<td :style="$.bold" class="text-right pr-1">{{ doc.netTotal }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="[$.bold, $.fontColor]"
class="text-left pl-0"
>{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.bold" class="text-right pr-1">{{ tax.amount }}</td>
</tr>
<tr>
<td colspan="2" :style="$.hideBorderTop" class="text-left pl-1"></td>
<td
colspan="2"
:style="[$.bold, $.fontColor, $.mediumFontSize]"
class="text-left pl-0"
>TOTAL</td>
<td :style="[$.bold, $.mediumFontSize]" class="text-right pr-1">{{ doc.grandTotal }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row pl-5 pr-5 mt-5">
<div class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showNoticeBorderBottom]">
<td :style="$.hideBorderTop" class="pl-0">NOTICE</td>
</tr>
<tr>
<td class="pl-0">{{ doc.terms }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'font'],
components: {
CompanyAddress,
CustomerAddress
},
data() {
return {
$: Styles
};
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.fontColor.color = this.themeColor;
this.$.headerColor.backgroundColor = this.themeColor;
this.$.showBorderBottom.borderBottom = '0.1rem solid #e0e0d1';
this.$.showNoticeBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.showBorderTop.borderTop = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
};
</script>

View File

@ -0,0 +1,145 @@
<template>
<div :style="[$.regularFontSize, $.font]" style="font-family: sans-serif;">
<div class="row no-gutters mt-5">
<div class="col-6" :style="$.bgColor"></div>
<div class="col-4 text-center" style="vertical-align: middle">
<h1>INVOICE</h1>
</div>
<div class="col-2" :style="$.bgColor"></div>
</div>
<div class="row no-gutters mt-5">
<div class="col-6 text-left pl-5">
<company-address />
</div>
<div class="col-6 pr-5 text-right">
<p :style="[$.bold, $.paraStyle, $.mediumFontSize]">{{ doc.name }}</p>
<p :style="$.paraStyle">{{ frappe.format(doc.date, 'Date') }}</p>
</div>
</div>
<div class="row no-gutters mt-5">
<div class="col-6 text-left pl-5">
<customer-address :customer="doc.customer" />
</div>
<div class="col-6"></div>
</div>
<div class="row mt-5 no-gutters">
<div class="col-12">
<table class="table">
<tbody>
<tr>
<td
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 15"
class="pl-5"
>{{ _("NO") }}</td>
<td
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 40%"
>{{ _("ITEM") }}</td>
<td
class="text-left"
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 20%"
>{{ _("RATE") }}</td>
<td
:style="[$.bold, $.showBorderRight, $.tablePadding]"
style="width: 10%"
>{{ _("QTY") }}</td>
<td
class="text-right pr-5"
:style="[$.bold, $.tablePadding]"
style="width: 20%"
>{{ _("AMOUNT") }}</td>
</tr>
<tr v-for="row in doc.items" :key="row.idx">
<td :style="$.tablePadding" class="pl-5 pr-5">{{ row.idx + 1 }}</td>
<td :style="$.tablePadding">{{ row.item }}</td>
<td
:style="$.tablePadding"
class="text-left"
>{{ frappe.format(row.rate, 'Currency') }}</td>
<td :style="$.tablePadding">{{ row.quantity }}</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ row.amount }}</td>
</tr>
<tr>
<td colspan="5" style="padding: 4%"></td>
</tr>
<tr>
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="$.tablePadding" colspan="2">SUBTOTAL</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ doc.netTotal }}</td>
</tr>
<tr v-for="tax in doc.taxes" :key="tax.name">
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td
:style="$.tablePadding"
med
colspan="2"
>{{ tax.account.toUpperCase() }} ({{ tax.rate }}%)</td>
<td :style="$.tablePadding" class="text-right pr-5">{{ tax.amount }}</td>
</tr>
<tr>
<td colspan="2" :style="[$.hideBorderTop, $.tablePadding]"></td>
<td :style="[$.bold, $.tablePadding, $.mediumFontSize]" colspan="2">TOTAL</td>
<td
:style="[$.bold, $.tablePadding, $.mediumFontSize]"
class="text-right pr-5"
>{{ doc.grandTotal }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row mt-5">
<div :style="$.regularFontSize" class="col-12">
<table class="table">
<tbody>
<tr :style="[$.bold, $.showBorderBottom]">
<td :style="$.hideBorderTop" class="pl-5">NOTICE</td>
</tr>
<tr>
<td class="pl-5">{{ doc.terms }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import Styles from './InvoiceStyles';
import CompanyAddress from './CompanyAddress';
import CustomerAddress from './CustomerAddress';
export default {
name: 'InvoicePrint',
props: ['doc', 'themeColor', 'font'],
components: {
CompanyAddress,
CustomerAddress
},
data() {
return {
$: Styles
};
},
watch: {
themeColor: function() {
this.setTheme();
},
font: function() {
this.setTheme();
}
},
async created() {
this.$ = Styles;
this.setTheme();
},
methods: {
setTheme() {
this.$.bgColor.backgroundColor = this.themeColor;
this.$.showBorderBottom.borderBottom = `0.22rem solid ${this.themeColor}`;
this.$.font.fontFamily = this.font;
}
}
};
</script>

View File

@ -1,11 +1,12 @@
module.exports = {
name: 'InvoiceItem',
name: 'SalesInvoiceItem',
doctype: 'DocType',
isSingle: 0,
isChild: 1,
keywordFields: [],
layout: 'ratio',
fields: [{
fields: [
{
fieldname: 'item',
label: 'Item',
fieldtype: 'Link',
@ -39,6 +40,7 @@ module.exports = {
hidden: 1,
fieldtype: 'Link',
target: 'Account',
required: 1,
formula: (row, doc) => doc.getFrom('Item', row.item, 'incomeAccount')
},
{
@ -57,7 +59,11 @@ module.exports = {
fieldtype: 'Currency',
readOnly: 1,
disabled: true,
formula: (row, doc) => row.quantity * row.rate
formula: async (row, doc) => {
return await doc.formatIntoCustomerCurrency(
row.quantity * frappe.parseNumber(row.rate)
);
}
},
{
fieldname: 'taxAmount',

View File

@ -0,0 +1,42 @@
module.exports = {
name: 'SalesInvoiceSettings',
label: 'SalesInvoice Settings',
doctype: 'DocType',
isSingle: 1,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: 'numberSeries',
label: 'Number Series',
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
default: 'SINV'
},
{
fieldname: 'template',
label: 'Template',
fieldtype: 'Select',
options: ['Basic I', 'Basic II', 'Modern'],
required: 1,
default: 'Basic I'
},
{
fieldname: 'font',
label: 'Font',
fieldtype: 'Select',
options: ['Montserrat', 'Open Sans', 'Oxygen', 'Merriweather'],
required: 1,
default: 'Montserrat'
},
{
fieldname: 'themeColor',
label: 'Theme Color',
fieldtype: 'Data',
required: 1,
default: '#000000',
hidden: 1
}
]
};

View File

@ -1,102 +1,135 @@
const countryList = require('../../../fixtures/countryInfo.json');
module.exports = {
name: "SetupWizard",
label: "Setup Wizard",
naming: "name",
isSingle: 1,
isChild: 0,
isSubmittable: 0,
settings: null,
keywordFields: [],
fields: [{
fieldname: 'country',
label: 'Country',
fieldtype: 'Autocomplete',
required: 1,
getList: () => Object.keys(countryList).sort()
},
name: 'SetupWizard',
label: 'Setup Wizard',
naming: 'name',
isSingle: 1,
isChild: 0,
isSubmittable: 0,
settings: null,
keywordFields: [],
fields: [
{
fieldname: 'country',
label: 'Country',
fieldtype: 'Autocomplete',
required: 1,
getList: () => Object.keys(countryList).sort()
},
{
fieldname: 'fullname',
label: 'Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'fullname',
label: 'Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'email',
label: 'Email',
fieldtype: 'Data',
required: 1,
inputType: 'email'
},
{
fieldname: 'email',
label: 'Email',
fieldtype: 'Data',
required: 1,
inputType: 'email'
},
{
fieldname: 'companyName',
label: 'Company Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'companyName',
label: 'Company Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'bankName',
label: 'Bank Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'bankName',
label: 'Bank Name',
fieldtype: 'Data',
required: 1
},
{
fieldname: 'fiscalYearStart',
label: 'Fiscal Year Start Date',
fieldtype: 'Date',
formula: (doc) => {
let date = countryList[doc.country]["fiscal_year_start"].split("-");
var currentYear = (new Date).getFullYear();
let currentMonth = date[0] - 1 ;
let currentDate = date[1];
return new Date(currentYear,currentMonth,currentDate).toISOString().substr(0, 10);;
},
required: 1
},
{
fieldname: 'fiscalYearStart',
label: 'Fiscal Year Start Date',
fieldtype: 'Date',
formula: doc => {
let date = countryList[doc.country]['fiscal_year_start'].split('-');
var currentYear = new Date().getFullYear();
let currentMonth = date[0] - 1;
let currentDate = date[1];
return new Date(currentYear, currentMonth, currentDate)
.toISOString()
.substr(0, 10);
},
required: 1
},
{
fieldname: 'fiscalYearEnd',
label: 'Fiscal Year End Date',
fieldtype: 'Date',
formula: (doc) => {
let date = countryList[doc.country]["fiscal_year_end"].split("-");
var currentYear = (new Date).getFullYear() + 1 ;
let currentMonth = date[0] - 1 ;
let currentDate = date[1];
return new Date(currentYear,currentMonth,currentDate).toISOString().substr(0, 10);;
},
required: 1
}
],
layout: {
paginated: true,
sections: [{
title: 'Select Country',
columns: [{
fields: ['country']
}]
},
{
title: 'Add a Profile',
columns: [{
fields: ['fullname', 'email']
}]
},
{
title: 'Add your Company',
columns: [{
fields: ['companyName', 'bankName', 'fiscalYearStart', 'fiscalYearEnd']
}]
}
].filter(Boolean)
{
fieldname: 'fiscalYearEnd',
label: 'Fiscal Year End Date',
fieldtype: 'Date',
formula: doc => {
let date = countryList[doc.country]['fiscal_year_end'].split('-');
var currentYear = new Date().getFullYear() + 1;
let currentMonth = date[0] - 1;
let currentDate = date[1];
return new Date(currentYear, currentMonth, currentDate)
.toISOString()
.substr(0, 10);
},
required: 1
},
{
fieldname: 'currency',
label: 'Currency',
fieldtype: 'Data',
formula: doc => {
return countryList[doc.country].currency;
},
required: 1
},
{
fieldname: 'completed',
label: 'Completed',
fieldtype: 'Check',
readonly: 1
}
}
],
layout: {
paginated: true,
sections: [
{
title: 'Select Country',
columns: [
{
fields: ['country']
}
]
},
{
title: 'Add a Profile',
columns: [
{
fields: ['fullname', 'email']
}
]
},
{
title: 'Add your Company',
columns: [
{
fields: [
'companyName',
'bankName',
'currency',
'fiscalYearStart',
'fiscalYearEnd'
]
}
]
}
].filter(Boolean)
}
};

View File

@ -0,0 +1,41 @@
module.exports = async function generateTaxes(country) {
if (country === 'India') {
const GSTs = {
GST: [28, 18, 12, 6, 5, 3, 0.25, 0],
IGST: [28, 18, 12, 6, 5, 3, 0.25, 0],
'Exempt-GST': [0],
'Exempt-IGST': [0]
};
let newTax = await frappe.getNewDoc('Tax');
for (const type of Object.keys(GSTs)) {
for (const percent of GSTs[type]) {
if (type === 'GST') {
await newTax.set({
name: `${type}-${percent}`,
details: [
{
account: 'CGST',
rate: percent / 2
},
{
account: 'SGST',
rate: percent / 2
}
]
});
} else {
await newTax.set({
name: `${type}-${percent}`,
details: [
{
account: type.toString().split('-')[0],
rate: percent
}
]
});
}
await newTax.insert();
}
}
}
};

View File

@ -1,10 +1,7 @@
import { _ } from 'frappejs/utils';
import indicators from 'frappejs/ui/constants/indicators';
export default {
doctype: 'Tax',
title: _('Tax'),
columns: [
'name'
]
}
columns: ['name']
};

View File

View File

@ -3,67 +3,72 @@ const path = require('path');
const fs = require('fs');
const countries = require('../../../fixtures/countryInfo.json');
const standardCOA = require('../../../fixtures/verified/standardCOA.json');
const accountFields = ['accountType', 'rootType', 'isGroup', 'account_type', 'root_type', 'is_group'];
const accountFields = [
'accountType',
'rootType',
'isGroup',
'account_type',
'root_type',
'is_group'
];
async function importAccounts(children, parent, rootType, rootAccount) {
for (let accountName in children) {
const child = children[accountName];
for (let accountName in children) {
const child = children[accountName];
if (rootAccount) {
rootType = child.rootType || child.root_type;
}
if (!accountFields.includes(accountName)) {
let isGroup = identifyIsGroup(child);
const doc = frappe.newDoc({
doctype: 'Account',
name: accountName,
parentAccount: parent,
isGroup,
rootType,
balance: 0,
accountType: child.accountType
})
await doc.insert()
await importAccounts(child, accountName, rootType)
}
if (rootAccount) {
rootType = child.rootType || child.root_type;
}
if (!accountFields.includes(accountName)) {
let isGroup = identifyIsGroup(child);
const doc = frappe.newDoc({
doctype: 'Account',
name: accountName,
parentAccount: parent,
isGroup,
rootType,
balance: 0,
accountType: child.accountType || child.account_type
});
await doc.insert();
await importAccounts(child, accountName, rootType);
}
}
}
function identifyIsGroup(child) {
if (child.isGroup || child.is_group) {
return child.isGroup || child.is_group;
}
if (child.isGroup || child.is_group) {
return child.isGroup || child.is_group;
}
const keys = Object.keys(child);
const children = keys.filter(key => !accountFields.includes(key))
const keys = Object.keys(child);
const children = keys.filter(key => !accountFields.includes(key));
if (children.length) {
return 1;
}
if (children.length) {
return 1;
}
return 0;
return 0;
}
async function getCountryCOA(){
const doc = await frappe.getDoc('AccountingSettings');
const conCode = countries[doc.country].code;
async function getCountryCOA() {
const doc = await frappe.getDoc('AccountingSettings');
const conCode = countries[doc.country].code;
const countryCOA = path.resolve(path.join('./fixtures/verified/', conCode + '.json'));
if(fs.existsSync(countryCOA)){
const jsonText = fs.readFileSync(countryCOA, 'utf-8');
const json = JSON.parse(jsonText);
return json.tree;
} else {
return standardCOA;
}
try {
const countryCoa = require('../../../fixtures/verified/' +
conCode +
'.json');
return countryCoa.tree;
} catch (e) {
return standardCOA;
}
}
module.exports = async function importCharts() {
const chart = await getCountryCOA();
await importAccounts(chart, '', '', true)
}
const chart = await getCountryCOA();
await importAccounts(chart, '', '', true);
};

View File

@ -1,62 +1,68 @@
module.exports = {
models: {
SetupWizard: require('./doctype/SetupWizard/SetupWizard'),
Account: require('./doctype/Account/Account.js'),
AccountingSettings: require('./doctype/AccountingSettings/AccountingSettings'),
CompanySettings: require('./doctype/CompanySettings/CompanySettings'),
AccountingLedgerEntry: require('./doctype/AccountingLedgerEntry/AccountingLedgerEntry.js'),
Party: require('./doctype/Party/Party.js'),
models: {
SetupWizard: require('./doctype/SetupWizard/SetupWizard'),
DashboardSettings: require('./doctype/DashboardSettings/DashboardSettings'),
DashboardChart: require('./doctype/DashboardChart/DashboardChart'),
Currency: require('./doctype/Currency/Currency'),
Color: require('./doctype/Color/Color'),
Account: require('./doctype/Account/Account.js'),
AccountingSettings: require('./doctype/AccountingSettings/AccountingSettings'),
CompanySettings: require('./doctype/CompanySettings/CompanySettings'),
AccountingLedgerEntry: require('./doctype/AccountingLedgerEntry/AccountingLedgerEntry.js'),
Party: require('./doctype/Party/Party.js'),
Payment: require('./doctype/Payment/Payment.js'),
PaymentFor: require('./doctype/PaymentFor/PaymentFor.js'),
PaymentSettings: require('./doctype/PaymentSettings/PaymentSettings.js'),
Payment: require('./doctype/Payment/Payment.js'),
PaymentFor: require('./doctype/PaymentFor/PaymentFor.js'),
PaymentSettings: require('./doctype/PaymentSettings/PaymentSettings.js'),
Item: require('./doctype/Item/Item.js'),
Item: require('./doctype/Item/Item.js'),
Invoice: require('./doctype/Invoice/Invoice.js'),
InvoiceItem: require('./doctype/InvoiceItem/InvoiceItem.js'),
InvoiceSettings: require('./doctype/InvoiceSettings/InvoiceSettings.js'),
SalesInvoice: require('./doctype/SalesInvoice/SalesInvoice.js'),
SalesInvoiceItem: require('./doctype/SalesInvoiceItem/SalesInvoiceItem.js'),
SalesInvoiceSettings: require('./doctype/SalesInvoiceSettings/SalesInvoiceSettings.js'),
Bill: require('./doctype/Bill/Bill.js'),
BillItem: require('./doctype/BillItem/BillItem.js'),
BillSettings: require('./doctype/BillSettings/BillSettings.js'),
PurchaseInvoice: require('./doctype/PurchaseInvoice/PurchaseInvoice.js'),
PurchaseInvoiceItem: require('./doctype/PurchaseInvoiceItem/PurchaseInvoiceItem.js'),
PurchaseInvoiceSettings: require('./doctype/PurchaseInvoiceSettings/PurchaseInvoiceSettings.js'),
Tax: require('./doctype/Tax/Tax.js'),
TaxDetail: require('./doctype/TaxDetail/TaxDetail.js'),
TaxSummary: require('./doctype/TaxSummary/TaxSummary.js'),
Tax: require('./doctype/Tax/Tax.js'),
TaxDetail: require('./doctype/TaxDetail/TaxDetail.js'),
TaxSummary: require('./doctype/TaxSummary/TaxSummary.js'),
Address: require('./doctype/Address/Address.js'),
Contact: require('./doctype/Contact/Contact.js'),
GSTR3B: require('./doctype/GSTR3B/GSTR3B.js'),
JournalEntry: require('./doctype/JournalEntry/JournalEntry.js'),
JournalEntryAccount: require('./doctype/JournalEntryAccount/JournalEntryAccount.js'),
JournalEntrySettings: require('./doctype/JournalEntrySettings/JournalEntrySettings.js'),
Address: require('./doctype/Address/Address.js'),
Contact: require('./doctype/Contact/Contact.js'),
Quotation: require('./doctype/Quotation/Quotation.js'),
QuotationItem: require('./doctype/QuotationItem/QuotationItem.js'),
QuotationSettings: require('./doctype/QuotationSettings/QuotationSettings.js'),
JournalEntry: require('./doctype/JournalEntry/JournalEntry.js'),
JournalEntryAccount: require('./doctype/JournalEntryAccount/JournalEntryAccount.js'),
JournalEntrySettings: require('./doctype/JournalEntrySettings/JournalEntrySettings.js'),
SalesOrder: require('./doctype/SalesOrder/SalesOrder.js'),
SalesOrderItem: require('./doctype/SalesOrderItem/SalesOrderItem.js'),
SalesOrderSettings: require('./doctype/SalesOrderSettings/SalesOrderSettings.js'),
Quotation: require('./doctype/Quotation/Quotation.js'),
QuotationItem: require('./doctype/QuotationItem/QuotationItem.js'),
QuotationSettings: require('./doctype/QuotationSettings/QuotationSettings.js'),
Fulfillment: require('./doctype/Fulfillment/Fulfillment.js'),
FulfillmentItem: require('./doctype/FulfillmentItem/FulfillmentItem.js'),
FulfillmentSettings: require('./doctype/FulfillmentSettings/FulfillmentSettings.js'),
SalesOrder: require('./doctype/SalesOrder/SalesOrder.js'),
SalesOrderItem: require('./doctype/SalesOrderItem/SalesOrderItem.js'),
SalesOrderSettings: require('./doctype/SalesOrderSettings/SalesOrderSettings.js'),
PurchaseOrder: require('./doctype/PurchaseOrder/PurchaseOrder.js'),
PurchaseOrderItem: require('./doctype/PurchaseOrderItem/PurchaseOrderItem.js'),
PurchaseOrderSettings: require('./doctype/PurchaseOrderSettings/PurchaseOrderSettings.js'),
Fulfillment: require('./doctype/Fulfillment/Fulfillment.js'),
FulfillmentItem: require('./doctype/FulfillmentItem/FulfillmentItem.js'),
FulfillmentSettings: require('./doctype/FulfillmentSettings/FulfillmentSettings.js'),
PurchaseReceipt: require('./doctype/PurchaseReceipt/PurchaseReceipt.js'),
PurchaseReceiptItem: require('./doctype/PurchaseReceiptItem/PurchaseReceiptItem.js'),
PurchaseReceiptSettings: require('./doctype/PurchaseReceiptSettings/PurchaseReceiptSettings.js'),
PurchaseOrder: require('./doctype/PurchaseOrder/PurchaseOrder.js'),
PurchaseOrderItem: require('./doctype/PurchaseOrderItem/PurchaseOrderItem.js'),
PurchaseOrderSettings: require('./doctype/PurchaseOrderSettings/PurchaseOrderSettings.js'),
Event: require('./doctype/Event/Event'),
EventSchedule: require('./doctype/EventSchedule/EventSchedule'),
EventSettings: require('./doctype/EventSettings/EventSettings'),
PurchaseReceipt: require('./doctype/PurchaseReceipt/PurchaseReceipt.js'),
PurchaseReceiptItem: require('./doctype/PurchaseReceiptItem/PurchaseReceiptItem.js'),
PurchaseReceiptSettings: require('./doctype/PurchaseReceiptSettings/PurchaseReceiptSettings.js'),
Email: require('./doctype/Email/Email'),
EmailAccount: require('./doctype/EmailAccount/EmailAccount'),
}
}
Event: require('./doctype/Event/Event'),
EventSchedule: require('./doctype/EventSchedule/EventSchedule'),
EventSettings: require('./doctype/EventSettings/EventSettings'),
Email: require('./doctype/Email/Email'),
EmailAccount: require('./doctype/EmailAccount/EmailAccount')
}
};

9879
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,8 @@
"dist/electron"
],
"dmg": {
"contents": [{
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
@ -58,10 +59,12 @@
},
"dependencies": {
"cross-env": "^5.2.0",
"frappejs": "github:frappe/frappejs",
"file-saver": "^2.0.2",
"frappe-charts": "^1.2.4",
"frappejs": "https://github.com/thefalconx33/frappejs#test",
"nodemailer": "^4.7.0",
"popper.js": "^1.14.4",
"vue-color": "^2.7.0",
"vue-toasted": "^1.1.25"
}
}
}

View File

@ -1,154 +1,170 @@
const frappe = require('frappejs');
module.exports = class AccountsReceivablePayable {
async run(reportType, { date }) {
async run(reportType, { date }) {
const rows = await getReceivablePayable({
reportType,
date
});
const rows = await getReceivablePayable({
reportType,
date
});
return { rows };
}
}
return { rows };
}
};
async function getReceivablePayable({ reportType = 'Receivable', date }) {
let entries = [];
const debitOrCredit = reportType === 'Receivable' ? 'debit' : 'credit';
const referenceType = reportType === 'Receivable' ? 'Invoice' : 'Bill';
let entries = [];
const debitOrCredit = reportType === 'Receivable' ? 'debit' : 'credit';
const referenceType =
reportType === 'Receivable' ? 'SalesInvoice' : 'PurchaseInvoice';
entries = await getLedgerEntries();
const vouchers = await getVouchers();
entries = await getLedgerEntries();
const vouchers = await getVouchers();
const futureEntries = getFutureEntries();
const returnEntries = getReturnEntries();
const pdc = getPDC();
const futureEntries = getFutureEntries();
const returnEntries = getReturnEntries();
const pdc = getPDC();
const validEntries = getValidEntries();
const validEntries = getValidEntries();
let data = [];
let data = [];
for (let entry of validEntries) {
for (let entry of validEntries) {
const { outStandingAmount, creditNoteAmount } = getOutstandingAmount(entry);
const { outStandingAmount, creditNoteAmount } = getOutstandingAmount(entry);
console.log(outStandingAmount);
console.log(outStandingAmount);
if (outStandingAmount > 0.1 / 10) {
const row = {
date: entry.date,
party: entry.party
};
if (outStandingAmount > 0.1 / 10) {
const row = {
date: entry.date,
party: entry.party
};
// due date / bill date
// due date / bill date
row.voucherType = entry.referenceType;
row.voucherNo = entry.referenceName;
row.voucherType = entry.referenceType;
row.voucherNo = entry.referenceName;
// bill details
// bill details
const invoicedAmount = entry[debitOrCredit] || 0;
const paidAmount = invoicedAmount - outStandingAmount - creditNoteAmount;
const invoicedAmount = entry[debitOrCredit] || 0;
const paidAmount = invoicedAmount - outStandingAmount - creditNoteAmount;
Object.assign(row, {
invoicedAmount,
paidAmount,
outStandingAmount,
creditNoteAmount
});
Object.assign(row, {
invoicedAmount,
paidAmount,
outStandingAmount,
creditNoteAmount
});
// ageing
// ageing
data.push(row);
}
}
data.push(row);
return data;
// helpers
async function getVouchers() {
return await frappe.db.getAll({
doctype: referenceType,
fields: ['name', 'date'],
filters: {
submitted: 1
}
});
}
function getValidEntries() {
return entries.filter(entry => {
return (
entry.date <= date &&
entry.referenceType === referenceType &&
entry[debitOrCredit] > 0
);
});
}
function getOutstandingAmount(entry) {
let paymentAmount = 0.0,
creditNoteAmount = 0.0;
let reverseDebitOrCredit = debitOrCredit === 'debit' ? 'credit' : 'debit';
for (let e of getEntriesFor(
entry.party,
entry.referenceType,
entry.referenceName
)) {
if (e.date <= date) {
const amount = e[reverseDebitOrCredit] - e[debitOrCredit];
if (!Object.keys(returnEntries).includes(e.referenceName)) {
paymentAmount += amount;
} else {
creditNoteAmount += amount;
}
}
}
return data;
return {
outStandingAmount:
entry[debitOrCredit] -
entry[reverseDebitOrCredit] -
paymentAmount -
creditNoteAmount,
creditNoteAmount
};
}
// helpers
function getEntriesFor(party, againstVoucherType, againstVoucher) {
// TODO
return [];
}
async function getVouchers() {
return await frappe.db.getAll({
doctype: referenceType,
fields: ['name', 'date'],
filters: {
submitted: 1
}
});
function getFutureEntries() {
return entries.filter(entry => entry.date > date);
}
function getReturnEntries() {
// TODO
return {};
}
function getPDC() {
return [];
}
async function getLedgerEntries() {
if (entries.length) {
return entries;
}
function getValidEntries() {
return entries.filter(entry => {
return (
entry.date <= date &&
entry.referenceType === referenceType && entry[debitOrCredit] > 0
);
});
}
const partyType = reportType === 'Receivable' ? 'customer' : 'supplier';
const partyList = (await frappe.db.getAll({
doctype: 'Party',
filters: {
[partyType]: 1
}
})).map(d => d.name);
function getOutstandingAmount(entry) {
let paymentAmount = 0.0, creditNoteAmount = 0.0;
let reverseDebitOrCredit = debitOrCredit === 'debit' ? 'credit' : 'debit';
for (let e of getEntriesFor(entry.party, entry.referenceType, entry.referenceName)) {
if (e.date <= date) {
const amount = e[reverseDebitOrCredit] - e[debitOrCredit];
if (!Object.keys(returnEntries).includes(e.referenceName)) {
paymentAmount += amount;
} else {
creditNoteAmount += amount;
}
}
}
return {
outStandingAmount: (entry[debitOrCredit] - entry[reverseDebitOrCredit]) - paymentAmount - creditNoteAmount,
creditNoteAmount
}
}
function getEntriesFor(party, againstVoucherType, againstVoucher) {
// TODO
return []
}
function getFutureEntries() {
return entries.filter(entry => entry.date > date);
}
function getReturnEntries() {
// TODO
return {};
}
function getPDC() {
return [];
}
async function getLedgerEntries() {
if (entries.length) {
return entries;
}
const partyType = reportType === 'Receivable' ? 'customer': 'supplier'
const partyList = (await frappe.db.getAll({
doctype: 'Party',
filters: {
[partyType]: 1
}
})).map(d => d.name);
return await frappe.db.getAll({
doctype: 'AccountingLedgerEntry',
fields: ['name', 'date', 'account', 'party',
'referenceType', 'referenceName',
'sum(debit) as debit', 'sum(credit) as credit'],
filters: {
party: ['in', partyList]
},
groupBy: ['referenceType', 'referenceName', 'party'],
orderBy: 'date'
});
}
return await frappe.db.getAll({
doctype: 'AccountingLedgerEntry',
fields: [
'name',
'date',
'account',
'party',
'referenceType',
'referenceName',
'sum(debit) as debit',
'sum(credit) as credit'
],
filters: {
party: ['in', partyList]
},
groupBy: ['referenceType', 'referenceName', 'party'],
orderBy: 'date'
});
}
}

View File

@ -0,0 +1,46 @@
module.exports = {
title: 'Balance Sheet',
method: 'balance-sheet',
filterFields: [
{
fieldtype: 'Date',
fieldname: 'toDate',
size: 'small',
placeholder: 'ToDate',
label: 'To Date',
required: 1
},
{
fieldtype: 'Select',
size: 'small',
options: [
'Select Period...',
'Monthly',
'Quarterly',
'Half Yearly',
'Yearly'
],
label: 'Periodicity',
fieldname: 'periodicity',
default: 'Monthly'
}
],
getColumns(data) {
const columns = [
{ label: 'Account', fieldtype: 'Data', fieldname: 'account', width: 340 }
];
if (data && data.columns) {
const currencyColumns = data.columns;
const columnDefs = currencyColumns.map(name => ({
label: name,
fieldname: name,
fieldtype: 'Currency'
}));
columns.push(...columnDefs);
}
return columns;
}
};

View File

@ -2,21 +2,32 @@ const frappe = require('frappejs');
class BankReconciliation {
async run(params) {
if (!Object.keys(params).length) return [];
const filters = {};
if (params.paymentAccount) filters.paymentAccount = params.paymentAccount;
if (params.party) filters.party = params.party;
// if (params.referenceType) filters.referenceType = params.referenceType;
// if (params.referenceName) filters.referenceName = params.referenceName;
if (params.toDate || params.fromDate) {
filters.date = [];
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
filters.paymentMethod = ['in', ['Cheque', 'Transfer']];
let data = await frappe.db.getAll({
doctype: 'Payment',
fields: ['date', 'account', 'paymentAccount', 'party', 'name', 'referenceDate','clearanceDate'],
filters: filters,
fields: [
'date',
'account',
'paymentAccount',
'party',
'name',
'referenceDate',
'referenceId',
'clearanceDate'
],
filters: filters
});
for (var i = 0; i < data.length; i++) {
@ -28,9 +39,10 @@ class BankReconciliation {
account: data[i].paymentAccount,
referenceName: data[i].name
}
})
});
data[i].credit = ledger[0].credit;
data[i].debit = ledger[0].debit;
data[i].debit = ledger[0].debit;
data[i].referenceName = ledger[0].referenceName;
data[i].referenceType = ledger[0].referenceType;
}

View File

@ -0,0 +1,96 @@
const csv2json = require('csvjson-csv2json');
const frappe = require('frappejs');
import Vue from 'vue';
export const fileImportHandler = (file, report) => {
const reader = new FileReader();
reader.onload = () => {
const csv = reader.result;
const json = csvToJsonHandler(csv);
findMatchingReferences(json, report);
};
reader.readAsBinaryString(file);
};
export const csvToJsonHandler = csv => {
const json = csv2json(csv, { parseNumbers: true });
return json;
};
export const findMatchingReferences = async (json, report) => {
const referenceField = Object.keys(json[0]).filter(field => {
return field.toLowerCase().indexOf('ref') > -1 ? true : false;
});
const clearanceDateField = Object.keys(json[0]).filter(field => {
return field.toLowerCase().indexOf('date') > -1 ? true : false;
});
const debitField = Object.keys(json[0]).filter(field => {
return field.toLowerCase().indexOf('debit') > -1 ||
field.toLowerCase().indexOf('deposit') > -1
? true
: false;
});
const creditField = Object.keys(json[0]).filter(field => {
return field.toLowerCase().indexOf('credit') > -1 ||
field.toLowerCase().indexOf('withdraw') > -1
? true
: false;
});
const balanceField = Object.keys(json[0]).filter(field => {
return field.toLowerCase().indexOf('balance') > -1 ? true : false;
});
const references = json.map(row => {
return row[referenceField];
});
const payments = await frappe.db.getAll({
doctype: 'Payment',
fields: ['*'],
filters: {
referenceId: ['in', references],
paymentAccount: report.currentFilters.paymentAccount,
clearanceDate: ['in', [null, undefined, '']]
}
});
if (payments.length) {
const entries = payments.map(payment => {
const jsonEntry = json.filter(row => {
return row[referenceField] === payment.referenceId;
});
return Object.assign(payment, jsonEntry[0]);
});
const normalizedEntries = entries.map(entry => {
return {
'Posting Date': frappe.format(entry.date, 'Date'),
'Payment Entry': entry.name,
'Ref/Cheq. ID': entry[referenceField],
'Cr/Dr':
frappe.parseNumber(entry[debitField]) > 0
? entry[debitField] + ' Dr.'
: entry[creditField] + ' Cr.',
'Clearance Date': entry[clearanceDateField]
};
});
report.$modal.show({
modalProps: {
title: `Validate Matching Entries`,
noFooter: true
},
component: require('../../src/components/ReconciliationValidation')
.default,
props: {
entries: normalizedEntries,
afterReconcile: async () => {
await report.getReportData(report.currentFilters);
}
}
});
} else {
frappe.call({
method: 'show-dialog',
args: {
title: 'Message',
message: 'No entries found with matching Ref / Cheque ID'
}
});
}
};

View File

@ -2,31 +2,76 @@ const title = 'Bank Reconciliation';
module.exports = {
title: title,
method: 'bank-reconciliation',
filterFields: [{
filterFields: [
{
fieldtype: 'Link',
target: 'Account',
label: 'Payement Account',
fieldname: 'paymentAccount'
size: 'small',
placeholder: 'Payment Account',
label: 'Payment Account',
fieldname: 'paymentAccount',
getFilters: () => {
return {
accountType: 'Bank',
isGroup: 0
};
}
},
{
fieldtype: 'Link',
target: 'Party',
size: 'small',
label: 'Party',
placeholder: 'Party',
fieldname: 'party'
},
{
fieldtype: 'Date',
size: 'small',
placeholder: 'From Date',
label: 'From Date',
fieldname: 'fromDate'
},
{
fieldtype: 'Date',
size: 'small',
placeholder: 'To Date',
label: 'To Date',
fieldname: 'toDate'
}
],
linkFields: [
{
label: 'Reconcile',
type: 'secondary',
condition: report => report.currentFilters.paymentAccount,
action: async report => {
report.$modal.show({
modalProps: {
title: `Import Bank Account Statement`,
noFooter: true
},
component: require('../../src/components/ImportWizard').default,
props: {
importHandler: require('./BankReconciliationImport')
.fileImportHandler,
report
}
});
}
},
{
label: 'Clear Filters',
type: 'secondary',
action: async report => {
await report.getReportData({});
report.usedToReRender += 1;
}
}
],
getColumns() {
return [{
return [
{
label: 'Posting Date',
fieldtype: 'Date',
fieldname: 'date'
@ -47,6 +92,16 @@ module.exports = {
label: 'Balance',
fieldtype: 'Currency'
},
{
label: 'Ref/Cheque ID',
fieldtype: 'Data',
fieldname: 'referenceId'
},
{
label: 'Clearance Date',
fieldtype: 'Date',
fieldname: 'clearanceDate'
},
{
label: 'Ref. Type',
fieldtype: 'Data',
@ -62,11 +117,7 @@ module.exports = {
fieldtype: 'Date',
fieldname: 'referenceDate'
},
{
label: 'Clearance Date',
fieldtype: 'Date',
fieldname: 'clearanceDate'
},
{
label: 'Party',
fieldtype: 'Link'

View File

@ -3,249 +3,288 @@ const { DateTime } = require('luxon');
const { unique } = require('frappejs/utils');
async function getData({
rootType,
balanceMustBe = 'Debit',
fromDate,
toDate,
periodicity = 'Monthly',
accumulateValues = false
rootType,
balanceMustBe = 'Debit',
fromDate,
toDate,
periodicity = 'Monthly',
accumulateValues = false
}) {
let accounts = await getAccounts(rootType);
let fiscalYear = await getFiscalYear();
let ledgerEntries = await getLedgerEntries(fromDate, toDate, accounts);
let periodList = getPeriodList(fromDate, toDate, periodicity, fiscalYear);
let accounts = await getAccounts(rootType);
let fiscalYear = await getFiscalYear();
let ledgerEntries = await getLedgerEntries(fromDate, toDate, accounts);
let periodList = getPeriodList(fromDate, toDate, periodicity, fiscalYear);
for (let account of accounts) {
const entries = ledgerEntries.filter(
entry => entry.account === account.name
);
for (let entry of entries) {
let periodKey = getPeriodKey(entry.date, periodicity);
if (!account[periodKey]) {
account[periodKey] = 0.0;
}
const multiplier = balanceMustBe === 'Debit' ? 1 : -1;
const value = (entry.debit - entry.credit) * multiplier;
account[periodKey] += value;
}
}
if (accumulateValues) {
periodList.forEach((periodKey, i) => {
if (i === 0) return;
const previousPeriodKey = periodList[i - 1];
for (let account of accounts) {
if (!account[periodKey]) {
account[periodKey] = 0.0;
}
account[periodKey] += account[previousPeriodKey] || 0.0;
}
});
}
// calculate totalRow
let totalRow = {
account: `Total ${rootType} (${balanceMustBe})`
};
periodList.forEach(periodKey => {
if (!totalRow[periodKey]) {
totalRow[periodKey] = 0.0;
}
for (let account of accounts) {
const entries = ledgerEntries.filter(entry => entry.account === account.name);
for (let entry of entries) {
let periodKey = getPeriodKey(entry.date, periodicity);
if (!account[periodKey]) {
account[periodKey] = 0.0;
}
const multiplier = balanceMustBe === 'Debit' ? 1 : -1;
const value = (entry.debit - entry.credit) * multiplier
account[periodKey] += value;
}
totalRow[periodKey] += account[periodKey] || 0.0;
}
});
if (accumulateValues) {
periodList.forEach((periodKey, i) => {
if (i === 0) return;
const previousPeriodKey = periodList[i - 1];
for (let account of accounts) {
if (!account[periodKey]) {
account[periodKey] = 0.0;
}
account[periodKey] += account[previousPeriodKey] || 0.0;
}
});
}
// calculate totalRow
let totalRow = {
account: `Total ${rootType} (${balanceMustBe})`
};
periodList.forEach((periodKey) => {
if (!totalRow[periodKey]) {
totalRow[periodKey] = 0.0;
}
for (let account of accounts) {
totalRow[periodKey] += account[periodKey] || 0.0;
}
});
return { accounts, totalRow, periodList };
return { accounts, totalRow, periodList };
}
async function getTrialBalance({ rootType, fromDate, toDate }) {
let accounts = await getAccounts(rootType);
let ledgerEntries = await getLedgerEntries(null, toDate, accounts);
let accounts = await getAccounts(rootType);
let ledgerEntries = await getLedgerEntries(null, toDate, accounts);
for (let account of accounts) {
const accountEntries = ledgerEntries.filter(entry => entry.account === account.name);
// opening
const beforePeriodEntries = accountEntries.filter(entry => entry.date < fromDate);
account.opening = beforePeriodEntries.reduce((acc, entry) => {
return acc + (entry.debit - entry.credit);
}, 0);
for (let account of accounts) {
const accountEntries = ledgerEntries.filter(
entry => entry.account === account.name
);
// opening
const beforePeriodEntries = accountEntries.filter(
entry => entry.date < fromDate
);
account.opening = beforePeriodEntries.reduce((acc, entry) => {
return acc + (entry.debit - entry.credit);
}, 0);
if (account.opening >= 0) {
account.openingDebit = account.opening;
} else {
account.openingCredit = -account.opening;
}
// debit / credit
const periodEntries = accountEntries.filter(entry => entry.date >= fromDate);
account.debit = periodEntries.reduce((acc, entry) => acc + entry.debit, 0);
account.credit = periodEntries.reduce((acc, entry) => acc + entry.credit, 0);
// closing
account.closing = account.opening + account.debit - account.credit;
if (account.closing >= 0) {
account.closingDebit = account.closing;
} else {
account.closingCredit = -account.closing;
}
if (account.opening >= 0) {
account.openingDebit = account.opening;
} else {
account.openingCredit = -account.opening;
}
return accounts;
// debit / credit
const periodEntries = accountEntries.filter(
entry => entry.date >= fromDate && entry.date < toDate
);
account.debit = periodEntries.reduce((acc, entry) => acc + entry.debit, 0);
account.credit = periodEntries.reduce(
(acc, entry) => acc + entry.credit,
0
);
// closing
account.closing = account.opening + account.debit - account.credit;
if (account.closing >= 0) {
account.closingDebit = account.closing;
} else {
account.closingCredit = -account.closing;
}
if (account.debit != 0 || account.credit != 0) {
setParentEntry(account, account.parentAccount);
}
}
function setParentEntry(leafAccount, parentName) {
for (let acc of accounts) {
if (acc.name === parentName) {
acc.debit += leafAccount.debit;
acc.credit += leafAccount.credit;
acc.closing = acc.opening + acc.debit - acc.credit;
if (acc.closing >= 0) {
acc.closingDebit = acc.closing;
} else {
acc.closingCredit = -acc.closing;
}
if (acc.parentAccount) {
setParentEntry(leafAccount, acc.parentAccount);
} else {
return;
}
}
}
}
return accounts;
}
function getPeriodList(fromDate, toDate, periodicity, fiscalYear) {
if (!fromDate) {
fromDate = fiscalYear.start;
}
if (!fromDate) {
fromDate = fiscalYear.start;
}
let monthsToAdd = {
'Monthly': 1,
'Quarterly': 3,
'Half Yearly': 6,
'Yearly': 12
}[periodicity];
let monthsToAdd = {
Monthly: 1,
Quarterly: 3,
'Half Yearly': 6,
Yearly: 12
}[periodicity];
let startDate = DateTime.fromISO(fromDate).startOf('month');
let endDate = DateTime.fromISO(toDate).endOf('month');
let curDate = startDate;
let out = [];
let startDate = DateTime.fromISO(fromDate).startOf('month');
let endDate = DateTime.fromISO(toDate).endOf('month');
let curDate = startDate;
let out = [];
while (curDate <= endDate) {
out.push(getPeriodKey(curDate, periodicity));
curDate = curDate.plus({ months: monthsToAdd });
}
while (curDate <= endDate) {
out.push(getPeriodKey(curDate, periodicity));
curDate = curDate.plus({ months: monthsToAdd });
}
return out;
return out;
}
function getPeriodKey(date, periodicity) {
let key;
let dateObj = DateTime.fromISO(date);
let year = dateObj.year;
let quarter = dateObj.quarter;
let month = dateObj.month;
let key;
let dateObj = DateTime.fromISO(date);
let year = dateObj.year;
let quarter = dateObj.quarter;
let month = dateObj.month;
let getKey = {
'Monthly': () => `${dateObj.monthShort} ${year}`,
'Quarterly': () => {
return {
1: `Jan ${year} - Mar ${year}`,
2: `Apr ${year} - Jun ${year}`,
3: `Jun ${year} - Sep ${year}`,
4: `Oct ${year} - Dec ${year}`
}[quarter]
},
'Half Yearly': () => {
return {
1: `Apr ${year} - Sep ${year}`,
2: `Oct ${year} - Mar ${year}`
}[[2, 3].includes(quarter) ? 1 : 2]
},
'Yearly': () => {
if (month > 3) {
return `${year} - ${year + 1}`
}
return `${year - 1} - ${year}`
}
}[periodicity];
let getKey = {
Monthly: () => `${dateObj.monthShort} ${year}`,
Quarterly: () => {
return {
1: `Jan ${year} - Mar ${year}`,
2: `Apr ${year} - Jun ${year}`,
3: `Jun ${year} - Sep ${year}`,
4: `Oct ${year} - Dec ${year}`
}[quarter];
},
'Half Yearly': () => {
return {
1: `Apr ${year} - Sep ${year}`,
2: `Oct ${year} - Mar ${year}`
}[[2, 3].includes(quarter) ? 1 : 2];
},
Yearly: () => {
if (month > 3) {
return `${year} - ${year + 1}`;
}
return `${year - 1} - ${year}`;
}
}[periodicity];
return getKey();
return getKey();
}
function setIndentLevel(accounts, parentAccount, level) {
if (!parentAccount) {
// root
parentAccount = null;
level = 0;
if (!parentAccount) {
// root
parentAccount = null;
level = 0;
}
accounts.forEach(account => {
if (
account.parentAccount === parentAccount &&
account.indent === undefined
) {
account.indent = level;
setIndentLevel(accounts, account.name, level + 1);
}
});
accounts.forEach(account => {
if (account.parentAccount === parentAccount && account.indent === undefined) {
account.indent = level;
setIndentLevel(accounts, account.name, level + 1);
}
});
return accounts;
return accounts;
}
function sortAccounts(accounts) {
let out = [];
let pushed = {};
let out = [];
let pushed = {};
pushToOut(null);
pushToOut(null);
function pushToOut(parentAccount) {
accounts.forEach(account => {
if (account.parentAccount === parentAccount && !pushed[account.name]) {
out.push(account);
pushed[account.name] = 1;
function pushToOut(parentAccount) {
accounts.forEach(account => {
if (account.parentAccount === parentAccount && !pushed[account.name]) {
out.push(account);
pushed[account.name] = 1;
pushToOut(account.name);
}
})
}
pushToOut(account.name);
}
});
}
return out;
return out;
}
async function getLedgerEntries(fromDate, toDate, accounts) {
const dateFilter = () => {
const before = ['<=', toDate];
const after = ['>=', fromDate];
if (fromDate) {
return [...after, ...before];
}
return before;
const dateFilter = () => {
const before = ['<=', toDate];
const after = ['>=', fromDate];
if (fromDate) {
return [...after, ...before];
}
return before;
};
const ledgerEntries = await frappe.db.getAll({
doctype: 'AccountingLedgerEntry',
fields: ['account', 'debit', 'credit', 'date'],
filters: {
account: ['in', accounts.map(d => d.name)],
date: dateFilter()
}
});
const ledgerEntries = await frappe.db.getAll({
doctype: 'AccountingLedgerEntry',
fields: ['account', 'debit', 'credit', 'date'],
filters: {
account: ['in', accounts.map(d => d.name)],
date: dateFilter()
}
});
return ledgerEntries;
return ledgerEntries;
}
async function getAccounts(rootType) {
let accounts = await frappe.db.getAll({
doctype: 'Account',
fields: ['name', 'parentAccount'],
filters: {
rootType
}
});
let accounts = await frappe.db.getAll({
doctype: 'Account',
fields: ['name', 'parentAccount'],
filters: {
rootType
}
});
accounts = setIndentLevel(accounts);
accounts = sortAccounts(accounts);
accounts = setIndentLevel(accounts);
accounts = sortAccounts(accounts);
accounts.forEach(account => {
account.account = account.name;
});
accounts.forEach(account => {
account.account = account.name;
});
return accounts;
return accounts;
}
async function getFiscalYear() {
let { fiscalYearStart, fiscalYearEnd } = await frappe.getSingle('AccountingSettings');
return {
start: fiscalYearStart,
end: fiscalYearEnd
};
let { fiscalYearStart, fiscalYearEnd } = await frappe.getSingle(
'AccountingSettings'
);
return {
start: fiscalYearStart,
end: fiscalYearEnd
};
}
module.exports = {
getData,
getTrialBalance
}
getData,
getTrialBalance
};

View File

@ -2,6 +2,8 @@ const frappe = require('frappejs');
class GeneralLedger {
async run(params) {
if (!Object.keys(params).length) return [];
const filters = {};
if (params.account) filters.account = params.account;
if (params.party) filters.party = params.party;
@ -12,13 +14,67 @@ class GeneralLedger {
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
let data = await frappe.db.getAll({
doctype: 'AccountingLedgerEntry',
fields: ['date', 'account', 'party', 'referenceType', 'referenceName', 'debit', 'credit'],
fields: [
'date',
'account',
'party',
'referenceType',
'referenceName',
'debit',
'credit'
],
filters: filters
});
return data;
return this.appendOpeningEntry(data);
}
appendOpeningEntry(data) {
let glEntries = [];
let balance = 0,
debitTotal = 0,
creditTotal = 0;
glEntries.push({
date: '',
account: '<b>Opening</b>',
party: '',
debit: 0,
credit: 0,
balance: 0,
referenceType: '',
referenceName: ''
});
for (let entry of data) {
balance += entry.debit > 0 ? entry.debit : -entry.credit;
debitTotal += entry.debit;
creditTotal += entry.credit;
entry.balance = balance;
glEntries.push(entry);
}
glEntries.push({
date: '',
account: '<b>Total</b>',
party: '',
debit: debitTotal,
credit: creditTotal,
balance: balance,
referenceType: '',
referenceName: ''
});
glEntries.push({
date: '',
account: '<b>Closing</b>',
party: '',
debit: debitTotal,
credit: creditTotal,
balance: balance,
referenceType: '',
referenceName: ''
});
return glEntries;
}
}

View File

@ -5,9 +5,10 @@ module.exports = class GeneralLedgerView extends ReportPage {
constructor() {
super({
title: frappe._('General Ledger'),
filterFields: [{
filterFields: [
{
fieldtype: 'Select',
options: ['', 'Invoice', 'Payment'],
options: ['', 'SalesInvoice', 'Payment'],
label: 'Reference Type',
fieldname: 'referenceType'
},
@ -42,7 +43,8 @@ module.exports = class GeneralLedgerView extends ReportPage {
}
getColumns() {
return [{
return [
{
label: 'Date',
fieldtype: 'Date'
},

View File

@ -1,15 +1,19 @@
const title = 'General Ledger';
module.exports = {
title: title,
method: 'general-ledger',
filterFields: [{
let title = 'General Ledger';
const viewConfig = {
title,
filterFields: [
{
fieldtype: 'Select',
options: ['', 'Invoice', 'Payment'],
options: ['Select...', 'SalesInvoice', 'Payment', 'PurchaseInvoice'],
size: 'small',
label: 'Reference Type',
fieldname: 'referenceType'
},
{
fieldtype: 'DynamicLink',
size: 'small',
placeholder: 'Reference Name',
references: 'referenceType',
label: 'Reference Name',
fieldname: 'referenceName'
@ -17,6 +21,8 @@ module.exports = {
{
fieldtype: 'Link',
target: 'Account',
size: 'small',
placeholder: 'Account',
label: 'Account',
fieldname: 'account'
},
@ -24,21 +30,70 @@ module.exports = {
fieldtype: 'Link',
target: 'Party',
label: 'Party',
size: 'small',
placeholder: 'Party',
fieldname: 'party'
},
{
fieldtype: 'Date',
size: 'small',
placeholder: 'From Date',
label: 'From Date',
fieldname: 'fromDate'
},
{
fieldtype: 'Date',
size: 'small',
placeholder: 'To Date',
label: 'To Date',
fieldname: 'toDate'
}
],
method: 'general-ledger',
linkFields: [
{
label: 'Clear Filters',
type: 'secondary',
action: async report => {
await report.getReportData({});
report.usedToReRender += 1;
}
},
{
label: 'Export',
type: 'primary',
action: async report => {
async function getReportDetails() {
let [rows, columns] = await report.getReportData(
report.currentFilters
);
let columnData = columns.map(column => {
return {
id: column.id,
content: column.content,
checked: true
};
});
return {
title: title,
rows: rows,
columnData: columnData
};
}
report.$modal.show({
modalProps: {
title: `Export ${title}`,
noFooter: true
},
component: require('../../src/components/ExportWizard').default,
props: await getReportDetails()
});
}
}
],
getColumns() {
return [{
return [
{
label: 'Date',
fieldtype: 'Date'
},
@ -77,3 +132,5 @@ module.exports = {
];
}
};
module.exports = viewConfig;

View File

@ -0,0 +1,68 @@
const frappe = require('frappejs');
class BaseGSTR {
async getCompleteReport(gstrType, filters) {
if (['GSTR-1', 'GSTR-2'].includes(gstrType)) {
let entries = await frappe.db.getAll({
doctype: gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice',
filters
});
let tableData = [];
for (let entry of entries) {
entry.doctype = gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice';
const row = await this.getRow(entry);
tableData.push(row);
}
if (Object.keys(filters).length != 0) {
tableData = tableData.filter(row => {
if (filters.account) return row.account === filters.account;
if (filters.transferType)
return row.transferType === filters.transferType;
if (filters.place) return row.place === filters.place;
return true;
});
}
return tableData;
} else {
return [];
}
}
async getRow(ledgerEntry) {
let row = {};
ledgerEntry = await frappe.getDoc(ledgerEntry.doctype, ledgerEntry.name);
let party = await frappe.getDoc(
'Party',
ledgerEntry.customer || ledgerEntry.supplier
);
if (party.address) {
let addressDetails = await frappe.getDoc('Address', party.address);
row.place = addressDetails.state || '';
}
row.gstin = party.gstin;
row.partyName = ledgerEntry.customer || ledgerEntry.supplier;
row.invNo = ledgerEntry.name;
row.invDate = ledgerEntry.date;
row.rate = 0;
row.inState = true;
row.reverseCharge = !party.gstin ? 'Y' : 'N';
ledgerEntry.taxes.forEach(tax => {
row.rate += tax.rate;
const taxAmt = (tax.rate * ledgerEntry.netTotal) / 100;
if (tax.account === 'IGST') row.igstAmt = taxAmt;
if (tax.account === 'IGST') row.inState = false;
if (tax.account === 'CGST') row.cgstAmt = taxAmt;
if (tax.account === 'SGST') row.sgstAmt = taxAmt;
if (tax.account === 'Nil Rated') row.nilRated = true;
if (tax.account === 'Exempt') row.exempt = true;
if (tax.account === 'Non GST') row.nonGST = true;
});
row.invAmt = ledgerEntry.grandTotal;
row.taxVal = ledgerEntry.netTotal;
return row;
}
}
module.exports = BaseGSTR;

View File

@ -0,0 +1,156 @@
module.exports = {
filterFields: [
{
fieldtype: 'Select',
label: 'Transfer Type',
fieldname: 'transferType',
options: [
'',
'B2B',
'B2C-Large',
'B2C-Small',
'Nil Rated, Exempted and Non GST supplies'
],
size: 'small'
},
{
fieldtype: 'Data',
label: 'Place',
size: 'small',
placeholder: 'Place',
fieldname: 'place'
},
{
fieldtype: 'Date',
label: 'From Date',
size: 'small',
placeholder: 'From Date',
fieldname: 'fromDate'
},
{
fieldtype: 'Date',
label: 'To Date',
size: 'small',
placeholder: 'To Date',
fieldname: 'toDate'
}
],
linkFields: [
{
label: 'Export',
type: 'primary',
action: async report => {
async function getReportDetails() {
let [rows, columns] = await report.getReportData(
report.currentFilters
);
let columnData = columns.map(column => {
return {
id: column.id,
content: column.content,
checked: true
};
});
return {
title: title,
rows: rows,
columnData: columnData
};
}
report.$modal.show({
modalProps: {
title: `Export ${title}`,
noFooter: true
},
component: require('../../src/components/ExportWizard').default,
props: await getReportDetails()
});
}
},
{
label: 'Clear Filters',
type: 'secondary',
action: async report => {
await report.getReportData({});
report.usedToReRender += 1;
}
}
],
getColumns() {
return [
{
label: 'GSTIN No.',
fieldname: 'gstin',
fieldtype: 'Data',
width: 100
},
{
fieldtype: 'Data',
fieldname: 'partyName',
label: 'Party',
width: 100
},
{
label: 'Invoice No.',
fieldname: 'invNo',
fieldtype: 'Data',
width: 100
},
{
label: 'Invoice Value',
fieldname: 'invAmt',
fieldtype: 'Currency',
width: 100
},
{
label: 'Invoice Date',
fieldname: 'invDate',
fieldtype: 'Date',
width: 100
},
{
label: 'Place of supply',
fieldname: 'place',
fieldtype: 'Data',
width: 100
},
{
label: 'Rate',
fieldname: 'rate',
fieldtype: 'Data',
width: 80
},
{
label: 'Taxable Value',
fieldname: 'taxVal',
fieldtype: 'Currency',
width: 100
},
{
label: 'Reverse Chrg.',
fieldname: 'reverseCharge',
fieldtype: 'Data',
width: 80
},
{
label: 'Intergrated Tax',
fieldname: 'igstAmt',
fieldtype: 'Currency',
width: 100
},
{
label: 'Central Tax',
fieldname: 'cgstAmt',
fieldtype: 'Currency',
width: 100
},
{
label: 'State Tax',
fieldname: 'sgstAmt',
fieldtype: 'Currency',
width: 100
}
];
}
};

View File

@ -0,0 +1,28 @@
const BaseGSTR = require('./BaseGSTR');
class GSTR1 extends BaseGSTR {
async run(params) {
if (!Object.keys(params).length) return [];
let filters = {};
if (params.toDate || params.fromDate) {
filters.date = [];
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
const data = await this.getCompleteReport('GSTR-1', filters);
const conditions = {
B2B: row => row.gstin,
'B2C-Large': row => !row.gstin && !row.inState && row.invAmt >= 250000,
'B2C-Small': row =>
!row.gstin && (row.inState || (row.inState && row.invAmt < 250000))
};
if (!params.transferType) return data;
return data.filter(row => conditions[params.transferType](row));
}
}
module.exports = GSTR1;

View File

@ -0,0 +1,9 @@
const title = 'GSTR 1';
const baseConfig = require('./BaseViewConfig');
module.exports = {
title: title,
method: 'gstr-1',
filterFields: baseConfig.filterFields,
linkFields: baseConfig.linkFields,
getColumns: baseConfig.getColumns
};

View File

@ -0,0 +1,28 @@
const BaseGSTR = require('./BaseGSTR');
class GSTR2 extends BaseGSTR {
async run(params) {
if (!Object.keys(params).length) return [];
let filters = {};
if (params.toDate || params.fromDate) {
filters.date = [];
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
const data = await this.getCompleteReport('GSTR-2', filters);
const conditions = {
B2B: row => row.gstin,
'B2C-Large': row => !row.gstin && !row.inState && row.invAmt >= 250000,
'B2C-Small': row =>
!row.gstin && (row.inState || (row.inState && row.invAmt < 250000))
};
if (!params.transferType) return data;
return data.filter(row => conditions[params.transferType](row));
}
}
module.exports = GSTR2;

View File

@ -0,0 +1,9 @@
const title = 'GSTR 2';
const baseConfig = require('./BaseViewConfig');
module.exports = {
title: title,
method: 'gstr-2',
filterFields: baseConfig.filterFields,
linkFields: baseConfig.linkFields,
getColumns: baseConfig.getColumns
};

View File

@ -1,59 +0,0 @@
const frappe = require('frappejs');
class GoodsAndServiceTax {
async run(params) {
let filters = {};
if (params.toDate || params.fromDate) {
filters.date = [];
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
if (params.transferType) filters.transferType = params.transferType;
let invoiceNames = await frappe.db.getAll({
doctype: 'Invoice',
filter: filters
});
let tableData = [];
for (let invoice of invoiceNames) {
const row = await this.getRow(invoice.name)
tableData.push(row)
}
if(Object.keys(filters).length != 0){
tableData = tableData.filter((row) => {
if(filters.account) return row.account === filters.account
if(filters.transferType) return row.transferType === filters.transferType
if(filters.place) return row.place === filters.place
return true
})
}
return tableData;
}
async getRow(name) {
let row = {}
let invoiceDetails = await frappe.getDoc('Invoice', name);
let customerDetails = await frappe.getDoc('Party', invoiceDetails.customer);
let addressDetails = await frappe.getDoc('Address', customerDetails.address);
row.gstin = customerDetails.gstin
row.cusName = invoiceDetails.customer
row.invNo = invoiceDetails.name
row.invDate = invoiceDetails.date
row.place = addressDetails.state
row.rate = 0
row.transferType = 'In State';
invoiceDetails.taxes.forEach(tax => {
row.rate += tax.rate
if (tax.account === 'IGST') row.transferType = 'Out of State';
});
row.invAmt = invoiceDetails.grandTotal
row.taxAmt = invoiceDetails.netTotal
return row
}
}
module.exports = GoodsAndServiceTax;

View File

@ -1,74 +0,0 @@
const ReportPage = require('frappejs/client/desk/reportpage');
const frappe = require('frappejs');
module.exports = class GoodsAndServiceTaxView extends ReportPage {
constructor() {
super({
title: frappe._('Goods and Service Tax'),
filterFields: [
{
fieldtype: 'Data',
label: 'Transfer Type'
},
{
fieldtype: 'Data',
label: 'Place'
},
{
fieldtype: 'Date',
label: 'From Date'
},
{
fieldtype: 'Date',
label: 'To Date'
}
]
});
this.method = 'gst-taxes';
}
getColumns() {
return [{
label: 'GSTIN No.',
fieldname: 'gstin',
fieldtype: 'Data'
},
{
fieldtype: 'Data',
fieldname: 'cusName',
label: 'Customer Name'
},
{
label: 'Invoice No.',
fieldname: 'invNo',
fieldtype: 'Data'
},
{
label: 'Invoice Value',
fieldname: 'invAmt',
fieldtype: 'Data'
},
{
label: 'Invoice Date',
fieldname: 'invDate',
fieldtype: 'Date'
},
{
label: 'Place of supply',
fieldname: 'place',
fieldtype: 'Data'
},
{
label: 'Rate',
fieldname: 'rate',
fieldtype: 'Data'
},
{
label: 'Taxable Amount',
fieldname: 'taxAmt',
fieldtype: 'Data'
}
];
}
};

View File

@ -1,69 +0,0 @@
const title = 'Goods and Service Tax';
module.exports = {
title: title,
method: 'gst-taxes',
filterFields: [{
fieldtype: 'Data',
label: 'Transfer Type',
fieldname: 'transferType'
},
{
fieldtype: 'Data',
label: 'Place',
fieldname: 'place'
},
{
fieldtype: 'Date',
label: 'From Date',
fieldname: 'fromDate'
},
{
fieldtype: 'Date',
label: 'To Date',
fieldname: 'toDate'
}],
getColumns() {
return [{
label: 'GSTIN No.',
fieldname: 'gstin',
fieldtype: 'Data'
},
{
fieldtype: 'Data',
fieldname: 'cusName',
label: 'Customer Name'
},
{
label: 'Invoice No.',
fieldname: 'invNo',
fieldtype: 'Data'
},
{
label: 'Invoice Value',
fieldname: 'invAmt',
fieldtype: 'Data'
},
{
label: 'Invoice Date',
fieldname: 'invDate',
fieldtype: 'Date'
},
{
label: 'Place of supply',
fieldname: 'place',
fieldtype: 'Data'
},
{
label: 'Rate',
fieldname: 'rate',
fieldtype: 'Data'
},
{
label: 'Taxable Amount',
fieldname: 'taxAmt',
fieldtype: 'Data'
}
];
}
};

Some files were not shown because too many files have changed in this diff Show More