diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f7a71223436..455ab861f93 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -42,6 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
-bench start &
+bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 4bc55da1d8f..412a05b0a15 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -102,3 +102,7 @@ jobs:
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+
+ - name: Show bench console if tests failed
+ if: ${{ failure() }}
+ run: cat ~/frappe-bench/bench_run_logs.txt
diff --git a/.gitignore b/.gitignore
index 63c51c49765..89f56263ffa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ __pycache__
.idea/
.vscode/
node_modules/
+.backportrc.json
\ No newline at end of file
diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js
new file mode 100644
index 00000000000..fcb7533a225
--- /dev/null
+++ b/cypress/integration/test_item.js
@@ -0,0 +1,44 @@
+describe("Test Item Dashboard", () => {
+ before(() => {
+ cy.login();
+ cy.visit("/app/item");
+ cy.insert_doc(
+ "Item",
+ {
+ item_code: "e2e_test_item",
+ item_group: "All Item Groups",
+ opening_stock: 42,
+ valuation_rate: 100,
+ },
+ true
+ );
+ cy.go_to_doc("item", "e2e_test_item");
+ });
+
+ it("should show dashboard with correct data on first load", () => {
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").contains("e2e_test_item").should("exist");
+
+ // reserved and available qty
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(0)
+ .contains("0")
+ .should("exist");
+ cy.get(".stock-levels .inline-graph-count")
+ .eq(1)
+ .contains("42")
+ .should("exist");
+ });
+
+ it("should persist on field change", () => {
+ cy.get('input[data-fieldname="disabled"]').check();
+ cy.wait(500);
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ cy.get(".stock-levels").should("have.length", 1);
+ });
+
+ it("should persist on reload", () => {
+ cy.reload();
+ cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
+ });
+});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 7929a2e0ef5..7ddc80ab8dd 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -23,3 +23,9 @@
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
+
+const slug = (name) => name.toLowerCase().replace(" ", "-");
+
+Cypress.Commands.add("go_to_doc", (doctype, name) => {
+ cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
+});
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 3b764aab103..cb1f2df7f01 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -13,7 +13,9 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document):
- pass
+ def validate(self):
+ validate_accounts(self.import_file)
+
@frappe.whitelist()
def validate_company(company):
@@ -301,28 +303,28 @@ def validate_accounts(file_name):
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1
- message = validate_root(accounts_dict)
- if message: return message
- message = validate_account_types(accounts_dict)
- if message: return message
+ validate_root(accounts_dict)
+
+ validate_account_types(accounts_dict)
+
return [True, len(accounts)]
def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4:
- return _("Number of root accounts cannot be less than 4")
+ frappe.throw(_("Number of root accounts cannot be less than 4"))
error_messages = []
for account in roots:
if not account.get("root_type") and account.get("account_name"):
- error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
+ error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
- error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
+ error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
if error_messages:
- return "
".join(error_messages)
+ frappe.throw("
".join(error_messages))
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -356,7 +358,7 @@ def validate_account_types(accounts):
missing = list(set(account_types_for_ledger) - set(account_types))
if missing:
- return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))
+ frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug
@@ -364,7 +366,7 @@ def validate_account_types(accounts):
missing = list(set(account_types_for_group) - set(account_groups))
if missing:
- return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))
+ frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index c6c689212b6..1ef512a4894 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -25,7 +25,7 @@ class Dunning(AccountsController):
def validate_amount(self):
amounts = calculate_interest_and_amount(
- self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
+ self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
if self.interest_amount != amounts.get('interest_amount'):
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
if self.dunning_amount != amounts.get('dunning_amount'):
@@ -91,13 +91,13 @@ def resolve_dunning(doc, state):
for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
-def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
+def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0
- grand_total = 0
+ grand_total = flt(outstanding_amount) + flt(dunning_fee)
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365
- grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
+ grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
'interest_amount': interest_amount,
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index e2d4d82e418..31cb078cd4f 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase):
@classmethod
def setUpClass(self):
create_dunning_type()
+ create_dunning_type_with_zero_interest_rate()
unlink_payment_on_cancel_of_invoice()
@classmethod
@@ -25,11 +26,19 @@ class TestDunning(unittest.TestCase):
def test_dunning(self):
dunning = create_dunning()
amounts = calculate_interest_and_amount(
- dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
+ def test_dunning_with_zero_interest_rate(self):
+ dunning = create_dunning_with_zero_interest_rate()
+ amounts = calculate_interest_and_amount(
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
+ self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
+ self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
+ self.assertEqual(round(amounts.get('grand_total'), 2), 120)
+
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()
@@ -83,6 +92,27 @@ def create_dunning():
dunning.save()
return dunning
+def create_dunning_with_zero_interest_rate():
+ posting_date = add_days(today(), -20)
+ due_date = add_days(today(), -15)
+ sales_invoice = create_sales_invoice_against_cost_center(
+ posting_date=posting_date, due_date=due_date, status='Overdue')
+ dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
+ dunning = frappe.new_doc("Dunning")
+ dunning.sales_invoice = sales_invoice.name
+ dunning.customer_name = sales_invoice.customer_name
+ dunning.outstanding_amount = sales_invoice.outstanding_amount
+ dunning.debit_to = sales_invoice.debit_to
+ dunning.currency = sales_invoice.currency
+ dunning.company = sales_invoice.company
+ dunning.posting_date = nowdate()
+ dunning.due_date = sales_invoice.due_date
+ dunning.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning.rate_of_interest = dunning_type.rate_of_interest
+ dunning.dunning_fee = dunning_type.dunning_fee
+ dunning.save()
+ return dunning
+
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice'
@@ -98,3 +128,19 @@ def create_dunning_type():
}
)
dunning_type.save()
+
+def create_dunning_type_with_zero_interest_rate():
+ dunning_type = frappe.new_doc("Dunning Type")
+ dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning_type.start_day = 10
+ dunning_type.end_day = 20
+ dunning_type.dunning_fee = 20
+ dunning_type.rate_of_interest = 0
+ dunning_type.append(
+ "dunning_letter_text", {
+ 'language': 'en',
+ 'body_text': 'We have still not received payment for our invoice ',
+ 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
+ }
+ )
+ dunning_type.save()
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index adaf99a7900..ff00fde523f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -183,6 +183,13 @@ class PaymentEntry(AccountsController):
d.reference_name, self.party_account_currency)
for field, value in iteritems(ref_details):
+ if d.exchange_gain_loss:
+ # for cases where gain/loss is booked into invoice
+ # exchange_gain_loss is calculated from invoice & populated
+ # and row.exchange_rate is already set to payment entry's exchange rate
+ # refer -> `update_reference_in_payment_entry()` in utils.py
+ continue
+
if field == 'exchange_rate' or not d.get(field) or force:
d.db_set(field, value)
@@ -664,8 +671,8 @@ class PaymentEntry(AccountsController):
gl_entries.append(gle)
if self.unallocated_amount:
- base_unallocated_amount = self.unallocated_amount * \
- (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate)
+ exchange_rate = self.get_exchange_rate()
+ base_unallocated_amount = (self.unallocated_amount * exchange_rate)
gle = party_gl_dict.copy()
@@ -806,10 +813,17 @@ class PaymentEntry(AccountsController):
if account_details:
row.update(account_details)
+
+ if not row.get('amount'):
+ # if no difference amount
+ return
self.append('deductions', row)
self.set_unallocated_amount()
+ def get_exchange_rate(self):
+ return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
+
def initialize_taxes(self):
for tax in self.get("taxes"):
validate_taxes_and_charges(tax)
@@ -1318,9 +1332,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
return frappe._dict({
"due_date": ref_doc.get("due_date"),
- "total_amount": total_amount,
- "outstanding_amount": outstanding_amount,
- "exchange_rate": exchange_rate,
+ "total_amount": flt(total_amount),
+ "outstanding_amount": flt(outstanding_amount),
+ "exchange_rate": flt(exchange_rate),
"bill_no": bill_no
})
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 4641d6b5ffa..d1302f5ae78 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
self.assertEqual(pe.cost_center, si.cost_center)
- self.assertEqual(expected_account_balance, account_balance)
- self.assertEqual(expected_party_balance, party_balance)
- self.assertEqual(expected_party_account_balance, party_account_balance)
+ self.assertEqual(flt(expected_account_balance), account_balance)
+ self.assertEqual(flt(expected_party_balance), party_balance)
+ self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template():
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 912ad0977a2..43eb0b6e2aa 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -14,7 +14,8 @@
"total_amount",
"outstanding_amount",
"allocated_amount",
- "exchange_rate"
+ "exchange_rate",
+ "exchange_gain_loss"
],
"fields": [
{
@@ -90,12 +91,19 @@
"fieldtype": "Link",
"label": "Payment Term",
"options": "Payment Term"
+ },
+ {
+ "fieldname": "exchange_gain_loss",
+ "fieldtype": "Currency",
+ "label": "Exchange Gain/Loss",
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-10 11:25:47.144392",
+ "modified": "2021-04-21 13:30:11.605388",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c1cc092554d..b99d75ec496 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -451,6 +451,7 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 189260a29da..db6f143eb80 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -974,6 +974,120 @@ class TestPurchaseInvoice(unittest.TestCase):
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
+ def test_gain_loss_with_advance_entry(self):
+ unlink_enabled = frappe.db.get_value(
+ "Accounts Settings", "Accounts Settings",
+ "unlink_payment_on_cancel_of_invoice")
+
+ frappe.db.set_value(
+ "Accounts Settings", "Accounts Settings",
+ "unlink_payment_on_cancel_of_invoice", 1)
+
+ original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
+ frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
+
+ pay = frappe.get_doc({
+ 'doctype': 'Payment Entry',
+ 'company': '_Test Company',
+ 'payment_type': 'Pay',
+ 'party_type': 'Supplier',
+ 'party': '_Test Supplier USD',
+ 'paid_to': '_Test Payable USD - _TC',
+ 'paid_from': 'Cash - _TC',
+ 'paid_amount': 70000,
+ 'target_exchange_rate': 70,
+ 'received_amount': 1000,
+ })
+ pay.insert()
+ pay.submit()
+
+ pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
+ conversion_rate=75, rate=500, do_not_save=1, qty=1)
+ pi.cost_center = "_Test Cost Center - _TC"
+ pi.advances = []
+ pi.append("advances", {
+ "reference_type": "Payment Entry",
+ "reference_name": pay.name,
+ "advance_amount": 1000,
+ "remarks": pay.remarks,
+ "allocated_amount": 500,
+ "ref_exchange_rate": 70
+ })
+ pi.save()
+ pi.submit()
+
+ expected_gle = [
+ ["_Test Account Cost for Goods Sold - _TC", 37500.0],
+ ["_Test Payable USD - _TC", -40000.0],
+ ["Exchange Gain/Loss - _TC", 2500.0]
+ ]
+
+ gl_entries = frappe.db.sql("""
+ select account, sum(debit - credit) as balance from `tabGL Entry`
+ where voucher_no=%s
+ group by account
+ order by account asc""", (pi.name), as_dict=1)
+
+ for i, gle in enumerate(gl_entries):
+ self.assertEqual(expected_gle[i][0], gle.account)
+ self.assertEqual(expected_gle[i][1], gle.balance)
+
+ pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
+ conversion_rate=73, rate=500, do_not_save=1, qty=1)
+ pi_2.cost_center = "_Test Cost Center - _TC"
+ pi_2.advances = []
+ pi_2.append("advances", {
+ "reference_type": "Payment Entry",
+ "reference_name": pay.name,
+ "advance_amount": 500,
+ "remarks": pay.remarks,
+ "allocated_amount": 500,
+ "ref_exchange_rate": 70
+ })
+ pi_2.save()
+ pi_2.submit()
+
+ expected_gle = [
+ ["_Test Account Cost for Goods Sold - _TC", 36500.0],
+ ["_Test Payable USD - _TC", -38000.0],
+ ["Exchange Gain/Loss - _TC", 1500.0]
+ ]
+
+ gl_entries = frappe.db.sql("""
+ select account, sum(debit - credit) as balance from `tabGL Entry`
+ where voucher_no=%s
+ group by account order by account asc""", (pi_2.name), as_dict=1)
+
+ for i, gle in enumerate(gl_entries):
+ self.assertEqual(expected_gle[i][0], gle.account)
+ self.assertEqual(expected_gle[i][1], gle.balance)
+
+ expected_gle = [
+ ["_Test Payable USD - _TC", 70000.0],
+ ["Cash - _TC", -70000.0]
+ ]
+
+ gl_entries = frappe.db.sql("""
+ select account, sum(debit - credit) as balance from `tabGL Entry`
+ where voucher_no=%s and is_cancelled=0
+ group by account order by account asc""", (pay.name), as_dict=1)
+
+ for i, gle in enumerate(gl_entries):
+ self.assertEqual(expected_gle[i][0], gle.account)
+ self.assertEqual(expected_gle[i][1], gle.balance)
+
+ pi.reload()
+ pi.cancel()
+
+ pi_2.reload()
+ pi_2.cancel()
+
+ pay.reload()
+ pay.cancel()
+
+ frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled)
+ frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
+
def test_purchase_invoice_advance_taxes(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
index 5801b17f66f..63dfff8921f 100644
--- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
+++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json
@@ -1,235 +1,127 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-03-08 15:36:46",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2013-03-08 15:36:46",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "remarks",
+ "reference_row",
+ "col_break1",
+ "advance_amount",
+ "allocated_amount",
+ "exchange_gain_loss",
+ "ref_exchange_rate"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Type",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "journal_voucher",
- "oldfieldtype": "Link",
- "options": "DocType",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "180px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "no_copy": 1,
+ "oldfieldname": "journal_voucher",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "print_width": "180px",
+ "read_only": 1,
"width": "180px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "reference_name",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Reference Name",
- "length": 0,
- "no_copy": 1,
- "options": "reference_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 3,
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "no_copy": 1,
+ "options": "reference_type",
+ "read_only": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "remarks",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Remarks",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "remarks",
- "oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "150px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 3,
+ "fieldname": "remarks",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Remarks",
+ "no_copy": 1,
+ "oldfieldname": "remarks",
+ "oldfieldtype": "Small Text",
+ "print_width": "150px",
+ "read_only": 1,
"width": "150px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_row",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Row",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "jv_detail_no",
- "oldfieldtype": "Date",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "print_width": "80px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_row",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Reference Row",
+ "no_copy": 1,
+ "oldfieldname": "jv_detail_no",
+ "oldfieldtype": "Date",
+ "print_hide": 1,
+ "print_width": "80px",
+ "read_only": 1,
"width": "80px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "col_break1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "col_break1",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "advance_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Advance Amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "advance_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "advance_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Advance Amount",
+ "no_copy": 1,
+ "oldfieldname": "advance_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "100px",
+ "read_only": 1,
"width": "100px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "allocated_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Allocated Amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "allocated_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "100px",
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated Amount",
+ "no_copy": 1,
+ "oldfieldname": "allocated_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "100px",
"width": "100px"
+ },
+ {
+ "fieldname": "exchange_gain_loss",
+ "fieldtype": "Currency",
+ "label": "Exchange Gain/Loss",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "ref_exchange_rate",
+ "fieldtype": "Float",
+ "label": "Reference Exchange Rate",
+ "non_negative": 1,
+ "read_only": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2016-08-26 02:30:54.407138",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Purchase Invoice Advance",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-20 16:26:53.820530",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Purchase Invoice Advance",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 55a5b99907b..88899130a24 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.assets.doctype.asset.depreciation \
- import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal
+ import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no
from erpnext.setup.doctype.company.company import update_company_current_month_sales
@@ -149,7 +149,7 @@ class SalesInvoice(SellingController):
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
- elif asset.status in ("Scrapped", "Cancelled", "Sold"):
+ elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
def validate_item_cost_centers(self):
@@ -840,6 +840,7 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
@@ -917,22 +918,33 @@ class SalesInvoice(SellingController):
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
if item.is_fixed_asset:
- asset = frappe.get_doc("Asset", item.asset)
-
+ if item.get('asset'):
+ asset = frappe.get_doc("Asset", item.asset)
+ else:
+ frappe.throw(_(
+ "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
+ title=_("Missing Asset")
+ )
if (len(asset.finance_books) > 1 and not item.finance_book
and asset.finance_books[0].finance_book):
frappe.throw(_("Select finance book for the item {0} at row {1}")
.format(item.item_code, item.idx))
- fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
- item.base_net_amount, item.finance_book)
+ if self.is_return:
+ fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
+ item.base_net_amount, item.finance_book)
+ asset.db_set("disposal_date", None)
+ else:
+ fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
+ item.base_net_amount, item.finance_book)
+ asset.db_set("disposal_date", self.posting_date)
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
- asset.db_set("disposal_date", self.posting_date)
- asset.set_status("Sold" if self.docstatus==1 else None)
+ self.set_asset_status(asset)
+
else:
# Do not book income for transfer within same company
if not self.is_internal_transfer():
@@ -958,6 +970,12 @@ class SalesInvoice(SellingController):
erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super(SalesInvoice, self).get_gl_entries()
+ def set_asset_status(self, asset):
+ if self.is_return:
+ asset.set_status()
+ else:
+ asset.set_status("Sold" if self.docstatus==1 else None)
+
def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points):
gl_entries.append(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index fe531d3b227..6dc2767b701 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -10,6 +10,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@@ -1069,6 +1070,36 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(si1.outstanding_amount)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
+ def test_gle_made_when_asset_is_returned(self):
+ create_asset_data()
+ asset = create_asset(item_code="Macbook Pro")
+
+ si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000)
+ return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000)
+
+ disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account")
+
+ # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000
+ loss_for_si = frappe.get_all(
+ "GL Entry",
+ filters = {
+ "voucher_no": si.name,
+ "account": disposal_account
+ },
+ fields = ["credit", "debit"]
+ )[0]
+
+ loss_for_return_si = frappe.get_all(
+ "GL Entry",
+ filters = {
+ "voucher_no": return_si.name,
+ "account": disposal_account
+ },
+ fields = ["credit", "debit"]
+ )[0]
+
+ self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit'])
+ self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit'])
def test_discount_on_net_total(self):
si = frappe.copy_doc(test_records[2])
@@ -2087,9 +2118,9 @@ def make_sales_invoice_for_ewaybill():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
- "cgst_account": "CGST - _TC",
- "sgst_account": "SGST - _TC",
- "igst_account": "IGST - _TC",
+ "cgst_account": "Output Tax CGST - _TC",
+ "sgst_account": "Output Tax SGST - _TC",
+ "igst_account": "Output Tax IGST - _TC",
})
gst_settings.save()
@@ -2106,7 +2137,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "CGST - _TC",
+ "account_head": "Output Tax CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
@@ -2114,7 +2145,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "SGST - _TC",
+ "account_head": "Output Tax SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
@@ -2164,6 +2195,7 @@ def create_sales_invoice(**args):
"rate": args.rate if args.get("rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
+ "asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": 1
diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
index 14bf4d81330..29422d68cf6 100644
--- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
+++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
@@ -1,235 +1,128 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-02-22 01:27:41",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2013-02-22 01:27:41",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "remarks",
+ "reference_row",
+ "col_break1",
+ "advance_amount",
+ "allocated_amount",
+ "exchange_gain_loss",
+ "ref_exchange_rate"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_type",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Type",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "journal_voucher",
- "oldfieldtype": "Link",
- "options": "DocType",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "250px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "no_copy": 1,
+ "oldfieldname": "journal_voucher",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "print_width": "250px",
+ "read_only": 1,
"width": "250px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "reference_name",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Reference Name",
- "length": 0,
- "no_copy": 1,
- "options": "reference_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 3,
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "no_copy": 1,
+ "options": "reference_type",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 3,
- "fieldname": "remarks",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Remarks",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "remarks",
- "oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "150px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 3,
+ "fieldname": "remarks",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Remarks",
+ "no_copy": 1,
+ "oldfieldname": "remarks",
+ "oldfieldtype": "Small Text",
+ "print_width": "150px",
+ "read_only": 1,
"width": "150px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_row",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Reference Row",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "jv_detail_no",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "reference_row",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Reference Row",
+ "no_copy": 1,
+ "oldfieldname": "jv_detail_no",
+ "oldfieldtype": "Data",
+ "print_hide": 1,
+ "print_width": "120px",
+ "read_only": 1,
"width": "120px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "col_break1",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "col_break1",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "advance_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Advance amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "advance_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "advance_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Advance amount",
+ "no_copy": 1,
+ "oldfieldname": "advance_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "120px",
+ "read_only": 1,
"width": "120px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "allocated_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Allocated amount",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "allocated_amount",
- "oldfieldtype": "Currency",
- "options": "party_account_currency",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "120px",
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "columns": 2,
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated amount",
+ "no_copy": 1,
+ "oldfieldname": "allocated_amount",
+ "oldfieldtype": "Currency",
+ "options": "party_account_currency",
+ "print_width": "120px",
"width": "120px"
+ },
+ {
+ "fieldname": "exchange_gain_loss",
+ "fieldtype": "Currency",
+ "label": "Exchange Gain/Loss",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "ref_exchange_rate",
+ "fieldtype": "Float",
+ "label": "Reference Exchange Rate",
+ "non_negative": 1,
+ "read_only": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2016-08-26 02:36:10.718057",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Invoice Advance",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_order": "DESC",
- "track_seen": 0
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-06-04 20:25:49.832052",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Invoice Advance",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index 8e6952a93c4..6690bdafc34 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -743,7 +743,6 @@
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
- "no_copy": 1,
"options": "Asset"
},
{
@@ -826,7 +825,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:05:22.123527",
+ "modified": "2021-06-21 23:03:11.599901",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 744ada9e558..1759fa3a48f 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -48,17 +48,18 @@ def validate_filters(filters, account_details):
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
-
- for account in filters.account:
- if not account_details.get(account):
- frappe.throw(_("Account {0} does not exists").format(account))
if filters.get('account'):
filters.account = frappe.parse_json(filters.get('account'))
+ for account in filters.account:
+ if not account_details.get(account):
+ frappe.throw(_("Account {0} does not exists").format(account))
- if (filters.get("account") and filters.get("group_by") == _('Group by Account')
- and account_details[filters.account].is_group == 0):
- frappe.throw(_("Can not filter based on Account, if grouped by Account"))
+ if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
+ filters.account = frappe.parse_json(filters.get('account'))
+ for account in filters.account:
+ if account_details[account].is_group == 0:
+ frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if (filters.get("voucher_no")
and filters.get("group_by") in [_('Group by Voucher')]):
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index e15715dccd8..6b9df41f54e 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
select voucher_no, credit
from `tabGL Entry`
where party in (%s) and credit > 0
- and company=%s and posting_date between %s and %s
+ and company=%s and is_cancelled = 0
+ and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1)
supplier_credit_amount = flt(sum(d.credit for d in entries))
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 66a9b601257..1cdbd8d38a6 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
"total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount,
- "exchange_rate": d.exchange_rate
+ "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(),
+ "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation
}
if d.voucher_detail_no:
@@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_amounts()
if d.difference_amount and d.difference_account:
- payment_entry.set_gain_or_loss(account_details={
+ account_details = {
'account': d.difference_account,
'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company',
- payment_entry.company, "cost_center"),
- 'amount': d.difference_amount
- })
+ payment_entry.company, "cost_center")
+ }
+ if d.difference_amount:
+ account_details['amount'] = d.difference_amount
+
+ payment_entry.set_gain_or_loss(account_details=account_details)
if not do_not_save:
payment_entry.save(ignore_permissions=True)
@@ -784,7 +788,7 @@ def get_children(doctype, parent, company, is_root=False):
return acc
def create_payment_gateway_account(gateway, payment_channel="Email"):
- from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
+ from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
company = frappe.db.get_value("Global Defaults", None, "default_company")
if not company:
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 8f0afb42b2c..8fdbbf95d47 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -176,22 +176,34 @@ def restore_asset(asset_name):
asset.set_status()
-@frappe.whitelist()
+def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
+ fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
+ get_asset_details(asset, finance_book)
+
+ gl_entries = [
+ {
+ "account": fixed_asset_account,
+ "debit_in_account_currency": asset.gross_purchase_amount,
+ "debit": asset.gross_purchase_amount,
+ "cost_center": depreciation_cost_center
+ },
+ {
+ "account": accumulated_depr_account,
+ "credit_in_account_currency": accumulated_depr_amount,
+ "credit": accumulated_depr_amount,
+ "cost_center": depreciation_cost_center
+ }
+ ]
+
+ profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
+ if profit_amount:
+ get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
+
+ return gl_entries
+
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None):
- fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
- disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
- depreciation_cost_center = asset.cost_center or depreciation_cost_center
-
- idx = 1
- if finance_book:
- for d in asset.finance_books:
- if d.finance_book == finance_book:
- idx = d.idx
- break
-
- value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
- if asset.calculate_depreciation else asset.value_after_depreciation)
- accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
+ fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
+ get_asset_details(asset, finance_book)
gl_entries = [
{
@@ -210,16 +222,37 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
if profit_amount:
- debit_or_credit = "debit" if profit_amount < 0 else "credit"
- gl_entries.append({
- "account": disposal_account,
- "cost_center": depreciation_cost_center,
- debit_or_credit: abs(profit_amount),
- debit_or_credit + "_in_account_currency": abs(profit_amount)
- })
+ get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
return gl_entries
+def get_asset_details(asset, finance_book=None):
+ fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
+ disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
+ depreciation_cost_center = asset.cost_center or depreciation_cost_center
+
+ idx = 1
+ if finance_book:
+ for d in asset.finance_books:
+ if d.finance_book == finance_book:
+ idx = d.idx
+ break
+
+ value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
+ if asset.calculate_depreciation else asset.value_after_depreciation)
+ accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
+
+ return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation
+
+def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center):
+ debit_or_credit = "debit" if profit_amount < 0 else "credit"
+ gl_entries.append({
+ "account": disposal_account,
+ "cost_center": depreciation_cost_center,
+ debit_or_credit: abs(profit_amount),
+ debit_or_credit + "_in_account_currency": abs(profit_amount)
+ })
+
@frappe.whitelist()
def get_disposal_account_and_cost_center(company):
disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company,
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 4ddc458175b..1766c2c80cc 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
+ frm.add_custom_button(__('Get Supplier Group Details'), function () {
+ frm.trigger("get_supplier_group_details");
+ }, __('Actions'));
+
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
},
+ get_supplier_group_details: function(frm) {
+ frappe.call({
+ method: "get_supplier_group_details",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh();
+ }
+ });
+ },
is_internal_supplier: function(frm) {
if (frm.doc.is_internal_supplier == 1) {
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index edeb135d951..fd16b23c220 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -51,6 +51,23 @@ class Supplier(TransactionBase):
validate_party_accounts(self)
self.validate_internal_supplier()
+ @frappe.whitelist()
+ def get_supplier_group_details(self):
+ doc = frappe.get_doc('Supplier Group', self.supplier_group)
+ self.payment_terms = ""
+ self.accounts = []
+
+ if doc.accounts:
+ for account in doc.accounts:
+ child = self.append('accounts')
+ child.company = account.company
+ child.account = account.account
+
+ if doc.payment_terms:
+ self.payment_terms = doc.payment_terms
+
+ self.save()
+
def validate_internal_supplier(self):
internal_supplier = frappe.db.get_value("Supplier",
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
@@ -86,4 +103,4 @@ class Supplier(TransactionBase):
create_contact(supplier, 'Supplier',
doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError:
- pass
\ No newline at end of file
+ pass
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index f9c8d35518d..89804662700 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -13,6 +13,30 @@ test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase):
+ def test_get_supplier_group_details(self):
+ doc = frappe.new_doc("Supplier Group")
+ doc.supplier_group_name = "_Testing Supplier Group"
+ doc.payment_terms = "_Test Payment Term Template 3"
+ doc.accounts = []
+ test_account_details = {
+ "company": "_Test Company",
+ "account": "Creditors - _TC",
+ }
+ doc.append("accounts", test_account_details)
+ doc.save()
+ s_doc = frappe.new_doc("Supplier")
+ s_doc.supplier_name = "Testing Supplier"
+ s_doc.supplier_group = "_Testing Supplier Group"
+ s_doc.payment_terms = ""
+ s_doc.accounts = []
+ s_doc.insert()
+ s_doc.get_supplier_group_details()
+ self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
+ self.assertEqual(s_doc.accounts[0].company, "_Test Company")
+ self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
+ s_doc.delete()
+ doc.delete()
+
def test_supplier_default_payment_terms(self):
# Payment Term based on Days after invoice date
frappe.db.set_value(
@@ -136,4 +160,4 @@ def create_supplier(**args):
return doc
except frappe.DuplicateEntryError:
- return frappe.get_doc("Supplier", args.supplier_name)
\ No newline at end of file
+ return frappe.get_doc("Supplier", args.supplier_name)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 1c086e9edcd..a9860ed2f05 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -124,6 +124,8 @@ class AccountsController(TransactionBase):
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances()
+ self.set_advance_gain_or_loss()
+
if self.is_return:
self.validate_qty()
else:
@@ -584,15 +586,18 @@ class AccountsController(TransactionBase):
allocated_amount = min(amount - advance_allocated, d.amount)
advance_allocated += flt(allocated_amount)
- self.append("advances", {
+ advance_row = {
"doctype": self.doctype + " Advance",
"reference_type": d.reference_type,
"reference_name": d.reference_name,
"reference_row": d.reference_row,
"remarks": d.remarks,
"advance_amount": flt(d.amount),
- "allocated_amount": allocated_amount
- })
+ "allocated_amount": allocated_amount,
+ "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry
+ }
+
+ self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True):
if self.doctype == "Sales Invoice":
@@ -650,6 +655,66 @@ class AccountsController(TransactionBase):
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.")
.format(d.reference_name, d.against_order))
+ def set_advance_gain_or_loss(self):
+ if not self.get("advances"):
+ return
+
+ for d in self.get("advances"):
+ advance_exchange_rate = d.ref_exchange_rate
+ if (d.allocated_amount and self.conversion_rate != 1
+ and self.conversion_rate != advance_exchange_rate):
+
+ base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
+ base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
+ difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate
+
+ d.exchange_gain_loss = difference
+
+ def make_exchange_gain_loss_gl_entries(self, gl_entries):
+ if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
+ for d in self.get("advances"):
+ if d.exchange_gain_loss:
+ party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer
+ party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to
+ party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer"
+
+ gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
+ account_currency = get_account_currency(gain_loss_account)
+ if account_currency != self.company_currency:
+ frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
+
+ # for purchase
+ dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
+ # just reverse for sales?
+ dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": gain_loss_account,
+ "account_currency": account_currency,
+ "against": party,
+ dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
+ dr_or_cr: abs(d.exchange_gain_loss),
+ "cost_center": self.cost_center,
+ "project": self.project
+ }, item=d)
+ )
+
+ dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": party_account,
+ "party_type": party_type,
+ "party": party,
+ "against": gain_loss_account,
+ dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
+ dr_or_cr: abs(d.exchange_gain_loss),
+ "cost_center": self.cost_center,
+ "project": self.project
+ }, self.party_account_currency, item=self)
+ )
+
def update_against_document_in_jv(self):
"""
Links invoice and advance voucher:
@@ -690,7 +755,9 @@ class AccountsController(TransactionBase):
if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total
if self.party_account_currency == self.company_currency else self.grand_total),
- 'outstanding_amount': self.outstanding_amount
+ 'outstanding_amount': self.outstanding_amount,
+ 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'),
+ 'exchange_gain_loss': flt(d.get('exchange_gain_loss'))
})
lst.append(args)
@@ -1289,6 +1356,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
payment_type = "Receive" if party_type == "Customer" else "Pay"
+ exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate"
+
payment_entries_against_order, unallocated_payment_entries = [], []
limit_cond = "limit %s" % limit if limit else ""
@@ -1305,27 +1374,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
"Payment Entry" as reference_type, t1.name as reference_name,
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date,
- t1.{0} as currency
+ t1.{0} as currency, t1.{4} as exchange_rate
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2}
order by t1.posting_date {3}
- """.format(currency_field, party_account_field, reference_condition, limit_cond),
+ """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field),
[party_account, payment_type, party_type, party,
order_doctype] + order_list, as_dict=1)
if include_unallocated:
unallocated_payment_entries = frappe.db.sql("""
select "Payment Entry" as reference_type, name as reference_name,
- remarks, unallocated_amount as amount
+ remarks, unallocated_amount as amount, {2} as exchange_rate
from `tabPayment Entry`
where
{0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0
order by posting_date {1}
- """.format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1)
+ """.format(party_account_field, limit_cond, exchange_rate_field),
+ (party_account, party_type, party, payment_type), as_dict=1)
return list(payment_entries_against_order) + list(unallocated_payment_entries)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 8196cff849d..2526e6df0ef 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -356,42 +356,68 @@ class StockController(AccountsController):
}, update_modified)
def validate_inspection(self):
- '''Checks if quality inspection is set for Items that require inspection.
- On submit, throw an exception'''
- inspection_required_fieldname = None
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- inspection_required_fieldname = "inspection_required_before_purchase"
- elif self.doctype in ["Delivery Note", "Sales Invoice"]:
- inspection_required_fieldname = "inspection_required_before_delivery"
+ """Checks if quality inspection is set/ is valid for Items that require inspection."""
+ inspection_fieldname_map = {
+ "Purchase Receipt": "inspection_required_before_purchase",
+ "Purchase Invoice": "inspection_required_before_purchase",
+ "Sales Invoice": "inspection_required_before_delivery",
+ "Delivery Note": "inspection_required_before_delivery"
+ }
+ inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
+ # return if inspection is not required on document level
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
(self.doctype == "Stock Entry" and not self.inspection_required) or
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
return
- for d in self.get('items'):
- qa_required = False
- if (inspection_required_fieldname and not d.quality_inspection and
- frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)):
- qa_required = True
- elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
- qa_required = True
- if self.docstatus == 1 and d.quality_inspection:
- qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
- if qa_doc.docstatus == 0:
- link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
- frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
+ for row in self.get('items'):
+ qi_required = False
+ if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
+ qi_required = True
+ elif self.doctype == "Stock Entry" and row.t_warehouse:
+ qi_required = True # inward stock needs inspection
- if qa_doc.status != 'Accepted':
- frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
- .format(d.idx, d.item_code), QualityInspectionRejectedError)
- elif qa_required :
- action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted
- if self.docstatus==1 and action == 'Stop':
- frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)),
- exc=QualityInspectionRequiredError)
- else:
- frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code)))
+ if qi_required: # validate row only if inspection is required on item level
+ self.validate_qi_presence(row)
+ if self.docstatus == 1:
+ self.validate_qi_submission(row)
+ self.validate_qi_rejection(row)
+
+ def validate_qi_presence(self, row):
+ """Check if QI is present on row level. Warn on save and stop on submit if missing."""
+ if not row.quality_inspection:
+ msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
+ if self.docstatus == 1:
+ frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
+ else:
+ frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
+
+ def validate_qi_submission(self, row):
+ """Check if QI is submitted on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
+ qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
+
+ if not qa_docstatus == 1:
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
+
+ def validate_qi_rejection(self, row):
+ """Check if QI is rejected on row level, during submission"""
+ action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
+ qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
+
+ if qa_status == "Rejected":
+ link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+ msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
+ if action == "Stop":
+ frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
+ else:
+ frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
index 3c2e59ab821..b0e662d3f32 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
@@ -7,16 +7,21 @@ import frappe
import unittest
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
+from erpnext.erpnext_integrations.utils import create_mode_of_payment
class TestMpesaSettings(unittest.TestCase):
+ def setUp(self):
+ # create payment gateway in setup
+ create_mpesa_settings(payment_gateway_name="_Test")
+ create_mpesa_settings(payment_gateway_name="_Account Balance")
+ create_mpesa_settings(payment_gateway_name="Payment")
+
def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self):
- create_mpesa_settings(payment_gateway_name="_Test")
-
- mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
+ mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
self.assertTrue(mode_of_payment.name)
self.assertEqual(mode_of_payment.type, "Phone")
@@ -47,7 +52,6 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.delete()
def test_processing_of_callback_payload(self):
- create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
@@ -90,7 +94,6 @@ class TestMpesaSettings(unittest.TestCase):
pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self):
- create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -141,7 +144,6 @@ class TestMpesaSettings(unittest.TestCase):
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
- create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -202,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"):
doc = frappe.get_doc(dict( #nosec
doctype="Mpesa Settings",
+ sandbox=1,
payment_gateway_name=payment_gateway_name,
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index 3840e781b4c..a5e162f8b5d 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
"payment_gateway": gateway
}, ['payment_account'])
- if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
+ mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
+ if not mode_of_payment and payment_gateway_account:
mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment",
"mode_of_payment": gateway,
@@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"):
})
mode_of_payment.insert(ignore_permissions=True)
+ return mode_of_payment
+ elif mode_of_payment:
+ return frappe.get_doc("Mode of Payment", mode_of_payment)
+
def get_tracking_url(carrier, tracking_number):
# Return the formatted Tracking URL.
tracking_url = ''
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index ba10b58f858..9717bb9b179 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -245,7 +245,10 @@ doc_events = {
"erpnext.portal.utils.set_default_role"]
},
"Communication": {
- "on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time"
+ "on_update": [
+ "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
+ "erpnext.support.doctype.issue.issue.set_first_response_time"
+ ]
},
("Sales Taxes and Charges Template", 'Price List'): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 578eccf787d..96ea686706c 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()
- expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes)
+ expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
+ do_not_submit=True, taxes=taxes)
expense_claim.submit()
gl_entries = frappe.db.sql("""select account, debit, credit
@@ -82,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- ['CGST - _TC4',18.0, 0.0],
+ ['Output Tax CGST - _TC4',18.0, 0.0],
[payable_account, 0.0, 218.0],
["Travel Expenses - _TC4", 200.0, 0.0]
])
@@ -145,7 +146,7 @@ def generate_taxes():
parent_account = frappe.db.get_value('Account',
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
'name')
- account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account)
+ account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{
"account_head": account,
"rate": 0,
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index 064dfb24557..d5f6e5f573e 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -33,7 +33,8 @@ frappe.ui.form.on('Training Event', {
frm.set_query("employee", "employees", function () {
return {
filters: {
- name: ["NOT IN", emp]
+ name: ["NOT IN", emp],
+ status: "Active"
}
};
});
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 28af3a9c415..f9c201ab603 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', {
frm.set_query("loan_type", function () {
return {
"filters": {
- "docstatus": 1
+ "docstatus": 1,
+ "company": frm.doc.company
}
};
});
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js
index 13652749711..017026ca13f 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.js
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.js
@@ -14,11 +14,18 @@ frappe.ui.form.on('Loan Application', {
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
+ frm.set_query("loan_type", () => {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
},
repayment_method: function(frm) {
- frm.doc.repayment_amount = frm.doc.repayment_periods = ""
- frm.trigger("toggle_fields")
- frm.trigger("toggle_required")
+ frm.doc.repayment_amount = frm.doc.repayment_periods = "";
+ frm.trigger("toggle_fields");
+ frm.trigger("toggle_required");
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 986b0c5711e..c93f7a7ed91 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -290,5 +290,6 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
+erpnext.patches.v13_0.update_response_by_variance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
diff --git a/erpnext/patches/v13_0/update_response_by_variance.py b/erpnext/patches/v13_0/update_response_by_variance.py
new file mode 100644
index 00000000000..ef4d9763837
--- /dev/null
+++ b/erpnext/patches/v13_0/update_response_by_variance.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2020, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ if frappe.db.exists('DocType', 'Issue') and frappe.db.count('Issue'):
+ invalid_issues = frappe.get_all('Issue', {
+ 'first_responded_on': ['is', 'set'],
+ 'response_by_variance': ['<', 0]
+ }, ["name", "response_by_variance", "timestampdiff(Second, `first_responded_on`, `response_by`) as variance"])
+
+ # issues which has response_by_variance set as -ve
+ # but diff between first_responded_on & response_by is +ve i.e SLA isn't failed
+ invalid_issues = [d for d in invalid_issues if d.get('variance') > 0]
+
+ for issue in invalid_issues:
+ frappe.db.set_value('Issue', issue.get('name'), 'response_by_variance', issue.get('variance'), update_modified=False)
+
+ invalid_issues = frappe.get_all('Issue', {
+ 'resolution_date': ['is', 'set'],
+ 'resolution_by_variance': ['<', 0]
+ }, ["name", "resolution_by_variance", "timestampdiff(Second, `resolution_date`, `resolution_by`) as variance"])
+
+ # issues which has resolution_by_variance set as -ve
+ # but diff between resolution_date & resolution_by is +ve i.e SLA isn't failed
+ invalid_issues = [d for d in invalid_issues if d.get('variance') > 0]
+
+ for issue in invalid_issues:
+ frappe.db.set_value('Issue', issue.get('name'), 'resolution_by_variance', issue.get('variance'), update_modified=False)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index c55bec89be8..f82b0d51bb1 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1088,6 +1088,7 @@ class SalarySlip(TransactionBase):
"applicant": self.employee,
"docstatus": 1,
"repay_from_salary": 1,
+ "company": self.company
})
def make_loan_repayment_entry(self):
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index ce88cc3f1e1..d730fcf1fa9 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -481,15 +481,19 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
+ employee = frappe.db.get_value("Employee",
+ {
+ "user_id": user
+ },
+ ["name", "company", "employee_name"],
+ as_dict=True)
- employee = frappe.db.get_value("Employee", {"user_id": user})
- salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
+ salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip_name:
- salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee)
- salary_slip.employee_name = frappe.get_value("Employee",
- {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name")
+ salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name)
+ salary_slip.employee_name = employee.employee_name
salary_slip.payroll_frequency = payroll_frequency
salary_slip.posting_date = nowdate()
salary_slip.insert()
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index e7d123c9960..374dd7ee443 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
- if not frappe.db.exists('Salary Structure', salary_structure):
- details = {
- "doctype": "Salary Structure",
- "name": salary_structure,
- "company": company or erpnext.get_default_company(),
- "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
- "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
- "payroll_frequency": payroll_frequency,
- "payment_account": get_random("Account", filters={'account_currency': currency}),
- "currency": currency
- }
- if other_details and isinstance(other_details, dict):
- details.update(other_details)
- salary_structure_doc = frappe.get_doc(details)
- salary_structure_doc.insert()
- if not dont_submit:
- salary_structure_doc.submit()
+ if frappe.db.exists("Salary Structure", salary_structure):
+ frappe.db.delete("Salary Structure", salary_structure)
- else:
- salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure)
+ details = {
+ "doctype": "Salary Structure",
+ "name": salary_structure,
+ "company": company or erpnext.get_default_company(),
+ "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
+ "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
+ "payroll_frequency": payroll_frequency,
+ "payment_account": get_random("Account", filters={"account_currency": currency}),
+ "currency": currency
+ }
+ if other_details and isinstance(other_details, dict):
+ details.update(other_details)
+ salary_structure_doc = frappe.get_doc(details)
+ salary_structure_doc.insert()
+ if not dont_submit:
+ salary_structure_doc.submit()
filters = {'employee':employee, 'docstatus': 1}
if not from_date and payroll_period:
diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py
index 500543ceb02..05a5366a5c1 100644
--- a/erpnext/payroll/report/bank_remittance/bank_remittance.py
+++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py
@@ -95,6 +95,7 @@ def execute(filters=None):
"amount": salary.net_pay,
}
data.append(row)
+
return columns, data
def get_bank_accounts():
@@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters):
entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"])
payment_accounts = [d.payment_account for d in entries]
- set_company_account(payment_accounts, entries)
+ entries = set_company_account(payment_accounts, entries)
return entries
def get_salary_slips(payroll_entries):
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index d77eb2c3966..211b94a9cfd 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -2,6 +2,7 @@ import frappe
from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
from erpnext.shopping_cart.product_info import get_product_info_for_website
+from erpnext.setup.doctype.item_group.item_group import get_child_groups
def get_field_filter_data():
product_settings = get_product_settings()
@@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search=
def get_products_html_for_website(field_filters=None, attribute_filters=None):
field_filters = frappe.parse_json(field_filters)
attribute_filters = frappe.parse_json(attribute_filters)
+ set_item_group_filters(field_filters)
items = get_products_for_website(field_filters, attribute_filters)
html = ''.join(get_html_for_items(items))
@@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None):
return html
+def set_item_group_filters(field_filters):
+ if 'item_group' in field_filters:
+ field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
+
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = []
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 39a6024e2cc..5976e016fa9 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -77,9 +77,6 @@ class Task(NestedSet):
if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100."))
- if flt(self.progress) == 100:
- self.status = 'Completed'
-
if self.status == 'Completed':
self.progress = 100
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 0471704c015..181e340427b 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -67,6 +67,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_discount_amount(){
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
+ this.calculate_item_values();
+ this.calculate_net_total();
this.set_discount_amount();
this.apply_discount_amount();
}
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index aa9bba17c77..d0c935f4887 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [
frappe.help.help_links["Form/System Settings"] = [
{
- label: "Naming Series",
+ label: "System Settings",
url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
},
];
@@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [
label: "PayPal Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/paypal-integration",
+ "user/manual/en/erpnext_integration/paypal-integration",
},
];
@@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [
label: "Razorpay Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/razorpay-integration",
+ "user/manual/en/erpnext_integration/razorpay-integration",
},
];
frappe.help.help_links["Form/Dropbox Settings"] = [
{
label: "Dropbox Settings",
- url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup",
+ url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup",
},
];
@@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [
{
label: "LDAP Settings",
url:
- docsUrl + "user/manual/en/setting-up/integrations/ldap-integration",
+ docsUrl + "user/manual/en/erpnext_integration/ldap-integration",
},
];
@@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [
label: "Stripe Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/stripe-integration",
+ "user/manual/en/erpnext_integration/stripe-integration",
},
];
@@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [
label: "Nested BOM Structure",
url:
docsUrl +
- "user/manual/en/manufacturing/articles/nested-bom-structure",
+ "user/manual/en/manufacturing/articles/managing-multi-level-bom",
},
];
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index ef03b01698c..6f5d67c7462 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -147,7 +147,7 @@ erpnext.setup.slides_settings = [
}
// Validate bank name
- if(me.values.bank_account){
+ if(me.values.bank_account) {
frappe.call({
async: false,
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.py b/erpnext/regional/doctype/gst_settings/gst_settings.py
index bc956e9fa88..af3d92e59a7 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.py
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.py
@@ -19,6 +19,21 @@ class GSTSettings(Document):
from tabAddress where country = "India" and ifnull(gstin, '')!='' ''')
self.set_onload('data', data)
+ def validate(self):
+ # Validate duplicate accounts
+ self.validate_duplicate_accounts()
+
+ def validate_duplicate_accounts(self):
+ account_list = []
+ for account in self.get('gst_accounts'):
+ for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']:
+ if account.get(fieldname) in account_list:
+ frappe.throw(_("Account {0} appears multiple times").format(
+ frappe.bold(account.get(fieldname))))
+
+ if account.get(fieldname):
+ account_list.append(account.get(fieldname))
+
@frappe.whitelist()
def send_reminder():
frappe.has_permission('GST Settings', throw=True)
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 3857ce1cdb8..065f80d610a 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -46,14 +46,14 @@ class TestGSTR3BReport(unittest.TestCase):
make_sales_invoice()
create_purchase_invoices()
- if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"):
- report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing")
+ if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"):
+ report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing")
report.save()
else:
report = frappe.get_doc({
"doctype": "GSTR 3B Report",
"company": "_Test Company GST",
- "company_address": "_Test Address-Billing",
+ "company_address": "_Test Address GST-Billing",
"year": getdate().year,
"month": month_number_mapping.get(getdate().month)
}).insert()
@@ -89,7 +89,7 @@ class TestGSTR3BReport(unittest.TestCase):
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -117,7 +117,7 @@ def make_sales_invoice():
si.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -138,7 +138,7 @@ def make_sales_invoice():
si1.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -159,7 +159,7 @@ def make_sales_invoice():
si2.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "IGST - _GST",
+ "account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -195,7 +195,7 @@ def create_purchase_invoices():
pi.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "CGST - _GST",
+ "account_head": "Input Tax CGST - _GST",
"cost_center": "Main - _GST",
"description": "CGST @ 9.0",
"rate": 9
@@ -203,7 +203,7 @@ def create_purchase_invoices():
pi.append("taxes", {
"charge_type": "On Net Total",
- "account_head": "SGST - _GST",
+ "account_head": "Input Tax SGST - _GST",
"cost_center": "Main - _GST",
"description": "SGST @ 9.0",
"rate": 9
@@ -410,10 +410,10 @@ def make_company():
company.country = "India"
company.insert()
- if not frappe.db.exists('Address', '_Test Address-Billing'):
+ if not frappe.db.exists('Address', '_Test Address GST-Billing'):
address = frappe.get_doc({
+ "address_title": "_Test Address GST",
"address_line1": "_Test Address Line 1",
- "address_title": "_Test Address",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
@@ -444,9 +444,9 @@ def set_account_heads():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company GST",
- "cgst_account": "CGST - _GST",
- "sgst_account": "SGST - _GST",
- "igst_account": "IGST - _GST",
+ "cgst_account": "Output Tax CGST - _GST",
+ "sgst_account": "Output Tax SGST - _GST",
+ "igst_account": "Output Tax IGST - _GST"
})
gst_settings.save()
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 23d4fe9030b..8ad30fa9106 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,6 +1,8 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
+ if (frm.doc.docstatus == 2) return;
+
const res = await frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
args: { doc: frm.doc }
@@ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const action = () => {
- let message = __('Cancellation of e-way bill is currently not supported. ');
+ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
message += '
';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 5d33c1b100a..81c7a6b9a07 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -42,7 +42,10 @@ def validate_eligibility(doc):
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
- no_taxes_applied = not doc.get('taxes')
+
+ # if export invoice, then taxes can be empty
+ # invoice can only be ineligible if no taxes applied and is not an export invoice
+ no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
@@ -188,9 +191,10 @@ def get_item_list(invoice):
item.qty = abs(item.qty)
- item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
- item.gross_amount = abs(item.taxable_value) + item.discount_amount
+ item.unit_rate = abs(item.taxable_value / item.qty)
+ item.gross_amount = abs(item.taxable_value)
item.taxable_value = abs(item.taxable_value)
+ item.discount_amount = 0
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 3e0b9b733b6..5f9d5ed0d61 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -25,6 +25,7 @@ def setup_company_independent_fixtures(patch=False):
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
create_gratuity_rule()
add_print_formats()
+ update_accounts_settings_for_taxes()
def add_hsn_sac_codes():
if frappe.flags.in_test and frappe.flags.created_hsn_codes:
@@ -680,7 +681,7 @@ def make_custom_fields(update=True):
def make_fixtures(company=None):
docs = []
- company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company")
+ company = company or frappe.db.get_value("Global Defaults", None, "default_company")
set_salary_components(docs)
set_tds_account(docs, company)
@@ -698,6 +699,53 @@ def make_fixtures(company=None):
# create records for Tax Withholding Category
set_tax_withholding_category(company)
+def update_regional_tax_settings(country, company):
+ # Will only add default GST accounts if present
+ input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST']
+ output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST']
+ rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM']
+ gst_settings = frappe.get_single('GST Settings')
+ existing_account_list = []
+
+ for account in gst_settings.get('gst_accounts'):
+ for key in ['cgst_account', 'sgst_account', 'igst_account']:
+ existing_account_list.append(account.get(key))
+
+ gst_accounts = frappe._dict(frappe.get_all("Account",
+ {'company': company, 'account_name': ('in', input_account_names +
+ output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1))
+
+ add_accounts_in_gst_settings(company, input_account_names, gst_accounts,
+ existing_account_list, gst_settings)
+ add_accounts_in_gst_settings(company, output_account_names, gst_accounts,
+ existing_account_list, gst_settings)
+ add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts,
+ existing_account_list, gst_settings, is_reverse_charge=1)
+
+ gst_settings.save()
+
+def add_accounts_in_gst_settings(company, account_names, gst_accounts,
+ existing_account_list, gst_settings, is_reverse_charge=0):
+ accounts_not_added = 1
+
+ for account in account_names:
+ # Default Account Added does not exists
+ if not gst_accounts.get(account):
+ accounts_not_added = 0
+
+ # Check if already added in GST Settings
+ if gst_accounts.get(account) in existing_account_list:
+ accounts_not_added = 0
+
+ if accounts_not_added:
+ gst_settings.append('gst_accounts', {
+ 'company': company,
+ 'cgst_account': gst_accounts.get(account_names[0]),
+ 'sgst_account': gst_accounts.get(account_names[1]),
+ 'igst_account': gst_accounts.get(account_names[2]),
+ 'is_reverse_charge_account': is_reverse_charge
+ })
+
def set_salary_components(docs):
docs.extend([
{'doctype': 'Salary Component', 'salary_component': 'Professional Tax',
@@ -731,12 +779,13 @@ def set_tax_withholding_category(company):
docs = get_tds_details(accounts, fiscal_year)
for d in docs:
- try:
+ if not frappe.db.exists("Tax Withholding Category", d.get("name")):
doc = frappe.get_doc(d)
+ doc.flags.ignore_validate = True
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.insert()
- except frappe.DuplicateEntryError:
+ else:
doc = frappe.get_doc("Tax Withholding Category", d.get("name"))
if accounts:
@@ -749,11 +798,12 @@ def set_tax_withholding_category(company):
doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True
+ doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
+ doc.flags.ignore_links = True
doc.save()
def set_tds_account(docs, company):
- abbr = frappe.get_value("Company", company, "abbr")
parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company})
if parent_account:
docs.extend([
@@ -912,7 +962,6 @@ def get_tds_details(accounts, fiscal_year):
]
def create_gratuity_rule():
-
# Standard Indain Gratuity Rule
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
rule = frappe.new_doc("Gratuity Rule")
@@ -930,3 +979,7 @@ def create_gratuity_rule():
rule.flags.ignore_mandatory = True
rule.save()
+
+def update_accounts_settings_for_taxes():
+ if frappe.db.count('Company') == 1:
+ frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0)
\ No newline at end of file
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
index 4deb073a53d..d0000ad50df 100644
--- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
@@ -11,7 +11,7 @@
"is_standard": "Yes",
"json": "{}",
"letter_head": "Logo",
- "modified": "2021-03-12 12:36:48.689413",
+ "modified": "2021-03-13 12:36:48.689413",
"modified_by": "Administrator",
"module": "Regional",
"name": "E-Invoice Summary",
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 10961593e1c..cfcb8c3444f 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -584,7 +584,7 @@ class Gstr1Report(object):
def get_json(filters, report_name, data):
filters = json.loads(filters)
report_data = json.loads(data)
- gstin = get_company_gstin_number(filters["company"], filters["company_address"])
+ gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address"))
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index 825b170a901..28494662673 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -130,6 +130,10 @@ frappe.ui.form.on("Customer", {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
+ frm.add_custom_button(__('Get Customer Group Details'), function () {
+ frm.trigger("get_customer_group_details");
+ }, __('Actions'));
+
// indicator
erpnext.utils.set_party_dashboard_indicators(frm);
@@ -145,4 +149,15 @@ frappe.ui.form.on("Customer", {
if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name);
},
-});
\ No newline at end of file
+ get_customer_group_details: function(frm) {
+ frappe.call({
+ method: "get_customer_group_details",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh();
+ }
+ });
+
+ }
+});
+
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 818888c0c12..3b62081e24c 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -78,6 +78,29 @@ class Customer(TransactionBase):
if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100:
frappe.throw(_("Total contribution percentage should be equal to 100"))
+ @frappe.whitelist()
+ def get_customer_group_details(self):
+ doc = frappe.get_doc('Customer Group', self.customer_group)
+ self.accounts = self.credit_limits = []
+ self.payment_terms = self.default_price_list = ""
+
+ tables = [["accounts", "account"], ["credit_limits", "credit_limit"]]
+ fields = ["payment_terms", "default_price_list"]
+
+ for row in tables:
+ table, field = row[0], row[1]
+ if not doc.get(table): continue
+
+ for entry in doc.get(table):
+ child = self.append(table)
+ child.update({"company": entry.company, field: entry.get(field)})
+
+ for field in fields:
+ if not doc.get(field): continue
+ self.update({field: doc.get(field)})
+
+ self.save()
+
def check_customer_group_change(self):
frappe.flags.customer_group_changed = False
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 7761aa70fb2..b1a5b52f963 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -27,6 +27,42 @@ class TestCustomer(unittest.TestCase):
def tearDown(self):
set_credit_limit('_Test Customer', '_Test Company', 0)
+ def test_get_customer_group_details(self):
+ doc = frappe.new_doc("Customer Group")
+ doc.customer_group_name = "_Testing Customer Group"
+ doc.payment_terms = "_Test Payment Term Template 3"
+ doc.accounts = []
+ doc.default_price_list = "Standard Buying"
+ doc.credit_limits = []
+ test_account_details = {
+ "company": "_Test Company",
+ "account": "Creditors - _TC",
+ }
+ test_credit_limits = {
+ "company": "_Test Company",
+ "credit_limit": 350000
+ }
+ doc.append("accounts", test_account_details)
+ doc.append("credit_limits", test_credit_limits)
+ doc.insert()
+
+ c_doc = frappe.new_doc("Customer")
+ c_doc.customer_name = "Testing Customer"
+ c_doc.customer_group = "_Testing Customer Group"
+ c_doc.payment_terms = c_doc.default_price_list = ""
+ c_doc.accounts = c_doc.credit_limits= []
+ c_doc.insert()
+ c_doc.get_customer_group_details()
+ self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3")
+
+ self.assertEqual(c_doc.accounts[0].company, "_Test Company")
+ self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC")
+
+ self.assertEqual(c_doc.credit_limits[0].company, "_Test Company")
+ self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000)
+ c_doc.delete()
+ doc.delete()
+
def test_party_details(self):
from erpnext.accounts.party import get_party_details
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 7cae0e47974..f7b2c1d93c3 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -472,12 +472,7 @@ erpnext.PointOfSale.ItemCart = class {
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
- const taxes = frm.doc.taxes.map(t => {
- return {
- description: t.description, rate: t.rate
- };
- });
- this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
+ this.render_taxes(frm.doc.taxes);
}
render_net_total(value) {
@@ -502,14 +497,14 @@ erpnext.PointOfSale.ItemCart = class {
);
}
- render_taxes(value, taxes) {
+ render_taxes(taxes) {
if (taxes.length) {
const currency = this.events.get_frm().doc.currency;
const taxes_html = taxes.map(t => {
const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
return `