[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:
parent
3639b85663
commit
7048925016
@ -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"));
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
||||
|
@ -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'])
|
||||
|
BIN
erpnext/docs/assets/img/accounts/credit-note.png
Normal file
BIN
erpnext/docs/assets/img/accounts/credit-note.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
BIN
erpnext/docs/assets/img/accounts/credit_note_example1.gif
Normal file
BIN
erpnext/docs/assets/img/accounts/credit_note_example1.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 MiB |
BIN
erpnext/docs/assets/img/accounts/debit-note.png
Normal file
BIN
erpnext/docs/assets/img/accounts/debit-note.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
BIN
erpnext/docs/assets/img/accounts/debit_note_example1.gif
Normal file
BIN
erpnext/docs/assets/img/accounts/debit_note_example1.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 MiB |
20
erpnext/docs/user/manual/en/accounts/credit-note.md
Normal file
20
erpnext/docs/user/manual/en/accounts/credit-note.md
Normal 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">
|
19
erpnext/docs/user/manual/en/accounts/debit-note.md
Normal file
19
erpnext/docs/user/manual/en/accounts/debit-note.md
Normal 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">
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user