Merge branch 'develop' of https://github.com/frappe/erpnext into customer-details-in-tax-withholding-category-report

This commit is contained in:
Deepesh Garg 2023-07-25 14:45:36 +05:30
commit e1d6bf364e
215 changed files with 3054 additions and 3584 deletions

View File

@ -2,65 +2,32 @@
"env": { "env": {
"browser": true, "browser": true,
"node": true, "node": true,
"es6": true "es2022": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 11,
"sourceType": "module" "sourceType": "module"
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",
"rules": { "rules": {
"indent": [ "indent": "off",
"error", "brace-style": "off",
"tab", "no-mixed-spaces-and-tabs": "off",
{ "SwitchCase": 1 } "no-useless-escape": "off",
], "space-unary-ops": ["error", { "words": true }],
"brace-style": [ "linebreak-style": "off",
"error", "quotes": ["off"],
"1tbs" "semi": "off",
], "camelcase": "off",
"space-unary-ops": [ "no-unused-vars": "off",
"error", "no-console": ["warn"],
{ "words": true } "no-extra-boolean-cast": ["off"],
], "no-control-regex": ["off"]
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"off"
],
"semi": [
"warn",
"always"
],
"camelcase": [
"off"
],
"no-unused-vars": [
"warn"
],
"no-redeclare": [
"warn"
],
"no-console": [
"warn"
],
"no-extra-boolean-cast": [
"off"
],
"no-control-regex": [
"off"
],
"space-before-blocks": "warn",
"keyword-spacing": "warn",
"comma-spacing": "warn",
"key-spacing": "warn"
}, },
"root": true, "root": true,
"globals": { "globals": {
"frappe": true, "frappe": true,
"Vue": true, "Vue": true,
"SetVueGlobals": true,
"erpnext": true, "erpnext": true,
"hub": true, "hub": true,
"$": true, "$": true,
@ -97,8 +64,10 @@
"is_null": true, "is_null": true,
"in_list": true, "in_list": true,
"has_common": true, "has_common": true,
"posthog": true,
"has_words": true, "has_words": true,
"validate_email": true, "validate_email": true,
"open_web_template_values_editor": true,
"get_number_format": true, "get_number_format": true,
"format_number": true, "format_number": true,
"format_currency": true, "format_currency": true,

View File

@ -9,21 +9,22 @@ jobs:
name: linters name: linters
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python 3.10 - name: Set up Python 3.10
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: '3.10' python-version: '3.10'
cache: pip
- name: Install and Run Pre-commit - name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3 uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Download semgrep - name: Download semgrep
run: pip install semgrep==0.97.0 run: pip install semgrep
- name: Run Semgrep rules - name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness

View File

@ -7,11 +7,9 @@ on:
- '**.css' - '**.css'
- '**.md' - '**.md'
- '**.html' - '**.html'
push: schedule:
branches: [ develop ] # Run everday at midnight UTC / 5:30 IST
paths-ignore: - cron: "0 0 * * *"
- '**.js'
- '**.md'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
user: user:

View File

@ -16,8 +16,26 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
erpnext/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 5.0.4 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [ additional_dependencies: [

View File

@ -4,18 +4,19 @@
"creation": "2020-07-17 11:25:34.593061", "creation": "2020-07-17 11:25:34.593061",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}", "filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 12:24:49.144210", "modified": "2023-07-19 13:13:13.307073",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget Variance", "name": "Budget Variance",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Budget Variance Report", "report_name": "Budget Variance Report",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -4,18 +4,19 @@
"creation": "2020-07-17 11:25:34.448572", "creation": "2020-07-17 11:25:34.448572",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}", "filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 12:33:48.888943", "modified": "2023-07-19 13:08:56.470390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Profit and Loss", "name": "Profit and Loss",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Profit and Loss Statement", "report_name": "Profit and Loss Statement",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -14,10 +14,8 @@ class AccountClosingBalance(Document):
pass pass
def make_closing_entries(closing_entries, voucher_name): def make_closing_entries(closing_entries, voucher_name, company, closing_date):
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
company = closing_entries[0].get("company")
closing_date = closing_entries[0].get("closing_date")
previous_closing_entries = get_previous_closing_entries( previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions company, closing_date, accounting_dimensions

View File

@ -20,5 +20,11 @@ frappe.ui.form.on('Accounting Period', {
} }
}); });
} }
frm.set_query("document_type", "closed_documents", () => {
return {
query: "erpnext.controllers.queries.get_doctypes_for_closing",
}
});
} }
}); });

View File

@ -11,6 +11,10 @@ class OverlapError(frappe.ValidationError):
pass pass
class ClosedAccountingPeriod(frappe.ValidationError):
pass
class AccountingPeriod(Document): class AccountingPeriod(Document):
def validate(self): def validate(self):
self.validate_overlap() self.validate_overlap()
@ -65,3 +69,42 @@ class AccountingPeriod(Document):
"closed_documents", "closed_documents",
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
) )
def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance":
return
elif doc.doctype == "Asset":
if doc.is_existing_asset:
return
else:
date = doc.available_for_use_date
elif doc.doctype == "Asset Repair":
date = doc.completion_date
else:
date = doc.posting_date
ap = frappe.qb.DocType("Accounting Period")
cd = frappe.qb.DocType("Closed Document")
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
& (cd.closed == 1)
& (cd.document_type == doc.doctype)
& (date >= ap.start_date)
& (date <= ap.end_date)
)
).run(as_dict=1)
if accounting_period:
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])
),
ClosedAccountingPeriod,
)

View File

@ -6,9 +6,11 @@ import unittest
import frappe import frappe
from frappe.utils import add_months, nowdate from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
OverlapError,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ["Item"] test_dependencies = ["Item"]
@ -33,9 +35,9 @@ class TestAccountingPeriod(unittest.TestCase):
ap1.save() ap1.save()
doc = create_sales_invoice( doc = create_sales_invoice(
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
) )
self.assertRaises(ClosedAccountingPeriod, doc.submit) self.assertRaises(ClosedAccountingPeriod, doc.save)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Accounting Period"): for d in frappe.get_all("Accounting Period"):

View File

@ -102,7 +102,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
} }
onScriptLoaded(me) { onScriptLoaded(me) {
me.linkHandler = Plaid.create({ me.linkHandler = Plaid.create({ // eslint-disable-line no-undef
env: me.plaid_env, env: me.plaid_env,
token: me.token, token: me.token,
onSuccess: me.plaid_success onSuccess: me.plaid_success

View File

@ -70,7 +70,7 @@ frappe.ui.form.on('Cost Center', {
} }
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); let data = d.get_values();
if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) { if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) {
d.hide(); d.hide();
return; return;
@ -91,8 +91,8 @@ frappe.ui.form.on('Cost Center', {
if(r.message) { if(r.message) {
frappe.set_route("Form", "Cost Center", r.message); frappe.set_route("Form", "Cost Center", r.message);
} else { } else {
me.frm.set_value("cost_center_name", data.cost_center_name); frm.set_value("cost_center_name", data.cost_center_name);
me.frm.set_value("cost_center_number", data.cost_center_number); frm.set_value("cost_center_number", data.cost_center_number);
} }
d.hide(); d.hide();
} }

View File

@ -1,13 +1,14 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Dunning", { frappe.ui.form.on("Dunning", {
setup: function (frm) { setup: function (frm) {
frm.set_query("sales_invoice", () => { frm.set_query("sales_invoice", "overdue_payments", () => {
return { return {
filters: { filters: {
docstatus: 1, docstatus: 1,
company: frm.doc.company, company: frm.doc.company,
customer: frm.doc.customer,
outstanding_amount: [">", 0], outstanding_amount: [">", 0],
status: "Overdue" status: "Overdue"
}, },
@ -22,14 +23,24 @@ frappe.ui.form.on("Dunning", {
} }
}; };
}); });
frm.set_query("cost_center", () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query("contact_person", erpnext.queries.contact_query);
frm.set_query("customer_address", erpnext.queries.address_query);
frm.set_query("company_address", erpnext.queries.company_address_query);
// cannot add rows manually, only via button "Fetch Overdue Payments"
frm.set_df_property("overdue_payments", "cannot_add_rows", true);
}, },
refresh: function (frm) { refresh: function (frm) {
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
frm.set_df_property(
"sales_invoice",
"read_only",
frm.doc.__islocal ? 0 : 1
);
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") { if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
frm.add_custom_button(__("Resolve"), () => { frm.add_custom_button(__("Resolve"), () => {
frm.set_value("status", "Resolved"); frm.set_value("status", "Resolved");
@ -40,42 +51,111 @@ frappe.ui.form.on("Dunning", {
__("Payment"), __("Payment"),
function () { function () {
frm.events.make_payment_entry(frm); frm.events.make_payment_entry(frm);
},__("Create") }, __("Create")
); );
frm.page.set_inner_btn_group_as_primary(__("Create")); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
if(frm.doc.docstatus > 0) { if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Ledger'), function() { frm.add_custom_button(__("Fetch Overdue Payments"), () => {
frappe.route_options = { erpnext.utils.map_current_doc({
"voucher_no": frm.doc.name, method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
"from_date": frm.doc.posting_date, source_doctype: "Sales Invoice",
"to_date": frm.doc.posting_date, date_field: "due_date",
"company": frm.doc.company, target: frm,
"show_cancelled_entries": frm.doc.docstatus === 2 setters: {
}; customer: frm.doc.customer || undefined,
frappe.set_route("query-report", "General Ledger"); },
}, __('View')); get_query_filters: {
docstatus: 1,
status: "Overdue",
company: frm.doc.company
},
allow_child_item_selection: true,
child_fieldname: "payment_schedule",
child_columns: ["due_date", "outstanding"],
});
});
} }
frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
}, },
overdue_days: function (frm) { // When multiple companies are set up. in case company name is changed set default company address
frappe.db.get_value( company: function (frm) {
"Dunning Type", if (frm.doc.company) {
{ frappe.call({
start_day: ["<", frm.doc.overdue_days], method: "erpnext.setup.doctype.company.company.get_default_company_address",
end_day: [">=", frm.doc.overdue_days], args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
}, debounce: 2000,
"dunning_type", callback: function (r) {
(r) => { frm.set_value("company_address", r && r.message || "");
if (r) { }
frm.set_value("dunning_type", r.dunning_type); });
} else {
frm.set_value("dunning_type", ""); if (frm.fields_dict.currency) {
frm.set_value("rate_of_interest", ""); const company_currency = erpnext.get_currency(frm.doc.company);
frm.set_value("dunning_fee", "");
if (!frm.doc.currency) {
frm.set_value("currency", company_currency);
}
if (frm.doc.currency == company_currency) {
frm.set_value("conversion_rate", 1.0);
} }
} }
);
const company_doc = frappe.get_doc(":Company", frm.doc.company);
if (company_doc.default_letter_head) {
if (frm.fields_dict.letter_head) {
frm.set_value("letter_head", company_doc.default_letter_head);
}
}
}
},
currency: function (frm) {
// this.set_dynamic_labels();
const company_currency = erpnext.get_currency(frm.doc.company);
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
if (frm.doc.currency && frm.doc.currency !== company_currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
transaction_date: frm.doc.posting_date,
from_currency: frm.doc.currency,
to_currency: company_currency,
args: "for_selling"
},
freeze: true,
freeze_message: __("Fetching exchange rates ..."),
callback: function(r) {
const exchange_rate = flt(r.message);
if (exchange_rate != frm.doc.conversion_rate) {
frm.set_value("conversion_rate", exchange_rate);
}
}
});
} else {
frm.trigger("conversion_rate");
}
},
customer: (frm) => {
erpnext.utils.get_party_details(frm);
},
conversion_rate: function (frm) {
if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
frm.set_value("conversion_rate", 1.0);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
customer_address: function (frm) {
erpnext.utils.get_address_display(frm, "customer_address");
},
company_address: function (frm) {
erpnext.utils.get_address_display(frm, "company_address");
}, },
dunning_type: function (frm) { dunning_type: function (frm) {
frm.trigger("get_dunning_letter_text"); frm.trigger("get_dunning_letter_text");
@ -87,7 +167,7 @@ frappe.ui.form.on("Dunning", {
if (frm.doc.dunning_type) { if (frm.doc.dunning_type) {
frappe.call({ frappe.call({
method: method:
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: { args: {
dunning_type: frm.doc.dunning_type, dunning_type: frm.doc.dunning_type,
language: frm.doc.language, language: frm.doc.language,
@ -106,49 +186,62 @@ frappe.ui.form.on("Dunning", {
}); });
} }
}, },
due_date: function (frm) {
frm.trigger("calculate_overdue_days");
},
posting_date: function (frm) { posting_date: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_overdue_days");
}, },
rate_of_interest: function (frm) { rate_of_interest: function (frm) {
frm.trigger("calculate_interest_and_amount"); frm.trigger("calculate_interest");
},
outstanding_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
},
interest_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
}, },
dunning_fee: function (frm) { dunning_fee: function (frm) {
frm.trigger("calculate_interest_and_amount"); frm.trigger("calculate_totals");
}, },
sales_invoice: function (frm) { overdue_payments_add: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_totals");
},
overdue_payments_remove: function (frm) {
frm.trigger("calculate_totals");
}, },
calculate_overdue_days: function (frm) { calculate_overdue_days: function (frm) {
if (frm.doc.posting_date && frm.doc.due_date) { frm.doc.overdue_payments.forEach((row) => {
const overdue_days = moment(frm.doc.posting_date).diff( if (frm.doc.posting_date && row.due_date) {
frm.doc.due_date, const overdue_days = moment(frm.doc.posting_date).diff(
"days" row.due_date,
); "days"
frm.set_value("overdue_days", overdue_days); );
} frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
}
});
}, },
calculate_interest_and_amount: function (frm) { calculate_interest: function (frm) {
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; frm.doc.overdue_payments.forEach((row) => {
const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount')); const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount')); const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total')); frappe.model.set_value(row.doctype, row.name, "interest", interest);
frm.set_value("interest_amount", interest_amount); });
frm.set_value("dunning_amount", dunning_amount); },
frm.set_value("grand_total", grand_total); calculate_totals: function (frm) {
const total_interest = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.interest, 0);
const total_outstanding = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.outstanding, 0);
const dunning_amount = total_interest + frm.doc.dunning_fee;
const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
const grand_total = total_outstanding + dunning_amount;
function setWithPrecison(field, value) {
frm.set_value(field, flt(value, precision(field)));
}
setWithPrecison("total_outstanding", total_outstanding);
setWithPrecison("total_interest", total_interest);
setWithPrecison("dunning_amount", dunning_amount);
setWithPrecison("base_dunning_amount", base_dunning_amount);
setWithPrecison("grand_total", grand_total);
}, },
make_payment_entry: function (frm) { make_payment_entry: function (frm) {
return frappe.call({ return frappe.call({
method: method:
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
args: { args: {
dt: frm.doc.doctype, dt: frm.doc.doctype,
dn: frm.doc.name, dn: frm.doc.name,
@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", {
}); });
}, },
}); });
frappe.ui.form.on("Overdue Payment", {
interest: function (frm) {
frm.trigger("calculate_totals");
}
});

