diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index a26916d40ea..66c526c4745 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -1,3 +1,4 @@
+import functools
import inspect
import frappe
@@ -120,12 +121,14 @@ def get_region(company=None):
You can also set global company flag in `frappe.flags.company`
"""
- if company or frappe.flags.company:
- return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
- elif frappe.flags.country:
- return frappe.flags.country
- else:
- return frappe.get_system_settings("country")
+
+ if not company:
+ company = frappe.local.flags.company
+
+ if company:
+ return frappe.get_cached_value("Company", company, "country")
+
+ return frappe.flags.country or frappe.get_system_settings("country")
def allow_regional(fn):
@@ -136,6 +139,7 @@ def allow_regional(fn):
def myfunction():
pass"""
+ @functools.wraps(fn)
def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index f9c9173af08..3a360c48c43 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -297,7 +297,7 @@ def _make_test_records(verbose=None):
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
- ["_Test Depreciations", "Expenses", 0, None, None],
+ ["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index eafdb3c7467..d5ed0676fd1 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -19,6 +19,8 @@
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
+ "journals_section",
+ "merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
@@ -38,6 +40,8 @@
"show_payment_schedule_in_print",
"currency_exchange_section",
"allow_stale",
+ "section_break_jpd0",
+ "auto_reconcile_payments",
"stale_days",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
@@ -168,11 +172,6 @@
"fieldtype": "Int",
"label": "Stale Days"
},
- {
- "fieldname": "report_settings_sb",
- "fieldtype": "Section Break",
- "label": "Report Settings"
- },
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
@@ -356,6 +355,29 @@
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
+ },
+ {
+ "fieldname": "journals_section",
+ "fieldtype": "Section Break",
+ "label": "Journals"
+ },
+ {
+ "default": "0",
+ "description": "Rows with Same Account heads will be merged on Ledger",
+ "fieldname": "merge_similar_account_heads",
+ "fieldtype": "Check",
+ "label": "Merge Similar Account Heads"
+ },
+ {
+ "fieldname": "section_break_jpd0",
+ "fieldtype": "Section Break",
+ "label": "Payment Reconciliations"
+ },
+ {
+ "default": "0",
+ "fieldname": "auto_reconcile_payments",
+ "fieldtype": "Check",
+ "label": "Auto Reconcile Payments"
}
],
"icon": "icon-cog",
@@ -363,7 +385,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-04-14 17:22:03.680886",
+ "modified": "2023-04-21 13:11:37.130743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 081718726bd..8ad0bd17b48 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -56,7 +56,7 @@ class BankClearance(Document):
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
- if(paid_from=%(account)s, paid_amount, 0) as credit,
+ if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 15162376c15..d2d961f7c2a 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -281,10 +281,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
)
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value(
- "Journal Entry Account",
- {"parent": payment_entry.payment_entry, "account": gl_bank_account},
- "sum(credit_in_account_currency)",
+ return abs(
+ frappe.db.get_value(
+ "Journal Entry Account",
+ {"parent": payment_entry.payment_entry, "account": gl_bank_account},
+ "sum(debit_in_account_currency-credit_in_account_currency)",
+ )
+ or 0
)
elif payment_entry.payment_document == "Expense Claim":
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 089f20b467d..302acc4f1f7 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"];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 608267154b6..92c37bbe5e5 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -69,6 +69,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
+ self.validate_depr_entry_voucher_type()
if self.docstatus == 0:
self.apply_tax_withholding()
@@ -130,6 +131,13 @@ class JournalEntry(AccountsController):
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
+ def validate_depr_entry_voucher_type(self):
+ if (
+ any(d.account_type == "Depreciation" for d in self.get("accounts"))
+ and self.voucher_type != "Depreciation Entry"
+ ):
+ frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
+
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
@@ -233,25 +241,30 @@ class JournalEntry(AccountsController):
self.remove(d)
def update_asset_value(self):
- if self.voucher_type != "Depreciation Entry":
+ if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
return
- processed_assets = []
-
for d in self.get("accounts"):
if (
- d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
+ d.reference_type == "Asset"
+ and d.reference_name
+ and d.account_type == "Depreciation"
+ and d.debit
):
- processed_assets.append(d.reference_name)
-
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
- continue
-
- depr_value = d.debit or d.credit
-
- asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
+ fb_idx = 1
+ if self.finance_book:
+ for fb_row in asset.get("finance_books"):
+ if fb_row.finance_book == self.finance_book:
+ fb_idx = fb_row.idx
+ break
+ fb_row = asset.get("finance_books")[fb_idx - 1]
+ fb_row.value_after_depreciation -= d.debit
+ fb_row.db_update()
+ else:
+ asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
asset.set_status()
@@ -316,35 +329,35 @@ class JournalEntry(AccountsController):
if self.voucher_type != "Depreciation Entry":
return
- processed_assets = []
-
for d in self.get("accounts"):
if (
- d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
+ d.reference_type == "Asset"
+ and d.reference_name
+ and d.account_type == "Depreciation"
+ and d.debit
):
- processed_assets.append(d.reference_name)
-
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
+ fb_idx = None
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
-
- idx = cint(s.finance_book_id) or 1
- finance_books = asset.get("finance_books")[idx - 1]
- finance_books.value_after_depreciation += s.depreciation_amount
- finance_books.db_update()
-
- asset.set_status()
-
+ fb_idx = cint(s.finance_book_id) or 1
break
+ if not fb_idx:
+ fb_idx = 1
+ if self.finance_book:
+ for fb_row in asset.get("finance_books"):
+ if fb_row.finance_book == self.finance_book:
+ fb_idx = fb_row.idx
+ break
+ fb_row = asset.get("finance_books")[fb_idx - 1]
+ fb_row.value_after_depreciation += d.debit
+ fb_row.db_update()
else:
- depr_value = d.debit or d.credit
-
- asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
-
- asset.set_status()
+ asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
+ asset.set_status()
def unlink_inter_company_jv(self):
if (
@@ -878,6 +891,8 @@ class JournalEntry(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
+ merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
+
gl_map = self.build_gl_map()
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
update_outstanding = "No"
@@ -885,7 +900,13 @@ class JournalEntry(AccountsController):
update_outstanding = "Yes"
if gl_map:
- make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
+ make_gl_entries(
+ gl_map,
+ cancel=cancel,
+ adv_adj=adv_adj,
+ merge_entries=merge_entries,
+ update_outstanding=update_outstanding,
+ )
@frappe.whitelist()
def get_balance(self, difference_account=None):
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 00a948a8387..969027dd97c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -978,6 +978,7 @@ frappe.ui.form.on('Payment Entry', {
precision("difference_amount"));
const add_deductions = (details) => {
+ let row = null;
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = details[account];
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index a4319521e36..e1536fac537 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
def validate(self):
self.setup_party_account_field()
self.set_missing_values()
+ self.set_missing_ref_details()
self.validate_payment_type()
self.validate_party_details()
self.set_exchange_rate()
@@ -219,11 +220,16 @@ class PaymentEntry(AccountsController):
else self.paid_to_account_currency
)
- self.set_missing_ref_details()
-
- def set_missing_ref_details(self, force=False):
+ def set_missing_ref_details(
+ self, force: bool = False, update_ref_details_only_for: list | None = None
+ ) -> None:
for d in self.get("references"):
if d.allocated_amount:
+ if update_ref_details_only_for and (
+ not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
+ ):
+ continue
+
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
@@ -1687,7 +1693,10 @@ def get_payment_entry(
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
- if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
+ over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
+ if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
+ 100.0 + over_billing_allowance
+ ):
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type:
@@ -1811,6 +1820,7 @@ def get_payment_entry(
pe.setup_party_account_field()
pe.set_missing_values()
+ pe.set_missing_ref_details()
update_accounting_dimensions(pe, doc)
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index caffac5354f..08d38dde474 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -82,6 +82,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default');
}
+
+ // 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
+ }
+ }
+ }).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");
+ }
+ });
+ }
+ });
+ }
+
}
company() {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index d8082d058f3..cc2b9420cc2 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -7,9 +7,12 @@ from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
-from frappe.utils import flt, getdate, nowdate, today
+from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
import erpnext
+from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
+ is_any_doc_running,
+)
from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
@@ -304,9 +307,7 @@ class PaymentReconciliation(Document):
}
)
- @frappe.whitelist()
- def reconcile(self):
- self.validate_allocation()
+ def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -330,12 +331,35 @@ class PaymentReconciliation(Document):
self.make_difference_entry(payment_details)
if entry_list:
- reconcile_against_document(entry_list)
+ reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
+ @frappe.whitelist()
+ def reconcile(self):
+ if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
+ running_doc = is_any_doc_running(
+ dict(
+ company=self.company,
+ party_type=self.party_type,
+ party=self.party,
+ receivable_payable_account=self.receivable_payable_account,
+ )
+ )
+
+ if running_doc:
+ frappe.throw(
+ _("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
+ get_link_to_form("Auto Reconcile", running_doc)
+ )
+ )
+ return
+
+ self.validate_allocation()
+ self.reconcile_allocations()
msgprint(_("Successfully Reconciled"))
+
self.get_unreconciled_entries()
def make_difference_entry(self, row):
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
new file mode 100644
index 00000000000..dd601bfc451
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js
@@ -0,0 +1,130 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Process Payment Reconciliation", {
+ onload: function(frm) {
+ // set queries
+ frm.set_query("party_type", function() {
+ return {
+ "filters": {
+ "name": ["in", Object.keys(frappe.boot.party_account_types)],
+ }
+ }
+ });
+ frm.set_query('receivable_payable_account', function(doc) {
+ return {
+ filters: {
+ "company": doc.company,
+ "is_group": 0,
+ "account_type": frappe.boot.party_account_types[doc.party_type]
+ }
+ };
+ });
+ frm.set_query('cost_center', function(doc) {
+ return {
+ filters: {
+ "company": doc.company,
+ "is_group": 0,
+ }
+ };
+ });
+ frm.set_query('bank_cash_account', function(doc) {
+ return {
+ filters:[
+ ['Account', 'company', '=', doc.company],
+ ['Account', 'is_group', '=', 0],
+ ['Account', 'account_type', 'in', ['Bank', 'Cash']]
+ ]
+ };
+ });
+
+ },
+ refresh: function(frm) {
+ if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
+ let execute_btn = __("Start / Resume")
+
+ frm.add_custom_button(execute_btn, () => {
+ frm.call({
+ method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
+ args: {
+ docname: frm.doc.name
+ }
+ }).then(r => {
+ if(!r.exc) {
+ frappe.show_alert(__("Job Started"));
+ frm.reload_doc();
+ }
+ });
+ });
+ }
+ if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
+ frm.call({
+ 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
+ args: {
+ "docname": frm.docname,
+ }
+ }).then(r => {
+ if (r.message) {
+ let progress = 0;
+ let description = "";
+
+ if (r.message.processed) {
+ progress = (r.message.processed/r.message.total) * 100;
+ description = r.message.processed + "/" + r.message.total + " processed";
+ } else if (r.message.total == 0 && frm.doc.status == "Completed") {
+ progress = 100;
+ }
+
+
+ frm.dashboard.add_progress('Reconciliation Progress', progress, description);
+ }
+ })
+ }
+ if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
+ let execute_btn = __("Pause")
+
+ frm.add_custom_button(execute_btn, () => {
+ frm.call({
+ 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
+ args: {
+ "docname": frm.docname,
+ }
+ }).then(r => {
+ if (!r.exc) {
+ frappe.show_alert(__("Job Paused"));
+ frm.reload_doc()
+ }
+ });
+
+ });
+ }
+ },
+ company(frm) {
+ frm.set_value('party', '');
+ frm.set_value('receivable_payable_account', '');
+ },
+ party_type(frm) {
+ frm.set_value('party', '');
+ },
+
+ party(frm) {
+ frm.set_value('receivable_payable_account', '');
+ if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
+ return frappe.call({
+ method: "erpnext.accounts.party.get_party_account",
+ args: {
+ company: frm.doc.company,
+ party_type: frm.doc.party_type,
+ party: frm.doc.party
+ },
+ callback: (r) => {
+ if (!r.exc && r.message) {
+ frm.set_value("receivable_payable_account", r.message);
+ }
+ frm.refresh();
+
+ }
+ });
+ }
+ }
+});
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
new file mode 100644
index 00000000000..8bb7092dc50
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json
@@ -0,0 +1,173 @@
+{
+ "actions": [],
+ "autoname": "format:ACC-PPR-{#####}",
+ "beta": 1,
+ "creation": "2023-03-30 21:28:39.793927",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "party_type",
+ "column_break_io6c",
+ "party",
+ "receivable_payable_account",
+ "filter_section",
+ "from_invoice_date",
+ "to_invoice_date",
+ "column_break_kegk",
+ "from_payment_date",
+ "to_payment_date",
+ "column_break_uj04",
+ "cost_center",
+ "bank_cash_account",
+ "section_break_2n02",
+ "status",
+ "error_log",
+ "section_break_a8yx",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "allow_on_submit": 1,
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "party_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Party Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_io6c",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Party",
+ "options": "party_type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "receivable_payable_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Receivable/Payable Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "filter_section",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "from_invoice_date",
+ "fieldtype": "Date",
+ "label": "From Invoice Date"
+ },
+ {
+ "fieldname": "to_invoice_date",
+ "fieldtype": "Date",
+ "label": "To Invoice Date"
+ },
+ {
+ "fieldname": "column_break_kegk",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "from_payment_date",
+ "fieldtype": "Date",
+ "label": "From Payment Date"
+ },
+ {
+ "fieldname": "to_payment_date",
+ "fieldtype": "Date",
+ "label": "To Payment Date"
+ },
+ {
+ "fieldname": "column_break_uj04",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "bank_cash_account",
+ "fieldtype": "Link",
+ "label": "Bank/Cash Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "section_break_2n02",
+ "fieldtype": "Section Break",
+ "label": "Status"
+ },
+ {
+ "depends_on": "eval:doc.error_log",
+ "fieldname": "error_log",
+ "fieldtype": "Long Text",
+ "label": "Error Log"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Process Payment Reconciliation",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_a8yx",
+ "fieldtype": "Section Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-04-21 17:19:30.912953",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Payment Reconciliation",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "company"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
new file mode 100644
index 00000000000..ecb51ce1445
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -0,0 +1,503 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.utils import get_link_to_form
+from frappe.utils.scheduler import is_scheduler_inactive
+
+
+class ProcessPaymentReconciliation(Document):
+ def validate(self):
+ self.validate_receivable_payable_account()
+ self.validate_bank_cash_account()
+
+ def validate_receivable_payable_account(self):
+ if self.receivable_payable_account:
+ if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
+ frappe.throw(
+ _("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
+ frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
+ )
+ )
+
+ def validate_bank_cash_account(self):
+ if self.bank_cash_account:
+ if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
+ frappe.throw(
+ _("Bank/Cash Account {0} doesn't belong to company {1}").format(
+ frappe.bold(self.bank_cash_account), frappe.bold(self.company)
+ )
+ )
+
+ def before_save(self):
+ self.status = ""
+ self.error_log = ""
+
+ def on_submit(self):
+ self.db_set("status", "Queued")
+ self.db_set("error_log", None)
+
+ def on_cancel(self):
+ self.db_set("status", "Cancelled")
+ log = frappe.db.get_value(
+ "Process Payment Reconciliation Log", filters={"process_pr": self.name}
+ )
+ if log:
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
+
+
+@frappe.whitelist()
+def get_reconciled_count(docname: str | None = None) -> float:
+ current_status = {}
+ if docname:
+ reconcile_log = frappe.db.get_value(
+ "Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
+ )
+ if reconcile_log:
+ res = frappe.get_all(
+ "Process Payment Reconciliation Log",
+ filters={"name": reconcile_log},
+ fields=["reconciled_entries", "total_allocations"],
+ as_list=1,
+ )
+ current_status["processed"], current_status["total"] = res[0]
+
+ return current_status
+
+
+def get_pr_instance(doc: str):
+ process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
+
+ pr = frappe.get_doc("Payment Reconciliation")
+ fields = [
+ "company",
+ "party_type",
+ "party",
+ "receivable_payable_account",
+ "from_invoice_date",
+ "to_invoice_date",
+ "from_payment_date",
+ "to_payment_date",
+ ]
+ d = {}
+ for field in fields:
+ d[field] = process_payment_reconciliation.get(field)
+ pr.update(d)
+ pr.invoice_limit = 1000
+ pr.payment_limit = 1000
+ return pr
+
+
+def is_job_running(job_name: str) -> bool:
+ jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
+ for x in jobs:
+ if x.job_name == job_name:
+ return True
+ return False
+
+
+@frappe.whitelist()
+def pause_job_for_doc(docname: str | None = None):
+ if docname:
+ frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
+ log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
+ if log:
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
+
+
+@frappe.whitelist()
+def trigger_job_for_doc(docname: str | None = None):
+ """
+ Trigger background job
+ """
+ if not docname:
+ return
+
+ if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
+ frappe.throw(
+ _("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
+ get_link_to_form("Accounts Settings", "Accounts Settings")
+ )
+ )
+
+ return
+
+ if not is_scheduler_inactive():
+ if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
+ frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
+ job_name = f"start_processing_{docname}"
+ if not is_job_running(job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
+ queue="long",
+ is_async=True,
+ job_name=job_name,
+ enqueue_after_commit=True,
+ doc=docname,
+ )
+
+ elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
+ frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
+ log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
+ if log:
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
+
+ # Resume tasks for running doc
+ job_name = f"start_processing_{docname}"
+ if not is_job_running(job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
+ queue="long",
+ is_async=True,
+ job_name=job_name,
+ doc=docname,
+ )
+ else:
+ frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
+
+
+def trigger_reconciliation_for_queued_docs():
+ """
+ Will be called from Cron Job
+ Fetch queued docs and start reconciliation process for each one
+ """
+ if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
+ frappe.throw(
+ _("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
+ get_link_to_form("Accounts Settings", "Accounts Settings")
+ )
+ )
+
+ return
+
+ if not is_scheduler_inactive():
+ # Get all queued documents
+ all_queued = frappe.db.get_all(
+ "Process Payment Reconciliation",
+ filters={"docstatus": 1, "status": "Queued"},
+ order_by="creation desc",
+ as_list=1,
+ )
+
+ docs_to_trigger = []
+ unique_filters = set()
+ queue_size = 5
+
+ fields = ["company", "party_type", "party", "receivable_payable_account"]
+
+ def get_filters_as_tuple(fields, doc):
+ filters = ()
+ for x in fields:
+ filters += tuple(doc.get(x))
+ return filters
+
+ for x in all_queued:
+ doc = frappe.get_doc("Process Payment Reconciliation", x)
+ filters = get_filters_as_tuple(fields, doc)
+ if filters not in unique_filters:
+ unique_filters.add(filters)
+ docs_to_trigger.append(doc.name)
+ if len(docs_to_trigger) == queue_size:
+ break
+
+ # trigger reconcilation process for queue_size unique filters
+ for doc in docs_to_trigger:
+ trigger_job_for_doc(doc)
+
+ else:
+ frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
+
+
+def reconcile_based_on_filters(doc: None | str = None) -> None:
+ """
+ Identify current state of document and execute next tasks in background
+ """
+ if doc:
+ log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
+ if not log:
+ log = frappe.new_doc("Process Payment Reconciliation Log")
+ log.process_pr = doc
+ log.status = "Running"
+ log = log.save()
+
+ job_name = f"process_{doc}_fetch_and_allocate"
+ if not is_job_running(job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
+ queue="long",
+ timeout="3600",
+ is_async=True,
+ job_name=job_name,
+ enqueue_after_commit=True,
+ doc=doc,
+ )
+ else:
+ res = frappe.get_all(
+ "Process Payment Reconciliation Log",
+ filters={"name": log},
+ fields=["allocated", "reconciled"],
+ as_list=1,
+ )
+ allocated, reconciled = res[0]
+
+ if not allocated:
+ job_name = f"process__{doc}_fetch_and_allocate"
+ if not is_job_running(job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
+ queue="long",
+ timeout="3600",
+ is_async=True,
+ job_name=job_name,
+ enqueue_after_commit=True,
+ doc=doc,
+ )
+ elif not reconciled:
+ allocation = get_next_allocation(log)
+ if allocation:
+ reconcile_job_name = (
+ f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
+ )
+ else:
+ reconcile_job_name = f"process_{doc}_reconcile"
+ if not is_job_running(reconcile_job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
+ queue="long",
+ timeout="3600",
+ is_async=True,
+ job_name=reconcile_job_name,
+ enqueue_after_commit=True,
+ doc=doc,
+ )
+ elif reconciled:
+ frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+
+
+def get_next_allocation(log: str) -> list:
+ if log:
+ allocations = []
+ next = frappe.db.get_all(
+ "Process Payment Reconciliation Log Allocations",
+ filters={"parent": log, "reconciled": 0},
+ fields=["reference_type", "reference_name"],
+ order_by="idx",
+ limit=1,
+ )
+
+ if next:
+ allocations = frappe.db.get_all(
+ "Process Payment Reconciliation Log Allocations",
+ filters={
+ "parent": log,
+ "reconciled": 0,
+ "reference_type": next[0].reference_type,
+ "reference_name": next[0].reference_name,
+ },
+ fields=["*"],
+ order_by="idx",
+ )
+
+ return allocations
+ return []
+
+
+def fetch_and_allocate(doc: str) -> None:
+ """
+ Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
+ """
+
+ if doc:
+ log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
+ if log:
+ if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
+ reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
+
+ pr = get_pr_instance(doc)
+ pr.get_unreconciled_entries()
+
+ if len(pr.invoices) > 0 and len(pr.payments) > 0:
+ invoices = [x.as_dict() for x in pr.invoices]
+ payments = [x.as_dict() for x in pr.payments]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ for x in pr.get("allocation"):
+ reconcile_log.append(
+ "allocations",
+ x.as_dict().update(
+ {
+ "parenttype": "Process Payment Reconciliation Log",
+ "parent": reconcile_log.name,
+ "name": None,
+ "reconciled": False,
+ }
+ ),
+ )
+ reconcile_log.allocated = True
+ reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
+ reconcile_log.reconciled_entries = 0
+ reconcile_log.save()
+
+ # generate reconcile job name
+ allocation = get_next_allocation(log)
+ if allocation:
+ reconcile_job_name = (
+ f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
+ )
+ else:
+ reconcile_job_name = f"process_{doc}_reconcile"
+
+ if not is_job_running(reconcile_job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
+ queue="long",
+ timeout="3600",
+ is_async=True,
+ job_name=reconcile_job_name,
+ enqueue_after_commit=True,
+ doc=doc,
+ )
+
+
+def reconcile(doc: None | str = None) -> None:
+ if doc:
+ log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
+ if log:
+ res = frappe.get_all(
+ "Process Payment Reconciliation Log",
+ filters={"name": log},
+ fields=["reconciled_entries", "total_allocations"],
+ as_list=1,
+ limit=1,
+ )
+
+ reconciled_entries, total_allocations = res[0]
+ if reconciled_entries != total_allocations:
+ try:
+ # Fetch next allocation
+ allocations = get_next_allocation(log)
+
+ pr = get_pr_instance(doc)
+
+ # pass allocation to PR instance
+ for x in allocations:
+ pr.append("allocation", x)
+
+ # reconcile
+ pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
+
+ # If Payment Entry, update details only for newly linked references
+ # This is for performance
+ if allocations[0].reference_type == "Payment Entry":
+
+ references = [(x.invoice_type, x.invoice_number) for x in allocations]
+ pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
+ pe.flags.ignore_validate_update_after_submit = True
+ pe.set_missing_ref_details(update_ref_details_only_for=references)
+ pe.save()
+
+ # Update reconciled flag
+ allocation_names = [x.name for x in allocations]
+ ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
+ qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
+
+ # Update reconciled count
+ reconciled_count = frappe.db.count(
+ "Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
+ )
+ frappe.db.set_value(
+ "Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
+ )
+
+ except Exception as err:
+ # Update the parent doc about the exception
+ frappe.db.rollback()
+
+ traceback = frappe.get_traceback()
+ if traceback:
+ message = "Traceback:
" + traceback
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
+ frappe.db.set_value(
+ "Process Payment Reconciliation",
+ doc,
+ "error_log",
+ message,
+ )
+ if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
+ frappe.db.set_value(
+ "Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
+ )
+ frappe.db.set_value(
+ "Process Payment Reconciliation",
+ doc,
+ "status",
+ "Partially Reconciled",
+ )
+ else:
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
+ frappe.db.set_value(
+ "Process Payment Reconciliation",
+ doc,
+ "status",
+ "Failed",
+ )
+ finally:
+ if reconciled_entries == total_allocations:
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
+ frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+ else:
+
+ if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
+ # trigger next batch in job
+ # generate reconcile job name
+ allocation = get_next_allocation(log)
+ if allocation:
+ reconcile_job_name = (
+ f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
+ )
+ else:
+ reconcile_job_name = f"process_{doc}_reconcile"
+
+ if not is_job_running(reconcile_job_name):
+ job = frappe.enqueue(
+ method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
+ queue="long",
+ timeout="3600",
+ is_async=True,
+ job_name=reconcile_job_name,
+ enqueue_after_commit=True,
+ doc=doc,
+ )
+ else:
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
+ frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
+ frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
+
+
+@frappe.whitelist()
+def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
+ running_doc = None
+ if for_filter:
+ if type(for_filter) == str:
+ for_filter = frappe.json.loads(for_filter)
+
+ running_doc = frappe.db.get_value(
+ "Process Payment Reconciliation",
+ filters={
+ "docstatus": 1,
+ "status": ["in", ["Running", "Paused"]],
+ "company": for_filter.get("company"),
+ "party_type": for_filter.get("party_type"),
+ "party": for_filter.get("party"),
+ "receivable_payable_account": for_filter.get("receivable_payable_account"),
+ },
+ fieldname="name",
+ )
+ else:
+ running_doc = frappe.db.get_value(
+ "Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
+ )
+ return running_doc
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py
new file mode 100644
index 00000000000..784f4548bd4
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py
@@ -0,0 +1,15 @@
+from frappe import _
+
+
+def get_data():
+ return {
+ "fieldname": "process_pr",
+ "transactions": [
+ {
+ "label": _("Reconciliation Logs"),
+ "items": [
+ "Process Payment Reconciliation Log",
+ ],
+ },
+ ],
+ }
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js
new file mode 100644
index 00000000000..8012d6e0374
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js
@@ -0,0 +1,15 @@
+frappe.listview_settings['Process Payment Reconciliation'] = {
+ add_fields: ["status"],
+ get_indicator: function(doc) {
+ let colors = {
+ 'Queued': 'orange',
+ 'Paused': 'orange',
+ 'Completed': 'green',
+ 'Partially Reconciled': 'orange',
+ 'Running': 'blue',
+ 'Failed': 'red',
+ };
+ let status = doc.status;
+ return [__(status), colors[status], 'status,=,'+status];
+ },
+};
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py
new file mode 100644
index 00000000000..ad1e952579d
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestProcessPaymentReconciliation(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js
new file mode 100644
index 00000000000..2468f10bccf
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js
@@ -0,0 +1,17 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Process Payment Reconciliation Log", {
+ refresh(frm) {
+ if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
+ let progress = 0;
+ if (frm.doc.reconciled_entries != 0) {
+ progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
+ } else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
+ progress = 100;
+ }
+ frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
+ }
+
+ },
+});
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
new file mode 100644
index 00000000000..1131a0fca69
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json
@@ -0,0 +1,137 @@
+{
+ "actions": [],
+ "autoname": "format:PPR-LOG-{##}",
+ "beta": 1,
+ "creation": "2023-03-13 15:00:09.149681",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "process_pr",
+ "section_break_fvdw",
+ "status",
+ "tasks_section",
+ "allocated",
+ "reconciled",
+ "column_break_yhin",
+ "total_allocations",
+ "reconciled_entries",
+ "section_break_4ywv",
+ "error_log",
+ "allocations_section",
+ "allocations"
+ ],
+ "fields": [
+ {
+ "fieldname": "allocations",
+ "fieldtype": "Table",
+ "label": "Allocations",
+ "options": "Process Payment Reconciliation Log Allocations",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "description": "All allocations have been successfully reconciled",
+ "fieldname": "reconciled",
+ "fieldtype": "Check",
+ "label": "Reconciled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_allocations",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Total Allocations",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "description": "Invoices and Payments have been Fetched and Allocated",
+ "fieldname": "allocated",
+ "fieldtype": "Check",
+ "label": "Allocated",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reconciled_entries",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Reconciled Entries",
+ "read_only": 1
+ },
+ {
+ "fieldname": "tasks_section",
+ "fieldtype": "Section Break",
+ "label": "Tasks"
+ },
+ {
+ "fieldname": "allocations_section",
+ "fieldtype": "Section Break",
+ "label": "Allocations"
+ },
+ {
+ "fieldname": "column_break_yhin",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_4ywv",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "eval:doc.error_log",
+ "fieldname": "error_log",
+ "fieldtype": "Long Text",
+ "label": "Reconciliation Error Log",
+ "read_only": 1
+ },
+ {
+ "fieldname": "process_pr",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Parent Document",
+ "options": "Process Payment Reconciliation",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_fvdw",
+ "fieldtype": "Section Break",
+ "label": "Status"
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-04-21 17:36:26.642617",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Payment Reconciliation Log",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py
new file mode 100644
index 00000000000..85d70a48327
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ProcessPaymentReconciliationLog(Document):
+ pass
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js
new file mode 100644
index 00000000000..5a652048a23
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js
@@ -0,0 +1,15 @@
+frappe.listview_settings['Process Payment Reconciliation Log'] = {
+ add_fields: ["status"],
+ get_indicator: function(doc) {
+ var colors = {
+ 'Partially Reconciled': 'orange',
+ 'Paused': 'orange',
+ 'Reconciled': 'green',
+ 'Failed': 'red',
+ 'Cancelled': 'red',
+ 'Running': 'blue',
+ };
+ let status = doc.status;
+ return [__(status), colors[status], "status,=,"+status];
+ },
+};
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py
new file mode 100644
index 00000000000..c2da62e2de4
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestProcessPaymentReconciliationLog(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json
new file mode 100644
index 00000000000..b97d73886a9
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json
@@ -0,0 +1,170 @@
+{
+ "actions": [],
+ "creation": "2023-03-13 13:51:27.351463",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_type",
+ "reference_name",
+ "reference_row",
+ "column_break_3",
+ "invoice_type",
+ "invoice_number",
+ "section_break_6",
+ "allocated_amount",
+ "unreconciled_amount",
+ "column_break_8",
+ "amount",
+ "is_advance",
+ "section_break_5",
+ "difference_amount",
+ "column_break_7",
+ "difference_account",
+ "exchange_rate",
+ "currency",
+ "reconciled"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_type",
+ "fieldtype": "Link",
+ "label": "Reference Type",
+ "options": "DocType",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_type",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_row",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Reference Row",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "invoice_type",
+ "fieldtype": "Link",
+ "label": "Invoice Type",
+ "options": "DocType",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "invoice_number",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Invoice Number",
+ "options": "invoice_type",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated Amount",
+ "options": "currency",
+ "reqd": 1
+ },
+ {
+ "fieldname": "unreconciled_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Unreconciled Amount",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Amount",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "is_advance",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Is Advance",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "difference_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Difference Amount",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "difference_account",
+ "fieldtype": "Link",
+ "label": "Difference Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "label": "Exchange Rate",
+ "read_only": 1
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconciled",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Reconciled"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-03-20 21:05:43.121945",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Payment Reconciliation Log Allocations",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py
new file mode 100644
index 00000000000..c3e43297d08
--- /dev/null
+++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ProcessPaymentReconciliationLogAllocations(Document):
+ pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 56e412b297c..8cb29505eb2 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -334,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
make_inter_company_invoice() {
+ let me = this;
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
frm: me.frm
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 f0146ea70eb..ad3477ef3d3 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -215,7 +215,7 @@ def get_tax_row_for_tds(tax_details, tax_amount):
}
-def get_lower_deduction_certificate(tax_details, pan_no):
+def get_lower_deduction_certificate(company, tax_details, pan_no):
ldc_name = frappe.db.get_value(
"Lower Deduction Certificate",
{
@@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
"tax_withholding_category": tax_details.tax_withholding_category,
"valid_from": (">=", tax_details.from_date),
"valid_upto": ("<=", tax_details.to_date),
+ "company": company,
},
"name",
)
@@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
- ldc = get_lower_deduction_certificate(tax_details, pan_no)
+ ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 22cff133089..30d949b73b6 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -259,6 +259,8 @@ def set_address_details(
)
if doctype in TRANSACTION_TYPES:
+ # required to set correct region
+ frappe.flags.company = company
get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index 9cea37c4f80..d67eee3552d 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -114,28 +114,6 @@ def get_assets(filters):
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
- ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
- ds.depreciation_amount
- else
- 0
- end), 0) as accumulated_depreciation_as_on_from_date,
- ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
- and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
- ds.depreciation_amount
- else
- 0
- end), 0) as depreciation_eliminated_during_the_period,
- ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
- and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
- ds.depreciation_amount
- else
- 0
- end), 0) as depreciation_amount_during_the_period
- from `tabAsset` a, `tabDepreciation Schedule` ds
- where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
- group by a.asset_category
- union
- SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
@@ -160,7 +138,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
- where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
group by a.asset_category
union
SELECT a.asset_category,
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index 306af722ba8..2d68bb70b83 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -80,7 +80,7 @@ def get_entries(filters):
payment_entries = frappe.db.sql(
"""SELECT
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
- if(paid_from=%(account)s, paid_amount * -1, received_amount)
+ if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
FROM
`tabPayment Entry`
WHERE
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index bfe2a0fd2be..98838907be1 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -4,7 +4,6 @@
import frappe
from frappe import _
-from frappe.utils import flt
def execute(filters=None):
@@ -66,12 +65,6 @@ def get_result(
else:
total_amount_credited += entry.credit
- ## Check if ldc is applied and show rate as per ldc
- actual_rate = (tds_deducted / total_amount_credited) * 100
-
- if flt(actual_rate) < flt(rate):
- rate = actual_rate
-
if tds_deducted:
row = {
"pan"
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 4ab0d56f681..320cf9de68c 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -420,7 +420,7 @@ def add_cc(args=None):
return cc.name
-def reconcile_against_document(args): # nosemgrep
+def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
"""
Cancel PE or JV, Update against document, split if required and resubmit
"""
@@ -449,7 +449,9 @@ def reconcile_against_document(args): # nosemgrep
if voucher_type == "Journal Entry":
update_reference_in_journal_entry(entry, doc, do_not_save=True)
else:
- update_reference_in_payment_entry(entry, doc, do_not_save=True)
+ update_reference_in_payment_entry(
+ entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
+ )
doc.save(ignore_permissions=True)
# re-submit advance entry
@@ -586,7 +588,9 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
journal_entry.save(ignore_permissions=True)
-def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
+def update_reference_in_payment_entry(
+ d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
+):
reference_details = {
"reference_doctype": d.against_voucher_type,
"reference_name": d.against_voucher,
@@ -632,6 +636,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_gain_or_loss(account_details=account_details)
+ payment_entry.flags.ignore_validate_update_after_submit = True
+ payment_entry.setup_party_account_field()
+ payment_entry.set_missing_values()
+ if not skip_ref_details_update_for_pe:
+ payment_entry.set_missing_ref_details()
+ payment_entry.set_amounts()
+
if not do_not_save:
payment_entry.save(ignore_permissions=True)
@@ -1352,10 +1363,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
if wh_details.account == account and not wh_details.is_group
]
- total_stock_value = 0.0
- for warehouse in related_warehouses:
- value = get_stock_value_on(warehouse, posting_date)
- total_stock_value += value
+ total_stock_value = get_stock_value_on(related_warehouses, posting_date)
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 74625890a69..a1d29f88cb1 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -126,6 +126,7 @@ def make_depreciation_entry(asset_name, date=None):
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
+ je.flags.planned_depr_entry = True
je.save()
if not je.meta.get_workflow():
je.submit()
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 7eaa4bf997a..8a1df6f71ce 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -1453,7 +1453,7 @@ class TestDepreciationBasics(AssetSetup):
)
self.assertEqual(asset.status, "Submitted")
- self.assertEqual(asset.get("value_after_depreciation"), 100000)
+ self.assertEqual(asset.get_value_after_depreciation(), 100000)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
@@ -1466,12 +1466,68 @@ class TestDepreciationBasics(AssetSetup):
jv.submit()
asset.reload()
- self.assertEqual(asset.get("value_after_depreciation"), 99900)
+ self.assertEqual(asset.get_value_after_depreciation(), 99900)
jv.cancel()
asset.reload()
- self.assertEqual(asset.get("value_after_depreciation"), 100000)
+ self.assertEqual(asset.get_value_after_depreciation(), 100000)
+
+ def test_manual_depreciation_for_depreciable_asset(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ purchase_date="2020-01-30",
+ available_for_use_date="2020-01-30",
+ expected_value_after_useful_life=10000,
+ total_number_of_depreciations=10,
+ frequency_of_depreciation=1,
+ submit=1,
+ )
+
+ self.assertEqual(asset.status, "Submitted")
+ self.assertEqual(asset.get_value_after_depreciation(), 100000)
+
+ jv = make_journal_entry(
+ "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
+ )
+ for d in jv.accounts:
+ d.reference_type = "Asset"
+ d.reference_name = asset.name
+ jv.voucher_type = "Depreciation Entry"
+ jv.insert()
+ jv.submit()
+
+ asset.reload()
+ self.assertEqual(asset.get_value_after_depreciation(), 99900)
+
+ jv.cancel()
+
+ asset.reload()
+ self.assertEqual(asset.get_value_after_depreciation(), 100000)
+
+ def test_manual_depreciation_with_incorrect_jv_voucher_type(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ purchase_date="2020-01-30",
+ available_for_use_date="2020-01-30",
+ expected_value_after_useful_life=10000,
+ total_number_of_depreciations=10,
+ frequency_of_depreciation=1,
+ submit=1,
+ )
+
+ jv = make_journal_entry(
+ "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
+ )
+ for d in jv.accounts:
+ d.reference_type = "Asset"
+ d.reference_name = asset.name
+ d.account_type = "Depreciation"
+ jv.voucher_type = "Journal Entry"
+
+ self.assertRaises(frappe.ValidationError, jv.insert)
def create_asset_data():
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
index 65a4226ebdf..4f7b8361077 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
@@ -94,11 +94,11 @@ frappe.query_reports["Fixed Asset Register"] = {
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
- depends_on: "eval: doc.only_depreciable_assets == 1",
+ depends_on: "eval: doc.filter_by_finance_book == 1",
},
{
- fieldname:"only_depreciable_assets",
- label: __("Only depreciable assets"),
+ fieldname:"filter_by_finance_book",
+ label: __("Filter by Finance Book"),
fieldtype: "Check"
},
{
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 63f9889f054..c6fbc6c58e8 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -45,8 +45,6 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
- if filters.get("only_depreciable_assets"):
- conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
@@ -106,7 +104,7 @@ def get_data(filters):
assets_linked_to_fb = None
- if filters.only_depreciable_assets:
+ if filters.filter_by_finance_book:
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js
index d7907e4274b..6594746cfc5 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js
@@ -43,7 +43,7 @@ frappe.listview_settings['Purchase Order'] = {
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
- erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
+ erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
}
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c7416228eed..d0ec6541623 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -5,7 +5,7 @@
import json
import frappe
-from frappe import _, throw
+from frappe import _, bold, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import (
@@ -405,6 +405,15 @@ class AccountsController(TransactionBase):
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
+ label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
+
+ field = frappe.scrub(label)
+
+ for row in self.get("items"):
+ if not row.get(field):
+ msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
+ frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
+
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
@@ -1653,7 +1662,10 @@ class AccountsController(TransactionBase):
)
self.append("payment_schedule", data)
- if not automatically_fetch_payment_terms:
+ if not (
+ automatically_fetch_payment_terms
+ and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
+ ):
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(
@@ -1892,12 +1904,14 @@ class AccountsController(TransactionBase):
reconcilation_entry.party = secondary_party
reconcilation_entry.reference_type = self.doctype
reconcilation_entry.reference_name = self.name
- reconcilation_entry.cost_center = self.cost_center
+ reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(
+ self.company
+ )
advance_entry.account = primary_account
advance_entry.party_type = primary_party_type
advance_entry.party = primary_party
- advance_entry.cost_center = self.cost_center
+ advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
if self.doctype == "Sales Invoice":
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index b0cf7241669..799fed99cc7 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -576,7 +576,9 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
-def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
+def get_filtered_dimensions(
+ doctype, txt, searchfield, start, page_len, filters, reference_doctype=None
+):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
@@ -617,7 +619,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(["name", query_selector, dimensions])
output = frappe.get_list(
- doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1
+ doctype,
+ fields=fields,
+ filters=query_filters,
+ or_filters=or_filters,
+ as_list=1,
+ reference_doctype=reference_doctype,
)
return [tuple(d) for d in set(output)]
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 1edd7bf85e1..4661c5ca7e8 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -976,6 +976,8 @@ def get_itemised_tax_breakup_html(doc):
@frappe.whitelist()
def get_round_off_applicable_accounts(company, account_list):
+ # required to set correct region
+ frappe.flags.company = company
account_list = get_regional_round_off_accounts(company, account_list)
return account_list
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index f4b6e910ed1..cc4ee06072d 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -60,7 +60,7 @@ class Opportunity(TransactionBase, CRMNote):
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
- self.db_set(field, value)
+ self.set(field, value)
except Exception:
continue
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index f1ee370e97e..0ba8a89bea3 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -370,6 +370,7 @@ scheduler_events = {
"cron": {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
+ "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 4304193afae..7cdcef9c7ae 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -411,7 +411,6 @@ frappe.ui.form.on("BOM", {
}
frm.set_value("process_loss_qty", qty);
- frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
}
});
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a085af859a4..b53149affd3 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1317,7 +1317,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if not field in searchfields
]
- query_filters = {"disabled": 0, "end_of_life": (">", today())}
+ query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
or_cond_filters = {}
if txt:
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index e82f37977cd..f89951619e0 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -74,6 +74,37 @@ class JobCard(Document):
self.update_sub_operation_status()
self.validate_work_order()
+ def on_update(self):
+ self.validate_job_card_qty()
+
+ def validate_job_card_qty(self):
+ if not (self.operation_id and self.work_order):
+ return
+
+ wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty"))
+
+ completed_qty = flt(
+ frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")
+ )
+
+ job_card_qty = frappe.get_all(
+ "Job Card",
+ fields=["sum(for_quantity)"],
+ filters={
+ "work_order": self.work_order,
+ "operation_id": self.operation_id,
+ "docstatus": ["!=", 2],
+ },
+ as_list=1,
+ )
+
+ job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
+
+ if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
+ msg = f"""Job Card quantity cannot be greater than
+ Work Order quantity for the operation {self.operation}"""
+ frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
+
def set_sub_operations(self):
if not self.sub_operations and self.operation:
self.sub_operations = []
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 729ed42f51a..540b7dc9ea6 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1598,6 +1598,57 @@ class TestWorkOrder(FrappeTestCase):
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
self.assertEqual(row.workstation, workstations_to_check[index])
+ def test_job_card_extra_qty(self):
+ items = [
+ "Test FG Item for Scrap Item Test 1",
+ "Test RM Item 1 for Scrap Item Test 1",
+ "Test RM Item 2 for Scrap Item Test 1",
+ ]
+
+ company = "_Test Company with perpetual inventory"
+ for item_code in items:
+ create_item(
+ item_code=item_code,
+ is_stock_item=1,
+ is_purchase_item=1,
+ opening_stock=100,
+ valuation_rate=10,
+ company=company,
+ warehouse="Stores - TCP1",
+ )
+
+ item = "Test FG Item for Scrap Item Test 1"
+ raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"]
+ if not frappe.db.get_value("BOM", {"item": item}):
+ bom = make_bom(
+ item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True
+ )
+ bom.with_operations = 1
+ bom.append(
+ "operations",
+ {
+ "operation": "_Test Operation 1",
+ "workstation": "_Test Workstation 1",
+ "hour_rate": 20,
+ "time_in_mins": 60,
+ },
+ )
+
+ bom.submit()
+
+ wo_order = make_wo_order_test_record(
+ item=item,
+ company=company,
+ planned_start_date=now(),
+ qty=20,
+ )
+ job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name")
+ job_card_doc = frappe.get_doc("Job Card", job_card)
+
+ # Make another Job Card for the same Work Order
+ job_card2 = frappe.copy_doc(job_card_doc)
+ self.assertRaises(frappe.ValidationError, job_card2.save)
+
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 97480b29454..d0c9966f8ba 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -625,20 +625,18 @@ erpnext.work_order = {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
- } else {
- frappe.db.get_doc("Manufacturing Settings").then((doc) => {
- let allowance_percentage = doc.overproduction_percentage_for_work_order;
+ } else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) {
+ let allowance_percentage = frm.doc.__onload.overproduction_percentage;
- if (allowance_percentage > 0) {
- let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
+ if (allowance_percentage > 0) {
+ let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
- if ((flt(doc.produced_qty) < allowed_qty)) {
- frm.add_custom_button(__('Finish'), function() {
- erpnext.work_order.make_se(frm, 'Manufacture');
- });
- }
+ if ((flt(doc.produced_qty) < allowed_qty)) {
+ frm.add_custom_button(__('Finish'), function() {
+ erpnext.work_order.make_se(frm, 'Manufacture');
+ });
}
- });
+ }
}
}
} else {
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 5803f46dea3..e11ee0385b0 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -326,5 +326,7 @@ erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v14_0.set_pick_list_status
erpnext.patches.v13_0.update_docs_link
+execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
+erpnext.patches.v14_0.update_company_in_ldc
diff --git a/erpnext/patches/v14_0/update_company_in_ldc.py b/erpnext/patches/v14_0/update_company_in_ldc.py
new file mode 100644
index 00000000000..ca95cf2fd7a
--- /dev/null
+++ b/erpnext/patches/v14_0/update_company_in_ldc.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+
+import frappe
+
+from erpnext import get_default_company
+
+
+def execute():
+ company = get_default_company()
+ if company:
+ for d in frappe.get_all("Lower Deduction Certificate", pluck="name"):
+ frappe.db.set_value("Lower Deduction Certificate", d, "company", company, update_modified=False)
diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
index 27c8fe4c95a..3df56e67f60 100644
--- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py
+++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
@@ -10,62 +10,6 @@ from frappe.website.serve import get_response
class TestHomepageSection(unittest.TestCase):
- def test_homepage_section_card(self):
- try:
- frappe.get_doc(
- {
- "doctype": "Homepage Section",
- "name": "Card Section",
- "section_based_on": "Cards",
- "section_cards": [
- {
- "title": "Card 1",
- "subtitle": "Subtitle 1",
- "content": "This is test card 1",
- "route": "/card-1",
- },
- {
- "title": "Card 2",
- "subtitle": "Subtitle 2",
- "content": "This is test card 2",
- "image": "test.jpg",
- },
- ],
- "no_of_columns": 3,
- }
- ).insert(ignore_if_duplicate=True)
- except frappe.DuplicateEntryError:
- pass
-
- set_request(method="GET", path="home")
- response = get_response()
-
- self.assertEqual(response.status_code, 200)
-
- html = frappe.safe_decode(response.get_data())
-
- soup = BeautifulSoup(html, "html.parser")
- sections = soup.find("main").find_all("section")
- self.assertEqual(len(sections), 3)
-
- homepage_section = sections[2]
- self.assertEqual(homepage_section.h3.text, "Card Section")
-
- cards = homepage_section.find_all(class_="card")
-
- self.assertEqual(len(cards), 2)
- self.assertEqual(cards[0].h5.text, "Card 1")
- self.assertEqual(cards[0].a["href"], "/card-1")
- self.assertEqual(cards[1].p.text, "Subtitle 2")
-
- img = cards[1].find(class_="card-img-top")
-
- self.assertEqual(img["src"], "test.jpg")
- self.assertEqual(img["loading"], "lazy")
-
- # cleanup
- frappe.db.rollback()
-
def test_homepage_section_custom_html(self):
frappe.get_doc(
{
diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json
index 468300661a0..ba6262dc3de 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.json
+++ b/erpnext/projects/doctype/timesheet/timesheet.json
@@ -96,7 +96,6 @@
"read_only": 1
},
{
- "depends_on": "eval:!doc.work_order || doc.docstatus == 1",
"fieldname": "employee_detail",
"fieldtype": "Section Break",
"label": "Employee Detail"
@@ -311,7 +310,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-02-14 04:55:41.735991",
+ "modified": "2023-04-20 15:59:11.107831",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js
index 9dae7118d9a..0209f4c2322 100644
--- a/erpnext/public/js/projects/timer.js
+++ b/erpnext/public/js/projects/timer.js
@@ -68,7 +68,7 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) {
// New activity if no activities found
var args = dialog.get_values();
if(!args) return;
- if (frm.doc.time_logs.length <= 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) {
+ if (frm.doc.time_logs.length == 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) {
frm.doc.time_logs = [];
}
row = frappe.model.add_child(frm.doc, "Timesheet Detail", "time_logs");
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 9288f515cd4..a913844e186 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -13,19 +13,11 @@ frappe.setup.on("before_load", function () {
erpnext.setup.slides_settings = [
{
- // Brand
- name: 'brand',
- icon: "fa fa-bookmark",
- title: __("The Brand"),
- // help: __('Upload your letter head and logo. (you can edit them later).'),
+ // Organization
+ name: 'organization',
+ title: __("Setup your organization"),
+ icon: "fa fa-building",
fields: [
- {
- fieldtype: "Attach Image", fieldname: "attach_logo",
- label: __("Attach Logo"),
- description: __("100px by 100px"),
- is_private: 0,
- align: 'center'
- },
{
fieldname: 'company_name',
label: __('Company Name'),
@@ -35,54 +27,9 @@ erpnext.setup.slides_settings = [
{
fieldname: 'company_abbr',
label: __('Company Abbreviation'),
- fieldtype: 'Data'
- }
- ],
- onload: function(slide) {
- this.bind_events(slide);
- },
- bind_events: function (slide) {
- slide.get_input("company_name").on("change", function () {
- var parts = slide.get_input("company_name").val().split(" ");
- var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
- slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
- }).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
-
- slide.get_input("company_abbr").on("change", function () {
- if (slide.get_input("company_abbr").val().length > 10) {
- frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
- slide.get_field("company_abbr").set_value("");
- }
- });
- },
- validate: function() {
- if ((this.values.company_name || "").toLowerCase() == "company") {
- frappe.msgprint(__("Company Name cannot be Company"));
- return false;
- }
- if (!this.values.company_abbr) {
- return false;
- }
- if (this.values.company_abbr.length > 10) {
- return false;
- }
- return true;
- }
- },
- {
- // Organisation
- name: 'organisation',
- title: __("Your Organization"),
- icon: "fa fa-building",
- fields: [
- {
- fieldname: 'company_tagline',
- label: __('What does it do?'),
fieldtype: 'Data',
- placeholder: __('e.g. "Build tools for builders"'),
- reqd: 1
+ hidden: 1
},
- { fieldname: 'bank_account', label: __('Bank Name'), fieldtype: 'Data', reqd: 1 },
{
fieldname: 'chart_of_accounts', label: __('Chart of Accounts'),
options: "", fieldtype: 'Select'
@@ -94,40 +41,24 @@ erpnext.setup.slides_settings = [
],
onload: function (slide) {
- this.load_chart_of_accounts(slide);
this.bind_events(slide);
+ this.load_chart_of_accounts(slide);
this.set_fy_dates(slide);
},
-
validate: function () {
- let me = this;
- let exist;
-
if (!this.validate_fy_dates()) {
return false;
}
- // Validate bank name
- if(me.values.bank_account) {
- frappe.call({
- async: false,
- method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
- args: {
- "coa": me.values.chart_of_accounts,
- "bank_account": me.values.bank_account
- },
- callback: function (r) {
- if(r.message){
- exist = r.message;
- me.get_field("bank_account").set_value("");
- let message = __('Account {0} already exists. Please enter a different name for your bank account.',
- [me.values.bank_account]
- );
- frappe.msgprint(message);
- }
- }
- });
- return !exist; // Return False if exist = true
+ if ((this.values.company_name || "").toLowerCase() == "company") {
+ frappe.msgprint(__("Company Name cannot be Company"));
+ return false;
+ }
+ if (!this.values.company_abbr) {
+ return false;
+ }
+ if (this.values.company_abbr.length > 10) {
+ return false;
}
return true;
@@ -151,15 +82,15 @@ erpnext.setup.slides_settings = [
var country = frappe.wizard.values.country;
if (country) {
- var fy = erpnext.setup.fiscal_years[country];
- var current_year = moment(new Date()).year();
- var next_year = current_year + 1;
+ let fy = erpnext.setup.fiscal_years[country];
+ let current_year = moment(new Date()).year();
+ let next_year = current_year + 1;
if (!fy) {
fy = ["01-01", "12-31"];
next_year = current_year;
}
- var year_start_date = current_year + "-" + fy[0];
+ let year_start_date = current_year + "-" + fy[0];
if (year_start_date > frappe.datetime.get_today()) {
next_year = current_year;
current_year -= 1;
@@ -171,7 +102,7 @@ erpnext.setup.slides_settings = [
load_chart_of_accounts: function (slide) {
- var country = frappe.wizard.values.country;
+ let country = frappe.wizard.values.country;
if (country) {
frappe.call({
@@ -202,12 +133,25 @@ erpnext.setup.slides_settings = [
me.charts_modal(slide, chart_template);
});
+
+ slide.get_input("company_name").on("change", function () {
+ let parts = slide.get_input("company_name").val().split(" ");
+ let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
+ slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
+ }).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
+
+ slide.get_input("company_abbr").on("change", function () {
+ if (slide.get_input("company_abbr").val().length > 10) {
+ frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
+ slide.get_field("company_abbr").set_value("");
+ }
+ });
},
charts_modal: function(slide, chart_template) {
let parent = __('All Accounts');
- var dialog = new frappe.ui.Dialog({
+ let dialog = new frappe.ui.Dialog({
title: chart_template,
fields: [
{'fieldname': 'expand_all', 'label': __('Expand All'), 'fieldtype': 'Button',
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
index c32ab6bec24..d332b4e76bd 100644
--- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
@@ -10,6 +10,7 @@
"tax_withholding_category",
"fiscal_year",
"column_break_3",
+ "company",
"certificate_no",
"section_break_3",
"supplier",
@@ -123,11 +124,18 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-10-23 18:33:38.962622",
+ "modified": "2023-04-18 08:25:35.302081",
"modified_by": "Administrator",
"module": "Regional",
"name": "Lower Deduction Certificate",
@@ -136,5 +144,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py
index c75179ee5d1..67179890088 100644
--- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py
+++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py
@@ -1,31 +1,135 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-
import re
import frappe
from frappe import _
from frappe.utils import format_datetime
+COLUMNS = [
+ {
+ "label": "JournalCode",
+ "fieldname": "JournalCode",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "JournalLib",
+ "fieldname": "JournalLib",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "EcritureNum",
+ "fieldname": "EcritureNum",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "EcritureDate",
+ "fieldname": "EcritureDate",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "CompteNum",
+ "fieldname": "CompteNum",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 100,
+ },
+ {
+ "label": "CompteLib",
+ "fieldname": "CompteLib",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 200,
+ },
+ {
+ "label": "CompAuxNum",
+ "fieldname": "CompAuxNum",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "CompAuxLib",
+ "fieldname": "CompAuxLib",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "PieceRef",
+ "fieldname": "PieceRef",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "PieceDate",
+ "fieldname": "PieceDate",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "EcritureLib",
+ "fieldname": "EcritureLib",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "Debit",
+ "fieldname": "Debit",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "Credit",
+ "fieldname": "Credit",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "EcritureLet",
+ "fieldname": "EcritureLet",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "DateLet",
+ "fieldname": "DateLet",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "ValidDate",
+ "fieldname": "ValidDate",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "Montantdevise",
+ "fieldname": "Montantdevise",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+ {
+ "label": "Idevise",
+ "fieldname": "Idevise",
+ "fieldtype": "Data",
+ "width": 90,
+ },
+]
+
def execute(filters=None):
- account_details = {}
- for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
- account_details.setdefault(acc.name, acc)
-
- validate_filters(filters, account_details)
-
- filters = set_account_currency(filters)
-
- columns = get_columns(filters)
-
- res = get_result(filters)
-
- return columns, res
+ validate_filters(filters)
+ return COLUMNS, get_result(
+ company=filters["company"],
+ fiscal_year=filters["fiscal_year"],
+ )
-def validate_filters(filters, account_details):
+def validate_filters(filters):
if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company")))
@@ -33,107 +137,96 @@ def validate_filters(filters, account_details):
frappe.throw(_("{0} is mandatory").format(_("Fiscal Year")))
-def set_account_currency(filters):
+def get_gl_entries(company, fiscal_year):
+ gle = frappe.qb.DocType("GL Entry")
+ sales_invoice = frappe.qb.DocType("Sales Invoice")
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+ journal_entry = frappe.qb.DocType("Journal Entry")
+ payment_entry = frappe.qb.DocType("Payment Entry")
+ customer = frappe.qb.DocType("Customer")
+ supplier = frappe.qb.DocType("Supplier")
+ employee = frappe.qb.DocType("Employee")
- filters["company_currency"] = frappe.get_cached_value(
- "Company", filters.company, "default_currency"
+ debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit")
+ credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit")
+ debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_(
+ "debitCurr"
+ )
+ credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_(
+ "creditCurr"
)
- return filters
-
-
-def get_columns(filters):
- columns = [
- "JournalCode" + "::90",
- "JournalLib" + "::90",
- "EcritureNum" + ":Dynamic Link:90",
- "EcritureDate" + "::90",
- "CompteNum" + ":Link/Account:100",
- "CompteLib" + ":Link/Account:200",
- "CompAuxNum" + "::90",
- "CompAuxLib" + "::90",
- "PieceRef" + "::90",
- "PieceDate" + "::90",
- "EcritureLib" + "::90",
- "Debit" + "::90",
- "Credit" + "::90",
- "EcritureLet" + "::90",
- "DateLet" + "::90",
- "ValidDate" + "::90",
- "Montantdevise" + "::90",
- "Idevise" + "::90",
- ]
-
- return columns
-
-
-def get_result(filters):
- gl_entries = get_gl_entries(filters)
-
- result = get_result_as_list(gl_entries, filters)
-
- return result
-
-
-def get_gl_entries(filters):
-
- group_by_condition = (
- "group by voucher_type, voucher_no, account"
- if filters.get("group_by_voucher")
- else "group by gl.name"
+ query = (
+ frappe.qb.from_(gle)
+ .left_join(sales_invoice)
+ .on(gle.voucher_no == sales_invoice.name)
+ .left_join(purchase_invoice)
+ .on(gle.voucher_no == purchase_invoice.name)
+ .left_join(journal_entry)
+ .on(gle.voucher_no == journal_entry.name)
+ .left_join(payment_entry)
+ .on(gle.voucher_no == payment_entry.name)
+ .left_join(customer)
+ .on(gle.party == customer.name)
+ .left_join(supplier)
+ .on(gle.party == supplier.name)
+ .left_join(employee)
+ .on(gle.party == employee.name)
+ .select(
+ gle.posting_date.as_("GlPostDate"),
+ gle.name.as_("GlName"),
+ gle.account,
+ gle.transaction_date,
+ debit,
+ credit,
+ debit_currency,
+ credit_currency,
+ gle.voucher_type,
+ gle.voucher_no,
+ gle.against_voucher_type,
+ gle.against_voucher,
+ gle.account_currency,
+ gle.against,
+ gle.party_type,
+ gle.party,
+ sales_invoice.name.as_("InvName"),
+ sales_invoice.title.as_("InvTitle"),
+ sales_invoice.posting_date.as_("InvPostDate"),
+ purchase_invoice.name.as_("PurName"),
+ purchase_invoice.title.as_("PurTitle"),
+ purchase_invoice.posting_date.as_("PurPostDate"),
+ journal_entry.cheque_no.as_("JnlRef"),
+ journal_entry.posting_date.as_("JnlPostDate"),
+ journal_entry.title.as_("JnlTitle"),
+ payment_entry.name.as_("PayName"),
+ payment_entry.posting_date.as_("PayPostDate"),
+ payment_entry.title.as_("PayTitle"),
+ customer.customer_name,
+ customer.name.as_("cusName"),
+ supplier.supplier_name,
+ supplier.name.as_("supName"),
+ employee.employee_name,
+ employee.name.as_("empName"),
+ )
+ .where((gle.company == company) & (gle.fiscal_year == fiscal_year))
+ .groupby(gle.voucher_type, gle.voucher_no, gle.account)
+ .orderby(gle.posting_date, gle.voucher_no)
)
- gl_entries = frappe.db.sql(
- """
- select
- gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date,
- sum(gl.debit) as debit, sum(gl.credit) as credit,
- sum(gl.debit_in_account_currency) as debitCurr, sum(gl.credit_in_account_currency) as creditCurr,
- gl.voucher_type, gl.voucher_no, gl.against_voucher_type,
- gl.against_voucher, gl.account_currency, gl.against,
- gl.party_type, gl.party,
- inv.name as InvName, inv.title as InvTitle, inv.posting_date as InvPostDate,
- pur.name as PurName, pur.title as PurTitle, pur.posting_date as PurPostDate,
- jnl.cheque_no as JnlRef, jnl.posting_date as JnlPostDate, jnl.title as JnlTitle,
- pay.name as PayName, pay.posting_date as PayPostDate, pay.title as PayTitle,
- cus.customer_name, cus.name as cusName,
- sup.supplier_name, sup.name as supName,
- emp.employee_name, emp.name as empName,
- stu.title as student_name, stu.name as stuName,
- member_name, mem.name as memName
-
- from `tabGL Entry` gl
- left join `tabSales Invoice` inv on gl.voucher_no = inv.name
- left join `tabPurchase Invoice` pur on gl.voucher_no = pur.name
- left join `tabJournal Entry` jnl on gl.voucher_no = jnl.name
- left join `tabPayment Entry` pay on gl.voucher_no = pay.name
- left join `tabCustomer` cus on gl.party = cus.name
- left join `tabSupplier` sup on gl.party = sup.name
- left join `tabEmployee` emp on gl.party = emp.name
- left join `tabStudent` stu on gl.party = stu.name
- left join `tabMember` mem on gl.party = mem.name
- where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s
- {group_by_condition}
- order by GlPostDate, voucher_no""".format(
- group_by_condition=group_by_condition
- ),
- filters,
- as_dict=1,
- )
-
- return gl_entries
+ return query.run(as_dict=True)
-def get_result_as_list(data, filters):
+def get_result(company, fiscal_year):
+ data = get_gl_entries(company, fiscal_year)
+
result = []
- company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
+ company_currency = frappe.get_cached_value("Company", company, "default_currency")
accounts = frappe.get_all(
- "Account", filters={"Company": filters.company}, fields=["name", "account_number"]
+ "Account", filters={"Company": company}, fields=["name", "account_number"]
)
for d in data:
-
JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0]
if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith(
@@ -141,9 +234,7 @@ def get_result_as_list(data, filters):
):
EcritureNum = re.split("-|/", d.get("voucher_no"))[1]
else:
- EcritureNum = re.search(
- r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE
- ).group(1)
+ EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1]
EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
@@ -185,7 +276,7 @@ def get_result_as_list(data, filters):
ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd")
- PieceRef = d.get("voucher_no") if d.get("voucher_no") else "Sans Reference"
+ PieceRef = d.get("voucher_no") or "Sans Reference"
# EcritureLib is the reference title unless it is an opening entry
if d.get("is_opening") == "Yes":
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index ccea8407ab8..4f498fb20d5 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -30,10 +30,6 @@
"cost_center",
"dimension_col_break",
"project",
- "column_break_77",
- "source",
- "campaign",
- "custom_dimensions_section",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -162,7 +158,9 @@
"is_internal_customer",
"represents_company",
"column_break_152",
+ "source",
"inter_company_order_reference",
+ "campaign",
"party_account_currency",
"connections_tab"
],
@@ -1164,12 +1162,6 @@
"print_hide": 1,
"read_only": 1
},
- {
- "fieldname": "column_break_77",
- "fieldtype": "Column Break",
- "hide_days": 1,
- "hide_seconds": 1
- },
{
"fieldname": "source",
"fieldtype": "Link",
@@ -1612,10 +1604,6 @@
"fieldname": "column_break_92",
"fieldtype": "Column Break"
},
- {
- "fieldname": "custom_dimensions_section",
- "fieldtype": "Section Break"
- },
{
"collapsible": 1,
"fieldname": "additional_info_section",
@@ -1643,7 +1631,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-12-12 18:34:00.681780",
+ "modified": "2023-04-20 11:14:01.036202",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ee9161bee48..d995517af82 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -1340,8 +1340,9 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
- & (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
- & (wo.docstatus.lte(2))
+ & (wo.sales_order == so.name)
+ & (wo.sales_order_item == i.name)
+ & (wo.docstatus.lt(2))
)
.run()[0][0]
)
diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js
index 4691190d2a5..64c58ef5d7b 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_list.js
+++ b/erpnext/selling/doctype/sales_order/sales_order_list.js
@@ -57,7 +57,7 @@ frappe.listview_settings['Sales Order'] = {
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
- erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment");
+ erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry");
});
}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index da798ab6d2d..07b1c2eb7ee 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -559,8 +559,10 @@ erpnext.PointOfSale.Controller = class {
item_row = this.frm.add_child('items', new_item);
- if (field === 'qty' && value !== 0 && !this.allow_negative_stock)
- await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse);
+ if (field === 'qty' && value !== 0 && !this.allow_negative_stock) {
+ const qty_needed = value * item_row.conversion_factor;
+ await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
+ }
await this.trigger_new_item_events(item_row);
diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py
index aadc98989fa..ace5cca0b02 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.utils import cstr, getdate
-from .default_website import website_maker
def create_fiscal_year_and_company(args):
@@ -48,83 +47,6 @@ def enable_shopping_cart(args): # nosemgrep
).insert()
-def create_email_digest():
- from frappe.utils.user import get_system_managers
-
- system_managers = get_system_managers(only_name=True)
-
- if not system_managers:
- return
-
- recipients = []
- for d in system_managers:
- recipients.append({"recipient": d})
-
- companies = frappe.db.sql_list("select name FROM `tabCompany`")
- for company in companies:
- if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
- edigest = frappe.get_doc(
- {
- "doctype": "Email Digest",
- "name": "Default Weekly Digest - " + company,
- "company": company,
- "frequency": "Weekly",
- "recipients": recipients,
- }
- )
-
- for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
- if df.fieldname != "scheduler_errors":
- edigest.set(df.fieldname, 1)
-
- edigest.insert()
-
- # scheduler errors digest
- if companies:
- edigest = frappe.new_doc("Email Digest")
- edigest.update(
- {
- "name": "Scheduler Errors",
- "company": companies[0],
- "frequency": "Daily",
- "recipients": recipients,
- "scheduler_errors": 1,
- "enabled": 1,
- }
- )
- edigest.insert()
-
-
-def create_logo(args):
- if args.get("attach_logo"):
- attach_logo = args.get("attach_logo").split(",")
- if len(attach_logo) == 3:
- filename, filetype, content = attach_logo
- _file = frappe.get_doc(
- {
- "doctype": "File",
- "file_name": filename,
- "attached_to_doctype": "Website Settings",
- "attached_to_name": "Website Settings",
- "decode": True,
- }
- )
- _file.save()
- fileurl = _file.file_url
- frappe.db.set_value(
- "Website Settings",
- "Website Settings",
- "brand_html",
- "
{1}".format(
- fileurl, args.get("company_name")
- ),
- )
-
-
-def create_website(args):
- website_maker(args)
-
-
def get_fy_details(fy_start_date, fy_end_date):
start_year = getdate(fy_start_date).year
if start_year == getdate(fy_end_date).year:
diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py
deleted file mode 100644
index 40b02b35dfd..00000000000
--- a/erpnext/setup/setup_wizard/operations/default_website.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-from frappe import _
-from frappe.utils import nowdate
-
-
-class website_maker(object):
- def __init__(self, args):
- self.args = args
- self.company = args.company_name
- self.tagline = args.company_tagline
- self.user = args.get("email")
- self.make_web_page()
- self.make_website_settings()
- self.make_blog()
-
- def make_web_page(self):
- # home page
- homepage = frappe.get_doc("Homepage", "Homepage")
- homepage.company = self.company
- homepage.tag_line = self.tagline
- homepage.setup_items()
- homepage.save()
-
- def make_website_settings(self):
- # update in home page in settings
- website_settings = frappe.get_doc("Website Settings", "Website Settings")
- website_settings.home_page = "home"
- website_settings.brand_html = self.company
- website_settings.copyright = self.company
- website_settings.top_bar_items = []
- website_settings.append(
- "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"}
- )
- website_settings.append(
- "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"}
- )
- website_settings.append(
- "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"}
- )
- website_settings.save()
-
- def make_blog(self):
- blog_category = frappe.get_doc(
- {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")}
- ).insert()
-
- if not self.user:
- # Admin setup
- return
-
- blogger = frappe.new_doc("Blogger")
- user = frappe.get_doc("User", self.user)
- blogger.user = self.user
- blogger.full_name = user.first_name + (" " + user.last_name if user.last_name else "")
- blogger.short_name = user.first_name.lower()
- blogger.avatar = user.user_image
- blogger.insert()
-
- frappe.get_doc(
- {
- "doctype": "Blog Post",
- "title": "Welcome",
- "published": 1,
- "published_on": nowdate(),
- "blogger": blogger.name,
- "blog_category": blog_category.name,
- "blog_intro": "My First Blog",
- "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(),
- }
- ).insert()
-
-
-def test():
- frappe.delete_doc("Web Page", "test-company")
- frappe.delete_doc("Blog Post", "welcome")
- frappe.delete_doc("Blogger", "administrator")
- frappe.delete_doc("Blog Category", "general")
- website_maker(
- {
- "company": "Test Company",
- "company_tagline": "Better Tools for Everyone",
- "name": "Administrator",
- }
- )
- frappe.db.commit()
diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py
index bd86a5b9693..65b268e385c 100644
--- a/erpnext/setup/setup_wizard/setup_wizard.py
+++ b/erpnext/setup/setup_wizard/setup_wizard.py
@@ -5,7 +5,6 @@
import frappe
from frappe import _
-from .operations import company_setup
from .operations import install_fixtures as fixtures
@@ -35,7 +34,6 @@ def get_setup_stages(args=None):
"fail_msg": "Failed to set defaults",
"tasks": [
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")},
- {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")},
],
},
{
@@ -60,12 +58,6 @@ def setup_defaults(args):
fixtures.install_defaults(frappe._dict(args))
-def stage_four(args):
- company_setup.create_website(args)
- company_setup.create_email_digest()
- company_setup.create_logo(args)
-
-
def fin(args):
frappe.local.message_log = []
login_as_first_user(args)
@@ -81,5 +73,4 @@ def setup_complete(args=None):
stage_fixtures(args)
setup_company(args)
setup_defaults(args)
- stage_four(args)
fin(args)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 3b9fe7b97cd..1843c6e7975 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.query_builder.functions import CurDate, Sum, Timestamp
+from frappe.query_builder.functions import CombineDatetime, CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@@ -192,7 +192,8 @@ def get_batch_qty(
posting_time = nowtime()
query = query.where(
- Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time)
+ CombineDatetime(sle.posting_date, sle.posting_time)
+ <= CombineDatetime(posting_date, posting_time)
)
out = query.run(as_list=True)[0][0] or 0
@@ -376,7 +377,7 @@ def get_pos_reserved_batch_qty(filters):
p = frappe.qb.DocType("POS Invoice").as_("p")
item = frappe.qb.DocType("POS Invoice Item").as_("item")
- sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty")
+ sum_qty = frappe.query_builder.functions.Sum(item.stock_qty).as_("qty")
reserved_batch_qty = (
frappe.qb.from_(p)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 0c1f82029e6..2adf9c310f9 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -28,8 +28,6 @@
"column_break_18",
"project",
"dimension_col_break",
- "campaign",
- "source",
"custom_dimensions_section",
"currency_and_price_list",
"currency",
@@ -161,11 +159,12 @@
"inter_company_reference",
"customer_group",
"territory",
+ "source",
+ "campaign",
"column_break5",
"excise_page",
"instructions",
- "connections_tab",
- "column_break_25"
+ "connections_tab"
],
"fields": [
{
@@ -1339,10 +1338,6 @@
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
- {
- "fieldname": "column_break_25",
- "fieldtype": "Column Break"
- },
{
"fieldname": "section_break_30",
"fieldtype": "Section Break",
@@ -1403,7 +1398,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2023-02-14 04:45:44.179670",
+ "modified": "2023-04-21 11:15:23.931084",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 3548148145e..ebd9a17c3bc 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -619,7 +619,7 @@ def make_stock_entry(source_name, target_doc=None):
target.set_transfer_qty()
target.set_actual_qty()
target.calculate_rate_and_amount(raise_error_if_no_rate=False)
- target.set_stock_entry_type()
+ target.stock_entry_type = target.purpose
target.set_job_card_data()
doclist = get_mapped_doc(
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index a707c74c7db..03f58c664d3 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -54,6 +54,8 @@ class TestMaterialRequest(FrappeTestCase):
mr.submit()
se = make_stock_entry(mr.name)
+ self.assertEqual(se.stock_entry_type, "Material Transfer")
+ self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items")))
@@ -69,6 +71,8 @@ class TestMaterialRequest(FrappeTestCase):
in_transit_warehouse = get_in_transit_warehouse(mr.company)
se = make_in_transit_stock_entry(mr.name, in_transit_warehouse)
+ self.assertEqual(se.stock_entry_type, "Material Transfer")
+ self.assertEqual(se.purpose, "Material Transfer")
self.assertEqual(se.doctype, "Stock Entry")
for row in se.get("items"):
self.assertEqual(row.t_warehouse, in_transit_warehouse)
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index d268cc11963..530427328a8 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -380,7 +380,19 @@ class PurchaseReceipt(BuyingController):
outgoing_amount = d.base_net_amount
if self.is_internal_supplier and d.valuation_rate:
- outgoing_amount = d.valuation_rate * d.stock_qty
+ outgoing_amount = abs(
+ frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": self.name,
+ "voucher_detail_no": d.name,
+ "warehouse": d.from_warehouse,
+ "is_cancelled": 0,
+ },
+ "stock_value_difference",
+ )
+ )
credit_amount = outgoing_amount
if credit_amount:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 7567cfe98c5..b5740ca575b 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1610,6 +1610,147 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
+ def test_internal_pr_gl_entries(self):
+ from erpnext.stock import get_warehouse_account_map
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+
+ prepare_data_for_internal_transfer()
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+ from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
+
+ item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
+ make_stock_entry(
+ purpose="Material Receipt",
+ item_code=item.name,
+ qty=10,
+ company=company,
+ to_warehouse=from_warehouse,
+ posting_date=add_days(today(), -3),
+ )
+
+ # Step - 1: Create Delivery Note with Internal Customer
+ dn = create_delivery_note(
+ item_code=item.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=10,
+ rate=100,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ posting_date=add_days(today(), -2),
+ )
+
+ # Step - 2: Create Internal Purchase Receipt
+ pr = make_inter_company_purchase_receipt(dn.name)
+ pr.items[0].qty = 10
+ pr.items[0].from_warehouse = target_warehouse
+ pr.items[0].warehouse = to_warehouse
+ pr.items[0].rejected_warehouse = from_warehouse
+ pr.save()
+ pr.submit()
+
+ # Step - 3: Create back-date Stock Reconciliation [After DN and Before PR]
+ create_stock_reconciliation(
+ item_code=item.name,
+ warehouse=target_warehouse,
+ qty=10,
+ rate=50,
+ company=company,
+ posting_date=add_days(today(), -1),
+ )
+
+ warehouse_account = get_warehouse_account_map(company)
+ stock_account_value = frappe.db.get_value(
+ "GL Entry",
+ {
+ "account": warehouse_account[target_warehouse]["account"],
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "is_cancelled": 0,
+ },
+ fieldname=["credit"],
+ )
+ stock_diff = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": pr.name,
+ "is_cancelled": 0,
+ },
+ fieldname=["sum(stock_value_difference)"],
+ )
+
+ # Value of Stock Account should be equal to the sum of Stock Value Difference
+ self.assertEqual(stock_account_value, stock_diff)
+
+ def test_internal_pr_reference(self):
+ item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+ from_warehouse = create_warehouse("_Test Internal From Warehouse New 1", company=company)
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New 1", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New 1", company=company)
+
+ # Step 2: Create Stock Entry (Material Receipt)
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ make_stock_entry(
+ purpose="Material Receipt",
+ item_code=item.name,
+ qty=15,
+ company=company,
+ to_warehouse=from_warehouse,
+ )
+
+ # Step 3: Create Delivery Note with Internal Customer
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ dn = create_delivery_note(
+ item_code=item.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=10,
+ rate=100,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ )
+
+ # Step 4: Create Internal Purchase Receipt
+ from erpnext.controllers.status_updater import OverAllowanceError
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+
+ pr = make_inter_company_purchase_receipt(dn.name)
+ pr.inter_company_reference = ""
+ self.assertRaises(frappe.ValidationError, pr.save)
+
+ pr.inter_company_reference = dn.name
+ pr.items[0].qty = 10
+ pr.items[0].from_warehouse = target_warehouse
+ pr.items[0].warehouse = to_warehouse
+ pr.items[0].rejected_warehouse = from_warehouse
+ pr.save()
+
+ delivery_note_item = pr.items[0].delivery_note_item
+ pr.items[0].delivery_note_item = ""
+
+ self.assertRaises(frappe.ValidationError, pr.save)
+
+ pr.load_from_db()
+ pr.items[0].delivery_note_item = delivery_note_item
+ pr.save()
+
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/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 7e39cb92f70..5d6e45013d8 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -2349,7 +2349,7 @@ def move_sample_to_retention_warehouse(company, items):
@frappe.whitelist()
def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target):
- target.set_stock_entry_type()
+ target.stock_entry_type = "Material Transfer"
target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index cc06bd709ad..c43a1b1b81e 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -202,6 +202,9 @@ class TestStockEntry(FrappeTestCase):
)
end_transit_entry = make_stock_in_entry(transit_entry.name)
+
+ self.assertEqual(end_transit_entry.stock_entry_type, "Material Transfer")
+ self.assertEqual(end_transit_entry.purpose, "Material Transfer")
self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry)
self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry)
self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 7d59441d8b7..2e5d2c3aaff 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -530,7 +530,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
# check if cancellation of stock reco is blocked
self.assertRaises(NegativeStockError, sr.cancel)
- repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name}))
+ repost_exists = bool(
+ frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name, "status": "Queued"})
+ )
self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation")
def test_intermediate_sr_bin_update(self):
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
index df01b14d11a..e4f657ca707 100644
--- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder import Field
-from frappe.query_builder.functions import Min, Timestamp
+from frappe.query_builder.functions import CombineDatetime, Min
from frappe.utils import add_days, getdate, today
import erpnext
@@ -75,7 +75,7 @@ def get_data(report_filters):
& (sle.company == report_filters.company)
& (sle.is_cancelled == 0)
)
- .orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation)
+ .orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation)
).run(as_dict=True)
for d in data:
@@ -84,7 +84,7 @@ def get_data(report_filters):
closing_date = add_days(from_date, -1)
for key, stock_data in voucher_wise_dict.items():
prev_stock_value = get_stock_value_on(
- posting_date=closing_date, item_code=key[0], warehouse=key[1]
+ posting_date=closing_date, item_code=key[0], warehouses=key[1]
)
for data in stock_data:
expected_stock_value = prev_stock_value + data.stock_value_difference
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 6234f95e876..82fc0df8def 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1388,7 +1388,11 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
def regenerate_sle_for_batch_stock_reco(detail):
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
doc.recalculate_current_qty(detail.item_code, detail.batch_no)
- doc.repost_future_sle_and_gle()
+
+ if not frappe.db.exists(
+ "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
+ ):
+ doc.repost_future_sle_and_gle()
def get_stock_reco_qty_shift(args):
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index b8c5187b2c3..fb526971ede 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -7,10 +7,11 @@ from typing import Dict, Optional
import frappe
from frappe import _
-from frappe.query_builder.functions import CombineDatetime
+from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
import erpnext
+from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
BarcodeScanResult = Dict[str, Optional[str]]
@@ -53,50 +54,36 @@ def get_stock_value_from_bin(warehouse=None, item_code=None):
return stock_value
-def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
+def get_stock_value_on(
+ warehouses: list | str = None, posting_date: str = None, item_code: str = None
+) -> float:
if not posting_date:
posting_date = nowdate()
- values, condition = [posting_date], ""
-
- if warehouse:
-
- lft, rgt, is_group = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt", "is_group"])
-
- if is_group:
- values.extend([lft, rgt])
- condition += "and exists (\
- select name from `tabWarehouse` wh where wh.name = sle.warehouse\
- and wh.lft >= %s and wh.rgt <= %s)"
-
- else:
- values.append(warehouse)
- condition += " AND warehouse = %s"
-
- if item_code:
- values.append(item_code)
- condition += " AND item_code = %s"
-
- stock_ledger_entries = frappe.db.sql(
- """
- SELECT item_code, stock_value, name, warehouse
- FROM `tabStock Ledger Entry` sle
- WHERE posting_date <= %s {0}
- and is_cancelled = 0
- ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC
- """.format(
- condition
- ),
- values,
- as_dict=1,
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+ query = (
+ frappe.qb.from_(sle)
+ .select(IfNull(Sum(sle.stock_value_difference), 0))
+ .where((sle.posting_date <= posting_date) & (sle.is_cancelled == 0))
+ .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=frappe.qb.desc)
+ .orderby(sle.creation, order=frappe.qb.desc)
)
- sle_map = {}
- for sle in stock_ledger_entries:
- if not (sle.item_code, sle.warehouse) in sle_map:
- sle_map[(sle.item_code, sle.warehouse)] = flt(sle.stock_value)
+ if warehouses:
+ if isinstance(warehouses, str):
+ warehouses = [warehouses]
- return sum(sle_map.values())
+ warehouses = set(warehouses)
+ for wh in list(warehouses):
+ if frappe.db.get_value("Warehouse", wh, "is_group"):
+ warehouses.update(get_child_warehouses(wh))
+
+ query = query.where(sle.warehouse.isin(warehouses))
+
+ if item_code:
+ query = query.where(sle.item_code == item_code)
+
+ return query.run(as_list=True)[0][0]
@frappe.whitelist()
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 3a2c53f4e44..45289b1dab5 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -67,6 +67,15 @@ frappe.ui.form.on('Subcontracting Receipt', {
}
});
+ frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ return {
+ filters: {
+ item: row.rm_item_code
+ }
+ }
+ });
+
let batch_no_field = frm.get_docfield("items", "batch_no");
if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = function(row) {
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 1698e2380f7..1c6f24b23ce 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -192,7 +192,7 @@
}
],
"links": [],
- "modified": "2021-11-26 15:45:33.289911",
+ "modified": "2023-04-21 17:16:56.192560",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
@@ -212,19 +212,12 @@
"write": 1
},
{
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
"read": 1,
- "report": 1,
- "role": "All",
- "share": 1,
- "write": 1
+ "role": "All"
}
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py
index c1579b3cbcc..5e57b317937 100644
--- a/erpnext/utilities/bulk_transaction.py
+++ b/erpnext/utilities/bulk_transaction.py
@@ -69,7 +69,7 @@ def task(doc_name, from_doctype, to_doctype):
"Sales Order": {
"Sales Invoice": sales_order.make_sales_invoice,
"Delivery Note": sales_order.make_delivery_note,
- "Advance Payment": payment_entry.get_payment_entry,
+ "Payment Entry": payment_entry.get_payment_entry,
},
"Sales Invoice": {
"Delivery Note": sales_invoice.make_delivery_note,
@@ -86,11 +86,11 @@ def task(doc_name, from_doctype, to_doctype):
"Supplier Quotation": {
"Purchase Order": supplier_quotation.make_purchase_order,
"Purchase Invoice": supplier_quotation.make_purchase_invoice,
- "Advance Payment": payment_entry.get_payment_entry,
},
"Purchase Order": {
"Purchase Invoice": purchase_order.make_purchase_invoice,
"Purchase Receipt": purchase_order.make_purchase_receipt,
+ "Payment Entry": payment_entry.get_payment_entry,
},
"Purchase Invoice": {
"Purchase Receipt": purchase_invoice.make_purchase_receipt,
@@ -98,12 +98,13 @@ def task(doc_name, from_doctype, to_doctype):
},
"Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice},
}
- if to_doctype in ["Advance Payment", "Payment Entry"]:
+ if to_doctype in ["Payment Entry"]:
obj = mapper[from_doctype][to_doctype](from_doctype, doc_name)
else:
obj = mapper[from_doctype][to_doctype](doc_name)
obj.flags.ignore_validate = True
+ obj.set_title_field()
obj.insert(ignore_mandatory=True)