[Enhance] Standalone debit/credit note (#14269)

* [Enhance] Standalone debit/credit note

* Test cases

* Test cases and documentation

* Removed credit, debit note links from accounts module
This commit is contained in:
rohitwaghchaure 2018-06-11 12:02:14 +05:30 committed by Nabin Hait
parent 3639b85663
commit 7048925016
20 changed files with 519 additions and 122 deletions

View File

@ -59,12 +59,13 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}
}
if(!doc.is_return && doc.docstatus==1) {
if(doc.outstanding_amount != 0) {
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __("Make"));
cur_frm.page.set_inner_btn_group_as_primary(__("Make"));
}
if(doc.docstatus == 1 && doc.outstanding_amount != 0
&& !(doc.is_return && doc.return_against)) {
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __("Make"));
cur_frm.page.set_inner_btn_group_as_primary(__("Make"));
}
if(!doc.is_return && doc.docstatus==1) {
if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
cur_frm.add_custom_button(__('Return / Debit Note'),
this.make_debit_note, __("Make"));

View File

@ -334,7 +334,7 @@ class PurchaseInvoice(BuyingController):
if update_outstanding == "No":
update_outstanding_amt(self.credit_to, "Supplier", self.supplier,
self.doctype, self.return_against if cint(self.is_return) else self.name)
self.doctype, self.return_against if cint(self.is_return and self.return_against) else self.name)
if repost_future_gle and cint(self.update_stock) and self.auto_accounting_for_stock:
from erpnext.controllers.stock_controller import update_gl_entries_after
@ -379,7 +379,7 @@ class PurchaseInvoice(BuyingController):
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
}, self.party_account_currency)
)
@ -618,7 +618,7 @@ class PurchaseInvoice(BuyingController):
"debit": self.base_paid_amount,
"debit_in_account_currency": self.base_paid_amount \
if self.party_account_currency==self.company_currency else self.paid_amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
}, self.party_account_currency)
)
@ -648,7 +648,7 @@ class PurchaseInvoice(BuyingController):
"debit": self.base_write_off_amount,
"debit_in_account_currency": self.base_write_off_amount \
if self.party_account_currency==self.company_currency else self.write_off_amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
}, self.party_account_currency)
)

View File

@ -765,6 +765,31 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, pi.insert)
def test_debit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount
pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
outstanding_amount = get_outstanding_amount(pi.doctype,
pi.name, "Creditors - _TC", pi.supplier, "Supplier")
self.assertEqual(pi.outstanding_amount, outstanding_amount)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_from_account_currency = pi.currency
pe.paid_to_account_currency = pi.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.paid_amount = pi.grand_total * -1
pe.insert()
pe.submit()
pi_doc = frappe.get_doc('Purchase Invoice', pi.name)
self.assertEqual(pi_doc.outstanding_amount, 0)
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

@ -48,6 +48,13 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
if(doc.update_stock) this.show_stock_ledger();
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {
cur_frm.add_custom_button(__('Payment'),
this.make_payment_entry, __("Make"));
cur_frm.page.set_inner_btn_group_as_primary(__("Make"));
}
if(doc.docstatus==1 && !doc.is_return) {
var is_delivered_by_supplier = false;
@ -76,11 +83,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
}
}
if(doc.outstanding_amount!=0 && !cint(doc.is_return)) {
cur_frm.add_custom_button(__('Payment'),
this.make_payment_entry, __("Make"));
}
if(doc.outstanding_amount>0 && !cint(doc.is_return)) {
cur_frm.add_custom_button(__('Payment Request'),
this.make_payment_request, __("Make"));

File diff suppressed because it is too large Load Diff

View File

@ -613,7 +613,7 @@ class SalesInvoice(SellingController):
if update_outstanding == "No":
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
update_outstanding_amt(self.debit_to, "Customer", self.customer,
self.doctype, self.return_against if cint(self.is_return) else self.name)
self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name)
if repost_future_gle and cint(self.update_stock) \
and cint(auto_accounting_for_stock):
@ -662,7 +662,7 @@ class SalesInvoice(SellingController):
"debit": grand_total_in_company_currency,
"debit_in_account_currency": grand_total_in_company_currency \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype
}, self.party_account_currency)
)
@ -729,7 +729,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount \
if self.party_account_currency==self.company_currency \
else payment_mode.amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
}, self.party_account_currency)
)
@ -758,7 +758,7 @@ class SalesInvoice(SellingController):
"debit": flt(self.base_change_amount),
"debit_in_account_currency": flt(self.base_change_amount) \
if self.party_account_currency==self.company_currency else flt(self.change_amount),
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype
}, self.party_account_currency)
)
@ -788,7 +788,7 @@ class SalesInvoice(SellingController):
"credit": self.base_write_off_amount,
"credit_in_account_currency": self.base_write_off_amount \
if self.party_account_currency==self.company_currency else self.write_off_amount,
"against_voucher": self.return_against if cint(self.is_return) else self.name,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype
}, self.party_account_currency)
)