View File

@ -2,49 +2,60 @@
"actions": [], "actions": [],
"allow_events_in_timeline": 1, "allow_events_in_timeline": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238", "creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title",
"naming_series", "naming_series",
"sales_invoice",
"customer", "customer",
"customer_name", "customer_name",
"outstanding_amount",
"currency",
"conversion_rate",
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"posting_time", "posting_time",
"due_date", "status",
"overdue_days", "section_break_9",
"currency",
"column_break_11",
"conversion_rate",
"address_and_contact_section", "address_and_contact_section",
"customer_address",
"address_display", "address_display",
"contact_person",
"contact_display", "contact_display",
"column_break_16",
"company_address",
"company_address_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"column_break_18",
"company_address_display",
"section_break_6", "section_break_6",
"dunning_type", "dunning_type",
"dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"interest_amount",
"section_break_12", "section_break_12",
"dunning_amount", "overdue_payments",
"grand_total", "section_break_28",
"income_account", "total_interest",
"dunning_fee",
"column_break_17", "column_break_17",
"status", "dunning_amount",
"printing_setting_section", "base_dunning_amount",
"section_break_32",
"spacer",
"column_break_33",
"total_outstanding",
"grand_total",
"printing_settings_section",
"language", "language",
"body_text", "body_text",
"column_break_22", "column_break_22",
"letter_head", "letter_head",
"closing_text", "closing_text",
"accounting_details_section",
"income_account",
"column_break_48",
"cost_center",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -60,32 +71,17 @@
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"options": "DUNN-.MM.-.YY.-" "options": "DUNN-.MM.-.YY.-",
"print_hide": 1
}, },
{ {
"fieldname": "sales_invoice", "fetch_from": "customer.customer_name",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"reqd": 1
},
{
"fetch_from": "sales_invoice.customer_name",
"fieldname": "customer_name", "fieldname": "customer_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Customer Name", "label": "Customer Name",
"read_only": 1 "read_only": 1
}, },
{
"fetch_from": "sales_invoice.outstanding_amount",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"label": "Outstanding Amount",
"read_only": 1
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -94,13 +90,8 @@
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Date" "label": "Date",
}, "reqd": 1
{
"fieldname": "overdue_days",
"fieldtype": "Int",
"label": "Overdue Days",
"read_only": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
@ -112,16 +103,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Dunning Type", "label": "Dunning Type",
"options": "Dunning Type", "options": "Dunning Type"
"reqd": 1
},
{
"default": "0",
"fieldname": "interest_amount",
"fieldtype": "Currency",
"label": "Interest Amount",
"precision": "2",
"read_only": 1
}, },
{ {
"fieldname": "column_break_8", "fieldname": "column_break_8",
@ -134,6 +116,7 @@
"fieldname": "dunning_fee", "fieldname": "dunning_fee",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Dunning Fee", "label": "Dunning Fee",
"options": "currency",
"precision": "2" "precision": "2"
}, },
{ {
@ -144,36 +127,24 @@
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "printing_setting_section",
"fieldtype": "Section Break",
"label": "Printing Setting"
},
{ {
"fieldname": "language", "fieldname": "language",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Print Language", "label": "Print Language",
"options": "Language" "options": "Language",
"print_hide": 1
}, },
{ {
"fieldname": "letter_head", "fieldname": "letter_head",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Letter Head", "label": "Letter Head",
"options": "Letter Head" "options": "Letter Head",
"print_hide": 1
}, },
{ {
"fieldname": "column_break_22", "fieldname": "column_break_22",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fetch_from": "sales_invoice.currency",
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@ -183,14 +154,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title"
},
{ {
"fieldname": "body_text", "fieldname": "body_text",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
@ -201,13 +164,6 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Closing Text" "label": "Closing Text"
}, },
{
"fetch_from": "sales_invoice.due_date",
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{ {
"fieldname": "posting_time", "fieldname": "posting_time",
"fieldtype": "Time", "fieldtype": "Time",
@ -222,26 +178,24 @@
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
}, },
{ {
"collapsible": 1,
"fieldname": "address_and_contact_section", "fieldname": "address_and_contact_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
}, },
{ {
"fetch_from": "sales_invoice.address_display",
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Address", "label": "Address",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_display",
"fieldname": "contact_display", "fieldname": "contact_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact", "label": "Contact",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_mobile",
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
@ -249,18 +203,12 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.company_address_display",
"fieldname": "company_address_display", "fieldname": "company_address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Company Address", "label": "Company Address Display",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_email",
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Contact Email", "label": "Contact Email",
@ -268,18 +216,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.customer",
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Customer", "label": "Customer",
"options": "Customer", "options": "Customer",
"read_only": 1 "reqd": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "grand_total", "fieldname": "grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total", "label": "Grand Total",
"options": "currency",
"precision": "2", "precision": "2",
"read_only": 1 "read_only": 1
}, },
@ -290,33 +238,150 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "Draft\nResolved\nUnresolved\nCancelled" "options": "Draft\nResolved\nUnresolved\nCancelled",
},
{
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Dunning Amount",
"read_only": 1 "read_only": 1
}, },
{ {
"description": "For dunning fee and interest",
"fetch_from": "dunning_type.income_account",
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
"options": "Account" "options": "Account",
"print_hide": 1
},
{
"fieldname": "overdue_payments",
"fieldtype": "Table",
"label": "Overdue Payments",
"options": "Overdue Payment"
},
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "total_interest",
"fieldtype": "Currency",
"label": "Total Interest",
"options": "currency",
"precision": "2",
"read_only": 1
},
{
"fieldname": "total_outstanding",
"fieldtype": "Currency",
"label": "Total Outstanding",
"options": "currency",
"read_only": 1
},
{
"fieldname": "customer_address",
"fieldtype": "Link",
"label": "Customer Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"default": "0",
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount",
"options": "currency",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "dunning_type.cost_center",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "printing_settings_section",
"fieldtype": "Section Break",
"label": "Printing Settings"
},
{
"fieldname": "section_break_32",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "spacer",
"fieldtype": "Data",
"hidden": 1,
"label": "Spacer",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
"label": "Company Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}, },
{ {
"fetch_from": "sales_invoice.conversion_rate",
"fieldname": "conversion_rate", "fieldname": "conversion_rate",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 1, "label": "Conversion Rate"
"label": "Conversion Rate", },
{
"default": "0",
"fieldname": "base_dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_48",
"fieldtype": "Column Break"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-03 16:24:01.677026", "modified": "2023-06-15 15:46:53.865712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning", "name": "Dunning",

View File

@ -1,131 +1,150 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
"""
# Accounting
1. Payment of outstanding invoices with dunning amount
- Debit full amount to bank
- Credit invoiced amount to receivables
- Credit dunning amount to interest and similar revenue
-> Resolves dunning automatically
"""
import json import json
import frappe import frappe
from frappe.utils import cint, flt, getdate from frappe import _
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class Dunning(AccountsController): class Dunning(AccountsController):
def validate(self): def validate(self):
self.validate_overdue_days() self.validate_same_currency()
self.validate_amount() self.validate_overdue_payments()
if not self.income_account: self.validate_totals()
self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") self.set_party_details()
self.set_dunning_level()
def validate_overdue_days(self): def validate_same_currency(self):
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 """
Throw an error if invoice currency differs from dunning currency.
"""
for row in self.overdue_payments:
invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
if invoice_currency != self.currency:
frappe.throw(
_(
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
).format(row.sales_invoice, invoice_currency, self.currency)
)
def validate_amount(self): def validate_overdue_payments(self):
amounts = calculate_interest_and_amount( daily_interest = self.rate_of_interest / 100 / 365
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
for row in self.overdue_payments:
row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
row.interest = row.outstanding * daily_interest * row.overdue_days
def validate_totals(self):
self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
self.total_interest = sum(row.interest for row in self.overdue_payments)
self.dunning_amount = self.total_interest + self.dunning_fee
self.base_dunning_amount = self.dunning_amount * self.conversion_rate
self.grand_total = self.total_outstanding + self.dunning_amount
def set_party_details(self):
from erpnext.accounts.party import _get_party_details
party_details = _get_party_details(
self.customer,
ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype,
company=self.company,
posting_date=self.get("posting_date"),
fetch_payment_terms_template=False,
party_address=self.customer_address,
company_address=self.get("company_address"),
) )
if self.interest_amount != amounts.get("interest_amount"): for field in [
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) "customer_address",
if self.dunning_amount != amounts.get("dunning_amount"): "address_display",
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) "company_address",
if self.grand_total != amounts.get("grand_total"): "contact_person",
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) "contact_display",
"contact_mobile",
]:
self.set(field, party_details.get(field))
def on_submit(self): self.set("company_address_display", get_address_display(self.company_address))
self.make_gl_entries()
def on_cancel(self): def set_dunning_level(self):
if self.dunning_amount: for row in self.overdue_payments:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") past_dunnings = frappe.get_all(
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) "Overdue Payment",
filters={
def make_gl_entries(self): "payment_schedule": row.payment_schedule,
if not self.dunning_amount: "parent": ("!=", row.parent),
return "docstatus": 1,
gl_entries = []
invoice_fields = [
"project",
"cost_center",
"debit_to",
"party_account_currency",
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(
self.get_gl_dict(
{
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}, },
inv.party_account_currency,
item=inv,
) )
) row.dunning_level = len(past_dunnings) + 1
gl_entries.append(
self.get_gl_dict(
{
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"cost_center": inv.cost_center or default_cost_center,
"credit_in_account_currency": self.dunning_amount,
"project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)
def resolve_dunning(doc, state): def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: # Consider partial and full payments:
dunnings = frappe.get_list( # Submitting full payment: outstanding_amount will be 0
"Dunning", # Submitting 1st partial payment: outstanding_amount will be the pending installment
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")}, # Cancelling full payment: outstanding_amount will revert to total amount
ignore_permissions=True, # Cancelling last partial payment: outstanding_amount will revert to pending amount
) submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
dunning.status = "Resolved" if resolve else "Unresolved"
dunning.save()
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): def get_linked_dunnings_as_per_state(sales_invoice, state):
interest_amount = 0 dunning = frappe.qb.DocType("Dunning")
grand_total = flt(outstanding_amount) + flt(dunning_fee) overdue_payment = frappe.qb.DocType("Overdue Payment")
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 return (
interest_amount = (interest_per_year * cint(overdue_days)) / 365 frappe.qb.from_(dunning)
grand_total += flt(interest_amount) .join(overdue_payment)
dunning_amount = flt(interest_amount) + flt(dunning_fee) .on(overdue_payment.parent == dunning.name)
return { .select(dunning.name)
"interest_amount": interest_amount, .where(
"grand_total": grand_total, (dunning.status == state)
"dunning_amount": dunning_amount, & (dunning.docstatus != 2)
} & (overdue_payment.sales_invoice == sales_invoice)
)
).run(as_dict=True)
@frappe.whitelist() @frappe.whitelist()

View File

@ -1,12 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "dunning",
"non_standard_fieldnames": {
"Journal Entry": "reference_name",
"Payment Entry": "reference_name",
},
"transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
}

View File

@ -1,162 +1,197 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate, today from frappe.utils import add_days, nowdate, today
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
unlink_payment_on_cancel_of_invoice, unlink_payment_on_cancel_of_invoice,
) )
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
create_dunning as create_dunning_from_sales_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice_against_cost_center, create_sales_invoice_against_cost_center,
) )
test_dependencies = ["Company", "Cost Center"]
class TestDunning(unittest.TestCase):
class TestDunning(FrappeTestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(cls):
create_dunning_type() super().setUpClass()
create_dunning_type_with_zero_interest_rate() create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1)
create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
unlink_payment_on_cancel_of_invoice() unlink_payment_on_cancel_of_invoice()
@classmethod @classmethod
def tearDownClass(self): def tearDownClass(cls):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
super().tearDownClass()
def test_dunning(self): def test_dunning_without_fees(self):
dunning = create_dunning() dunning = create_dunning(overdue_days=20)
amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
)
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
def test_dunning_with_zero_interest_rate(self): self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
dunning = create_dunning_with_zero_interest_rate() self.assertEqual(round(dunning.total_interest, 2), 0.00)
amounts = calculate_interest_and_amount( self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
) self.assertEqual(round(dunning.grand_total, 2), 100.00)
self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
def test_gl_entries(self): def test_dunning_with_fees_and_interest(self):
dunning = create_dunning() dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit()
gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
order by account asc""",
dunning.name,
as_dict=1,
)
self.assertTrue(gl_entries)
expected_values = dict(
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_payment_entry(self): self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
dunning = create_dunning() self.assertEqual(round(dunning.total_interest, 2), 0.41)
self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
self.assertEqual(round(dunning.grand_total, 2), 110.41)
def test_dunning_with_payment_entry(self):
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit() dunning.submit()
pe = get_payment_entry("Dunning", dunning.name) pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = nowdate() pe.reference_date = nowdate()
pe.paid_from_account_currency = dunning.currency
pe.paid_to_account_currency = dunning.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.insert() pe.insert()
pe.submit() pe.submit()
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0) for overdue_payment in dunning.overdue_payments:
outstanding_amount = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
self.assertEqual(outstanding_amount, 0)
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
def test_dunning_and_payment_against_partially_due_invoice(self):
"""
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
"""
create_payment_terms_template_for_dunning()
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -1 * 6),
qty=1,
rate=100,
do_not_submit=True,
)
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
sales_invoice.submit()
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
self.assertEqual(len(dunning.overdue_payments), 1)
self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
dunning.submit()
pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no, pe.reference_date = "2", nowdate()
pe.insert()
pe.submit()
sales_invoice.load_from_db()
dunning.load_from_db()
self.assertEqual(sales_invoice.status, "Partly Paid")
self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
self.assertEqual(dunning.status, "Resolved")
# Test impact on cancellation of PE
pe.cancel()
sales_invoice.reload()
dunning.reload()
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(): def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -20) posting_date = add_days(today(), -1 * overdue_days)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center( sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status="Overdue" posting_date=posting_date, qty=1, rate=100
) )
dunning_type = frappe.get_doc("Dunning Type", "First Notice") dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name if dunning_type_name:
dunning.customer_name = sales_invoice.customer_name dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
dunning.outstanding_amount = sales_invoice.outstanding_amount dunning.dunning_type = dunning_type.name
dunning.debit_to = sales_invoice.debit_to dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.currency = sales_invoice.currency dunning.dunning_fee = dunning_type.dunning_fee
dunning.company = sales_invoice.company dunning.income_account = dunning_type.income_account
dunning.posting_date = nowdate() dunning.cost_center = dunning_type.cost_center
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice" return dunning.save()
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_with_zero_interest_rate(): def create_dunning_type(title, fee, interest, is_default):
posting_date = add_days(today(), -20) company = "_Test Company"
due_date = add_days(today(), -15) if frappe.db.exists("Dunning Type", f"{title} - _TC"):
sales_invoice = create_sales_invoice_against_cost_center( return
posting_date=posting_date, due_date=due_date, status="Overdue"
)
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
dunning.outstanding_amount = sales_invoice.outstanding_amount
dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = "First Notice" dunning_type.dunning_type = title
dunning_type.start_day = 10 dunning_type.company = company
dunning_type.end_day = 20 dunning_type.is_default = is_default
dunning_type.dunning_fee = 20 dunning_type.dunning_fee = fee
dunning_type.rate_of_interest = 8 dunning_type.rate_of_interest = interest
dunning_type.income_account = get_income_account(company)
dunning_type.cost_center = get_default_cost_center(company)
dunning_type.append( dunning_type.append(
"dunning_letter_text", "dunning_letter_text",
{ {
"language": "en", "language": "en",
"body_text": "We have still not received payment for our invoice ", "body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
}, },
) )
dunning_type.save() dunning_type.insert()
def create_dunning_type_with_zero_interest_rate(): def get_income_account(company):
dunning_type = frappe.new_doc("Dunning Type") return (
dunning_type.dunning_type = "First Notice with 0% Rate of Interest" frappe.get_value("Company", company, "default_income_account")
dunning_type.start_day = 10 or frappe.get_all(
dunning_type.end_day = 20 "Account",
dunning_type.dunning_fee = 20 filters={"is_group": 0, "company": company},
dunning_type.rate_of_interest = 0 or_filters={
dunning_type.append( "report_type": "Profit and Loss",
"dunning_letter_text", "account_type": ("in", ("Income Account", "Temporary")),
{ },
"language": "en", limit=1,
"body_text": "We have still not received payment for our invoice ", pluck="name",
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.", )[0]
},
) )
dunning_type.save()
def create_payment_terms_template_for_dunning():
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
create_payment_term("_Test Payment Term 1 for Dunning")
create_payment_term("_Test Payment Term 2 for Dunning")
if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50 for Dunning",
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 1 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 5,
},
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 2 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 10,
},
],
}
).insert()

View File

@ -1,8 +1,24 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Dunning Type', { frappe.ui.form.on("Dunning Type", {
// refresh: function(frm) { setup: function (frm) {
frm.set_query("income_account", () => {
// } return {
filters: {
root_type: "Income",
is_group: 0,
company: frm.doc.company,
},
};
});
frm.set_query("cost_center", () => {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
},
}); });

View File

@ -1,23 +1,26 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:dunning_type", "beta": 1,
"creation": "2019-12-04 04:59:08.003664", "creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"dunning_type", "dunning_type",
"overdue_interval_section", "is_default",
"start_day", "column_break_3",
"column_break_4", "company",
"end_day",
"section_break_6", "section_break_6",
"dunning_fee", "dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"text_block_section", "text_block_section",
"dunning_letter_text" "dunning_letter_text",
"section_break_9",
"income_account",
"column_break_13",
"cost_center"
], ],
"fields": [ "fields": [
{ {
@ -45,10 +48,6 @@
"fieldtype": "Table", "fieldtype": "Table",
"options": "Dunning Letter Text" "options": "Dunning Letter Text"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@ -57,33 +56,62 @@
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "overdue_interval_section",
"fieldtype": "Section Break",
"label": "Overdue Interval"
},
{
"fieldname": "start_day",
"fieldtype": "Int",
"label": "Start Day"
},
{
"fieldname": "end_day",
"fieldtype": "Int",
"label": "End Day"
},
{ {
"fieldname": "rate_of_interest", "fieldname": "rate_of_interest",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
},
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Income Account",
"options": "Account"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
} }
], ],
"links": [], "links": [
"modified": "2020-07-15 17:14:17.835074", {
"link_doctype": "Dunning",
"link_fieldname": "dunning_type"
}
],
"modified": "2021-11-13 00:25:35.659283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Type", "name": "Dunning Type",
"naming_rule": "By script",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -2,9 +2,11 @@
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DunningType(Document): class DunningType(Document):
pass def autoname(self):
company_abbr = frappe.get_value("Company", self.company, "abbr")
self.name = f"{self.dunning_type} - {company_abbr}"

View File

@ -0,0 +1,36 @@
[
{
"doctype": "Dunning Type",
"dunning_type": "_Test First Notice",
"company": "_Test Company",
"is_default": 1,
"dunning_fee": 0.0,
"rate_of_interest": 0.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
},
{
"doctype": "Dunning Type",
"dunning_type": "_Test Second Notice",
"company": "_Test Company",
"is_default": 0,
"dunning_fee": 10.0,
"rate_of_interest": 10.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
}
]

View File

@ -264,11 +264,11 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
} }
if(jvd.party_type && jvd.party) { if(jvd.party_type && jvd.party) {
var party_field = ""; let party_field = "";
if(jvd.reference_type.indexOf("Sales")===0) { if(jvd.reference_type.indexOf("Sales")===0) {
var party_field = "customer"; party_field = "customer";
} else if (jvd.reference_type.indexOf("Purchase")===0) { } else if (jvd.reference_type.indexOf("Purchase")===0) {
var party_field = "supplier"; party_field = "supplier";
} }
if (party_field) { if (party_field) {
@ -368,7 +368,7 @@ cur_frm.cscript.update_totals = function(doc) {
td += flt(accounts[i].debit, precision("debit", accounts[i])); td += flt(accounts[i].debit, precision("debit", accounts[i]));
tc += flt(accounts[i].credit, precision("credit", accounts[i])); tc += flt(accounts[i].credit, precision("credit", accounts[i]));
} }
var doc = locals[doc.doctype][doc.name]; doc = locals[doc.doctype][doc.name];
doc.total_debit = td; doc.total_debit = td;
doc.total_credit = tc; doc.total_credit = tc;
doc.difference = flt((td - tc), precision("difference")); doc.difference = flt((td - tc), precision("difference"));

View File

@ -0,0 +1,170 @@
{
"actions": [],
"creation": "2021-09-15 18:34:27.172906",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sales_invoice",
"payment_schedule",
"dunning_level",
"payment_term",
"section_break_15",
"description",
"section_break_4",
"due_date",
"overdue_days",
"mode_of_payment",
"column_break_5",
"invoice_portion",
"section_break_16",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"interest"
],
"fields": [
{
"columns": 2,
"fieldname": "payment_term",
"fieldtype": "Link",
"label": "Payment Term",
"options": "Payment Term",
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Description"
},
{
"columns": 2,
"fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "invoice_portion",
"fieldtype": "Percent",
"label": "Invoice Portion",
"read_only": 1
},
{
"columns": 2,
"fieldname": "payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount",
"options": "currency",
"read_only": 1
},
{
"fetch_from": "payment_amount",
"fieldname": "outstanding",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount",
"options": "currency"
},
{
"default": "0",
"depends_on": "discounted_amount",
"fieldname": "discounted_amount",
"fieldtype": "Currency",
"label": "Discounted Amount",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "payment_schedule",
"fieldtype": "Data",
"label": "Payment Schedule",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "overdue_days",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Overdue Days",
"read_only": 1
},
{
"default": "1",
"fieldname": "dunning_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Dunning Level",
"read_only": 1
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break"
},
{
"fieldname": "interest",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Interest",
"options": "currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-23 13:48:27.898830",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Overdue Payment",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class OverduePayment(Document):
pass

View File

@ -1,10 +1,12 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.provide("erpnext.accounts.dimensions"); frappe.provide("erpnext.accounts.dimensions");
cur_frm.cscript.tax_table = "Advance Taxes and Charges"; cur_frm.cscript.tax_table = "Advance Taxes and Charges";
erpnext.accounts.taxes.setup_tax_validations("Payment Entry");
erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"]; frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
@ -106,12 +108,11 @@ frappe.ui.form.on('Payment Entry', {
}); });
frm.set_query("reference_doctype", "references", function() { frm.set_query("reference_doctype", "references", function() {
let doctypes = ["Journal Entry"];
if (frm.doc.party_type == "Customer") { if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type == "Supplier") { } else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else {
var doctypes = ["Journal Entry"];
} }
return { return {
@ -122,13 +123,10 @@ frappe.ui.form.on('Payment Entry', {
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) { frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
const child = locals[cdt][cdn]; const child = locals[cdt][cdn];
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) { if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
payment_term_list = payment_term_list.map(pt => pt.payment_term);
return { return {
query: "erpnext.controllers.queries.get_payment_terms_for_references",
filters: { filters: {
'name': ['in', payment_term_list] 'reference': child.reference_name
} }
} }
} }
@ -165,6 +163,7 @@ frappe.ui.form.on('Payment Entry', {
}, },
company: function(frm) { company: function(frm) {
frm.trigger('party');
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
@ -287,6 +286,13 @@ frappe.ui.form.on('Payment Entry', {
} }
}, },
mode_of_payment: function(frm) {
erpnext.accounts.pos.get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){
let payment_account_field = frm.doc.payment_type == "Receive" ? "paid_to" : "paid_from";
frm.set_value(payment_account_field, account);
})
},
party_type: function(frm) { party_type: function(frm) {
let party_types = Object.keys(frappe.boot.party_account_types); let party_types = Object.keys(frappe.boot.party_account_types);
@ -319,10 +325,6 @@ frappe.ui.form.on('Payment Entry', {
} }
}, },
company: function(frm){
frm.trigger('party');
},
party: function(frm) { party: function(frm) {
if (frm.doc.contact_email || frm.doc.contact_person) { if (frm.doc.contact_email || frm.doc.contact_person) {
frm.set_value("contact_email", ""); frm.set_value("contact_email", "");
@ -1106,7 +1108,7 @@ frappe.ui.form.on('Payment Entry', {
if (tax.charge_type === 'On Net Total') { if (tax.charge_type === 'On Net Total') {
tax.charge_type = 'On Paid Amount'; tax.charge_type = 'On Paid Amount';
} }
me.frm.add_child("taxes", tax); frm.add_child("taxes", tax);
} }
frm.events.apply_taxes(frm); frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm); frm.events.set_unallocated_amount(frm);
@ -1222,7 +1224,7 @@ frappe.ui.form.on('Payment Entry', {
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
} else { } else {
tax.grand_total_fraction_for_current_item = tax.grand_total_fraction_for_current_item =
me.frm.doc["taxes"][i-1].grand_total_fraction_for_current_item + frm.doc["taxes"][i-1].grand_total_fraction_for_current_item +
tax.tax_fraction_for_current_item; tax.tax_fraction_for_current_item;
} }
@ -1269,7 +1271,7 @@ frappe.ui.form.on('Payment Entry', {
} }
}); });
$.each(me.frm.doc["taxes"] || [], function(i, tax) { $.each(frm.doc["taxes"] || [], function(i, tax) {
let current_tax_amount = frm.events.get_current_tax_amount(frm, tax); let current_tax_amount = frm.events.get_current_tax_amount(frm, tax);
// Adjust divisional loss to the last item // Adjust divisional loss to the last item
@ -1463,4 +1465,4 @@ frappe.ui.form.on('Payment Entry', {
}); });
} }
}, },
}) })

View File

@ -207,6 +207,20 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(fail_message.format(d.idx)) frappe.throw(fail_message.format(d.idx))
def term_based_allocation_enabled_for_reference(
self, reference_doctype: str, reference_name: str
) -> bool:
if (
reference_doctype
and reference_doctype in ["Sales Invoice", "Sales Order", "Purchase Order", "Purchase Invoice"]
and reference_name
):
if template := frappe.db.get_value(reference_doctype, reference_name, "payment_terms_template"):
return frappe.db.get_value(
"Payment Terms Template", template, "allocate_payment_based_on_payment_terms"
)
return False
def validate_allocated_amount_with_latest_data(self): def validate_allocated_amount_with_latest_data(self):
latest_references = get_outstanding_reference_documents( latest_references = get_outstanding_reference_documents(
{ {
@ -226,10 +240,25 @@ class PaymentEntry(AccountsController):
latest_lookup = {} latest_lookup = {}
for d in latest_references: for d in latest_references:
d = frappe._dict(d) d = frappe._dict(d)
latest_lookup.update({(d.voucher_type, d.voucher_no): d}) latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
for d in self.get("references"): for idx, d in enumerate(self.get("references"), start=1):
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
# If term based allocation is enabled, throw
if (
d.payment_term is None or d.payment_term == ""
) and self.term_based_allocation_enabled_for_reference(
d.reference_doctype, d.reference_name
):
frappe.throw(
_(
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
).format(frappe.bold(d.reference_name), frappe.bold(idx))
)
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
latest = latest.get(d.payment_term) or latest.get(None)
# The reference has already been fully paid # The reference has already been fully paid
if not latest: if not latest:
@ -251,6 +280,18 @@ class PaymentEntry(AccountsController):
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx)) frappe.throw(fail_message.format(d.idx))
if d.payment_term and (
(flt(d.allocated_amount)) > 0
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
):
frappe.throw(
_(
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
).format(
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
)
)
# Check for negative outstanding invoices as well # Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx)) frappe.throw(fail_message.format(d.idx))
@ -1500,7 +1541,9 @@ def get_outstanding_reference_documents(args, validate=False):
accounting_dimensions=accounting_dimensions_filter, accounting_dimensions=accounting_dimensions_filter,
) )
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) outstanding_invoices = split_invoices_based_on_payment_terms(
outstanding_invoices, args.get("company")
)
for d in outstanding_invoices: for d in outstanding_invoices:
d["exchange_rate"] = 1 d["exchange_rate"] = 1
@ -1560,8 +1603,27 @@ def get_outstanding_reference_documents(args, validate=False):
return data return data
def split_invoices_based_on_payment_terms(outstanding_invoices): def split_invoices_based_on_payment_terms(outstanding_invoices, company):
invoice_ref_based_on_payment_terms = {} invoice_ref_based_on_payment_terms = {}
company_currency = (
frappe.db.get_value("Company", company, "default_currency") if company else None
)
exc_rates = frappe._dict()
for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all(
doctype,
filters={"name": ["in", invoices]},
fields=["name", "currency", "conversion_rate", "party_account_currency"],
):
exc_rates[x.name] = frappe._dict(
conversion_rate=x.conversion_rate,
currency=x.currency,
party_account_currency=x.party_account_currency,
company_currency=company_currency,
)
for idx, d in enumerate(outstanding_invoices): for idx, d in enumerate(outstanding_invoices):
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
payment_term_template = frappe.db.get_value( payment_term_template = frappe.db.get_value(
@ -1578,6 +1640,14 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
for payment_term in payment_schedule: for payment_term in payment_schedule:
if payment_term.outstanding > 0.1: if payment_term.outstanding > 0.1:
doc_details = exc_rates.get(payment_term.parent, None)
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
doc_details.party_account_currency != doc_details.company_currency
)
payment_term_outstanding = flt(payment_term.outstanding)
if not is_multi_currency_acc:
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
invoice_ref_based_on_payment_terms.setdefault(idx, []) invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append( invoice_ref_based_on_payment_terms[idx].append(
frappe._dict( frappe._dict(
@ -1589,6 +1659,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
"posting_date": d.posting_date, "posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount), "invoice_amount": flt(d.invoice_amount),
"outstanding_amount": flt(d.outstanding_amount), "outstanding_amount": flt(d.outstanding_amount),
"payment_term_outstanding": payment_term_outstanding,
"allocated_amount": payment_term_outstanding
if payment_term_outstanding
else d.outstanding_amount,
"payment_amount": payment_term.payment_amount, "payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term, "payment_term": payment_term.payment_term,
"account": d.account, "account": d.account,
@ -2010,28 +2084,27 @@ def get_payment_entry(
pe.append("references", reference) pe.append("references", reference)
else: else:
if dt == "Dunning": if dt == "Dunning":
for overdue_payment in doc.overdue_payments:
pe.append(
"references",
{
"reference_doctype": "Sales Invoice",
"reference_name": overdue_payment.sales_invoice,
"payment_term": overdue_payment.payment_term,
"due_date": overdue_payment.due_date,
"total_amount": overdue_payment.outstanding,
"outstanding_amount": overdue_payment.outstanding,
"allocated_amount": overdue_payment.outstanding,
},
)
pe.append( pe.append(
"references", "deductions",
{ {
"reference_doctype": "Sales Invoice", "account": doc.income_account,
"reference_name": doc.get("sales_invoice"), "cost_center": doc.cost_center,
"bill_no": doc.get("bill_no"), "amount": -1 * doc.dunning_amount,
"due_date": doc.get("due_date"), "description": _("Interest and/or dunning fee"),
"total_amount": doc.get("outstanding_amount"),
"outstanding_amount": doc.get("outstanding_amount"),
"allocated_amount": doc.get("outstanding_amount"),
},
)
pe.append(
"references",
{
"reference_doctype": dt,
"reference_name": dn,
"bill_no": doc.get("bill_no"),
"due_date": doc.get("due_date"),
"total_amount": doc.get("dunning_amount"),
"outstanding_amount": doc.get("dunning_amount"),
"allocated_amount": doc.get("dunning_amount"),
}, },
) )
else: else:
@ -2125,8 +2198,10 @@ def set_party_account_currency(dt, party_account, doc):
def set_payment_type(dt, doc): def set_payment_type(dt, doc):
if ( if (
dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) (dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0))
) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): or (dt == "Purchase Invoice" and doc.outstanding_amount < 0)
or dt == "Dunning"
):
payment_type = "Receive" payment_type = "Receive"
else: else:
payment_type = "Pay" payment_type = "Pay"
@ -2371,6 +2446,7 @@ def get_reference_as_per_payment_terms(
"due_date": doc.get("due_date"), "due_date": doc.get("due_date"),
"total_amount": grand_total, "total_amount": grand_total,
"outstanding_amount": outstanding_amount, "outstanding_amount": outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_term": payment_term.payment_term, "payment_term": payment_term.payment_term,
"allocated_amount": payment_term_outstanding, "allocated_amount": payment_term_outstanding,
} }

View File

@ -1061,6 +1061,101 @@ class TestPaymentEntry(FrappeTestCase):
} }
self.assertDictEqual(ref_details, expected_response) self.assertDictEqual(ref_details, expected_response)
@change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
"delete_linked_ledger_entries": 1,
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_overallocation_validation_on_payment_terms(self):
"""
Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
"""
customer = create_customer()
create_payment_terms_template()
# Validate allocation on base/company currency
si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si1.payment_terms_template = "Test Receivable Template"
si1.save().submit()
si1.reload()
pe = get_payment_entry(si1.doctype, si1.name).save()
# Allocated amount should be according to the payment schedule
for idx, schedule in enumerate(si1.payment_schedule):
with self.subTest(idx=idx):
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 400
pe.references[0].allocated_amount = 200
pe.references[1].allocated_amount = 200
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si1.cancel()
si1.delete()
# Validate allocation on foreign currency
si2 = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=80,
do_not_save=1,
)
si2.payment_terms_template = "Test Receivable Template"
si2.save().submit()
si2.reload()
pe = get_payment_entry(si2.doctype, si2.name).save()
# Allocated amount should be according to the payment schedule
for idx, schedule in enumerate(si2.payment_schedule):
with self.subTest(idx=idx):
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 200
pe.references[0].allocated_amount = 100
pe.references[1].allocated_amount = 100
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si2.cancel()
si2.delete()
# Validate allocation in base/company currency on a foreign currency document
# when invoice is made is foreign currency, but posted to base/company currency debtors account
si3 = create_sales_invoice(
customer=customer,
currency="USD",
conversion_rate=80,
do_not_save=1,
)
si3.payment_terms_template = "Test Receivable Template"
si3.save().submit()
si3.reload()
pe = get_payment_entry(si3.doctype, si3.name).save()
# Allocated amount should be equal to payment term outstanding
self.assertEqual(len(pe.references), 2)
for idx, ref in enumerate(pe.references):
with self.subTest(idx=idx):
self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 16000
pe.references[0].allocated_amount = 8000
pe.references[1].allocated_amount = 8000
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si3.cancel()
si3.delete()
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")
@ -1150,3 +1245,17 @@ def create_payment_terms_template_with_discount(
def create_payment_term(name): def create_payment_term(name):
if not frappe.db.exists("Payment Term", name): if not frappe.db.exists("Payment Term", name):
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert() frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer = None
if frappe.db.exists("Customer", name):
customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.default_currency = currency
customer.type = "Individual"
customer.save()
customer = customer.name
return customer

View File

@ -124,7 +124,7 @@ frappe.ui.form.on('Payment Order', {
return frappe.call({ return frappe.call({
method: "erpnext.accounts.doctype.payment_order.payment_order.make_payment_records", method: "erpnext.accounts.doctype.payment_order.payment_order.make_payment_records",
args: { args: {
"name": me.frm.doc.name, "name": frm.doc.name,
"supplier": args.supplier, "supplier": args.supplier,
"mode_of_payment": args.mode_of_payment "mode_of_payment": args.mode_of_payment
}, },

View File

@ -14,7 +14,7 @@ frappe.ui.form.on('Payment Term', {
if (frm.doc.discount) { if (frm.doc.discount) {
let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]); let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]);
if (frm.doc.discount_type == 'Amount') { if (frm.doc.discount_type == 'Amount') {
description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]); description = __("{0} will be given as discount.", [frm.doc.discount]);
} }
frm.set_df_property("discount", "description", description); frm.set_df_property("discount", "description", description);
} }

View File

@ -126,21 +126,22 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self, get_opening_entries=False): def make_gl_entries(self, get_opening_entries=False):
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if gl_entries: if len(gl_entries) > 5000:
if len(gl_entries) > 5000: frappe.enqueue(
frappe.enqueue( process_gl_entries,
process_gl_entries, gl_entries=gl_entries,
gl_entries=gl_entries, closing_entries=closing_entries,
closing_entries=closing_entries, voucher_name=self.name,
voucher_name=self.name, company=self.company,
queue="long", closing_date=self.posting_date,
) queue="long",
frappe.msgprint( )
_("The GL Entries will be processed in the background, it can take a few minutes."), frappe.msgprint(
alert=True, _("The GL Entries will be processed in the background, it can take a few minutes."),
) alert=True,
else: )
process_gl_entries(gl_entries, closing_entries, voucher_name=self.name) else:
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
def get_grouped_gl_entries(self, get_opening_entries=False): def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = [] closing_entries = []
@ -321,24 +322,22 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1) return query.run(as_dict=1)
def process_gl_entries(gl_entries, closing_entries, voucher_name=None): def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries, make_closing_entries,
) )
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
try: try:
make_gl_entries(gl_entries, merge_entries=False) if gl_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name) make_gl_entries(gl_entries, merge_entries=False)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
) frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e: except Exception as e:
frappe.db.rollback() frappe.db.rollback()
frappe.log_error(e) frappe.log_error(e)
frappe.db.set_value( frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
)
def make_reverse_gl_entries(voucher_type, voucher_no): def make_reverse_gl_entries(voucher_type, voucher_no):

View File

@ -1,9 +1,10 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.sales_common.setup_selling_controller();
erpnext.accounts.pos.setup("POS Invoice");
erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnext.selling.SellingController { erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnext.selling.SellingController {
settings = {}; settings = {};

View File

@ -1,8 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.ui.form.on('POS Profile', { frappe.ui.form.on('POS Profile', {
setup: function(frm) { setup: function(frm) {
frm.set_query("selling_price_list", function() { frm.set_query("selling_price_list", function() {
@ -148,4 +146,4 @@ frappe.ui.form.on('POS Profile', {
frm.toggle_display('expense_account', frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company)); erpnext.is_perpetual_inventory_enabled(frm.doc.company));
} }
}); });

View File

@ -140,7 +140,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
def get_ar_filters(doc, entry): def get_ar_filters(doc, entry):
return { return {
"report_date": doc.posting_date if doc.posting_date else None, "report_date": doc.posting_date if doc.posting_date else None,
"customer_name": entry.customer, "customer": entry.customer,
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
"sales_partner": doc.sales_partner if doc.sales_partner else None, "sales_partner": doc.sales_partner if doc.sales_partner else None,
"sales_person": doc.sales_person if doc.sales_person else None, "sales_person": doc.sales_person if doc.sales_person else None,

View File

@ -10,16 +10,12 @@
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2> <h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
<h4 class="text-center"> <h4 class="text-center">
{% if (filters.customer_name) %} {{ filters.customer }}
{{ filters.customer_name }}
{% else %}
{{ filters.customer ~ filters.supplier }}
{% endif %}
</h4> </h4>
<h6 class="text-center"> <h6 class="text-center">
{% if (filters.tax_id) %} {% if (filters.tax_id) %}
{{ _("Tax Id: ") }}{{ filters.tax_id }} {{ _("Tax Id: ") }}{{ filters.tax_id }}
{% endif %} {% endif %}
</h6> </h6>
<h5 class="text-center"> <h5 class="text-center">
{{ _(filters.ageing_based_on) }} {{ _(filters.ageing_based_on) }}

View File

@ -2,7 +2,11 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
{% include 'erpnext/public/js/controllers/buying.js' %};
erpnext.accounts.payment_triggers.setup("Purchase Invoice");
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
erpnext.accounts.taxes.setup_tax_validations("Purchase Invoice");
erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController { erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) { setup(doc) {
@ -506,7 +510,8 @@ frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Purchase Invoice': 'Return / Debit Note', 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment' 'Payment Entry': 'Payment',
'Landed Cost Voucher': function () { frm.trigger('create_landed_cost_voucher') },
} }
frm.set_query("additional_discount_account", function() { frm.set_query("additional_discount_account", function() {
@ -544,6 +549,26 @@ frappe.ui.form.on("Purchase Invoice", {
frm.events.add_custom_buttons(frm); frm.events.add_custom_buttons(frm);
}, },
mode_of_payment: function(frm) {
erpnext.accounts.pos.get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account) {
frm.set_value("cash_bank_account", account);
})
},
create_landed_cost_voucher: function (frm) {
let lcv = frappe.model.get_new_doc('Landed Cost Voucher');
lcv.company = frm.doc.company;
let lcv_receipt = frappe.model.get_new_doc('Landed Cost Purchase Invoice');
lcv_receipt.receipt_document_type = 'Purchase Invoice';
lcv_receipt.receipt_document = frm.doc.name;
lcv_receipt.supplier = frm.doc.supplier;
lcv_receipt.grand_total = frm.doc.grand_total;
lcv.purchase_receipts = [lcv_receipt];
frappe.set_route("Form", lcv.doctype, lcv.name);
},
add_custom_buttons: function(frm) { add_custom_buttons: function(frm) {
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) { if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => { frm.add_custom_button(__('Purchase Receipt'), () => {

View File

@ -1,30 +1,31 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
cur_frm.cscript.tax_table = "Purchase Taxes and Charges"; erpnext.accounts.taxes.setup_tax_validations("Purchase Taxes and Charges Template");
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
{% include "erpnext/public/js/controllers/accounts.js" %} frappe.ui.form.on("Purchase Taxes and Charges", {
add_deduct_tax(doc, cdt, cdn) {
let d = locals[cdt][cdn];
frappe.ui.form.on("Purchase Taxes and Charges", "add_deduct_tax", function(doc, cdt, cdn) { if(!d.category && d.add_deduct_tax) {
var d = locals[cdt][cdn]; frappe.msgprint(__("Please select Category first"));
d.add_deduct_tax = '';
}
else if(d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Valuation and Total'"));
d.add_deduct_tax = '';
}
refresh_field('add_deduct_tax', d.name, 'taxes');
},
if(!d.category && d.add_deduct_tax) { category(doc, cdt, cdn) {
frappe.msgprint(__("Please select Category first")); let d = locals[cdt][cdn];
d.add_deduct_tax = '';
if(d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Valuation and Total'"));
d.add_deduct_tax = '';
}
refresh_field('add_deduct_tax', d.name, 'taxes');
} }
else if(d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Valuation and Total'"));
d.add_deduct_tax = '';
}
refresh_field('add_deduct_tax', d.name, 'taxes');
});
frappe.ui.form.on("Purchase Taxes and Charges", "category", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.category != 'Total' && d.add_deduct_tax == 'Deduct') {
frappe.msgprint(__("Cannot deduct when category is for 'Valuation' or 'Vaulation and Total'"));
d.add_deduct_tax = '';
}
refresh_field('add_deduct_tax', d.name, 'taxes');
}); });

View File

@ -1,3 +1,23 @@
{% include "erpnext/regional/italy/sales_invoice.js" %} frappe.ui.form.on("Sales Invoice", {
refresh: (frm) => {
erpnext.setup_e_invoice_button('Sales Invoice') if(frm.doc.docstatus == 1) {
frm.add_custom_button(__('Generate E-Invoice'), () => {
frm.call({
method: "erpnext.regional.italy.utils.generate_single_invoice",
args: {
docname: frm.doc.name
},
callback: function(r) {
frm.reload_doc();
if(r.message) {
open_url_post(frappe.request.url, {
cmd: 'frappe.core.doctype.file.file.download_file',
file_url: r.message
});
}
}
});
});
}
}
});

View File

@ -1,10 +1,13 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
erpnext.accounts.taxes.setup_tax_validations("Sales Invoice");
erpnext.accounts.payment_triggers.setup("Sales Invoice");
erpnext.accounts.pos.setup("Sales Invoice");
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
erpnext.sales_common.setup_selling_controller();
erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends erpnext.selling.SellingController { erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends erpnext.selling.SellingController {
setup(doc) { setup(doc) {
this.setup_posting_date_time_check(); this.setup_posting_date_time_check();
@ -142,9 +145,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
cur_frm.events.create_invoice_discounting(cur_frm); cur_frm.events.create_invoice_discounting(cur_frm);
}, __('Create')); }, __('Create'));
if (doc.due_date < frappe.datetime.get_today()) { const payment_is_overdue = doc.payment_schedule.map(
cur_frm.add_custom_button(__('Dunning'), function() { row => Date.parse(row.due_date) < Date.now()
cur_frm.events.create_dunning(cur_frm); ).reduce(
(prev, current) => prev || current
);
if (payment_is_overdue) {
this.frm.add_custom_button(__('Dunning'), () => {
this.frm.events.create_dunning(this.frm);
}, __('Create')); }, __('Create'));
} }
} }
@ -711,7 +720,7 @@ frappe.ui.form.on('Sales Invoice', {
frm.set_query('pos_profile', function(doc) { frm.set_query('pos_profile', function(doc) {
if(!doc.company) { if(!doc.company) {
frappe.throw(_('Please set Company')); frappe.throw(__('Please set Company'));
} }
return { return {
@ -858,7 +867,7 @@ frappe.ui.form.on('Sales Invoice', {
kwargs = Object(); kwargs = Object();
} }
if (!kwargs.hasOwnProperty("project") && frm.doc.project) { if (!Object.prototype.hasOwnProperty.call(kwargs, "project") && frm.doc.project) {
kwargs.project = frm.doc.project; kwargs.project = frm.doc.project;
} }
@ -891,6 +900,8 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0); frm.events.append_time_log(frm, timesheet, 1.0);
} }
}); });
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
}, },
async get_exchange_rate(frm, from_currency, to_currency) { async get_exchange_rate(frm, from_currency, to_currency) {
@ -930,9 +941,6 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate); row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name; row.timesheet_detail = time_log.name;
row.project_name = time_log.project_name; row.project_name = time_log.project_name;
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
}, },
calculate_timesheet_totals: function(frm) { calculate_timesheet_totals: function(frm) {

View File

@ -2516,55 +2516,49 @@ def get_mode_of_payment_info(mode_of_payment, company):
@frappe.whitelist() @frappe.whitelist()
def create_dunning(source_name, target_doc=None): def create_dunning(source_name, target_doc=None, ignore_permissions=False):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import ( def postprocess_dunning(source, target):
calculate_interest_and_amount, from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
get_dunning_letter_text,
)
def set_missing_values(source, target): dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
target.sales_invoice = source_name if dunning_type:
target.outstanding_amount = source.outstanding_amount dunning_type = frappe.get_doc("Dunning Type", dunning_type)
overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
target.overdue_days = overdue_days
if frappe.db.exists(
"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
):
dunning_type = frappe.get_doc(
"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
)
target.dunning_type = dunning_type.name target.dunning_type = dunning_type.name
target.rate_of_interest = dunning_type.rate_of_interest target.rate_of_interest = dunning_type.rate_of_interest
target.dunning_fee = dunning_type.dunning_fee target.dunning_fee = dunning_type.dunning_fee
letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) target.income_account = dunning_type.income_account
target.cost_center = dunning_type.cost_center
letter_text = get_dunning_letter_text(
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
)
if letter_text: if letter_text:
target.body_text = letter_text.get("body_text") target.body_text = letter_text.get("body_text")
target.closing_text = letter_text.get("closing_text") target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language") target.language = letter_text.get("language")
amounts = calculate_interest_and_amount(
target.outstanding_amount,
target.rate_of_interest,
target.dunning_fee,
target.overdue_days,
)
target.interest_amount = amounts.get("interest_amount")
target.dunning_amount = amounts.get("dunning_amount")
target.grand_total = amounts.get("grand_total")
doclist = get_mapped_doc( target.validate()
"Sales Invoice",
source_name, return get_mapped_doc(
{ from_doctype="Sales Invoice",
from_docname=source_name,
target_doc=target_doc,
table_maps={
"Sales Invoice": { "Sales Invoice": {
"doctype": "Dunning", "doctype": "Dunning",
} "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"},
},
"Payment Schedule": {
"doctype": "Overdue Payment",
"field_map": {"name": "payment_schedule", "parent": "sales_invoice"},
"condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(),
},
}, },
target_doc, postprocess=postprocess_dunning,
set_missing_values, ignore_permissions=ignore_permissions,
) )
return doclist
def check_if_return_invoice_linked_with_payment_entry(self): def check_if_return_invoice_linked_with_payment_entry(self):

View File

@ -1900,16 +1900,22 @@ class TestSalesInvoice(unittest.TestCase):
si = self.create_si_to_test_tax_breakup() si = self.create_si_to_test_tax_breakup()
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) itemised_tax_data = get_itemised_tax_breakup_data(si)
expected_itemised_tax = { expected_itemised_tax = [
"_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}}, {
"_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}}, "item": "_Test Item",
} "taxable_amount": 10000.0,
expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0} "Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0},
},
{
"item": "_Test Item 2",
"taxable_amount": 5000.0,
"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0},
},
]
self.assertEqual(itemised_tax, expected_itemised_tax) self.assertEqual(itemised_tax_data, expected_itemised_tax)
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
frappe.flags.country = None frappe.flags.country = None

View File

@ -1,6 +1,5 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
cur_frm.cscript.tax_table = "Sales Taxes and Charges"; erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template");
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
{% include "erpnext/public/js/controllers/accounts.js" %}

View File

@ -13,14 +13,11 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry from erpnext.accounts.utils import create_payment_ledger_entry
class ClosedAccountingPeriod(frappe.ValidationError):
pass
def make_gl_entries( def make_gl_entries(
gl_map, gl_map,
cancel=False, cancel=False,

View File

@ -33,6 +33,7 @@ import erpnext
from erpnext import get_company_currency from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
SALES_TRANSACTION_TYPES = { SALES_TRANSACTION_TYPES = {
@ -261,9 +262,8 @@ def set_address_details(
) )
if doctype in TRANSACTION_TYPES: if doctype in TRANSACTION_TYPES:
# required to set correct region with temporary_flag("company", company):
frappe.flags.company = company get_regional_address_details(party_details, doctype, company)
get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address return party_address, shipping_address

View File

@ -1,4 +1,5 @@
{ {
"absolute_value": 0,
"align_labels_right": 0, "align_labels_right": 0,
"creation": "2019-12-11 04:37:14.012805", "creation": "2019-12-11 04:37:14.012805",
"css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n", "css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n",
@ -9,10 +10,10 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Print Format", "doctype": "Print Format",
"font": "Arial", "font": "Arial",
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-borderless table-data\\\">\\n <tbody>\\n <tr>\\n <th>{{_(\\\"Description\\\")}}</th>\\n\\t <th style=\\\"text-align: right;\\\">{{_(\\\"Amount\\\")}}</th>\\n </tr>\\n <tr>\\n <td>\\n {{_(\\\"Outstanding Amount\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n </td>\\n </tr>\\n {%if doc.rate_of_interest > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Dunning Fee\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n </tbody>\\n</table>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n<div class=\\\"row total\\\" style =\\\"margin-right: 0px;\\\">\\n\\t\\t<div class=\\\"col-xs-5\\\">\\n\\t\\t\\t<b>{{_(\\\"Grand Total\\\")}}</b></div>\\n\\t\\t<div class=\\\"col-xs-7 text-right\\\" style=\\\"padding-right: 4px;\\\">\\n\\t\\t\\t<b>{{doc.get_formatted(\\\"grand_total\\\")}}</b>\\n\\t\\t</div>\\n</div>\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
"idx": 0, "idx": 0,
"line_breaks": 0, "line_breaks": 0,
"modified": "2020-07-14 18:25:44.348207", "modified": "2021-09-30 10:22:02.603871",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Letter", "name": "Dunning Letter",

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Account Balance"] = { frappe.query_reports["Account Balance"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports['Billed Items To Be Received'] = { frappe.query_reports['Billed Items To Be Received'] = {
'filters': [ 'filters': [
@ -17,7 +17,7 @@ frappe.query_reports['Billed Items To Be Received'] = {
'fieldname': 'posting_date', 'fieldname': 'posting_date',
'fieldtype': 'Date', 'fieldtype': 'Date',
'reqd': 1, 'reqd': 1,
'default': get_today() 'default': frappe.datetime.get_today()
}, },
{ {
'label': __('Purchase Invoice'), 'label': __('Purchase Invoice'),

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Consolidated Financial Statement"] = { frappe.query_reports["Consolidated Financial Statement"] = {

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Customer Ledger Summary"] = { frappe.query_reports["Customer Ledger Summary"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
function get_filters() { function get_filters() {
let filters = [ let filters = [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.require("assets/erpnext/js/financial_statements.js", function() {
frappe.query_reports["Dimension-wise Accounts Balance Report"] = { frappe.query_reports["Dimension-wise Accounts Balance Report"] = {

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Gross and Net Profit Report"] = { frappe.query_reports["Gross and Net Profit Report"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Inactive Sales Items"] = { frappe.query_reports["Inactive Sales Items"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
function get_filters() { function get_filters() {
let filters = [ let filters = [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["POS Register"] = { frappe.query_reports["POS Register"] = {
"filters": [ "filters": [

View File

@ -16,9 +16,30 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldname": "based_on", "fieldname": "based_on",
"label": __("Based On"), "label": __("Based On"),
"fieldtype": "Select", "fieldtype": "Select",
"options": ["Cost Center", "Project"], "options": ["Cost Center", "Project", "Accounting Dimension"],
"default": "Cost Center", "default": "Cost Center",
"reqd": 1 "reqd": 1,
"on_change": function(query_report){
let based_on = query_report.get_values().based_on;
if(based_on!='Accounting Dimension'){
frappe.query_report.set_filter_value({
accounting_dimension: ''
});
}
}
},
{
"fieldname": "accounting_dimension",
"label": __("Accounting Dimension"),
"fieldtype": "Link",
"options": "Accounting Dimension",
"get_query": () =>{
return {
filters: {
"disabled": 0
}
}
}
}, },
{ {
"fieldname": "fiscal_year", "fieldname": "fiscal_year",

View File

@ -6,6 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
filter_accounts, filter_accounts,
filter_out_zero_value_rows, filter_out_zero_value_rows,
@ -16,10 +17,12 @@ value_fields = ("income", "expense", "gross_profit_loss")
def execute(filters=None): def execute(filters=None):
if not filters.get("based_on"): if filters.get("based_on") == "Accounting Dimension" and not filters.get("accounting_dimension"):
filters["based_on"] = "Cost Center" frappe.throw(_("Select Accounting Dimension."))
based_on = filters.based_on.replace(" ", "_").lower() based_on = (
filters.based_on if filters.based_on != "Accounting Dimension" else filters.accounting_dimension
)
validate_filters(filters) validate_filters(filters)
accounts = get_accounts_data(based_on, filters.get("company")) accounts = get_accounts_data(based_on, filters.get("company"))
data = get_data(accounts, filters, based_on) data = get_data(accounts, filters, based_on)
@ -28,14 +31,14 @@ def execute(filters=None):
def get_accounts_data(based_on, company): def get_accounts_data(based_on, company):
if based_on == "cost_center": if based_on == "Cost Center":
return frappe.db.sql( return frappe.db.sql(
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt """select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
from `tabCost Center` where company=%s order by name""", from `tabCost Center` where company=%s order by name""",
company, company,
as_dict=True, as_dict=True,
) )
elif based_on == "project": elif based_on == "Project":
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name") return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
else: else:
filters = {} filters = {}
@ -56,11 +59,17 @@ def get_data(accounts, filters, based_on):
gl_entries_by_account = {} gl_entries_by_account = {}
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)[0]
fieldname = ""
for dimension in accounting_dimensions:
if dimension["document_type"] == based_on:
fieldname = dimension["fieldname"]
set_gl_entries_by_account( set_gl_entries_by_account(
filters.get("company"), filters.get("company"),
filters.get("from_date"), filters.get("from_date"),
filters.get("to_date"), filters.get("to_date"),
based_on, fieldname,
gl_entries_by_account, gl_entries_by_account,
ignore_closing_entries=not flt(filters.get("with_period_closing_entry")), ignore_closing_entries=not flt(filters.get("with_period_closing_entry")),
) )

