diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index d5f00527444..aae2928bf0d 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -43,14 +43,16 @@ jobs: fi - name: Setup Python - uses: "gabrielfalcao/pyenv-action@v9" + uses: "actions/setup-python@v4" with: - versions: 3.10:latest, 3.7:latest + python-version: | + 3.7 + 3.10 - name: Setup Node uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 18 check-latest: true - name: Add to Hosts @@ -92,7 +94,6 @@ jobs: - name: Install run: | pip install frappe-bench - pyenv global $(pyenv versions | grep '3.10') bash ${GITHUB_WORKSPACE}/.github/helper/install.sh env: DB: mariadb @@ -107,7 +108,6 @@ jobs: git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git - pyenv global $(pyenv versions | grep '3.7') for version in $(seq 12 13) do echo "Updating to v$version" @@ -120,7 +120,7 @@ jobs: git -C "apps/erpnext" checkout -q -f $branch_name rm -rf ~/frappe-bench/env - bench setup env + bench setup env --python python3.7 bench pip install -e ./apps/payments bench pip install -e ./apps/erpnext @@ -132,9 +132,8 @@ jobs: git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" - pyenv global $(pyenv versions | grep '3.10') rm -rf ~/frappe-bench/env - bench -v setup env + bench -v setup env --python python3.10 bench pip install -e ./apps/payments bench pip install -e ./apps/erpnext diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml index 1744bc33a9e..0e478d551d9 100644 --- a/.github/workflows/semantic-commits.yml +++ b/.github/workflows/semantic-commits.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 18 check-latest: true - name: Check commit titles diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index 8959f7fd45a..9b4db49d084 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -71,7 +71,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 18 check-latest: true - name: Add to Hosts diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index df438014789..a6887066570 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -59,7 +59,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 18 check-latest: true - name: Add to Hosts diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 8a6b021b8ad..6f0b6fcd912 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -68,6 +68,16 @@ frappe.ui.form.on('Accounting Dimension Filter', { frm.refresh_field("dimensions"); frm.trigger('setup_filters'); }, + apply_restriction_on_values: function(frm) { + /** If restriction on values is not applied, we should set "allow_or_restrict" to "Restrict" with an empty allowed dimension table. + * Hence it's not "restricted" on any value. + */ + if (!frm.doc.apply_restriction_on_values) { + frm.set_value("allow_or_restrict", "Restrict"); + frm.clear_table("dimensions"); + frm.refresh_field("dimensions"); + } + } }); frappe.ui.form.on('Allowed Dimension', { diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index 0f3fbc0b8d3..2bd6c12a0a3 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -10,6 +10,7 @@ "disabled", "column_break_2", "company", + "apply_restriction_on_values", "allow_or_restrict", "section_break_4", "accounts", @@ -24,94 +25,80 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Accounting Dimension", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "column_break_2", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "section_break_4", "fieldtype": "Section Break", - "hide_border": 1, - "show_days": 1, - "show_seconds": 1 + "hide_border": 1 }, { "fieldname": "column_break_6", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.apply_restriction_on_values == 1;", "fieldname": "allow_or_restrict", "fieldtype": "Select", "label": "Allow Or Restrict Dimension", "options": "Allow\nRestrict", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "accounts", "fieldtype": "Table", "label": "Applicable On Account", "options": "Applicable On Account", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { - "depends_on": "eval:doc.accounting_dimension", + "depends_on": "eval:doc.accounting_dimension && doc.apply_restriction_on_values", "fieldname": "dimensions", "fieldtype": "Table", "label": "Applicable Dimension", - "options": "Allowed Dimension", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.apply_restriction_on_values == 1;", + "options": "Allowed Dimension" }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled", - "show_days": 1, - "show_seconds": 1 + "label": "Disabled" }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "dimension_filter_help", "fieldtype": "HTML", - "label": "Dimension Filter Help", - "show_days": 1, - "show_seconds": 1 + "label": "Dimension Filter Help" }, { "fieldname": "section_break_10", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" + }, + { + "default": "1", + "fieldname": "apply_restriction_on_values", + "fieldtype": "Check", + "label": "Apply restriction on dimension values" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-03 12:04:58.678402", + "modified": "2023-06-07 14:59:41.869117", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -154,5 +141,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 80f736fa5bb..de1b82c1d5f 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -8,6 +8,12 @@ from frappe.model.document import Document class AccountingDimensionFilter(Document): + def before_save(self): + # If restriction is not applied on values, then remove all the dimensions and set allow_or_restrict to Restrict + if not self.apply_restriction_on_values: + self.allow_or_restrict = "Restrict" + self.set("dimensions", []) + def validate(self): self.validate_applicable_accounts() @@ -44,12 +50,12 @@ def get_dimension_filter_map(): a.applicable_on_account, d.dimension_value, p.accounting_dimension, p.allow_or_restrict, a.is_mandatory FROM - `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabApplicable On Account` a, `tabAccounting Dimension Filter` p + LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name WHERE p.name = a.parent AND p.disabled = 0 - AND p.name = d.parent """, as_dict=1, ) @@ -76,4 +82,5 @@ def build_map(map_object, dimension, account, filter_value, allow_or_restrict, i (dimension, account), {"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict}, ) - map_object[(dimension, account)]["allowed_dimensions"].append(filter_value) + if filter_value: + map_object[(dimension, account)]["allowed_dimensions"].append(filter_value) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index f13f2f9f279..6aba2ab253f 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -64,6 +64,7 @@ def create_accounting_dimension_filter(): "accounting_dimension": "Cost Center", "allow_or_restrict": "Allow", "company": "_Test Company", + "apply_restriction_on_values": 1, "accounts": [ { "applicable_on_account": "Sales - _TC", @@ -85,6 +86,7 @@ def create_accounting_dimension_filter(): "doctype": "Accounting Dimension Filter", "accounting_dimension": "Department", "allow_or_restrict": "Allow", + "apply_restriction_on_values": 1, "company": "_Test Company", "accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}], "dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}], diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index b91f0f91371..363a2776aa7 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -70,7 +70,6 @@ def make_bank_account(doctype, docname): return doc -@frappe.whitelist() def get_party_bank_account(party_type, party): return frappe.db.get_value(party_type, party, "default_bank_account") diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index c4a23a640c3..0eef3e9a67b 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt +from erpnext import get_default_cost_center from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( get_amounts_not_reflected_in_system, @@ -140,6 +141,9 @@ def create_journal_entry_bts( second_account ) ) + + company = frappe.get_value("Account", company_account, "company") + accounts = [] # Multi Currency? accounts.append( @@ -149,6 +153,7 @@ def create_journal_entry_bts( "debit_in_account_currency": bank_transaction.withdrawal, "party_type": party_type, "party": party, + "cost_center": get_default_cost_center(company), } ) @@ -158,11 +163,10 @@ def create_journal_entry_bts( "bank_account": bank_transaction.bank_account, "credit_in_account_currency": bank_transaction.withdrawal, "debit_in_account_currency": bank_transaction.deposit, + "cost_center": get_default_cost_center(company), } ) - company = frappe.get_value("Account", company_account, "company") - journal_entry_dict = { "voucher_type": entry_type, "company": company, diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index f51b90d8f6a..1ef5c837402 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', { validate_rounding_loss: function(frm) { let allowance = frm.doc.rounding_loss_allowance; - if (!(allowance > 0 && allowance < 1)) { + if (!(allowance >= 0 && allowance < 1)) { frappe.throw(__("Rounding Loss Allowance should be between 0 and 1")); } }, diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json index 2310d1272cd..79428d591b4 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json @@ -100,15 +100,16 @@ }, { "default": "0.05", - "description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account", + "description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account", "fieldname": "rounding_loss_allowance", "fieldtype": "Float", - "label": "Rounding Loss Allowance" + "label": "Rounding Loss Allowance", + "precision": "9" } ], "is_submittable": 1, "links": [], - "modified": "2023-06-12 21:02:09.818208", + "modified": "2023-06-20 07:29:06.972434", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation", 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 5d239c91f71..598db642f33 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -22,7 +22,7 @@ class ExchangeRateRevaluation(Document): self.set_total_gain_loss() def validate_rounding_loss_allowance(self): - if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1): + if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1): frappe.throw(_("Rounding Loss Allowance should be between 0 and 1")) def set_total_gain_loss(self): @@ -373,6 +373,24 @@ class ExchangeRateRevaluation(Document): "credit": 0, } ) + + journal_entry_accounts.append(journal_account) + + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit": 0, + "credit": 0, + "debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0, + "credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "exchange_rate": 1, + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"): # Base currency has balance dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit" @@ -388,22 +406,22 @@ class ExchangeRateRevaluation(Document): } ) - journal_entry_accounts.append(journal_account) + journal_entry_accounts.append(journal_account) - journal_entry_accounts.append( - { - "account": unrealized_exchange_gain_loss_account, - "balance": get_balance_on(unrealized_exchange_gain_loss_account), - "debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, - "credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0, - "debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, - "credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "exchange_rate": 1, - "reference_type": "Exchange Rate Revaluation", - "reference_name": self.name, - } - ) + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit": abs(d.gain_loss) if d.gain_loss < 0 else 0, + "credit": abs(d.gain_loss) if d.gain_loss > 0 else 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "exchange_rate": 1, + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_total_debit_credit() diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json index 2968359a0d0..fd2d931315c 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json @@ -73,6 +73,7 @@ "fieldname": "current_exchange_rate", "fieldtype": "Float", "label": "Current Exchange Rate", + "precision": "9", "read_only": 1 }, { @@ -92,6 +93,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "New Exchange Rate", + "precision": "9", "reqd": 1 }, { @@ -147,7 +149,7 @@ ], "istable": 1, "links": [], - "modified": "2022-12-29 19:38:52.915295", + "modified": "2023-06-22 12:39:56.446722", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation Account", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 6d9e3202f10..a51e38eefea 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule']; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 74fd5596123..83312dbd229 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -326,12 +326,10 @@ class JournalEntry(AccountsController): d.db_update() def unlink_asset_reference(self): - if self.voucher_type != "Depreciation Entry": - return - for d in self.get("accounts"): if ( - d.reference_type == "Asset" + self.voucher_type == "Depreciation Entry" + and d.reference_type == "Asset" and d.reference_name and d.account_type == "Depreciation" and d.debit @@ -370,6 +368,15 @@ class JournalEntry(AccountsController): else: asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit) asset.set_status() + elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name: + journal_entry_for_scrap = frappe.db.get_value( + "Asset", d.reference_name, "journal_entry_for_scrap" + ) + + if journal_entry_for_scrap == self.name: + frappe.throw( + _("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.") + ) def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index aa67537e9f0..0701435dfc7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -155,6 +155,7 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); }, validate_company: (frm) => { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cbf8c0865f3..37459e3cf00 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1855,7 +1855,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if not total_amount: if party_account_currency == company_currency: # for handling cases that don't have multi-currency (base field) - total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total") + total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total") exchange_rate = 1 else: total_amount = ref_doc.get("grand_total") diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 278b12f6595..ae2625b6539 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -11,6 +11,7 @@ from frappe.utils import flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( InvalidPaymentEntry, get_payment_entry, + get_reference_details, ) from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( make_purchase_invoice, @@ -1037,6 +1038,29 @@ class TestPaymentEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, pe_draft.submit) + def test_details_update_on_reference_table(self): + so = make_sales_order( + customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True + ) + so.conversion_rate = 50 + so.submit() + pe = get_payment_entry("Sales Order", so.name) + pe.references.clear() + pe.paid_from = "Debtors - _TC" + pe.paid_from_account_currency = "INR" + pe.source_exchange_rate = 50 + pe.save() + + ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency) + expected_response = { + "total_amount": 5000.0, + "outstanding_amount": 5000.0, + "exchange_rate": 1.0, + "due_date": None, + "bill_no": None, + } + self.assertDictEqual(ref_details, expected_response) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 16e3f95ee63..2adc1238b70 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -96,25 +96,29 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo // check for any running reconciliation jobs if (this.frm.doc.receivable_payable_account) { - frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => { - if(enabled) { - this.frm.call({ - 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", - "args": { - for_filter: { - company: this.frm.doc.company, - party_type: this.frm.doc.party_type, - party: this.frm.doc.party, - receivable_payable_account: this.frm.doc.receivable_payable_account + this.frm.call({ + doc: this.frm.doc, + method: 'is_auto_process_enabled', + callback: (r) => { + if (r.message) { + this.frm.call({ + 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", + "args": { + for_filter: { + company: this.frm.doc.company, + party_type: this.frm.doc.party_type, + party: this.frm.doc.party, + receivable_payable_account: this.frm.doc.receivable_payable_account + } } - } - }).then(r => { - if (r.message) { - let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); - let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); - this.frm.dashboard.add_comment(msg, "yellow"); - } - }); + }).then(r => { + if (r.message) { + let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); + let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); + this.frm.dashboard.add_comment(msg, "yellow"); + } + }); + } } }); } diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 8e2f0e5232b..25d94c55d3a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -268,6 +268,10 @@ class PaymentReconciliation(Document): return difference_amount + @frappe.whitelist() + def is_auto_process_enabled(self): + return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments") + @frappe.whitelist() def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) @@ -359,7 +363,10 @@ class PaymentReconciliation(Document): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - if payment_details.difference_amount: + if payment_details.difference_amount and row.reference_type not in [ + "Sales Invoice", + "Purchase Invoice", + ]: self.make_difference_entry(payment_details) if entry_list: @@ -445,6 +452,8 @@ class PaymentReconciliation(Document): journal_entry.save() journal_entry.submit() + return journal_entry + def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -610,6 +619,16 @@ class PaymentReconciliation(Document): def reconcile_dr_cr_note(dr_cr_notes, company): + def get_difference_row(inv): + if inv.difference_amount != 0 and inv.difference_account: + difference_row = { + "account": inv.difference_account, + inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0, + reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0, + "cost_center": erpnext.get_default_cost_center(company), + } + return difference_row + for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -654,5 +673,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ], } ) + + if difference_entry := get_difference_row(inv): + jv.append("accounts", difference_entry) + jv.flags.ignore_mandatory = True jv.submit() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 3be11ae31a7..2ac7df0e39b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -11,10 +11,13 @@ from frappe.utils import add_days, flt, nowdate from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item +test_dependencies = ["Item"] + class TestPaymentReconciliation(FrappeTestCase): def setUp(self): @@ -163,7 +166,9 @@ class TestPaymentReconciliation(FrappeTestCase): def create_payment_reconciliation(self): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = "Customer" + pr.party_type = ( + self.party_type if hasattr(self, "party_type") and self.party_type else "Customer" + ) pr.party = self.customer pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() @@ -890,6 +895,42 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].allocated_amount, 85) self.assertEqual(pr.allocation[0].difference_amount, 0) + def test_reconciliation_purchase_invoice_against_return(self): + pi = make_purchase_invoice( + supplier="_Test Supplier USD", currency="USD", conversion_rate=50 + ).submit() + + pi_return = frappe.get_doc(pi.as_dict()) + pi_return.name = None + pi_return.docstatus = 0 + pi_return.is_return = 1 + pi_return.conversion_rate = 80 + pi_return.items[0].qty = -pi_return.items[0].qty + pi_return.submit() + + self.company = "_Test Company" + self.party_type = "Supplier" + self.customer = "_Test Supplier USD" + + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + + invoices = [] + payments = [] + for invoice in pr.invoices: + if invoice.invoice_number == pi.name: + invoices.append(invoice.as_dict()) + break + for payment in pr.payments: + if payment.reference_name == pi_return.name: + payments.append(payment.as_dict()) + break + + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. + pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js index ea18adefa35..6046c13e146 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js @@ -2,7 +2,11 @@ // For license information, please see license.txt frappe.ui.form.on('Payment Terms Template', { - setup: function(frm) { + refresh: function(frm) { + frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); + }, + allocate_payment_based_on_payment_terms: function(frm) { + frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); } }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index ea3b76c5243..7b04a68e89a 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -11,7 +11,7 @@ from frappe.utils import flt class PaymentTermsTemplate(Document): def validate(self): self.validate_invoice_portion() - self.check_duplicate_terms() + self.validate_terms() def validate_invoice_portion(self): total_portion = 0 @@ -23,9 +23,12 @@ class PaymentTermsTemplate(Document): _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red" ) - def check_duplicate_terms(self): + def validate_terms(self): terms = [] for term in self.terms: + if self.allocate_payment_based_on_payment_terms and not term.payment_term: + frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx)) + term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index e6d9fe2b54d..a6c0102a7f9 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -123,22 +123,29 @@ frappe.ui.form.on('POS Closing Entry', { row.expected_amount = row.opening_amount; } - const pos_inv_promises = frm.doc.pos_transactions.map( - row => frappe.db.get_doc("POS Invoice", row.pos_invoice) - ); - - const pos_invoices = await Promise.all(pos_inv_promises); - - for (let doc of pos_invoices) { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - } - + await Promise.all([ + frappe.call({ + method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', + args: { + start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), + end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), + pos_profile: frm.doc.pos_profile, + user: frm.doc.user + }, + callback: (r) => { + let pos_invoices = r.message; + for (let doc of pos_invoices) { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); + } + } + }) + ]) frappe.dom.unfreeze(); } }); diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 03abc93e0b8..5307ccb1931 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -1,6 +1,6 @@
| {{ _(" ") }} | +{{ _(range1) }} | +{{ _(range2) }} | +{{ _(range3) }} | +{{ _(range4) }} | +{{ _(range5) }} | +{{ _(range6) }} | +{{ _("Total") }} | +
|---|---|---|---|---|---|---|---|
| {{ _("Total Outstanding") }} | ++ {{ format_number(balance_row["age"], null, 2) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }} + | ++ {{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }} + | +{{ _("Future Payments") }} | ++ | + | + | + | + | + | + {{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }} + | +
| {{ _("Cheques Required") }} | ++ | + | + | + | + | + | + {{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }} | +
| {{ _("Date") }} | +{{ _("Age (Days)") }} | + + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} +{{ _("Reference") }} | +{{ _("Sales Person") }} | + {% else %} +{{ _("Reference") }} | + {% endif %} + {% if not(filters.show_future_payments) %} ++ {% if (filters.customer or filters.supplier or filters.customer_name) %} + {{ _("Remarks") }} + {% else %} + {{ _("Party") }} + {% endif %} + | + {% endif %} +{{ _("Invoiced Amount") }} | + {% if not(filters.show_future_payments) %} +{{ _("Paid Amount") }} | ++ {% if report.report_name == "Accounts Receivable" %} + {{ _('Credit Note') }} + {% else %} + {{ _('Debit Note') }} + {% endif %} + | + {% endif %} +{{ _("Outstanding Amount") }} | + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} +{{ _("Customer LPO No.") }} | + {% endif %} +{{ _("Future Payment Ref") }} | +{{ _("Future Payment Amount") }} | +{{ _("Remaining Balance") }} | + {% endif %} + {% else %} ++ {% if (filters.customer or filters.supplier or filters.customer_name) %} + {{ _("Remarks")}} + {% else %} + {{ _("Party") }} + {% endif %} + | +{{ _("Total Invoiced Amount") }} | +{{ _("Total Paid Amount") }} | ++ {% if report.report_name == "Accounts Receivable Summary" %} + {{ _('Credit Note Amount') }} + {% else %} + {{ _('Debit Note Amount') }} + {% endif %} + | +{{ _("Total Outstanding Amount") }} | + {% endif %} +|||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ (data[i]["posting_date"]) }} | +{{ data[i]["age"] }} | +
+ {% if not(filters.show_future_payments) %}
+ {{ data[i]["voucher_type"] }}
+ + {% endif %} + {{ data[i]["voucher_no"] }} + |
+
+ {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+ {{ data[i]["sales_person"] }} | + {% endif %} + + {% if not (filters.show_future_payments) %} +
+ {% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
+ {{ data[i]["party"] }}
+ {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+ {{ data[i]["customer_name"] }} + {% elif(data[i]["supplier_name"] != data[i]["party"]) %} + {{ data[i]["supplier_name"] }} + {% endif %} + {% endif %} +
+ {% if data[i]["remarks"] %}
+ {{ _("Remarks") }}:
+ {{ data[i]["remarks"] }}
+ {% endif %}
+
+ |
+ {% endif %}
+
+ + {{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }} | + + {% if not(filters.show_future_payments) %} ++ {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} | ++ {{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} | + {% endif %} ++ {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} | + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} ++ {{ data[i]["po_no"] }} | + {% endif %} +{{ data[i]["future_ref"] }} | +{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }} | + {% endif %} + {% else %} ++ {% if not(filters.show_future_payments) %} + | + {% endif %} + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} + | + {% endif %} + | + | {{ _("Total") }} | ++ {{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }} | + + {% if not(filters.show_future_payments) %} ++ {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} | + {% endif %} ++ {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} | + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} ++ {{ data[i]["po_no"] }} | + {% endif %} +{{ data[i]["future_ref"] }} | +{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }} | + {% endif %} + {% endif %} + {% else %} + {% if(data[i]["party"] or " ") %} + {% if not(data[i]["is_total_row"]) %} +
+ {% if(not(filters.customer | filters.supplier)) %}
+ {{ data[i]["party"] }}
+ {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+ {{ data[i]["customer_name"] }} + {% elif(data[i]["supplier_name"] != data[i]["party"]) %} + {{ data[i]["supplier_name"] }} + {% endif %} + {% endif %} + {{ _("Remarks") }}: + {{ data[i]["remarks"] }} + |
+ {% else %}
+ {{ _("Total") }} | + {% endif %} +{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} | +{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} | + {% endif %} + {% endif %} ++ | + | + | + | {{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }} | + +
| 30 Days | +60 Days | +90 Days | +120 Days | +
|---|---|---|---|
| {{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }} | +{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }} | +
{{ _("Printed On ") }}{{ frappe.utils.now() }}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ab7884d5209..6a558ca606b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -54,9 +54,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. hide_fields(this.frm.doc); // Show / Hide button this.show_general_ledger(); + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); - if(doc.update_stock==1 && doc.docstatus==1) { + if(doc.update_stock==1) { this.show_stock_ledger(); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); } if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8cb29505eb2..68407e02210 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -88,8 +88,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } this.show_general_ledger(); + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); - if(doc.update_stock) this.show_stock_ledger(); + if(doc.update_stock){ + this.show_stock_ledger(); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); + } if (doc.docstatus == 1 && doc.outstanding_amount!=0 && !(cint(doc.is_return) && doc.return_against)) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ab4aab3da29..f0d3f720948 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2157,7 +2157,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-06-19 16:02:05.309332", + "modified": "2023-06-21 16:02:18.988799", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", 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 c2b7ff0f352..58792d1d8ad 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -585,7 +585,9 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): "supplier": ("in", parties), "apply_tds": 1, "docstatus": 1, + "tax_withholding_category": ldc.tax_withholding_category, "posting_date": ("between", (ldc.valid_from, ldc.valid_upto)), + "company": ldc.company, }, "sum(tax_withholding_net_total)", ) @@ -615,7 +617,7 @@ def is_valid_certificate( ): valid = False - available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount) + available_amount = flt(certificate_limit) - flt(deducted_amount) if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0: valid = True diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index f2bf9424f72..ed3b9915591 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -284,4 +284,4 @@ {% } %} -{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
+{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
\ No newline at end of file diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index d34c21348c8..6fdb2f337c0 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -15,7 +15,6 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i get_group_by_conditions, get_tax_accounts, ) -from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details def execute(filters=None): @@ -40,6 +39,16 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum tax_doctype="Purchase Taxes and Charges", ) + scrubbed_tax_fields = {} + + for tax in tax_columns: + scrubbed_tax_fields.update( + { + tax + " Rate": frappe.scrub(tax + " Rate"), + tax + " Amount": frappe.scrub(tax + " Amount"), + } + ) + po_pr_map = get_purchase_receipts_against_purchase_order(item_list) data = [] @@ -50,11 +59,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get("group_by"): grand_total = get_grand_total(filters, "Purchase Invoice") - item_details = get_item_details() - for d in item_list: - item_record = item_details.get(d.item_code) - purchase_receipt = None if d.purchase_receipt: purchase_receipt = d.purchase_receipt @@ -67,8 +72,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { "item_code": d.item_code, - "item_name": item_record.item_name if item_record else d.item_name, - "item_group": item_record.item_group if item_record else d.item_group, + "item_name": d.pi_item_name if d.pi_item_name else d.i_item_name, + "item_group": d.pi_item_group if d.pi_item_group else d.i_item_group, "description": d.description, "invoice": d.parent, "posting_date": d.posting_date, @@ -87,7 +92,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum "project": d.project, "company": d.company, "purchase_order": d.purchase_order, - "purchase_receipt": d.purchase_receipt, + "purchase_receipt": purchase_receipt, "expense_account": expense_account, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, @@ -101,8 +106,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( { - frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), - frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0), + scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0), } ) total_tax += flt(item_tax.get("tax_amount")) @@ -241,7 +246,7 @@ def get_columns(additional_table_columns, filters): }, { "label": _("Purchase Receipt"), - "fieldname": "Purchase Receipt", + "fieldname": "purchase_receipt", "fieldtype": "Link", "options": "Purchase Receipt", "width": 100, @@ -325,15 +330,17 @@ def get_items(filters, additional_query_columns): `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice`.unrealized_profit_loss_account, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, - `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, + `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group, + `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`, `tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {0} - from `tabPurchase Invoice`, `tabPurchase Invoice Item` + from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem` where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and - `tabPurchase Invoice`.docstatus = 1 %s + `tabItem`.name = `tabPurchase Invoice Item`.`item_code` and + `tabPurchase Invoice`.docstatus = 1 %s """.format( additional_query_columns ) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 0ebe13f4f32..bd7d02e0430 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -11,7 +11,6 @@ from frappe.utils.xlsxutils import handle_html from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import ( get_customer_details, - get_item_details, ) @@ -35,6 +34,16 @@ def _execute( if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) + scrubbed_tax_fields = {} + + for tax in tax_columns: + scrubbed_tax_fields.update( + { + tax + " Rate": frappe.scrub(tax + " Rate"), + tax + " Amount": frappe.scrub(tax + " Amount"), + } + ) + mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list)) so_dn_map = get_delivery_notes_against_sales_order(item_list) @@ -47,11 +56,9 @@ def _execute( grand_total = get_grand_total(filters, "Sales Invoice") customer_details = get_customer_details() - item_details = get_item_details() for d in item_list: customer_record = customer_details.get(d.customer) - item_record = item_details.get(d.item_code) delivery_note = None if d.delivery_note: @@ -64,8 +71,8 @@ def _execute( row = { "item_code": d.item_code, - "item_name": item_record.item_name if item_record else d.item_name, - "item_group": item_record.item_group if item_record else d.item_group, + "item_name": d.si_item_name if d.si_item_name else d.i_item_name, + "item_group": d.si_item_group if d.si_item_group else d.i_item_group, "description": d.description, "invoice": d.parent, "posting_date": d.posting_date, @@ -107,8 +114,8 @@ def _execute( item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( { - frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), - frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0), + scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0), } ) if item_tax.get("is_other_charges"): @@ -404,15 +411,18 @@ def get_items(filters, additional_query_columns, additional_conditions=None): `tabSales Invoice Item`.project, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`, + `tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group, + `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} - from `tabSales Invoice`, `tabSales Invoice Item` - where `tabSales Invoice`.name = `tabSales Invoice Item`.parent - and `tabSales Invoice`.docstatus = 1 {1} + from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem` + where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and + `tabItem`.name = `tabSales Invoice Item`.`item_code` and + `tabSales Invoice`.docstatus = 1 {1} """.format( additional_query_columns or "", conditions ), diff --git a/erpnext/accounts/report/voucher_wise_balance/__init__.py b/erpnext/accounts/report/voucher_wise_balance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js new file mode 100644 index 00000000000..0c148f85fb8 --- /dev/null +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js @@ -0,0 +1,28 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Voucher-wise Balance"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company" + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "width": "60px" + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "width": "60px" + }, + ] +}; diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json new file mode 100644 index 00000000000..434e5a3b257 --- /dev/null +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-06-27 16:40:15.109554", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "LetterHead", + "modified": "2023-06-27 16:40:32.493725", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Voucher-wise Balance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Voucher-wise Balance", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py new file mode 100644 index 00000000000..5ab3611b9af --- /dev/null +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py @@ -0,0 +1,66 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.query_builder.functions import Sum + + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + return columns, data + + +def get_columns(): + return [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 300}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 300, + }, + { + "fieldname": "debit", + "label": _("Debit"), + "fieldtype": "Currency", + "options": "currency", + "width": 300, + }, + { + "fieldname": "credit", + "label": _("Credit"), + "fieldtype": "Currency", + "options": "currency", + "width": 300, + }, + ] + + +def get_data(filters): + gle = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gle) + .select( + gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit") + ) + .groupby(gle.voucher_no) + ) + query = apply_filters(query, filters, gle) + gl_entries = query.run(as_dict=True) + unmatched = [entry for entry in gl_entries if entry.debit != entry.credit] + return unmatched + + +def apply_filters(query, filters, gle): + if filters.get("company"): + query = query.where(gle.company == filters.company) + if filters.get("voucher_type"): + query = query.where(gle.voucher_type == filters.voucher_type) + if filters.get("from_date"): + query = query.where(gle.posting_date >= filters.from_date) + if filters.get("to_date"): + query = query.where(gle.posting_date <= filters.to_date) + return query diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2a40a3abafc..9000b0d32ed 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -237,11 +237,6 @@ def get_balance_on( if not (frappe.flags.ignore_account_permission or ignore_account_permission): acc.check_permission("read") - if report_type == "Profit and Loss": - # for pl accounts, get balance within a fiscal year - cond.append( - "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date - ) # different filter for group and ledger - improved performance if acc.is_group: cond.append( diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index bfef57e4947..259568a24b1 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -159,15 +159,15 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None): je.flags.ignore_permissions = True je.flags.planned_depr_entry = True je.save() - if not je.meta.get_workflow(): - je.submit() d.db_set("journal_entry", je.name) - idx = cint(asset_depr_schedule_doc.finance_book_id) - row = asset.get("finance_books")[idx - 1] - row.value_after_depreciation -= d.depreciation_amount - row.db_update() + if not je.meta.get_workflow(): + je.submit() + idx = cint(asset_depr_schedule_doc.finance_book_id) + row = asset.get("finance_books")[idx - 1] + row.value_after_depreciation -= d.depreciation_amount + row.db_update() asset.db_set("depr_entry_posting_status", "Successful") diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 01fcb11d817..6d55d7772b2 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -15,7 +15,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } refresh() { - erpnext.hide_company(); this.show_general_ledger(); if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { this.show_stock_ledger(); @@ -129,10 +128,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } - target_asset() { - return this.get_target_asset_details(); - } - item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -247,26 +242,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } - get_target_asset_details() { - var me = this; - - if (me.frm.doc.target_asset) { - return me.frm.call({ - method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", - child: me.frm.doc, - args: { - asset: me.frm.doc.target_asset, - company: me.frm.doc.company, - }, - callback: function (r) { - if (!r.exc) { - me.frm.refresh_fields(); - } - } - }); - } - } - get_consumed_stock_item_details(row) { var me = this; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 01b35f64ab0..04b0c4e5132 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -11,13 +11,14 @@ "naming_series", "entry_type", "target_item_code", + "target_asset", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", "column_break_9", - "target_asset", "target_asset_name", + "target_asset_location", "target_warehouse", "target_qty", "target_stock_uom", @@ -85,14 +86,13 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", "no_copy": 1, - "options": "Asset" + "options": "Asset", + "read_only": 1 }, { "depends_on": "eval:doc.entry_type=='Capitalization'", @@ -108,11 +108,11 @@ "fieldtype": "Column Break" }, { - "fetch_from": "asset.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", + "remember_last_selected_value": 1, "reqd": 1 }, { @@ -158,7 +158,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", + "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))", "fieldname": "section_break_16", "fieldtype": "Section Break", "label": "Consumed Stock Items" @@ -189,7 +189,7 @@ "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", - "read_only_depends_on": "target_is_fixed_asset" + "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { "fetch_from": "target_item_code.stock_uom", @@ -227,7 +227,7 @@ "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", "fieldname": "section_break_26", "fieldtype": "Section Break", - "label": "Consumed Asset Items" + "label": "Consumed Assets" }, { "fieldname": "asset_items", @@ -266,7 +266,7 @@ "options": "Finance Book" }, { - "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", + "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))", "fieldname": "service_expenses_section", "fieldtype": "Section Break", "label": "Service Expenses" @@ -329,12 +329,20 @@ "label": "Target Fixed Asset Account", "options": "Account", "read_only": 1 + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "target_asset_location", + "fieldtype": "Link", + "label": "Target Asset Location", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "Location" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-10-12 15:09:40.771332", + "modified": "2023-06-22 14:17:07.995120", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 6841c56b108..a883bec71b0 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -19,9 +19,6 @@ from erpnext.assets.doctype.asset.depreciation import ( reverse_depreciation_entry_made_after_disposal, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account -from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - make_new_active_asset_depr_schedules_and_cancel_current_ones, -) from erpnext.controllers.stock_controller import StockController from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -45,7 +42,6 @@ force_fields = [ "target_has_batch_no", "target_stock_uom", "stock_uom", - "target_fixed_asset_account", "fixed_asset_account", "valuation_rate", ] @@ -56,7 +52,6 @@ class AssetCapitalization(StockController): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() - self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -71,11 +66,12 @@ class AssetCapitalization(StockController): def before_submit(self): self.validate_source_mandatory() + if self.entry_type == "Capitalization": + self.create_target_asset() def on_submit(self): self.update_stock_ledger() self.make_gl_entries() - self.update_target_asset() def on_cancel(self): self.ignore_linked_doctypes = ( @@ -86,7 +82,7 @@ class AssetCapitalization(StockController): ) self.update_stock_ledger() self.make_gl_entries() - self.update_target_asset() + self.restore_consumed_asset_items() def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -97,15 +93,6 @@ class AssetCapitalization(StockController): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) - # Remove asset if item not a fixed asset - if not self.target_is_fixed_asset: - self.target_asset = None - - target_asset_details = get_target_asset_details(self.target_asset, self.company) - for k, v in target_asset_details.items(): - if self.meta.has_field(k) and (not self.get(k) or k in force_fields): - self.set(k, v) - for d in self.stock_items: args = self.as_dict() args.update(d.as_dict()) @@ -157,9 +144,6 @@ class AssetCapitalization(StockController): if not target_item.is_stock_item: self.target_warehouse = None - if not target_item.is_fixed_asset: - self.target_asset = None - self.target_fixed_asset_account = None if not target_item.has_batch_no: self.target_batch_no = None if not target_item.has_serial_no: @@ -170,17 +154,6 @@ class AssetCapitalization(StockController): self.validate_item(target_item) - def validate_target_asset(self): - if self.target_asset: - target_asset = self.get_asset_for_validation(self.target_asset) - - if target_asset.item_code != self.target_item_code: - frappe.throw( - _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) - ) - - self.validate_asset(target_asset) - def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -386,7 +359,11 @@ class AssetCapitalization(StockController): gl_entries, target_account, target_against, precision ) + if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable: + return [] + self.get_gl_entries_for_target_item(gl_entries, target_against, precision) + return gl_entries def get_target_account(self): @@ -429,11 +406,14 @@ class AssetCapitalization(StockController): def get_gl_entries_for_consumed_asset_items( self, gl_entries, target_account, target_against, precision ): + self.are_all_asset_items_non_depreciable = True + # Consumed Assets for item in self.asset_items: - asset = self.get_asset(item) + asset = frappe.get_doc("Asset", item.asset) if asset.calculate_depreciation: + self.are_all_asset_items_non_depreciable = False notes = _( "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." ).format( @@ -519,40 +499,46 @@ class AssetCapitalization(StockController): ) ) - def update_target_asset(self): + def create_target_asset(self): total_target_asset_value = flt(self.total_value, self.precision("total_value")) - if self.docstatus == 1 and self.entry_type == "Capitalization": - asset_doc = frappe.get_doc("Asset", self.target_asset) - asset_doc.purchase_date = self.posting_date - asset_doc.gross_purchase_amount = total_target_asset_value - asset_doc.purchase_receipt_amount = total_target_asset_value - notes = _( - "This schedule was created when target Asset {0} was updated through Asset Capitalization {1}." - ).format( - get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name) - ) - make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes) - asset_doc.flags.ignore_validate_update_after_submit = True - asset_doc.save() - elif self.docstatus == 2: - for item in self.asset_items: - asset = self.get_asset(item) - asset.db_set("disposal_date", None) - self.set_consumed_asset_status(asset) + asset_doc = frappe.new_doc("Asset") + asset_doc.company = self.company + asset_doc.item_code = self.target_item_code + asset_doc.is_existing_asset = 1 + asset_doc.location = self.target_asset_location + asset_doc.available_for_use_date = self.posting_date + asset_doc.purchase_date = self.posting_date + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.flags.ignore_validate = True + asset_doc.insert() - if asset.calculate_depreciation: - reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) - notes = _( - "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." - ).format( - get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) - ) - reset_depreciation_schedule(asset, self.posting_date, notes) + self.target_asset = asset_doc.name - def get_asset(self, item): - asset = frappe.get_doc("Asset", item.asset) - self.check_finance_books(item, asset) - return asset + self.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=self.target_item_code, company=asset_doc.company + ) + + frappe.msgprint( + _( + "Asset {0} has been created. Please set the depreciation details if any and submit it." + ).format(get_link_to_form("Asset", asset_doc.name)) + ) + + def restore_consumed_asset_items(self): + for item in self.asset_items: + asset = frappe.get_doc("Asset", item.asset) + asset.db_set("disposal_date", None) + self.set_consumed_asset_status(asset) + + if asset.calculate_depreciation: + reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + notes = _( + "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) + ) + reset_depreciation_schedule(asset, self.posting_date, notes) def set_consumed_asset_status(self, asset): if self.docstatus == 1: @@ -602,33 +588,6 @@ def get_target_item_details(item_code=None, company=None): return out -@frappe.whitelist() -def get_target_asset_details(asset=None, company=None): - out = frappe._dict() - - # Get Asset Details - asset_details = frappe._dict() - if asset: - asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) - if not asset_details: - frappe.throw(_("Asset {0} does not exist").format(asset)) - - # Re-set item code from Asset - out.target_item_code = asset_details.item_code - - # Set Asset Details - out.asset_name = asset_details.asset_name - - if asset_details.item_code: - out.target_fixed_asset_account = get_asset_category_account( - "fixed_asset_account", item=asset_details.item_code, company=company - ) - else: - out.target_fixed_asset_account = None - - return out - - @frappe.whitelist() def get_consumed_stock_item_details(args): if isinstance(args, str): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 5345d0e7f2b..6e0a6856f56 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -47,13 +47,6 @@ class TestAssetCapitalization(unittest.TestCase): total_amount = 103000 - # Create assets - target_asset = create_asset( - asset_name="Asset Capitalization Target Asset", - submit=1, - warehouse="Stores - TCP1", - company=company, - ) consumed_asset = create_asset( asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_value, @@ -65,7 +58,8 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", - target_asset=target_asset.name, + target_item_code="Macbook Pro", + target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, consumed_asset=consumed_asset.name, @@ -94,7 +88,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) # Test Target Asset values - target_asset.reload() + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) self.assertEqual(target_asset.gross_purchase_amount, total_amount) self.assertEqual(target_asset.purchase_receipt_amount, total_amount) @@ -142,13 +136,6 @@ class TestAssetCapitalization(unittest.TestCase): total_amount = 103000 - # Create assets - target_asset = create_asset( - asset_name="Asset Capitalization Target Asset", - submit=1, - warehouse="Stores - _TC", - company=company, - ) consumed_asset = create_asset( asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_value, @@ -160,7 +147,8 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", - target_asset=target_asset.name, + target_item_code="Macbook Pro", + target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, consumed_asset=consumed_asset.name, @@ -189,7 +177,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) # Test Target Asset values - target_asset.reload() + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) self.assertEqual(target_asset.gross_purchase_amount, total_amount) self.assertEqual(target_asset.purchase_receipt_amount, total_amount) @@ -364,6 +352,7 @@ def create_asset_capitalization(**args): "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), "target_item_code": target_item_code, "target_asset": target_asset.name, + "target_asset_location": "Test Location", "target_warehouse": target_warehouse, "target_qty": flt(args.target_qty) or 1, "target_batch_no": args.target_batch_no, diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 83031415ec3..641d35fa04c 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -42,7 +42,6 @@ class AssetMaintenance(Document): maintenance_log.db_set("maintenance_status", "Cancelled") -@frappe.whitelist() def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, next_due_date): team_member = frappe.db.get_value("User", assign_to_member, "email") args = { diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 2df7db97446..f9c600731b3 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -70,19 +70,21 @@ frappe.ui.form.on('Asset Movement', { else if (frm.doc.purpose === 'Issue') { fieldnames_to_be_altered = { target_location: { read_only: 1, reqd: 0 }, - source_location: { read_only: 1, reqd: 1 }, + source_location: { read_only: 1, reqd: 0 }, from_employee: { read_only: 1, reqd: 0 }, to_employee: { read_only: 0, reqd: 1 } }; } - Object.keys(fieldnames_to_be_altered).forEach(fieldname => { - let property_to_be_altered = fieldnames_to_be_altered[fieldname]; - Object.keys(property_to_be_altered).forEach(property => { - let value = property_to_be_altered[property]; - frm.set_df_property(fieldname, property, value, cdn, 'assets'); + if (fieldnames_to_be_altered) { + Object.keys(fieldnames_to_be_altered).forEach(fieldname => { + let property_to_be_altered = fieldnames_to_be_altered[fieldname]; + Object.keys(property_to_be_altered).forEach(property => { + let value = property_to_be_altered[property]; + frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value); + }); }); - }); - frm.refresh_field('assets'); + frm.refresh_field('assets'); + } } }); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index bdce639b039..5382f9e75f2 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -37,6 +37,7 @@ "reqd": 1 }, { + "default": "Now", "fieldname": "transaction_date", "fieldtype": "Datetime", "in_list_view": 1, @@ -95,10 +96,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-01-22 12:30:55.295670", + "modified": "2023-06-28 16:54:26.571083", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -148,5 +150,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 143f215db2e..b58ca10482b 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -28,25 +28,20 @@ class AssetMovement(Document): def validate_location(self): for d in self.assets: if self.purpose in ["Transfer", "Issue"]: - if not d.source_location: - d.source_location = frappe.db.get_value("Asset", d.asset, "location") - - if not d.source_location: - frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset)) - + current_location = frappe.db.get_value("Asset", d.asset, "location") if d.source_location: - current_location = frappe.db.get_value("Asset", d.asset, "location") - if current_location != d.source_location: frappe.throw( _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location) ) + else: + d.source_location = current_location if self.purpose == "Issue": if d.target_location: frappe.throw( _( - "Issuing cannot be done to a location. Please enter employee who has issued Asset {0}" + "Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to" ).format(d.asset), title=_("Incorrect Movement Purpose"), ) @@ -107,12 +102,12 @@ class AssetMovement(Document): ) def on_submit(self): - self.set_latest_location_in_asset() + self.set_latest_location_and_custodian_in_asset() def on_cancel(self): - self.set_latest_location_in_asset() + self.set_latest_location_and_custodian_in_asset() - def set_latest_location_in_asset(self): + def set_latest_location_and_custodian_in_asset(self): current_location, current_employee = "", "" cond = "1=1" diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 72c05752c57..27e7e557f19 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -47,7 +47,7 @@ class TestAssetMovement(unittest.TestCase): if not frappe.db.exists("Location", "Test Location 2"): frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement1 = create_asset_movement( + create_asset_movement( purpose="Transfer", company=asset.company, assets=[ @@ -58,7 +58,7 @@ class TestAssetMovement(unittest.TestCase): ) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") - create_asset_movement( + movement1 = create_asset_movement( purpose="Transfer", company=asset.company, assets=[ @@ -70,21 +70,32 @@ class TestAssetMovement(unittest.TestCase): self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") movement1.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") employee = make_employee("testassetmovemp@example.com", company="_Test Company") create_asset_movement( purpose="Issue", company=asset.company, - assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}], + assets=[{"asset": asset.name, "source_location": "Test Location 2", "to_employee": employee}], reference_doctype="Purchase Receipt", reference_name=pr.name, ) - # after issuing asset should belong to an employee not at a location + # after issuing, asset should belong to an employee not at a location self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) + create_asset_movement( + purpose="Receipt", + company=asset.company, + assets=[{"asset": asset.name, "from_employee": employee, "target_location": "Test Location"}], + reference_doctype="Purchase Receipt", + reference_name=pr.name, + ) + + # after receiving, asset should belong to a location not at an employee + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + def test_last_movement_cancellation(self): pr = make_purchase_receipt( item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index f810819b4fc..6911f94bbbb 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -115,7 +115,11 @@ def get_data(filters): depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book) for asset in assets_record: - if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb: + if ( + assets_linked_to_fb + and asset.calculate_depreciation + and asset.asset_id not in assets_linked_to_fb + ): continue asset_value = get_asset_value_after_depreciation( diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index c6c9f1f98a3..8fa8f305549 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -286,7 +286,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e source_name: this.frm.doc.supplier, target: this.frm, setters: { - company: me.frm.doc.company + company: this.frm.doc.company }, get_query_filters: { docstatus: ["!=", 2], diff --git a/erpnext/buying/doctype/supplier/patches/__init__.py b/erpnext/buying/doctype/supplier/patches/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py b/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py new file mode 100644 index 00000000000..5834952d5b6 --- /dev/null +++ b/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py @@ -0,0 +1,56 @@ +import os + +import frappe + +in_ci = os.environ.get("CI") + + +def execute(): + try: + contacts = get_portal_user_contacts() + add_portal_users(contacts) + except Exception: + frappe.db.rollback() + frappe.log_error("Failed to migrate portal users") + + if in_ci: # TODO: better way to handle this. + raise + + +def get_portal_user_contacts(): + contact = frappe.qb.DocType("Contact") + dynamic_link = frappe.qb.DocType("Dynamic Link") + + return ( + frappe.qb.from_(contact) + .inner_join(dynamic_link) + .on(contact.name == dynamic_link.parent) + .select( + (dynamic_link.link_doctype).as_("doctype"), + (dynamic_link.link_name).as_("parent"), + (contact.email_id).as_("portal_user"), + ) + .where( + (dynamic_link.parenttype == "Contact") + & (dynamic_link.link_doctype.isin(["Supplier", "Customer"])) + ) + ).run(as_dict=True) + + +def add_portal_users(contacts): + for contact in contacts: + user = frappe.db.get_value("User", {"email": contact.portal_user}, "name") + if not user: + continue + + roles = frappe.get_roles(user) + required_role = contact.doctype + if required_role not in roles: + continue + + portal_user_doc = frappe.new_doc("Portal User") + portal_user_doc.parenttype = contact.doctype + portal_user_doc.parentfield = "portal_users" + portal_user_doc.parent = contact.parent + portal_user_doc.user = user + portal_user_doc.insert() diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index c9ac2797229..5b95d0fde37 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -55,6 +55,14 @@ frappe.ui.form.on("Supplier", { } }; }); + + frm.set_query("user", "portal_users", function(doc) { + return { + filters: { + "ignore_user_type": true, + } + }; + }); }, refresh: function (frm) { diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 4f69aa7a282..a07af7124e5 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -69,7 +69,10 @@ "on_hold", "hold_type", "column_break_59", - "release_date" + "release_date", + "portal_users_tab", + "portal_users", + "column_break_1mqv" ], "fields": [ { @@ -451,6 +454,21 @@ "fieldname": "default_accounts_section", "fieldtype": "Section Break", "label": "Default Accounts" + }, + { + "fieldname": "portal_users_tab", + "fieldtype": "Tab Break", + "label": "Portal Users" + }, + { + "fieldname": "portal_users", + "fieldtype": "Table", + "label": "Supplier Portal Users", + "options": "Portal User" + }, + { + "fieldname": "column_break_1mqv", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", @@ -463,7 +481,7 @@ "link_fieldname": "party" } ], - "modified": "2023-05-09 15:34:13.408932", + "modified": "2023-06-26 14:20:00.961554", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 01b5c8f064f..31bf439dbb4 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -16,6 +16,7 @@ from erpnext.accounts.party import ( # noqa get_timeline_data, validate_party_accounts, ) +from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -46,12 +47,35 @@ class Supplier(TransactionBase): self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def on_update(self): - if not self.naming_series: - self.naming_series = "" - self.create_primary_contact() self.create_primary_address() + def add_role_for_user(self): + for portal_user in self.portal_users: + add_role_for_portal_user(portal_user, "Supplier") + + def _add_supplier_role(self, portal_user): + if not portal_user.is_new(): + return + + user_doc = frappe.get_doc("User", portal_user.user) + roles = {r.role for r in user_doc.roles} + + if "Supplier" in roles: + return + + if "System Manager" not in frappe.get_roles(): + frappe.msgprint( + _("Please add 'Supplier' role to user {0}.").format(portal_user.user), + alert=True, + ) + return + + user_doc.add_roles("Supplier") + frappe.msgprint( + _("Added Supplier Role to User {0}.").format(frappe.bold(user_doc.name)), alert=True + ) + def validate(self): self.flags.is_new_doc = self.is_new() @@ -62,6 +86,7 @@ class Supplier(TransactionBase): validate_party_accounts(self) self.validate_internal_supplier() + self.add_role_for_user() @frappe.whitelist() def get_supplier_group_details(self): diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 7a205ac20ce..7be1d834a65 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -7,6 +7,7 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ from frappe.test_runner import make_test_records from erpnext.accounts.party import get_due_date +from erpnext.controllers.website_list_for_contact import get_customers_suppliers from erpnext.exceptions import PartyDisabled test_dependencies = ["Payment Term", "Payment Terms Template"] @@ -195,6 +196,9 @@ class TestSupplier(FrappeTestCase): def create_supplier(**args): args = frappe._dict(args) + if not args.supplier_name: + args.supplier_name = frappe.generate_hash() + if frappe.db.exists("Supplier", args.supplier_name): return frappe.get_doc("Supplier", args.supplier_name) @@ -209,3 +213,25 @@ def create_supplier(**args): ).insert() return doc + + +class TestSupplierPortal(FrappeTestCase): + def test_portal_user_can_access_supplier_data(self): + + supplier = create_supplier() + + user = frappe.generate_hash() + "@example.com" + frappe.new_doc( + "User", + first_name="Supplier Portal User", + email=user, + send_welcome_email=False, + ).insert() + + supplier.append("portal_users", {"user": user}) + supplier.save() + + frappe.set_user(user) + _, suppliers = get_customers_suppliers("Purchase Order", user) + + self.assertIn(supplier.name, suppliers) diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index a8b76db0931..1967df2a26b 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -99,7 +99,6 @@ def import_string_path(path): return mod -@frappe.whitelist() def make_supplier_scorecard(source_name, target_doc=None): def update_criteria_fields(obj, target, source_parent): target.max_score, target.formula = frappe.db.get_value( diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index c951154a9e0..d86607d8dbc 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -10,6 +10,7 @@ def set_print_templates_for_item_table(doc, settings): doc.child_print_templates = { "items": { "qty": "templates/print_formats/includes/item_table_qty.html", + "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", } } diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 818c7894b7d..954668055e1 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -320,7 +320,9 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): return data[0] -def make_return_doc(doctype: str, source_name: str, target_doc=None): +def make_return_doc( + doctype: str, source_name: str, target_doc=None, return_against_rejected_qty=False +): from frappe.model.mapper import get_mapped_doc company = frappe.db.get_value("Delivery Note", source_name, "company") @@ -471,7 +473,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) - if hasattr(target_doc, "stock_qty"): + if hasattr(target_doc, "stock_qty") and not return_against_rejected_qty: target_doc.stock_qty = -1 * flt( source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0) ) @@ -490,6 +492,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.purchase_receipt_item = source_doc.name + if doctype == "Purchase Receipt" and return_against_rejected_qty: + target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0)) + target_doc.rejected_qty = 0.0 + target_doc.rejected_warehouse = "" + target_doc.warehouse = source_doc.rejected_warehouse + target_doc.received_qty = target_doc.qty + elif doctype == "Purchase Invoice": returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index cdbf6c7cdba..5137e030582 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -845,6 +845,149 @@ class StockController(AccountsController): gl_entries.append(self.get_gl_dict(gl_entry, item=item)) +@frappe.whitelist() +def show_accounting_ledger_preview(company, doctype, docname): + filters = {"company": company, "include_dimensions": 1} + doc = frappe.get_doc(doctype, docname) + + gl_columns, gl_data = get_accounting_ledger_preview(doc, filters) + + frappe.db.rollback() + + return {"gl_columns": gl_columns, "gl_data": gl_data} + + +@frappe.whitelist() +def show_stock_ledger_preview(company, doctype, docname): + filters = {"company": company} + doc = frappe.get_doc(doctype, docname) + + sl_columns, sl_data = get_stock_ledger_preview(doc, filters) + + frappe.db.rollback() + + return { + "sl_columns": sl_columns, + "sl_data": sl_data, + } + + +def get_accounting_ledger_preview(doc, filters): + from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + + gl_columns, gl_data = [], [] + fields = [ + "posting_date", + "account", + "debit", + "credit", + "against", + "party", + "party_type", + "cost_center", + "against_voucher_type", + "against_voucher", + ] + + doc.docstatus = 1 + + if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"): + doc.update_stock_ledger() + + doc.make_gl_entries() + columns = get_gl_columns(filters) + gl_entries = get_gl_entries_for_preview(doc.doctype, doc.name, fields) + + gl_columns = get_columns(columns, fields) + gl_data = get_data(fields, gl_entries) + + return gl_columns, gl_data + + +def get_stock_ledger_preview(doc, filters): + from erpnext.stock.report.stock_ledger.stock_ledger import get_columns as get_sl_columns + + sl_columns, sl_data = [], [] + fields = [ + "item_code", + "stock_uom", + "actual_qty", + "qty_after_transaction", + "warehouse", + "incoming_rate", + "valuation_rate", + "stock_value", + "stock_value_difference", + ] + columns_fields = [ + "item_code", + "stock_uom", + "in_qty", + "out_qty", + "qty_after_transaction", + "warehouse", + "incoming_rate", + "in_out_rate", + "stock_value", + "stock_value_difference", + ] + + if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"): + doc.docstatus = 1 + doc.update_stock_ledger() + columns = get_sl_columns(filters) + sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields) + + sl_columns = get_columns(columns, columns_fields) + sl_data = get_data(columns_fields, sl_entries) + + return sl_columns, sl_data + + +def get_sl_entries_for_preview(doctype, docname, fields): + sl_entries = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields + ) + + for entry in sl_entries: + if entry.actual_qty > 0: + entry["in_qty"] = entry.actual_qty + entry["out_qty"] = 0 + else: + entry["out_qty"] = abs(entry.actual_qty) + entry["in_qty"] = 0 + + entry["in_out_rate"] = entry["valuation_rate"] + + return sl_entries + + +def get_gl_entries_for_preview(doctype, docname, fields): + return frappe.get_all( + "GL Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields + ) + + +def get_columns(raw_columns, fields): + return [ + {"name": d.get("label"), "editable": False, "width": 110} + for d in raw_columns + if not d.get("hidden") and d.get("fieldname") in fields + ] + + +def get_data(raw_columns, raw_data): + datatable_data = [] + for row in raw_data: + data_row = [] + for column in raw_columns: + data_row.append(row.get(column) or "") + + datatable_data.append(data_row) + + return datatable_data + + def repost_required_for_queue(doc: StockController) -> bool: """check if stock document contains repeated item-warehouse with queue based valuation. diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 7c3c38706dc..642722ae6bf 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -232,22 +232,8 @@ def get_customers_suppliers(doctype, user): has_supplier_field = meta.has_field("supplier") if has_common(["Supplier", "Customer"], frappe.get_roles(user)): - contacts = frappe.db.sql( - """ - select - `tabContact`.email_id, - `tabDynamic Link`.link_doctype, - `tabDynamic Link`.link_name - from - `tabContact`, `tabDynamic Link` - where - `tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s - """, - user, - as_dict=1, - ) - customers = [c.link_name for c in contacts if c.link_doctype == "Customer"] - suppliers = [c.link_name for c in contacts if c.link_doctype == "Supplier"] + suppliers = get_parents_for_user("Supplier") + customers = get_parents_for_user("Customer") elif frappe.has_permission(doctype, "read", user=user): customer_list = frappe.get_list("Customer") customers = suppliers = [customer.name for customer in customer_list] @@ -255,6 +241,17 @@ def get_customers_suppliers(doctype, user): return customers if has_customer_field else None, suppliers if has_supplier_field else None +def get_parents_for_user(parenttype: str) -> list[str]: + portal_user = frappe.qb.DocType("Portal User") + + return ( + frappe.qb.from_(portal_user) + .select(portal_user.parent) + .where(portal_user.user == frappe.session.user) + .where(portal_user.parenttype == parenttype) + ).run(pluck="name") + + def has_website_permission(doc, ptype, user, verbose=False): doctype = doc.doctype customers, suppliers = get_customers_suppliers(doctype, user) @@ -282,3 +279,28 @@ def get_customer_field_name(doctype): return "party_name" else: return "customer" + + +def add_role_for_portal_user(portal_user, role): + """When a new portal user is added, give appropriate roles to user if + posssible, else warn user to add roles.""" + if not portal_user.is_new(): + return + + user_doc = frappe.get_doc("User", portal_user.user) + roles = {r.role for r in user_doc.roles} + + if role in roles: + return + + if "System Manager" not in frappe.get_roles(): + frappe.msgprint( + _("Please add {1} role to user {0}.").format(portal_user.user, role), + alert=True, + ) + return + + user_doc.add_roles(role) + frappe.msgprint( + _("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True + ) diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 55db29a627a..3654d29bdc0 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -74,7 +74,7 @@ class SocialMediaPost(Document): def process_scheduled_social_media_posts(): - posts = frappe.get_list( + posts = frappe.get_all( "Social Media Post", filters={"post_status": "Scheduled", "docstatus": 1}, fields=["name", "scheduled_time"], diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py index 1a3e7379281..4466c457436 100644 --- a/erpnext/e_commerce/variant_selector/utils.py +++ b/erpnext/e_commerce/variant_selector/utils.py @@ -162,6 +162,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): product_info = get_item_variant_price_dict(exact_match[0], cart_settings) if product_info: + product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item") product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock) else: product_info = None diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e57a30a88e1..61d2acefae4 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -161,7 +161,6 @@ def add_account_subtype(account_subtype): frappe.throw(frappe.get_traceback()) -@frappe.whitelist() def sync_transactions(bank, bank_account): """Sync transactions based on the last integration date as the start date, after sync is completed add the transaction date of the oldest transaction as the last integration date.""" diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 45a59cf7325..48986910b07 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -99,7 +99,7 @@ frappe.ui.form.on('Production Plan', { }, __('Create')); } - if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { + if (frm.doc.mr_items && frm.doc.mr_items.length && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { frm.add_custom_button(__("Material Request"), ()=> { frm.trigger("make_material_request"); }, __('Create')); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0800bdd2af9..6dc1ff6a49f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -515,6 +515,9 @@ class ProductionPlan(Document): self.show_list_created_message("Work Order", wo_list) self.show_list_created_message("Purchase Order", po_list) + if not wo_list: + frappe.msgprint(_("No Work Orders were created")) + def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() @@ -618,6 +621,9 @@ class ProductionPlan(Document): def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + if item.get("qty") <= 0: + return + wo = frappe.new_doc("Work Order") wo.update(item) wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date") diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 75b43ec1c30..fcfba7fca56 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -76,6 +76,13 @@ class TestProductionPlan(FrappeTestCase): "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 ) + pln.make_work_order() + nwork_orders = frappe.get_all( + "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 + ) + + self.assertTrue(len(work_orders), len(nwork_orders)) + self.assertTrue(len(work_orders), len(pln.po_items)) for name in material_requests: diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js index 123a82a3882..a3f0d008772 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js @@ -30,7 +30,7 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = { "fieldname":"based_on_document", "label": __("Based On Document"), "fieldtype": "Select", - "options": ["Sales Order", "Delivery Note", "Quotation"], + "options": ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"], "default": "Sales Order", "reqd": 1 }, diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index d3bce831551..daef7f6cca7 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -99,7 +99,9 @@ class ForecastingReport(ExponentialSmoothingForecast): parent = frappe.qb.DocType(self.doctype) child = frappe.qb.DocType(self.child_doctype) - date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date" + date_field = ( + "posting_date" if self.doctype in ("Delivery Note", "Sales Invoice") else "transaction_date" + ) query = ( frappe.qb.from_(parent) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 18bd10f45f8..fe6346e81e8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,3 +339,5 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapper', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True) execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True) erpnext.patches.v14_0.cleanup_workspaces +erpnext.patches.v14_0.set_report_in_process_SOA +erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users diff --git a/erpnext/patches/v14_0/set_report_in_process_SOA.py b/erpnext/patches/v14_0/set_report_in_process_SOA.py new file mode 100644 index 00000000000..9eb5e3ab9bd --- /dev/null +++ b/erpnext/patches/v14_0/set_report_in_process_SOA.py @@ -0,0 +1,10 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import frappe + + +def execute(): + process_soa = frappe.qb.DocType("Process Statement Of Accounts") + q = frappe.qb.update(process_soa).set(process_soa.report, "General Ledger") + q.run() diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 5c46bf32807..a53adf1a83a 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -6,10 +6,14 @@ def execute(): assets = get_details_of_draft_or_submitted_depreciable_assets() - for asset in assets: - finance_book_rows = get_details_of_asset_finance_books_rows(asset.name) + asset_finance_books_map = get_asset_finance_books_map() - for fb_row in finance_book_rows: + asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() + + for asset in assets: + depreciation_schedules = asset_depreciation_schedules_map[asset.name] + + for fb_row in asset_finance_books_map[asset.name]: asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row) @@ -19,7 +23,11 @@ def execute(): if asset.docstatus == 1: asset_depr_schedule_doc.submit() - update_depreciation_schedules(asset.name, asset_depr_schedule_doc.name, fb_row.idx) + depreciation_schedules_of_fb_row = [ + ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx) + ] + + update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name) def get_details_of_draft_or_submitted_depreciable_assets(): @@ -41,12 +49,33 @@ def get_details_of_draft_or_submitted_depreciable_assets(): return records -def get_details_of_asset_finance_books_rows(asset_name): +def group_records_by_asset_name(records): + grouped_dict = {} + + for item in records: + key = list(item.keys())[0] + value = item[key] + + if value not in grouped_dict: + grouped_dict[value] = [] + + del item["asset_name"] + + grouped_dict[value].append(item) + + return grouped_dict + + +def get_asset_finance_books_map(): afb = frappe.qb.DocType("Asset Finance Book") + asset = frappe.qb.DocType("Asset") records = ( frappe.qb.from_(afb) + .join(asset) + .on(afb.parent == asset.name) .select( + asset.name.as_("asset_name"), afb.finance_book, afb.idx, afb.depreciation_method, @@ -55,23 +84,44 @@ def get_details_of_asset_finance_books_rows(asset_name): afb.rate_of_depreciation, afb.expected_value_after_useful_life, ) - .where(afb.parent == asset_name) + .where(asset.docstatus < 2) + .orderby(afb.idx) ).run(as_dict=True) - return records + asset_finance_books_map = group_records_by_asset_name(records) + + return asset_finance_books_map -def update_depreciation_schedules(asset_name, asset_depr_schedule_name, fb_row_idx): +def get_asset_depreciation_schedules_map(): ds = frappe.qb.DocType("Depreciation Schedule") + asset = frappe.qb.DocType("Asset") - depr_schedules = ( + records = ( frappe.qb.from_(ds) - .select(ds.name) - .where((ds.parent == asset_name) & (ds.finance_book_id == str(fb_row_idx))) + .join(asset) + .on(ds.parent == asset.name) + .select( + asset.name.as_("asset_name"), + ds.name, + ds.finance_book_id, + ) + .where(asset.docstatus < 2) .orderby(ds.idx) ).run(as_dict=True) - for idx, depr_schedule in enumerate(depr_schedules, start=1): + asset_depreciation_schedules_map = group_records_by_asset_name(records) + + return asset_depreciation_schedules_map + + +def update_depreciation_schedules( + depreciation_schedules, + asset_depr_schedule_name, +): + ds = frappe.qb.DocType("Depreciation Schedule") + + for idx, depr_schedule in enumerate(depreciation_schedules, start=1): ( frappe.qb.update(ds) .set(ds.idx, idx) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 2b3392a67bc..f007430ab37 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -364,7 +364,8 @@ "default": "0", "fieldname": "collect_progress", "fieldtype": "Check", - "label": "Collect Progress" + "label": "Collect Progress", + "search_index": 1 }, { "depends_on": "collect_progress", @@ -451,7 +452,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2023-04-17 21:11:11.346986", + "modified": "2023-06-28 18:57:11.603497", "modified_by": "Administrator", "module": "Projects", "name": "Project", diff --git a/erpnext/projects/doctype/project_update/project_update.json b/erpnext/projects/doctype/project_update/project_update.json index 497b2b73285..c548111f2fb 100644 --- a/erpnext/projects/doctype/project_update/project_update.json +++ b/erpnext/projects/doctype/project_update/project_update.json @@ -1,355 +1,106 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2018-01-18 09:44:47.565494", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "naming_series:", + "creation": "2018-01-18 09:44:47.565494", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "project", + "sent", + "column_break_2", + "date", + "time", + "section_break_5", + "users", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Data", - "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": "Series", - "length": 0, - "no_copy": 0, - "options": "PROJ-UPD-.YYYY.-", - "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": "naming_series", + "fieldtype": "Data", + "hidden": 1, + "label": "Series", + "options": "PROJ-UPD-.YYYY.-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "project", - "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": "Project", - "length": 0, - "no_copy": 0, - "options": "Project", - "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": "project", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Project", + "options": "Project", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "sent", - "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": "Sent", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "sent", + "fieldtype": "Check", + "label": "Sent", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "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 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date", - "fieldtype": "Date", - "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": "Date", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "read_only": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "time", - "fieldtype": "Time", - "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": "Time", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "time", + "fieldtype": "Time", + "label": "Time", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "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, - "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": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "users", - "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": "Users", - "length": 0, - "no_copy": 0, - "options": "Project User", - "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": "users", + "fieldtype": "Table", + "label": "Users", + "options": "Project User" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Project Update", - "permlevel": 0, - "print_hide": 1, - "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 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Project Update", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-16 19:31:05.210656", - "modified_by": "Administrator", - "module": "Projects", - "name": "Project Update", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2023-06-28 18:59:50.678917", + "modified_by": "Administrator", + "module": "Projects", + "name": "Project Update", + "naming_rule": "By \"Naming Series\" field", + "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": "Projects User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects User", + "share": 1, + "submit": 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", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index d346357a8f8..720423b0a49 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -66,7 +66,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con } show_general_ledger() { - var me = this; + let me = this; if(this.frm.doc.docstatus > 0) { cur_frm.add_custom_button(__('Accounting Ledger'), function() { frappe.route_options = { diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index cc020fc2f11..4e028e4c313 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -17,6 +17,7 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; +import "./utils/ledger_preview.js" import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; diff --git a/erpnext/public/js/utils/ledger_preview.js b/erpnext/public/js/utils/ledger_preview.js new file mode 100644 index 00000000000..85d4a7d51e9 --- /dev/null +++ b/erpnext/public/js/utils/ledger_preview.js @@ -0,0 +1,78 @@ +frappe.provide('erpnext.accounts'); + +erpnext.accounts.ledger_preview = { + show_accounting_ledger_preview(frm) { + let me = this; + if(!frm.is_new() && frm.doc.docstatus == 0) { + frm.add_custom_button(__('Accounting Ledger'), function() { + frappe.call({ + "type": "GET", + "method": "erpnext.controllers.stock_controller.show_accounting_ledger_preview", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + "callback": function(response) { + me.make_dialog("Accounting Ledger Preview", "accounting_ledger_preview_html", response.message.gl_columns, response.message.gl_data); + } + }) + }, __("Preview")); + } + }, + + show_stock_ledger_preview(frm) { + let me = this + if(!frm.is_new() && frm.doc.docstatus == 0) { + frm.add_custom_button(__('Stock Ledger'), function() { + frappe.call({ + "type": "GET", + "method": "erpnext.controllers.stock_controller.show_stock_ledger_preview", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + "callback": function(response) { + me.make_dialog("Stock Ledger Preview", "stock_ledger_preview_html", response.message.sl_columns, response.message.sl_data); + } + }) + }, __("Preview")); + } + }, + + make_dialog(label, fieldname, columns, data) { + let me = this; + let dialog = new frappe.ui.Dialog({ + "size": "extra-large", + "title": __(label), + "fields": [ + { + "fieldtype": "HTML", + "fieldname": fieldname, + }, + ] + }); + + setTimeout(function() { + me.get_datatable(columns, data, dialog.get_field(fieldname).wrapper); + }, 200); + + dialog.show(); + }, + + get_datatable(columns, data, wrapper) { + const datatable_options = { + columns: columns, + data: data, + dynamicRowHeight: true, + checkboxColumn: false, + inlineFilters: true, + }; + + new frappe.DataTable( + wrapper, + datatable_options + ); + } +} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index c3bd753f1b3..540e767d323 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -76,6 +76,14 @@ frappe.ui.form.on("Customer", { } } }); + + frm.set_query("user", "portal_users", function() { + return { + filters: { + "ignore_user_type": true, + } + }; + }); }, customer_primary_address: function(frm){ if(frm.doc.customer_primary_address){ diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 7002a36d271..be8f62f7156 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -81,7 +81,9 @@ "dn_required", "column_break_53", "is_frozen", - "disabled" + "disabled", + "portal_users_tab", + "portal_users" ], "fields": [ { @@ -555,6 +557,17 @@ { "fieldname": "column_break_54", "fieldtype": "Column Break" + }, + { + "fieldname": "portal_users_tab", + "fieldtype": "Tab Break", + "label": "Portal Users" + }, + { + "fieldname": "portal_users", + "fieldtype": "Table", + "label": "Customer Portal Users", + "options": "Portal User" } ], "icon": "fa fa-user", @@ -568,7 +581,7 @@ "link_fieldname": "party" } ], - "modified": "2023-05-09 15:38:40.255193", + "modified": "2023-06-22 13:21:10.678382", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 6367e3cb6a5..555db59b082 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -22,6 +22,7 @@ from erpnext.accounts.party import ( # noqa get_timeline_data, validate_party_accounts, ) +from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -82,6 +83,7 @@ class Customer(TransactionBase): self.check_customer_group_change() self.validate_default_bank_account() self.validate_internal_customer() + self.add_role_for_user() # set loyalty program tier if frappe.db.exists("Customer", self.name): @@ -170,6 +172,10 @@ class Customer(TransactionBase): self.update_customer_groups() + def add_role_for_user(self): + for portal_user in self.portal_users: + add_role_for_portal_user(portal_user, "Customer") + def update_customer_groups(self): ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"] if frappe.flags.customer_group_changed: diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 77545e0e1ad..a648195933b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -200,6 +200,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn } } + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); + if (doc.docstatus > 0) { this.show_stock_ledger(); if (erpnext.is_perpetual_inventory_enabled(doc.company)) { diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index 11b71c20761..9d8fe46e8ca 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -66,8 +66,7 @@ "fieldname": "driver", "fieldtype": "Link", "label": "Driver", - "options": "Driver", - "reqd": 1 + "options": "Driver" }, { "fetch_from": "driver.full_name", @@ -189,10 +188,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-04-30 21:21:36.610142", + "modified": "2023-06-27 11:22:27.927637", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -228,5 +228,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "driver_name" } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 1febbded52b..af2f4113e1e 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -24,6 +24,9 @@ class DeliveryTrip(Document): ) def validate(self): + if self._action == "submit" and not self.driver: + frappe.throw(_("A driver must be set to submit.")) + self.validate_stop_addresses() def on_submit(self): diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 6f1f981e2b9..31a3ecbc47e 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -590,7 +590,7 @@ $.extend(erpnext.item, { let selected_attributes = {}; me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { if(i===0) return; - let attribute_name = $(col).find('.control-label').html().trim(); + let attribute_name = $(col).find('.column-label').html().trim(); selected_attributes[attribute_name] = []; let checked_opts = $(col).find('.checkbox input'); checked_opts.each((i, opt) => { diff --git a/erpnext/stock/doctype/item_reorder/item_reorder.json b/erpnext/stock/doctype/item_reorder/item_reorder.json index fb4c558cfd7..a03bd458d4f 100644 --- a/erpnext/stock/doctype/item_reorder/item_reorder.json +++ b/erpnext/stock/doctype/item_reorder/item_reorder.json @@ -81,7 +81,7 @@ "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -147,7 +147,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-28 19:15:38.270046", + "modified": "2023-06-21 15:13:38.270046", "modified_by": "Administrator", "module": "Stock", "name": "Item Reorder", @@ -158,4 +158,4 @@ "read_only_onload": 0, "sort_order": "ASC", "track_seen": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 312c166f8b7..35aad78c1a3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -121,6 +121,10 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend refresh() { var me = this; super.refresh(); + + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm); + erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); + if(this.frm.doc.docstatus > 0) { this.show_stock_ledger(); //removed for temporary @@ -209,10 +213,43 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend } make_purchase_return() { - frappe.model.open_mapped_doc({ - method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return", - frm: cur_frm + let me = this; + + let has_rejected_items = cur_frm.doc.items.filter((item) => { + if (item.rejected_qty > 0) { + return true; + } }) + + if (has_rejected_items && has_rejected_items.length > 0) { + frappe.prompt([ + { + label: __("Return Qty from Rejected Warehouse"), + fieldtype: "Check", + fieldname: "return_for_rejected_warehouse", + default: 1 + }, + ], function(values){ + if (values.return_for_rejected_warehouse) { + frappe.call({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return_against_rejected_warehouse", + args: { + source_name: cur_frm.doc.name + }, + callback: function(r) { + if(r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }) + } else { + cur_frm.cscript._make_purchase_return(); + } + }, __("Return Qty"), __("Make Return Entry")); + } else { + cur_frm.cscript._make_purchase_return(); + } } close_purchase_receipt() { @@ -322,6 +359,13 @@ frappe.ui.form.on('Purchase Receipt Item', { }, }); +cur_frm.cscript._make_purchase_return = function() { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return", + frm: cur_frm + }); +} + cur_frm.cscript['Make Stock Entry'] = function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_stock_entry", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 387f0313804..0b5dc05c3ab 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1136,6 +1136,13 @@ def get_returned_qty_map(purchase_receipt): return returned_qty_map +@frappe.whitelist() +def make_purchase_return_against_rejected_warehouse(source_name): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Purchase Receipt", source_name, return_against_rejected_qty=True) + + @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ddc055656f2..c6c84cadc81 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -72,6 +72,11 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) def test_make_purchase_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term + + create_payment_term("_Test Payment Term 1 for Purchase Invoice") + create_payment_term("_Test Payment Term 2 for Purchase Invoice") + if not frappe.db.exists( "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ): @@ -83,12 +88,14 @@ class TestPurchaseReceipt(FrappeTestCase): "terms": [ { "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 1 for Purchase Invoice", "invoice_portion": 50.00, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 00, }, { "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 2 for Purchase Invoice", "invoice_portion": 50.00, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 30, @@ -1827,6 +1834,33 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(abs(data["stock_value_difference"]), 400.00) + def test_return_from_rejected_warehouse(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return_against_rejected_warehouse, + ) + + item_code = "_Test Item Return from Rejected Warehouse" + create_item(item_code) + + warehouse = create_warehouse("_Test Warehouse Return Qty Warehouse") + rejected_warehouse = create_warehouse("_Test Rejected Warehouse Return Qty Warehouse") + + # Step 1: Create Purchase Receipt with valuation rate 100 + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=100, + rejected_qty=2, + rejected_warehouse=rejected_warehouse, + ) + + pr_return = make_purchase_return_against_rejected_warehouse(pr.name) + self.assertEqual(pr_return.items[0].warehouse, rejected_warehouse) + self.assertEqual(pr_return.items[0].qty, 2.0 * -1) + self.assertEqual(pr_return.items[0].rejected_qty, 0.0) + self.assertEqual(pr_return.items[0].rejected_warehouse, "") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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 d5fc710625a..27066b825c1 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -13,6 +13,7 @@ from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException import erpnext +from erpnext.accounts.general_ledger import validate_accounting_period from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers from erpnext.stock.stock_ledger import ( get_affected_transactions, @@ -44,11 +45,49 @@ class RepostItemValuation(Document): self.validate_accounts_freeze() def validate_period_closing_voucher(self): + # Period Closing Voucher year_end_date = self.get_max_year_end_date(self.company) if year_end_date and getdate(self.posting_date) <= getdate(year_end_date): - msg = f"Due to period closing, you cannot repost item valuation before {year_end_date}" + date = frappe.format(year_end_date, "Date") + msg = f"Due to period closing, you cannot repost item valuation before {date}" frappe.throw(_(msg)) + # Accounting Period + if self.voucher_type: + validate_accounting_period( + [ + frappe._dict( + { + "posting_date": self.posting_date, + "company": self.company, + "voucher_type": self.voucher_type, + } + ) + ] + ) + + # Closing Stock Balance + closing_stock = self.get_closing_stock_balance() + if closing_stock and closing_stock[0].name: + name = get_link_to_form("Closing Stock Balance", closing_stock[0].name) + to_date = frappe.format(closing_stock[0].to_date, "Date") + msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}" + frappe.throw(_(msg)) + + def get_closing_stock_balance(self): + filters = { + "company": self.company, + "status": "Completed", + "docstatus": 1, + "to_date": (">=", self.posting_date), + } + + for field in ["warehouse", "item_code"]: + if self.get(field): + filters.update({field: ("in", ["", self.get(field)])}) + + return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters) + @staticmethod def get_max_year_end_date(company): data = frappe.get_all( diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 9c4d997b316..1853f45f583 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -392,3 +392,33 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): pr.cancel() self.assertTrue(pr.docstatus == 2) self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name})) + + def test_repost_item_valuation_for_closing_stock_balance(self): + from erpnext.stock.doctype.closing_stock_balance.closing_stock_balance import ( + prepare_closing_stock_balance, + ) + + doc = frappe.new_doc("Closing Stock Balance") + doc.company = "_Test Company" + doc.from_date = today() + doc.to_date = today() + doc.submit() + + prepare_closing_stock_balance(doc.name) + doc.load_from_db() + self.assertEqual(doc.docstatus, 1) + self.assertEqual(doc.status, "Completed") + + riv = frappe.new_doc("Repost Item Valuation") + riv.update( + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "based_on": "Item and Warehouse", + "posting_date": today(), + "posting_time": "00:01:00", + } + ) + + self.assertRaises(frappe.ValidationError, riv.save) + doc.cancel() diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 87a23efc590..746a1cbaf17 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Warehouse", { }; }); - frm.set_query("parent_warehouse", function () { + frm.set_query("parent_warehouse", function (doc) { return { filters: { is_group: 1, diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 64650bc2018..4f85ac054d0 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -191,7 +191,6 @@ def process_string_args(args): return args -@frappe.whitelist() def get_item_code(barcode=None, serial_no=None): if barcode: item_code = frappe.db.get_value("Item Barcode", {"barcode": barcode}, fieldname=["parent"]) diff --git a/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json b/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json index 21132e070c5..a8ab8f6ac7d 100644 --- a/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json +++ b/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json @@ -8,14 +8,14 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"| Sr | \\n\\t\\t\\tItem Name | \\n\\t\\t\\tDescription | \\n\\t\\t\\tQty | \\n\\t\\t\\tRate | \\n\\t\\t\\tAmount | \\n\\t\\t
|---|---|---|---|---|---|
| {{ row.idx }} | \\n\\t\\t\\t\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t | \\n\\t\\t\\t\\n\\t\\t\\t\\t {{ row.description }} | \\n\\t\\t\\t{{ row.qty }} {{ row.uom or row.stock_uom }} | \\n\\t\\t\\t{{\\n\\t\\t\\t\\trow.get_formatted(\\\"rate\\\", doc) }} | \\n\\t\\t\\t{{\\n\\t\\t\\t\\trow.get_formatted(\\\"amount\\\", doc) }} | \\n\\t\\t\\t\\n\\t\\t
| Sr | \\n\\t\\t\\tItem Name | \\n\\t\\t\\tQty | \\n\\t\\t\\tSerial Nos | \\n\\t\\t\\tBatch Nos (Qty) | \\n\\t\\t
|---|---|---|---|---|
| {{ row.idx }} | \\n\\t\\t\\t\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t | \\n\\t\\t\\t{{ row.qty }} {{ row.uom or row.stock_uom }} | \\n\\t\\t\\t\\n\\t\\t\\t{{ serial_nos|join(',') }} | \\n\\t\\t\\t\\n\\t\\t\\t {% if batches %}\\n {% for batch_no, qty in batches.items() %}\\n {{batch_no}} : {{qty}} {{ row.uom or row.stock_uom }} \\n {% endfor %}\\n {% endif %}\\n\\t\\t\\t | \\n\\t\\t\\t\\n\\t\\t
| Sr | \\n\\t\\t\\tItem Name | \\n\\t\\t\\tDescription | \\n\\t\\t\\tQty | \\n\\t\\t\\tRate | \\n\\t\\t\\tAmount | \\n\\t\\t
|---|---|---|---|---|---|
| {{ row.idx }} | \\n\\t\\t\\t\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t | \\n\\t\\t\\t\\n\\t\\t\\t\\t {{ row.description }} | \\n\\t\\t\\t{{ row.qty }} {{ row.uom or row.stock_uom }} | \\n\\t\\t\\t{{\\n\\t\\t\\t\\trow.get_formatted(\\\"rate\\\", doc) }} | \\n\\t\\t\\t{{\\n\\t\\t\\t\\trow.get_formatted(\\\"amount\\\", doc) }} | \\n\\t\\t\\t\\n\\t\\t
| Sr | \\n\\t\\t\\tItem Name | \\n\\t\\t\\tQty | \\n\\t\\t\\tSerial Nos | \\n\\t\\t\\tBatch Nos (Qty) | \\n\\t\\t
|---|---|---|---|---|
| {{ row.idx }} | \\n\\t\\t\\t\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t | \\n\\t\\t\\t{{ row.qty }} {{ row.uom or row.stock_uom }} | \\n\\t\\t\\t\\n\\t\\t\\t{{ serial_nos|join(',') }} | \\n\\t\\t\\t\\n\\t\\t\\t {% if batches %}\\n {% for batch_no, qty in batches.items() %}\\n {{batch_no}} : {{qty}} {{ row.uom or row.stock_uom }} \\n {% endfor %}\\n {% endif %}\\n\\t\\t\\t | \\n\\t\\t\\t\\n\\t\\t
| {d.batch_no} | {d.serial_no} | {abs(d.qty)} |
|---|---|---|
| {d.batch_no} | {abs(d.qty)} |