diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml index faab3344a6..d9603e89aa 100644 --- a/.github/helper/semgrep_rules/frappe_correctness.yml +++ b/.github/helper/semgrep_rules/frappe_correctness.yml @@ -98,8 +98,6 @@ rules: 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 5a5098bf50..8b21979208 100644 --- a/.github/helper/semgrep_rules/security.yml +++ b/.github/helper/semgrep_rules/security.yml @@ -8,18 +8,3 @@ rules: 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/backport.yml b/.github/workflows/backport.yml index 7c6b8432b8..1d180f251e 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,16 +1,25 @@ name: Backport on: - pull_request: + pull_request_target: types: - closed - labeled jobs: - backport: - runs-on: ubuntu-18.04 - name: Backport + main: + runs-on: ubuntu-latest steps: - - name: Backport - uses: tibdex/backport@v1 + - name: Checkout Actions + uses: actions/checkout@v2 with: - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + repository: "frappe/backport" + path: ./actions + ref: develop + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run backport + uses: ./actions/backport + with: + token: ${{secrets.BACKPORT_BOT_TOKEN}} + labelsToAdd: "backport" + title: "{{originalTitle}}" diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 389524e968..e27b406df0 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 7cf65a7a73..a4a14de1b8 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 @pateljannat +erpnext/education/ @ruchamahabal @pateljannat +erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand +erpnext/hr/ @ruchamahabal @pateljannat +erpnext/non_profit/ @ruchamahabal +erpnext/payroll @ruchamahabal @pateljannat +erpnext/projects/ @ruchamahabal @pateljannat + +erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination + +.github/ @surajshetty3416 @ankush +requirements.txt @gavindsouza diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 1be2fbf5c8..f763df0852 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -230,7 +230,7 @@ class Account(NestedSet): if self.check_gle_exists(): throw(_("Account with existing transaction can not be converted to group.")) elif self.account_type and not self.flags.exclude_account_type_check: - throw(_("Cannot covert to Group because Account Type is selected.")) + throw(_("Cannot convert to Group because Account Type is selected.")) else: self.is_group = 1 self.save() diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 603e21ea24..6c25f0024d 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): if budget_against_field == "project": - budget_against = "_Test Project" + budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"}) else: budget_against = budget_against_CC or "_Test Cost Center - _TC" @@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) elif budget_against_field == "project": make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate()) + "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate()) def make_budget(**args): args = frappe._dict(args) 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 3b764aab10..8456b49c8e 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -13,7 +13,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file class ChartofAccountsImporter(Document): - pass + def validate(self): + validate_accounts(self.import_file) @frappe.whitelist() def validate_company(company): @@ -301,28 +302,27 @@ def validate_accounts(file_name): if account["parent_account"] and accounts_dict.get(account["parent_account"]): accounts_dict[account["parent_account"]]["is_group"] = 1 - message = validate_root(accounts_dict) - if message: return message - message = validate_account_types(accounts_dict) - if message: return message + validate_root(accounts_dict) + + validate_account_types(accounts_dict) return [True, len(accounts)] def validate_root(accounts): roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] if len(roots) < 4: - return _("Number of root accounts cannot be less than 4") + frappe.throw(_("Number of root accounts cannot be less than 4")) error_messages = [] for account in roots: if not account.get("root_type") and account.get("account_name"): - error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) + error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) elif account.get("root_type") not in get_root_types() and account.get("account_name"): - error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) + error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) if error_messages: - return "
".join(error_messages) + frappe.throw("
".join(error_messages)) def get_root_types(): return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') @@ -356,7 +356,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_ledger) - set(account_types)) if missing: - return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))) account_types_for_group = ["Bank", "Cash", "Stock"] # fix logic bug @@ -364,7 +364,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_group) - set(account_groups)) if missing: - return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))) def unset_existing_data(company): linked = frappe.db.sql('''select fieldname from tabDocField @@ -391,5 +391,5 @@ def set_default_accounts(company): }) 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/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c6c689212b..1ef512a489 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -25,7 +25,7 @@ class Dunning(AccountsController): def validate_amount(self): amounts = calculate_interest_and_amount( - self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) + self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) if self.interest_amount != amounts.get('interest_amount'): self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.dunning_amount != amounts.get('dunning_amount'): @@ -91,13 +91,13 @@ def resolve_dunning(doc, state): for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') -def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): +def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 - grand_total = 0 + grand_total = flt(outstanding_amount) + flt(dunning_fee) if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) + grand_total += flt(interest_amount) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { 'interest_amount': interest_amount, diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e2d4d82e41..ed50f784b2 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): create_dunning_type() + create_dunning_type_with_zero_interest_rate() unlink_payment_on_cancel_of_invoice() @classmethod @@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase): def test_dunning(self): dunning = create_dunning() amounts = calculate_interest_and_amount( - dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) + def test_dunning_with_zero_interest_rate(self): + dunning = create_dunning_with_zero_interest_rate() + amounts = calculate_interest_and_amount( + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + self.assertEqual(round(amounts.get('interest_amount'), 2), 0) + self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) + self.assertEqual(round(amounts.get('grand_total'), 2), 120) + + def test_gl_entries(self): dunning = create_dunning() dunning.submit() @@ -83,6 +93,27 @@ def create_dunning(): dunning.save() return dunning +def create_dunning_with_zero_interest_rate(): + posting_date = add_days(today(), -20) + due_date = add_days(today(), -15) + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=posting_date, due_date=due_date, status='Overdue') + dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') + dunning = frappe.new_doc("Dunning") + dunning.sales_invoice = sales_invoice.name + dunning.customer_name = sales_invoice.customer_name + dunning.outstanding_amount = sales_invoice.outstanding_amount + dunning.debit_to = sales_invoice.debit_to + dunning.currency = sales_invoice.currency + dunning.company = sales_invoice.company + dunning.posting_date = nowdate() + dunning.due_date = sales_invoice.due_date + dunning.dunning_type = 'First Notice with 0% Rate of Interest' + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee + dunning.save() + return dunning + def create_dunning_type(): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = 'First Notice' @@ -98,3 +129,19 @@ def create_dunning_type(): } ) dunning_type.save() + +def create_dunning_type_with_zero_interest_rate(): + dunning_type = frappe.new_doc("Dunning Type") + dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' + dunning_type.start_day = 10 + dunning_type.end_day = 20 + dunning_type.dunning_fee = 20 + dunning_type.rate_of_interest = 0 + dunning_type.append( + "dunning_letter_text", { + 'language': 'en', + 'body_text': 'We have still not received payment for our invoice ', + 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' + } + ) + dunning_type.save() \ No newline at end of file diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 56193216a2..f2b0a8c08a 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 @@ class ExchangeRateRevaluation(Document): 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 @@ class ExchangeRateRevaluation(Document): 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 @@ class ExchangeRateRevaluation(Document): "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 @@ class ExchangeRateRevaluation(Document): "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 @@ def get_account_details(account, company, posting_date, party_type=None, party=N 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.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d3ac3a6676..439b1edbce 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 51f18a5a4e..6f362c1fbb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -667,6 +667,7 @@ { "fieldname": "base_paid_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Paid Amount After Tax (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 @@ -693,21 +694,25 @@ "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax", - "options": "paid_to_account_currency" + "options": "paid_to_account_currency", + "read_only": 1 }, { "depends_on": "doc.received_amount", "fieldname": "base_received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax (Company Currency)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-22 20:37:06.154206", + "modified": "2021-07-09 08:58:15.008761", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0c21aae944..46904f7c57 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -183,6 +183,13 @@ class PaymentEntry(AccountsController): d.reference_name, self.party_account_currency) for field, value in iteritems(ref_details): + if d.exchange_gain_loss: + # for cases where gain/loss is booked into invoice + # exchange_gain_loss is calculated from invoice & populated + # and row.exchange_rate is already set to payment entry's exchange rate + # refer -> `update_reference_in_payment_entry()` in utils.py + continue + if field == 'exchange_rate' or not d.get(field) or force: d.db_set(field, value) @@ -404,9 +411,15 @@ class PaymentEntry(AccountsController): if not self.advance_tax_account: frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - reference_doclist = [] net_total = self.paid_amount - included_in_paid_amount = 0 + + for reference in self.get("references"): + net_total_for_tds = 0 + if reference.reference_doctype == 'Purchase Order': + net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) + + if net_total_for_tds: + net_total = net_total_for_tds # Adding args as purchase invoice to get TDS amount args = frappe._dict({ @@ -423,7 +436,7 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ - 'included_in_paid_amount': included_in_paid_amount, + 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -512,16 +525,19 @@ class PaymentEntry(AccountsController): self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive" \ - and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ - and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.received_amount_after_tax + total_deductions - + and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ + and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): + self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + self.unallocated_amount -= included_taxes elif self.payment_type == "Pay" \ - and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \ - and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate): - self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions + + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ + and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): + self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate + self.unallocated_amount -= included_taxes def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) @@ -530,17 +546,29 @@ class PaymentEntry(AccountsController): base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount_after_tax + self.difference_amount = base_party_amount - self.base_received_amount elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount_after_tax - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount else: - self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() - self.difference_amount = flt(self.difference_amount - total_deductions, + self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")) + def get_included_taxes(self): + included_taxes = 0 + for tax in self.get('taxes'): + if tax.included_in_paid_amount: + if tax.add_deduct_tax == 'Add': + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount + + return included_taxes + # Paid amount is auto allocated in the reference document by default. # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast def clear_unallocated_reference_document_rows(self): @@ -664,8 +692,8 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) if self.unallocated_amount: - base_unallocated_amount = self.unallocated_amount * \ - (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate) + exchange_rate = self.get_exchange_rate() + base_unallocated_amount = (self.unallocated_amount * exchange_rate) gle = party_gl_dict.copy() @@ -683,8 +711,8 @@ class PaymentEntry(AccountsController): "account": self.paid_from, "account_currency": self.paid_from_account_currency, "against": self.party if self.payment_type=="Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount_after_tax, - "credit": self.base_paid_amount_after_tax, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, "cost_center": self.cost_center }, item=self) ) @@ -694,8 +722,8 @@ class PaymentEntry(AccountsController): "account": self.paid_to, "account_currency": self.paid_to_account_currency, "against": self.party if self.payment_type=="Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount_after_tax, - "debit": self.base_received_amount_after_tax, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, "cost_center": self.cost_center }, item=self) ) @@ -708,35 +736,42 @@ class PaymentEntry(AccountsController): if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" + against = self.party or self.paid_from elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" + against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() + tax_amount = d.tax_amount + base_tax_amount = d.base_tax_amount + + if self.advance_tax_account: + tax_amount = -1 * tax_amount + base_tax_amount = -1 * base_tax_amount gl_entries.append( self.get_gl_dict({ "account": d.account_head, - "against": self.party if self.payment_type=="Receive" else self.paid_from, - dr_or_cr: d.base_tax_amount, - dr_or_cr + "_in_account_currency": d.base_tax_amount + "against": against, + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": d.cost_center }, account_currency, item=d)) #Intentionally use -1 to get net values in party account - gl_entries.append( - self.get_gl_dict({ - "account": payment_or_advance_account, - "against": self.party if self.payment_type=="Receive" else self.paid_from, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - "party_type": self.party_type, - "party": self.party - }, account_currency, item=d)) + if not d.included_in_paid_amount or self.advance_tax_account: + gl_entries.append( + self.get_gl_dict({ + "account": payment_or_advance_account, + "against": against, + dr_or_cr: -1 * tax_amount, + dr_or_cr + "_in_account_currency": -1 * base_tax_amount + if account_currency==self.company_currency + else d.tax_amount, + "cost_center": self.cost_center, + }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): for d in self.get("deductions"): @@ -760,9 +795,9 @@ class PaymentEntry(AccountsController): if self.advance_tax_account: return self.advance_tax_account elif self.payment_type == 'Receive': - return self.paid_from - elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to + elif self.payment_type in ('Pay', 'Internal Transfer'): + return self.paid_from def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: @@ -806,10 +841,17 @@ class PaymentEntry(AccountsController): if account_details: row.update(account_details) + + if not row.get('amount'): + # if no difference amount + return self.append('deductions', row) self.set_unallocated_amount() + def get_exchange_rate(self): + return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate + def initialize_taxes(self): for tax in self.get("taxes"): validate_taxes_and_charges(tax) @@ -1634,12 +1676,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) - if dt == "Purchase Order" and doc.apply_tds: - if party_account_currency == bank.account_currency: - paid_amount = received_amount = doc.base_net_total - else: - paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) - return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): 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 912ad0977a..43eb0b6e2a 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 6635128f9e..d788d91855 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ] }) - + jv.flags.ignore_mandatory = True jv.submit() \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 7459c11d4d..33c3e0432b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1545,6 +1545,7 @@ "fieldname": "consolidated_invoice", "fieldtype": "Link", "label": "Consolidated Sales Invoice", + "no_copy": 1, "options": "Sales Invoice", "read_only": 1 } @@ -1552,7 +1553,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-02-01 15:03:33.800707", + "modified": "2021-07-29 13:37:20.636171", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b54d0e73a8..94abf3b3c0 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): 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.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index dc9094c3e9..c588d45a9f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }, get_query_filters: { docstatus: 1, - status: ["not in", ["Closed", "Completed"]], + status: ["not in", ["Closed", "Completed", "Return Issued"]], company: me.frm.doc.company, is_return: 0 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 45d89ad1c8..f7992797ed 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -451,6 +451,7 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 311745d3cd..4bc22a544d 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 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + def test_gain_loss_with_advance_entry(self): + unlink_enabled = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", + "unlink_payment_on_cancel_of_invoice") + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", + "unlink_payment_on_cancel_of_invoice", 1) + + original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") + frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC") + + pay = frappe.get_doc({ + 'doctype': 'Payment Entry', + 'company': '_Test Company', + 'payment_type': 'Pay', + 'party_type': 'Supplier', + 'party': '_Test Supplier USD', + 'paid_to': '_Test Payable USD - _TC', + 'paid_from': 'Cash - _TC', + 'paid_amount': 70000, + 'target_exchange_rate': 70, + 'received_amount': 1000, + }) + pay.insert() + pay.submit() + + pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=75, rate=500, do_not_save=1, qty=1) + pi.cost_center = "_Test Cost Center - _TC" + pi.advances = [] + pi.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 1000, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 37500.0], + ["_Test Payable USD - _TC", -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 5801b17f66..63dfff8921 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.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e7dd6b8a60..0a9a105b7c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -48,6 +48,8 @@ "shipping_address", "company_address", "company_address_display", + "dispatch_address_name", + "dispatch_address", "currency_and_price_list", "currency", "conversion_rate", @@ -1966,6 +1968,21 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -1978,7 +1995,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-05-20 22:48:33.988881", + "modified": "2021-07-08 14:03:55.502522", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 55a5b99907..6d1f6249c1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -840,6 +840,7 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index dbc7f8632f..70bccd7166 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1908,6 +1908,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) + self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) + self.assertEqual(data['billLists'][0]['fromStateCode'],27) def test_einvoice_submission_without_irn(self): # init @@ -2062,6 +2064,30 @@ def make_test_address_for_ewaybill(): address.save() + if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'): + address = frappe.get_doc({ + "address_line1": "_Test Dispatch Address Line 1", + "address_title": "_Test Dispatch-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 0, + "phone": "+910000000000", + "gstin": "07AAACC1206D1ZI", + "gst_state": "Delhi", + "gst_state_number": "07", + "pincode": "1100101" + }).insert() + + address.append("links", { + "link_doctype": "Company", + "link_name": "_Test Company" + }) + + address.save() + def make_test_transporter_for_ewaybill(): if not frappe.db.exists('Supplier', '_Test Transporter'): frappe.get_doc({ @@ -2100,6 +2126,7 @@ def make_sales_invoice_for_ewaybill(): si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" si.customer_address = "_Test Customer-Address for Eway bill-Shipping" + si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping" si.vehicle_no = "KA12KA1234" si.gst_category = "Registered Regular" si.mode_of_transport = 'Road' 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 14bf4d8133..29422d68cf 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 1b7a0fe562..cfdb167bbc 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 f9160e281d..153906ffe9 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 b9ee4a0963..481ef285e7 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 @@ from __future__ import unicode_literals 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 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "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_lower_deduction_certificate(fiscal_year, pan_no): 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_deducted_tax(taxable_vouchers, fiscal_year, tax_details): 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 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu 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 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu 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 dd26be7c99..2ba22ca435 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 @@ class TestTaxWithholdingCategory(unittest.TestCase): 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_sales_invoice(**args): 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 @@ def create_tax_with_holding_category(): '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 a11b77a6f6..b54646fd27 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -99,7 +99,6 @@ class ReceivablePayableReport(object): 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 @@ class ReceivablePayableReport(object): 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 @@ class ReceivablePayableReport(object): 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 744ada9e55..1759fa3a48 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -48,17 +48,18 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - - for account in filters.account: - if not account_details.get(account): - frappe.throw(_("Account {0} does not exists").format(account)) if filters.get('account'): filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) - if (filters.get("account") and filters.get("group_by") == _('Group by Account') - and account_details[filters.account].is_group == 0): - frappe.throw(_("Can not filter based on Account, if grouped by Account")) + if (filters.get("account") and filters.get("group_by") == _('Group by Account')): + filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if account_details[account].is_group == 0: + frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) if (filters.get("voucher_no") and filters.get("group_by") in [_('Group by Voucher')]): diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 84c74543da..6d8623c189 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -241,6 +241,7 @@ class GrossProfitGenerator(object): 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 @@ class GrossProfitGenerator(object): 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/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index e15715dccd..6b9df41f54 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f select voucher_no, credit from `tabGL Entry` where party in (%s) and credit > 0 - and company=%s and posting_date between %s and %s + and company=%s and is_cancelled = 0 + and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) supplier_credit_amount = flt(sum(d.credit for d in entries)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index ed6e28da1e..9afe365f74 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate + "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation } if d.voucher_detail_no: @@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.set_amounts() if d.difference_amount and d.difference_account: - payment_entry.set_gain_or_loss(account_details={ + account_details = { 'account': d.difference_account, 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', - payment_entry.company, "cost_center"), - 'amount': d.difference_amount - }) + payment_entry.company, "cost_center") + } + if d.difference_amount: + account_details['amount'] = d.difference_amount + + payment_entry.set_gain_or_loss(account_details=account_details) if not do_not_save: payment_entry.save(ignore_permissions=True) @@ -962,7 +966,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): 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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1c086e9edc..a9b7efbe98 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -124,6 +124,8 @@ class AccountsController(TransactionBase): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() + self.set_advance_gain_or_loss() + if self.is_return: self.validate_qty() else: @@ -584,15 +586,18 @@ class AccountsController(TransactionBase): allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) - self.append("advances", { + advance_row = { "doctype": self.doctype + " Advance", "reference_type": d.reference_type, "reference_name": d.reference_name, "reference_row": d.reference_row, "remarks": d.remarks, "advance_amount": flt(d.amount), - "allocated_amount": allocated_amount - }) + "allocated_amount": allocated_amount, + "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry + } + + self.append("advances", advance_row) def get_advance_entries(self, include_unallocated=True): if self.doctype == "Sales Invoice": @@ -650,6 +655,71 @@ class AccountsController(TransactionBase): "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") .format(d.reference_name, d.against_order)) + def set_advance_gain_or_loss(self): + if not self.get("advances"): + return + + for d in self.get("advances"): + advance_exchange_rate = d.ref_exchange_rate + if (d.allocated_amount and self.conversion_rate != 1 + and self.conversion_rate != advance_exchange_rate): + + base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount + base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount + difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate + + d.exchange_gain_loss = difference + + def make_exchange_gain_loss_gl_entries(self, gl_entries): + if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: + for d in self.get("advances"): + if d.exchange_gain_loss: + 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 @@ class AccountsController(TransactionBase): if self.party_account_currency != self.company_currency else 1), 'grand_total': (self.base_grand_total if self.party_account_currency == self.company_currency else self.grand_total), - 'outstanding_amount': self.outstanding_amount + 'outstanding_amount': self.outstanding_amount, + 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'), + 'exchange_gain_loss': flt(d.get('exchange_gain_loss')) }) lst.append(args) @@ -751,11 +823,11 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - else: dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + else: + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount @@ -1045,8 +1117,11 @@ class AccountsController(TransactionBase): 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 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" payment_type = "Receive" if party_type == "Customer" else "Pay" + exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" + payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" @@ -1305,27 +1382,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, "Payment Entry" as reference_type, t1.name as reference_name, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency + t1.{0} as currency, t1.{4} as exchange_rate from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 where t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t2.reference_doctype = %s {2} order by t1.posting_date {3} - """.format(currency_field, party_account_field, reference_condition, limit_cond), + """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1) if include_unallocated: unallocated_payment_entries = frappe.db.sql(""" select "Payment Entry" as reference_type, name as reference_name, - remarks, unallocated_amount as amount + remarks, unallocated_amount as amount, {2} as exchange_rate from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s and docstatus = 1 and unallocated_amount > 0 order by posting_date {1} - """.format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1) + """.format(party_account_field, limit_cond, exchange_rate_field), + (party_account, party_type, party, payment_type), as_dict=1) return list(payment_entries_against_order) + list(unallocated_payment_entries) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 280319321f..21c052a391 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): 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 8196cff849..17bd7354f9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -53,12 +53,17 @@ class StockController(AccountsController): 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") @@ -356,42 +361,68 @@ class StockController(AccountsController): }, update_modified) def validate_inspection(self): - '''Checks if quality inspection is set for Items that require inspection. - On submit, throw an exception''' - inspection_required_fieldname = None - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - inspection_required_fieldname = "inspection_required_before_purchase" - elif self.doctype in ["Delivery Note", "Sales Invoice"]: - inspection_required_fieldname = "inspection_required_before_delivery" + """Checks if quality inspection is set/ is valid for Items that require inspection.""" + inspection_fieldname_map = { + "Purchase Receipt": "inspection_required_before_purchase", + "Purchase Invoice": "inspection_required_before_purchase", + "Sales Invoice": "inspection_required_before_delivery", + "Delivery Note": "inspection_required_before_delivery" + } + inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) + # return if inspection is not required on document level if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or (self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): return - for d in self.get('items'): - qa_required = False - if (inspection_required_fieldname and not d.quality_inspection and - frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): - qa_required = True - elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: - qa_required = True - if self.docstatus == 1 and d.quality_inspection: - qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) - if qa_doc.docstatus == 0: - link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) - frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) + for row in self.get('items'): + qi_required = False + if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): + qi_required = True + elif self.doctype == "Stock Entry" and row.t_warehouse: + qi_required = True # inward stock needs inspection - if qa_doc.status != 'Accepted': - frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") - .format(d.idx, d.item_code), QualityInspectionRejectedError) - elif qa_required : - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted - if self.docstatus==1 and action == 'Stop': - frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), - exc=QualityInspectionRequiredError) - else: - frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) + if qi_required: # validate row only if inspection is required on item level + self.validate_qi_presence(row) + if self.docstatus == 1: + self.validate_qi_submission(row) + self.validate_qi_rejection(row) + + def validate_qi_presence(self, row): + """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if not row.quality_inspection: + msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" + if self.docstatus == 1: + frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) + else: + frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") + + def validate_qi_submission(self, row): + """Check if QI is submitted on row level, during submission""" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") + qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") + + if not qa_docstatus == 1: + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" + if action == "Stop": + frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + else: + frappe.msgprint(_(msg), alert=True, indicator="orange") + + def validate_qi_rejection(self, row): + """Check if QI is rejected on row level, during submission""" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected") + qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") + + if qa_status == "Rejected": + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" + if action == "Stop": + frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + else: + frappe.msgprint(_(msg), alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 56da5b71da..099c7d4346 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object): 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 @@ class calculate_taxes_and_totals(object): 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 @@ class calculate_taxes_and_totals(object): 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/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index ac374a95f4..089a63fc1c 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, + status:function(frm){ + if (frm.doc.status == "Lost"){ + frm.trigger('set_as_lost_dialog'); + } + + }, + customer_address: function(frm, cdt, cdn) { erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false); }, @@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", { frm.add_custom_button(__('Quotation'), cur_frm.cscript.create_quotation, __('Create')); - if(doc.status!=="Quotation") { - frm.add_custom_button(__('Lost'), () => { - frm.trigger('set_as_lost_dialog'); - }); - } } if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { diff --git a/erpnext/education/api.py b/erpnext/education/api.py index afa0be9b9f..4493a3fef1 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -34,11 +34,14 @@ def enroll_student(source_name): } }}, ignore_permissions=True) student.save() + + student_applicant = frappe.db.get_value("Student Applicant", source_name, + ["student_category", "program"], as_dict=True) program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment.student = student.name - program_enrollment.student_category = student.student_category + program_enrollment.student_category = student_applicant.student_category program_enrollment.student_name = student.title - program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") + program_enrollment.program = student_applicant.program frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) return program_enrollment diff --git a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json index 9be292b65e..1d7497387f 100644 --- a/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json +++ b/erpnext/education/doctype/program_enrollment_tool_student/program_enrollment_tool_student.json @@ -1,195 +1,68 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-06-10 03:29:02.539914", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-06-10 03:29:02.539914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "student_applicant", + "student", + "student_name", + "column_break_3", + "student_batch_name", + "student_category" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "student_applicant", - "fieldtype": "Link", - "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": "Student Applicant", - "length": 0, - "no_copy": 0, - "options": "Student Applicant", - "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, - "unique": 0 - }, + "fieldname": "student_applicant", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Applicant", + "options": "Student Applicant" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "student", - "fieldtype": "Link", - "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": "Student", - "length": 0, - "no_copy": 0, - "options": "Student", - "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, - "unique": 0 - }, + "fieldname": "student", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student", + "options": "Student" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_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": "Student Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "student_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Student Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_batch_name", - "fieldtype": "Link", - "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": "Student Batch Name", - "length": 0, - "no_copy": 0, - "options": "Student Batch Name", - "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, - "unique": 0 + "fieldname": "student_batch_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Student Batch Name", + "options": "Student Batch Name" + }, + { + "fieldname": "student_category", + "fieldtype": "Link", + "label": "Student Category", + "options": "Student Category", + "read_only": 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": 1, - "max_attachments": 0, - "modified": "2018-01-02 12:03:53.890741", - "modified_by": "Administrator", - "module": "Education", - "name": "Program Enrollment Tool Student", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-07-29 18:19:54.471594", + "modified_by": "Administrator", + "module": "Education", + "name": "Program Enrollment Tool Student", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js index d3fe7d2b4d..12faeecc87 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js +++ b/erpnext/erpnext_integrations/doctype/shopify_log/shopify_log.js @@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', { }) }).addClass('btn-primary'); } + + let app_link = "Ecommerce Integrations" + frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); } }); diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js index 1574795dfa..a926a7e52a 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js @@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){ frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note); } + + let app_link = "Ecommerce Integrations" + frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); + }) $.extend(erpnext_integrations.shopify_settings, { diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 52daec9180..e6b6cc4a8a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -24,7 +24,8 @@ doctype_js = { "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 = { @@ -429,7 +430,8 @@ regional_overrides = { 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', - 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount' + 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', + 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py index f7601870fa..c2ed457984 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.py +++ b/erpnext/hr/doctype/appraisal/appraisal.py @@ -9,7 +9,7 @@ from frappe.utils import flt, getdate 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 @@ class Appraisal(Document): 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 3412675d81..f79f0fe418 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate 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 090d53262c..7f88fed73a 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 import _ 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 a6fe429be1..0d7fded921 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 @@ import frappe 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 fa017d9d4c..5ca47560b1 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -13,8 +13,10 @@ from frappe.model.document import Document 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 @@ class Employee(NestedSet): message += "