View File

@ -1414,6 +1414,29 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.insert)
def test_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
outstanding_amount = get_outstanding_amount(si.doctype,
si.name, "Debtors - _TC", si.customer, "Customer")
self.assertEqual(si.outstanding_amount, outstanding_amount)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_from_account_currency = si.currency
pe.paid_to_account_currency = si.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.paid_amount = si.grand_total * -1
pe.insert()
pe.submit()
si_doc = frappe.get_doc('Sales Invoice', si.name)
self.assertEqual(si_doc.outstanding_amount, 0)
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
@ -1456,3 +1479,16 @@ def create_sales_invoice(**args):
test_dependencies = ["Journal Entry", "Contact", "Address"]
test_records = frappe.get_test_records('Sales Invoice')
def get_outstanding_amount(against_voucher_type, against_voucher, account, party, party_type):
bal = flt(frappe.db.sql("""
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where against_voucher_type=%s and against_voucher=%s
and account = %s and party = %s and party_type = %s""",
(against_voucher_type, against_voucher, account, party, party_type))[0][0] or 0.0)
if against_voucher_type == 'Purchase Invoice':
bal = bal * -1
return bal

View File

@ -82,6 +82,9 @@ class AccountsController(TransactionBase):
if self.doctype == 'Purchase Invoice':
self.validate_paid_amount()
if self.doctype in ['Purchase Invoice', 'Sales Invoice'] and self.is_return:
self.validate_qty()
def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates()
self.set_due_date()

View File

@ -12,41 +12,39 @@ def validate_return(doc):
if not doc.meta.get_field("is_return") or not doc.is_return:
return
validate_return_against(doc)
validate_returned_items(doc)
if doc.return_against:
validate_return_against(doc)
validate_returned_items(doc)
def validate_return_against(doc):
if not doc.return_against:
frappe.throw(_("{0} is mandatory for Return").format(doc.meta.get_label("return_against")))
filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company}
if doc.meta.get_field("customer") and doc.customer:
filters["customer"] = doc.customer
elif doc.meta.get_field("supplier") and doc.supplier:
filters["supplier"] = doc.supplier
if not frappe.db.exists(filters):
frappe.throw(_("Invalid {0}: {1}")
.format(doc.meta.get_label("return_against"), doc.return_against))
else:
filters = {"doctype": doc.doctype, "docstatus": 1, "company": doc.company}
if doc.meta.get_field("customer") and doc.customer:
filters["customer"] = doc.customer
elif doc.meta.get_field("supplier") and doc.supplier:
filters["supplier"] = doc.supplier
ref_doc = frappe.get_doc(doc.doctype, doc.return_against)
if not frappe.db.exists(filters):
frappe.throw(_("Invalid {0}: {1}")
.format(doc.meta.get_label("return_against"), doc.return_against))
else:
ref_doc = frappe.get_doc(doc.doctype, doc.return_against)
# validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00")
# validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
ref_posting_datetime = "%s %s" % (ref_doc.posting_date, ref_doc.get("posting_time") or "00:00:00")
if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime):
frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)))
if get_datetime(return_posting_datetime) < get_datetime(ref_posting_datetime):
frappe.throw(_("Posting timestamp must be after {0}").format(format_datetime(ref_posting_datetime)))
# validate same exchange rate
if doc.conversion_rate != ref_doc.conversion_rate:
frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})")
.format(doc.doctype, doc.return_against, ref_doc.conversion_rate))
# validate same exchange rate
if doc.conversion_rate != ref_doc.conversion_rate:
frappe.throw(_("Exchange Rate must be same as {0} {1} ({2})")
.format(doc.doctype, doc.return_against, ref_doc.conversion_rate))
# validate update stock
if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}")
.format(doc.return_against))
# validate update stock
if doc.doctype == "Sales Invoice" and doc.update_stock and not ref_doc.update_stock:
frappe.throw(_("'Update Stock' can not be checked because items are not delivered via {0}")
.format(doc.return_against))
def validate_returned_items(doc):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View File

