Fixed merge conflict

This commit is contained in:
Nabin Hait 2017-11-21 20:18:03 +05:30
commit 32a62a02df
81 changed files with 3288 additions and 371 deletions

View File

@ -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

View File

@ -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",

View File

@ -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);
}
}
},

View File

@ -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

View File

@ -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",

View File

@ -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
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

View File

@ -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")

View File

@ -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
}

View File

@ -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

View File

@ -0,0 +1,2 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

View File

@ -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
}

View File

@ -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

View File

@ -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()
]);
});

View File

@ -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

View File

@ -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
}
]

View File

@ -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");
}
});

View File

@ -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
}

View File

@ -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)

View File

@ -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()
]);
});

View File

@ -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)

View File

@ -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"
}
]

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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");
},
}
})

View File

@ -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",

View File

@ -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"))

View File

@ -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'),

View File

@ -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

View File

@ -607,3 +607,4 @@ var calculate_total_billing_amount = function(frm) {
refresh_field('total_billing_amount')
}

View File

@ -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",

View File

@ -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"))

View File

@ -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()
]);
});

View File

@ -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"]

View File

@ -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

View File

@ -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):

View File

@ -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"):

View File

@ -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)

View File

@ -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()))

View File

@ -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",

View File

@ -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 [

View File

@ -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",

View File

@ -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",

View File

@ -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')

View File

@ -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 = '<br>' + '<br>'.join(li)
frappe.throw(_("Rows with duplicate due dates in other rows were found: {list}")
.format(list=duplicates))
def validate_payment_schedule_amount(self):
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""")
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

View File

@ -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

View File

@ -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.

View File

@ -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):

29
erpnext/hub_node/api.py Normal file
View File

@ -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)

View File

@ -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),

View File

@ -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

View File

View File

@ -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

View File

@ -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
]

View File

@ -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) {

View File

@ -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) {

View File

@ -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",

View File

@ -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()
]);
});

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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()
]);

View File

@ -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(),

View File

@ -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": [

View File

@ -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()
]);
});

View File

@ -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

View File

@ -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");

View File

@ -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(),

View File

@ -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(),

View File

@ -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'),

View File

@ -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",

View File

@ -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()
]);
});

View File

@ -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
})

View File

@ -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()

View File

@ -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:

View File

@ -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}
]
]}
]
}
});