" 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 7d652a7366..8fc7cf1934 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -7,7 +7,7 @@ import frappe 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 @@ class TestEmployee(unittest.TestCase): 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 @@ def make_employee(user, company=None, **kwargs): 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 cb72f6b6d9..cbb3cc813b 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 import _ 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,11 +19,11 @@ class EmployeeAdvance(Document): 'make_payment_via_journal_entry') def validate(self): + validate_active_employee(self.employee) self.set_status() def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') - self.set_status() def set_status(self): if self.docstatus == 0: @@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, 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) @@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None): if mode_of_payment_type == "Bank": voucher_type = "Bank Entry" - return voucher_type \ No newline at end of file + return voucher_type diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 15fbd4e015..60ea0f9895 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.model.document import Document 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 mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki 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 83fb235f92..a3a61834c8 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -7,12 +7,11 @@ import frappe 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 45d68729ce..0493306166 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral.py @@ -7,9 +7,11 @@ import frappe 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 6eec9fa12a..c2007747fb 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -10,10 +10,6 @@ from frappe.utils import getdate 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 5010fc3f75..95e2806aed 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -6,7 +6,7 @@ import frappe, erpnext 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 @@ class ExpenseClaim(AccountsController): 'make_payment_via_journal_entry') def validate(self): + validate_active_employee(self.employee) self.validate_advances() self.validate_sanctioned_amount() self.calculate_total_amount() @@ -35,8 +36,8 @@ class ExpenseClaim(AccountsController): if self.task and not self.project: self.project = frappe.db.get_value("Task", self.task, "project") - def set_status(self): - self.status = { + def set_status(self, update=False): + status = { "0": "Draft", "1": "Submitted", "2": "Cancelled" @@ -44,14 +45,18 @@ class ExpenseClaim(AccountsController): paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) precision = self.precision("grand_total") - if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 - and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ - and self.docstatus == 1 and self.approval_status == 'Approved': - self.status = "Paid" + if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 + and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved': + status = "Paid" elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 1 and self.approval_status == 'Rejected': - self.status = 'Rejected' + status = 'Rejected' + + if update: + self.db_set("status", status) + else: + self.status = status def on_update(self): share_doc_with_approver(self, self.expense_approver) @@ -74,7 +79,7 @@ class ExpenseClaim(AccountsController): if self.is_paid: update_reimbursed_amount(self) - self.set_status() + self.set_status(update=True) self.update_claimed_amount_in_employee_advance() def on_cancel(self): @@ -86,7 +91,6 @@ class ExpenseClaim(AccountsController): if self.is_paid: update_reimbursed_amount(self) - self.set_status() self.update_claimed_amount_in_employee_advance() def update_claimed_amount_in_employee_advance(self): diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index cee6f374fd..93fb19f4a1 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals 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 @@ class LeaveApplication(Document): 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 e041b7fb8f..912bd8ad92 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -7,7 +7,7 @@ import frappe 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 @@ from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leav 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 ab65260c09..89ae4d535d 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.model.document import Document 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 177c45edc6..6461f07552 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -7,12 +7,13 @@ import frappe 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 01d3f34706..60834d3f4a 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 ebb1734347..a6a8406803 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 @@ class EmployeeBoardingController(Document): 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 @@ def share_doc_with_approver(doc, user): 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 28af3a9c41..f9c201ab60 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', { frm.set_query("loan_type", function () { return { "filters": { - "docstatus": 1 + "docstatus": 1, + "company": frm.doc.company } }; }); diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index 1365274971..eccbdc3e91 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 @@ frappe.ui.form.on('Loan Application', { refresh: function(frm) { frm.trigger("toggle_fields"); frm.trigger("add_toolbar_buttons"); + frm.set_query('loan_type', () => { + return { + filters: { + company: frm.doc.company + } + }; + }); }, repayment_method: function(frm) { frm.doc.repayment_amount = frm.doc.repayment_periods = "" diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 15a7c316c9..bfbc6790b2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -83,7 +83,7 @@ frappe.ui.form.on("BOM", { 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 @@ frappe.ui.form.on("BOM", { }) }, - 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 c32a8a95a1..4e93fc6799 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -330,7 +330,7 @@ class BOM(WebsiteGenerator): 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: @@ -713,7 +713,8 @@ def get_bom_item_rate(args, bom_doc): "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function "conversion_factor": args.get("conversion_factor") or 1, "plc_conversion_rate": 1, - "ignore_party": True + "ignore_party": True, + "ignore_conversion_rate": True }) item_doc = frappe.get_cached_doc("Item", args.get("item_code")) out = frappe._dict() @@ -747,7 +748,7 @@ def get_valuation_rate(args): 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 @@ -773,7 +774,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item.image, bom.project, bom_item.rate, - bom_item.amount, + sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount, item.stock_uom, item.item_group, item.allow_alternative_item, @@ -1068,13 +1069,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): 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: @@ -1083,7 +1077,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): 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 420bb00803..69c7f5c614 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -192,11 +192,11 @@ class JobCard(Document): "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 38a0ee77ad..6a024f275a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -747,9 +747,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): 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 @@ def get_warehouse_list(warehouses, warehouse_list=None): 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 cce1bb61b6..93e6d7a97f 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.stock.doctype.item.test_item import create_item 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 @@ class TestProductionPlan(unittest.TestCase): 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 f63d2b9864..10cee32398 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/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 68de0b29d3..bf1ccb7159 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_batch_size_for_fg_item(self): + fg_item = "Test Batch Size Item For BOM 3" + rm1 = "Test Batch Size Item RM 1 For BOM 3" + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: + item_args = { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + } + + if item == fg_item: + item_args['has_batch_no'] = 1 + item_args['create_new_batch'] = 1 + item_args['batch_number_series'] = 'TBSI3.#####' + + make_item(item, item_args) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), + qty=30, do_not_save = True) + work_order.batch_size = 10 + work_order.insert() + work_order.submit() + self.assertEqual(work_order.has_batch_no, 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + self.assertEqual(row.qty, 10) + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 779ae42d65..282b5d0afe 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -239,7 +239,7 @@ class WorkOrder(Document): self.create_serial_no_batch_no() def on_submit(self): - if not self.wip_warehouse: + if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) @@ -487,21 +487,20 @@ class WorkOrder(Document): 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() @@ -656,7 +655,7 @@ class WorkOrder(Document): for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): self.append('required_items', { 'rate': item.rate, - 'amount': item.amount, + 'amount': item.rate * item.qty, 'operation': item.operation or operation, 'item_code': item.item_code, 'item_name': item.item_name, diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index f190cfae75..7c1baf1a8d 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" @@ -151,12 +151,6 @@ "fieldname": "column_break_21", "fieldtype": "Column Break" }, - { - "default": "0", - "fieldname": "subscription_activated", - "fieldtype": "Check", - "label": "Subscription Activated" - }, { "fieldname": "subscription_start", "fieldtype": "Date", @@ -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 30be585e9a..67828d6efc 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -84,7 +84,9 @@ def create_member(user_details): "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 e8ae6187b7..b584116df3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings): 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 get_member_based_on_subscription(subscription_id, email): 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 @@ def verify_signature(data, endpoint="Membership"): @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 @@ def trigger_razorpay_subscription(*args, **kwargs): # 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 @@ def trigger_razorpay_subscription(*args, **kwargs): 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 @@ def set_expired_status(): `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 31da792e53..0f5a9bed82 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -6,6 +6,7 @@ import unittest 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 @@ class TestMembership(unittest.TestCase): 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 @@ class TestMembership(unittest.TestCase): "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 @@ def setup_membership(): 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 29376f00a1..ae01496f02 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -291,3 +291,10 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice 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_amt_in_work_order_required_items +erpnext.patches.v13_0.update_export_type_for_gst +erpnext.patches.v13_0.update_tds_check_field #3 +erpnext.patches.v13_0.update_recipient_email_digest +erpnext.patches.v13_0.shopify_deprecation_warning \ No newline at end of file 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 new file mode 100644 index 0000000000..0d8109c41a --- /dev/null +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -0,0 +1,112 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.utils import cstr, flt, cint +from erpnext.stock.stock_ledger import make_sl_entries +from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + +def execute(): + 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 + + frappe.reload_doc('manufacturing', 'doctype', 'work_order') + filters = { + 'docstatus': 1, + 'produced_qty': ('>', 0), + 'creation': ('>=', '2021-06-29 00:00:00'), + 'has_batch_no': 1 + } + + fields = ['name', 'production_item'] + + work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + + if not work_orders: + return + + repost_stock_entries = [] + + stock_entries = frappe.db.sql_list(''' + SELECT + se.name + FROM + `tabStock Entry` se + WHERE + se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s + and not exists( + select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 + ) + ORDER BY + se.posting_date, se.posting_time + ''', (work_orders,)) + + if stock_entries: + print('Length of stock entries', len(stock_entries)) + + for stock_entry in stock_entries: + doc = frappe.get_doc('Stock Entry', stock_entry) + doc.set_work_order_details() + doc.load_items_from_bom() + doc.calculate_rate_and_amount() + set_expense_account(doc) + doc.make_batches('t_warehouse') + + if doc.docstatus == 0: + doc.save() + else: + repost_stock_entry(doc) + repost_stock_entries.append(doc) + + for repost_doc in repost_stock_entries: + repost_future_sle_and_gle(repost_doc) + +def set_expense_account(doc): + for row in doc.items: + if row.is_finished_item and not row.expense_account: + row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + +def repost_stock_entry(doc): + doc.db_update() + for child_row in doc.items: + if child_row.is_finished_item: + child_row.db_update() + + sl_entries = [] + finished_item_row = doc.get_finished_item_row() + get_sle_for_target_warehouse(doc, sl_entries, finished_item_row) + + if sl_entries: + try: + make_sl_entries(sl_entries, True) + except Exception: + print(f'SLE entries not posted for the stock entry {doc.name}') + traceback = frappe.get_traceback() + frappe.log_error(traceback) + +def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): + for d in doc.get('items'): + if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: + sle = doc.get_sl_entries(d, { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate) + }) + + sle.recalculate_rate = 1 + sl_entries.append(sle) + +def repost_future_sle_and_gle(doc): + args = frappe._dict({ + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company + }) + + create_repost_item_valuation_entry(args) diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index fa1dfed643..41c51c36dc 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -37,7 +37,7 @@ def execute(): if frappe.db.exists('DocType', 'Opportunity'): opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') - frappe.reload_doc('crm', 'doctype', 'opportunity') + frappe.reload_doctype('Opportunity', force=True) rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') # change fieldtype to duration diff --git a/erpnext/patches/v13_0/shopify_deprecation_warning.py b/erpnext/patches/v13_0/shopify_deprecation_warning.py new file mode 100644 index 0000000000..8b0f1935cf --- /dev/null +++ b/erpnext/patches/v13_0/shopify_deprecation_warning.py @@ -0,0 +1,15 @@ +import click +import frappe + + +def execute(): + + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings") + if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"): + return + + click.secho( + "Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py new file mode 100644 index 0000000000..eae5ff60b9 --- /dev/null +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + """ Correct amount in child table of required items table.""" + + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + + frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""") + 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 0000000000..478a2a6c80 --- /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_recipient_email_digest.py b/erpnext/patches/v13_0/update_recipient_email_digest.py new file mode 100644 index 0000000000..d9aa03f0fd --- /dev/null +++ b/erpnext/patches/v13_0/update_recipient_email_digest.py @@ -0,0 +1,21 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("setup", "doctype", "Email Digest") + frappe.reload_doc("setup", "doctype", "Email Digest Recipient") + email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list']) + for email_digest in email_digests: + if email_digest.recipient_list: + for recipient in email_digest.recipient_list.split("\n"): + doc = frappe.get_doc({ + 'doctype': 'Email Digest Recipient', + 'parenttype': 'Email Digest', + 'parentfield': 'recipients', + 'parent': email_digest.name, + 'recipient': recipient + }) + doc.insert() 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 0000000000..28e650e9ce --- /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 0000000000..3d149586a0 --- /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 ebeddf97f9..381f399e9f 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -7,6 +7,7 @@ import frappe 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 @@ class AdditionalSalary(Document): 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() 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 27df30a459..5ebe514ac0 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.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt 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 d9937a7bb9..c6713f3aa4 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 import _ 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 ead3db126f..6b918ba76d 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -6,9 +6,11 @@ from __future__ import unicode_literals 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 fb71a2877a..e11d60a464 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.model.document import Document 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 5bc33a65f2..8131ae0fa8 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 @@ import frappe 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 049ea265cc..055bea7410 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -7,11 +7,10 @@ import frappe 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_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index dbf75140ac..e9e6f81862 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -4,11 +4,18 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { frm.set_query("account", "accounts", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + let d = frappe.get_doc(cdt, cdn); + + let root_type = "Liability"; + if (frm.doc.type == "Deduction") { + root_type = "Expense"; + } + return { filters: { "is_group": 0, - "company": d.company + "company": d.company, + "root_type": root_type } }; }); diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 877503b41c..7e1fb0616d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -19,6 +19,7 @@ from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_appli 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 @@ class SalarySlip(TransactionBase): 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: @@ -1091,6 +1093,7 @@ class SalarySlip(TransactionBase): "applicant": self.employee, "docstatus": 1, "repay_from_salary": 1, + "company": self.company }) def make_loan_repayment_entry(self): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index ce88cc3f1e..6e8d3b3f30 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 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): 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 e7d123c996..3957d834d3 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) - if not frappe.db.exists('Salary Structure', salary_structure): - details = { - "doctype": "Salary Structure", - "name": salary_structure, - "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={'account_currency': currency}), - "currency": currency - } - if other_details and isinstance(other_details, dict): - details.update(other_details) - salary_structure_doc = frappe.get_doc(details) - salary_structure_doc.insert() - if not dont_submit: - salary_structure_doc.submit() + if frappe.db.exists("Salary Structure", salary_structure): + frappe.db.delete("Salary Structure", salary_structure) - else: - salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) + details = { + "doctype": "Salary Structure", + "name": salary_structure, + "company": company or erpnext.get_default_company(), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "payroll_frequency": payroll_frequency, + "payment_account": get_random("Account", filters={'account_currency': currency}), + "currency": currency + } + if other_details and isinstance(other_details, dict): + details.update(other_details) + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() + if not dont_submit: + salary_structure_doc.submit() filters = {'employee':employee, 'docstatus': 1} if not from_date and payroll_period: diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py index 500543ceb0..05a5366a5c 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -95,6 +95,7 @@ def execute(filters=None): "amount": salary.net_pay, } data.append(row) + return columns, data def get_bank_accounts(): @@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters): entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) payment_accounts = [d.payment_account for d in entries] - set_company_account(payment_accounts, entries) + entries = set_company_account(payment_accounts, entries) return entries def get_salary_slips(payroll_entries): diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index d77eb2c396..d60b1a2b05 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -2,6 +2,7 @@ import frappe from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.shopping_cart.product_info import get_product_info_for_website +from erpnext.setup.doctype.item_group.item_group import get_child_groups def get_field_filter_data(): product_settings = get_product_settings() @@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search= def get_products_html_for_website(field_filters=None, attribute_filters=None): field_filters = frappe.parse_json(field_filters) attribute_filters = frappe.parse_json(attribute_filters) + set_item_group_filters(field_filters) items = get_products_for_website(field_filters, attribute_filters) html = ''.join(get_html_for_items(items)) @@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None): return html +def set_item_group_filters(field_filters): + if 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'])] + def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c8bd80fca0..ae38d4ca19 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -15,12 +15,15 @@ from erpnext.manufacturing.doctype.workstation.workstation import (check_if_with 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 0000000000..41a0e8a9f9 --- /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/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1de9ec1a7d..53d5278bbf 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -65,7 +65,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.refresh_fields(); }, - calculate_discount_amount: function(){ + calculate_discount_amount: function() { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { this.set_discount_amount(); this.apply_discount_amount(); @@ -73,18 +73,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, _calculate_taxes_and_totals: function() { - frappe.run_serially([ - () => this.validate_conversion_rate(), - () => this.calculate_item_values(), - () => this.update_item_tax_map(), - () => this.initialize_taxes(), - () => this.determine_exclusive_rate(), - () => this.calculate_net_total(), - () => this.calculate_taxes(), - () => this.manipulate_grand_total_for_inclusive_tax(), - () => this.calculate_totals(), - () => this._cleanup() - ]); + this.validate_conversion_rate(); + this.calculate_item_values(); + this.initialize_taxes(); + this.determine_exclusive_rate(); + this.calculate_net_total(); + this.calculate_taxes(); + this.manipulate_grand_total_for_inclusive_tax(); + this.calculate_totals(); + this._cleanup(); }, validate_conversion_rate: function() { @@ -105,7 +102,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, calculate_item_values: function() { - var me = this; + let me = this; if (!this.discount_amount_applied) { $.each(this.frm.doc["items"] || [], function(i, item) { frappe.model.round_floats_in(item); @@ -266,46 +263,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); }, - update_item_tax_map: function() { - let me = this; - let item_codes = []; - let item_rates = {}; - let item_tax_templates = {}; - - $.each(this.frm.doc.items || [], function(i, item) { - if (item.item_code) { - // Use combination of name and item code in case same item is added multiple times - item_codes.push([item.item_code, item.name]); - item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template; - } - }); - - if (item_codes.length) { - return this.frm.call({ - method: "erpnext.stock.get_item_details.get_item_tax_info", - args: { - company: me.frm.doc.company, - tax_category: cstr(me.frm.doc.tax_category), - item_codes: item_codes, - item_rates: item_rates, - item_tax_templates: item_tax_templates - }, - callback: function(r) { - if (!r.exc) { - $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { - item.item_tax_template = r.message[item.name].item_tax_template; - item.item_tax_rate = r.message[item.name].item_tax_rate; - me.add_taxes_from_item_tax_template(item.item_tax_rate); - } - }); - } - } - }); - } - }, - add_taxes_from_item_tax_template: function(item_tax_map) { let me = this; @@ -630,8 +587,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail); }); } - - this.frm.refresh_fields(); }, set_discount_amount: function() { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b3af3d67ea..5475383759 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -826,9 +826,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.run_serially([ () => me.frm.script_manager.trigger("currency"), + () => me.update_item_tax_map(), () => me.apply_default_taxes(), - () => me.apply_pricing_rule(), - () => me.calculate_taxes_and_totals() + () => me.apply_pricing_rule() ]); } } @@ -1787,6 +1787,46 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ ]); }, + update_item_tax_map: function() { + let me = this; + let item_codes = []; + let item_rates = {}; + let item_tax_templates = {}; + + $.each(this.frm.doc.items || [], function(i, item) { + if (item.item_code) { + // Use combination of name and item code in case same item is added multiple times + item_codes.push([item.item_code, item.name]); + item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template; + } + }); + + if (item_codes.length) { + return this.frm.call({ + method: "erpnext.stock.get_item_details.get_item_tax_info", + args: { + company: me.frm.doc.company, + tax_category: cstr(me.frm.doc.tax_category), + item_codes: item_codes, + item_rates: item_rates, + item_tax_templates: item_tax_templates + }, + callback: function(r) { + if (!r.exc) { + $.each(me.frm.doc.items || [], function(i, item) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { + item.item_tax_template = r.message[item.name].item_tax_template; + item.item_tax_rate = r.message[item.name].item_tax_rate; + me.add_taxes_from_item_tax_template(item.item_tax_rate); + } + }); + } + } + }); + } + }, + item_tax_template: function(doc, cdt, cdn) { var me = this; if(me.frm.updating_party_details) return; diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index aa9bba17c7..d0c935f488 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [ frappe.help.help_links["Form/System Settings"] = [ { - label: "Naming Series", + label: "System Settings", url: docsUrl + "user/manual/en/setting-up/settings/system-settings", }, ]; @@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [ label: "PayPal Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/paypal-integration", + "user/manual/en/erpnext_integration/paypal-integration", }, ]; @@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [ label: "Razorpay Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/razorpay-integration", + "user/manual/en/erpnext_integration/razorpay-integration", }, ]; frappe.help.help_links["Form/Dropbox Settings"] = [ { label: "Dropbox Settings", - url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup", + url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup", }, ]; @@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [ { label: "LDAP Settings", url: - docsUrl + "user/manual/en/setting-up/integrations/ldap-integration", + docsUrl + "user/manual/en/erpnext_integration/ldap-integration", }, ]; @@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [ label: "Stripe Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/stripe-integration", + "user/manual/en/erpnext_integration/stripe-integration", }, ]; @@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [ label: "Nested BOM Structure", url: docsUrl + - "user/manual/en/manufacturing/articles/nested-bom-structure", + "user/manual/en/manufacturing/articles/managing-multi-level-bom", }, ]; diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js index ebe6cd98f8..7bd21df67b 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 cc2d9f06d2..54e488610d 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.", [`documentation`]) ); 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 641520437f..0ee5b097b5 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 @@ class GSTR3BReport(Document): 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 @@ class GSTR3BReport(Document): 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 @@ class GSTR3BReport(Document): 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/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index ea600d9097..e65442dbff 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -316,10 +316,6 @@ def get_payment_details(invoice): )) def get_return_doc_reference(invoice): - if not invoice.return_against: - frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') - .format(frappe.bold('Return Against')), title=_('Missing Field')) - invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') @@ -435,7 +431,7 @@ def make_einvoice(invoice): if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) - if invoice.is_return: + if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) if invoice.transporter and not invoice.is_return: @@ -966,7 +962,7 @@ class GSPConnector(): "attached_to_doctype": doctype, "attached_to_name": docname, "attached_to_field": "qrcode_image", - "is_private": 1, + "is_private": 0, "content": qr_image.getvalue()}) _file.save() frappe.db.commit() diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5f9d5ed0d6..e9372f9b8f 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -12,7 +12,10 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): - setup_company_independent_fixtures(patch=patch) + # Company independent fixtures should be called only once at the first company setup + if frappe.db.count('Company', {'country': 'India'}) <=1: + setup_company_independent_fixtures(patch=patch) + if not patch: make_fixtures(company) @@ -122,10 +125,12 @@ def add_print_formats(): def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] + sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") if not patch: - make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') + make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') + make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '') make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') def make_custom_fields(update=True): @@ -636,7 +641,6 @@ def make_custom_fields(update=True): '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' } @@ -655,7 +659,6 @@ def make_custom_fields(update=True): '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' } @@ -786,7 +789,7 @@ def set_tax_withholding_category(company): doc.flags.ignore_mandatory = True doc.insert() else: - doc = frappe.get_doc("Tax Withholding Category", d.get("name")) + doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True) if accounts: doc.append("accounts", accounts[0]) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 81c0918b99..88c350ac89 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -431,9 +431,11 @@ def get_ewb_data(dt, dn): company_address = frappe.get_doc('Address', doc.company_address) billing_address = frappe.get_doc('Address', doc.customer_address) + #added dispatch address + dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address shipping_address = frappe.get_doc('Address', doc.shipping_address_name) - data = get_address_details(data, doc, company_address, billing_address) + data = get_address_details(data, doc, company_address, billing_address, dispatch_address) data.itemList = [] data.totalValue = doc.total @@ -519,10 +521,10 @@ def get_gstins_for_company(company): `tabDynamic Link`.link_name = %(company)s""", {"company": company}) return company_gstins -def get_address_details(data, doc, company_address, billing_address): +def get_address_details(data, doc, company_address, billing_address, dispatch_address): data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') - data.fromStateCode = data.actualFromStateCode = validate_state_code( - company_address.gst_state_number, 'Company Address') + data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') + data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address') if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: data.toGstin = 'URP' @@ -857,4 +859,15 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) - return depreciation_amount \ No newline at end of file + return depreciation_amount + +def set_item_tax_from_hsn_code(item): + if not item.taxes and item.gst_hsn_code: + hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code) + + for tax in hsn_doc.taxes: + item.append('taxes', { + 'item_tax_template': tax.item_tax_template, + 'tax_category': tax.tax_category, + 'valid_from': tax.valid_from + }) \ No newline at end of file diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 10961593e1..4b7309440c 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -217,9 +217,8 @@ class Gstr1Report(object): 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 @@ class Gstr1Report(object): # 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): @@ -584,7 +584,7 @@ class Gstr1Report(object): def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = get_company_gstin_number(filters["company"], filters["company_address"]) + gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 762b6f1d6c..d31db820ab 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -38,6 +38,8 @@ "col_break46", "shipping_address_name", "shipping_address", + "dispatch_address_name", + "dispatch_address", "customer_group", "territory", "currency_and_price_list", @@ -1486,13 +1488,29 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "dispatch_address_name", + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-04-15 23:55:13.439068", + "modified": "2021-07-08 21:37:44.177493", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", 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 7cae0e4797..a4a4b0e0ed 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 @@ erpnext.PointOfSale.ItemCart = class { `
` ); 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); @@ -472,12 +473,7 @@ erpnext.PointOfSale.ItemCart = class { const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); - const taxes = frm.doc.taxes.map(t => { - return { - description: t.description, rate: t.rate - }; - }); - this.render_taxes(frm.doc.total_taxes_and_charges, taxes); + this.render_taxes(frm.doc.taxes); } render_net_total(value) { @@ -502,14 +498,14 @@ erpnext.PointOfSale.ItemCart = class { ); } - render_taxes(value, taxes) { + render_taxes(taxes) { if (taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; return `
${description}
-
${format_currency(value, currency)}
+
${format_currency(t.tax_amount_after_discount_amount, currency)}
`; }).join(''); this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); @@ -568,7 +564,6 @@ erpnext.PointOfSale.ItemCart = class { ) set_dynamic_rate_header_width(); - this.scroll_to_item($item_to_update); function set_dynamic_rate_header_width() { const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); @@ -643,12 +638,6 @@ erpnext.PointOfSale.ItemCart = class { $($img).parent().replaceWith(`
${item_abbr}
`); } - scroll_to_item($item) { - if ($item.length === 0) return; - const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop(); - this.$cart_items_wrapper.animate({ scrollTop }); - } - update_selector_value_in_cart_item(selector, value, item) { const $item_to_update = this.get_cart_item(item); $item_to_update.attr(`data-${selector}`, escape(value)); @@ -970,8 +959,23 @@ erpnext.PointOfSale.ItemCart = class { }); } + attach_refresh_field_event(frm) { + $(frm.wrapper).off('refresh-fields'); + $(frm.wrapper).on('refresh-fields', () => { + if (frm.doc.items.length) { + frm.doc.items.forEach(item => { + this.update_item_html(item); + }); + } + this.update_totals_section(frm); + }); + } + load_invoice() { const frm = this.events.get_frm(); + + this.attach_refresh_field_event(frm); + this.fetch_customer_details(frm.doc.customer).then(() => { this.events.customer_details_updated(this.customer_info); this.update_customer_section(); diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index f1a166b523..63306adc6f 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -198,6 +198,7 @@ erpnext.PointOfSale.Payment = class { const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible'); this.attach_cash_shortcuts(frm.doc); !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid'); + this.render_payment_mode_dom(); }); frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => { diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 915e6a4f31..8755125c81 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -291,7 +291,7 @@ class Company(NestedSet): cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') if cash and self.default_cash_account \ and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash) + mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) mode_of_payment.append('accounts', { 'company': self.name, 'default_account': self.default_cash_account @@ -395,7 +395,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue="long", company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) diff --git a/erpnext/setup/doctype/email_digest/email_digest.js b/erpnext/setup/doctype/email_digest/email_digest.js index 1071ea2020..2e415af282 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.js +++ b/erpnext/setup/doctype/email_digest/email_digest.js @@ -1,78 +1,31 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.cscript.refresh = function(doc, dt, dn) { - doc = locals[dt][dn]; - cur_frm.add_custom_button(__('View Now'), function() { - frappe.call({ - method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', - args: { - name: doc.name - }, - callback: function(r) { - var d = new frappe.ui.Dialog({ - title: __('Email Digest: ') + dn, - width: 800 +frappe.ui.form.on("Email Digest", { + refresh: function(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__('View Now'), function() { + frappe.call({ + method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', + args: { + name: frm.doc.name + }, + callback: function(r) { + let d = new frappe.ui.Dialog({ + title: __('Email Digest: {0}', [frm.doc.name]), + width: 800 + }); + $(d.body).html(r.message); + d.show(); + } }); - $(d.body).html(r.message); - d.show(); - } - }); - }, "fa fa-eye-open", "btn-default"); - - if (!cur_frm.is_new()) { - cur_frm.add_custom_button(__('Send Now'), function() { - return cur_frm.call('send', null, (r) => { - frappe.show_alert(__('Message Sent')); }); - }); + + frm.add_custom_button(__('Send Now'), function() { + return frm.call('send', null, () => { + frappe.show_alert({ message: __("Message Sent"), indicator: 'green'}); + }); + }); + } } -}; - -cur_frm.cscript.addremove_recipients = function(doc, dt, dn) { - // Get user list - - return cur_frm.call('get_users', null, function(r) { - // Open a dialog and display checkboxes against email addresses - doc = locals[dt][dn]; - var d = new frappe.ui.Dialog({ - title: __('Add/Remove Recipients'), - width: 400 - }); - - $.each(r.user_list, function(i, v) { - var fullname = frappe.user.full_name(v.name); - if(fullname !== v.name) fullname = fullname + " <" + v.name + ">"; - - if(v.enabled==0) { - fullname = repl(" %(name)s (" + __("disabled user") + ")", {name: v.name}); - } - - $('
').appendTo(d.body); - }); - - // Display add recipients button - d.set_primary_action("Update", function() { - cur_frm.cscript.add_to_rec_list(doc, d.body, r.user_list.length); - }); - - cur_frm.rec_dialog = d; - d.show(); - }); -} - -cur_frm.cscript.add_to_rec_list = function(doc, dialog, length) { - // add checked users to list of recipients - var rec_list = []; - $(dialog).find('input:checked').each(function(i, input) { - rec_list.push($(input).attr('data-id')); - }); - - doc.recipient_list = rec_list.join('\n'); - cur_frm.rec_dialog.hide(); - cur_frm.save(); - cur_frm.refresh_fields(); -} +}); \ No newline at end of file diff --git a/erpnext/setup/doctype/email_digest/email_digest.json b/erpnext/setup/doctype/email_digest/email_digest.json index 125aca17c7..06c98e5ff9 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.json +++ b/erpnext/setup/doctype/email_digest/email_digest.json @@ -1,1482 +1,338 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-09-16 22:00:00", - "custom": 0, - "description": "Send regular summary reports via Email.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, + "actions": [], + "autoname": "Prompt", + "creation": "2018-09-16 22:00:00", + "description": "Send regular summary reports via Email.", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "settings", + "column_break0", + "enabled", + "company", + "frequency", + "next_send", + "column_break1", + "recipients", + "accounts", + "accounts_module", + "income", + "expenses_booked", + "income_year_to_date", + "expense_year_to_date", + "column_break_16", + "bank_balance", + "credit_balance", + "invoiced_amount", + "payables", + "work_in_progress", + "sales_orders_to_bill", + "purchase_orders_to_bill", + "operation", + "column_break_21", + "sales_order", + "purchase_order", + "sales_orders_to_deliver", + "purchase_orders_to_receive", + "sales_invoice", + "purchase_invoice", + "column_break_operation", + "new_quotations", + "pending_quotations", + "issue", + "project", + "purchase_orders_items_overdue", + "other", + "tools", + "calendar_events", + "todo_list", + "notifications", + "column_break_32", + "add_quote" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "settings", - "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": "Email Digest Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "settings", + "fieldtype": "Section Break", + "label": "Email Digest Settings" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enabled", - "fieldtype": "Check", - "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": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "For Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "For Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "How frequently?", - "length": 0, - "no_copy": 0, - "options": "Daily\nWeekly\nMonthly", - "permlevel": 0, - "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": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "How frequently?", + "options": "Daily\nWeekly\nMonthly", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.enabled", - "fieldname": "next_send", - "fieldtype": "Data", - "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": "Next email will be sent on:", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.enabled", + "fieldname": "next_send", + "fieldtype": "Data", + "label": "Next email will be sent on:", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "column_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Note: Email will not be sent to disabled users", - "fieldname": "recipient_list", - "fieldtype": "Code", - "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": "Recipients", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "accounts", + "fieldtype": "Section Break", + "label": "Accounts" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "addremove_recipients", - "fieldtype": "Button", - "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": "Add/Remove Recipients", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "accounts_module", + "fieldtype": "Column Break", + "hidden": 1, + "label": "Profit & Loss" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "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": "Accounts", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "income", + "fieldtype": "Check", + "label": "New Income" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts_module", - "fieldtype": "Column Break", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Profit & Loss", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "expenses_booked", + "fieldtype": "Check", + "label": "New Expenses" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "income", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Income", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "income_year_to_date", + "fieldtype": "Check", + "label": "Annual Income" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "expenses_booked", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Expenses", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "expense_year_to_date", + "fieldtype": "Check", + "label": "Annual Expenses" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "income_year_to_date", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Annual Income", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "column_break_16", + "fieldtype": "Column Break", + "label": "Balance Sheet" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_year_to_date", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Annual Expenses", - "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 - }, + "default": "0", + "fieldname": "bank_balance", + "fieldtype": "Check", + "label": "Bank Balance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "column_break_16", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Balance Sheet", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "credit_balance", + "fieldtype": "Check", + "label": "Bank Credit Balance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "bank_balance", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bank Balance", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "invoiced_amount", + "fieldtype": "Check", + "label": "Receivables" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "credit_balance", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bank Credit Balance", - "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 - }, + "default": "0", + "fieldname": "payables", + "fieldtype": "Check", + "label": "Payables" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "invoiced_amount", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Receivables", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "work_in_progress", + "fieldtype": "Column Break", + "label": "Work in Progress" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "payables", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payables", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "default": "0", + "fieldname": "sales_orders_to_bill", + "fieldtype": "Check", + "label": "Sales Orders to Bill" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "work_in_progress", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Work in Progress", - "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 - }, + "default": "0", + "fieldname": "purchase_orders_to_bill", + "fieldtype": "Check", + "label": "Purchase Orders to Bill" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_orders_to_bill", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Orders to Bill", - "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 - }, + "fieldname": "operation", + "fieldtype": "Section Break", + "label": "Operations" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_orders_to_bill", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Orders to Bill", - "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 - }, + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "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": "Operations", - "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 - }, + "default": "0", + "fieldname": "sales_order", + "fieldtype": "Check", + "label": "New Sales Orders" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_21", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "purchase_order", + "fieldtype": "Check", + "label": "New Purchase Orders" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_order", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Sales Orders", - "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 - }, + "default": "0", + "fieldname": "sales_orders_to_deliver", + "fieldtype": "Check", + "label": "Sales Orders to Deliver" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_order", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Purchase Orders", - "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 - }, + "default": "0", + "fieldname": "purchase_orders_to_receive", + "fieldtype": "Check", + "label": "Purchase Orders to Receive" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_orders_to_deliver", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Orders to Deliver", - "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 - }, + "default": "0", + "fieldname": "sales_invoice", + "fieldtype": "Check", + "label": "New Sales Invoice" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_orders_to_receive", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Orders to Receive", - "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 - }, + "default": "0", + "fieldname": "purchase_invoice", + "fieldtype": "Check", + "label": "New Purchase Invoice" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_invoice", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Sales Invoice", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_operation", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_invoice", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Purchase Invoice", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "new_quotations", + "fieldtype": "Check", + "label": "New Quotations" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_operation", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "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 - }, + "default": "0", + "fieldname": "pending_quotations", + "fieldtype": "Check", + "label": "Open Quotations" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "new_quotations", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "New Quotations", - "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 - }, + "default": "0", + "fieldname": "issue", + "fieldtype": "Check", + "label": "Open Issues" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pending_quotations", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Open Quotations", - "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 - }, + "default": "0", + "fieldname": "project", + "fieldtype": "Check", + "label": "Open Projects" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "issue", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Open Issues", - "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 - }, + "default": "0", + "fieldname": "purchase_orders_items_overdue", + "fieldtype": "Check", + "label": "Purchase Orders Items Overdue" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "project", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Open Projects", - "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 - }, + "fieldname": "other", + "fieldtype": "Section Break", + "label": "Other" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_orders_items_overdue", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Orders Items Overdue", - "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 - }, + "fieldname": "tools", + "fieldtype": "Column Break", + "label": "Tools" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "other", - "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": "Other", - "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 - }, + "default": "0", + "fieldname": "calendar_events", + "fieldtype": "Check", + "label": "Upcoming Calendar Events" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tools", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tools", - "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 - }, + "default": "0", + "fieldname": "todo_list", + "fieldtype": "Check", + "label": "Open To Do" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "calendar_events", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Upcoming Calendar Events", - "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 - }, + "default": "0", + "fieldname": "notifications", + "fieldtype": "Check", + "label": "Open Notifications" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "todo_list", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Open To Do", - "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 - }, + "fieldname": "column_break_32", + "fieldtype": "Column Break", + "label": " " + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "notifications", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Open Notifications", - "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 - }, + "default": "0", + "fieldname": "add_quote", + "fieldtype": "Check", + "label": "Add Quote" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_32", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": " ", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "add_quote", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Add Quote", - "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 + "description": "Note: Email will not be sent to disabled users", + "fieldname": "recipients", + "fieldtype": "Table MultiSelect", + "label": "Recipients", + "options": "Email Digest Recipient", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-envelope", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-01-16 09:52:15.149908", - "modified_by": "Administrator", - "module": "Setup", - "name": "Email Digest", - "owner": "Administrator", + ], + "icon": "fa fa-envelope", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-24 23:49:00.081695", + "modified_by": "Administrator", + "module": "Setup", + "name": "Email Digest", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "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, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 1, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "permlevel": 1, + "read": 1, + "role": "System Manager" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 340d89bdf8..6fbd4cd78c 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -47,19 +47,13 @@ class EmailDigest(Document): # send email only to enabled users valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` where enabled=1""")] - recipients = list(filter(lambda r: r in valid_users, - self.recipient_list.split("\n"))) - original_user = frappe.session.user - - if recipients: - for user_id in recipients: - frappe.set_user(user_id) - frappe.set_user_lang(user_id) + if self.recipients: + for row in self.recipients: msg_for_this_recipient = self.get_msg_html() - if msg_for_this_recipient: + if msg_for_this_recipient and row.recipient in valid_users: frappe.sendmail( - recipients=user_id, + recipients=row.recipient, subject=_("{0} Digest").format(self.frequency), message=msg_for_this_recipient, reference_doctype = self.doctype, diff --git a/erpnext/setup/doctype/email_digest_recipient/__init__.py b/erpnext/setup/doctype/email_digest_recipient/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.json b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.json new file mode 100644 index 0000000000..8b2a6dcf49 --- /dev/null +++ b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-06-08 12:19:40.428949", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "recipient" + ], + "fields": [ + { + "fieldname": "recipient", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Recipient", + "options": "User", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-08-24 23:10:23.217572", + "modified_by": "Administrator", + "module": "Setup", + "name": "Email Digest Recipient", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.py b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.py new file mode 100644 index 0000000000..968c51c345 --- /dev/null +++ b/erpnext/setup/doctype/email_digest_recipient/email_digest_recipient.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EmailDigestRecipient(Document): + pass diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 1c72cebfa9..5fcad00af1 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -87,8 +87,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): if not field_filters: field_filters = {} - # Ensure the query remains within current item group - field_filters['item_group'] = self.name + # Ensure the query remains within current item group & sub group + field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)] engine = ProductQuery() context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 9313f95516..23e59472a6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -54,7 +54,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-08 23:13:48.049879", + "modified": "2021-08-04 20:15:59.071493", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", @@ -70,6 +70,7 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 } ], 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 ece9fb5699..c3db27f81c 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 @@ from frappe.desk.notifications import clear_notifications 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 @@ class TransactionDeletionRecord(Document): 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 @@ class TransactionDeletionRecord(Document): '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 @@ class TransactionDeletionRecord(Document): 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/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index 4edf9485dc..4833d93c4a 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -45,9 +45,16 @@ def enable_shopping_cart(args): def create_email_digest(): from frappe.utils.user import get_system_managers system_managers = get_system_managers(only_name=True) + if not system_managers: return + recipients = [] + for d in system_managers: + recipients.append({ + 'recipient': d + }) + companies = frappe.db.sql_list("select name FROM `tabCompany`") for company in companies: if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company): @@ -56,7 +63,7 @@ def create_email_digest(): "name": "Default Weekly Digest - " + company, "company": company, "frequency": "Weekly", - "recipient_list": "\n".join(system_managers) + "recipients": recipients }) for df in edigest.meta.get("fields", {"fieldtype": "Check"}): @@ -72,7 +79,7 @@ def create_email_digest(): "name": "Scheduler Errors", "company": companies[0], "frequency": "Daily", - "recipient_list": "\n".join(system_managers), + "recipients": recipients, "scheduler_errors": 1, "enabled": 1 }) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b6eef6ca48..b37ae3f4f6 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -162,19 +162,19 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No 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/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index f20e76f5bf..dbfeb4a10b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -32,6 +32,8 @@ "contact_info", "shipping_address_name", "shipping_address", + "dispatch_address_name", + "dispatch_address", "contact_person", "contact_display", "contact_mobile", @@ -1282,13 +1284,28 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "depends_on": "dispatch_address_name", + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-06-11 19:27:30.901112", + "modified": "2021-07-08 21:37:20.802652", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 8aec89381a..3ca98738cb 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -93,17 +93,18 @@ frappe.ui.form.on("Item", { erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); - + if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } 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); @@ -137,20 +138,6 @@ frappe.ui.form.on("Item", { frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); }, - gst_hsn_code: function(frm) { - if((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { - frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { - $.each(hsn_doc.taxes || [], function(i, tax) { - let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); - a.item_tax_template = tax.item_tax_template; - a.tax_category = tax.tax_category; - a.valid_from = tax.valid_from; - frm.refresh_field('taxes'); - }); - }); - } - }, - is_fixed_asset: function(frm) { // set serial no to false & toggles its visibility frm.set_value('has_serial_no', 0); @@ -186,8 +173,6 @@ frappe.ui.form.on("Item", { 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) { @@ -381,7 +366,8 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('assets/js/item-dashboard.min.js', function() { - const section = frm.dashboard.add_section('', __("Stock Levels")); + frm.dashboard.parent.find('.stock-levels').remove(); + const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index fbd30cf6c7..9bf4dbf2e8 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -123,6 +123,7 @@ class Item(WebsiteGenerator): self.cant_change() self.update_show_in_website() self.validate_item_tax_net_rate_range() + set_item_tax_from_hsn_code(self) if not self.is_new(): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -1305,3 +1306,7 @@ def update_variants(variants, template, publish_progress=True): def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Item", ["route(500)"]) + +@erpnext.allow_regional +def set_item_tax_from_hsn_code(item): + pass \ No newline at end of file diff --git a/erpnext/stock/doctype/item/regional/india.js b/erpnext/stock/doctype/item/regional/india.js new file mode 100644 index 0000000000..77ae51fa34 --- /dev/null +++ b/erpnext/stock/doctype/item/regional/india.js @@ -0,0 +1,15 @@ +frappe.ui.form.on('Item', { + gst_hsn_code: function(frm) { + if ((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { + frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { + $.each(hsn_doc.taxes || [], function(i, tax) { + let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); + a.item_tax_template = tax.item_tax_template; + a.tax_category = tax.tax_category; + a.valid_from = tax.valid_from; + frm.refresh_field('taxes'); + }); + }); + } + }, +}); \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e795742ea4..516ae43089 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -239,6 +239,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re 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 82c87a83a5..26ea11e01d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -415,7 +415,7 @@ class PurchaseReceipt(BuyingController): "cost_center": cost_center, "debit": debit, "credit": credit, - "against_account": against_account, + "against": against_account, "remarks": remarks, } diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 7f3d701034..f5d076a077 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -14,7 +14,7 @@ from erpnext.controllers.stock_controller import ( ) from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # test_records = frappe.get_test_records('Quality Inspection') @@ -159,6 +159,47 @@ class TestQualityInspection(unittest.TestCase): frappe.delete_doc("Quality Inspection", qi) dn.delete() + def test_rejected_qi_validation(self): + """Test if rejected QI blocks Stock Entry as per Stock Settings.""" + se = make_stock_entry( + item_code="_Test Item with QA", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100, + inspection_required=True, + do_not_submit=True + ) + + readings = [ + { + "specification": "Iron Content", + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + } + ] + + qa = create_quality_inspection( + reference_type="Stock Entry", + reference_name=se.name, + readings=readings, + status="Rejected" + ) + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + se.reload() + self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") + se.reload() + se.submit() # when allowed in Stock settings, allow rejected QI + + # teardown + qa.reload() + qa.cancel() + se.reload() + se.cancel() + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") def create_quality_inspection(**args): args = frappe._dict(args) @@ -175,12 +216,11 @@ def create_quality_inspection(**args): if not args.readings: create_quality_inspection_parameter("Size") readings = {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save else: readings = args.readings - if args.status == "Rejected": - readings["reading_1"] = "12" # status is auto set in child on save - if isinstance(readings, list): for entry in readings: create_quality_inspection_parameter(entry["specification"]) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index b3e4286bcc..4cd40bf38e 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -29,13 +29,50 @@ frappe.ui.form.on('Repost Item Valuation', { }; }); } + + frm.trigger('setup_realtime_progress'); }, + + setup_realtime_progress: function(frm) { + frappe.realtime.on('item_reposting_progress', data => { + if (frm.doc.name !== data.name) { + return; + } + + if (frm.doc.status == 'In Progress') { + frm.doc.current_index = data.current_index; + frm.doc.items_to_be_repost = data.items_to_be_repost; + + frm.dashboard.reset(); + frm.trigger('show_reposting_progress'); + } + }); + }, + refresh: function(frm) { if (frm.doc.status == "Failed" && frm.doc.docstatus==1) { frm.add_custom_button(__('Restart'), function () { frm.trigger("restart_reposting"); }).addClass("btn-primary"); } + + frm.trigger('show_reposting_progress'); + }, + + show_reposting_progress: function(frm) { + var bars = []; + + let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0; + let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5; + var title = __('Reposting Completed {0}%', [progress]); + + bars.push({ + 'title': title, + 'width': progress + '%', + 'progress_class': 'progress-bar-success' + }); + + frm.dashboard.add_progress(__('Reposting Progress'), bars); }, restart_reposting: function(frm) { diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 071fc86d9b..a800bf8701 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -21,7 +21,10 @@ "allow_zero_rate", "amended_from", "error_section", - "error_log" + "error_log", + "items_to_be_repost", + "distinct_item_and_warehouse", + "current_index" ], "fields": [ { @@ -142,12 +145,39 @@ "fieldname": "allow_zero_rate", "fieldtype": "Check", "label": "Allow Zero Rate" + }, + { + "fieldname": "items_to_be_repost", + "fieldtype": "Code", + "hidden": 1, + "label": "Items to Be Repost", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "distinct_item_and_warehouse", + "fieldtype": "Code", + "hidden": 1, + "label": "Distinct Item and Warehouse", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "current_index", + "fieldtype": "Int", + "hidden": 1, + "label": "Current Index", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-12-10 07:52:12.476589", + "modified": "2021-07-22 18:59:43.057878", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 55f2ebb224..2e454a5159 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -80,7 +80,7 @@ def repost(doc): def repost_sl_entries(doc): if doc.based_on == 'Transaction': - repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, + repost_future_sle(doc=doc, voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) else: repost_future_sle(args=[frappe._dict({ @@ -133,6 +133,6 @@ def repost_entries(): def get_repost_item_valuation_entries(): return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` - WHERE status != 'Completed' and creation <= %s and docstatus = 1 + WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc """, now(), as_dict=1) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356..654755ec2f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -529,7 +529,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) @@ -719,6 +719,10 @@ class StockEntry(StockController): frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": + if not finished_items: + frappe.throw(_('Finished Good has not set in the stock entry {0}') + .format(self.name)) + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) @@ -1090,13 +1094,13 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.has_batch_no: + if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', + 'make_serial_no_batch_from_work_order', cache=True)): self.set_batchwise_finished_goods(args, item) else: - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) def set_batchwise_finished_goods(self, args, item): - qty = flt(self.fg_completed_qty) filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, @@ -1105,7 +1109,17 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): + data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + + if not data: + self.add_finished_goods(args, item) + else: + self.add_batchwise_finished_good(data, args, item) + + def add_batchwise_finished_good(self, data, args, item): + qty = flt(self.fg_completed_qty) + + for row in data: batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1121,9 +1135,9 @@ class StockEntry(StockController): args["qty"] = fg_qty args["batch_no"] = row.name - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) - def add_finisged_goods(self, args, item): + def add_finished_goods(self, args, item): self.add_to_stock_entry_detail({ item.name: args }, bom_no = self.bom_no) @@ -1775,7 +1789,7 @@ def get_expired_batch_items(): 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_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b12a8547fe..563fcb0397 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -45,6 +45,8 @@ def make_stock_entry(**args): s.posting_date = args.posting_date if args.posting_time: s.posting_time = args.posting_time + if args.inspection_required: + s.inspection_required = args.inspection_required # map names if args.from_warehouse: diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index a178283904..22f412a298 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -307,6 +307,7 @@ "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", + "no_copy": 1, "options": "Quality Inspection" }, { @@ -548,7 +549,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-22 20:08:23.799715", + "modified": "2021-06-21 16:03:18.834880", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", 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 0febcb6891..b4f458388b 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 @@ class StockLedgerEntry(Document): 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: @@ -89,17 +89,16 @@ class StockLedgerEntry(Document): if item_det.is_stock_item != 1: frappe.throw(_("Item {0} must be a stock Item").format(self.item_code)) - # check if batch number is required - if self.voucher_type != 'Stock Reconciliation': - if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name - if not self.batch_no: - frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + # check if batch number is valid + if item_det.has_batch_no == 1: + batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + if not self.batch_no: + frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) + elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): + frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: - frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) + elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: + frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), @@ -153,7 +152,7 @@ class StockLedgerEntry(Document): 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") @@ -178,3 +177,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) + frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"]) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index a01db80da4..349e59f31d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -17,6 +17,14 @@ frappe.ui.form.on("Stock Reconciliation", { } } }); + frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + filters: { + 'item': item.item_code + } + }; + }); if (frm.doc.company) { erpnext.queries.setup_queries(frm, "Warehouse", function() { diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 84cdc49128..c192582531 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -16,6 +16,7 @@ from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valua from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + class TestStockReconciliation(unittest.TestCase): @classmethod def setUpClass(self): @@ -352,6 +353,26 @@ class TestStockReconciliation(unittest.TestCase): dn2.cancel() pr1.cancel() + def test_valid_batch(self): + create_batch_item_with_batch("Testing Batch Item 1", "001") + create_batch_item_with_batch("Testing Batch Item 2", "002") + sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" + , do_not_submit=True) + self.assertRaises(frappe.ValidationError, sr.submit) + +def create_batch_item_with_batch(item_name, batch_id): + batch_item_doc = create_item(item_name, is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + batch_item_doc.save(ignore_permissions=True) + + if not frappe.db.exists('Batch', batch_id): + b = frappe.new_doc('Batch') + b.item = item_name + b.batch_id = batch_id + b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cf5d98d092..2a9dcfb67e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -23,7 +23,10 @@ "allow_negative_stock", "show_barcode_field", "clean_description_html", + "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", + "column_break_21", + "action_if_quality_inspection_is_rejected", "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", @@ -264,6 +267,22 @@ { "fieldname": "column_break_31", "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_settings_section", + "fieldtype": "Section Break", + "label": "Quality Inspection Settings" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "Stop", + "fieldname": "action_if_quality_inspection_is_rejected", + "fieldtype": "Select", + "label": "Action If Quality Inspection Is Rejected", + "options": "Stop\nWarn" } ], "icon": "icon-cog", @@ -271,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-30 17:27:42.709231", + "modified": "2021-07-10 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index ca174a3f63..2ed7a04ba8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -74,9 +74,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru 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)) @@ -313,8 +312,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "transaction_date": args.get("transaction_date"), "against_blanket_order": args.get("against_blanket_order"), "bom_no": item.get("default_bom"), - "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), - "weight_uom": args.get("weight_uom") or item.get("weight_uom") + "weight_per_unit": item.get("weight_per_unit"), + "weight_uom": item.get("weight_uom") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): @@ -441,7 +440,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t if item_tax_templates is None: item_tax_templates = {} - + if item_rates is None: item_rates = {} @@ -807,10 +806,14 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - if (not args.conversion_rate - and args.currency==frappe.get_cached_value('Company', args.company, "default_currency")): + company_currency = frappe.get_cached_value('Company', args.company, "default_currency") + if (not args.conversion_rate and args.currency==company_currency): args.conversion_rate = 1.0 + if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): + args.conversion_rate = get_exchange_rate(args.currency, + company_currency, args.transaction_date, "for_buying") or 1.0 + # validate currency conversion rate validate_conversion_rate(args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company) 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 0000000000..e69de29bb2 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 0000000000..d7c50a6697 --- /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 0000000000..a14adf8a45 --- /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 0000000000..9e5e63e37e --- /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 14d543b174..bfc4471b9a 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 @@ def get_data(report_filters): data = [] filters = { + "is_cancelled": 0, "company": report_filters.company, "posting_date": ("<=", report_filters.as_on_date) } @@ -34,7 +35,7 @@ def get_data(report_filters): 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/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index b6a8063189..9e56ad4130 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -16,8 +16,6 @@ def execute(filters=None): is_reposting_item_valuation_in_progress() if not filters: filters = {} - validate_filters(filters) - from_date = filters.get('from_date') to_date = filters.get('to_date') @@ -295,12 +293,6 @@ def get_item_reorder_details(items): return dict((d.parent + d.warehouse, d) for d in item_reorder_details) -def validate_filters(filters): - if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) - if sle_count > 500000: - frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries.")) - def get_variants_attributes(): '''Return all item variant attributes.''' return [i.name for i in frappe.get_all('Item Attribute')] 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 5873a7a300..4108a57554 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 @@ def get_consumed_details(filters): 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/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e9c7689ae..8f9ec465e5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -6,13 +6,14 @@ import frappe import erpnext import copy from frappe import _ -from frappe.utils import cint, flt, cstr, now, get_link_to_form +from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel from erpnext.stock.utils import get_bin import json from six import iteritems + # future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): @@ -126,29 +127,61 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.submit() return sle -def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): +def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): if not args and voucher_type and voucher_no: - args = get_args_for_voucher(voucher_type, voucher_no) + args = get_items_to_be_repost(voucher_type, voucher_no, doc) - distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] + distinct_item_warehouses = get_distinct_item_warehouse(args, doc) - i = 0 + i = get_current_index(doc) or 0 while i < len(args): obj = update_entries_after({ - "item_code": args[i].item_code, - "warehouse": args[i].warehouse, - "posting_date": args[i].posting_date, - "posting_time": args[i].posting_time, - "creation": args[i].get("creation") + "item_code": args[i].get('item_code'), + "warehouse": args[i].get('warehouse'), + "posting_date": args[i].get('posting_date'), + "posting_time": args[i].get('posting_time'), + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - for item_wh, new_sle in iteritems(obj.new_items): - if item_wh not in distinct_item_warehouses: - args.append(new_sle) + distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True + if obj.new_items_found: + for item_wh, data in iteritems(distinct_item_warehouses): + if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + data.args_idx = len(args) + args.append(data.sle) + elif data.sle_changed and not data.reposting_status: + args[data.args_idx] = data.sle + + data.sle_changed = False i += 1 -def get_args_for_voucher(voucher_type, voucher_no): + if doc and i % 2 == 0: + update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + + if doc and args: + update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + +def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): + frappe.db.set_value(doc.doctype, doc.name, { + 'items_to_be_repost': json.dumps(args, default=str), + 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str), + 'current_index': index + }) + + frappe.db.commit() + + frappe.publish_realtime('item_reposting_progress', { + 'name': doc.name, + 'items_to_be_repost': json.dumps(args, default=str), + 'current_index': index + }) + +def get_items_to_be_repost(voucher_type, voucher_no, doc=None): + if doc and doc.items_to_be_repost: + return json.loads(doc.items_to_be_repost) or [] + return frappe.db.get_all("Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], @@ -156,6 +189,25 @@ def get_args_for_voucher(voucher_type, voucher_no): group_by="item_code, warehouse" ) +def get_distinct_item_warehouse(args=None, doc=None): + distinct_item_warehouses = {} + if doc and doc.distinct_item_and_warehouse: + distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) + distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()} + else: + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) + + return distinct_item_warehouses + +def get_current_index(doc=None): + if doc and doc.current_index: + return doc.current_index + class update_entries_after(object): """ update valution rate and qty after transaction @@ -186,11 +238,12 @@ class update_entries_after(object): self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) - self.new_items = {} + + self.new_items_found = False + self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.data = frappe._dict() self.initialize_previous_data(self.args) - self.build() def get_precision(self): @@ -296,11 +349,29 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: - if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: - self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + self.update_distinct_item_warehouses(dependant_sle) return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix + else: + return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + + def update_distinct_item_warehouses(self, dependant_sle): + key = (dependant_sle.item_code, dependant_sle.warehouse) + val = frappe._dict({ + "sle": dependant_sle + }) + if key not in self.distinct_item_warehouses: + self.distinct_item_warehouses[key] = val + self.new_items_found = True + else: + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + val.sle_changed = True + self.distinct_item_warehouses[key] = val + self.new_items_found = True + + def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -393,6 +464,7 @@ class update_entries_after(object): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": + self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): @@ -442,7 +514,11 @@ class update_entries_after(object): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True) + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) + + def recalculate_amounts_in_stock_entry(self, voucher_no): + stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8a6a3a3e4a..b57b2aa6b8 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -314,13 +314,16 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto 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 @@ def is_reposting_item_valuation_in_progress(): 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) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 4d553df08b..c00dfa9056 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -142,7 +142,7 @@ def link_existing_conversations(doc, state): for log in logs: call_log = frappe.get_doc('Call Log', log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) - call_log.save() + call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: frappe.log_error(title=_('Error during caller information update')) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 393c3a43af..9050cc388a 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -9,7 +9,7 @@ {% endblock %} {% block page_content %} -
+
{% if slideshow %} {{ web_block( @@ -127,15 +127,36 @@
-
-
+
+
+
+
{% if frappe.form_dict.start|int > 0 %} - + {% endif %} {% if items|length >= page_length %} - + {% endif %}
+ + {% endblock %} \ No newline at end of file diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 0721056816..1c641b59ad 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -124,6 +124,10 @@ $(() => { attribute_filters: if_key_exists(attribute_filters) }; + const item_group = $(".item-group-content").data('item-group'); + if (item_group) { + Object.assign(field_filters, { item_group }); + } return new Promise((resolve, reject) => { frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args) .then(r => {