2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 03:19:01 +00:00

Reports and Models

- Models
  - Bill
  - Quotation (extended from Invoice)
  - Journal Entry

- Reports
  - Sales Register
  - Purchase Register
This commit is contained in:
Faris Ansari 2018-04-26 15:53:27 +05:30
parent 8adb52c57c
commit 17e5187c51
31 changed files with 1098 additions and 235 deletions

View File

@ -21,7 +21,7 @@ module.exports = class LedgerPosting {
if (!this.entryMap[account]) {
const entry = {
account: account,
party: this.party,
party: this.party || '',
date: this.date || this.reference.date,
referenceType: referenceType || this.reference.doctype,
referenceName: referenceName || this.reference.name,

View File

@ -16,13 +16,18 @@ module.exports = {
frappe.desk.menu.addItem('Customers', '#list/Customer');
frappe.desk.menu.addItem('Quotation', '#list/Quotation');
frappe.desk.menu.addItem('Invoice', '#list/Invoice');
frappe.desk.menu.addItem('Bill', '#list/Bill');
frappe.desk.menu.addItem('Journal Entry', '#list/JournalEntry');
frappe.desk.menu.addItem('Address', "#list/Address");
frappe.desk.menu.addItem('Contact', "#list/Contact");
frappe.desk.menu.addItem('Settings', () => frappe.desk.showFormModal('SystemSettings'));
// reports
frappe.desk.menu.addItem('General Ledger', '#report/general-ledger');
frappe.desk.menu.addItem('Profit And Loss', '#report/profit-and-loss');
frappe.desk.menu.addItem('Balance Sheet', '#report/balance-sheet');
frappe.desk.menu.addItem('Sales Register', '#report/sales-register');
frappe.desk.menu.addItem('Purchase Register', '#report/purchase-register');
frappe.router.default = '#tree/Account';

View File

@ -32,8 +32,7 @@ module.exports = {
fieldname: "party",
label: "Party",
fieldtype: "Link",
target: "Party",
required: 1
target: "Party"
},
{
fieldname: "debit",
@ -48,8 +47,7 @@ module.exports = {
{
fieldname: "againstAccount",
label: "Against Account",
fieldtype: "Text",
required: 0
fieldtype: "Text"
},
{
fieldname: "referenceType",

138
models/doctype/Bill/Bill.js Normal file
View File

@ -0,0 +1,138 @@
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

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

View File

@ -0,0 +1,29 @@
const frappe = require('frappejs');
const Bill = require('./BillDocument');
const LedgerPosting = rootRequire('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

@ -0,0 +1,67 @@
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

@ -0,0 +1,18 @@
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

@ -25,7 +25,13 @@ module.exports = {
"label": "Customer",
"fieldtype": "Link",
"target": "Party",
"required": 1
"required": 1,
getFilters: (query, control) => {
return {
keywords: ["like", query],
customer: 1
}
}
},
{
"fieldname": "account",
@ -35,7 +41,8 @@ module.exports = {
getFilters: (query, control) => {
return {
keywords: ["like", query],
isGroup: 0
isGroup: 0,
accountType: "Receivable"
}
}
},

View File

@ -9,7 +9,7 @@ module.exports = {
isSubmittable: 1,
keywordFields: ["name"],
showTitle: true,
settings: "JournalEntrySetting",
settings: "JournalEntrySettings",
fields: [
{
fieldname: "date",

View File

@ -1,20 +1,20 @@
const JournalEntry = require('./JournalEntry');
const frappe = require('frappejs');
const BaseDocument = require('frappejs/model/document');
const LedgerPosting = rootRequire('accounting/ledgerPosting');
module.exports = class JournalEntryServer extends BaseDocument {
/**
getPosting() {
let entries = new LedgerPosting({reference: this, party: this.party});
entries.debit(this.paymentAccount, this.amount);
let entries = new LedgerPosting({reference: this });
for (let row of this.for) {
entries.credit(this.account, row.amount, row.referenceType, row.referenceName);
for (let row of this.accounts) {
if (row.debit) {
entries.debit(row.account, row.debit);
} else if (row.credit) {
entries.credit(row.account, row.credit);
}
}
return entries;
}
async afterSubmit() {
@ -24,6 +24,4 @@ module.exports = class JournalEntryServer extends BaseDocument {
async afterRevert() {
await this.getPosting().postReverse();
}
**/
}

View File

@ -12,6 +12,12 @@ module.exports = {
"fieldtype": "Link",
"target": "Account",
"required": 1,
getFilters: (query, control) => {
return {
keywords: ["like", query],
isGroup: 0
}
}
},
{
"fieldname": "debit",

View File

@ -1,10 +1,20 @@
const deepmerge = require('deepmerge');
const model = require('frappejs/model');
const Invoice = require('../Invoice/Invoice');
const Quotation = deepmerge(Invoice, {
const Quotation = model.extend(Invoice, {
name: "Quotation",
label: "Quotation",
settings: "QuotationSettings"
settings: "QuotationSettings",
fields: [
{
"fieldname": "items",
"childtype": "QuotationItem"
}
],
links: []
}, {
skipFields: ['account'],
overrideProps: ['links']
});
module.exports = Quotation;

View File

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

View File

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

View File

@ -1,11 +1,13 @@
const deepmerge = require('deepmerge');
const model = require('frappejs/model');
const InvoiceSettings = require('../InvoiceSettings/InvoiceSettings');
const QuotationSettings = deepmerge(InvoiceSettings, {
module.exports = model.extend(InvoiceSettings, {
"name": "QuotationSettings",
"label": "Quotation Settings",
"fields": {
"default": "INV"
"fields": [
{
"fieldname": "numberSeries",
"default": "QTN"
}
})
module.exports = QuotationSettings;
]
});

View File

@ -15,6 +15,10 @@ module.exports = {
InvoiceItem: require('./doctype/InvoiceItem/InvoiceItem.js'),
InvoiceSettings: require('./doctype/InvoiceSettings/InvoiceSettings.js'),
Bill: require('./doctype/Bill/Bill.js'),
BillItem: require('./doctype/BillItem/BillItem.js'),
BillSettings: require('./doctype/BillSettings/BillSettings.js'),
Tax: require('./doctype/Tax/Tax.js'),
TaxDetail: require('./doctype/TaxDetail/TaxDetail.js'),
TaxSummary: require('./doctype/TaxSummary/TaxSummary.js'),
@ -27,6 +31,7 @@ module.exports = {
JournalEntrySettings: require('./doctype/JournalEntrySettings/JournalEntrySettings.js'),
Quotation: require('./doctype/Quotation/Quotation.js'),
QuotationItem: require('./doctype/QuotationItem/QuotationItem.js'),
QuotationSettings: require('./doctype/QuotationSettings/QuotationSettings.js'),
}
}

View File

@ -48,6 +48,4 @@ class BalanceSheet {
}
}
module.exports = function execute(params) {
return new BalanceSheet().run(params);
}
module.exports = BalanceSheet;

View File

@ -7,10 +7,18 @@ module.exports = class BalanceSheetView extends FinancialStatementsView {
title: frappe._('Balance Sheet'),
method: 'balance-sheet',
filterFields: [
{fieldtype: 'Date', label: 'To Date', required: 1},
{fieldtype: 'Date', fieldname: 'toDate', label: 'To Date', required: 1},
{fieldtype: 'Select', options: ['Monthly', 'Quarterly', 'Half Yearly', 'Yearly'],
label: 'Periodicity', fieldname: 'periodicity', default: 'Monthly'}
]
});
}
async setDefaultFilterValues() {
const accountingSettings = await frappe.getSingle('AccountingSettings');
this.filters.setValue('toDate', accountingSettings.fiscalYearEnd);
this.filters.setValue('periodicity', 'Monthly');
this.run();
}
}

View File

@ -42,6 +42,4 @@ class ProfitAndLoss {
}
}
module.exports = function execute(params) {
return new ProfitAndLoss().run(params);
}
module.exports = ProfitAndLoss;

View File

@ -7,11 +7,20 @@ module.exports = class ProfitAndLossView extends FinancialStatementsView {
title: frappe._('Profit and Loss'),
method: 'profit-and-loss',
filterFields: [
{fieldtype: 'Date', label: 'From Date', required: 1},
{fieldtype: 'Date', label: 'To Date', required: 1},
{fieldtype: 'Date', fieldname: 'fromDate', label: 'From Date', required: 1},
{fieldtype: 'Date', fieldname: 'toDate', label: 'To Date', required: 1},
{fieldtype: 'Select', options: ['Monthly', 'Quarterly', 'Half Yearly', 'Yearly'],
label: 'Periodicity', fieldname: 'periodicity', default: 'Monthly'}
]
});
}
async setDefaultFilterValues() {
const accountingSettings = await frappe.getSingle('AccountingSettings');
this.filters.setValue('fromDate', accountingSettings.fiscalYearStart);
this.filters.setValue('toDate', accountingSettings.fiscalYearEnd);
this.filters.setValue('periodicity', 'Monthly');
this.run();
}
}

View File

@ -0,0 +1,43 @@
const frappe = require('frappejs');
class PurchaseRegister {
async run({ fromDate, toDate }) {
const bills = await frappe.db.getAll({
doctype: 'Bill',
fields: ['name', 'date', 'supplier', 'account', 'netTotal', 'grandTotal'],
filters: {
date: ['>=', fromDate, '<=', toDate],
submitted: 1
},
orderBy: 'date',
order: 'desc'
});
const billNames = bills.map(d => d.name);
const taxes = await frappe.db.getAll({
doctype: 'TaxSummary',
fields: ['parent', 'amount'],
filters: {
parenttype: 'Bill',
parent: ['in', billNames]
},
orderBy: 'name'
});
for (let bill of bills) {
bill.totalTax = taxes
.filter(tax => tax.parent === bill.name)
.reduce((acc, tax) => {
if (tax.amount) {
acc = acc + tax.amount;
}
return acc;
}, 0);
}
return { rows: bills };
}
}
module.exports = PurchaseRegister;

View File

@ -0,0 +1,24 @@
const frappe = require('frappejs');
const RegisterView = require('../Register/RegisterView');
module.exports = class PurchaseRegisterView extends RegisterView {
constructor() {
super({
title: frappe._('Purchase Register'),
});
this.method = 'purchase-register';
}
getColumns() {
return [
{ label: 'Bill', fieldname: 'name' },
{ label: 'Posting Date', fieldname: 'date' },
{ label: 'Supplier', fieldname: 'supplier' },
{ label: 'Payable Account', fieldname: 'account' },
{ label: 'Net Total', fieldname: 'netTotal', fieldtype: 'Currency' },
{ label: 'Total Tax', fieldname: 'totalTax', fieldtype: 'Currency' },
{ label: 'Grand Total', fieldname: 'grandTotal', fieldtype: 'Currency' },
];
}
}

View File

@ -0,0 +1,34 @@
const ReportPage = require('frappejs/client/desk/reportpage');
const frappe = require('frappejs');
const { DateTime } = require('luxon');
const { unique } = require('frappejs/utils');
module.exports = class RegisterView extends ReportPage {
constructor({ title }) {
super({
title,
filterFields: [
{fieldtype: 'Date', fieldname: 'fromDate', label: 'From Date', required: 1},
{fieldtype: 'Date', fieldname: 'toDate', label: 'To Date', required: 1}
]
});
this.datatableOptions = {
layout: 'fixed'
}
}
async setDefaultFilterValues() {
const today = DateTime.local();
const oneMonthAgo = today.minus({ months: 1 });
this.filters.setValue('fromDate', oneMonthAgo.toISODate());
this.filters.setValue('toDate', today.toISODate());
this.run();
}
getRowsForDataTable(data) {
return data.rows || [];
}
}

View File

@ -0,0 +1,43 @@
const frappe = require('frappejs');
class SalesRegister {
async run({ fromDate, toDate }) {
const invoices = await frappe.db.getAll({
doctype: 'Invoice',
fields: ['name', 'date', 'customer', 'account', 'netTotal', 'grandTotal'],
filters: {
date: ['>=', fromDate, '<=', toDate],
submitted: 1
},
orderBy: 'date',
order: 'desc'
});
const invoiceNames = invoices.map(d => d.name);
const taxes = await frappe.db.getAll({
doctype: 'TaxSummary',
fields: ['parent', 'amount'],
filters: {
parenttype: 'Invoice',
parent: ['in', invoiceNames]
},
orderBy: 'name'
});
for (let invoice of invoices) {
invoice.totalTax = taxes
.filter(tax => tax.parent === invoice.name)
.reduce((acc, tax) => {
if (tax.amount) {
acc = acc + tax.amount;
}
return acc;
}, 0);
}
return { rows: invoices };
}
}
module.exports = SalesRegister;

View File

@ -0,0 +1,26 @@
const RegisterView = require('../Register/RegisterView');
const frappe = require('frappejs');
const { DateTime } = require('luxon');
const { unique } = require('frappejs/utils');
module.exports = class SalesRegisterView extends RegisterView {
constructor() {
super({
title: frappe._('Sales Register')
});
this.method = 'sales-register';
}
getColumns() {
return [
{ label: 'Invoice', fieldname: 'name' },
{ label: 'Posting Date', fieldname: 'date' },
{ label: 'Customer', fieldname: 'customer' },
{ label: 'Receivable Account', fieldname: 'account' },
{ label: 'Net Total', fieldname: 'netTotal', fieldtype: 'Currency' },
{ label: 'Total Tax', fieldname: 'totalTax', fieldtype: 'Currency' },
{ label: 'Grand Total', fieldname: 'grandTotal', fieldtype: 'Currency' },
];
}
}

View File

@ -20,6 +20,4 @@ class GeneralLedger {
}
}
module.exports = function execute(params) {
return new GeneralLedger().run(params);
}
module.exports = GeneralLedger;

View File

@ -9,21 +9,37 @@ const ProfitAndLossView = require('./ProfitAndLoss/ProfitAndLossView');
const BalanceSheet = require('./BalanceSheet/BalanceSheet');
const BalanceSheetView = require('./BalanceSheet/BalanceSheetView');
const SalesRegister = require('./SalesRegister/SalesRegister');
const SalesRegisterView = require('./SalesRegister/SalesRegisterView');
const PurchaseRegister = require('./PurchaseRegister/PurchaseRegister');
const PurchaseRegisterView = require('./PurchaseRegister/PurchaseRegisterView');
// called on server side
function registerReportMethods() {
frappe.registerMethod({
method: 'general-ledger',
handler: args => GeneralLedger(args)
handler: getReportData(GeneralLedger)
});
frappe.registerMethod({
method: 'profit-and-loss',
handler: args => ProfitAndLoss(args)
handler: getReportData(ProfitAndLoss)
});
frappe.registerMethod({
method: 'balance-sheet',
handler: args => BalanceSheet(args)
handler: getReportData(BalanceSheet)
});
frappe.registerMethod({
method: 'sales-register',
handler: getReportData(SalesRegister)
});
frappe.registerMethod({
method: 'purchase-register',
handler: getReportData(PurchaseRegister)
});
}
@ -49,6 +65,24 @@ function registerReportRoutes() {
}
await frappe.views.BalanceSheet.show(params);
});
frappe.router.add('report/sales-register', async (params) => {
if (!frappe.views.SalesRegister) {
frappe.views.SalesRegister = new SalesRegisterView();
}
await frappe.views.SalesRegister.show(params);
});
frappe.router.add('report/purchase-register', async (params) => {
if (!frappe.views.PurchaseRegister) {
frappe.views.PurchaseRegister = new PurchaseRegisterView();
}
await frappe.views.PurchaseRegister.show(params);
});
}
function getReportData(ReportClass) {
return args => new ReportClass().run(args);
}
module.exports = {

View File

@ -23,7 +23,8 @@ module.exports = {
// set server-side modules
frappe.models.Invoice.documentClass = require('../models/doctype/Invoice/InvoiceServer.js');
frappe.models.Payment.documentClass = require('../models/doctype/Payment/PaymentServer.js');
// frappe.models.JournalEntry.documentClass = require('../models/doctype/JournalEntry/JournalEntryServer.js');
frappe.models.Bill.documentClass = require('../models/doctype/Bill/BillServer.js');
frappe.models.JournalEntry.documentClass = require('../models/doctype/JournalEntry/JournalEntryServer.js');
frappe.metaCache = {};
@ -31,6 +32,7 @@ module.exports = {
// init naming series if missing
await naming.createNumberSeries('INV-', 'InvoiceSettings');
await naming.createNumberSeries('BILL-', 'BillSettings');
await naming.createNumberSeries('PAY-', 'PaymentSettings');
await naming.createNumberSeries('JV-', 'JournalEntrySettings');
await naming.createNumberSeries('QTN-', 'QuotationSettings');

345
www/dist/css/style.css vendored
View File

@ -7130,6 +7130,169 @@ div.CodeMirror-dragcursors {
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext {
background: none; }
.indicator, .indicator-right {
background: none;
vertical-align: middle; }
.indicator::before, .indicator-right::after {
content: '';
display: inline-block;
height: 8px;
width: 8px;
border-radius: 8px;
background: #dee2e6; }
.indicator::before {
margin: 0 0.5rem 0 0; }
.indicator-right::after {
margin: 0 0 0 0.5rem; }
.indicator.grey::before, .indicator-right.grey::after {
background: #dee2e6; }
.indicator.blue::before, .indicator-right.blue::after {
background: #007bff; }
.indicator.red::before, .indicator-right.red::after {
background: #dc3545; }
.indicator.green::before, .indicator-right.green::after {
background: #28a745; }
.indicator.orange::before, .indicator-right.orange::after {
background: #fd7e14; }
.indicator.purple::before, .indicator-right.purple::after {
background: #6f42c1; }
.indicator.darkgrey::before, .indicator-right.darkgrey::after {
background: #6c757d; }
.indicator.black::before, .indicator-right.black::after {
background: #343a40; }
.indicator.yellow::before, .indicator-right.yellow::after {
background: #ffc107; }
.modal-header .indicator {
float: left;
margin-top: 7.5px;
margin-right: 3px; }
html {
font-size: 12px; }
.desk-body {
border-left: 1px solid #dee2e6;
min-height: 100vh; }
.desk-center {
border-left: 1px solid #dee2e6; }
.hide {
display: none !important; }
.page {
padding-bottom: 2rem; }
.page .page-nav {
padding: 0.5rem 1rem;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6; }
.page .page-nav .btn {
margin-left: 0.5rem; }
.page .page-title {
font-weight: bold;
padding: 2rem;
padding-bottom: 0; }
.page .page-links {
padding: 1rem 2rem; }
.page .page-error {
text-align: center;
padding: 200px 0px; }
.form-body {
padding: 1rem 2rem; }
.form-body .form-check {
margin-bottom: 0.5rem; }
.form-body .form-check .form-check-input {
margin-top: 0.25rem; }
.form-body .form-check .form-check-label {
margin-left: 0.25rem; }
.form-body .form-control.font-weight-bold {
background-color: lightyellow; }
.form-body .alert {
margin-top: 1rem; }
.form-inline .form-group {
margin-right: 1rem;
margin-bottom: 1rem; }
.list-search {
padding: 1rem 2rem; }
.list-body .list-row {
padding: 0.5rem 2rem;
border-bottom: 1px solid #e9ecef;
cursor: pointer; }
.list-body .list-row .checkbox {
margin-right: 0.5rem; }
.list-body .list-row a, .list-body .list-row a:hover, .list-body .list-row a:visited, .list-body .list-row a:active {
color: #343a40; }
.list-body .list-row:hover {
background-color: #f8f9fa; }
.list-body .list-row.active {
background-color: #e9ecef; }
.dropdown-item {
padding: 0.5rem 1rem; }
.bottom-right-float {
position: fixed;
margin-bottom: 0px;
bottom: 1rem;
right: 1rem;
max-width: 200px;
padding: 0.5rem 1rem; }
.desk-menu {
background-color: #e9ecef; }
.desk-menu .list-row {
border-bottom: 1px solid #e9ecef; }
.desk-menu .list-row:hover {
background-color: #dee2e6; }
.desk-menu .list-row.active {
background-color: #ced4da; }
.print-page {
padding: 3rem;
line-height: 1.8; }
.print-page td, .print-page th {
padding: 0.5rem; }
.table-page-wrapper {
width: 100%;
padding: 1rem 2rem; }
.filter-toolbar {
padding: 1rem 2rem; }
.table-wrapper {
margin-top: 2rem;
margin-bottom: 2rem; }
.table-toolbar {
margin-top: 0.5rem; }
.CodeMirror {
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 0.5rem; }
.awesomplete {
display: block; }
.awesomplete ul {
max-height: 150px;
overflow: auto; }
.awesomplete > ul > li {
padding: .75rem .375rem; }
.awesomplete > ul > li:hover {
background: #dee2e6;
color: #212529; }
.awesomplete > ul > li[aria-selected="true"] {
background: #dee2e6;
color: #212529; }
.awesomplete > ul > li[aria-selected="true"]:hover {
background: #dee2e6;
color: #212529; }
.awesomplete li[aria-selected="true"] mark, .awesomplete li[aria-selected="false"] mark {
background: inherit;
color: inherit;
padding: 0px; }
mark {
padding: none;
background: inherit; }
.align-right {
text-align: right; }
.align-center {
text-align: center; }
.btn-sm, .btn-group-sm > .btn {
margin: 0.25rem; }
.vertical-margin {
margin: 1rem 0px; }
.tree-body {
padding: 1rem 2rem; }
f-tree-node {
--tree-node-hover: #f8f9fa; }
.datatable *, .datatable *::after, .datatable *::before {
-webkit-box-sizing: border-box;
box-sizing: border-box; }
@ -7310,183 +7473,27 @@ span.CodeMirror-selectedtext {
left: -999em; }
body.dt-resize {
cursor: col-resize; }
.indicator, .indicator-right {
background: none;
vertical-align: middle; }
.indicator::before, .indicator-right::after {
content: '';
display: inline-block;
height: 8px;
width: 8px;
border-radius: 8px;
background: #dee2e6; }
.indicator::before {
margin: 0 0.5rem 0 0; }
.indicator-right::after {
margin: 0 0 0 0.5rem; }
.indicator.grey::before, .indicator-right.grey::after {
background: #dee2e6; }
.indicator.blue::before, .indicator-right.blue::after {
background: #007bff; }
.indicator.red::before, .indicator-right.red::after {
background: #dc3545; }
.indicator.green::before, .indicator-right.green::after {
background: #28a745; }
.indicator.orange::before, .indicator-right.orange::after {
background: #fd7e14; }
.indicator.purple::before, .indicator-right.purple::after {
background: #6f42c1; }
.indicator.darkgrey::before, .indicator-right.darkgrey::after {
background: #6c757d; }
.indicator.black::before, .indicator-right.black::after {
background: #343a40; }
.indicator.yellow::before, .indicator-right.yellow::after {
background: #ffc107; }
.modal-header .indicator {
float: left;
margin-top: 7.5px;
margin-right: 3px; }
html {
font-size: 12px; }
.desk-body {
border-left: 1px solid #dee2e6;
min-height: 100vh; }
.desk-center {
border-left: 1px solid #dee2e6; }
.hide {
display: none !important; }
.page {
padding-bottom: 2rem; }
.page .page-nav {
padding: 0.5rem 1rem;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6; }
.page .page-nav .btn {
margin-left: 0.5rem; }
.page .page-title {
font-weight: bold;
padding: 2rem;
padding-bottom: 0; }
.page .page-links {
padding: 1rem 2rem; }
.page .page-error {
text-align: center;
padding: 200px 0px; }
.form-body {
padding: 1rem 2rem; }
.form-body .form-check {
margin-bottom: 0.5rem; }
.form-body .form-check .form-check-input {
margin-top: 0.25rem; }
.form-body .form-check .form-check-label {
margin-left: 0.25rem; }
.form-body .form-control.font-weight-bold {
background-color: lightyellow; }
.form-body .alert {
margin-top: 1rem; }
.form-inline .form-group {
margin-right: 1rem;
margin-bottom: 1rem; }
.list-search {
padding: 1rem 2rem; }
.list-body .list-row {
padding: 0.5rem 2rem;
border-bottom: 1px solid #e9ecef;
cursor: pointer; }
.list-body .list-row .checkbox {
margin-right: 0.5rem; }
.list-body .list-row a, .list-body .list-row a:hover, .list-body .list-row a:visited, .list-body .list-row a:active {
color: #343a40; }
.list-body .list-row:hover {
background-color: #f8f9fa; }
.list-body .list-row.active {
background-color: #e9ecef; }
.dropdown-item {
padding: 0.5rem 1rem; }
.bottom-right-float {
position: fixed;
margin-bottom: 0px;
bottom: 1rem;
right: 1rem;
max-width: 200px;
padding: 0.5rem 1rem; }
.desk-menu {
background-color: #e9ecef; }
.desk-menu .list-row {
border-bottom: 1px solid #e9ecef; }
.desk-menu .list-row:hover {
background-color: #dee2e6; }
.desk-menu .list-row.active {
background-color: #ced4da; }
.print-page {
padding: 3rem;
line-height: 1.8; }
.print-page td, .print-page th {
padding: 0.5rem; }
.table-page-wrapper {
width: 100%;
padding: 1rem 2rem; }
.filter-toolbar {
padding: 1rem 2rem; }
.table-wrapper {
margin-top: 2rem;
margin-bottom: 2rem; }
.table-toolbar {
margin-top: 0.5rem; }
.data-table .body-scrollable {
border-bottom: 0px !important; }
.data-table .body-scrollable tr:first-child .data-table-col {
border-top: 0px !important; }
.data-table thead td {
.dt-header {
background-color: #e9ecef !important; }
.data-table .data-table-cell .edit-cell {
padding: 0px !important; }
.data-table .data-table-cell .edit-cell input, .data-table .data-table-cell .edit-cell textarea {
.dt-cell__edit {
padding: 0px; }
.dt-cell__edit input, .dt-cell__edit textarea {
outline: none;
border-radius: none;
border: none;
margin: none;
padding: 0.5rem; }
.data-table .data-table-cell .edit-cell .awesomplete > ul {
.dt-cell__edit input:focus, .dt-cell__edit textarea:focus {
border: none;
-webkit-box-shadow: none;
box-shadow: none; }
.dt-cell__edit .awesomplete > ul {
position: fixed;
left: auto;
width: auto;
min-width: 120px; }
.CodeMirror {
font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 0.5rem; }
.awesomplete {
display: block; }
.awesomplete > ul > li {
padding: .75rem .375rem; }
.awesomplete > ul > li:hover {
background: #dee2e6;
color: #212529; }
.awesomplete > ul > li[aria-selected="true"] {
background: #dee2e6;
color: #212529; }
.awesomplete > ul > li[aria-selected="true"]:hover {
background: #dee2e6;
color: #212529; }
.awesomplete li[aria-selected="true"] mark, .awesomplete li[aria-selected="false"] mark {
background: inherit;
color: inherit;
padding: 0px; }
mark {
padding: none;
background: inherit; }
.align-right {
text-align: right; }
.align-center {
text-align: center; }
.btn-sm, .btn-group-sm > .btn {
margin: 0.25rem; }
.vertical-margin {
margin: 1rem 0px; }
.tree-body {
padding: 1rem 2rem; }
f-tree-node {
--tree-node-hover: #f8f9fa; }
.dt-cell--highlight {
background-color: #e9ecef; }
.setup-container {
margin: 40px auto;
padding: 20px 0px;

383
www/dist/js/bundle.js vendored
View File

@ -58972,9 +58972,7 @@ class GeneralLedger {
}
}
var GeneralLedger_1 = function execute(params) {
return new GeneralLedger().run(params);
};
var GeneralLedger_1 = GeneralLedger;
var reportpage = class ReportPage extends page {
constructor({title, filterFields = []}) {
@ -65181,9 +65179,7 @@ class ProfitAndLoss {
}
}
var ProfitAndLoss_1 = function execute(params) {
return new ProfitAndLoss().run(params);
};
var ProfitAndLoss_1 = ProfitAndLoss;
var FinancialStatementsView_1 = class FinancialStatementsView extends reportpage {
constructor(opts) {
@ -65287,9 +65283,7 @@ class BalanceSheet {
}
}
var BalanceSheet_1 = function execute(params) {
return new BalanceSheet().run(params);
};
var BalanceSheet_1 = BalanceSheet;
var BalanceSheetView_1 = class BalanceSheetView extends FinancialStatementsView_1 {
constructor() {
@ -65305,20 +65299,100 @@ var BalanceSheetView_1 = class BalanceSheetView extends FinancialStatementsView_
}
};
class SalesRegister {
async run({ fromDate, toDate }) {
const invoices = await frappejs.db.getAll({
doctype: 'Invoice',
fields: ['name', 'date', 'customer', 'account', 'netTotal', 'grandTotal'],
filters: {
date: ['>=', fromDate, '<=', toDate],
submitted: 1
},
orderBy: 'date',
order: 'desc'
});
const invoiceNames = invoices.map(d => d.name);
const taxes = await frappejs.db.getAll({
doctype: 'TaxSummary',
fields: ['parent', 'amount'],
filters: {
parenttype: 'Invoice',
parent: ['in', invoiceNames]
},
orderBy: 'name'
});
for (let invoice of invoices) {
invoice.totalTax = taxes
.filter(tax => tax.parent === invoice.name)
.reduce((acc, tax) => {
if (tax.amount) {
acc = acc + tax.amount;
}
return acc;
}, 0);
}
return { rows: invoices };
}
}
var SalesRegister_1 = SalesRegister;
var SalesRegisterView_1 = class SalesRegisterView extends reportpage {
constructor() {
super({
title: frappejs._('Sales Register'),
filterFields: [
{fieldtype: 'Date', label: 'From Date', required: 1},
{fieldtype: 'Date', label: 'To Date', required: 1}
]
});
this.method = 'sales-register';
this.datatableOptions = {
layout: 'fixed'
};
}
getRowsForDataTable(data) {
return data.rows || [];
}
getColumns() {
return [
{ label: 'Invoice', fieldname: 'name' },
{ label: 'Posting Date', fieldname: 'date' },
{ label: 'Customer', fieldname: 'customer' },
{ label: 'Receivable Account', fieldname: 'account' },
{ label: 'Net Total', fieldname: 'netTotal', fieldtype: 'Currency' },
{ label: 'Total Tax', fieldname: 'totalTax', fieldtype: 'Currency' },
{ label: 'Grand Total', fieldname: 'grandTotal', fieldtype: 'Currency' },
];
}
};
function registerReportMethods() {
frappejs.registerMethod({
method: 'general-ledger',
handler: args => GeneralLedger_1(args)
handler: getReportData(GeneralLedger_1)
});
frappejs.registerMethod({
method: 'profit-and-loss',
handler: args => ProfitAndLoss_1(args)
handler: getReportData(ProfitAndLoss_1)
});
frappejs.registerMethod({
method: 'balance-sheet',
handler: args => BalanceSheet_1(args)
handler: getReportData(BalanceSheet_1)
});
frappejs.registerMethod({
method: 'sales-register',
handler: getReportData(SalesRegister_1)
});
}
@ -65344,6 +65418,17 @@ function registerReportRoutes() {
}
await frappejs.views.BalanceSheet.show(params);
});
frappejs.router.add('report/sales-register', async (params) => {
if (!frappejs.views.SalesRegister) {
frappejs.views.SalesRegister = new SalesRegisterView_1();
}
await frappejs.views.SalesRegister.show(params);
});
}
function getReportData(ReportClass) {
return args => new ReportClass().run(args);
}
var reports = {
@ -66171,8 +66256,7 @@ var AccountingLedgerEntry = {
fieldname: "party",
label: "Party",
fieldtype: "Link",
target: "Party",
required: 1
target: "Party"
},
{
fieldname: "debit",
@ -66187,8 +66271,7 @@ var AccountingLedgerEntry = {
{
fieldname: "againstAccount",
label: "Against Account",
fieldtype: "Text",
required: 0
fieldtype: "Text"
},
{
fieldname: "referenceType",
@ -66578,7 +66661,13 @@ module.exports = {
"label": "Customer",
"fieldtype": "Link",
"target": "Party",
"required": 1
"required": 1,
getFilters: (query, control) => {
return {
keywords: ["like", query],
customer: 1
}
}
},
{
"fieldname": "account",
@ -66588,7 +66677,8 @@ module.exports = {
getFilters: (query, control) => {
return {
keywords: ["like", query],
isGroup: 0
isGroup: 0,
accountType: "Receivable"
}
}
},
@ -66777,6 +66867,237 @@ var InvoiceSettings = {
]
};
var BillDocument = class Bill extends InvoiceDocument { };
var Bill = createCommonjsModule(function (module) {
module.exports = {
"name": "Bill",
"doctype": "DocType",
"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'>
${frappejs.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 frappejs.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 frappejs.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">${frappejs.format(data.grandTotal, "Currency")}</div>`;
}
},
documentClass: BillDocument
};
});
var Bill_1 = Bill.layout;
var Bill_2 = Bill.links;
var Bill_3 = Bill.listSettings;
var Bill_4 = Bill.documentClass;
var BillItem = {
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)
}
]
};
var BillSettings = {
"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"
}
]
};
var Tax = {
"name": "Tax",
"doctype": "DocType",
@ -67069,7 +67390,7 @@ var JournalEntry = {
isSubmittable: 1,
keywordFields: ["name"],
showTitle: true,
settings: "JournalEntrySetting",
settings: "JournalEntrySettings",
fields: [
{
fieldname: "date",
@ -67142,7 +67463,8 @@ var JournalEntry = {
]
};
var JournalEntryAccount = {
var JournalEntryAccount = createCommonjsModule(function (module) {
module.exports = {
name: "JournalEntryAccount",
doctype: "DocType",
isSingle: 0,
@ -67156,6 +67478,12 @@ var JournalEntryAccount = {
"fieldtype": "Link",
"target": "Account",
"required": 1,
getFilters: (query, control) => {
return {
keywords: ["like", query],
isGroup: 0
}
}
},
{
"fieldname": "debit",
@ -67169,6 +67497,15 @@ var JournalEntryAccount = {
}
]
};
});
var JournalEntryAccount_1 = JournalEntryAccount.name;
var JournalEntryAccount_2 = JournalEntryAccount.doctype;
var JournalEntryAccount_3 = JournalEntryAccount.isSingle;
var JournalEntryAccount_4 = JournalEntryAccount.isChild;
var JournalEntryAccount_5 = JournalEntryAccount.keywordFields;
var JournalEntryAccount_6 = JournalEntryAccount.layout;
var JournalEntryAccount_7 = JournalEntryAccount.fields;
var JournalEntrySettings = {
"name": "JournalEntrySetting",
@ -67321,6 +67658,10 @@ var models$2 = {
InvoiceItem: InvoiceItem,
InvoiceSettings: InvoiceSettings,
Bill: Bill,
BillItem: BillItem,
BillSettings: BillSettings,
Tax: Tax,
TaxDetail: TaxDetail,
TaxSummary: TaxSummary,
@ -67365,6 +67706,7 @@ var client$2 = {
frappejs.desk.menu.addItem('Customers', '#list/Customer');
frappejs.desk.menu.addItem('Quotation', '#list/Quotation');
frappejs.desk.menu.addItem('Invoice', '#list/Invoice');
frappejs.desk.menu.addItem('Bill', '#list/Bill');
frappejs.desk.menu.addItem('Journal Entry', '#list/JournalEntry');
frappejs.desk.menu.addItem('Address', "#list/Address");
frappejs.desk.menu.addItem('Contact', "#list/Contact");
@ -67372,6 +67714,7 @@ var client$2 = {
frappejs.desk.menu.addItem('General Ledger', '#report/general-ledger');
frappejs.desk.menu.addItem('Profit And Loss', '#report/profit-and-loss');
frappejs.desk.menu.addItem('Balance Sheet', '#report/balance-sheet');
frappejs.desk.menu.addItem('Sales Register', '#report/sales-register');
frappejs.router.default = '#tree/Account';