diff --git a/.travis.yml b/.travis.yml
index 8681c03f55d..66753374c7a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -29,7 +29,7 @@ install:
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
before_script:
- - wget http://chromedriver.storage.googleapis.com/2.27/chromedriver_linux64.zip
+ - wget http://chromedriver.storage.googleapis.com/2.32/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip
- sudo apt-get install libnss3
- sudo apt-get --only-upgrade install google-chrome-stable
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index 76e66d0906b..e27eaabedcc 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -74,6 +74,36 @@
"set_only_once": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "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,
@@ -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 9047a4edccf..4ce68865bea 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -185,8 +185,39 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
})
},
+ 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 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
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 cc356529ab1..f010e67b998 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -436,7 +436,8 @@ class JournalEntry(AccountsController):
"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
})
)
@@ -898,3 +899,14 @@ def get_average_exchange_rate(account):
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 54af579adb4..48d5ed2ca9e 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.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 56bdfbaf4b6..36ff0ac381b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -8,14 +8,16 @@ from frappe import _, scrub, ValidationError
from frappe.utils import flt, comma_or, nowdate
from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on
from erpnext.accounts.party import get_party_account
-from erpnext.accounts.doctype.journal_entry.journal_entry \
- import get_average_exchange_rate, get_default_bank_cash_account
+from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.controllers.accounts_controller import AccountsController
-class InvalidPaymentEntry(ValidationError): pass
+
+class InvalidPaymentEntry(ValidationError):
+ pass
+
class PaymentEntry(AccountsController):
def setup_party_account_field(self):
@@ -69,10 +71,9 @@ class PaymentEntry(AccountsController):
def validate_duplicate_entry(self):
reference_names = []
for d in self.get("references"):
- if (d.reference_doctype, d.reference_name) in reference_names:
+ if (d.reference_doctype, d.reference_name, d.due_date) in reference_names:
frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}").format(d.idx, d.reference_doctype, d.reference_name))
- reference_names.append((d.reference_doctype, d.reference_name))
-
+ reference_names.append((d.reference_doctype, d.reference_name, d.due_date))
def validate_allocated_amount(self):
for d in self.get("references"):
@@ -80,7 +81,6 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) > flt(d.outstanding_amount):
frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
-
def delink_advance_entry_references(self):
for reference in self.references:
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
@@ -128,7 +128,6 @@ class PaymentEntry(AccountsController):
self.set_missing_ref_details()
-
def set_missing_ref_details(self):
for d in self.get("references"):
if d.allocated_amount:
@@ -295,7 +294,7 @@ class PaymentEntry(AccountsController):
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)
@@ -413,7 +412,8 @@ class PaymentEntry(AccountsController):
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),
@@ -505,12 +505,10 @@ def get_outstanding_reference_documents(args):
company_currency = frappe.db.get_value("Company", args.get("company"), "default_currency")
# Get negative outstanding sales /purchase invoices
- total_field = "base_grand_total" if party_account_currency == company_currency else "grand_total"
-
negative_outstanding_invoices = []
- if (args.get("party_type") != "Student"):
+ if args.get("party_type") not in ["Student", "Employee"]:
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"),
- args.get("party"), args.get("party_account"), total_field)
+ args.get("party"), args.get("party_account"), party_account_currency, company_currency)
# Get positive outstanding sales /purchase invoices/ Fees
outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
@@ -580,28 +578,34 @@ def get_orders_to_be_billed(posting_date, party_type, party, party_account_curre
return order_list
-def get_negative_outstanding_invoices(party_type, party, party_account, total_field):
- if party_type != "Employee":
- voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
- return frappe.db.sql("""
- select
- "{voucher_type}" as voucher_type, name as voucher_no,
- {total_field} as invoice_amount, outstanding_amount, posting_date,
- due_date, conversion_rate as exchange_rate
- from
- `tab{voucher_type}`
- where
- {party_type} = %s and {party_account} = %s and docstatus = 1 and outstanding_amount < 0
- order by
- posting_date, name
- """.format(**{
- "total_field": total_field,
- "voucher_type": voucher_type,
- "party_type": scrub(party_type),
- "party_account": "debit_to" if party_type=="Customer" else "credit_to"
- }), (party, party_account), as_dict = True)
+def get_negative_outstanding_invoices(party_type, party, party_account, party_account_currency, company_currency):
+ voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
+ if party_account_currency == company_currency:
+ grand_total_field = "base_grand_total"
+ rounded_total_field = "base_rounded_total"
else:
- return []
+ grand_total_field = "grand_total"
+ rounded_total_field = "rounded_total"
+
+ return frappe.db.sql("""
+ select
+ "{voucher_type}" as voucher_type, name as voucher_no,
+ if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
+ outstanding_amount, posting_date,
+ due_date, conversion_rate as exchange_rate
+ from
+ `tab{voucher_type}`
+ where
+ {party_type} = %s and {party_account} = %s and docstatus = 1 and outstanding_amount < 0
+ order by
+ posting_date, name
+ """.format(**{
+ "rounded_total_field": rounded_total_field,
+ "grand_total_field": grand_total_field,
+ "voucher_type": voucher_type,
+ "party_type": scrub(party_type),
+ "party_account": "debit_to" if party_type == "Customer" else "credit_to"
+ }), (party, party_account), as_dict=True)
@frappe.whitelist()
def get_party_details(company, party_type, party, date):
@@ -721,7 +725,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
if party_amount:
grand_total = outstanding_amount = party_amount
elif dt in ("Sales Invoice", "Purchase Invoice"):
- grand_total = doc.base_grand_total if party_account_currency == doc.company_currency else doc.grand_total
+ if party_account_currency == doc.company_currency:
+ grand_total = doc.base_rounded_total or doc.base_grand_total
+ else:
+ grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount
@@ -730,8 +737,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
else:
- total_field = "base_grand_total" if party_account_currency == doc.company_currency else "grand_total"
- grand_total = flt(doc.get(total_field))
+ if party_account_currency == doc.company_currency:
+ grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
+ else:
+ grand_total = flt(doc.get("rounded_total") or doc.grand_total)
outstanding_amount = grand_total - flt(doc.advance_paid)
# bank or cash
@@ -766,20 +775,51 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
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,
- "bill_no": doc.get("bill_no"),
- "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,
+ "bill_no": reference.get("bill_no")
+ })
+ 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 60be20dd89a..a3a78a33e7d 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -14,6 +14,7 @@ from erpnext.hr.doctype.expense_claim.test_expense_claim import make_expense_cla
test_dependencies = ["Item"]
+
class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_order(self):
so = make_sales_order()
@@ -40,7 +41,7 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(so_advance_paid, 0)
def test_payment_entry_against_si_usd_to_usd(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, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
@@ -65,8 +66,20 @@ class TestPaymentEntry(unittest.TestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
+ def test_payment_entry_against_si_multi_due_dates(self):
+ si = create_sales_invoice(do_not_save=1)
+ si.payment_terms_template = '_Test Payment Term Template'
+ si.insert()
+ si.submit()
+
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.reference_no = "1"
+ pe.reference_date = "2016-01-01"
+ pe.insert()
+ pe.submit()
+
def test_payment_entry_against_pi(self):
- pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
+ pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
@@ -88,7 +101,7 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_ec(self):
payable = frappe.db.get_value('Company', "_Test Company", 'default_payable_account')
- ec = make_expense_claim(payable, 300, 300, "_Test Company","Travel Expenses - _TC")
+ ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC")
pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300)
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -108,7 +121,7 @@ class TestPaymentEntry(unittest.TestCase):
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 +225,7 @@ class TestPaymentEntry(unittest.TestCase):
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 00000000000..e69de29bb2d
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 00000000000..b34f894538c
--- /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": 1,
+ "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": "",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "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": 1,
+ "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-11-21 19:23:08.490659",
+ "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 00000000000..41740170f03
--- /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 00000000000..e69de29bb2d
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 00000000000..054c2d11917
--- /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 00000000000..702319ba74d
--- /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 00000000000..5d4df053fb2
--- /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 00000000000..b26e42aa37d
--- /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 00000000000..d9baa5907b9
--- /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 00000000000..ef6e0693b3c
--- /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 00000000000..e69de29bb2d
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 00000000000..558297fea76
--- /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 00000000000..095965886d4
--- /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 00000000000..7042df05942
--- /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, cint
+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 += 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')
+
+ def validate_credit_days(self):
+ for term in self.terms:
+ if cint(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 00000000000..494a0ed21fb
--- /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 00000000000..6daaf1ed74e
--- /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 00000000000..fea0b35c112
--- /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 00000000000..e69de29bb2d
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 00000000000..f808a0f8637
--- /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-26 05:21:51.738319",
+ "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 00000000000..54c0fda0119
--- /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 82a3d653abf..3fa34e279a3 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 @@ class TestPricingRule(unittest.TestCase):
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 @@ class TestPricingRule(unittest.TestCase):
# 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 ff58becb08f..2fbf014289e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -378,5 +378,5 @@ frappe.ui.form.on("Purchase Invoice", {
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 9837f77e444..75e121e7cb1 100755
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -2973,6 +2973,99 @@
"set_only_once": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:(!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 Terms",
+ "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,
@@ -3760,7 +3853,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2017-11-16 01:04:15.308603",
+ "modified": "2017-11-21 01:04:15.308603",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 626dd928273..0c8bfc002cb 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -95,7 +95,7 @@ class PurchaseInvoice(BuyingController):
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)
@@ -362,7 +362,27 @@ class PurchaseInvoice(BuyingController):
def make_supplier_gl_entry(self, gl_entries):
grand_total = self.rounded_total or self.grand_total
- if 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 grand_total:
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js
index e01dda32bf6..5e6d95c33ed 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.js
@@ -20,7 +20,8 @@ QUnit.test("test purchase invoice", function(assert) {
{contact_person: 'Contact 3-Test Supplier'},
{taxes_and_charges: 'TEST In State GST'},
{tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
+ {terms: 'This is Test'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
@@ -34,6 +35,9 @@ QUnit.test("test purchase invoice", function(assert) {
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
+ assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
+ assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
+
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 68bd3c2bc3f..3ac521a7cdb 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 @@ from __future__ import unicode_literals
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):
@@ -62,6 +63,12 @@ class TestPurchaseInvoice(unittest.TestCase):
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)
@@ -248,6 +255,7 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(pi.outstanding_amount, 1212.30)
pi.disable_rounded_total = 0
+ pi.get("payment_schedule")[0].payment_amount = 1512.0
pi.save()
self.assertEqual(pi.outstanding_amount, 1212.0)
@@ -263,6 +271,56 @@ class TestPurchaseInvoice(unittest.TestCase):
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.disable_rounded_total = 1
+ 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""")
@@ -589,6 +647,55 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEquals(pi.total_taxes_and_charges, 462.3)
self.assertEquals(pi.grand_total, 1712.3)
+ 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(), 25),
+ "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(), 25)],
+ ["_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 ccaae496704..ad52dee9737 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -607,3 +607,4 @@ var calculate_total_billing_amount = function(frm) {
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 c0ea00ccd82..e013dc682bc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -2748,6 +2748,101 @@
"set_only_once": 0,
"unique": 0
},
+ {
+ "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 Terms",
+ "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,
@@ -3002,6 +3097,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "is_pos",
"fieldname": "base_change_amount",
"fieldtype": "Currency",
"hidden": 0,
@@ -3062,6 +3158,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "is_pos",
"fieldname": "change_amount",
"fieldtype": "Currency",
"hidden": 0,
@@ -3093,6 +3190,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
+ "depends_on": "is_pos",
"fieldname": "account_for_change_amount",
"fieldtype": "Link",
"hidden": 0,
@@ -4434,9 +4532,9 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2017-11-15 01:02:36.885752",
+ "modified": "2017-11-17 01:02:36.885752",
"modified_by": "Administrator",
- "module": "Accounts",
+ "module": "Accounts",
"name": "Sales Invoice",
"name_case": "Title Case",
"owner": "Administrator",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index ee2b5542fbf..5c067fea3dc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -242,7 +242,7 @@ class SalesInvoice(SellingController):
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)
@@ -633,7 +633,27 @@ class SalesInvoice(SellingController):
def make_customer_gl_entry(self, gl_entries):
grand_total = self.rounded_total or self.grand_total
- if 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 grand_total:
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(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 35b255875ea..b4be3ba67cb 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.js
@@ -1,7 +1,7 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice", function(assert) {
- assert.expect(4);
+ assert.expect(6);
let done = assert.async();
frappe.run_serially([
() => {
@@ -19,7 +19,8 @@ QUnit.test("test sales Invoice", function(assert) {
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST'},
{tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
+ {terms: 'This is Test'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
@@ -31,7 +32,10 @@ QUnit.test("test sales Invoice", function(assert) {
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
- assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
+ assert.ok(cur_frm.doc.grand_total==590, "Grand Total correct");
+
+ assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
+ assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
},
() => frappe.tests.click_button('Submit'),
@@ -40,4 +44,3 @@ QUnit.test("test sales Invoice", function(assert) {
() => done()
]);
});
-
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 460158b3ed8..b3a143cbcef 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3,8 +3,9 @@
from __future__ import unicode_literals
import frappe
+
import unittest, copy, time
-from frappe.utils import nowdate, add_days, flt, cint
+from frappe.utils import nowdate, add_days, flt, getdate, cint
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 +59,13 @@ class TestSalesInvoice(unittest.TestCase):
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(getdate(si.payment_schedule[0].due_date), getdate(si.due_date))
+
def test_sales_invoice_calculation_base_currency(self):
si = frappe.copy_doc(test_records[2])
si.insert()
@@ -199,6 +207,7 @@ class TestSalesInvoice(unittest.TestCase):
# additional discount
si.discount_amount = 100
si.apply_discount_on = 'Net Total'
+ si.payment_schedule = []
si.save()
@@ -211,6 +220,7 @@ class TestSalesInvoice(unittest.TestCase):
# additional discount on grand total
si.discount_amount = 100
si.apply_discount_on = 'Grand Total'
+ si.payment_schedule = []
si.save()
@@ -932,20 +942,6 @@ class TestSalesInvoice(unittest.TestCase):
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)
@@ -1325,6 +1321,40 @@ class TestSalesInvoice(unittest.TestCase):
})
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")
@@ -1404,6 +1434,20 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEquals(si.total_taxes_and_charges, 577.05)
self.assertEquals(si.grand_total, 1827.05)
+ 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)
@@ -1437,6 +1481,11 @@ def create_sales_invoice(**args):
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/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js
index 736443e2609..14c0d55bd8a 100644
--- a/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js
+++ b/erpnext/accounts/doctype/sales_invoice/tests/test_sales_invoice_with_payment.js
@@ -19,7 +19,8 @@ QUnit.test("test sales Invoice with payment", function(assert) {
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST'},
{tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
+ {terms: 'This is Test'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
@@ -43,6 +44,7 @@ QUnit.test("test sales Invoice with payment", function(assert) {
() => { cur_frm.set_value('paid_to','Cash - '+frappe.get_abbr(frappe.defaults.get_default('Company')));},
() => {cur_frm.set_value('reference_no','TEST1234');},
() => {cur_frm.set_value('reference_date',frappe.datetime.add_days(frappe.datetime.nowdate(), 0));},
+ () => cur_frm.set_value("payment_schedule", []),
() => cur_frm.save(),
() => {
// get payment details
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index c575d59ae74..d370c496fbc 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 @@ def check_if_in_list(gle, gl_map):
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 e6887baacce..7bccfe89f36 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -9,7 +9,7 @@ from frappe import _, msgprint, scrub
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 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
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
@@ -163,7 +164,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date,
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
@@ -262,51 +263,54 @@ def validate_party_accounts(doc):
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
@@ -316,7 +320,8 @@ def validate_due_date(posting_date, due_date, party_type, party, company):
msgprint(_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)")
.format(date_diff(due_date, default_due_date)))
else:
- frappe.throw(_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)))
+ frappe.throw(_("Due / Reference Date cannot be after {0}")
+ .format(formatdate(default_due_date)))
@frappe.whitelist()
def set_taxes(party, party_type, posting_date, company, customer_group=None, supplier_type=None,
@@ -353,6 +358,16 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup
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 ba5b7f2d4a5..300e6a81985 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -113,7 +113,7 @@ class ReceivablePayableReport(object):
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 @@ class ReceivablePayableReport(object):
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 @@ class ReceivablePayableReport(object):
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
@@ -250,12 +250,12 @@ class ReceivablePayableReport(object):
else:
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,
+ self.gl_entries = frappe.db.sql("""select name, posting_date, account, party_type, party,
+ 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 d592816692f..ba5d8b3b2a5 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -571,11 +571,12 @@ def get_stock_rbnb_difference(posting_date, company):
# Amount should be credited
return flt(stock_rbnb) + flt(sys_bal)
+
def get_outstanding_invoices(party_type, party, account, condition=None):
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount")
- if party_type=="Customer":
+ if party_type == "Customer":
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
payment_dr_or_cr = "payment_gl_entry.credit_in_account_currency - payment_gl_entry.debit_in_account_currency"
else:
@@ -585,12 +586,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
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,
- (
- case when (voucher_type = 'Sales Invoice' or voucher_type = 'Purchase Invoice')
- then (select due_date from `tab{invoice}` where name = voucher_no)
- else posting_date end
- ) as due_date,
+ voucher_no, voucher_type, posting_date, ifnull(sum({dr_or_cr}), 0) as invoice_amount, due_date,
(
select ifnull(sum({payment_dr_or_cr}), 0)
from `tabGL Entry` payment_gl_entry
@@ -601,6 +597,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
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
@@ -612,13 +609,13 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
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(
- dr_or_cr = dr_or_cr,
- invoice = invoice,
- payment_dr_or_cr = payment_dr_or_cr,
- condition = condition or ""
+ order by posting_date, name, due_date""".format(
+ dr_or_cr=dr_or_cr,
+ invoice=invoice,
+ payment_dr_or_cr=payment_dr_or_cr,
+ condition=condition or ""
), {
"party_type": party_type,
"party": party,
@@ -626,17 +623,24 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
}, as_dict=True)
for d in invoice_list:
- outstanding_invoices.append(frappe._dict({
- 'voucher_no': d.voucher_no,
- 'voucher_type': d.voucher_type,
- 'due_date': d.due_date,
- 'posting_date': d.posting_date,
- '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 = 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,
+ 'posting_date': d.posting_date,
+ 'invoice_amount': flt(d.invoice_amount),
+ 'payment_amount': flt(d.payment_amount),
+ 'outstanding_amount': flt(d.invoice_amount - d.payment_amount, precision),
+ '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 0b62b71a715..8f6f468f4c6 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -2440,6 +2440,98 @@
"set_only_once": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "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",
+ "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,
@@ -3172,7 +3264,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-11-15 01:03:44.591992",
+ "modified": "2017-11-18 01:03:44.591992",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 2af55825d44..d31b23007f8 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 @@ import frappe.defaults
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 @@ class TestPurchaseOrder(unittest.TestCase):
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)
@@ -125,6 +153,35 @@ class TestPurchaseOrder(unittest.TestCase):
"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 711e05d913d..e6cea536f59 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,
@@ -622,36 +623,6 @@
"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,
- "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,
@@ -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 cae4aac6355..7479f55650d 100644
--- a/erpnext/buying/doctype/supplier/test_records.json
+++ b/erpnext/buying/doctype/supplier/test_records.json
@@ -1,4 +1,16 @@
[
+ {
+ "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",
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 1d089e70982..16dda5c5ffc 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -5,56 +5,62 @@ from __future__ import unicode_literals
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 @@ class TestSupplier(unittest.TestCase):
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 3cefaff42a9..fb97c91f1fa 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
@@ -26,11 +26,19 @@ class AccountsController(TransactionBase):
return self.__company_currency
def onload(self):
- self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings', 'make_payment_via_journal_entry')
+ 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", "Sales Invoice")
+ 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 +50,7 @@ class AccountsController(TransactionBase):
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 +61,26 @@ class AccountsController(TransactionBase):
if self.doctype == 'Purchase Invoice':
self.validate_paid_amount()
+ def validate_invoice_documents_schedule(self):
+ self.validate_payment_schedule_dates()
+ self.set_due_date()
+ self.validate_invoice_portion()
+ self.set_payment_schedule()
+ self.validate_payment_schedule_amount()
+ self.validate_due_date()
+ self.validate_advance_entries()
+
+ def validate_non_invoice_documents_schedule(self):
+ self.validate_invoice_portion()
+ self.set_payment_schedule()
+ self.validate_payment_schedule_amount()
+
+ 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 @@ class AccountsController(TransactionBase):
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 @@ class AccountsController(TransactionBase):
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"):
@@ -619,6 +645,67 @@ class AccountsController(TransactionBase):
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
+ grand_total = self.get("rounded_total") or self.grand_total
+
+ if not self.get("payment_schedule"):
+ if self.get("payment_terms_template"):
+ data = get_payment_terms(self.payment_terms_template, posting_date, grand_total)
+ for item in data:
+ self.append("payment_schedule", item)
+ else:
+ data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total)
+ self.append("payment_schedule", data)
+ else:
+ for d in self.get("payment_schedule"):
+ d.payment_amount = grand_total * flt(d.invoice_portion) / 100
+
+ def set_due_date(self):
+ due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
+ if due_dates:
+ self.due_date = max(due_dates)
+
+ 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 = '
' + '
'.join(li)
+ frappe.throw(_("Rows with duplicate due dates in other rows were found: {list}")
+ .format(list=duplicates))
+
+ def validate_payment_schedule_amount(self):
+ if self.get("payment_schedule"):
+ total = 0
+ for d in self.get("payment_schedule"):
+ total += flt(d.payment_amount)
+
+ grand_total = self.get("rounded_total") or self.grand_total
+ if total != grand_total:
+ frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
+
+ def validate_invoice_portion(self):
+ if self.get("payment_schedule"):
+ 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.throw(_('Combined invoice portion must equal 100%'), indicator='red')
+
def is_rounded_total_disabled(self):
if self.meta.get_field("disable_rounded_total"):
return self.disable_rounded_total
@@ -796,4 +883,43 @@ def update_invoice_status():
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 00745601845..d609b9e02b5 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -256,6 +256,9 @@ def make_return_doc(doctype, source_name, target_doc=None):
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 @@ def make_return_doc(doctype, source_name, target_doc=None):
},
"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 00000000000..4f28d0da17c
--- /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 b8135374416..9b4832ac5e7 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 frappe.utils import random_string, nowdate
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/hub_node/api.py b/erpnext/hub_node/api.py
new file mode 100644
index 00000000000..b32efd9799f
--- /dev/null
+++ b/erpnext/hub_node/api.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2015, Web Notes Technologies Pvt. Ltd. and Contributors and contributors
+# For license information, please see license.txt
+
+
+import frappe, json
+from frappe.utils import now, nowdate
+from erpnext.hub_node.doctype.hub_settings.hub_settings import get_hub_settings
+
+# API wrapper
+@frappe.whitelist(allow_guest=True)
+def call_method(access_token, method, message):
+ try:
+ args = json.loads(message)
+ if args:
+ return globals()[method](access_token, args)
+ else:
+ return globals()[method](access_token)
+ except:
+ print("Client Exception")
+ print(frappe.get_traceback())
+
+def disable_and_suspend_hub_user(access_token):
+ hub_settings = get_hub_settings()
+ hub_settings.publish = 0
+ hub_settings.publish_pricing = 0
+ hub_settings.publish_availability = 0
+ hub_settings.suspended = 1
+ hub_settings.enabled = 0
+ hub_settings.save(ignore_permissions=True)
diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.js b/erpnext/manufacturing/doctype/production_order/test_production_order.js
index 32cc3efd97a..670f0b06bf6 100644
--- a/erpnext/manufacturing/doctype/production_order/test_production_order.js
+++ b/erpnext/manufacturing/doctype/production_order/test_production_order.js
@@ -16,7 +16,7 @@ QUnit.test("test: production order", function (assert) {
frappe.run_serially([
// test production order
() => frappe.set_route("List", "Production Order"),
- () => frappe.timeout(0.5),
+ () => frappe.timeout(3),
// Create a laptop production order
() => {
@@ -29,7 +29,7 @@ QUnit.test("test: production order", function (assert) {
{fg_warehouse: "Finished Goods - FT"}
]);
},
- () => frappe.timeout(2),
+ () => frappe.timeout(3),
() => {
assert.equal(cur_frm.doc.planned_operating_cost, cur_frm.doc.total_operating_cost,
"Total and Planned Cost is equal");
@@ -54,7 +54,7 @@ QUnit.test("test: production order", function (assert) {
() => cur_frm.savesubmit(),
() => frappe.timeout(1),
() => frappe.click_button('Yes'),
- () => frappe.timeout(1),
+ () => frappe.timeout(2.5),
// Confirm the production order timesheet, save and submit it
() => frappe.click_link("TS-00"),
@@ -62,12 +62,10 @@ QUnit.test("test: production order", function (assert) {
() => frappe.click_button("Submit"),
() => frappe.timeout(1),
() => frappe.click_button("Yes"),
- () => frappe.timeout(2),
+ () => frappe.timeout(2.5),
// Start the production order process
() => frappe.set_route("List", "Production Order"),
- () => frappe.timeout(.5),
- () => frappe.set_route("List", "Production Order"),
() => frappe.timeout(2),
() => frappe.click_link("Laptop"),
() => frappe.timeout(1),
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index bc63f4a794e..53e2d2879c9 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -460,6 +460,8 @@ execute:frappe.delete_doc_if_exists("DocType", "Program Fee")
erpnext.patches.v9_0.set_pos_profile_name
erpnext.patches.v9_0.remove_non_existing_warehouse_from_stock_settings
execute:frappe.delete_doc_if_exists("DocType", "Program Fee")
+erpnext.patches.v8_10.update_gl_due_date_for_pi_and_si
+erpnext.patches.v8_10.change_default_customer_credit_days
erpnext.patches.v9_0.update_employee_loan_details
erpnext.patches.v9_2.delete_healthcare_domain_default_items
erpnext.patches.v9_1.create_issue_opportunity_type
diff --git a/erpnext/patches/v8_10/__init__.py b/erpnext/patches/v8_10/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
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 00000000000..d2bc3ec93e4
--- /dev/null
+++ b/erpnext/patches/v8_10/change_default_customer_credit_days.py
@@ -0,0 +1,88 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ frappe.reload_doc("selling", "doctype", "customer")
+ frappe.reload_doc("buying", "doctype", "supplier")
+ frappe.reload_doc("setup", "doctype", "supplier_type")
+ 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 = []
+ records = []
+ for doctype in ("Customer", "Supplier", "Supplier Type"):
+ credit_days = frappe.db.sql("""
+ SELECT DISTINCT `credit_days`, `credit_days_based_on`, `name`
+ from `tab{0}`
+ where
+ (credit_days_based_on='Fixed Days' and credit_days is not null)
+ or credit_days_based_on='Last Day of the Next Month'
+ """.format(doctype))
+
+ credit_records = ((record[0], record[1], record[2]) for record in credit_days)
+ for days, based_on, party_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 Terms Template", pyt_template_name):
+ payment_term = make_payment_term(days, based_on)
+ template = make_template(payment_term)
+ else:
+ template = frappe.get_doc("Payment Terms Template", pyt_template_name)
+
+ payment_terms.append('WHEN `name`="%s" THEN "%s"' % (party_name, template.template_name))
+ records.append(party_name)
+
+ begin_query_str = "UPDATE `tab{0}` SET `payment_terms` = CASE ".format(doctype)
+ value_query_str = " ".join(payment_terms)
+ cond_query_str = " ELSE `payment_terms` END WHERE "
+
+ if records:
+ frappe.db.sql(
+ begin_query_str + value_query_str + cond_query_str + '`name` IN %s',
+ (records,)
+ )
+
+
+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/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 00000000000..8596e66cac8
--- /dev/null
+++ b/erpnext/patches/v8_10/update_gl_due_date_for_pi_and_si.py
@@ -0,0 +1,138 @@
+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():
+ frappe.reload_doc("accounts", "doctype", "gl_entry")
+
+ 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 abd5566243f..f38084fa68e 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -62,15 +62,32 @@ frappe.ui.form.on('Sales Invoice Payment', {
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 621e9a749e8..0a4d5f8512e 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -102,6 +102,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
});
}
+ 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;
}
@@ -1155,6 +1159,46 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}
}
},
+
+ payment_terms_template: function() {
+ var me = this;
+ if(this.frm.doc.payment_terms_template && this.frm.doc.payment_schedule.length === 0) {
+ 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.rounded_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.rounded_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 52c6b6db862..222d2e569eb 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",
@@ -812,69 +812,6 @@
"unique": 0,
"width": "50%"
},
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "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,
@@ -907,6 +844,38 @@
"set_only_once": 0,
"unique": 0
},
+ {
+ "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,
@@ -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 00000000000..65b81af32c1
--- /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 40ed6f98fe1..45546e348aa 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
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.selling.doctype.customer.customer import get_credit_limit, get_cust
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 @@ class TestCustomer(unittest.TestCase):
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 94f14ed6fb0..a012c1ba38d 100644
--- a/erpnext/selling/doctype/customer/test_records.json
+++ b/erpnext/selling/doctype/customer/test_records.json
@@ -1,4 +1,18 @@
[
+ {
+ "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",
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index 6d8a3fe97a5..440d26b9d0a 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -2152,6 +2152,100 @@
"unique": 0,
"width": "200px"
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": "",
+ "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 Terms",
+ "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,
@@ -2757,7 +2851,7 @@
"istable": 0,
"max_attachments": 1,
"menu_index": 0,
- "modified": "2017-11-15 01:01:16.774645",
+ "modified": "2017-11-17 01:01:16.774645",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index d7d84c7bd67..c6a488e3cc2 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -8,7 +8,29 @@ import unittest
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 @@ class TestQuotation(unittest.TestCase):
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/quotation/tests/test_quotation.js b/erpnext/selling/doctype/quotation/tests/test_quotation.js
index 1683fa56883..d69d799d0de 100644
--- a/erpnext/selling/doctype/quotation/tests/test_quotation.js
+++ b/erpnext/selling/doctype/quotation/tests/test_quotation.js
@@ -1,5 +1,5 @@
QUnit.test("test: quotation", function (assert) {
- assert.expect(10);
+ assert.expect(12);
let done = assert.async();
frappe.run_serially([
() => {
@@ -10,7 +10,8 @@ QUnit.test("test: quotation", function (assert) {
{"item_code": "Test Product 1"},
{"qty": 5}
]]
- }
+ },
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => {
@@ -18,7 +19,7 @@ QUnit.test("test: quotation", function (assert) {
assert.ok(cur_frm.doc.items[0].item_name == "Test Product 1", "Added Test Product 1");
// calculate_taxes_and_totals
- assert.ok(cur_frm.doc.grand_total === 500, "Total Amount is correct");
+ assert.ok(cur_frm.doc.grand_total === 500, String(cur_frm.doc.grand_total));
},
() => cur_frm.set_value("customer_address", "Test1-Billing"),
() => cur_frm.set_value("shipping_address_name", "Test1-Warehouse"),
@@ -30,6 +31,7 @@ QUnit.test("test: quotation", function (assert) {
() => cur_frm.doc.items[0].rate = 200,
() => frappe.timeout(0.3),
() => cur_frm.set_value("tc_name", "Test Term 1"),
+ () => cur_frm.set_value("payment_schedule", []),
() => frappe.timeout(0.5),
() => cur_frm.save(),
() => {
@@ -47,6 +49,9 @@ QUnit.test("test: quotation", function (assert) {
// Check Terms and Condtions
assert.ok(cur_frm.doc.tc_name == "Test Term 1", "Terms and Conditions Checked");
+ assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
+ assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
+
},
() => done()
]);
diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js b/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js
index b7b5a470d69..aeb5d1b9eb7 100644
--- a/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js
+++ b/erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js
@@ -16,13 +16,15 @@ QUnit.test("test quotation with additional discount in grand total", function(as
]},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
+ {contact_person: 'Contact 1-Test Customer 1'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => {
return frappe.tests.set_form_values(cur_frm, [
{apply_discount_on:'Grand Total'},
- {additional_discount_percentage:10}
+ {additional_discount_percentage:10},
+ {payment_schedule: []}
]);
},
() => cur_frm.save(),
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 86d042b8844..1acb8c1861a 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -2319,10 +2319,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,
@@ -2333,6 +2332,99 @@
"set_only_once": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": "",
+ "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",
+ "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,
@@ -3407,9 +3499,9 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2017-11-15 01:02:08.674118",
+ "modified": "2017-11-17 01:02:08.674118",
"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 00000000000..57ed19b6965
--- /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 da600fbbc9d..f8299611622 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -13,6 +13,7 @@ from frappe.tests.test_permissions import set_user_permission_doctypes
from erpnext.selling.doctype.sales_order.sales_order import make_production_orders
import json
+
class TestSalesOrder(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
@@ -60,6 +61,32 @@ class TestSalesOrder(unittest.TestCase):
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()
@@ -125,7 +152,6 @@ class TestSalesOrder(unittest.TestCase):
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)
@@ -181,7 +207,6 @@ class TestSalesOrder(unittest.TestCase):
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")
@@ -503,10 +528,39 @@ class TestSalesOrder(unittest.TestCase):
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, do_not_save=1)
+ so.payment_terms_template = '_Test Payment Term Template'
+ so.insert()
+ so.submit()
+ self.assertTrue(so.get('payment_schedule'))
+
+ si = make_sales_invoice(so.name)
+ si.insert()
+ self.assertTrue(si.get('payment_schedule'))
+
def test_make_production_order(self):
# Make a new Sales Order
so = make_sales_order(**{
@@ -575,6 +629,10 @@ def make_sales_order(**args):
so.insert()
if not args.do_not_submit:
so.submit()
+ else:
+ so.payment_schedule = []
+ else:
+ so.payment_schedule = []
return so
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js
index daa81318818..939261c0c55 100644
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js
+++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js
@@ -1,7 +1,7 @@
QUnit.module('Sales Order');
QUnit.test("test sales order", function(assert) {
- assert.expect(10);
+ assert.expect(12);
let done = assert.async();
frappe.run_serially([
() => {
@@ -19,7 +19,8 @@ QUnit.test("test sales order", function(assert) {
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST'},
{tc_name: 'Test Term 1'},
- {terms: 'This is Test'}
+ {terms: 'This is Test'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => {
@@ -28,7 +29,7 @@ QUnit.test("test sales order", function(assert) {
{currency: 'USD'}
]);
},
- () => frappe.timeout(1),
+ () => frappe.timeout(1.5),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 3', "Item name correct");
@@ -36,18 +37,24 @@ QUnit.test("test sales order", function(assert) {
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
- // calculate totals
- assert.ok(cur_frm.doc.items[0].price_list_rate==250, "Item 1 price_list_rate");
- assert.ok(cur_frm.doc.net_total== 1280.75, "net total correct ");
- assert.ok(cur_frm.doc.base_grand_total== flt(1511.29* cur_frm.doc.conversion_rate, precision('base_grand_total')), "base round total correct ");
- assert.ok(cur_frm.doc.grand_total== 1511.29 , "grand total correct ");
- assert.ok(cur_frm.doc.rounded_total== 1511.30, "rounded total correct ");
},
() => cur_frm.save(),
() => frappe.timeout(1),
() => cur_frm.print_doc(),
() => frappe.timeout(1),
() => {
+ // Payment Terms
+ assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
+ assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
+
+ // totals
+ assert.ok(cur_frm.doc.items[0].price_list_rate==250, "Item 1 price_list_rate");
+ assert.ok(cur_frm.doc.net_total== 1280.75, "net total correct ");
+ assert.ok(cur_frm.doc.base_grand_total== flt(1511.29* cur_frm.doc.conversion_rate, precision('base_grand_total')), String(flt(1511.29* cur_frm.doc.conversion_rate, precision('base_grand_total')) + ' ' + cur_frm.doc.base_grand_total));
+ assert.ok(cur_frm.doc.grand_total== 1511.29 , "grand total correct ");
+ assert.ok(cur_frm.doc.rounded_total== 1511.30, "rounded total correct ");
+
+ // print format
assert.ok($('.btn-print-print').is(':visible'), "Print Format Available");
frappe.timeout(1);
assert.ok($(".section-break+ .section-break .column-break:nth-child(1) .data-field:nth-child(1) .value").text().includes("Billing Street 1"), "Print Preview Works As Expected");
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js
index 3247c85d971..de61a6112c1 100644
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js
+++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_discount_on_grand_total.js
@@ -16,13 +16,15 @@ QUnit.test("test sales order with additional discount in grand total", function(
]},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
+ {contact_person: 'Contact 1-Test Customer 1'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => {
return frappe.tests.set_form_values(cur_frm, [
{apply_discount_on:'Grand Total'},
- {additional_discount_percentage:10}
+ {additional_discount_percentage:10},
+ {payment_schedule: []}
]);
},
() => cur_frm.save(),
diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js
index c745374fcc1..2c481083086 100644
--- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js
+++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_item_wise_discount.js
@@ -18,7 +18,8 @@ QUnit.test("test sales order", function(assert) {
]},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
- {contact_person: 'Contact 1-Test Customer 1'}
+ {contact_person: 'Contact 1-Test Customer 1'},
+ {payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
diff --git a/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js
index c70d076c70a..79d1700b4ed 100644
--- a/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js
+++ b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js
@@ -4,7 +4,7 @@ QUnit.test("test:Point of Sales", function(assert) {
frappe.run_serially([
() => frappe.set_route('point-of-sale'),
- () => frappe.timeout(2),
+ () => frappe.timeout(3),
() => frappe.set_control('customer', 'Test Customer 1'),
() => frappe.timeout(0.2),
() => cur_frm.set_value('customer', 'Test Customer 1'),
diff --git a/erpnext/setup/doctype/supplier_type/supplier_type.json b/erpnext/setup/doctype/supplier_type/supplier_type.json
index 9b40e0f4ba9..d7d7084d7ff 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",
- "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",
+ "label": "Default Payment Terms Template",
"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 00000000000..085dddd0b6f
--- /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()
+ ]);
+
+});
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index f7a7d48422f..fcbb63c8ba5 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -12,7 +12,9 @@ from frappe.utils.nestedset import get_root_of
from erpnext.accounts.utils import get_account_name
from erpnext.utilities.product import get_qty_in_stock
-class WebsitePriceListMissingError(frappe.ValidationError): pass
+
+class WebsitePriceListMissingError(frappe.ValidationError):
+ pass
def set_cart_count(quotation=None):
if cint(frappe.db.get_singles_value("Shopping Cart Settings", "enabled")):
@@ -104,6 +106,7 @@ def update_cart(item_code, qty, with_items=False):
apply_cart_settings(quotation=quotation)
quotation.flags.ignore_permissions = True
+ quotation.payment_schedule = []
if not empty_card:
quotation.save()
else:
@@ -179,6 +182,7 @@ def decorate_quotation_doc(doc):
return doc
+
def _get_cart_quotation(party=None):
'''Return the open Quotation of type "Shopping Cart" or make a new one'''
if not party:
@@ -200,6 +204,7 @@ def _get_cart_quotation(party=None):
"status": "Draft",
"docstatus": 0,
"__islocal": 1,
+ "payment_terms_template": "_Test Payment Term Template",
(party.doctype.lower()): party.name
})
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py
index 387b57c8745..0c94cd87dc6 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/shopping_cart/test_shopping_cart.py
@@ -8,6 +8,9 @@ from frappe.utils import nowdate, add_months
from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party
from erpnext.tests.utils import create_test_contact_and_address
+
+test_dependencies = ['Payment Terms Template']
+
class TestShoppingCart(unittest.TestCase):
"""
Note:
@@ -62,7 +65,6 @@ class TestShoppingCart(unittest.TestCase):
self.assertEquals(quotation.get("items")[0].qty, 1)
self.assertEquals(quotation.get("items")[0].amount, 10)
-
# add second item
update_cart("_Test Item 2", 1)
quotation = self.test_get_cart_customer()
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 46b8840538b..3189788c938 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -122,7 +122,8 @@ def create_material_request(material_requests):
mr.update({
"company": company,
"transaction_date": nowdate(),
- "material_request_type": "Material Transfer" if request_type=="Transfer" else request_type
+ "material_request_type": "Material Transfer" if request_type=="Transfer" else request_type,
+ "schedule_date": add_days(nowdate(), cint(items[0].lead_time_days))
})
for d in items:
diff --git a/erpnext/tests/ui/make_fixtures.js b/erpnext/tests/ui/make_fixtures.js
index 949e92b1ca4..8c9e50884af 100644
--- a/erpnext/tests/ui/make_fixtures.js
+++ b/erpnext/tests/ui/make_fixtures.js
@@ -217,6 +217,25 @@ $.extend(frappe.test_data, {
{price_list: '_Test Price List'},
{price_list_rate: 200}
]
+ },
+ "Payment Term": {
+ "_Test Payment Term": [
+ {payment_term_name: '_Test Payment Term'},
+ {due_date_based_on: 'Day(s) after invoice date'},
+ {invoice_portion: 100},
+ {credit_days: 0}
+ ]
+ },
+ "Payment Terms Template": {
+ "_Test Payment Term Template UI": [
+ {template_name: "_Test Payment Term Template UI"},
+ {terms: [
+ [
+ {payment_term: '_Test Payment Term'},
+ {invoice_portion: 100}
+ ]
+ ]}
+ ]
}
});