View File

@ -29,7 +29,6 @@ frappe.query_reports["Sales Payment Summary"] = {
"label": __("Owner"), "label": __("Owner"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "User", "options": "User",
"defaults": user
}, },
{ {
"fieldname":"is_pos", "fieldname":"is_pos",

View File

@ -1,7 +1,7 @@
// -*- coding: utf-8 -*- // -*- coding: utf-8 -*-
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Share Balance"] = { frappe.query_reports["Share Balance"] = {
"filters": [ "filters": [

View File

@ -1,7 +1,7 @@
// -*- coding: utf-8 -*- // -*- coding: utf-8 -*-
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Share Ledger"] = { frappe.query_reports["Share Ledger"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Supplier Ledger Summary"] = { frappe.query_reports["Supplier Ledger Summary"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Tax Withholding Details"] = { frappe.query_reports["Tax Withholding Details"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["TDS Computation Summary"] = { frappe.query_reports["TDS Computation Summary"] = {
"filters": [ "filters": [

View File

@ -231,6 +231,9 @@ def get_opening_balance(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes") (closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
) )
if doctype == "GL Entry":
opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
if ( if (
not filters.show_unclosed_fy_pl_balances not filters.show_unclosed_fy_pl_balances
and report_type == "Profit and Loss" and report_type == "Profit and Loss"

View File

@ -1,6 +1,6 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Voucher-wise Balance"] = { frappe.query_reports["Voucher-wise Balance"] = {
"filters": [ "filters": [

View File

@ -1112,7 +1112,8 @@ def get_autoname_with_number(number_value, doc_title, company):
def parse_naming_series_variable(doc, variable): def parse_naming_series_variable(doc, variable):
if variable == "FY": if variable == "FY":
return get_fiscal_year(date=doc.get("posting_date"), company=doc.get("company"))[0] date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
return get_fiscal_year(date=date, company=doc.get("company"))[0]
@frappe.whitelist() @frappe.whitelist()

View File

@ -473,7 +473,7 @@ frappe.ui.form.on('Asset', {
} }
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code); const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) { if (!item) {
doctype_field = frappe.scrub(doctype) let doctype_field = frappe.scrub(doctype)
frm.set_value(doctype_field, ''); frm.set_value(doctype_field, '');
frappe.msgprint({ frappe.msgprint({
title: __('Invalid {0}', [__(doctype)]), title: __('Invalid {0}', [__(doctype)]),

View File

@ -933,6 +933,8 @@ def create_new_asset_after_split(asset, split_qty):
) )
new_asset.gross_purchase_amount = new_gross_purchase_amount new_asset.gross_purchase_amount = new_gross_purchase_amount
if asset.purchase_receipt_amount:
new_asset.purchase_receipt_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name new_asset.split_from = asset.name

View File

@ -62,20 +62,21 @@ class AssetMovement(Document):
frappe.throw(_("Source and Target Location cannot be same")) frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt": if self.purpose == "Receipt":
if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee): if not (d.source_location) and not (d.target_location or d.to_employee):
frappe.throw( frappe.throw(
_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)
) )
elif d.from_employee and not d.target_location: elif d.source_location:
frappe.throw( if d.from_employee and not d.target_location:
_("Target Location is required while receiving Asset {0} from an employee").format(d.asset) frappe.throw(
) _("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
elif d.to_employee and d.target_location: )
frappe.throw( elif d.to_employee and d.target_location:
_( frappe.throw(
"Asset {0} cannot be received at a location and given to an employee in a single movement" _(
).format(d.asset) "Asset {0} cannot be received at a location and given to an employee in a single movement"
) ).format(d.asset)
)
def validate_employee(self): def validate_employee(self):
for d in self.assets: for d in self.assets:

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Fixed Asset Register"] = { frappe.query_reports["Fixed Asset Register"] = {
"filters": [ "filters": [

View File

@ -5,18 +5,19 @@
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}",
"idx": 0, "idx": 1,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-21 16:13:25.092287", "modified": "2023-07-19 13:06:42.937941",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Trends", "name": "Purchase Order Trends",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Purchase Order Trends", "report_name": "Purchase Order Trends",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Line", "type": "Line",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -4,18 +4,19 @@
"creation": "2020-07-20 21:01:02.329519", "creation": "2020-07-20 21:01:02.329519",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 12:43:40.829652", "modified": "2023-07-19 13:07:41.753556",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Top Suppliers", "name": "Top Suppliers",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Purchase Receipt Trends", "report_name": "Purchase Receipt Trends",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -3,7 +3,10 @@
frappe.provide("erpnext.buying"); frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions"); frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %};
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
erpnext.accounts.taxes.setup_tax_validations("Purchase Order");
erpnext.buying.setup_buying_controller();
frappe.ui.form.on("Purchase Order", { frappe.ui.form.on("Purchase Order", {
setup: function(frm) { setup: function(frm) {

View File

@ -1,11 +1,10 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
{% include 'erpnext/public/js/controllers/buying.js' %};
cur_frm.add_fetch('contact', 'email_id', 'email_id') cur_frm.add_fetch('contact', 'email_id', 'email_id')
erpnext.buying.setup_buying_controller();
frappe.ui.form.on("Request for Quotation",{ frappe.ui.form.on("Request for Quotation",{
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
@ -436,7 +435,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
//Remove blanks //Remove blanks
for (var j = 0; j < frm.doc.suppliers.length; j++) { for (var j = 0; j < frm.doc.suppliers.length; j++) {
if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) { if(!Object.prototype.hasOwnProperty.call(frm.doc.suppliers[j], "supplier")) {
frm.get_field("suppliers").grid.grid_rows[j].remove(); frm.get_field("suppliers").grid.grid_rows[j].remove();
} }
} }
@ -445,10 +444,11 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
if(r.message) { if(r.message) {
for (var i = 0; i < r.message.length; i++) { for (var i = 0; i < r.message.length; i++) {
var exists = false; var exists = false;
let supplier = "";
if (r.message[i].constructor === Array){ if (r.message[i].constructor === Array){
var supplier = r.message[i][0]; supplier = r.message[i][0];
} else { } else {
var supplier = r.message[i].name; supplier = r.message[i].name;
} }
for (var j = 0; j < doc.suppliers.length;j++) { for (var j = 0; j < doc.suppliers.length;j++) {

View File

@ -1,9 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
// attach required files erpnext.buying.setup_buying_controller();
{% include 'erpnext/public/js/controllers/buying.js' %};
erpnext.buying.SupplierQuotationController = class SupplierQuotationController extends erpnext.buying.BuyingController { erpnext.buying.SupplierQuotationController = class SupplierQuotationController extends erpnext.buying.BuyingController {
setup() { setup() {
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {

View File

@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* global frappe, refresh_field */
frappe.ui.form.on("Supplier Scorecard", { frappe.ui.form.on("Supplier Scorecard", {
setup: function(frm) { setup: function(frm) {
if (frm.doc.indicator_color !== "") { if (frm.doc.indicator_color !== "") {
@ -79,7 +77,7 @@ var loadAllStandings = function(frm) {
callback: function(r) { callback: function(r) {
for (var j = 0; j < frm.doc.standings.length; j++) for (var j = 0; j < frm.doc.standings.length; j++)
{ {
if(!frm.doc.standings[j].hasOwnProperty("standing_name")) { if(!Object.prototype.hasOwnProperty.call(frm.doc.standings[j], "standing_name")) {
frm.get_field("standings").grid.grid_rows[j].remove(); frm.get_field("standings").grid.grid_rows[j].remove();
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* global frappe, __ */
frappe.listview_settings["Supplier Scorecard"] = { frappe.listview_settings["Supplier Scorecard"] = {
add_fields: ["indicator_color", "status"], add_fields: ["indicator_color", "status"],
@ -14,4 +13,4 @@ frappe.listview_settings["Supplier Scorecard"] = {
} }
}, },
}; }

View File

@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* global frappe */
frappe.ui.form.on("Supplier Scorecard Criteria", { frappe.ui.form.on("Supplier Scorecard Criteria", {
refresh: function() {} refresh: function() {}
}); });

View File

@ -1,9 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* global frappe */
frappe.ui.form.on("Supplier Scorecard Period", { frappe.ui.form.on("Supplier Scorecard Period", {
onload: function(frm) { onload: function(frm) {
let criteria_grid = frm.get_field("criteria").grid; let criteria_grid = frm.get_field("criteria").grid;

View File

@ -1,7 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* global frappe */
frappe.ui.form.on("Supplier Scorecard Standing", { frappe.ui.form.on("Supplier Scorecard Standing", {
refresh: function() { refresh: function() {

View File

@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* global frappe */
frappe.ui.form.on("Supplier Scorecard Variable", { frappe.ui.form.on("Supplier Scorecard Variable", {
refresh: function() { refresh: function() {

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Procurement Tracker"] = { frappe.query_reports["Procurement Tracker"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Purchase Analytics"] = { frappe.query_reports["Purchase Analytics"] = {
"filters": [ "filters": [
@ -81,8 +81,9 @@ frappe.query_reports["Purchase Analytics"] = {
const tree_type = frappe.query_report.filters[0].value; const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return; if (data_doctype != tree_type) return;
row_name = data[2].content; let row_name = data[2].content;
length = data.length; let length = data.length;
let row_values = '';
if (tree_type == "Supplier") { if (tree_type == "Supplier") {
row_values = data row_values = data
@ -104,7 +105,7 @@ frappe.query_reports["Purchase Analytics"] = {
}); });
} }
entry = { let entry = {
name: row_name, name: row_name,
values: row_values, values: row_values,
}; };

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Purchase Order Analysis"] = { frappe.query_reports["Purchase Order Analysis"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Requested Items to Order and Receive"] = { frappe.query_reports["Requested Items to Order and Receive"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontract Order Summary"] = { frappe.query_reports["Subcontract Order Summary"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontracted Item To Be Received"] = { frappe.query_reports["Subcontracted Item To Be Received"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = { frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
"filters": [ "filters": [

View File

@ -56,6 +56,7 @@ from erpnext.stock.get_item_details import (
get_item_tax_map, get_item_tax_map,
get_item_warehouse, get_item_warehouse,
) )
from erpnext.utilities.regional import temporary_flag
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
@ -760,7 +761,9 @@ class AccountsController(TransactionBase):
} }
) )
update_gl_dict_with_regional_fields(self, gl_dict) with temporary_flag("company", self.company):
update_gl_dict_with_regional_fields(self, gl_dict)
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
dimension_dict = frappe._dict() dimension_dict = frappe._dict()

View File

@ -822,6 +822,15 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters) return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters):
doctypes = frappe.get_hooks("period_closing_doctypes")
if txt:
doctypes = [d for d in doctypes if txt.lower() in d.lower()]
return [(d,) for d in set(doctypes)]
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters): def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
@ -865,3 +874,18 @@ def get_fields(doctype, fields=None):
fields.insert(1, meta.title_field.strip()) fields.insert(1, meta.title_field.strip())
return unique(fields) return unique(fields)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list:
terms = []
if filters:
terms = frappe.db.get_all(
"Payment Schedule",
filters={"parent": filters.get("reference")},
fields=["payment_term"],
limit=page_len,
as_list=1,
)
return terms

View File

@ -18,6 +18,7 @@ from erpnext.controllers.accounts_controller import (
validate_taxes_and_charges, validate_taxes_and_charges,
) )
from erpnext.stock.get_item_details import _get_item_tax_template from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.utilities.regional import temporary_flag
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
@ -942,7 +943,6 @@ class calculate_taxes_and_totals(object):
def get_itemised_tax_breakup_html(doc): def get_itemised_tax_breakup_html(doc):
if not doc.taxes: if not doc.taxes:
return return
frappe.flags.company = doc.company
# get headers # get headers
tax_accounts = [] tax_accounts = []
@ -952,22 +952,17 @@ def get_itemised_tax_breakup_html(doc):
if tax.description not in tax_accounts: if tax.description not in tax_accounts:
tax_accounts.append(tax.description) tax_accounts.append(tax.description)
headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts) with temporary_flag("company", doc.company):
headers = get_itemised_tax_breakup_header(doc.doctype + " Item", tax_accounts)
# get tax breakup data itemised_tax_data = get_itemised_tax_breakup_data(doc)
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(doc) get_rounded_tax_amount(itemised_tax_data, doc.precision("tax_amount", "taxes"))
update_itemised_tax_data(doc)
get_rounded_tax_amount(itemised_tax, doc.precision("tax_amount", "taxes"))
update_itemised_tax_data(doc)
frappe.flags.company = None
return frappe.render_template( return frappe.render_template(
"templates/includes/itemised_tax_breakup.html", "templates/includes/itemised_tax_breakup.html",
dict( dict(
headers=headers, headers=headers,
itemised_tax=itemised_tax, itemised_tax_data=itemised_tax_data,
itemised_taxable_amount=itemised_taxable_amount,
tax_accounts=tax_accounts, tax_accounts=tax_accounts,
doc=doc, doc=doc,
), ),
@ -977,10 +972,8 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist() @frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list): def get_round_off_applicable_accounts(company, account_list):
# required to set correct region # required to set correct region
frappe.flags.company = company with temporary_flag("company", company):
account_list = get_regional_round_off_accounts(company, account_list) return get_regional_round_off_accounts(company, account_list)
return account_list
@erpnext.allow_regional @erpnext.allow_regional
@ -1005,7 +998,15 @@ def get_itemised_tax_breakup_data(doc):
itemised_taxable_amount = get_itemised_taxable_amount(doc.items) itemised_taxable_amount = get_itemised_taxable_amount(doc.items)
return itemised_tax, itemised_taxable_amount itemised_tax_data = []
for item_code, taxes in itemised_tax.items():
itemised_tax_data.append(
frappe._dict(
{"item": item_code, "taxable_amount": itemised_taxable_amount.get(item_code), **taxes}
)
)
return itemised_tax_data
def get_itemised_tax(taxes, with_tax_account=False): def get_itemised_tax(taxes, with_tax_account=False):
@ -1050,9 +1051,10 @@ def get_itemised_taxable_amount(items):
def get_rounded_tax_amount(itemised_tax, precision): def get_rounded_tax_amount(itemised_tax, precision):
# Rounding based on tax_amount precision # Rounding based on tax_amount precision
for taxes in itemised_tax.values(): for taxes in itemised_tax:
for tax_account in taxes: for row in taxes.values():
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) if isinstance(row, dict) and isinstance(row["tax_amount"], float):
row["tax_amount"] = flt(row["tax_amount"], precision)
class init_landed_taxes_and_totals(object): class init_landed_taxes_and_totals(object):

View File

@ -54,6 +54,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
} }
add_lead_to_prospect () { add_lead_to_prospect () {
let me = this;
frappe.prompt([ frappe.prompt([
{ {
fieldname: 'prospect', fieldname: 'prospect',
@ -67,12 +68,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
frappe.call({ frappe.call({
method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect', method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect',
args: { args: {
'lead': cur_frm.doc.name, 'lead': me.frm.doc.name,
'prospect': data.prospect 'prospect': data.prospect
}, },
callback: function(r) { callback: function(r) {
if (!r.exc) { if (!r.exc) {
frm.reload_doc(); me.frm.reload_doc();
} }
}, },
freeze: true, freeze: true,

View File

@ -1,10 +1,10 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
{% include 'erpnext/selling/sales_common.js' %}
frappe.provide("erpnext.crm"); frappe.provide("erpnext.crm");
erpnext.pre_sales.set_as_lost("Quotation");
erpnext.sales_common.setup_selling_controller();
cur_frm.email_field = "contact_email";
frappe.ui.form.on("Opportunity", { frappe.ui.form.on("Opportunity", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
@ -19,6 +19,8 @@ frappe.ui.form.on("Opportunity", {
} }
} }
}); });
frm.email_field = "contact_email";
}, },
validate: function(frm) { validate: function(frm) {
@ -46,10 +48,6 @@ frappe.ui.form.on("Opportunity", {
} }
}, },
onload_post_render: function(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
status:function(frm){ status:function(frm){
if (frm.doc.status == "Lost"){ if (frm.doc.status == "Lost"){
frm.trigger('set_as_lost_dialog'); frm.trigger('set_as_lost_dialog');
@ -252,13 +250,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
onload() { onload() {
if(!this.frm.doc.status) { if(!this.frm.doc.status) {
frm.set_value('status', 'Open'); this.frm.set_value('status', 'Open');
} }
if(!this.frm.doc.company && frappe.defaults.get_user_default("Company")) { if(!this.frm.doc.company && frappe.defaults.get_user_default("Company")) {
frm.set_value('company', frappe.defaults.get_user_default("Company")); this.frm.set_value('company', frappe.defaults.get_user_default("Company"));
} }
if(!this.frm.doc.currency) { if(!this.frm.doc.currency) {
frm.set_value('currency', frappe.defaults.get_user_default("Currency")); this.frm.set_value('currency', frappe.defaults.get_user_default("Currency"));
} }
this.setup_queries(); this.setup_queries();

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["First Response Time for Opportunity"] = { frappe.query_reports["First Response Time for Opportunity"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lead Conversion Time"] = { frappe.query_reports["Lead Conversion Time"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lead Details"] = { frappe.query_reports["Lead Details"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lost Opportunity"] = { frappe.query_reports["Lost Opportunity"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Opportunity Summary by Sales Stage"] = { frappe.query_reports["Opportunity Summary by Sales Stage"] = {
"filters": [ "filters": [

View File

@ -1,6 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Sales Pipeline Analytics"] = { frappe.query_reports["Sales Pipeline Analytics"] = {
"filters": [ "filters": [

View File

@ -96,7 +96,7 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
onScriptLoaded(me) { onScriptLoaded(me) {
me.linkHandler = Plaid.create({ me.linkHandler = Plaid.create({ // eslint-disable-line no-undef
clientName: me.client_name, clientName: me.client_name,
product: me.product, product: me.product,
env: me.plaid_env, env: me.plaid_env,

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