Ability to hold payment for disputed invoices #12048 (#13298)

* add new fields to Supplier Master:
- on_hold: To signal the Customer is blocked from completing certain transactions
- hold_type: 3 options - All, invoices and payments

* sanitize `on_hold` field input

* show hold status in list view

* add `release_date` field to Supplier Master:
- specifies the date when transaction restraint will be removed

* reset release date if supplier is not on hold

* add validation to stop transactions when Supplier is blocked

* add test cases

* return empty list for outstanding references if supplier is blocked

* block make button:payment if supplier is blocked

* adjust test cases

* PEP 8 clean up

* more tests

* adds new fields to Purchase Invoice:
- release_date: once set, invoice will be on hold until set date
- hold_comment: so user can add comment pertaining to why invoice is on hold

* implement individual purchase invoice on hold logic

* allow user to change release date

* update manual

* final cleanup including more validation and tests

* update supplier manual

* make default for release_date argument todays date

* remove Auto Repeat added by mistake

* add on_hold_field to purchase invoice

* add 'On Hold' or 'Temporarily on Hold' status for purchase invoice in list view

* implement explicit payment hold in purchase invoice

* update manual

* add dialog for saving comment

* bug fix, refactor, clean up

* more test cases
This commit is contained in:
tundebabzy 2018-05-16 07:01:41 +01:00 committed by Rushabh Mehta
parent 1906cadd94
commit ad08d4ce96
16 changed files with 865 additions and 38 deletions

View File

@ -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,6 +883,10 @@ 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")
# 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,

View File

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

View File

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

View File

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

View File

@ -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,6 +742,14 @@ class PurchaseInvoice(BuyingController):
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
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)

View File

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

View File

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

View File

@ -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,6 +600,8 @@ 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'
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,8 +640,9 @@ 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({

View File

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

View File

@ -773,6 +773,134 @@
"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
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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.
<img class="screenshot" alt="Purchase Invoice on hold" src="{{docs_base_url}}/assets/img/accounts/purchase-invoice-hold.png">
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}

View File

@ -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.
<iframe width="660" height="371" src="https://www.youtube.com/embed/anoGi_RpQ20" frameborder="0" allowfullscreen></iframe>
(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}