@ -159,6 +159,9 @@ class StatusUpdater(Document):
if hasattr(d, 'qty') and d.qty < 0 and not self.get('is_return'):
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
if hasattr(d, 'qty') and d.qty > 0 and self.get('is_return'):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
if d.doctype == args['source_dt'] and d.get(args["join_field"]):
args['name'] = d.get(args['join_field'])

View File

@ -450,7 +450,7 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice":
self.calculate_paid_amount()
if self.doc.is_return: return
if self.doc.is_return and self.doc.return_against: return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
self._set_in_company_currency(self.doc, ['write_off_amount'])

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@ -0,0 +1,20 @@
## Credit Note
A credit note is a document sent by a seller to the customer, notifying that a credit has been made to their account against the goods returned by the buyer.
A credit note is issued for the value of goods returned by the customer, it may be less than or equal to total amount of the order.
##### How to make credit note in ERPNext
The user can make a credit note against the sales invoice or they can directly make credit note from the sales invoice without reference
Goto module Accounts > Sales Invoice > New > Manually enabled Is Return checkbox
<img class="screenshot" alt="Sales Invoice" src="{{docs_base_url}}/assets/img/accounts/credit-note.png">
Note: For credit note set the negative quantity while adding the item
#### Example
Customer Sagar Malhotra has purchased Nokia Lumia worth Rs 42,400 and at the time of delivery customer were found that the piece has been damaged. Now sagar has returned the product and he got his money back.
Credit note with payment entry in ERPNext for above example is as below
<img class="screenshot" alt="Sales Invoice" src="{{docs_base_url}}/assets/img/accounts/credit_note_example1.gif">

View File

@ -0,0 +1,19 @@
## Debit Note
A debit note is a document sent by a buyer to a seller, or in other words, a purchaser to a vendor while returning goods received on credit. This notifies that a debit has been made to their accounts.
A debit note is issued for the value of the goods returned. In some cases, sellers are seen sending debit notes which should be treated as just another invoice.
##### How to make debit note in ERPNext
The user can make a debit note against the purchase invoice or they can directly make debit note from the purchase invoice without reference
Goto module Accounts > Purchase Invoice > New > Manually enabled Is Return checkbox
<img class="screenshot" alt="Sales Invoice" src="{{docs_base_url}}/assets/img/accounts/debit-note.png">
Note: For debit note set the negative quantity while adding the item
#### Example
Debit note with payment entry in ERPNext
<img class="screenshot" alt="Sales Invoice" src="{{docs_base_url}}/assets/img/accounts/debit_note_example1.gif">

View File

@ -3,7 +3,9 @@ opening-accounts
sales-invoice
point-of-sale-pos-invoice
point-of-sales
credit-note
purchase-invoice
debit-note
inter-company-invoices
payments
journal-entry

View File

@ -37,6 +37,7 @@ def make_item(item_code, properties=None):
if item.is_stock_item:
for item_default in [doc for doc in item.get("item_defaults") if not doc.default_warehouse]:
item_default.default_warehouse = "_Test Warehouse - _TC"
item_default.company = "_Test Company"
item.insert()
return item