Merge branch 'develop' into payment-terms
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index 76e66d0..e27eaab 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -80,6 +80,36 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Due Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
@@ -718,7 +748,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-08-03 12:40:09.611951",
+ "modified": "2017-08-10 18:06:44.904081",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 9047a4e..4ce6886 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -185,8 +185,39 @@
})
},
+ due_date_options_cache: {},
+
reference_name: function(doc, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn);
+ var me = this;
+
+ const get_invoice_due_dates = invoice_name => {
+ const options = this.due_date_options_cache[invoice_name];
+ const input = $(cur_frm.fields_dict["accounts"].wrapper).find("select[data-fieldname=reference_due_date]");
+
+ if (options) {
+ input.empty();
+ input.add_options(options);
+ frappe.model.set_value(cdt, cdn, "reference_due_date", options[0]);
+ }
+ else {
+ frappe.call({
+ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_invoice_due_dates",
+ args: {name: invoice_name},
+ callback: function(r) {
+ const options = [];
+ $.each(r.message, function(key, value) {
+ options.push(value.due_date);
+ });
+ input.empty();
+ input.add_options(options);
+ frappe.model.set_value(cdt, cdn, "reference_due_date", options[0]);
+ me.due_date_options_cache[d.reference_name] = options;
+ }
+ });
+ }
+ }
+
if(d.reference_name) {
if (d.reference_type==="Purchase Invoice" && !flt(d.debit)) {
this.get_outstanding('Purchase Invoice', d.reference_name, doc.company, d);
@@ -197,6 +228,9 @@
if (d.reference_type==="Journal Entry" && !flt(d.credit) && !flt(d.debit)) {
this.get_outstanding('Journal Entry', d.reference_name, doc.company, d);
}
+ if( in_list(["Sales Invoice", "Purchase Invoice"]), d.reference_type) {
+ get_invoice_due_dates(d.reference_name);
+ }
}
},
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 375d85d..f7e4023 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -436,7 +436,8 @@
"against_voucher": d.reference_name,
"remarks": self.remark,
"cost_center": d.cost_center,
- "project": d.project
+ "project": d.project,
+ "due_date": d.reference_due_date
})
)
@@ -889,3 +890,14 @@
exchange_rate = bank_balance_in_company_currency / bank_balance_in_account_currency
return exchange_rate
+
+
+@frappe.whitelist()
+def get_invoice_due_dates(name):
+ result = frappe.get_list(
+ doctype='GL Entry', group_by='name, due_date',
+ filters={'voucher_no': name, "ifnull(due_date, '')": ('!=', '')},
+ fields=['due_date'], distinct=True
+ )
+
+ return result
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 54af579..48d5ed2 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -1,5 +1,6 @@
{
"allow_copy": 0,
+ "allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
@@ -13,6 +14,7 @@
"engine": "InnoDB",
"fields": [
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
@@ -46,6 +48,7 @@
"width": "250px"
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -75,6 +78,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -106,6 +110,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -141,6 +146,7 @@
"width": "180px"
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -168,6 +174,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -197,6 +204,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -226,6 +234,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -256,6 +265,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -287,6 +297,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -317,6 +328,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -345,6 +357,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -374,6 +387,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -402,6 +416,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
@@ -432,6 +447,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
@@ -464,6 +480,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -491,6 +508,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
@@ -521,6 +539,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
@@ -553,6 +572,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -581,6 +601,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -611,6 +632,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -641,6 +663,39 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan'])",
+ "fieldname": "reference_due_date",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Reference Due Date",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -671,6 +726,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -698,6 +754,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -729,6 +786,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -759,17 +817,17 @@
"unique": 0
}
],
+ "has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
- "in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2017-03-02 05:02:10.102039",
+ "modified": "2017-08-30 08:44:54.295493",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 04db9e2..0177b85 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -252,7 +252,6 @@
date: frm.doc.posting_date
},
callback: function(r, rt) {
- console.log(r, rt);
if(r.message) {
if(frm.doc.payment_type == "Receive") {
frm.set_value("paid_from", r.message.party_account);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 31869fb..406dfbc 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -290,7 +290,7 @@
def set_difference_amount(self):
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
- if self.payment_type=="Receive" else flt(self.target_exchange_rate))
+ if self.payment_type == "Receive" else flt(self.target_exchange_rate))
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
@@ -408,7 +408,8 @@
gle = party_gl_dict.copy()
gle.update({
"against_voucher_type": d.reference_doctype,
- "against_voucher": d.reference_name
+ "against_voucher": d.reference_name,
+ "due_date": d.due_date
})
allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),
@@ -761,19 +762,50 @@
pe.received_amount = received_amount
pe.allocate_payment_amount = 1
pe.letter_head = doc.get("letter_head")
+ args = {
+ 'party_account': party_account, 'company': pe.company, 'party_type': pe.party_type,
+ 'party': pe.party, 'posting_date': pe.posting_date
+ }
+ references = get_outstanding_reference_documents(args=args)
- pe.append("references", {
- "reference_doctype": dt,
- "reference_name": dn,
- "due_date": doc.get("due_date"),
- "total_amount": grand_total,
- "outstanding_amount": outstanding_amount,
- "allocated_amount": outstanding_amount
- })
+ for reference in references:
+ if reference.voucher_no == dn:
+ allocated_amount = min(paid_amount, reference.outstanding_amount)
+ pe.append("references", {
+ 'reference_doctype': reference.voucher_type,
+ 'reference_name': reference.voucher_no,
+ 'due_date': reference.due_date,
+ 'total_amount': reference.invoice_amount,
+ 'outstanding_amount': reference.outstanding_amount,
+ 'allocated_amount': allocated_amount
+ })
+ if paid_amount:
+ paid_amount -= allocated_amount
pe.setup_party_account_field()
pe.set_missing_values()
if party_account and bank:
pe.set_exchange_rate()
pe.set_amounts()
- return pe
\ No newline at end of file
+ return pe
+
+
+def get_paid_amount(dt, dn, party_type, party, account, due_date):
+ if party_type=="Customer":
+ dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
+ else:
+ dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
+
+ paid_amount = frappe.db.sql("""
+ select ifnull(sum({dr_or_cr}), 0) as paid_amount
+ from `tabGL Entry`
+ where against_voucher_type = %s
+ and against_voucher = %s
+ and party_type = %s
+ and party = %s
+ and account = %s
+ and due_date = %s
+ and {dr_or_cr} > 0
+ """.format(dr_or_cr=dr_or_cr), (dt, dn, party_type, party, account, due_date))
+
+ return paid_amount[0][0] if paid_amount else 0
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 60be20d..3264bbb 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -108,7 +108,7 @@
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_against_si_usd_to_inr(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
+ si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
pe = get_payment_entry("Sales Invoice", si.name, party_amount=20,
bank_account="_Test Bank - _TC", bank_amount=900)
@@ -212,7 +212,7 @@
self.assertRaises(InvalidPaymentEntry, pe1.validate)
- si1 = create_sales_invoice()
+ si1 = create_sales_invoice()
# create full payment entry against si1
pe2 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC")
diff --git a/erpnext/accounts/doctype/payment_schedule/__init__.py b/erpnext/accounts/doctype/payment_schedule/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_schedule/__init__.py
diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
new file mode 100644
index 0000000..45a81b0
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
@@ -0,0 +1,197 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "autoname": "",
+ "beta": 0,
+ "creation": "2017-08-10 15:38:00.080575",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Payment Term",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Term",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Description",
+ "length": 0,
+ "no_copy": 0,
+ "options": "payment_term.description",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "due_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Due Date",
+ "length": 0,
+ "no_copy": 0,
+ "options": "payment_term.due_date",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Percent",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Invoice Portion",
+ "length": 0,
+ "no_copy": 0,
+ "options": "payment_term.invoice_portion",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "payment_amount",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Payment Amount",
+ "length": 0,
+ "no_copy": 0,
+ "options": "currency",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2017-08-10 18:09:42.122027",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Schedule",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.py b/erpnext/accounts/doctype/payment_schedule/payment_schedule.py
new file mode 100644
index 0000000..4174017
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+
+class PaymentSchedule(Document):
+ pass
diff --git a/erpnext/accounts/doctype/payment_term/__init__.py b/erpnext/accounts/doctype/payment_term/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/__init__.py
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js
new file mode 100644
index 0000000..054c2d1
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/payment_term.js
@@ -0,0 +1,2 @@
+// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.json b/erpnext/accounts/doctype/payment_term/payment_term.json
new file mode 100644
index 0000000..702319b
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/payment_term.json
@@ -0,0 +1,344 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "autoname": "field:payment_term_name",
+ "beta": 0,
+ "creation": "2017-08-10 15:24:54.876365",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 1,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_term_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Term Name",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 1,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Float",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Invoice Portion",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 1,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "due_date_based_on",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Due Date Based On",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 1,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
+ "fieldname": "credit_days",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Credit Days",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
+ "fieldname": "credit_months",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Credit Months",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 1,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Description",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2017-08-10 16:26:03.581501",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Term",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.py b/erpnext/accounts/doctype/payment_term/payment_term.py
new file mode 100644
index 0000000..5d4df05
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/payment_term.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+
+class PaymentTerm(Document):
+ pass
diff --git a/erpnext/accounts/doctype/payment_term/test_payment_term.js b/erpnext/accounts/doctype/payment_term/test_payment_term.js
new file mode 100644
index 0000000..b26e42a
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/test_payment_term.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Payment Term", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Payment Term
+ () => frappe.tests.make('Payment Term', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/payment_term/test_payment_term.py b/erpnext/accounts/doctype/payment_term/test_payment_term.py
new file mode 100644
index 0000000..d9baa59
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/test_payment_term.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import unittest
+
+
+class TestPaymentTerm(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/payment_term/test_records.json b/erpnext/accounts/doctype/payment_term/test_records.json
new file mode 100644
index 0000000..ef6e069
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_term/test_records.json
@@ -0,0 +1,34 @@
+[
+ {
+ "doctype":"Payment Term",
+ "due_date_based_on":"Day(s) after invoice date",
+ "payment_term_name":"_Test N30",
+ "description":"_Test Net 30 Days",
+ "invoice_portion":50,
+ "credit_days":30
+ },
+ {
+ "doctype":"Payment Term",
+ "due_date_based_on":"Day(s) after invoice date",
+ "payment_term_name":"_Test COD",
+ "description":"_Test Cash on Delivery",
+ "invoice_portion":50,
+ "credit_days":0
+ },
+ {
+ "doctype":"Payment Term",
+ "due_date_based_on":"Month(s) after the end of the invoice month",
+ "payment_term_name":"_Test EONM",
+ "description":"_Test End of Next Month",
+ "invoice_portion":100,
+ "credit_months":1
+ },
+ {
+ "doctype":"Payment Term",
+ "due_date_based_on":"Day(s) after invoice date",
+ "payment_term_name":"_Test N30 1",
+ "description":"_Test Net 30 Days",
+ "invoice_portion":100,
+ "credit_days":30
+ }
+]
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template/__init__.py b/erpnext/accounts/doctype/payment_terms_template/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/__init__.py
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
new file mode 100644
index 0000000..558297f
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
@@ -0,0 +1,12 @@
+// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Payment Terms Template', {
+ setup: function(frm) {
+ frm.add_fetch("payment_term", "description", "description");
+ frm.add_fetch("payment_term", "invoice_portion", "invoice_portion");
+ frm.add_fetch("payment_term", "due_date_based_on", "due_date_based_on");
+ frm.add_fetch("payment_term", "credit_days", "credit_days");
+ frm.add_fetch("payment_term", "credit_months", "credit_months");
+ }
+});
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json
new file mode 100644
index 0000000..0959658
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json
@@ -0,0 +1,164 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "autoname": "field:template_name",
+ "beta": 0,
+ "creation": "2017-08-10 15:34:28.058054",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "template_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Template Name",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "terms",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template Detail",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2017-08-10 15:46:33.877884",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Terms Template",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
new file mode 100644
index 0000000..7939d8d
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe.model.document import Document
+from frappe.utils import flt
+from frappe import _
+
+
+class PaymentTermsTemplate(Document):
+ def validate(self):
+ self.validate_invoice_portion()
+ self.validate_credit_days()
+ self.check_duplicate_terms()
+
+ def validate_invoice_portion(self):
+ total_portion = 0
+ for term in self.terms:
+ total_portion += term.invoice_portion
+
+ if flt(total_portion, 2) != 100.00:
+ frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
+
+ def validate_credit_days(self):
+ for term in self.terms:
+ if term.credit_days < 0:
+ frappe.msgprint(_('Credit Days cannot be a negative number'), raise_exception=1, indicator='red')
+
+ def check_duplicate_terms(self):
+ terms = []
+ for term in self.terms:
+ term_info = (term.credit_days, term.due_date_based_on)
+ if term_info in terms:
+ frappe.msgprint(
+ _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),
+ raise_exception=1, indicator='red'
+ )
+ else:
+ terms.append(term_info)
diff --git a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.js
new file mode 100644
index 0000000..494a0ed
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Payment Terms Template", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Payment Terms Template
+ () => frappe.tests.make('Payment Terms Template', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py
new file mode 100644
index 0000000..6daaf1e
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import unittest
+
+import frappe
+
+
+class TestPaymentTermsTemplate(unittest.TestCase):
+ def tearDown(self):
+ frappe.delete_doc('Payment Terms Template', '_Test Payment Terms Template For Test', force=1)
+
+ def test_create_template(self):
+ template = frappe.get_doc({
+ 'doctype': 'Payment Terms Template',
+ 'template_name': '_Test Payment Terms Template For Test',
+ 'terms': [{
+ 'doctype': 'Payment Terms Template Detail',
+ 'invoice_portion': 50.00,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 30
+ }]
+ })
+
+ self.assertRaises(frappe.ValidationError, template.insert)
+
+ template.append('terms', {
+ 'doctype': 'Payment Terms Template Detail',
+ 'invoice_portion': 50.00,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 0
+ })
+
+ template.insert()
+
+ def test_credit_days(self):
+ template = frappe.get_doc({
+ 'doctype': 'Payment Terms Template',
+ 'template_name': '_Test Payment Terms Template For Test',
+ 'terms': [{
+ 'doctype': 'Payment Terms Template Detail',
+ 'invoice_portion': 100.00,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': -30
+ }]
+ })
+
+ self.assertRaises(frappe.ValidationError, template.insert)
+
+ def test_duplicate_terms(self):
+ template = frappe.get_doc({
+ 'doctype': 'Payment Terms Template',
+ 'template_name': '_Test Payment Terms Template For Test',
+ 'terms': [
+ {
+ 'doctype': 'Payment Terms Template Detail',
+ 'invoice_portion': 50.00,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 30
+ },
+ {
+ 'doctype': 'Payment Terms Template Detail',
+ 'invoice_portion': 50.00,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 30
+ }
+
+ ]
+ })
+
+ self.assertRaises(frappe.ValidationError, template.insert)
diff --git a/erpnext/accounts/doctype/payment_terms_template/test_records.json b/erpnext/accounts/doctype/payment_terms_template/test_records.json
new file mode 100644
index 0000000..fea0b35
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template/test_records.json
@@ -0,0 +1,60 @@
+[
+ {
+ "doctype":"Payment Terms Template",
+ "terms":[
+ {
+ "doctype":"Payment Terms Template Detail",
+ "due_date_based_on":"Day(s) after invoice date",
+ "idx":1,
+ "description":"Cash on Delivery",
+ "invoice_portion":50,
+ "credit_days":0,
+ "credit_months":0,
+ "payment_term":"_Test COD"
+ },
+ {
+ "doctype":"Payment Terms Template Detail",
+ "due_date_based_on":"Day(s) after invoice date",
+ "idx":2,
+ "description":"Net 30 Days ",
+ "invoice_portion":50,
+ "credit_days":30,
+ "credit_months":0,
+ "payment_term":"_Test N30"
+ }
+ ],
+ "template_name":"_Test Payment Term Template"
+ },
+ {
+ "doctype":"Payment Terms Template",
+ "terms":[
+ {
+ "doctype":"Payment Terms Template Detail",
+ "due_date_based_on":"Month(s) after the end of the invoice month",
+ "idx":1,
+ "description":"_Test End of Next Months",
+ "invoice_portion":100,
+ "credit_days":0,
+ "credit_months":1,
+ "payment_term":"_Test EONM"
+ }
+ ],
+ "template_name":"_Test Payment Term Template 1"
+ },
+ {
+ "doctype":"Payment Terms Template",
+ "terms":[
+ {
+ "doctype":"Payment Terms Template Detail",
+ "due_date_based_on":"Day(s) after invoice date",
+ "idx":1,
+ "description":"_Test Net Within 30 days",
+ "invoice_portion":100,
+ "credit_days":30,
+ "credit_months":0,
+ "payment_term":"_Test N30 1"
+ }
+ ],
+ "template_name":"_Test Payment Term Template 3"
+ }
+]
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/__init__.py b/erpnext/accounts/doctype/payment_terms_template_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template_detail/__init__.py
diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
new file mode 100644
index 0000000..2d1f6a0
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
@@ -0,0 +1,232 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "autoname": "PTTD.#####",
+ "beta": 0,
+ "creation": "2017-08-10 15:34:09.409562",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Payment Term",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Term",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Description",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "default": "0",
+ "fieldname": "invoice_portion",
+ "fieldtype": "Percent",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Invoice Portion",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "fieldname": "due_date_based_on",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Due Date Based On",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 2,
+ "default": "0",
+ "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
+ "fieldname": "credit_days",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Credit Days",
+ "length": 0,
+ "no_copy": 0,
+ "options": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
+ "fieldname": "credit_months",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Credit Months",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2017-09-22 11:15:02.585019",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Terms Template Detail",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.py b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.py
new file mode 100644
index 0000000..54c0fda
--- /dev/null
+++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from frappe.model.document import Document
+
+
+class PaymentTermsTemplateDetail(Document):
+ pass
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 82a3d65..3fa34e2 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -252,9 +252,11 @@
self.assertEquals(so.items[0].rate, 100)
def test_pricing_rule_with_margin_and_discount(self):
+ frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10)
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
+ si.payment_schedule = []
si.insert(ignore_permissions=True)
item = si.items[0]
@@ -263,6 +265,7 @@
# With discount
item.discount_percentage = 10
+ si.payment_schedule = []
si.save()
item = si.items[0]
self.assertEquals(item.rate, 990)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index b9a7dae..e81dea2 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -377,5 +377,5 @@
erpnext.buying.get_default_bom(frm);
}
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes");
- },
+ }
})
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index e3c5fb4..163fb52 100755
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -2793,6 +2793,98 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
+ "columns": 0,
+ "fieldname": "payment_schedule_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Schedule",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_schedule",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 1,
+ "options": "Payment Schedule",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
"collapsible_depends_on": "terms",
"columns": 0,
"fieldname": "terms_section_break",
@@ -3920,8 +4012,8 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2017-09-19 11:22:47.074420",
- "modified_by": "Administrator",
+ "modified": "2017-09-19 11:22:47.074420",
+ "modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
"name_case": "Title Case",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index a46c4b9..a564d92 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -93,7 +93,7 @@
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
if not self.due_date:
- self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company)
+ self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier)
super(PurchaseInvoice, self).set_missing_values(for_validate)
@@ -359,7 +359,27 @@
return gl_entries
def make_supplier_gl_entry(self, gl_entries):
- if self.grand_total:
+ if self.get("payment_schedule"):
+ for d in self.get("payment_schedule"):
+ payment_amount_in_company_currency = flt(d.payment_amount * self.conversion_rate,
+ d.precision("payment_amount"))
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.credit_to,
+ "party_type": "Supplier",
+ "party": self.supplier,
+ "due_date": d.due_date,
+ "against": self.against_expense_account,
+ "credit": payment_amount_in_company_currency,
+ "credit_in_account_currency": payment_amount_in_company_currency \
+ if self.party_account_currency==self.company_currency else d.payment_amount,
+ "against_voucher": self.return_against if cint(self.is_return) else self.name,
+ "against_voucher_type": self.doctype
+ }, self.party_account_currency)
+ )
+
+ elif self.grand_total:
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(self.grand_total * self.conversion_rate,
self.precision("grand_total"))
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 639620f..3907029 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -6,15 +6,16 @@
import unittest
import frappe, erpnext
import frappe.model
-from frappe.utils import cint, flt, today, nowdate
+from frappe.utils import cint, flt, today, nowdate, getdate, add_days
import frappe.defaults
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
test_records as pr_test_records
+from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
from erpnext.accounts.doctype.account.test_account import get_inventory_account
-test_dependencies = ["Item", "Cost Center"]
+test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
test_ignore = ["Serial No"]
class TestPurchaseInvoice(unittest.TestCase):
@@ -61,6 +62,12 @@
set_perpetual_inventory(0, pi.company)
+ def test_terms_added_after_save(self):
+ pi = frappe.copy_doc(test_records[1])
+ pi.insert()
+ self.assertTrue(pi.payment_schedule)
+ self.assertEqual(pi.payment_schedule[0].due_date, pi.due_date)
+
def test_payment_entry_unlink_against_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
unlink_payment_on_cancel_of_invoice(0)
@@ -256,6 +263,55 @@
self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account`
where reference_type='Purchase Invoice' and reference_name=%s""", pi.name))
+ def test_invoice_with_advance_and_multi_payment_terms(self):
+ from erpnext.accounts.doctype.journal_entry.test_journal_entry \
+ import test_records as jv_test_records
+
+ jv = frappe.copy_doc(jv_test_records[1])
+ jv.insert()
+ jv.submit()
+
+ pi = frappe.copy_doc(test_records[0])
+
+ pi.append("advances", {
+ "reference_type": "Journal Entry",
+ "reference_name": jv.name,
+ "reference_row": jv.get("accounts")[0].name,
+ "advance_amount": 400,
+ "allocated_amount": 300,
+ "remarks": jv.remark
+ })
+ pi.insert()
+
+ pi.update(
+ {"payment_schedule": get_payment_terms("_Test Payment Term Template", pi.posting_date, pi.grand_total)}
+ )
+
+ pi.save()
+ pi.submit()
+ self.assertEqual(pi.payment_schedule[0].payment_amount, 756.15)
+ self.assertEqual(pi.payment_schedule[0].due_date, pi.posting_date)
+ self.assertEqual(pi.payment_schedule[1].payment_amount, 756.15)
+ self.assertEqual(pi.payment_schedule[1].due_date, add_days(pi.posting_date, 30))
+
+ pi.load_from_db()
+
+ self.assertTrue(
+ frappe.db.sql(
+ "select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and "
+ "reference_name=%s and debit_in_account_currency=300", pi.name)
+ )
+
+ self.assertEqual(pi.outstanding_amount, 1212.30)
+
+ pi.cancel()
+
+ self.assertFalse(
+ frappe.db.sql(
+ "select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and "
+ "reference_name=%s", pi.name)
+ )
+
def test_total_purchase_cost_for_project(self):
existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""")
@@ -547,6 +603,56 @@
#check outstanding after advance cancellation
self.assertEqual(flt(pi.outstanding_amount), flt(pi.grand_total + pi.total_advance))
+ def test_gl_entry_based_on_payment_schedule(self):
+ pi = make_purchase_invoice(do_not_save=True, supplier="_Test Supplier P")
+ pi.append("payment_schedule", {
+ "due_date": add_days(nowdate(), 15),
+ "payment_amount": 100,
+ "invoice_portion": 40.00
+ })
+ pi.append("payment_schedule", {
+ "due_date": add_days(nowdate(), 45),
+ "payment_amount": 150,
+ "invoice_portion": 60.00
+ })
+
+ pi.save()
+ pi.submit()
+
+ gl_entries = frappe.db.sql("""select account, debit, credit, due_date
+ from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s
+ order by account asc, debit asc""", pi.name, as_dict=1)
+ self.assertTrue(gl_entries)
+
+ expected_gl_entries = sorted([
+ [pi.credit_to, 0.0, 100.0, add_days(nowdate(), 15)],
+ [pi.credit_to, 0.0, 150.0, add_days(nowdate(), 45)],
+ ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, None]
+ ])
+
+ for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)):
+ self.assertEquals(expected_gl_entries[i][0], gle.account)
+ self.assertEquals(expected_gl_entries[i][1], gle.debit)
+ self.assertEquals(expected_gl_entries[i][2], gle.credit)
+ self.assertEquals(getdate(expected_gl_entries[i][3]), getdate(gle.due_date))
+
+ def test_make_pi_without_terms(self):
+ pi = make_purchase_invoice(do_not_save=1)
+
+ self.assertFalse(pi.get('payment_schedule'))
+
+ pi.insert()
+
+ self.assertTrue(pi.get('payment_schedule'))
+
+ def test_duplicate_due_date_in_terms(self):
+ pi = make_purchase_invoice(do_not_save=1)
+ pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
+ pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
+
+ self.assertRaises(frappe.ValidationError, pi.insert)
+
+
def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.unlink_payment_on_cancellation_of_invoice = enable
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 11d1825..c7258a4 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -568,3 +568,4 @@
refresh_field('total_billing_amount')
}
+
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 9aa0e6b..9dbb06d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2751,6 +2751,101 @@
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
+ "columns": 0,
+ "fieldname": "payment_schedule_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:(!doc.is_pos && !doc.is_return)",
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:(!doc.is_pos && !doc.is_return)",
+ "fieldname": "payment_schedule",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 1,
+ "options": "Payment Schedule",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
@@ -3001,6 +3096,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "is_pos",
"fieldname": "base_change_amount",
"fieldtype": "Currency",
"hidden": 0,
@@ -3061,6 +3157,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "is_pos",
"fieldname": "change_amount",
"fieldtype": "Currency",
"hidden": 0,
@@ -3092,6 +3189,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "is_pos",
"fieldname": "account_for_change_amount",
"fieldtype": "Link",
"hidden": 0,
@@ -4810,9 +4908,9 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "menu_index": 0,
+ "menu_index": 0,
"modified": "2017-09-19 11:23:08.675028",
- "modified_by": "Administrator",
+ "modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
"name_case": "Title Case",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 7a787c4..678988d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -242,7 +242,7 @@
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
if not self.due_date and self.customer:
- self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
+ self.due_date = get_due_date(self.posting_date, "Customer", self.customer)
super(SalesInvoice, self).set_missing_values(for_validate)
@@ -631,7 +631,27 @@
return gl_entries
def make_customer_gl_entry(self, gl_entries):
- if self.grand_total:
+ if self.get("payment_schedule"):
+ for d in self.get("payment_schedule"):
+ payment_amount_in_company_currency = flt(d.payment_amount * self.conversion_rate,
+ d.precision("payment_amount"))
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "due_date": d.due_date,
+ "against": self.against_income_account,
+ "debit": payment_amount_in_company_currency,
+ "debit_in_account_currency": payment_amount_in_company_currency \
+ if self.party_account_currency==self.company_currency else d.payment_amount,
+ "against_voucher": self.return_against if cint(self.is_return) else self.name,
+ "against_voucher_type": self.doctype
+ }, self.party_account_currency)
+ )
+
+ elif self.grand_total:
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(self.grand_total * self.conversion_rate,
self.precision("grand_total"))
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
index 35b2558..f7a4488 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
@@ -40,4 +40,3 @@
() => done()
]);
});
-
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 4dae78c..4d0fa90 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -4,7 +4,7 @@
import frappe
import unittest, copy
-from frappe.utils import nowdate, add_days, flt
+from frappe.utils import nowdate, add_days, flt, getdate
from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
@@ -58,6 +58,13 @@
self.assertRaises(frappe.CannotChangeConstantError, si.save)
+ def test_add_terms_after_save(self):
+ si = frappe.copy_doc(test_records[2])
+ si.insert()
+
+ self.assertTrue(si.payment_schedule)
+ self.assertEqual(si.payment_schedule[0].due_date, si.due_date)
+
def test_sales_invoice_calculation_base_currency(self):
si = frappe.copy_doc(test_records[2])
si.insert()
@@ -199,6 +206,7 @@
# additional discount
si.discount_amount = 100
si.apply_discount_on = 'Net Total'
+ si.payment_schedule = []
si.save()
@@ -211,6 +219,7 @@
# additional discount on grand total
si.discount_amount = 100
si.apply_discount_on = 'Grand Total'
+ si.payment_schedule = []
si.save()
@@ -891,20 +900,6 @@
self.assertEquals(si.get("items")[0].serial_no, dn.get("items")[0].serial_no)
- def test_invoice_due_date_against_customers_credit_days(self):
- # set customer's credit days
- frappe.db.set_value("Customer", "_Test Customer", "credit_days_based_on", "Fixed Days")
- frappe.db.set_value("Customer", "_Test Customer", "credit_days", 10)
-
- si = create_sales_invoice()
- self.assertEqual(si.due_date, add_days(nowdate(), 10))
-
- # set customer's credit days is last day of the next month
- frappe.db.set_value("Customer", "_Test Customer", "credit_days_based_on", "Last Day of the Next Month")
-
- si1 = create_sales_invoice(posting_date="2015-07-05")
- self.assertEqual(si1.due_date, "2015-08-31")
-
def test_return_sales_invoice(self):
set_perpetual_inventory()
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
@@ -1279,6 +1274,40 @@
})
si.insert()
return si
+
+ def test_gl_entry_based_on_payment_schedule(self):
+ si = create_sales_invoice(do_not_save=True, customer="_Test Customer P")
+ si.append("payment_schedule", {
+ "due_date": add_days(nowdate(), 15),
+ "payment_amount": 20,
+ "invoice_portion": 20.00
+ })
+ si.append("payment_schedule", {
+ "due_date": add_days(nowdate(), 45),
+ "payment_amount": 80,
+ "invoice_portion": 80.00
+ })
+
+ si.save()
+ si.submit()
+
+ gl_entries = frappe.db.sql("""select account, debit, credit, due_date
+ from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
+ order by account asc, debit asc""", si.name, as_dict=1)
+ self.assertTrue(gl_entries)
+
+ expected_gl_entries = sorted([
+ [si.debit_to, 20.0, 0.0, add_days(nowdate(), 15)],
+ [si.debit_to, 80.0, 0.0, add_days(nowdate(), 45)],
+ ["Sales - _TC", 0.0, 100.0, None]
+ ])
+
+ for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)):
+ self.assertEquals(expected_gl_entries[i][0], gle.account)
+ self.assertEquals(expected_gl_entries[i][1], gle.debit)
+ self.assertEquals(expected_gl_entries[i][2], gle.credit)
+ self.assertEquals(getdate(expected_gl_entries[i][3]), getdate(gle.due_date))
+
def test_company_monthly_sales(self):
existing_current_month_sales = frappe.db.get_value("Company", "_Test Company", "total_monthly_sales")
@@ -1325,6 +1354,21 @@
self.assertEquals(expected_values[gle.account][1], gle.debit)
self.assertEquals(expected_values[gle.account][2], gle.credit)
+ def test_create_invoice_without_terms(self):
+ si = create_sales_invoice(do_not_save=1)
+ self.assertFalse(si.get('payment_schedule'))
+
+ si.insert()
+ self.assertTrue(si.get('payment_schedule'))
+
+ def test_duplicate_due_date_in_terms(self):
+ si = create_sales_invoice(do_not_save=1)
+ si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
+ si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50))
+
+ self.assertRaises(frappe.ValidationError, si.insert)
+
+
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
@@ -1358,6 +1402,11 @@
si.insert()
if not args.do_not_submit:
si.submit()
+ else:
+ si.payment_schedule = []
+ else:
+ si.payment_schedule = []
+
return si
test_dependencies = ["Journal Entry", "Contact", "Address"]
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index c575d59..d370c49 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe, erpnext
-from frappe.utils import flt, cstr, cint
+from frappe.utils import flt, cstr, cint, getdate
from frappe import _
from frappe.model.meta import get_field_precision
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
@@ -75,7 +75,8 @@
and cstr(e.get('against_voucher'))==cstr(gle.get('against_voucher')) \
and cstr(e.get('against_voucher_type')) == cstr(gle.get('against_voucher_type')) \
and cstr(e.get('cost_center')) == cstr(gle.get('cost_center')) \
- and cstr(e.get('project')) == cstr(gle.get('project')):
+ and cstr(e.get('project')) == cstr(gle.get('project')) \
+ and getdate(e.get('due_date')) == getdate(gle.get('due_date')):
return e
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index ba7ae32..a1b737d 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -9,7 +9,7 @@
from frappe.defaults import get_user_permissions
from frappe.model.utils import get_fetch_values
from frappe.utils import (add_days, getdate, formatdate, get_first_day, date_diff,
- add_years, get_timestamp, nowdate, flt)
+ add_years, get_timestamp, nowdate, flt, add_months, get_last_day)
from frappe.contacts.doctype.address.address import (get_address_display,
get_default_address, get_company_address)
from frappe.contacts.doctype.contact.contact import get_contact_details, get_default_contact
@@ -51,6 +51,7 @@
set_other_values(out, party, party_type)
set_price_list(out, party, party_type, price_list)
out["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, out.customer_group, out.supplier_type)
+ out["payment_terms_template"] = get_pyt_term_template(party.name, party_type)
if not out.get("currency"):
out["currency"] = currency
@@ -161,7 +162,7 @@
out = {
party_type.lower(): party,
account_fieldname : account,
- "due_date": get_due_date(posting_date, party_type, party, company)
+ "due_date": get_due_date(posting_date, party_type, party)
}
return out
@@ -258,51 +259,54 @@
if doc.get("default_currency") and party_account_currency and company_default_currency:
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
- frappe.throw(_("Billing currency must be equal to either default comapany's currency or party account currency"))
+ frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
+
@frappe.whitelist()
-def get_due_date(posting_date, party_type, party, company):
- """Set Due Date = Posting Date + Credit Days"""
+def get_due_date(posting_date, party_type, party):
+ """Get due date from `Payment Terms Template`"""
due_date = None
if posting_date and party:
due_date = posting_date
- credit_days_based_on, credit_days = get_credit_days(party_type, party, company)
- if credit_days_based_on == "Fixed Days" and credit_days:
- due_date = add_days(posting_date, credit_days)
- elif credit_days_based_on == "Last Day of the Next Month":
- due_date = (get_first_day(posting_date, 0, 2) + datetime.timedelta(-1)).strftime("%Y-%m-%d")
+ template_name = get_pyt_term_template(party, party_type)
+ if template_name:
+ due_date = get_due_date_from_template(template_name, posting_date).strftime("%Y-%m-%d")
+ else:
+ if party_type == "Supplier":
+ supplier_type = frappe.db.get_value(party_type, party, fieldname="supplier_type")
+ template_name = frappe.db.get_value("Supplier Type", supplier_type, fieldname="payment_terms")
+ if template_name:
+ due_date = get_due_date_from_template(template_name, posting_date).strftime("%Y-%m-%d")
return due_date
-def get_credit_days(party_type, party, company):
- credit_days = 0
- if party_type and party:
- if party_type == "Customer":
- credit_days_based_on, credit_days, customer_group = \
- frappe.db.get_value(party_type, party, ["credit_days_based_on", "credit_days", "customer_group"])
+
+def get_due_date_from_template(template_name, posting_date):
+ """
+ Inspects all `Payment Term`s from the a `Payment Terms Template` and returns the due
+ date after considering all the `Payment Term`s requirements.
+ :param template_name: Name of the `Payment Terms Template`
+ :return: String representing the calculated due date
+ """
+ due_date = getdate(posting_date)
+ template = frappe.get_doc('Payment Terms Template', template_name)
+
+ for term in template.terms:
+ if term.due_date_based_on == 'Day(s) after invoice date':
+ due_date = max(due_date, add_days(due_date, term.credit_days))
+ elif term.due_date_based_on == 'Day(s) after the end of the invoice month':
+ due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days))
else:
- credit_days_based_on, credit_days, supplier_type = \
- frappe.db.get_value(party_type, party, ["credit_days_based_on", "credit_days", "supplier_type"])
+ due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months))
- if not credit_days_based_on:
- if party_type == "Customer" and customer_group:
- credit_days_based_on, credit_days = \
- frappe.db.get_value("Customer Group", customer_group, ["credit_days_based_on", "credit_days"])
- elif party_type == "Supplier" and supplier_type:
- credit_days_based_on, credit_days = \
- frappe.db.get_value("Supplier Type", supplier_type, ["credit_days_based_on", "credit_days"])
+ return due_date
- if not credit_days_based_on:
- credit_days_based_on, credit_days = \
- frappe.db.get_value("Company", company, ["credit_days_based_on", "credit_days"])
- return credit_days_based_on, credit_days
-
-def validate_due_date(posting_date, due_date, party_type, party, company):
+def validate_due_date(posting_date, due_date, party_type, party):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting Date"))
else:
- default_due_date = get_due_date(posting_date, party_type, party, company)
+ default_due_date = get_due_date(posting_date, party_type, party)
if not default_due_date:
return
@@ -345,6 +349,16 @@
return get_tax_template(posting_date, args)
+
+@frappe.whitelist()
+def get_pyt_term_template(party_name, party_type):
+ template = None
+ if party_type in ('Customer', 'Supplier'):
+ template = frappe.db.get_value(party_type, party_name, fieldname='payment_terms')
+
+ return template
+
+
def validate_party_frozen_disabled(party_type, party_name):
if party_type and party_name:
if party_type in ("Customer", "Supplier"):
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 9906893..b72a669 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -113,7 +113,7 @@
row += [self.get_party_name(gle.party_type, gle.party)]
# get due date
- due_date = voucher_details.get(gle.voucher_no, {}).get("due_date", "")
+ due_date = gle.due_date or voucher_details.get(gle.voucher_no, {}).get("due_date", "")
row += [gle.voucher_type, gle.voucher_no, due_date]
@@ -162,8 +162,7 @@
def get_entries_till(self, report_date, party_type):
# returns a generator
- return (e for e in self.get_gl_entries(party_type)
- if getdate(e.posting_date) <= report_date)
+ return (e for e in self.get_gl_entries(party_type) if getdate(e.posting_date) <= report_date)
def is_receivable_or_payable(self, gle, dr_or_cr, future_vouchers):
return (
@@ -189,7 +188,8 @@
reverse_dr_or_cr = "credit" if dr_or_cr=="debit" else "debit"
for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no):
- if getdate(e.posting_date) <= report_date and e.name!=gle.name:
+ if getdate(e.posting_date) <= report_date and e.name!=gle.name \
+ and (not gle.due_date or getdate(e.due_date) == getdate(gle.due_date)):
amount = flt(e.get(reverse_dr_or_cr)) - flt(e.get(dr_or_cr))
if e.voucher_no not in return_entries:
payment_amount += amount
@@ -251,11 +251,11 @@
select_fields = "sum(debit) as debit, sum(credit) as credit"
self.gl_entries = frappe.db.sql("""select name, posting_date, account, party_type, party,
- voucher_type, voucher_no, against_voucher_type, against_voucher,
+ voucher_type, voucher_no, against_voucher_type, against_voucher, due_date,
account_currency, remarks, {0}
from `tabGL Entry`
where docstatus < 2 and party_type=%s and (party is not null and party != '') {1}
- group by voucher_type, voucher_no, against_voucher_type, against_voucher, party
+ group by voucher_type, voucher_no, against_voucher_type, against_voucher, party, due_date
order by posting_date, party"""
.format(select_fields, conditions), values, as_dict=True)
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 50530f5..6d6aced 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -583,7 +583,7 @@
invoice = 'Sales Invoice' if party_type == 'Customer' else 'Purchase Invoice'
invoice_list = frappe.db.sql("""
select
- voucher_no, voucher_type, posting_date, ifnull(sum({dr_or_cr}), 0) as invoice_amount,
+ voucher_no, voucher_type, posting_date, ifnull(sum({dr_or_cr}), 0) as invoice_amount, due_date,
(
case when (voucher_type = 'Sales Invoice' or voucher_type = 'Purchase Invoice')
then (select due_date from `tab{invoice}` where name = voucher_no)
@@ -597,6 +597,7 @@
and payment_gl_entry.party_type = invoice_gl_entry.party_type
and payment_gl_entry.party = invoice_gl_entry.party
and payment_gl_entry.account = invoice_gl_entry.account
+ and payment_gl_entry.due_date = invoice_gl_entry.due_date
and {payment_dr_or_cr} > 0
) as payment_amount
from
@@ -608,9 +609,9 @@
and ((voucher_type = 'Journal Entry'
and (against_voucher = '' or against_voucher is null))
or (voucher_type not in ('Journal Entry', 'Payment Entry')))
- group by voucher_type, voucher_no
+ group by voucher_type, voucher_no, due_date
having (invoice_amount - payment_amount) > 0.005
- order by posting_date, name""".format(
+ order by posting_date, name, due_date""".format(
dr_or_cr = dr_or_cr,
invoice = invoice,
payment_dr_or_cr = payment_dr_or_cr,
@@ -622,6 +623,8 @@
}, as_dict=True)
for d in invoice_list:
+ due_date = d.due_date or (frappe.db.get_value(d.voucher_type, d.voucher_no,
+ "posting_date" if party_type=="Employee" else "due_date"))
outstanding_invoices.append(frappe._dict({
'voucher_no': d.voucher_no,
'voucher_type': d.voucher_type,
@@ -630,8 +633,7 @@
'invoice_amount': flt(d.invoice_amount),
'payment_amount': flt(d.payment_amount),
'outstanding_amount': flt(d.invoice_amount - d.payment_amount, precision),
- 'due_date': frappe.db.get_value(d.voucher_type, d.voucher_no,
- "posting_date" if party_type=="Employee" else "due_date"),
+ 'due_date': due_date
}))
outstanding_invoices = sorted(outstanding_invoices, key=lambda k: k['due_date'] or getdate(nowdate()))
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 919707c..1e55f92 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -2355,6 +2355,98 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
+ "columns": 0,
+ "fieldname": "payment_schedule_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Schedule",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_schedule",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 1,
+ "options": "Payment Schedule",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
"collapsible_depends_on": "terms",
"columns": 0,
"fieldname": "terms_section_break",
@@ -3457,9 +3549,9 @@
"is_submittable": 1,
"issingle": 0,
"istable": 0,
- "max_attachments": 0,
+ "max_attachments": 0,
"modified": "2017-09-19 11:22:30.190589",
- "modified_by": "Administrator",
+ "modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
"owner": "Administrator",
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 17786aa..b672acb 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -8,6 +8,7 @@
from frappe.utils import flt, add_days, nowdate
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_purchase_invoice
+
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
@@ -83,6 +84,33 @@
self.assertEquals(pi.doctype, "Purchase Invoice")
self.assertEquals(len(pi.get("items", [])), 1)
+ def test_make_purchase_invoice_with_terms(self):
+ po = create_purchase_order(do_not_save=True)
+
+ self.assertRaises(frappe.ValidationError, make_purchase_invoice, po.name)
+
+ po.update(
+ {"payment_terms_template": "_Test Payment Term Template"}
+ )
+
+ po.save()
+ po.submit()
+
+ self.assertEqual(po.payment_schedule[0].payment_amount, 2500.0)
+ self.assertEqual(po.payment_schedule[0].due_date, po.transaction_date)
+ self.assertEqual(po.payment_schedule[1].payment_amount, 2500.0)
+ self.assertEqual(po.payment_schedule[1].due_date, add_days(po.transaction_date, 30))
+ pi = make_purchase_invoice(po.name)
+ pi.save()
+
+ self.assertEquals(pi.doctype, "Purchase Invoice")
+ self.assertEquals(len(pi.get("items", [])), 1)
+
+ self.assertEqual(pi.payment_schedule[0].payment_amount, 2500.0)
+ self.assertEqual(pi.payment_schedule[0].due_date, po.transaction_date)
+ self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
+ self.assertEqual(pi.payment_schedule[1].due_date, add_days(po.transaction_date, 30))
+
def test_subcontracting(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
self.assertEquals(len(po.get("supplied_items")), 2)
@@ -124,6 +152,35 @@
"group_same_items": 1
}).insert(ignore_permissions=True)
+ def test_make_po_without_terms(self):
+ po = create_purchase_order(do_not_save=1)
+
+ self.assertFalse(po.get('payment_schedule'))
+
+ po.insert()
+
+ self.assertTrue(po.get('payment_schedule'))
+
+ def test_terms_does_not_copy(self):
+ po = create_purchase_order()
+
+ self.assertTrue(po.get('payment_schedule'))
+
+ pi = make_purchase_invoice(po.name)
+
+ self.assertFalse(pi.get('payment_schedule'))
+
+ def test_terms_copied(self):
+ po = create_purchase_order(do_not_save=1)
+ po.payment_terms_template = '_Test Payment Term Template'
+ po.insert()
+ po.submit()
+ self.assertTrue(po.get('payment_schedule'))
+
+ pi = make_purchase_invoice(po.name)
+ pi.insert()
+ self.assertTrue(pi.get('payment_schedule'))
+
def get_same_items():
return [
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 711e05d..e6cea53 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -597,8 +597,9 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "fieldname": "credit_days_based_on",
- "fieldtype": "Select",
+ "depends_on": "",
+ "fieldname": "payment_terms",
+ "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -606,10 +607,10 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
- "label": "Credit Days Based On",
+ "label": "Default Payment Terms Template",
"length": 0,
"no_copy": 0,
- "options": "\nFixed Days\nLast Day of the Next Month",
+ "options": "Payment Terms Template",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -628,36 +629,6 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "depends_on": "eval:doc.credit_days_based_on == 'Fixed Days'",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.__islocal",
"fieldname": "address_contacts",
"fieldtype": "Section Break",
@@ -970,8 +941,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-07-06 16:40:46.935608",
- "modified_by": "Administrator",
+ "modified": "2017-08-31 16:10:44.049915",
+ "modified_by": "tundebabzy@gmail.com",
"module": "Buying",
"name": "Supplier",
"name_case": "Title Case",
diff --git a/erpnext/buying/doctype/supplier/test_records.json b/erpnext/buying/doctype/supplier/test_records.json
index d2b3995..1c78807 100644
--- a/erpnext/buying/doctype/supplier/test_records.json
+++ b/erpnext/buying/doctype/supplier/test_records.json
@@ -1,6 +1,18 @@
[
{
"doctype": "Supplier",
+ "supplier_name": "_Test Supplier With Template 1",
+ "supplier_type": "_Test Supplier Type",
+ "payment_terms": "_Test Payment Term Template 3"
+ },
+ {
+ "doctype": "Supplier",
+ "supplier_name": "_Test Supplier P",
+ "supplier_type": "_Test Supplier Type",
+ "credit_days_based_on": "Fixed Days"
+ },
+ {
+ "doctype": "Supplier",
"supplier_name": "_Test Supplier with Country",
"supplier_type": "_Test Supplier Type",
"country": "Greece"
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 1d089e7..16dda5c 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -5,56 +5,62 @@
import frappe, unittest
from erpnext.accounts.party import get_due_date
-from erpnext.exceptions import PartyFrozen, PartyDisabled
+from erpnext.exceptions import PartyDisabled
from frappe.test_runner import make_test_records
+test_dependencies = ['Payment Term', 'Payment Terms Template']
test_records = frappe.get_test_records('Supplier')
+
class TestSupplier(unittest.TestCase):
- def test_supplier_due_date_against_supplier_credit_limit(self):
- # Set Credit Limit based on Fixed days
- frappe.db.set_value("Supplier", "_Test Supplier", "credit_days_based_on", "Fixed Days")
- frappe.db.set_value("Supplier", "_Test Supplier", "credit_days", 10)
+ def test_supplier_default_payment_terms(self):
+ # Payment Term based on Days after invoice date
+ frappe.db.set_value(
+ "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier", "_Test Company")
- self.assertEqual(due_date, "2016-02-01")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-21")
- # Set Credit Limit based on Last day next month
- frappe.db.set_value("Supplier", "_Test Supplier", "credit_days", 0)
- frappe.db.set_value("Supplier", "_Test Supplier", "credit_days_based_on",
- "Last Day of the Next Month")
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2017-02-21")
- # Leap year
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier", "_Test Company")
+ # Payment Term based on last day of month
+ frappe.db.set_value(
+ "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
+
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-29")
- # Non Leap year
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier", "_Test Company")
+
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-28")
- frappe.db.set_value("Supplier", "_Test Supplier", "credit_days_based_on", "")
+ frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
# Set credit limit for the supplier type instead of supplier and evaluate the due date
- # based on Fixed days
- frappe.db.set_value("Supplier Type", "_Test Supplier Type", "credit_days_based_on",
- "Fixed Days")
- frappe.db.set_value("Supplier Type", "_Test Supplier Type", "credit_days", 10)
+ frappe.db.set_value("Supplier Type", "_Test Supplier Type", "payment_terms", "_Test Payment Term Template 3")
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier", "_Test Company")
- self.assertEqual(due_date, "2016-02-01")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+ self.assertEqual(due_date, "2016-02-21")
- # Set credit limit for the supplier type instead of supplier and evaluate the due date
- # based on Last day of next month
- frappe.db.set_value("Supplier", "_Test Supplier Type", "credit_days", 0)
- frappe.db.set_value("Supplier Type", "_Test Supplier Type", "credit_days_based_on",
- "Last Day of the Next Month")
+ # Payment terms for Supplier Type instead of supplier and evaluate the due date
+ frappe.db.set_value("Supplier Type", "_Test Supplier Type", "payment_terms", "_Test Payment Term Template 1")
# Leap year
- due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier", "_Test Company")
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-29")
- # Non Leap year
- due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier", "_Test Company")
+ # # Non Leap year
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-28")
+ # Supplier with no default Payment Terms Template
+ frappe.db.set_value("Supplier Type", "_Test Supplier Type", "payment_terms", "")
+ frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
+
+ due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
+ self.assertEqual(due_date, "2016-01-22")
+ # # Non Leap year
+ due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
+ self.assertEqual(due_date, "2017-01-22")
def test_supplier_disabled(self):
make_test_records("Item")
@@ -71,7 +77,6 @@
po.save()
-
def test_supplier_country(self):
# Test that country field exists in Supplier DocType
supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d04143d..9719c22 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _, throw
-from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate
+from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day
from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
from erpnext.utilities.transaction_base import TransactionBase
@@ -27,10 +27,15 @@
def onload(self):
self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings', 'make_payment_via_journal_entry')
+ if self.is_new():
+ relevant_docs = ("Quotation", "Purchase Order", "Sales Order", "Purchase Invoice", "Purchase Order")
+ if self.doctype in relevant_docs:
+ self.set_payment_schedule()
def validate(self):
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
+
self.validate_date_with_fiscal_year()
if self.meta.get_field("currency"):
@@ -42,9 +47,7 @@
validate_return(self)
self.set_total_in_words()
- if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
- self.validate_due_date()
- self.validate_advance_entries()
+ self.validate_all_documents_schedule()
if self.meta.get_field("taxes_and_charges"):
self.validate_enabled_taxes_and_charges()
@@ -55,6 +58,29 @@
if self.doctype == 'Purchase Invoice':
self.validate_paid_amount()
+ def validate_invoice_documents_schedule(self):
+ if self.get("payment_schedule"):
+ self.set_due_date()
+ self.validate_payment_schedule()
+ else:
+ self.set_payment_schedule()
+ self.set_due_date()
+ self.validate_due_date()
+ self.validate_advance_entries()
+
+ def validate_non_invoice_documents_schedule(self):
+ if self.get("payment_schedule"):
+ self.validate_invoice_portion()
+ self.validate_payment_schedule_amount()
+ else:
+ self.set_payment_schedule()
+
+ def validate_all_documents_schedule(self):
+ if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
+ self.validate_invoice_documents_schedule()
+ elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
+ self.validate_non_invoice_documents_schedule()
+
def before_print(self):
if self.doctype in ['Purchase Order', 'Sales Order']:
if self.get("group_same_items"):
@@ -74,11 +100,11 @@
self.paid_amount = 0
frappe.throw(_("Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified"))
else:
- frappe.db.set(self,'paid_amount',0)
+ frappe.db.set(self, 'paid_amount', 0)
def set_missing_values(self, for_validate=False):
if frappe.flags.in_test:
- for fieldname in ["posting_date","transaction_date"]:
+ for fieldname in ["posting_date", "transaction_date"]:
if self.meta.get_field(fieldname) and not self.get(fieldname):
self.set(fieldname, today())
break
@@ -109,9 +135,9 @@
if not self.due_date:
frappe.throw(_("Due Date is mandatory"))
- validate_due_date(self.posting_date, self.due_date, "Customer", self.customer, self.company)
+ validate_due_date(self.posting_date, self.due_date, "Customer", self.customer)
elif self.doctype == "Purchase Invoice":
- validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier, self.company)
+ validate_due_date(self.posting_date, self.due_date, "Supplier", self.supplier)
def set_price_list_currency(self, buying_or_selling):
if self.meta.get_field("posting_date"):
@@ -601,6 +627,63 @@
for item in duplicate_list:
self.remove(item)
+ def set_payment_schedule(self):
+ posting_date = self.get("posting_date") or self.get("transaction_date")
+ date = self.get("due_date")
+ due_date = date or posting_date
+
+ if self.get("payment_terms_template") and not self.get("payment_schedule"):
+ data = get_payment_terms(self.payment_terms_template, posting_date, self.grand_total)
+ for item in data:
+ self.append("payment_schedule", item)
+ elif not self.get("payment_schedule"):
+ data = dict(due_date=due_date, invoice_portion=100, payment_amount=self.grand_total)
+ self.append("payment_schedule", data)
+
+ def set_due_date(self):
+ self.due_date = max([d.due_date for d in self.get("payment_schedule")])
+
+ def validate_payment_schedule(self):
+ self.validate_payment_schedule_dates()
+ self.validate_invoice_portion()
+ self.validate_payment_schedule_amount()
+
+ def validate_payment_schedule_dates(self):
+ dates = []
+ li = []
+ if self.due_date and getdate(self.due_date) < getdate(self.posting_date):
+ frappe.throw(_("Due Date cannot be before posting date"))
+
+ for d in self.get("payment_schedule"):
+ if getdate(d.due_date) < getdate(self.posting_date):
+ frappe.throw(_("Row {0}: Due Date cannot be before posting date").format(d.idx))
+ elif d.due_date in dates:
+ li.append('{0} in row {1}'.format(d.due_date, d.idx))
+ # frappe.throw(_("Row {0}: Duplicate due date found").format(d.idx))
+ dates.append(d.due_date)
+
+ if li:
+ duplicates = '<br>' + '<br>'.join(li)
+ frappe.throw(_("Rows with duplicate due dates in other rows were found: {list}").format(list=duplicates))
+
+
+ def validate_payment_schedule_amount(self):
+ total = 0
+ for d in self.get("payment_schedule"):
+ total += flt(d.payment_amount)
+
+ if total != self.grand_total:
+ frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand Total"))
+
+ def validate_invoice_portion(self):
+ total_portion = 0
+ for term in self.payment_schedule:
+ total_portion += flt(term.get('invoice_portion', 0))
+
+ if flt(total_portion, 2) != 100.00:
+ frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
+
+
@frappe.whitelist()
def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
@@ -769,4 +852,43 @@
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
- where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
\ No newline at end of file
+ where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
+
+@frappe.whitelist()
+def get_payment_terms(terms_template, posting_date=None, grand_total=None):
+ if not terms_template:
+ return
+
+ terms_doc = frappe.get_doc("Payment Terms Template", terms_template)
+
+ schedule = []
+ for d in terms_doc.get("terms"):
+ term_details = get_payment_term_details(d, posting_date, grand_total)
+ schedule.append(term_details)
+
+ return schedule
+
+@frappe.whitelist()
+def get_payment_term_details(term, posting_date=None, grand_total=None):
+ term_details = frappe._dict()
+ if isinstance(term, unicode):
+ term = frappe.get_doc("Payment Term", term)
+ else:
+ term_details.payment_term = term.payment_term
+ term_details.description = term.description
+ term_details.invoice_portion = term.invoice_portion
+ term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
+ if posting_date:
+ term_details.due_date = get_due_date(posting_date, term)
+ return term_details
+
+def get_due_date(posting_date, term):
+ due_date = None
+ if term.due_date_based_on == "Day(s) after invoice date":
+ due_date = add_days(posting_date, term.credit_days)
+ elif term.due_date_based_on == "Day(s) after the end of the invoice month":
+ due_date = add_days(get_last_day(posting_date), term.credit_days)
+ elif term.due_date_based_on == "Month(s) after the end of the invoice month":
+ due_date = add_months(get_last_day(posting_date), term.credit_months)
+
+ return due_date
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 0074560..d609b9e 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -256,6 +256,9 @@
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
+ def update_terms(source_doc, target_doc, source_parent):
+ target_doc.payment_amount = -source_doc.payment_amount
+
doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": doctype,
@@ -272,6 +275,10 @@
},
"postprocess": update_item
},
+ "Payment Schedule": {
+ "doctype": "Payment Schedule",
+ "postprocess": update_terms
+ }
}, target_doc, set_missing_values)
return doclist
diff --git a/erpnext/docs/user/manual/en/accounts/payment-terms.md b/erpnext/docs/user/manual/en/accounts/payment-terms.md
new file mode 100644
index 0000000..4f28d0d
--- /dev/null
+++ b/erpnext/docs/user/manual/en/accounts/payment-terms.md
@@ -0,0 +1,70 @@
+# Payment Terms
+You can save your business' payment terms on ERPNext and include it in all documents in the sales/purchase cycle and ERPNext will make all the proper general ledger entries accordingly.
+
+The documents you can attach Payment Terms to are:
+- Sales Invoice
+- Purchase Invoice
+- Sales Order
+- Purchase Order
+- Quotation
+
+Note that the introduction of Payment Terms removes "Credit Days" and "Credit Days Based On" fields in Customer/Supplier master. Payment Term contains the same information and makes it more flexible to use.
+
+## Payment Terms
+Navigate to the Payment Term list page and click "New".
+> Accounts > Payment Term > New Payment Term
+
+Payment Term has the following fields:
+**Payment Term Name:** (optional) The name for this Payment Term.
+
+**Due Date Based On:** The basis by which the due date for the Payment Term is to be calculated. There are three options:
+- Day(s) after invoice date: Due date should be calculated in days with reference to the posting date of the invoice
+- Day(s) after the end of the invoice month: Due date should be calculated in days with reference to the last day of the month in which the invoice was created
+- Month(s) after the end of the invoice month: Due date should be calculated in months with reference to the last day of the month in which the invoice was created
+
+**Invoice Portion:** (optional) The portion of the total invoice amount for which this Payment Term should be applied. Value given will be regarded as percentage i.e 100 = 100%
+
+**Credit Days:** (optional) The number of days or month credit is allowed depending on the option chosen in the `Due Date Based On` field. 0 means no credit allowed.
+
+**Description:** (optional) A brief description of the Payment Term.
+
+## Payment Terms In Converted Documents
+When converting or copying documents in the sales/purchase cycle, the attached Payment Term(s) will not be copied. The reason for this is because the copied information might no longer be true. For example, a Quotation is created for a service costing $1000 on January 1 with payment term of "N 30" (Net payable within 30 days) and then sent to a customer. On the quotation, the due date on the invoice will be January 30. Let's say the customer agrees to the quotation of January 20 and you decide to make an invoice from the Quotation. If the Payment Terms from the Quotation is copied, the due date on the invoice will still wrongly read January 30. This issue also applies for recurring documents.
+
+This does not mean you have manually set Payment Terms for every document. If you want the Payment Terms to be copyable, make use of Payment Terms Template.
+
+## Payment Terms Template
+Payment Terms Template tells ERPNext how to populate the table in the Payment Terms Schedule section of the sales/purchase document.
+
+You should use it if you have a set of standard Payment Terms or if you want the Payment Term(s) on a sales/purchase document to be copyable.
+
+To create a new Payment Terms Template, navigate to the Payment Term Template creation form:
+> Accounts > Payment Terms Template > New Payment Terms Template
+
+**Payment Term:** (optional) The name for this Payment Term.
+
+**Due Date Based On:** The basis by which the due date for the Payment Term is to be calculated. There are three options:
+- Day(s) after invoice date: Due date should be calculated in days with reference to the posting date of the invoice
+- Day(s) after the end of the invoice month: Due date should be calculated in days with reference to the last day of the month in which the invoice was created
+- Month(s) after the end of the invoice month: Due date should be calculated in months with reference to the last day of the month in which the invoice was created
+
+**Invoice Portion:** (optional) The portion of the total invoice amount for which this Payment Term should be applied. Value given will be regarded as percentage i.e 100 = 100%
+
+**Credit Days:** (optional) The number of days or month credit is allowed depending on the option chosen in the `Due Date Based On` field. 0 means no credit allowed.
+
+**Description:** (optional) A brief description of the Payment Term.
+
+Add as many rows as needed but make sure that the sum of the values in the `Invoice Portion` fields in all populated rows equals 100.
+
+## How to Add Payment Terms To Documents
+You can add Payments Terms in the "Payment Terms Schedule" section of the Document. Each row in the table represents a portion of the document's grand total. The table collects the following information:
+
+**Payment Term:** (optional) The name of the Payment Term document you require. If this is added, the data from the selected Payment Term will be used to populate the remaining columns in the row.
+
+**Description:** (optional) Description of the Payment Term.
+
+**Due Date:** (optional) The due date for the portion of the invoice. Set this value only if you did not specify anything in the `Payment Term` column.
+
+**Invoice Portion:** The percentage portion of the document represented in each row.
+
+**Payment Amount:** The amount due from the portion of the invoice represented by each row.
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index b813537..9b4832a 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -8,6 +8,7 @@
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
test_records = frappe.get_test_records('Expense Claim')
+test_dependencies = ['Employee']
class TestExpenseClaim(unittest.TestCase):
def test_total_expense_claim_for_project(self):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 855358e..a664c74 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -446,4 +446,10 @@
erpnext.patches.v8_9.set_default_customer_group
erpnext.patches.v8_9.remove_employee_from_salary_structure_parent
erpnext.patches.v8_9.delete_gst_doctypes_for_outside_india_accounts
-erpnext.patches.v8_9.set_default_fields_in_variant_settings
\ No newline at end of file
+erpnext.patches.v8_9.set_default_fields_in_variant_settings
+erpnext.patches.v8_10.add_due_date_to_gle
+erpnext.patches.v8_10.update_gl_due_date_for_pi_and_si
+erpnext.patches.v8_10.add_payment_terms_field_to_supplier
+erpnext.patches.v8_10.change_default_customer_credit_days
+erpnext.patches.v8_10.add_payment_terms_field_to_supplier_type
+erpnext.patches.v8_10.change_default_supplier_type_credit_days
diff --git a/erpnext/patches/v8_10/__init__.py b/erpnext/patches/v8_10/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/patches/v8_10/__init__.py
diff --git a/erpnext/patches/v8_10/add_due_date_to_gle.py b/erpnext/patches/v8_10/add_due_date_to_gle.py
new file mode 100644
index 0000000..ec5e003
--- /dev/null
+++ b/erpnext/patches/v8_10/add_due_date_to_gle.py
@@ -0,0 +1,7 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ if not frappe.db.has_column("GL Entry", "due_date"):
+ frappe.db.sql("ALTER TABLE `tabGL Entry` ADD COLUMN `due_date` DATE NULL")
diff --git a/erpnext/patches/v8_10/add_payment_terms_field_to_supplier.py b/erpnext/patches/v8_10/add_payment_terms_field_to_supplier.py
new file mode 100644
index 0000000..b507b2a
--- /dev/null
+++ b/erpnext/patches/v8_10/add_payment_terms_field_to_supplier.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ if not frappe.db.has_column("Customer", "payment_terms"):
+ frappe.db.sql("ALTER TABLE `tabCustomer` ADD COLUMN `payment_terms` VARCHAR(140) NULL")
+ if not frappe.db.has_column("Supplier", "payment_terms"):
+ frappe.db.sql("ALTER TABLE `tabSupplier` ADD COLUMN `payment_terms` VARCHAR(140) NULL")
diff --git a/erpnext/patches/v8_10/add_payment_terms_field_to_supplier_type.py b/erpnext/patches/v8_10/add_payment_terms_field_to_supplier_type.py
new file mode 100644
index 0000000..78b8943
--- /dev/null
+++ b/erpnext/patches/v8_10/add_payment_terms_field_to_supplier_type.py
@@ -0,0 +1,7 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ if not frappe.db.has_column("Supplier Type", "payment_terms"):
+ frappe.db.sql("ALTER TABLE `tabSupplier Type` ADD COLUMN `payment_terms` VARCHAR(140) NULL")
diff --git a/erpnext/patches/v8_10/change_default_customer_credit_days.py b/erpnext/patches/v8_10/change_default_customer_credit_days.py
new file mode 100644
index 0000000..9008a43
--- /dev/null
+++ b/erpnext/patches/v8_10/change_default_customer_credit_days.py
@@ -0,0 +1,106 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "payment_term")
+ frappe.reload_doc("accounts", "doctype", "payment_terms_template_detail")
+ frappe.reload_doc("accounts", "doctype", "payment_terms_template")
+
+ payment_terms = []
+ customers = []
+ suppliers = []
+ credit_days = frappe.db.sql(
+ "SELECT DISTINCT `credit_days`, `credit_days_based_on`, `customer_name` from "
+ "`tabCustomer` where credit_days_based_on='Fixed Days' or "
+ "credit_days_based_on='Last Day of the Next Month'")
+
+ credit_records = ((record[0], record[1], record[2]) for record in credit_days)
+ for days, based_on, customer_name in credit_records:
+ payment_term = make_payment_term(days, based_on)
+ template = make_template(payment_term)
+ payment_terms.append('WHEN `customer_name`="%s" THEN "%s"' % (customer_name, template.template_name))
+ customers.append(customer_name)
+
+ begin_query_str = "UPDATE `tabCustomer` SET `payment_terms` = CASE "
+ value_query_str = " ".join(payment_terms)
+ cond_query_str = " ELSE `payment_terms` END WHERE "
+
+ if customers:
+ frappe.db.sql(
+ begin_query_str + value_query_str + cond_query_str + '`customer_name` IN %s',
+ (customers,)
+ )
+
+ # reset
+ payment_terms = []
+ credit_days = frappe.db.sql(
+ "SELECT DISTINCT `credit_days`, `credit_days_based_on`, `supplier_name` from "
+ "`tabSupplier` where credit_days_based_on='Fixed Days' or "
+ "credit_days_based_on='Last Day of the Next Month'")
+
+ credit_records = ((record[0], record[1], record[2]) for record in credit_days)
+ for days, based_on, supplier_name in credit_records:
+ if based_on == "Fixed Days":
+ pyt_template_name = 'Default Payment Term - N{0}'.format(days)
+ else:
+ pyt_template_name = 'Default Payment Term - EO2M'
+
+ if not frappe.db.exists("Payment Term Template", pyt_template_name):
+ payment_term = make_payment_term(days, based_on)
+ template = make_template(payment_term)
+ else:
+ template = frappe.get_doc("Payment Term Template", pyt_template_name)
+
+ payment_terms.append('WHEN `supplier_name`="%s" THEN "%s"' % (supplier_name, template.template_name))
+ suppliers.append(supplier_name)
+
+ begin_query_str = "UPDATE `tabSupplier` SET `payment_terms` = CASE "
+ value_query_str = " ".join(payment_terms)
+ cond_query_str = " ELSE `payment_terms` END WHERE "
+
+ if suppliers:
+ frappe.db.sql(
+ begin_query_str + value_query_str + cond_query_str + '`supplier_name` IN %s',
+ (suppliers,)
+ )
+
+
+def make_template(payment_term):
+ doc = frappe.new_doc('Payment Terms Template Detail')
+ doc.payment_term = payment_term.payment_term_name
+ doc.due_date_based_on = payment_term.due_date_based_on
+ doc.invoice_portion = payment_term.invoice_portion
+ doc.description = payment_term.description
+ doc.credit_days = payment_term.credit_days
+ doc.credit_months = payment_term.credit_months
+
+ template = frappe.new_doc('Payment Terms Template')
+ template.template_name = 'Default Payment Term - {0}'.format(payment_term.payment_term_name)
+ template.append('terms', doc)
+ template.save()
+
+ return template
+
+
+def make_payment_term(days, based_on):
+ based_on_map = {
+ 'Fixed Days': 'Day(s) after invoice date',
+ 'Last Day of the Next Month': 'Month(s) after the end of the invoice month'
+ }
+
+ doc = frappe.new_doc('Payment Term')
+ doc.due_date_based_on = based_on_map.get(based_on)
+ doc.invoice_portion = 100
+
+ if based_on == 'Fixed Days':
+ doc.credit_days = days
+ doc.description = 'Net payable within {0} days'.format(days)
+ doc.payment_term_name = 'N{0}'.format(days)
+ else:
+ doc.credit_months = 1
+ doc.description = 'Net payable by the end of next month'
+ doc.payment_term_name = 'EO2M'
+
+ doc.save()
+ return doc
diff --git a/erpnext/patches/v8_10/change_default_supplier_type_credit_days.py b/erpnext/patches/v8_10/change_default_supplier_type_credit_days.py
new file mode 100644
index 0000000..448bdbe
--- /dev/null
+++ b/erpnext/patches/v8_10/change_default_supplier_type_credit_days.py
@@ -0,0 +1,39 @@
+import frappe
+from erpnext.patches.v8_10.change_default_customer_credit_days import make_payment_term, make_template
+
+
+def execute():
+ payment_terms = []
+ supplier_types = []
+
+ credit_days = frappe.db.sql(
+ "SELECT DISTINCT `credit_days`, `credit_days_based_on`, `supplier_type` from "
+ "`tabSupplier Type` where credit_days_based_on='Fixed Days' or "
+ "credit_days_based_on='Last Day of the Next Month'")
+
+ records = ((record[0], record[1], record[2]) for record in credit_days)
+
+ for days, based_on, supplier_type in records:
+ if based_on == "Fixed Days":
+ pyt_term_name = 'N{0}'.format(days)
+ else:
+ pyt_term_name = 'EO2M'
+
+ if not frappe.db.exists("Payment Term", pyt_term_name):
+ payment_term = make_payment_term(days, based_on)
+ make_template(payment_term)
+ else:
+ payment_term = frappe.get_doc("Payment Term", pyt_term_name)
+
+ payment_terms.append('WHEN `supplier_type`="%s" THEN "%s"' % (supplier_type, payment_term.payment_term_name))
+ supplier_types.append(supplier_type)
+
+ begin_query_str = "UPDATE `tabSupplier Type` SET `payment_terms` = CASE "
+ value_query_str = " ".join(payment_terms)
+ cond_query_str = " ELSE `payment_terms` END WHERE "
+
+ if supplier_types:
+ frappe.db.sql(
+ begin_query_str + value_query_str + cond_query_str + '`supplier_type` IN %s',
+ (supplier_types,)
+ )
diff --git a/erpnext/patches/v8_10/update_gl_due_date_for_pi_and_si.py b/erpnext/patches/v8_10/update_gl_due_date_for_pi_and_si.py
new file mode 100644
index 0000000..f54ff80
--- /dev/null
+++ b/erpnext/patches/v8_10/update_gl_due_date_for_pi_and_si.py
@@ -0,0 +1,136 @@
+from __future__ import unicode_literals
+import frappe
+
+# This will update existing GL Entries by saving its linked Purchase/Sales Invoice's
+# Journal Entry's due date as the due date for the GL Entry
+
+
+def execute():
+ kwargs = get_query_kwargs()
+
+ for kwarg in kwargs:
+ for batch in get_result_in_batches(**kwarg):
+ voucher_num_col = kwarg.get('voucher_num_col', 'voucher_no')
+ voucher_type = kwarg.get('use_voucher_type') or kwarg.get('voucher_type')
+ conditions, names = build_conditions(batch, voucher_type, voucher_num_col)
+ if conditions and names:
+ start = 'UPDATE `tabGL Entry` SET `due_date` = CASE '
+ cond = ' '.join(conditions)
+ else_cond = ' ELSE `due_date` END WHERE '
+
+ frappe.db.sql(
+ start + cond + else_cond + voucher_num_col + ' IN %s',
+ values=(names,)
+ )
+
+
+def get_result_in_batches(**kwargs):
+ """A simple generator to yield slices of GL Entry records"""
+ while True:
+ batch = get_gle_batch(**kwargs)
+ if batch:
+ yield batch
+ else:
+ return
+
+
+def get_gle_batch(**kwargs):
+ """Returns a slice of records in GL Entry"""
+ doctype = kwargs.get('doctype')
+ fields = kwargs.get('fields')
+ limit_start = kwargs.get('limit_start')
+ limit_page_length = kwargs.get('limit_page_length')
+ filters = kwargs.get('filters')
+ or_filters = kwargs.get('or_filters')
+
+ results = frappe.get_list(
+ doctype, fields=fields, limit_start=limit_start, limit_page_length=limit_page_length,
+ filters=filters, or_filters=or_filters
+ )
+
+ return results
+
+
+def build_conditions(query_results, voucher_type, voucher_num_col):
+ """
+ builds the string to be used is sql CASE statement. Returns the a tuple of
+ the string for the CASE statement and a tuple of applicable voucher names
+ """
+ conditions = []
+ invoice_names = []
+
+ for result in query_results:
+ voucher_no = result.get(voucher_num_col)
+ if voucher_no:
+ invoice_names.append("%s" % (voucher_no,))
+
+ # get invoice details
+ invoice_details = frappe.get_list(
+ voucher_type, fields=['name', 'due_date'], filters={'name': ('in', invoice_names)}
+ )
+
+ if invoice_details:
+ for d in invoice_details:
+ conditions.append('WHEN `{voucher_no}`="{number}" THEN "{date}"'.format(
+ number=d.name, date=d.due_date, voucher_no=voucher_num_col))
+
+ return conditions, invoice_names
+
+
+def get_query_kwargs():
+ pi_kwargs = dict(
+ voucher_type='Purchase Invoice', doctype='GL Entry', fields=['voucher_no'],
+ limit_start=0, limit_page_length=5, filters={
+ "ifnull(due_date, '')": ('=', ''), "ifnull(party, '')": ('!=', ''),
+ 'voucher_type': 'Purchase Invoice', 'credit': ('!=', '0')
+ }
+ )
+
+ si_kwargs = dict(
+ voucher_type='Sales Invoice', doctype='GL Entry', fields=['voucher_no'],
+ limit_start=0, limit_page_length=5, filters={
+ "ifnull(due_date, '')": ('=', ''), "ifnull(party, '')": ('!=', ''),
+ 'voucher_type': 'Sales Invoice', 'debit': ('!=', '0')
+ }
+ )
+
+ journal_kwargs_si = dict(
+ voucher_type='Journal Entry', doctype='GL Entry', fields=['against_voucher'],
+ limit_start=0, limit_page_length=5, filters={
+ "ifnull(due_date, '')": ('=', ''), "ifnull(party, '')": ('!=', ''),
+ 'voucher_type': 'Journal Entry', 'against_voucher_type': 'Sales Invoice'
+ },
+ voucher_num_col='against_voucher', use_voucher_type='Sales Invoice',
+ )
+
+ journal_kwargs_pi = dict(
+ voucher_type='Journal Entry', doctype='GL Entry', fields=['against_voucher'],
+ limit_start=0, limit_page_length=5, filters={
+ "ifnull(due_date, '')": ('=', ''), "ifnull(party, '')": ('!=', ''),
+ 'voucher_type': 'Journal Entry', 'against_voucher_type': 'Purchase Invoice'
+ },
+ voucher_num_col='against_voucher', use_voucher_type='Purchase Invoice',
+ )
+
+ payment_entry_kwargs_pi = dict(
+ voucher_type='Payment Entry', doctype='GL Entry', fields=['against_voucher'],
+ limit_start=0, limit_page_length=5, filters={
+ "ifnull(due_date, '')": ('=', ''), "ifnull(party, '')": ('!=', ''),
+ 'voucher_type': 'Payment Entry', 'against_voucher_type': 'Purchase Invoice'
+ },
+ voucher_num_col='against_voucher', use_voucher_type='Purchase Invoice',
+ )
+
+ payment_entry_kwargs_si = dict(
+ voucher_type='Payment Entry', doctype='GL Entry', fields=['against_voucher'],
+ limit_start=0, limit_page_length=5, filters={
+ "ifnull(due_date, '')": ('=', ''), "ifnull(party, '')": ('!=', ''),
+ 'voucher_type': 'Payment Entry', 'against_voucher_type': 'Sales Invoice'
+ },
+ voucher_num_col='against_voucher', use_voucher_type='Sales Invoice',
+ )
+
+ return [
+ pi_kwargs, si_kwargs, journal_kwargs_pi, journal_kwargs_si,
+ payment_entry_kwargs_pi, payment_entry_kwargs_si
+ ]
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index abd5566..f38084f 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -62,15 +62,32 @@
frappe.model.set_value(cdt, cdn, 'account', account)
})
}
-})
+});
+
+frappe.ui.form.on("Sales Invoice", {
+ payment_terms_template: function() {
+ cur_frm.trigger("disable_due_date");
+ }
+});
frappe.ui.form.on('Purchase Invoice', {
mode_of_payment: function(frm) {
get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){
frm.set_value('cash_bank_account', account);
})
+ },
+
+ payment_terms_template: function() {
+ cur_frm.trigger("disable_due_date");
}
-})
+});
+
+frappe.ui.form.on("Payment Schedule", {
+ payment_schedule_remove: function() {
+ cur_frm.trigger("disable_due_date");
+ },
+
+});
frappe.ui.form.on('Payment Entry', {
mode_of_payment: function(frm) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 5f35ea4..9164f07 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -123,6 +123,10 @@
}
}
+ if(this.frm.fields_dict["payment_terms_template"]){
+ this.frm.trigger("payment_terms_template");
+ }
+
if(this.frm.fields_dict["taxes"]) {
this["taxes_remove"] = this.calculate_taxes_and_totals;
}
@@ -1124,6 +1128,46 @@
}
}
},
+
+ payment_terms_template: function() {
+ var me = this;
+ if(this.frm.doc.payment_terms_template) {
+ frappe.call({
+ method: "erpnext.controllers.accounts_controller.get_payment_terms",
+ args: {
+ terms_template: this.frm.doc.payment_terms_template,
+ posting_date: this.frm.doc.posting_date || this.frm.doc.transaction_date,
+ grand_total: this.frm.doc.grand_total
+ },
+ callback: function(r) {
+ if(r.message && !r.exc) {
+ me.frm.set_value("payment_schedule", r.message);
+ }
+ }
+ })
+ }
+ },
+
+ payment_term: function(doc, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ if(row.payment_term) {
+ frappe.call({
+ method: "erpnext.controllers.accounts_controller.get_payment_term_details",
+ args: {
+ term: row.payment_term,
+ posting_date: this.frm.doc.posting_date || this.frm.doc.transaction_date,
+ grand_total: this.frm.doc.grand_total
+ },
+ callback: function(r) {
+ if(r.message && !r.exc) {
+ for (var d in r.message) {
+ frappe.model.set_value(cdt, cdn, d, r.message[d]);
+ }
+ }
+ }
+ })
+ }
+ }
});
erpnext.show_serial_batch_selector = function(frm, d, callback, show_dialog) {
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 52c6b6d..222d2e5 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -786,7 +786,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
- "collapsible_depends_on": "eval:doc.credit_days || doc.credit_limit",
+ "collapsible_depends_on": "",
"columns": 0,
"fieldname": "credit_limit_section",
"fieldtype": "Section Break",
@@ -818,69 +818,6 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
- "fieldname": "credit_days_based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Days Based On",
- "length": 0,
- "no_copy": 0,
- "options": "\nFixed Days\nLast Day of the Next Month",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.credit_days_based_on=='Fixed Days'",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "credit_days",
- "oldfieldtype": "Int",
- "permlevel": 1,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "credit_limit",
"fieldtype": "Currency",
"hidden": 0,
@@ -911,6 +848,38 @@
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "",
+ "fieldname": "payment_terms",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Default Payment Terms Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
"collapsible": 1,
"collapsible_depends_on": "customer_details",
"columns": 0,
@@ -1202,8 +1171,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-07-24 00:55:07.445783",
- "modified_by": "Administrator",
+ "modified": "2017-08-31 15:12:18.637132",
+ "modified_by": "tundebabzy@gmail.com",
"module": "Selling",
"name": "Customer",
"name_case": "Title Case",
diff --git a/erpnext/selling/doctype/customer/test_customer.js b/erpnext/selling/doctype/customer/test_customer.js
new file mode 100644
index 0000000..65b81af
--- /dev/null
+++ b/erpnext/selling/doctype/customer/test_customer.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Customer", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Customer
+ () => frappe.tests.make('Customer', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 40ed6f9..45546e3 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -6,6 +6,7 @@
import frappe
import unittest
+from erpnext.accounts.party import get_due_date
from frappe.test_runner import make_test_records
from erpnext.exceptions import PartyFrozen, PartyDisabled
from frappe.utils import flt
@@ -13,7 +14,7 @@
from erpnext.tests.utils import create_test_contact_and_address
test_ignore = ["Price List"]
-
+test_dependencies = ['Payment Term', 'Payment Terms Template']
test_records = frappe.get_test_records('Customer')
class TestCustomer(unittest.TestCase):
@@ -181,6 +182,35 @@
customer.credit_limit = flt(outstanding_amt - 100)
self.assertRaises(frappe.ValidationError, customer.save)
+ def test_customer_payment_terms(self):
+ frappe.db.set_value(
+ "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3")
+
+ due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template")
+ self.assertEqual(due_date, "2016-02-21")
+
+ due_date = get_due_date("2017-01-22", "Customer", "_Test Customer With Template")
+ self.assertEqual(due_date, "2017-02-21")
+
+ frappe.db.set_value(
+ "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1")
+
+ due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template")
+ self.assertEqual(due_date, "2016-02-29")
+
+ due_date = get_due_date("2017-01-22", "Customer", "_Test Customer With Template")
+ self.assertEqual(due_date, "2017-02-28")
+
+ frappe.db.set_value("Customer", "_Test Customer With Template", "payment_terms", "")
+
+ # No default payment term template attached
+ due_date = get_due_date("2016-01-22", "Customer", "_Test Customer")
+ self.assertEqual(due_date, "2016-01-22")
+
+ due_date = get_due_date("2017-01-22", "Customer", "_Test Customer")
+ self.assertEqual(due_date, "2017-01-22")
+
+
def get_customer_dict(customer_name):
return {
"customer_group": "_Test Customer Group",
diff --git a/erpnext/selling/doctype/customer/test_records.json b/erpnext/selling/doctype/customer/test_records.json
index 94f14ed..a012c1b 100644
--- a/erpnext/selling/doctype/customer/test_records.json
+++ b/erpnext/selling/doctype/customer/test_records.json
@@ -1,6 +1,20 @@
[
{
"customer_group": "_Test Customer Group",
+ "customer_name": "_Test Customer With Template",
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory"
+ },
+ {
+ "customer_group": "_Test Customer Group",
+ "customer_name": "_Test Customer P",
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory"
+ },
+ {
+ "customer_group": "_Test Customer Group",
"customer_name": "_Test Customer",
"customer_type": "Individual",
"doctype": "Customer",
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index fe41426..0562c78 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -2156,6 +2156,100 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
+ "collapsible_depends_on": "eval:!doc.__islocal",
+ "columns": 0,
+ "depends_on": "",
+ "fieldname": "payment_schedule_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_schedule",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 1,
+ "options": "Payment Schedule",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
"collapsible_depends_on": "terms",
"columns": 0,
"fieldname": "terms_section_break",
@@ -2756,9 +2850,9 @@
"istable": 0,
"max_attachments": 1,
"menu_index": 0,
- "modified": "2017-09-19 11:22:15.268846",
+ "modified": "2017-09-19 11:22:15.268846",
"modified_by": "Administrator",
- "module": "Selling",
+ "module": "Selling",
"name": "Quotation",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index d7d84c7..c6a488e 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -8,7 +8,29 @@
test_dependencies = ["Product Bundle"]
+
class TestQuotation(unittest.TestCase):
+ def test_make_quotation_without_terms(self):
+ quotation = make_quotation(do_not_save=1)
+ self.assertFalse(quotation.get('payment_schedule'))
+
+ quotation.insert()
+
+ self.assertTrue(quotation.payment_schedule)
+
+ def test_make_sales_order_terms_not_copied(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ quotation = frappe.copy_doc(test_records[0])
+ quotation.transaction_date = nowdate()
+ quotation.valid_till = add_months(quotation.transaction_date, 1)
+ quotation.insert()
+ quotation.submit()
+
+ sales_order = make_sales_order(quotation.name)
+
+ self.assertFalse(sales_order.get('payment_schedule'))
+
def test_make_sales_order(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
@@ -33,6 +55,46 @@
sales_order.transaction_date = nowdate()
sales_order.insert()
+ def test_make_sales_order_with_terms(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ quotation = frappe.copy_doc(test_records[0])
+ quotation.transaction_date = nowdate()
+ quotation.valid_till = add_months(quotation.transaction_date, 1)
+ quotation.update(
+ {"payment_terms_template": "_Test Payment Term Template"}
+ )
+ quotation.insert()
+
+ self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
+ quotation.save()
+ quotation.submit()
+
+ self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.25)
+ self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date)
+ self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.25)
+ self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30))
+
+ sales_order = make_sales_order(quotation.name)
+
+ self.assertEquals(sales_order.doctype, "Sales Order")
+ self.assertEquals(len(sales_order.get("items")), 1)
+ self.assertEquals(sales_order.get("items")[0].doctype, "Sales Order Item")
+ self.assertEquals(sales_order.get("items")[0].prevdoc_docname, quotation.name)
+ self.assertEquals(sales_order.customer, "_Test Customer")
+
+ sales_order.delivery_date = "2014-01-01"
+ sales_order.naming_series = "_T-Quotation-"
+ sales_order.transaction_date = nowdate()
+ sales_order.insert()
+
+ self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.25)
+ self.assertEqual(sales_order.payment_schedule[0].due_date, quotation.transaction_date)
+ self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.25)
+ self.assertEqual(
+ sales_order.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)
+ )
+
def test_valid_till(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index b57895a..2b99639 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -2318,10 +2318,9 @@
"label": "Packed Items",
"length": 0,
"no_copy": 0,
- "oldfieldname": "packing_details",
- "oldfieldtype": "Table",
"options": "Packed Item",
"permlevel": 0,
+ "precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
@@ -2337,6 +2336,99 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
+ "collapsible_depends_on": "eval:!doc.__islocal||doc.payment_schedule",
+ "columns": 0,
+ "fieldname": "payment_schedule_section",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Schedule",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Terms Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Payment Terms Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "payment_schedule",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Payment Schedule",
+ "length": 0,
+ "no_copy": 1,
+ "options": "Payment Schedule",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
"collapsible_depends_on": "terms",
"columns": 0,
"fieldname": "terms_section_break",
@@ -3783,9 +3875,9 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-09-19 11:21:36.332326",
+ "modified": "2017-09-19 11:21:36.332326",
"modified_by": "Administrator",
- "module": "Selling",
+ "module": "Selling",
"name": "Sales Order",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.js b/erpnext/selling/doctype/sales_order/test_sales_order.js
new file mode 100644
index 0000000..57ed19b
--- /dev/null
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Sales Order", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially('Sales Order', [
+ // insert a new Sales Order
+ () => frappe.tests.make([
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 7c0d7f9..723123e 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -11,6 +11,7 @@
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from frappe.tests.test_permissions import set_user_permission_doctypes
+
class TestSalesOrder(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
@@ -58,6 +59,32 @@
si1 = make_sales_invoice(so.name)
self.assertEquals(len(si1.get("items")), 0)
+ def test_make_sales_invoice_with_terms(self):
+ so = make_sales_order(do_not_submit=True)
+
+ self.assertRaises(frappe.ValidationError, make_sales_invoice, so.name)
+
+ so.update({"payment_terms_template": "_Test Payment Term Template"})
+
+ so.save()
+ so.submit()
+ si = make_sales_invoice(so.name)
+
+ self.assertEquals(len(si.get("items")), len(so.get("items")))
+ self.assertEquals(len(si.get("items")), 1)
+
+ si.insert()
+
+ self.assertEqual(si.payment_schedule[0].payment_amount, 500.0)
+ self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date)
+ self.assertEqual(si.payment_schedule[1].payment_amount, 500.0)
+ self.assertEqual(si.payment_schedule[1].due_date, add_days(so.transaction_date, 30))
+
+ si.submit()
+
+ si1 = make_sales_invoice(so.name)
+ self.assertEquals(len(si1.get("items")), 0)
+
def test_update_qty(self):
so = make_sales_order()
@@ -123,7 +150,6 @@
so = make_sales_order()
self.assertEqual(get_reserved_qty(), existing_reserved_qty + 10)
-
dn = create_dn_against_so(so.name, 15)
self.assertEqual(get_reserved_qty(), existing_reserved_qty)
@@ -179,7 +205,6 @@
make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100)
make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100)
-
existing_reserved_qty_item1 = get_reserved_qty("_Test Item")
existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100")
@@ -501,10 +526,39 @@
self.assertEquals(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))
new_so.items[0].margin_rate_or_amount = 25
+ new_so.payment_schedule = []
+ new_so.save()
new_so.submit()
self.assertEquals(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))
+ def test_terms_auto_added(self):
+ so = make_sales_order(do_not_save=1)
+
+ self.assertFalse(so.get('payment_schedule'))
+
+ so.insert()
+
+ self.assertTrue(so.get('payment_schedule'))
+
+ def test_terms_not_copied(self):
+ so = make_sales_order()
+ self.assertTrue(so.get('payment_schedule'))
+
+ si = make_sales_invoice(so.name)
+ self.assertFalse(si.get('payment_schedule'))
+
+ def test_terms_copied(self):
+ so = make_sales_order(do_not_copy=1)
+ so.payment_terms_template = '_Test Payment Term Template'
+ so.insert()
+ self.assertTrue(so.get('payment_schedule'))
+
+ si = make_sales_invoice(so.name)
+ si.insert()
+ self.assertTrue(si.get('payment_schedule'))
+
+
def make_sales_order(**args):
so = frappe.new_doc("Sales Order")
args = frappe._dict(args)
@@ -539,6 +593,10 @@
so.insert()
if not args.do_not_submit:
so.submit()
+ else:
+ so.payment_schedule = []
+ else:
+ so.payment_schedule = []
return so
diff --git a/erpnext/setup/doctype/supplier_type/supplier_type.json b/erpnext/setup/doctype/supplier_type/supplier_type.json
index 9b40e0f..d7d7084 100644
--- a/erpnext/setup/doctype/supplier_type/supplier_type.json
+++ b/erpnext/setup/doctype/supplier_type/supplier_type.json
@@ -1,5 +1,6 @@
{
"allow_copy": 0,
+ "allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:supplier_type",
@@ -12,6 +13,7 @@
"editable_grid": 0,
"fields": [
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -42,6 +44,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@@ -71,12 +74,13 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
- "fieldname": "credit_days_based_on",
- "fieldtype": "Select",
+ "fieldname": "payment_terms",
+ "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -84,39 +88,10 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
- "label": "Credit Days Based On",
+ "label": "Default Payment Terms Template",
"length": 0,
"no_copy": 0,
- "options": "\nFixed Days\nLast Day of the Next Month",
- "permlevel": 1,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.credit_days_based_on=='Fixed Days'",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
+ "options": "Payment Terms Template",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@@ -130,6 +105,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -158,6 +134,7 @@
"unique": 0
},
{
+ "allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -189,18 +166,18 @@
"unique": 0
}
],
+ "has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-flag",
"idx": 1,
"image_view": 0,
"in_create": 0,
- "in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-02-20 13:25:25.641431",
+ "modified": "2017-09-04 18:54:10.093500",
"modified_by": "Administrator",
"module": "Setup",
"name": "Supplier Type",
diff --git a/erpnext/setup/doctype/supplier_type/test_supplier_type.js b/erpnext/setup/doctype/supplier_type/test_supplier_type.js
new file mode 100644
index 0000000..085dddd
--- /dev/null
+++ b/erpnext/setup/doctype/supplier_type/test_supplier_type.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Supplier Type", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Supplier Type
+ () => frappe.tests.make('Supplier Type', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});