diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 96b997f9d79..8539c36c43e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -5,14 +5,14 @@
from __future__ import unicode_literals
import frappe, erpnext, json
from frappe import _, scrub, ValidationError
-from frappe.utils import flt, comma_or, nowdate
+from frappe.utils import flt, comma_or, nowdate, getdate
from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on
from erpnext.accounts.party import get_party_account, get_patry_tax_withholding_details
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
+from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status
from six import string_types
@@ -59,6 +59,7 @@ class PaymentEntry(AccountsController):
self.set_remarks()
self.validate_duplicate_entry()
self.validate_allocated_amount()
+ self.ensure_supplier_is_not_blocked()
def on_submit(self):
self.setup_party_account_field()
@@ -537,6 +538,16 @@ def get_outstanding_reference_documents(args):
if isinstance(args, string_types):
args = json.loads(args)
+ # confirm that Supplier is not blocked
+ if args.get('party_type') == 'Supplier':
+ supplier_status = get_supplier_block_status(args['party'])
+ if supplier_status['on_hold']:
+ if supplier_status['hold_type'] == 'All':
+ return []
+ elif supplier_status['hold_type'] == 'Payments':
+ if not supplier_status['release_date'] or getdate(nowdate()) <= supplier_status['release_date']:
+ return []
+
party_account_currency = get_account_currency(args.get("party_account"))
company_currency = frappe.db.get_value("Company", args.get("company"), "default_currency")
@@ -621,6 +632,9 @@ def get_orders_to_be_billed(posting_date, party_type, party, party_account_curre
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"
+ supplier_condition = ""
+ if voucher_type == "Purchase Invoice":
+ supplier_condition = "and (release_date is null or release_date <= CURDATE())"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total"
@@ -638,9 +652,11 @@ def get_negative_outstanding_invoices(party_type, party, party_account, party_ac
`tab{voucher_type}`
where
{party_type} = %s and {party_account} = %s and docstatus = 1 and outstanding_amount < 0
+ {supplier_condition}
order by
posting_date, name
""".format(**{
+ "supplier_condition": supplier_condition,
"rounded_total_field": rounded_total_field,
"grand_total_field": grand_total_field,
"voucher_type": voucher_type,
@@ -854,6 +870,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.mode_of_payment = doc.get("mode_of_payment")
pe.party_type = party_type
pe.party = doc.get(scrub(party_type))
+
+ pe.ensure_supplier_is_not_blocked()
+
pe.paid_from = party_account if payment_type=="Receive" else bank.account
pe.paid_to = party_account if payment_type=="Pay" else bank.account
pe.paid_from_account_currency = party_account_currency \
@@ -864,15 +883,19 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.allocate_payment_amount = 1
pe.letter_head = doc.get("letter_head")
- 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
- })
+ # only Purchase Invoice can be blocked individually
+ if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
+ frappe.msgprint(_('{0} is on hold till {1}'.format(doc.name, doc.release_date)))
+ else:
+ 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
+ })
pe.setup_party_account_field()
pe.set_missing_values()
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 435bee06591..57516a1bbba 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -40,6 +40,69 @@ class TestPaymentEntry(unittest.TestCase):
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
+ def test_payment_entry_for_blocked_supplier_invoice(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Invoices'
+ supplier.save()
+
+ self.assertRaises(frappe.ValidationError, make_purchase_invoice)
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_payment_entry_for_blocked_supplier_payments(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.save()
+
+ pi = make_purchase_invoice()
+
+ self.assertRaises(
+ frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
+ bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_payment_entry_for_blocked_supplier_payments_today_date(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.release_date = nowdate()
+ supplier.save()
+
+ pi = make_purchase_invoice()
+
+ self.assertRaises(
+ frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
+ bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_payment_entry_for_blocked_supplier_payments_past_date(self):
+ # this test is meant to fail only if something fails in the try block
+ with self.assertRaises(Exception):
+ try:
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.release_date = '2018-03-01'
+ supplier.save()
+
+ pi = make_purchase_invoice()
+
+ get_payment_entry('Purchase Invoice', pi.name, bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+ except:
+ pass
+ else:
+ raise Exception
+
def test_payment_entry_against_si_usd_to_usd(self):
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index d2c41938fe4..c1a2c9741b1 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -27,6 +27,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
},
refresh: function(doc) {
+ const me = this;
this._super();
hide_fields(this.frm.doc);
@@ -37,6 +38,27 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
this.show_stock_ledger();
}
+ if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
+ if(doc.on_hold) {
+ this.frm.add_custom_button(
+ __('Change Release Date'),
+ function() {me.change_release_date()},
+ __('Hold Invoice')
+ );
+ this.frm.add_custom_button(
+ __('Unblock Invoice'),
+ function() {me.unblock_invoice()},
+ __('Make')
+ );
+ } else if (!doc.on_hold) {
+ this.frm.add_custom_button(
+ __('Block Invoice'),
+ function() {me.block_invoice()},
+ __('Make')
+ );
+ }
+ }
+
if(!doc.is_return && doc.docstatus==1) {
if(doc.outstanding_amount != 0) {
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __("Make"));
@@ -56,7 +78,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}
if(doc.docstatus===0) {
- var me = this;
this.frm.add_custom_button(__('Purchase Order'), function() {
erpnext.utils.map_current_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice",
@@ -109,6 +130,104 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}
},
+ unblock_invoice: function() {
+ const me = this;
+ frappe.call({
+ 'method': 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.unblock_invoice',
+ 'args': {'name': me.frm.doc.name},
+ 'callback': (r) => me.frm.reload_doc()
+ });
+ },
+
+ block_invoice: function() {
+ this.make_comment_dialog_and_block_invoice();
+ },
+
+ change_release_date: function() {
+ this.make_dialog_and_set_release_date();
+ },
+
+ can_change_release_date: function(date) {
+ const diff = frappe.datetime.get_diff(date, frappe.datetime.nowdate());
+ if (diff < 0) {
+ frappe.throw('New release date should be in the future');
+ return false;
+ } else {
+ return true;
+ }
+ },
+
+ make_comment_dialog_and_block_invoice: function(){
+ const me = this;
+
+ const title = __('Add Comment');
+ const fields = [
+ {
+ fieldname: 'hold_comment',
+ read_only: 0,
+ fieldtype:'Small Text',
+ label: __('Reason For Putting On Hold'),
+ default: ""
+ },
+ ];
+
+ this.dialog = new frappe.ui.Dialog({
+ title: title,
+ fields: fields
+ });
+
+ this.dialog.set_primary_action(__('Save'), function() {
+ const dialog_data = me.dialog.get_values();
+ frappe.call({
+ 'method': 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.block_invoice',
+ 'args': {'name': me.frm.doc.name, 'hold_comment': dialog_data.hold_comment},
+ 'callback': (r) => me.frm.reload_doc()
+ });
+ me.dialog.hide();
+ });
+
+ this.dialog.show();
+ },
+
+ make_dialog_and_set_release_date: function() {
+ const me = this;
+
+ const title = __('Set New Release Date');
+ const fields = [
+ {
+ fieldname: 'release_date',
+ read_only: 0,
+ fieldtype:'Date',
+ label: __('Release Date'),
+ default: me.frm.doc.release_date
+ },
+ ];
+
+ this.dialog = new frappe.ui.Dialog({
+ title: title,
+ fields: fields
+ });
+
+ this.dialog.set_primary_action(__('Save'), function() {
+ me.dialog_data = me.dialog.get_values();
+ if(me.can_change_release_date(me.dialog_data.release_date)) {
+ me.dialog_data.name = me.frm.doc.name;
+ me.set_release_date(me.dialog_data);
+ me.dialog.hide();
+ }
+ });
+
+ this.dialog.show();
+ },
+
+ set_release_date: function(data) {
+ return frappe.call({
+ 'method': 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.change_release_date',
+ 'args': data,
+ 'callback': (r) => this.frm.reload_doc()
+ });
+ },
+
supplier: function() {
var me = this;
if(this.frm.updating_party_details)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 8acdf5c4b84..a8fa9f7529a 100755
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -432,6 +432,165 @@
"translatable": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.on_hold",
+ "columns": 0,
+ "fieldname": "sb_14",
+ "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": "Hold Invoice",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "fieldname": "on_hold",
+ "fieldtype": "Check",
+ "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": "Hold Invoice",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.on_hold",
+ "description": "Once set, this invoice will be on hold till the set date",
+ "fieldname": "release_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": "Release 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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "cb_17",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.on_hold",
+ "fieldname": "hold_comment",
+ "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": "Reason For Putting On Hold",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -4072,7 +4231,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
- "modified": "2018-04-19 15:48:29.457594",
+ "modified": "2018-04-19 15:48:29.457594",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index f6103cc89f1..9599d1f53f9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe, erpnext
-from frappe.utils import cint, formatdate, flt, getdate
+from frappe.utils import cint, cstr, formatdate, flt, getdate, nowdate
from frappe import _, throw
import frappe.defaults
@@ -41,6 +41,13 @@ class PurchaseInvoice(BuyingController):
'overflow_type': 'billing'
}]
+ def before_save(self):
+ if not self.on_hold:
+ self.release_date = ''
+
+ def invoice_is_blocked(self):
+ return self.on_hold and (not self.release_date or self.release_date > getdate(nowdate()))
+
def validate(self):
if not self.is_opening:
self.is_opening = 'No'
@@ -61,6 +68,7 @@ class PurchaseInvoice(BuyingController):
if self._action=="submit" and self.update_stock:
self.make_batches('warehouse')
+ self.validate_release_date()
self.check_conversion_rate()
self.validate_credit_to_acc()
self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
@@ -78,6 +86,10 @@ class PurchaseInvoice(BuyingController):
self.set_status()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
+ def validate_release_date(self):
+ if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
+ frappe.msgprint('Release date must be in the future', raise_exception=True)
+
def validate_cash(self):
if not self.cash_bank_account and flt(self.paid_amount):
frappe.throw(_("Cash or Bank Account is mandatory for making payment entry"))
@@ -730,7 +742,15 @@ class PurchaseInvoice(BuyingController):
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
- def set_tax_withholding(self):
+ def block_invoice(self, hold_comment=None):
+ self.db_set('on_hold', 1)
+ self.db_set('hold_comment', cstr(hold_comment))
+
+ def unblock_invoice(self):
+ self.db_set('on_hold', 0)
+ self.db_set('release_date', None)
+
+ def set_tax_withholding(self):
"""
1. Get TDS Configurations against Supplier
"""
@@ -768,7 +788,28 @@ def make_stock_entry(source_name, target_doc=None):
return doc
+@frappe.whitelist()
+def change_release_date(name, release_date=None):
+ if frappe.db.exists('Purchase Invoice', name):
+ pi = frappe.get_doc('Purchase Invoice', name)
+ pi.db_set('release_date', release_date)
+
+
+@frappe.whitelist()
+def unblock_invoice(name):
+ if frappe.db.exists('Purchase Invoice', name):
+ pi = frappe.get_doc('Purchase Invoice', name)
+ pi.unblock_invoice()
+
+
+@frappe.whitelist()
+def block_invoice(name, hold_comment):
+ if frappe.db.exists('Purchase Invoice', name):
+ pi = frappe.get_doc('Purchase Invoice', name)
+ pi.block_invoice(hold_comment)
+
@frappe.whitelist()
def make_inter_company_sales_invoice(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_invoice
- return make_inter_company_invoice("Purchase Invoice", source_name, target_doc)
\ No newline at end of file
+ return make_inter_company_invoice("Purchase Invoice", source_name, target_doc)
+
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
index 8283acc4f12..4103e57df86 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
@@ -4,12 +4,16 @@
// render
frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
- "currency", "is_return"],
+ "currency", "is_return", "release_date", "on_hold"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
- if(frappe.datetime.get_diff(doc.due_date) < 0) {
+ if(cint(doc.on_hold) && !doc.release_date) {
+ return [__("On Hold"), "darkgrey"];
+ } else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
+ return [__("Temporarily on Hold"), "darkgrey"];
+ } else if(frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due,>=,Today"];
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index a99a86a9936..339d2750b04 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import unittest
import frappe, erpnext
import frappe.model
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days
import frappe.defaults
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
@@ -91,6 +92,106 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertRaises(frappe.LinkExistsError, pi_doc.cancel)
+ def test_purchase_invoice_for_blocked_supplier(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.save()
+
+ self.assertRaises(frappe.ValidationError, make_purchase_invoice)
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_purchase_invoice_for_blocked_supplier_invoice(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Invoices'
+ supplier.save()
+
+ self.assertRaises(frappe.ValidationError, make_purchase_invoice)
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_purchase_invoice_for_blocked_supplier_payment(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.save()
+
+ pi = make_purchase_invoice()
+
+ self.assertRaises(
+ frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_purchase_invoice_for_blocked_supplier_payment_today_date(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.release_date = nowdate()
+ supplier.save()
+
+ pi = make_purchase_invoice()
+
+ self.assertRaises(
+ frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
+ bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_purchase_invoice_for_blocked_supplier_payment_past_date(self):
+ # this test is meant to fail only if something fails in the try block
+ with self.assertRaises(Exception):
+ try:
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.release_date = '2018-03-01'
+ supplier.save()
+
+ pi = make_purchase_invoice()
+
+ get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+ except:
+ pass
+ else:
+ raise Exception
+
+ def test_purchase_invoice_blocked_invoice_must_be_in_future(self):
+ pi = make_purchase_invoice(do_not_save=True)
+ pi.release_date = nowdate()
+
+ self.assertRaises(frappe.ValidationError, pi.save)
+ pi.release_date = ''
+ pi.save()
+
+ def test_purchase_invoice_temporary_blocked(self):
+ pi = make_purchase_invoice(do_not_save=True)
+ pi.release_date = add_days(nowdate(), 10)
+ pi.save()
+ pi.submit()
+
+ pe = get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC")
+
+ self.assertRaises(frappe.ValidationError, pe.save)
+
+ def test_purchase_invoice_explicit_block(self):
+ pi = make_purchase_invoice()
+ pi.block_invoice()
+
+ self.assertEqual(pi.on_hold, 1)
+
+ pi.unblock_invoice()
+
+ self.assertEqual(pi.on_hold, 0)
+
def test_gl_entries_with_perpetual_inventory_against_pr(self):
pr = frappe.copy_doc(pr_test_records[0])
set_perpetual_inventory(1, pr.company)
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 9c7310fb454..8c868871864 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -572,6 +572,22 @@ def get_stock_rbnb_difference(posting_date, company):
return flt(stock_rbnb) + flt(sys_bal)
+def get_held_invoices(party_type, party):
+ """
+ Returns a list of names Purchase Invoices for the given party that are on hold
+ """
+ held_invoices = None
+
+ if party_type == 'Supplier':
+ held_invoices = frappe.db.sql(
+ 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
+ as_dict=1
+ )
+ held_invoices = [d['name'] for d in held_invoices]
+
+ return held_invoices
+
+
def get_outstanding_invoices(party_type, party, account, condition=None):
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount")
@@ -584,7 +600,9 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
payment_dr_or_cr = "payment_gl_entry.debit_in_account_currency - payment_gl_entry.credit_in_account_currency"
invoice = 'Sales Invoice' if erpnext.get_party_account_type(party_type) == 'Receivable' else 'Purchase Invoice'
- invoice_list = frappe.db.sql("""
+ held_invoices = get_held_invoices(party_type, party)
+
+ invoice_list = frappe.db.sql("""
select
voucher_no, voucher_type, posting_date, ifnull(sum({dr_or_cr}), 0) as invoice_amount,
(
@@ -622,20 +640,21 @@ def get_outstanding_invoices(party_type, party, account, condition=None):
}, as_dict=True)
for d in invoice_list:
- due_date = frappe.db.get_value(d.voucher_type, d.voucher_no,
- "posting_date" if party_type == "Employee" else "due_date")
+ if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
+ due_date = 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.append(
+ frappe._dict({
+ 'voucher_no': d.voucher_no,
+ 'voucher_type': d.voucher_type,
+ 'posting_date': d.posting_date,
+ 'invoice_amount': flt(d.invoice_amount),
+ 'payment_amount': flt(d.payment_amount),
+ 'outstanding_amount': flt(d.invoice_amount - d.payment_amount, precision),
+ 'due_date': due_date
+ })
+ )
outstanding_invoices = sorted(outstanding_invoices, key=lambda k: k['due_date'] or getdate(nowdate()))
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 873fc745372..0d46318731b 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
import frappe.defaults
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import flt, add_days, nowdate
from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt, make_purchase_invoice, make_rm_stock_entry as make_subcontract_transfer_entry)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -163,6 +164,77 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertTrue(po.get('payment_schedule'))
+ def test_po_for_blocked_supplier_all(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.save()
+
+ self.assertEqual(supplier.hold_type, 'All')
+ self.assertRaises(frappe.ValidationError, create_purchase_order)
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_po_for_blocked_supplier_invoices(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Invoices'
+ supplier.save()
+
+ self.assertRaises(frappe.ValidationError, create_purchase_order)
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_po_for_blocked_supplier_payments(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.save()
+
+ po = create_purchase_order()
+
+ self.assertRaises(
+ frappe.ValidationError, get_payment_entry, dt='Purchase Order', dn=po.name, bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_po_for_blocked_supplier_payments_with_today_date(self):
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.release_date = nowdate()
+ supplier.hold_type = 'Payments'
+ supplier.save()
+
+ po = create_purchase_order()
+
+ self.assertRaises(
+ frappe.ValidationError, get_payment_entry, dt='Purchase Order', dn=po.name, bank_account="_Test Bank - _TC")
+
+ supplier.on_hold = 0
+ supplier.save()
+
+ def test_po_for_blocked_supplier_payments_past_date(self):
+ # this test is meant to fail only if something fails in the try block
+ with self.assertRaises(Exception):
+ try:
+ supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier.on_hold = 1
+ supplier.hold_type = 'Payments'
+ supplier.release_date = '2018-03-01'
+ supplier.save()
+
+ po = create_purchase_order()
+ get_payment_entry('Purchase Order', po.name, bank_account='_Test Bank - _TC')
+
+ supplier.on_hold = 0
+ supplier.save()
+ except:
+ pass
+ else:
+ raise Exception
+
def test_terms_does_not_copy(self):
po = create_purchase_order()
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index eedbac1dffe..181b214b002 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -108,7 +108,7 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
- "translatable": 0,
+ "translatable": 0,
"unique": 0
},
{
@@ -770,7 +770,135 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
- "translatable": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "cb_21",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "fieldname": "on_hold",
+ "fieldtype": "Check",
+ "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": "Block Supplier",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.on_hold",
+ "fieldname": "hold_type",
+ "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": "Hold Type",
+ "length": 0,
+ "no_copy": 0,
+ "options": "\nAll\nInvoices\nPayments",
+ "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.on_hold",
+ "description": "Leave blank if the Supplier is blocked indefinitely",
+ "fieldname": "release_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": "Release 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,
+ "translatable": 0,
"unique": 0
},
{
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 6aa3b019d59..b6d588ed967 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -10,6 +10,7 @@ from frappe.contacts.address_and_contact import load_address_and_contact, delete
from erpnext.utilities.transaction_base import TransactionBase
from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this
+
class Supplier(TransactionBase):
def get_feed(self):
return self.supplier_name
@@ -19,6 +20,13 @@ class Supplier(TransactionBase):
load_address_and_contact(self)
self.load_dashboard_info()
+ def before_save(self):
+ if not self.on_hold:
+ self.hold_type = ''
+ self.release_date = ''
+ elif self.on_hold and not self.hold_type:
+ self.hold_type = 'All'
+
def load_dashboard_info(self):
info = get_dashboard_info(self.doctype, self.name)
self.set_onload('dashboard_info', info)
@@ -35,7 +43,7 @@ class Supplier(TransactionBase):
self.naming_series = ''
def validate(self):
- #validation for Naming Series mandatory field...
+ # validation for Naming Series mandatory field...
if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series':
if not self.naming_series:
msgprint(_("Series is mandatory"), raise_exception=1)
diff --git a/erpnext/buying/doctype/supplier/supplier_list.js b/erpnext/buying/doctype/supplier/supplier_list.js
index d99e3f8f0f2..c776b001a5a 100644
--- a/erpnext/buying/doctype/supplier/supplier_list.js
+++ b/erpnext/buying/doctype/supplier/supplier_list.js
@@ -1,3 +1,8 @@
frappe.listview_settings['Supplier'] = {
- add_fields: ["supplier_name", "supplier_group", "image"],
+ add_fields: ["supplier_name", "supplier_group", "image", "on_hold"],
+ get_indicator: function(doc) {
+ if(cint(doc.on_hold)) {
+ return [__("On Hold"), "red"];
+ }
+ }
};
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d3deb06b5b7..4802e020942 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _, throw
-from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day
+from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate
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
@@ -36,10 +36,29 @@ class AccountsController(TransactionBase):
if self.doctype in relevant_docs:
self.set_payment_schedule()
+ def ensure_supplier_is_not_blocked(self):
+ is_supplier_payment = self.doctype == 'Payment Entry' and self.party_type == 'Supplier'
+ is_buying_invoice = self.doctype in ['Purchase Invoice', 'Purchase Order']
+ supplier = None
+ supplier_name = None
+
+ if is_buying_invoice or is_supplier_payment:
+ supplier_name = self.supplier if is_buying_invoice else self.party
+ supplier = frappe.get_doc('Supplier', supplier_name)
+
+ if supplier and supplier_name and supplier.on_hold:
+ if (is_buying_invoice and supplier.hold_type in ['All', 'Invoices']) or \
+ (is_supplier_payment and supplier.hold_type in ['All', 'Payments']):
+ if not supplier.release_date or getdate(nowdate()) <= supplier.release_date:
+ frappe.msgprint(
+ _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1)
+
def validate(self):
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
+ self.ensure_supplier_is_not_blocked()
+
self.validate_date_with_fiscal_year()
if self.meta.get_field("currency"):
@@ -969,3 +988,18 @@ def get_due_date(term, posting_date=None, bill_date=None):
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
due_date = add_months(get_last_day(date), term.credit_months)
return due_date
+
+
+def get_supplier_block_status(party_name):
+ """
+ Returns a dict containing the values of `on_hold`, `release_date` and `hold_type` of
+ a `Supplier`
+ """
+ supplier = frappe.get_doc('Supplier', party_name)
+ info = {
+ 'on_hold': supplier.on_hold,
+ 'release_date': supplier.release_date,
+ 'hold_type': supplier.hold_type
+ }
+ return info
+
diff --git a/erpnext/docs/assets/img/accounts/purchase-invoice-hold.png b/erpnext/docs/assets/img/accounts/purchase-invoice-hold.png
new file mode 100644
index 00000000000..e87f9f58523
Binary files /dev/null and b/erpnext/docs/assets/img/accounts/purchase-invoice-hold.png differ
diff --git a/erpnext/docs/user/manual/en/accounts/purchase-invoice.md b/erpnext/docs/user/manual/en/accounts/purchase-invoice.md
index 8076bd84191..122dfa3ced8 100644
--- a/erpnext/docs/user/manual/en/accounts/purchase-invoice.md
+++ b/erpnext/docs/user/manual/en/accounts/purchase-invoice.md
@@ -92,4 +92,42 @@ every transaction.
For more help, please contact your Accountant!
+#### Hold Payments For A Purchase Invoice
+There are two ways to put a purchase invoice on hold:
+- Date Span Hold
+- Explicit Hold
+
+##### Explicit Hold
+Explicit hold holds the purchase invoice indefinitely.
+To do it, in the "Hold Invoice" section of the purchase invoice form, simply
+check the "Hold Invoice" checkbox. In the "Reason For Putting On Hold" text
+field, type a comment explaining why the invoice is to be put on hold.
+
+If you need to hold a submitted invoice, click the "Make" drop down button
+and click "Block Invoice". Also add a comment explaining why the invoice is
+to be put on hold in the dialog that pops up and click "Save".
+
+##### Date Span Hold
+Date span hold holds the purchase invoice until a
+specified date. To do it, in the "Hold Invoice" section of the purchase
+invoice form, check the "Hold Invoice" checkbox. Next, input the release date
+in the dialog that pops up and click "Save". The release date is the date
+that the hold on the document expires.
+
+After the invoice has been saved, you can change the release date by clicking
+on the "Hold Invoice" drop down button and then "Change Release Date". This
+action will cause a dialog to appear.
+
+
+
+Select the new release date and click "Save". You should also enter a comment
+in the "Reason For Putting On Hold" field.
+
+Take note of the following:
+- All purchases that have been placed on hold will not included in a Payment Entry's references table
+- The release date cannot be in the past.
+- You can only block or unblock a purchase invoice if it is unpaid.
+- You can only change the release date if the invoice is unpaid.
+
+
{next}
diff --git a/erpnext/docs/user/manual/en/buying/supplier.md b/erpnext/docs/user/manual/en/buying/supplier.md
index baf0fed525c..f116c889e03 100644
--- a/erpnext/docs/user/manual/en/buying/supplier.md
+++ b/erpnext/docs/user/manual/en/buying/supplier.md
@@ -38,7 +38,20 @@ If you don't want to customize payable account, and proceed with default payable
You can add multiple companies in your ERPNext instance, and one Supplier can be used across multiple companies. In this case, you should define Companywise Payable Account for the Supplier in the "Default Payable Accounts" table.
-
(Check from 2:20)
+### Place Supplier On Hold
+In the Supplier form, check the "Block Supplier" checkbox. Next, choose the "Hold Type".
+
+The hold types are as follows:
+- Invoices: ERPNext will not allow Purchase Invoices or Purchase Orders to be created for the supplier
+- Payments: ERPNext will not allow Payment Entries to be created for the Supplier
+- All: ERPNext will apply both hold types above
+
+After selecting the hold type, you can optionally set a release date in the "Release Date" field.
+
+Take note of the following:
+- If you do not select a hold type, ERPNext will set it to "All"
+- If you do not set a release date, ERPNext will hold the Supplier indefinitely
+
{next}