Merge branch 'version-13-pre-release' into version-13
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
index faab334..d9603e8 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -98,8 +98,6 @@
languages: [python]
severity: WARNING
paths:
- exclude:
- - test_*.py
include:
- "*/**/doctype/*"
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
index 5a5098b..8b21979 100644
--- a/.github/helper/semgrep_rules/security.yml
+++ b/.github/helper/semgrep_rules/security.yml
@@ -8,18 +8,3 @@
dynamic content. Avoid it or use safe_eval().
languages: [python]
severity: ERROR
-
-- id: frappe-sqli-format-strings
- patterns:
- - pattern-inside: |
- @frappe.whitelist()
- def $FUNC(...):
- ...
- - pattern-either:
- - pattern: frappe.db.sql("..." % ...)
- - pattern: frappe.db.sql(f"...", ...)
- - pattern: frappe.db.sql("...".format(...), ...)
- message: |
- Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
- languages: [python]
- severity: WARNING
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 389524e..e27b406 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,34 +1,18 @@
name: Semgrep
on:
- pull_request:
- branches:
- - develop
- - version-13-hotfix
- - version-13-pre-release
+ pull_request: { }
+
jobs:
semgrep:
name: Frappe Linter
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Setup python3
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
-
- - name: Setup semgrep
- run: |
- python -m pip install -q semgrep
- git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
-
- - name: Semgrep errors
- run: |
- files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
- semgrep --config="r/python.lang.correctness" --quiet --error $files
-
- - name: Semgrep warnings
- run: |
- files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
+ - uses: actions/checkout@v2
+ - uses: returntocorp/semgrep-action@v1
+ env:
+ SEMGREP_TIMEOUT: 120
+ with:
+ config: >-
+ r/python.lang.correctness
+ .github/helper/semgrep_rules
diff --git a/CODEOWNERS b/CODEOWNERS
index 7cf65a7..219b6bb 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,16 +3,33 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
-manufacturing/ @rohitwaghchaure @marination
-accounts/ @deepeshgarg007 @nextchamp-saqib
-loan_management/ @deepeshgarg007 @rohitwaghchaure
-pos* @nextchamp-saqib @rohitwaghchaure
-assets/ @nextchamp-saqib @deepeshgarg007
-stock/ @marination @rohitwaghchaure
-buying/ @marination @deepeshgarg007
-hr/ @Anurag810 @rohitwaghchaure
-projects/ @hrwX @nextchamp-saqib
-support/ @hrwX @marination
-healthcare/ @ruchamahabal @marination
-erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib
-requirements.txt @gavindsouza
+erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
+erpnext/assets/ @nextchamp-saqib @deepeshgarg007
+erpnext/erpnext_integrations/ @nextchamp-saqib
+erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
+erpnext/regional @nextchamp-saqib @deepeshgarg007
+erpnext/selling @nextchamp-saqib @deepeshgarg007
+erpnext/support/ @nextchamp-saqib @deepeshgarg007
+pos* @nextchamp-saqib
+
+erpnext/buying/ @marination @rohitwaghchaure @ankush
+erpnext/e_commerce/ @marination
+erpnext/maintenance/ @marination @rohitwaghchaure
+erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
+erpnext/portal/ @marination
+erpnext/quality_management/ @marination @rohitwaghchaure
+erpnext/shopping_cart/ @marination
+erpnext/stock/ @marination @rohitwaghchaure @ankush
+
+erpnext/crm/ @ruchamahabal
+erpnext/education/ @ruchamahabal
+erpnext/healthcare/ @ruchamahabal
+erpnext/hr/ @ruchamahabal
+erpnext/non_profit/ @ruchamahabal
+erpnext/payroll @ruchamahabal
+erpnext/projects/ @ruchamahabal
+
+erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
+
+.github/ @surajshetty3416 @ankush
+requirements.txt @gavindsouza
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index a181c2d..c90e01c 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '13.7.1'
+__version__ = '13.8.0'
def get_default_company(user=None):
'''Get default company for user'''
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 4fd8413..8456b49 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
@@ -391,5 +391,5 @@
})
company.save()
- install_country_fixtures(company.name)
+ install_country_fixtures(company.name, company.country)
company.create_default_tax_template()
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 5619321..f2b0a8c 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -27,6 +27,9 @@
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
+ def on_cancel(self):
+ self.ignore_linked_doctypes = ('GL Entry')
+
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", {
@@ -99,10 +102,12 @@
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
- group by account, party_type, party
+ and posting_date <= %s
+ and is_cancelled = 0
+ group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
- """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1)
+ """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details
@@ -143,9 +148,9 @@
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
- "balance": d.get("balance_in_account_currency"),
- dr_or_cr: abs(d.get("balance_in_account_currency")),
- "exchange_rate":d.get("new_exchange_rate"),
+ "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
+ dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
+ "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
})
@@ -154,9 +159,9 @@
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
- "balance": d.get("balance_in_account_currency"),
- reverse_dr_or_cr: abs(d.get("balance_in_account_currency")),
- "exchange_rate": d.get("current_exchange_rate"),
+ "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
+ reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
+ "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name
})
@@ -185,9 +190,9 @@
account_details = {}
company_currency = erpnext.get_company_currency(company)
- balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False)
+ balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance:
- balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party)
+ balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 7f665db..46904f7 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -183,6 +183,13 @@
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)
@@ -685,8 +692,8 @@
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()
@@ -834,10 +841,17 @@
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)
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 912ad09..43eb0b6 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/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 6635128..d788d91 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -306,5 +306,5 @@
}
]
})
-
+ jv.flags.ignore_mandatory = True
jv.submit()
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index b54d0e7..94abf3b 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -168,7 +168,7 @@
frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s`
- where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
+ where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 45d89ad..f799279 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -451,6 +451,7 @@
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 311745d..4bc22a5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -953,6 +953,120 @@
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", -35000.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", -35000.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 5801b17..63dfff8 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 55a5b99..6d1f624 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -840,6 +840,7 @@
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)
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 14bf4d8..29422d6 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_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
index 1b7a0fe..cfdb167 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
@@ -27,7 +27,8 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
- "item_wise_tax_detail"
+ "item_wise_tax_detail",
+ "dont_recompute_tax"
],
"fields": [
{
@@ -200,13 +201,22 @@
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
+ },
+ {
+ "default": "0",
+ "fieldname": "dont_recompute_tax",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Dont Recompute tax",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-14 01:44:36.899147",
+ "modified": "2021-07-27 12:40:59.051803",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json
index f9160e2..153906f 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json
@@ -1,263 +1,151 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "Prompt",
- "beta": 0,
- "creation": "2018-04-13 18:42:06.431683",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2018-04-13 18:42:06.431683",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "category_details_section",
+ "category_name",
+ "round_off_tax_amount",
+ "column_break_2",
+ "consider_party_ledger_amount",
+ "tax_on_excess_amount",
+ "section_break_8",
+ "rates",
+ "section_break_7",
+ "accounts"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "category_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Category Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Tax Withholding Rates",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "show_days": 1,
+ "show_seconds": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "rates",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Rates",
- "length": 0,
- "no_copy": 0,
"options": "Tax Withholding Rate",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "show_days": 1,
+ "show_seconds": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
"label": "Account Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "accounts",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Accounts",
- "length": 0,
- "no_copy": 0,
- "options": "Tax Withholding Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "accounts",
+ "fieldtype": "Table",
+ "label": "Accounts",
+ "options": "Tax Withholding Account",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "category_details_section",
+ "fieldtype": "Section Break",
+ "label": "Category Details",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
+ "fieldname": "consider_party_ledger_amount",
+ "fieldtype": "Check",
+ "label": "Consider Entire Party Ledger Amount",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "description": "Tax will be withheld only for amount exceeding the cumulative threshold",
+ "fieldname": "tax_on_excess_amount",
+ "fieldtype": "Check",
+ "label": "Only Deduct Tax On Excess Amount ",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "description": "Checking this will round off the tax amount to the nearest integer",
+ "fieldname": "round_off_tax_amount",
+ "fieldtype": "Check",
+ "label": "Round Off Tax Amount",
+ "show_days": 1,
+ "show_seconds": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-07-17 22:53:26.193179",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Tax Withholding Category",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-07-27 21:47:34.396071",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Tax Withholding Category",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index b9ee4a0..481ef28 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -6,7 +6,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import flt, getdate
+from frappe.utils import flt, getdate, cint
from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
@@ -86,7 +86,10 @@
"rate": tax_rate_detail.tax_withholding_rate,
"threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
- "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category
+ "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
+ "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
+ "tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
+ "round_off_tax_amount": tax_withholding.round_off_tax_amount
})
def get_tax_withholding_rates(tax_withholding, fiscal_year):
@@ -145,6 +148,7 @@
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0]
+
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
@@ -235,10 +239,18 @@
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
+ invoice_filters = {
+ 'name': ('in', vouchers),
+ 'docstatus': 1
+ }
- supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
- 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
- }, 'sum(net_total)') or 0.0
+ field = 'sum(net_total)'
+
+ if not cint(tax_details.consider_party_ledger_amount):
+ invoice_filters.update({'apply_tds': 1})
+ field = 'sum(grand_total)'
+
+ supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1,
@@ -255,6 +267,13 @@
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
+ if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
+ # Get net total again as TDS is calculated on net total
+ # Grand is used to just check for threshold breach
+ net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0
+ net_total += inv.net_total
+ supp_credit_amt = net_total - cumulative_threshold
+
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
@@ -263,6 +282,9 @@
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
+
+ if cint(tax_details.round_off_tax_amount):
+ tds_amount = round(tds_amount)
return tds_amount
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index dd26be7..2ba22ca 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -87,6 +87,31 @@
for d in invoices:
d.cancel()
+ def test_tax_withholding_category_checks(self):
+ invoices = []
+ frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
+
+ # First Invoice with no tds check
+ pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True)
+ pi.apply_tds = 0
+ pi.save()
+ pi.submit()
+ invoices.append(pi)
+
+ # Second Invoice will apply TDS checked
+ pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000)
+ pi1.submit()
+ invoices.append(pi1)
+
+ # Cumulative threshold is 30000
+ # Threshold calculation should be on both the invoices
+ # TDS should be applied only on 1000
+ self.assertEqual(pi1.taxes[0].tax_amount, 1000)
+
+ for d in invoices:
+ d.cancel()
+
+
def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = []
@@ -195,7 +220,7 @@
def create_records():
# create a new suppliers
- for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
+ for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
if frappe.db.exists('Supplier', name):
continue
@@ -311,3 +336,23 @@
'account': 'TDS - _TC'
}]
}).insert()
+
+ if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
+ frappe.get_doc({
+ "doctype": "Tax Withholding Category",
+ "name": "New TDS Category",
+ "category_name": "New TDS Category",
+ "round_off_tax_amount": 1,
+ "consider_party_ledger_amount": 1,
+ "tax_on_excess_amount": 1,
+ "rates": [{
+ 'fiscal_year': fiscal_year,
+ 'tax_withholding_rate': 10,
+ 'single_threshold': 0,
+ 'cumulative_threshold': 30000
+ }],
+ "accounts": [{
+ 'company': '_Test Company',
+ 'account': 'TDS - _TC'
+ }]
+ }).insert()
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index a11b77a..b54646f 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -99,7 +99,6 @@
voucher_no = gle.voucher_no,
party = gle.party,
posting_date = gle.posting_date,
- remarks = gle.remarks,
account_currency = gle.account_currency,
invoiced = 0.0,
paid = 0.0,
@@ -579,7 +578,7 @@
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
- against_voucher_type, against_voucher, account_currency, remarks, {0}
+ against_voucher_type, against_voucher, account_currency, {0}
from
`tabGL Entry`
where
@@ -792,8 +791,6 @@
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
options='Supplier Group')
- self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200)
-
def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120):
if not fieldname: fieldname = scrub(label)
if fieldtype=='Currency': options='currency'
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index e724e9b..1759fa3 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -55,9 +55,11 @@
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/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 84c7454..6d8623c 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -241,6 +241,7 @@
sle.voucher_detail_no == row.item_row:
previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0
+
if previous_stock_value:
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else:
@@ -335,7 +336,7 @@
res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry`
- where company=%(company)s
+ where company=%(company)s and is_cancelled = 0
order by
item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""", self.filters, as_dict=True)
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index ed6e28d..9afe365 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -472,7 +472,8 @@
"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 @@
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)
@@ -962,7 +966,7 @@
for e in existing_gle:
if entry.account == e.account:
account_existed = True
- if (entry.account == e.account and entry.against_account == e.against_account
+ if (entry.account == e.account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):
diff --git a/erpnext/change_log/v13/v13_8_0.md b/erpnext/change_log/v13/v13_8_0.md
new file mode 100644
index 0000000..98ed95a
--- /dev/null
+++ b/erpnext/change_log/v13/v13_8_0.md
@@ -0,0 +1,39 @@
+# Version 13.8.0 Release Notes
+
+### Features & Enhancements
+- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222))
+- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677))
+- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564))
+
+### Fixes
+- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600))
+- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651))
+- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460))
+- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438))
+- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
+- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487))
+- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502))
+- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644))
+- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431))
+- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508))
+- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420))
+- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558))
+- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712))
+- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721))
+- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691))
+- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518))
+- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646))
+- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727))
+- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294))
+- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292))
+- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436))
+- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412))
+- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613))
+- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614))
+- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734))
+- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485))
+- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560))
+- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
+- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643))
+- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418))
+- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498))
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 5d30b65..a9b7efb 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -124,6 +124,8 @@
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 @@
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,71 @@
"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:
+ is_purchase_invoice = self.get('doctype') == 'Purchase Invoice'
+ party = self.supplier if is_purchase_invoice else self.customer
+ party_account = self.credit_to if is_purchase_invoice else self.debit_to
+ party_type = "Supplier" if is_purchase_invoice else "Customer"
+
+ gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
+ if not gain_loss_account:
+ frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
+ .format(self.get('company')))
+ account_currency = get_account_currency(gain_loss_account)
+ if account_currency != self.company_currency:
+ frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency))
+
+ # for purchase
+ dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
+ if not is_purchase_invoice:
+ # 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 +760,9 @@
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)
@@ -1045,8 +1117,11 @@
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
- d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
+ d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount
+ elif not d.invoice_portion:
+ d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
+
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -1289,6 +1364,8 @@
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 +1382,28 @@
"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/queries.py b/erpnext/controllers/queries.py
index 2803193..21c052a 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -407,6 +407,7 @@
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where
batch.disabled = 0
+ and sle.is_cancelled = 0
and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2526e6d..17bd735 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -53,12 +53,17 @@
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.get("items"):
if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no:
- serial_nos = get_serial_nos(d.serial_no)
- for serial_no_data in frappe.get_all("Serial No",
- filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]):
- if serial_no_data.batch_no != d.batch_no:
+ serial_nos = frappe.get_all("Serial No",
+ fields=["batch_no", "name", "warehouse"],
+ filters={
+ "name": ("in", get_serial_nos(d.serial_no))
+ }
+ )
+
+ for row in serial_nos:
+ if row.warehouse and row.batch_no != d.batch_no:
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
- .format(d.idx, serial_no_data.name, d.batch_no))
+ .format(d.idx, row.name, d.batch_no))
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 56da5b7..099c7d4 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -152,7 +152,7 @@
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc)
- if not self.doc.get('is_consolidated'):
+ if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")):
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount",
@@ -347,7 +347,7 @@
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
- if not self.doc.get("is_consolidated"):
+ if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
@@ -455,7 +455,8 @@
def _cleanup(self):
if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"):
- tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
+ if not tax.get("dont_recompute_tax"):
+ tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self):
if self.doc.additional_discount_percentage:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 52daec9..1ba752a 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -24,7 +24,8 @@
"Address": "public/js/address.js",
"Communication": "public/js/communication.js",
"Event": "public/js/event.js",
- "Newsletter": "public/js/newsletter.js"
+ "Newsletter": "public/js/newsletter.js",
+ "Contact": "public/js/contact.js"
}
override_doctype_class = {
diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py
index f760187..c2ed457 100644
--- a/erpnext/hr/doctype/appraisal/appraisal.py
+++ b/erpnext/hr/doctype/appraisal/appraisal.py
@@ -9,7 +9,7 @@
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document
-from erpnext.hr.utils import set_employee_name
+from erpnext.hr.utils import set_employee_name, validate_active_employee
class Appraisal(Document):
def validate(self):
@@ -19,6 +19,7 @@
if not self.goals:
frappe.throw(_("Goals cannot be empty"))
+ validate_active_employee(self.employee)
set_employee_name(self)
self.validate_dates()
self.validate_existing_appraisal()
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 3412675..f79f0fe 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -8,11 +8,13 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate
+from erpnext.hr.utils import validate_active_employee
class Attendance(Document):
def validate(self):
from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
+ validate_active_employee(self.employee)
self.validate_attendance_date()
self.validate_duplicate_record()
self.validate_employee_status()
diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.py b/erpnext/hr/doctype/attendance_request/attendance_request.py
index 090d532..7f88fed 100644
--- a/erpnext/hr/doctype/attendance_request/attendance_request.py
+++ b/erpnext/hr/doctype/attendance_request/attendance_request.py
@@ -8,10 +8,11 @@
from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate
from erpnext.hr.doctype.employee.employee import is_holiday
-from erpnext.hr.utils import validate_dates
+from erpnext.hr.utils import validate_dates, validate_active_employee
class AttendanceRequest(Document):
def validate(self):
+ validate_active_employee(self.employee)
validate_dates(self, self.from_date, self.to_date)
if self.half_day:
if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date):
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index a6fe429..0d7fded 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
@@ -7,12 +7,13 @@
from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document
-from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
+from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
get_holidays_for_employee, create_additional_leave_ledger_entry
class CompensatoryLeaveRequest(Document):
def validate(self):
+ validate_active_employee(self.employee)
validate_dates(self, self.work_from_date, self.work_end_date)
if self.half_day:
if not self.half_day_date:
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index fa017d9..5ca4756 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -13,8 +13,10 @@
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
-class EmployeeUserDisabledError(frappe.ValidationError): pass
-class EmployeeLeftValidationError(frappe.ValidationError): pass
+class EmployeeUserDisabledError(frappe.ValidationError):
+ pass
+class InactiveEmployeeStatusError(frappe.ValidationError):
+ pass
class Employee(NestedSet):
nsm_parent_field = 'reports_to'
@@ -196,7 +198,7 @@
message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
message += "</li></ul><br>"
message += _("Please make sure the employees above report to another Active employee.")
- throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee"))
+ throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee"))
if not self.relieving_date:
throw(_("Please enter relieving date."))
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 7d652a7..8fc7cf1 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -7,7 +7,7 @@
import erpnext
import unittest
import frappe.utils
-from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError
+from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee')
@@ -45,10 +45,33 @@
employee2_doc.save()
employee1_doc.reload()
employee1_doc.status = 'Left'
- self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
+ self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
+
+ def test_employee_status_inactive(self):
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
+ from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
+
+ employee = make_employee("test_employee_status@company.com")
+ employee_doc = frappe.get_doc("Employee", employee)
+ employee_doc.status = "Inactive"
+ employee_doc.save()
+ employee_doc.reload()
+
+ make_holiday_list()
+ frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
+
+ frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
+ salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
+ employee=employee_doc.name, company=employee_doc.company)
+ salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name)
+
+ self.assertRaises(InactiveEmployeeStatusError, salary_slip.save)
+
+ def tearDown(self):
+ frappe.db.rollback()
def make_employee(user, company=None, **kwargs):
- ""
if not frappe.db.get_value("User", user):
frappe.get_doc({
"doctype": "User",
@@ -80,4 +103,5 @@
employee.insert()
return employee.name
else:
+ frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active")
return frappe.get_value("Employee", {"employee_name":user}, "name")
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index cb72f6b..ece627c 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -8,6 +8,7 @@
from frappe.model.document import Document
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
+from erpnext.hr.utils import validate_active_employee
class EmployeeAdvanceOverPayment(frappe.ValidationError):
pass
@@ -18,6 +19,7 @@
'make_payment_via_journal_entry')
def validate(self):
+ validate_active_employee(self.employee)
self.set_status()
def on_cancel(self):
@@ -183,9 +185,9 @@
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults"))
-
+
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
-
+
je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate()
je.voucher_type = get_voucher_type(mode_of_payment)
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index 15fbd4e..60ea0f9 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -9,9 +9,11 @@
from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
+from erpnext.hr.utils import validate_active_employee
class EmployeeCheckin(Document):
def validate(self):
+ validate_active_employee(self.employee)
self.validate_duplicate_log()
self.fetch_shift()
@@ -122,7 +124,7 @@
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases.
-
+
:param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
index 83fb235..a3a6183 100644
--- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py
+++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
@@ -7,12 +7,11 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
-from erpnext.hr.utils import update_employee
+from erpnext.hr.utils import update_employee, validate_active_employee
class EmployeePromotion(Document):
def validate(self):
- if frappe.get_value("Employee", self.employee, "status") != "Active":
- frappe.throw(_("Cannot promote Employee with status Left or Inactive"))
+ validate_active_employee(self.employee)
def before_submit(self):
if getdate(self.promotion_date) > getdate():
diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py
index 45d6872..0493306 100644
--- a/erpnext/hr/doctype/employee_referral/employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/employee_referral.py
@@ -7,9 +7,11 @@
from frappe import _
from frappe.utils import get_link_to_form
from frappe.model.document import Document
+from erpnext.hr.utils import validate_active_employee
class EmployeeReferral(Document):
def validate(self):
+ validate_active_employee(self.referrer)
self.set_full_name()
self.set_referral_bonus_payment_status()
diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
index 6eec9fa..c200774 100644
--- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
@@ -10,10 +10,6 @@
from erpnext.hr.utils import update_employee
class EmployeeTransfer(Document):
- def validate(self):
- if frappe.get_value("Employee", self.employee, "status") != "Active":
- frappe.throw(_("Cannot transfer Employee with status Left or Inactive"))
-
def before_submit(self):
if getdate(self.transfer_date) > getdate():
frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"),
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index 5010fc3..8f8dbb2 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.utils import get_fullname, flt, cstr, get_link_to_form
from frappe.model.document import Document
-from erpnext.hr.utils import set_employee_name, share_doc_with_approver
+from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee
from erpnext.accounts.party import get_party_account
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@@ -23,6 +23,7 @@
'make_payment_via_journal_entry')
def validate(self):
+ validate_active_employee(self.employee)
self.validate_advances()
self.validate_sanctioned_amount()
self.calculate_total_amount()
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index cee6f37..93fb19f 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
-from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
+from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver, validate_active_employee
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
@@ -22,6 +22,7 @@
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
def validate(self):
+ validate_active_employee(self.employee)
set_employee_name(self)
self.validate_dates()
self.validate_balance_leaves()
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index e041b7f..912bd8a 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -7,7 +7,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate, flt
-from erpnext.hr.utils import set_employee_name
+from erpnext.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
@@ -15,6 +15,7 @@
class LeaveEncashment(Document):
def validate(self):
set_employee_name(self)
+ validate_active_employee(self.employee)
self.get_leave_details_for_encashment()
self.validate_salary_structure()
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index ab65260..89ae4d5 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -9,10 +9,12 @@
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
+from erpnext.hr.utils import validate_active_employee
from datetime import timedelta, datetime
class ShiftAssignment(Document):
def validate(self):
+ validate_active_employee(self.employee)
self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date:
diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py
index 177c45e..6461f07 100644
--- a/erpnext/hr/doctype/shift_request/shift_request.py
+++ b/erpnext/hr/doctype/shift_request/shift_request.py
@@ -7,12 +7,13 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import formatdate, getdate
-from erpnext.hr.utils import share_doc_with_approver
+from erpnext.hr.utils import share_doc_with_approver, validate_active_employee
class OverlapError(frappe.ValidationError): pass
class ShiftRequest(Document):
def validate(self):
+ validate_active_employee(self.employee)
self.validate_dates()
self.validate_shift_request_overlap_dates()
self.validate_approver()
diff --git a/erpnext/hr/doctype/travel_request/travel_request.py b/erpnext/hr/doctype/travel_request/travel_request.py
index 01d3f34..60834d3 100644
--- a/erpnext/hr/doctype/travel_request/travel_request.py
+++ b/erpnext/hr/doctype/travel_request/travel_request.py
@@ -5,6 +5,8 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
+from erpnext.hr.utils import validate_active_employee
class TravelRequest(Document):
- pass
+ def validate(self):
+ validate_active_employee(self.employee)
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index ebb1734..a6a8406 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -3,13 +3,12 @@
import erpnext
import frappe
-from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, InactiveEmployeeStatusError
from frappe import _
from frappe.desk.form import assign_to
from frappe.model.document import Document
from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
- get_datetime, getdate, nowdate, today, unique)
-
+ get_datetime, getdate, nowdate, today, unique, get_link_to_form)
class DuplicateDeclarationError(frappe.ValidationError): pass
@@ -20,6 +19,7 @@
Assign to the concerned person and roles as per the onboarding/separation template
'''
def validate(self):
+ validate_active_employee(self.employee)
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
@@ -522,3 +522,8 @@
approver = approvers.get(doc.doctype)
if doc_before_save.get(approver) != doc.get(approver):
frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver))
+
+def validate_active_employee(employee):
+ if frappe.db.get_value("Employee", employee, "status") == "Inactive":
+ frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format(
+ get_link_to_form("Employee", employee)), InactiveEmployeeStatusError)
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 28af3a9..f9c201a 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -28,7 +28,8 @@
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 1365274..eccbdc3 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.js
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.js
@@ -14,6 +14,13 @@
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 = ""
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 15a7c31..bfbc679 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -83,7 +83,7 @@
if (!frm.doc.__islocal && frm.doc.docstatus<2) {
frm.add_custom_button(__("Update Cost"), function() {
- frm.events.update_cost(frm);
+ frm.events.update_cost(frm, true);
});
frm.add_custom_button(__("Browse BOM"), function() {
frappe.route_options = {
@@ -318,14 +318,15 @@
})
},
- update_cost: function(frm) {
+ update_cost: function(frm, save_doc=false) {
return frappe.call({
doc: frm.doc,
method: "update_cost",
freeze: true,
args: {
update_parent: true,
- from_child_bom:false
+ save: save_doc,
+ from_child_bom: false
},
callback: function(r) {
refresh_field("items");
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 9da461f..ebd9ae2 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -330,7 +330,7 @@
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
if not from_child_bom:
- frappe.msgprint(_("Cost Updated"))
+ frappe.msgprint(_("Cost Updated"), alert=True)
def update_parent_cost(self):
if self.total_cost:
@@ -748,7 +748,7 @@
if valuation_rate <= 0:
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
- where item_code = %s and valuation_rate > 0
+ where item_code = %s and valuation_rate > 0 and is_cancelled = 0
order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code'])
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
@@ -1069,13 +1069,6 @@
if barcodes:
or_cond_filters["name"] = ("in", barcodes)
- for cond in get_match_cond(doctype, as_condition=False):
- for key, value in cond.items():
- if key == doctype:
- key = "name"
-
- query_filters[key] = ("in", value)
-
if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants:
@@ -1084,7 +1077,7 @@
if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1
- return frappe.get_all("Item",
+ return frappe.get_list("Item",
fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by,
limit_start=start, limit_page_length=page_len, as_list=1)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 420bb00..69c7f5c 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -192,11 +192,11 @@
"completed_qty": args.get("completed_qty") or 0.0
})
elif args.get("start_time"):
- new_args = {
+ new_args = frappe._dict({
"from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
- }
+ })
if employees:
for name in employees:
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 38a0ee7..6a024f2 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -747,9 +747,8 @@
group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
-def get_warehouse_list(warehouses, warehouse_list=None):
- if not warehouse_list:
- warehouse_list = []
+def get_warehouse_list(warehouses):
+ warehouse_list = []
if isinstance(warehouses, str):
warehouses = json.loads(warehouses)
@@ -761,23 +760,19 @@
else:
warehouse_list.append(row.get("warehouse"))
+ return warehouse_list
+
@frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
- warehouse_list = []
if warehouses:
- get_warehouse_list(warehouses, warehouse_list)
-
- if warehouse_list:
- warehouses = list(set(warehouse_list))
+ warehouses = list(set(get_warehouse_list(warehouses)))
if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse"))
- warehouse_list = None
-
doc['mr_items'] = []
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index cce1bb6..93e6d7a 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -10,7 +10,7 @@
from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
-from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests
+from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
class TestProductionPlan(unittest.TestCase):
def setUp(self):
@@ -251,6 +251,27 @@
pln.cancel()
frappe.delete_doc("Production Plan", pln.name)
+ def test_get_warehouse_list_group(self):
+ """Check if required warehouses are returned"""
+ warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
+
+ warehouses = set(get_warehouse_list(warehouse_json))
+ expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"}
+
+ missing_warehouse = expected_warehouses - warehouses
+
+ self.assertTrue(len(missing_warehouse) == 0,
+ msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
+
+ def test_get_warehouse_list_single(self):
+ warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
+
+ warehouses = set(get_warehouse_list(warehouse_json))
+ expected_warehouses = {"_Test Scrap Warehouse - _TC", }
+
+ self.assertEqual(warehouses, expected_warehouses)
+
+
def create_production_plan(**args):
args = frappe._dict(args)
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
index f63d2b9..10cee32 100644
--- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
@@ -19,6 +19,7 @@
"options": "Operation"
},
{
+ "default": "0",
"description": "Time in mins",
"fieldname": "time_in_mins",
"fieldtype": "Float",
@@ -38,7 +39,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-07 18:09:18.005578",
+ "modified": "2021-07-15 16:39:41.635362",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sub Operation",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 0a8e532..69812c7 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -487,21 +487,20 @@
return
operations = []
- if not self.use_multi_level_bom:
- bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
- operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
- else:
+
+ if self.use_multi_level_bom:
bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
- bom_traversal = list(reversed(bom_tree.level_order_traversal()))
- bom_traversal.append(bom_tree) # add operation on top level item last
+ bom_traversal = reversed(bom_tree.level_order_traversal())
- for d in bom_traversal:
- if d.is_bom:
- operations.extend(_get_operations(d.name, qty=d.exploded_qty))
+ for node in bom_traversal:
+ if node.is_bom:
+ operations.extend(_get_operations(node.name, qty=node.exploded_qty))
- for correct_index, operation in enumerate(operations, start=1):
- operation.idx = correct_index
+ bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
+ operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
+ for correct_index, operation in enumerate(operations, start=1):
+ operation.idx = correct_index
self.set('operations', operations)
self.calculate_time()
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index f190cfa..7c1baf1 100644
--- a/erpnext/non_profit/doctype/member/member.json
+++ b/erpnext/non_profit/doctype/member/member.json
@@ -26,7 +26,7 @@
"razorpay_details_section",
"subscription_id",
"customer_id",
- "subscription_activated",
+ "subscription_status",
"column_break_21",
"subscription_start",
"subscription_end"
@@ -152,12 +152,6 @@
"fieldtype": "Column Break"
},
{
- "default": "0",
- "fieldname": "subscription_activated",
- "fieldtype": "Check",
- "label": "Subscription Activated"
- },
- {
"fieldname": "subscription_start",
"fieldtype": "Date",
"label": "Subscription Start "
@@ -166,11 +160,17 @@
"fieldname": "subscription_end",
"fieldtype": "Date",
"label": "Subscription End"
+ },
+ {
+ "fieldname": "subscription_status",
+ "fieldtype": "Select",
+ "label": "Subscription Status",
+ "options": "\nActive\nHalted"
}
],
"image_field": "image",
"links": [],
- "modified": "2020-11-09 12:12:10.174647",
+ "modified": "2021-07-11 14:27:26.368039",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 30be585..67828d6 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -84,7 +84,9 @@
"email_id": user_details.email,
"pan_number": user_details.pan or None,
"membership_type": user_details.plan_id,
- "subscription_id": user_details.subscription_id or None
+ "customer_id": user_details.customer_id or None,
+ "subscription_id": user_details.subscription_id or None,
+ "subscription_status": user_details.subscription_status or ""
})
member.insert(ignore_permissions=True)
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index e8ae618..b584116 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -196,11 +196,14 @@
return invoice
-def get_member_based_on_subscription(subscription_id, email):
- members = frappe.get_all("Member", filters={
- "subscription_id": subscription_id,
- "email_id": email
- }, order_by="creation desc")
+def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
+ filters = {"subscription_id": subscription_id}
+ if email:
+ filters.update({"email_id": email})
+ if customer_id:
+ filters.update({"customer_id": customer_id})
+
+ members = frappe.get_all("Member", filters=filters, order_by="creation desc")
try:
return frappe.get_doc("Member", members[0]["name"])
@@ -209,8 +212,6 @@
def verify_signature(data, endpoint="Membership"):
- if frappe.flags.in_test or os.environ.get("CI"):
- return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Non Profit Settings")
@@ -225,16 +226,7 @@
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
- try:
- verify_signature(data)
- except Exception as e:
- log = frappe.log_error(e, "Membership Webhook Verification Error")
- notify_failure(log)
- return { "status": "Failed", "reason": e}
-
- if isinstance(data, six.string_types):
- data = json.loads(data)
- data = frappe._dict(data)
+ data = process_request_data(data)
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
@@ -281,7 +273,7 @@
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
- member.subscription_activated = 1
+ member.subscription_status = "Active"
member.flags.ignore_mandatory = True
member.save()
@@ -294,9 +286,67 @@
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
- return { "status": "Failed", "reason": e}
+ return {"status": "Failed", "reason": e}
- return { "status": "Success" }
+ return {"status": "Success"}
+
+
+@frappe.whitelist(allow_guest=True)
+def update_halted_razorpay_subscription(*args, **kwargs):
+ """
+ When all retries have been exhausted, Razorpay moves the subscription to the halted state.
+ The customer has to manually retry the charge or change the card linked to the subscription,
+ for the subscription to move back to the active state.
+ """
+ if frappe.request:
+ data = frappe.request.get_data(as_text=True)
+ data = process_request_data(data)
+ elif frappe.flags.in_test:
+ data = kwargs.get("data")
+ data = frappe._dict(data)
+ else:
+ return
+
+ if not data.event == "subscription.halted":
+ return
+
+ subscription = data.payload.get("subscription", {}).get("entity", {})
+ subscription = frappe._dict(subscription)
+
+ try:
+ member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
+ if not member:
+ frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
+
+ member.subscription_status = "Halted"
+ member.flags.ignore_mandatory = True
+ member.save()
+
+ if subscription.get("notes"):
+ member = get_additional_notes(member, subscription)
+
+ except Exception as e:
+ message = "{0}\n\n{1}".format(e, frappe.get_traceback())
+ log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
+ notify_failure(log)
+ return {"status": "Failed", "reason": e}
+
+ return {"status": "Success"}
+
+
+def process_request_data(data):
+ try:
+ verify_signature(data)
+ except Exception as e:
+ log = frappe.log_error(e, "Membership Webhook Verification Error")
+ notify_failure(log)
+ return {"status": "Failed", "reason": e}
+
+ if isinstance(data, six.string_types):
+ data = json.loads(data)
+ data = frappe._dict(data)
+
+ return data
def get_company_for_memberships():
@@ -362,4 +412,4 @@
`tabMembership` SET `status` = 'Expired'
WHERE
`status` not in ('Cancelled') AND `to_date` < %s
- """, (nowdate()))
\ No newline at end of file
+ """, (nowdate()))
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index 31da792..0f5a9be 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -6,6 +6,7 @@
import frappe
import erpnext
from erpnext.non_profit.doctype.member.member import create_member
+from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
@@ -13,11 +14,16 @@
plan = setup_membership()
# make test member
- self.member_doc = create_member(frappe._dict({
- 'fullname': "_Test_Member",
- 'email': "_test_member_erpnext@example.com",
- 'plan_id': plan.name
- }))
+ self.member_doc = create_member(
+ frappe._dict({
+ "fullname": "_Test_Member",
+ "email": "_test_member_erpnext@example.com",
+ "plan_id": plan.name,
+ "subscription_id": "sub_DEX6xcJ1HSW4CR",
+ "customer_id": "cust_C0WlbKhp3aLA7W",
+ "subscription_status": "Active"
+ })
+ )
self.member_doc.make_customer_and_link()
self.member = self.member_doc.name
@@ -51,6 +57,20 @@
"to_date": add_months(nowdate(), 3),
})
+ def test_halted_memberships(self):
+ make_membership(self.member, {
+ "from_date": add_months(nowdate(), 2),
+ "to_date": add_months(nowdate(), 3)
+ })
+
+ self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
+ payload = get_subscription_payload()
+ update_halted_razorpay_subscription(data=payload)
+ self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
+
+ def tearDown(self):
+ frappe.db.rollback()
+
def set_config(key, value):
frappe.db.set_value("Non Profit Settings", None, key, value)
@@ -115,4 +135,28 @@
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
- return plan
\ No newline at end of file
+ return plan
+
+def get_subscription_payload():
+ return {
+ "entity": "event",
+ "account_id": "acc_BFQ7uQEaa7j2z7",
+ "event": "subscription.halted",
+ "contains": [
+ "subscription"
+ ],
+ "payload": {
+ "subscription": {
+ "entity": {
+ "id": "sub_DEX6xcJ1HSW4CR",
+ "entity": "subscription",
+ "plan_id": "_rzpy_test_milythm",
+ "customer_id": "cust_C0WlbKhp3aLA7W",
+ "status": "halted",
+ "notes": {
+ "Important": "Notes for Internal Reference"
+ },
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f63c7ed..3276375 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -292,3 +292,6 @@
erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
+erpnext.patches.v13_0.update_subscription_status_in_memberships
+erpnext.patches.v13_0.update_export_type_for_gst
+erpnext.patches.v13_0.update_tds_check_field #3
diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
index 48999e6..d7ad1fc 100644
--- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
+++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
@@ -10,6 +10,7 @@
if not frappe.db.has_column('Work Order', 'has_batch_no'):
return
+ frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings')
if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
return
@@ -107,4 +108,4 @@
"company": doc.company
})
- create_repost_item_valuation_entry(args)
\ No newline at end of file
+ create_repost_item_valuation_entry(args)
diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py
new file mode 100644
index 0000000..478a2a6
--- /dev/null
+++ b/erpnext/patches/v13_0/update_export_type_for_gst.py
@@ -0,0 +1,24 @@
+import frappe
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ # Update custom fields
+ fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'})
+ if fieldname:
+ frappe.db.set_value('Custom Field', fieldname, 'default', '')
+
+ fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'})
+ if fieldname:
+ frappe.db.set_value('Custom Field', fieldname, 'default', '')
+
+ # Update Customer/Supplier Masters
+ frappe.db.sql("""
+ UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export')
+ """)
+
+ frappe.db.sql("""
+ UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas')
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py
new file mode 100644
index 0000000..28e650e
--- /dev/null
+++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ if frappe.db.exists('DocType', 'Member'):
+ frappe.reload_doc('Non Profit', 'doctype', 'Member')
+
+ if frappe.db.has_column('Member', 'subscription_activated'):
+ frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1')
+ frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated')
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py
new file mode 100644
index 0000000..3d14958
--- /dev/null
+++ b/erpnext/patches/v13_0/update_tds_check_field.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ if frappe.db.has_table("Tax Withholding Category") \
+ and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"):
+ frappe.db.sql("""
+ UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0
+ WHERE round_off_tax_amount IS NULL
+ """)
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index ebeddf9..b978cbe 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate
+from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document):
def on_submit(self):
@@ -19,6 +20,7 @@
self.update_employee_referral(cancel=True)
def validate(self):
+ validate_active_employee(self.employee)
self.validate_dates()
self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap()
@@ -110,11 +112,11 @@
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days
+@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql("""
- select name, salary_component as component, type, amount,
- overwrite_salary_structure_amount as overwrite,
- deduct_full_tax_on_selected_payroll_date
+ select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
+ deduct_full_tax_on_selected_payroll_date, is_recurring
from `tabAdditional Salary`
where employee=%(employee)s
and docstatus = 1
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index 27df30a..5ebe514 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -9,10 +9,11 @@
from frappe.model.document import Document
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
-from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount
+from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee
class EmployeeBenefitApplication(Document):
def validate(self):
+ validate_active_employee(self.employee)
self.validate_duplicate_on_payroll_period()
if not self.max_benefits:
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py
index d9937a7..c6713f3 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py
@@ -8,12 +8,13 @@
from frappe.utils import flt
from frappe.model.document import Document
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits
-from erpnext.hr.utils import get_previous_claimed_amount
+from erpnext.hr.utils import get_previous_claimed_amount, validate_active_employee
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
class EmployeeBenefitClaim(Document):
def validate(self):
+ validate_active_employee(self.employee)
max_benefits = get_max_benefits(self.employee, self.claim_date)
if not max_benefits or max_benefits <= 0:
frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee))
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py
index ead3db1..6b918ba 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py
@@ -6,9 +6,11 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from erpnext.hr.utils import validate_active_employee
class EmployeeIncentive(Document):
def validate(self):
+ validate_active_employee(self.employee)
self.validate_salary_structure()
def validate_salary_structure(self):
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
index fb71a28..e11d60a 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
@@ -8,11 +8,12 @@
from frappe import _
from frappe.utils import flt
from frappe.model.mapper import get_mapped_doc
-from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \
+from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionDeclaration(Document):
def validate(self):
+ validate_active_employee(self.employee)
validate_tax_declaration(self.declarations)
validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee)
self.set_total_declared_amount()
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
index 5bc33a6..8131ae0 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
@@ -7,11 +7,12 @@
from frappe.model.document import Document
from frappe import _
from frappe.utils import flt
-from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \
+from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionProofSubmission(Document):
def validate(self):
+ validate_active_employee(self.employee)
validate_tax_declaration(self.tax_exemption_proofs)
self.set_total_actual_amount()
self.set_total_exemption_amount()
diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py
index 049ea26..055bea7 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py
@@ -7,11 +7,10 @@
from frappe.model.document import Document
from frappe import _
from frappe.utils import getdate
-
+from erpnext.hr.utils import validate_active_employee
class RetentionBonus(Document):
def validate(self):
- if frappe.get_value('Employee', self.employee, 'status') != 'Active':
- frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees'))
+ validate_active_employee(self.employee)
if getdate(self.bonus_payment_date) < getdate():
frappe.throw(_('Bonus Payment Date cannot be a past date'))
diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json
index 393f647..97608d7 100644
--- a/erpnext/payroll/doctype/salary_detail/salary_detail.json
+++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json
@@ -12,6 +12,7 @@
"year_to_date",
"section_break_5",
"additional_salary",
+ "is_recurring_additional_salary",
"statistical_component",
"depends_on_payment_days",
"exempted_from_income_tax",
@@ -235,11 +236,19 @@
"label": "Year To Date",
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
+ "fieldname": "is_recurring_additional_salary",
+ "fieldtype": "Check",
+ "label": "Is Recurring Additional Salary",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-01-14 13:39:15.847158",
+ "modified": "2021-03-14 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 877503b..3e82c0d 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -7,18 +7,19 @@
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname
+from frappe.utils.background_jobs import enqueue
from frappe import msgprint, _
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase
-from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.hr.utils import validate_active_employee
from six import iteritems
class SalarySlip(TransactionBase):
@@ -39,6 +40,7 @@
def validate(self):
self.status = self.get_status()
+ validate_active_employee(self.employee)
self.validate_dates()
self.check_existing()
if not self.salary_slip_based_on_timesheet:
@@ -616,7 +618,8 @@
get_salary_component_data(additional_salary.component),
additional_salary.amount,
component_type,
- additional_salary
+ additional_salary,
+ is_recurring = additional_salary.is_recurring
)
def add_tax_components(self, payroll_period):
@@ -637,7 +640,7 @@
tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")
- def update_component_row(self, component_data, amount, component_type, additional_salary=None):
+ def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
component_row = None
for d in self.get(component_type):
if d.salary_component != component_data.salary_component:
@@ -678,6 +681,7 @@
component_row.set('abbr', abbr)
if additional_salary:
+ component_row.is_recurring_additional_salary = is_recurring
component_row.default_amount = 0
component_row.additional_amount = amount
component_row.additional_salary = additional_salary.name
@@ -711,6 +715,7 @@
# get remaining numbers of sub-period (period for which one salary is processed)
remaining_sub_periods = get_period_factor(self.employee,
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
+
# get taxable_earnings, paid_taxes for previous period
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
self.start_date, tax_slab.allow_tax_exemption)
@@ -870,8 +875,16 @@
if earning.is_tax_applicable:
if additional_amount:
- taxable_earnings += (amount - additional_amount)
- additional_income += additional_amount
+ if not earning.is_recurring_additional_salary:
+ taxable_earnings += (amount - additional_amount)
+ additional_income += additional_amount
+ else:
+ to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date')
+ period = (getdate(to_date).month - getdate(self.start_date).month) + 1
+ if period > 0:
+ taxable_earnings += (amount - additional_amount) * period
+ additional_income += additional_amount * period
+
if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
continue
@@ -1091,6 +1104,7 @@
"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 ce88cc3..6e8d3b3 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -482,14 +482,19 @@
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
- employee = frappe.db.get_value("Employee", {"user_id": user})
- salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
+ employee = frappe.db.get_value("Employee",
+ {
+ "user_id": user
+ },
+ ["name", "company", "employee_name"],
+ as_dict=True)
+
+ 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 e7d123c..3957d83 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 @@
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/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index 211b94a..d60b1a2 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -101,7 +101,7 @@
return html
def set_item_group_filters(field_filters):
- if 'item_group' in field_filters:
+ if field_filters is not None and 'item_group' in field_filters:
field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index c8bd80f..ae38d4c 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -15,12 +15,15 @@
WorkstationHolidayError)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.setup.utils import get_exchange_rate
+from erpnext.hr.utils import validate_active_employee
class OverlapError(frappe.ValidationError): pass
class OverWorkLoggedError(frappe.ValidationError): pass
class Timesheet(Document):
def validate(self):
+ if self.employee:
+ validate_active_employee(self.employee)
self.set_employee_name()
self.set_status()
self.validate_dates()
diff --git a/erpnext/public/js/contact.js b/erpnext/public/js/contact.js
new file mode 100644
index 0000000..41a0e8a
--- /dev/null
+++ b/erpnext/public/js/contact.js
@@ -0,0 +1,16 @@
+
+
+frappe.ui.form.on("Contact", {
+ refresh(frm) {
+ frm.set_query('link_doctype', "links", function() {
+ return {
+ query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes",
+ filters: {
+ fieldtype: ["in", ["HTML", "Text Editor"]],
+ fieldname: ["in", ["contact_html", "company_description"]],
+ }
+ };
+ });
+ frm.refresh_field("links");
+ }
+});
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index aa9bba1..d0c935f 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -54,7 +54,7 @@
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 @@
label: "PayPal Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/paypal-integration",
+ "user/manual/en/erpnext_integration/paypal-integration",
},
];
@@ -215,14 +215,14 @@
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 @@
{
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 @@
label: "Stripe Settings",
url:
docsUrl +
- "user/manual/en/setting-up/integrations/stripe-integration",
+ "user/manual/en/erpnext_integration/stripe-integration",
},
];
@@ -991,7 +991,7 @@
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/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js
index ebe6cd9..7bd21df 100644
--- a/erpnext/public/js/utils/customer_quick_entry.js
+++ b/erpnext/public/js/utils/customer_quick_entry.js
@@ -1,9 +1,9 @@
frappe.provide('frappe.ui.form');
frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({
- init: function(doctype, after_insert) {
+ init: function(doctype, after_insert, init_callback, doc, force) {
+ this._super(doctype, after_insert, init_callback, doc, force);
this.skip_redirect_on_error = true;
- this._super(doctype, after_insert);
},
render_dialog: function() {
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
index cc2d9f0..54e4886 100644
--- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('E Invoice Settings', {
refresh(frm) {
- const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing';
+ const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing';
frm.dashboard.set_headline(
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
);
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 6415204..0ee5b09 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -214,9 +214,8 @@
for d in item_details:
if d.item_code not in self.invoice_items.get(d.parent, {}):
- self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
- sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details
- if i.item_code == d.item_code and i.parent == d.parent))
+ self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+ self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
self.is_nil_exempt.append(d.item_code)
@@ -281,9 +280,15 @@
if self.get('invoice_items'):
# Build itemised tax for export invoices, nil and exempted where tax table is blank
for invoice, items in iteritems(self.invoice_items):
- if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type')
- == "Without Payment of Tax"):
+ if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \
+ == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
+ else:
+ for item in items.keys():
+ if item in self.is_nil_exempt + self.is_non_gst and \
+ item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []):
+ self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, [])
+ self.items_based_on_tax_rate[invoice][0].append(item)
def set_outward_taxable_supplies(self):
inter_state_supply_details = {}
@@ -322,6 +327,9 @@
inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value
inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
+ if self.invoice_cess.get(inv):
+ self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2)
+
self.set_inter_state_supply(inter_state_supply_details)
def set_supplies_liable_to_reverse_charge(self):
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 9265460..e9372f9 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -641,7 +641,6 @@
'label': 'Export Type',
'fieldtype': 'Select',
'insert_after': 'gst_category',
- 'default': 'Without Payment of Tax',
'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
@@ -660,7 +659,6 @@
'label': 'Export Type',
'fieldtype': 'Select',
'insert_after': 'gst_category',
- 'default': 'Without Payment of Tax',
'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index cfcb8c3..4b73094 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -217,9 +217,8 @@
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
- self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
- sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items
- if i.item_code == d.item_code and i.parent == d.parent))
+ self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+ self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
item_tax_rate = {}
@@ -287,7 +286,8 @@
# Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
- and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax":
+ and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \
+ and self.invoices.get(invoice, {}).get('gst_category') == "Overseas":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self):
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 f7b2c1d..6e36d28 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -367,15 +367,16 @@
`<div class="add-discount-field"></div>`
);
const me = this;
+ const frm = me.events.get_frm();
+ let discount = frm.doc.additional_discount_percentage;
this.discount_field = frappe.ui.form.make_control({
df: {
label: __('Discount'),
fieldtype: 'Data',
- placeholder: __('Enter discount percentage.'),
+ placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ),
input_class: 'input-xs',
onchange: function() {
- const frm = me.events.get_frm();
if (flt(this.value) != 0) {
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
me.hide_discount_control(this.value);
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index ece9fb5..c3db27f 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -12,10 +12,14 @@
class TransactionDeletionRecord(Document):
def validate(self):
frappe.only_for('System Manager')
+ self.validate_doctypes_to_be_ignored()
+
+ def validate_doctypes_to_be_ignored(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in self.doctypes_to_be_ignored:
if doctype.doctype_name not in doctypes_to_be_ignored_list:
- frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed"))
+ frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."),
+ title=_("Not Allowed"))
def before_submit(self):
if not self.doctypes_to_be_ignored:
@@ -23,54 +27,9 @@
self.delete_bins()
self.delete_lead_addresses()
-
- company_obj = frappe.get_doc('Company', self.company)
- # reset company values
- company_obj.total_monthly_sales = 0
- company_obj.sales_monthly_history = None
- company_obj.save()
- # Clear notification counts
+ self.reset_company_values()
clear_notifications()
-
- singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
- tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
- doctypes_to_be_ignored_list = singles
- for doctype in self.doctypes_to_be_ignored:
- doctypes_to_be_ignored_list.append(doctype.doctype_name)
-
- docfields = frappe.get_all('DocField',
- filters = {
- 'fieldtype': 'Link',
- 'options': 'Company',
- 'parent': ['not in', doctypes_to_be_ignored_list]},
- fields=['parent', 'fieldname'])
-
- for docfield in docfields:
- if docfield['parent'] != self.doctype:
- no_of_docs = frappe.db.count(docfield['parent'], {
- docfield['fieldname'] : self.company
- })
-
- if no_of_docs > 0:
- self.delete_version_log(docfield['parent'], docfield['fieldname'])
- self.delete_communications(docfield['parent'], docfield['fieldname'])
-
- # populate DocTypes table
- if docfield['parent'] not in tables:
- self.append('doctypes', {
- 'doctype_name' : docfield['parent'],
- 'no_of_docs' : no_of_docs
- })
-
- # delete the docs linked with the specified company
- frappe.db.delete(docfield['parent'], {
- docfield['fieldname'] : self.company
- })
-
- naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
- if naming_series:
- if '#' in naming_series:
- self.update_naming_series(naming_series, docfield['parent'])
+ self.delete_company_transactions()
def populate_doctypes_to_be_ignored_table(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
@@ -79,6 +38,111 @@
'doctype_name' : doctype
})
+ def delete_bins(self):
+ frappe.db.sql("""delete from tabBin where warehouse in
+ (select name from tabWarehouse where company=%s)""", self.company)
+
+ def delete_lead_addresses(self):
+ """Delete addresses to which leads are linked"""
+ leads = frappe.get_all('Lead', filters={'company': self.company})
+ leads = ["'%s'" % row.get("name") for row in leads]
+ addresses = []
+ if leads:
+ addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
+ in ({leads})""".format(leads=",".join(leads)))
+
+ if addresses:
+ addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
+
+ frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
+ name not in (select distinct dl1.parent from `tabDynamic Link` dl1
+ inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
+ and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
+
+ frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
+ and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
+
+ frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
+
+ def reset_company_values(self):
+ company_obj = frappe.get_doc('Company', self.company)
+ company_obj.total_monthly_sales = 0
+ company_obj.sales_monthly_history = None
+ company_obj.save()
+
+ def delete_company_transactions(self):
+ doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
+ docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
+
+ tables = self.get_all_child_doctypes()
+ for docfield in docfields:
+ if docfield['parent'] != self.doctype:
+ no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
+
+ if no_of_docs > 0:
+ self.delete_version_log(docfield['parent'], docfield['fieldname'])
+ self.delete_communications(docfield['parent'], docfield['fieldname'])
+ self.populate_doctypes_table(tables, docfield['parent'], no_of_docs)
+
+ self.delete_child_tables(docfield['parent'], docfield['fieldname'])
+ self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname'])
+
+ naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
+ if naming_series:
+ if '#' in naming_series:
+ self.update_naming_series(naming_series, docfield['parent'])
+
+ def get_doctypes_to_be_ignored_list(self):
+ singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
+ doctypes_to_be_ignored_list = singles
+ for doctype in self.doctypes_to_be_ignored:
+ doctypes_to_be_ignored_list.append(doctype.doctype_name)
+
+ return doctypes_to_be_ignored_list
+
+ def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list):
+ docfields = frappe.get_all('DocField',
+ filters = {
+ 'fieldtype': 'Link',
+ 'options': 'Company',
+ 'parent': ['not in', doctypes_to_be_ignored_list]},
+ fields=['parent', 'fieldname'])
+
+ return docfields
+
+ def get_all_child_doctypes(self):
+ return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
+
+ def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
+ return frappe.db.count(doctype, {company_fieldname : self.company})
+
+ def populate_doctypes_table(self, tables, doctype, no_of_docs):
+ if doctype not in tables:
+ self.append('doctypes', {
+ 'doctype_name' : doctype,
+ 'no_of_docs' : no_of_docs
+ })
+
+ def delete_child_tables(self, doctype, company_fieldname):
+ parent_docs_to_be_deleted = frappe.get_all(doctype, {
+ company_fieldname : self.company
+ }, pluck = 'name')
+
+ child_tables = frappe.get_all('DocField', filters = {
+ 'fieldtype': 'Table',
+ 'parent': doctype
+ }, pluck = 'options')
+
+ for table in child_tables:
+ frappe.db.delete(table, {
+ 'parent': ['in', parent_docs_to_be_deleted]
+ })
+
+ def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
+ frappe.db.delete(doctype, {
+ company_fieldname : self.company
+ })
+
def update_naming_series(self, naming_series, doctype_name):
if '.' in naming_series:
prefix, hashes = naming_series.rsplit('.', 1)
@@ -107,32 +171,6 @@
frappe.delete_doc('Communication', communication_names, ignore_permissions=True)
- def delete_bins(self):
- frappe.db.sql("""delete from tabBin where warehouse in
- (select name from tabWarehouse where company=%s)""", self.company)
-
- def delete_lead_addresses(self):
- """Delete addresses to which leads are linked"""
- leads = frappe.get_all('Lead', filters={'company': self.company})
- leads = ["'%s'" % row.get("name") for row in leads]
- addresses = []
- if leads:
- addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
- in ({leads})""".format(leads=",".join(leads)))
-
- if addresses:
- addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
-
- frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
- name not in (select distinct dl1.parent from `tabDynamic Link` dl1
- inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
- and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
-
- frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
- and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
-
- frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
-
@frappe.whitelist()
def get_doctypes_to_be_ignored():
doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget',
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index b6eef6c..b37ae3f 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -162,19 +162,19 @@
out = float(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry`
- where warehouse=%s and batch_no=%s {0}""".format(cond),
+ where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond),
(warehouse, batch_no))[0][0] or 0)
if batch_no and not warehouse:
out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty
from `tabStock Ledger Entry`
- where batch_no=%s
+ where is_cancelled = 0 and batch_no=%s
group by warehouse''', batch_no, as_dict=1)
if not batch_no and item_code and warehouse:
out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty
from `tabStock Ledger Entry`
- where item_code = %s and warehouse=%s
+ where is_cancelled = 0 and item_code = %s and warehouse=%s
group by batch_no''', (item_code, warehouse), as_dict=1)
return out
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index b55374b..264baea 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -100,10 +100,11 @@
frm.add_custom_button(__('Duplicate'), function() {
var new_item = frappe.model.copy_doc(frm.doc);
- if(new_item.item_name===new_item.item_code) {
+ // Duplicate item could have different name, causing "copy paste" error.
+ if (new_item.item_name===new_item.item_code) {
new_item.item_name = null;
}
- if(new_item.description===new_item.description) {
+ if (new_item.item_code===new_item.description || new_item.item_code===new_item.description) {
new_item.description = null;
}
frappe.set_route('Form', 'Item', new_item.name);
@@ -186,8 +187,6 @@
item_code: function(frm) {
if(!frm.doc.item_name)
frm.set_value("item_name", frm.doc.item_code);
- if(!frm.doc.description)
- frm.set_value("description", frm.doc.item_code);
},
is_stock_item: function(frm) {
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index e795742..516ae43 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -239,6 +239,7 @@
and sle.`item_code`=%(item_code)s
and sle.`company` = %(company)s
and batch.disabled = 0
+ and sle.is_cancelled=0
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition}
GROUP BY
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 82c87a8..26ea11e 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -415,7 +415,7 @@
"cost_center": cost_center,
"debit": debit,
"credit": credit,
- "against_account": against_account,
+ "against": against_account,
"remarks": remarks,
}
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 872b1d0..654755e 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1789,7 +1789,7 @@
from `tabBatch` b, `tabStock Ledger Entry` sle
where b.expiry_date <= %s
and b.expiry_date is not NULL
- and b.batch_id = sle.batch_no
+ and b.batch_id = sle.batch_no and sle.is_cancelled = 0
group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1)
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 93482e8..b4f4583 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -60,7 +60,7 @@
if self.batch_no and not self.get("allow_negative_stock"):
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry`
- where warehouse=%s and item_code=%s and batch_no=%s""",
+ where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
(self.warehouse, self.item_code, self.batch_no))[0][0])
if batch_bal_after_transaction < 0:
@@ -152,7 +152,7 @@
last_transaction_time = frappe.db.sql("""
select MAX(timestamp(posting_date, posting_time)) as posting_time
from `tabStock Ledger Entry`
- where docstatus = 1 and item_code = %s
+ where docstatus = 1 and is_cancelled = 0 and item_code = %s
and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 4657700..cf52803 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -74,9 +74,8 @@
update_party_blanket_order(args, out)
- if not doc or cint(doc.get('is_return')) == 0:
- # get price list rate only if the invoice is not a credit or debit note
- get_price_list_rate(args, item, out)
+
+ get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True))
diff --git a/erpnext/stock/report/cogs_by_item_group/__init__.py b/erpnext/stock/report/cogs_by_item_group/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/cogs_by_item_group/__init__.py
diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js
new file mode 100644
index 0000000..d7c50a6
--- /dev/null
+++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.js
@@ -0,0 +1,31 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+
+frappe.query_reports["COGS By Item Group"] = {
+ filters: [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ mandatory: true,
+ default: frappe.defaults.get_user_default("Company"),
+ },
+ {
+ label: __("From Date"),
+ fieldname: "from_date",
+ fieldtype: "Date",
+ mandatory: true,
+ default: frappe.datetime.year_start(),
+ },
+ {
+ label: __("To Date"),
+ fieldname: "to_date",
+ fieldtype: "Date",
+ mandatory: true,
+ default: frappe.datetime.get_today(),
+ },
+ ]
+};
diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json
new file mode 100644
index 0000000..a14adf8
--- /dev/null
+++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-06-02 18:59:19.830928",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-06-02 18:59:55.470621",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "COGS By Item Group",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "COGS By Item Group",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Auditor"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py
new file mode 100644
index 0000000..9e5e63e
--- /dev/null
+++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py
@@ -0,0 +1,188 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from collections import OrderedDict
+import datetime
+from typing import Dict, List, Tuple, Union
+
+import frappe
+from frappe import _
+from frappe.utils import date_diff
+
+from erpnext.accounts.report.general_ledger.general_ledger import get_gl_entries
+
+
+Filters = frappe._dict
+Row = frappe._dict
+Data = List[Row]
+Columns = List[Dict[str, str]]
+DateTime = Union[datetime.date, datetime.datetime]
+FilteredEntries = List[Dict[str, Union[str, float, DateTime, None]]]
+ItemGroupsDict = Dict[Tuple[int, int], Dict[str, Union[str, int]]]
+SVDList = List[frappe._dict]
+
+
+def execute(filters: Filters) -> Tuple[Columns, Data]:
+ update_filters_with_account(filters)
+ validate_filters(filters)
+ columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+
+def update_filters_with_account(filters: Filters) -> None:
+ account = frappe.get_value("Company", filters.get("company"), "default_expense_account")
+ filters.update(dict(account=account))
+
+
+def validate_filters(filters: Filters) -> None:
+ if filters.from_date > filters.to_date:
+ frappe.throw(_("From Date must be before To Date"))
+
+
+def get_columns() -> Columns:
+ return [
+ {
+ 'label': 'Item Group',
+ 'fieldname': 'item_group',
+ 'fieldtype': 'Data',
+ 'width': '200'
+ },
+ {
+ 'label': 'COGS Debit',
+ 'fieldname': 'cogs_debit',
+ 'fieldtype': 'Currency',
+ 'width': '200'
+ }
+ ]
+
+
+def get_data(filters: Filters) -> Data:
+ filtered_entries = get_filtered_entries(filters)
+ svd_list = get_stock_value_difference_list(filtered_entries)
+ leveled_dict = get_leveled_dict()
+
+ assign_self_values(leveled_dict, svd_list)
+ assign_agg_values(leveled_dict)
+
+ data = []
+ for item in leveled_dict.items():
+ i = item[1]
+ if i['agg_value'] == 0:
+ continue
+ data.append(get_row(i['name'], i['agg_value'], i['is_group'], i['level']))
+ if i['self_value'] < i['agg_value'] and i['self_value'] > 0:
+ data.append(get_row(i['name'], i['self_value'], 0, i['level'] + 1))
+ return data
+
+
+def get_filtered_entries(filters: Filters) -> FilteredEntries:
+ gl_entries = get_gl_entries(filters, [])
+ filtered_entries = []
+ for entry in gl_entries:
+ posting_date = entry.get('posting_date')
+ from_date = filters.get('from_date')
+ if date_diff(from_date, posting_date) > 0:
+ continue
+ filtered_entries.append(entry)
+ return filtered_entries
+
+
+def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDList:
+ voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
+ svd_list = frappe.get_list(
+ 'Stock Ledger Entry', fields=['item_code','stock_value_difference'],
+ filters=[('voucher_no', 'in', voucher_nos)]
+ )
+ assign_item_groups_to_svd_list(svd_list)
+ return svd_list
+
+
+def get_leveled_dict() -> OrderedDict:
+ item_groups_dict = get_item_groups_dict()
+ lr_list = sorted(item_groups_dict, key=lambda x : int(x[0]))
+ leveled_dict = OrderedDict()
+ current_level = 0
+ nesting_r = []
+ for l, r in lr_list:
+ while current_level > 0 and nesting_r[-1] < l:
+ nesting_r.pop()
+ current_level -= 1
+
+ leveled_dict[(l,r)] = {
+ 'level' : current_level,
+ 'name' : item_groups_dict[(l,r)]['name'],
+ 'is_group' : item_groups_dict[(l,r)]['is_group']
+ }
+
+ if int(r) - int(l) > 1:
+ current_level += 1
+ nesting_r.append(r)
+
+ update_leveled_dict(leveled_dict)
+ return leveled_dict
+
+
+def assign_self_values(leveled_dict: OrderedDict, svd_list: SVDList) -> None:
+ key_dict = {v['name']:k for k, v in leveled_dict.items()}
+ for item in svd_list:
+ key = key_dict[item.get("item_group")]
+ leveled_dict[key]['self_value'] += -item.get("stock_value_difference")
+
+
+def assign_agg_values(leveled_dict: OrderedDict) -> None:
+ keys = list(leveled_dict.keys())[::-1]
+ prev_level = leveled_dict[keys[-1]]['level']
+ accu = [0]
+ for k in keys[:-1]:
+ curr_level = leveled_dict[k]['level']
+ if curr_level == prev_level:
+ accu[-1] += leveled_dict[k]['self_value']
+ leveled_dict[k]['agg_value'] = leveled_dict[k]['self_value']
+
+ elif curr_level > prev_level:
+ accu.append(leveled_dict[k]['self_value'])
+ leveled_dict[k]['agg_value'] = accu[-1]
+
+ elif curr_level < prev_level:
+ accu[-1] += leveled_dict[k]['self_value']
+ leveled_dict[k]['agg_value'] = accu[-1]
+
+ prev_level = curr_level
+
+ # root node
+ rk = keys[-1]
+ leveled_dict[rk]['agg_value'] = sum(accu) + leveled_dict[rk]['self_value']
+
+
+def get_row(name:str, value:float, is_bold:int, indent:int) -> Row:
+ item_group = name
+ if is_bold:
+ item_group = frappe.bold(item_group)
+ return frappe._dict(item_group=item_group, cogs_debit=value, indent=indent)
+
+
+def assign_item_groups_to_svd_list(svd_list: SVDList) -> None:
+ ig_map = get_item_groups_map(svd_list)
+ for item in svd_list:
+ item.item_group = ig_map[item.get("item_code")]
+
+
+def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]:
+ item_codes = set(i['item_code'] for i in svd_list)
+ ig_list = frappe.get_list(
+ 'Item', fields=['item_code','item_group'],
+ filters=[('item_code', 'in', item_codes)]
+ )
+ return {i['item_code']:i['item_group'] for i in ig_list}
+
+
+def get_item_groups_dict() -> ItemGroupsDict:
+ item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt"))
+ return {(i['lft'],i['rgt']):{'name':i['name'], 'is_group':i['is_group']}
+ for i in item_groups_list}
+
+
+def update_leveled_dict(leveled_dict: OrderedDict) -> None:
+ for k in leveled_dict:
+ leveled_dict[k].update({'self_value':0, 'agg_value':0})
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index 14d543b..bfc4471 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -22,6 +22,7 @@
data = []
filters = {
+ "is_cancelled": 0,
"company": report_filters.company,
"posting_date": ("<=", report_filters.as_on_date)
}
@@ -34,7 +35,7 @@
key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0)
- d.difference_value = (d.stock_value - d.account_value)
+ d.difference_value = abs(d.stock_value - d.account_value)
if abs(d.difference_value) > 0.1:
data.append(d)
diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py
index 5873a7a..4108a57 100644
--- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py
+++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py
@@ -69,7 +69,7 @@
i.stock_uom, sle.actual_qty, sle.stock_value_difference,
sle.voucher_no, sle.voucher_type
from `tabStock Ledger Entry` sle, `tabItem` i
- where sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1):
+ where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1):
consumed_details.setdefault(d.item_code, []).append(d)
return consumed_details
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 8a6a3a3..b57b2aa 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -314,13 +314,16 @@
for row_idx, row in enumerate(result):
data = row.items() if is_dict_obj else enumerate(row)
for key, value in data:
- if key not in convertible_columns or not conversion_factors[row_idx-1]:
+ if key not in convertible_columns:
continue
+ # If no conversion factor for the UOM, defaults to 1
+ if not conversion_factors[row_idx]:
+ conversion_factors[row_idx] = 1
if convertible_columns.get(key) == 'rate':
- new_value = flt(value) * conversion_factors[row_idx-1]
+ new_value = flt(value) * conversion_factors[row_idx]
else:
- new_value = flt(value) / conversion_factors[row_idx-1]
+ new_value = flt(value) / conversion_factors[row_idx]
if not is_dict_obj:
row.insert(key+1, new_value)
@@ -386,4 +389,4 @@
reposting_in_progress = frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
if reposting_in_progress:
- frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
\ No newline at end of file
+ frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)