diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 295cd983b3d..5258214dad2 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -11,6 +11,10 @@ from frappe.model import core_doctypes_list
from frappe.model.document import Document
from frappe.utils import cstr
+from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
+ get_allowed_types_from_settings,
+)
+
class AccountingDimension(Document):
# begin: auto-generated types
@@ -106,6 +110,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
doc_count = len(get_accounting_dimensions())
count = 0
+ repostable_doctypes = get_allowed_types_from_settings()
for doctype in doclist:
@@ -121,6 +126,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"options": doc.document_type,
"insert_after": insert_after_field,
"owner": "Administrator",
+ "allow_on_submit": 1 if doctype in repostable_doctypes else 0,
}
meta = frappe.get_meta(doctype, cached=False)
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index 22a59bfde08..af8cae3eb84 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -57,7 +57,9 @@ class BankAccount(Document):
def validate_account(self):
if self.account:
- if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
+ if accounts := frappe.db.get_all(
+ "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
+ ):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js
index ab0baab24a0..0da90161f51 100644
--- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js
@@ -3,16 +3,21 @@
frappe.ui.form.on('Cost Center Allocation', {
setup: function(frm) {
- let filters = {"is_group": 0};
- if (frm.doc.company) {
- $.extend(filters, {
- "company": frm.doc.company
- });
- }
-
frm.set_query('main_cost_center', function() {
return {
- filters: filters
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query('cost_center', 'allocation_percentages', function() {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
};
});
}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 07fb5e857ca..72469ea1e11 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -14,6 +14,25 @@ frappe.ui.form.on("Journal Entry", {
refresh: function(frm) {
erpnext.toggle_naming_series();
+ if (frm.doc.repost_required && frm.doc.docstatus===1) {
+ frm.set_intro(__("Accounting entries for this Journal Entry need to be reposted. Please click on 'Repost' button to update."));
+ frm.add_custom_button(__('Repost Accounting Entries'),
+ () => {
+ frm.call({
+ doc: frm.doc,
+ method: 'repost_accounting_entries',
+ freeze: true,
+ freeze_message: __('Reposting...'),
+ callback: (r) => {
+ if (!r.exc) {
+ frappe.msgprint(__('Accounting Entries are reposted.'));
+ frm.refresh();
+ }
+ }
+ });
+ }).removeClass('btn-default').addClass('btn-warning');
+ }
+
if(frm.doc.docstatus > 0) {
frm.add_custom_button(__('Ledger'), function() {
frappe.route_options = {
@@ -184,7 +203,6 @@ var update_jv_details = function(doc, r) {
$.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
frappe.model.set_value(row.doctype, row.name, "account", d.account)
- frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
});
refresh_field("accounts");
}
@@ -193,7 +211,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
onload() {
this.load_defaults();
this.setup_queries();
- this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
@@ -292,19 +309,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
}
- setup_balance_formatter() {
- const formatter = function(value, df, options, doc) {
- var currency = frappe.meta.get_field_currency(df, doc);
- var dr_or_cr = value ? ('') : "";
- return "
"
- + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
- + " " + dr_or_cr
- + "
";
- };
- this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
- this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
- }
-
reference_name(doc, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn);
@@ -400,23 +404,22 @@ frappe.ui.form.on("Journal Entry Account", {
if(!d.account && d.party_type && d.party) {
if(!frm.doc.company) frappe.throw(__("Please select Company"));
return frm.call({
- method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_balance",
+ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_currency",
child: d,
args: {
company: frm.doc.company,
party_type: d.party_type,
party: d.party,
- cost_center: d.cost_center
}
});
}
},
cost_center: function(frm, dt, dn) {
- erpnext.journal_entry.set_account_balance(frm, dt, dn);
+ erpnext.journal_entry.set_account_details(frm, dt, dn);
},
account: function(frm, dt, dn) {
- erpnext.journal_entry.set_account_balance(frm, dt, dn);
+ erpnext.journal_entry.set_account_details(frm, dt, dn);
},
debit_in_account_currency: function(frm, cdt, cdn) {
@@ -600,14 +603,14 @@ $.extend(erpnext.journal_entry, {
});
$.extend(erpnext.journal_entry, {
- set_account_balance: function(frm, dt, dn) {
+ set_account_details: function(frm, dt, dn) {
var d = locals[dt][dn];
if(d.account) {
if(!frm.doc.company) frappe.throw(__("Please select Company first"));
if(!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first"));
return frappe.call({
- method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_balance_and_party_type",
+ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_details_and_party_type",
args: {
account: d.account,
date: frm.doc.posting_date,
@@ -615,7 +618,6 @@ $.extend(erpnext.journal_entry, {
debit: flt(d.debit_in_account_currency),
credit: flt(d.credit_in_account_currency),
exchange_rate: d.exchange_rate,
- cost_center: d.cost_center
},
callback: function(r) {
if(r.message) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 906760ec312..91febb34052 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -64,7 +64,8 @@
"stock_entry",
"subscription_section",
"auto_repeat",
- "amended_from"
+ "amended_from",
+ "repost_required"
],
"fields": [
{
@@ -543,6 +544,15 @@
"label": "Is System Generated",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "repost_required",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Repost Required",
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 4537dede923..d5b9393ac08 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -13,6 +13,10 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
get_party_account_based_on_invoice_discounting,
)
+from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
+ validate_docs_for_deferred_accounting,
+ validate_docs_for_voucher_types,
+)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
@@ -140,7 +144,6 @@ class JournalEntry(AccountsController):
self.set_print_format_fields()
self.validate_credit_debit_note()
self.validate_empty_accounts_table()
- self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
@@ -150,6 +153,10 @@ class JournalEntry(AccountsController):
if not self.title:
self.title = self.get_title()
+ def validate_for_repost(self):
+ validate_docs_for_voucher_types(["Journal Entry"])
+ validate_docs_for_deferred_accounting([self.name], [])
+
def submit(self):
if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True)
@@ -173,6 +180,15 @@ class JournalEntry(AccountsController):
self.update_inter_company_jv()
self.update_invoice_discounting()
+ def on_update_after_submit(self):
+ if hasattr(self, "repost_required"):
+ self.needs_repost = self.check_if_fields_updated(
+ fields_to_check=[], child_tables={"accounts": []}
+ )
+ if self.needs_repost:
+ self.validate_for_repost()
+ self.db_set("repost_required", self.needs_repost)
+
def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super(JournalEntry, self).on_cancel()
@@ -559,17 +575,28 @@ class JournalEntry(AccountsController):
elif d.party_type == "Supplier" and flt(d.credit) > 0:
frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx))
+ def system_generated_gain_loss(self):
+ return (
+ self.voucher_type == "Exchange Gain Or Loss"
+ and self.multi_currency
+ and self.is_system_generated
+ )
+
def validate_against_jv(self):
for d in self.get("accounts"):
if d.reference_type == "Journal Entry":
account_root_type = frappe.get_cached_value("Account", d.account, "root_type")
- if account_root_type == "Asset" and flt(d.debit) > 0:
+ if account_root_type == "Asset" and flt(d.debit) > 0 and not self.system_generated_gain_loss():
frappe.throw(
_(
"Row #{0}: For {1}, you can select reference document only if account gets credited"
).format(d.idx, d.account)
)
- elif account_root_type == "Liability" and flt(d.credit) > 0:
+ elif (
+ account_root_type == "Liability"
+ and flt(d.credit) > 0
+ and not self.system_generated_gain_loss()
+ ):
frappe.throw(
_(
"Row #{0}: For {1}, you can select reference document only if account gets debited"
@@ -601,7 +628,7 @@ class JournalEntry(AccountsController):
for jvd in against_entries:
if flt(jvd[dr_or_cr]) > 0:
valid = True
- if not valid:
+ if not valid and not self.system_generated_gain_loss():
frappe.throw(
_("Against Journal Entry {0} does not have any unmatched {1} entry").format(
d.reference_name, dr_or_cr
@@ -1149,21 +1176,6 @@ class JournalEntry(AccountsController):
if not self.get("accounts"):
frappe.throw(_("Accounts table cannot be blank."))
- def set_account_and_party_balance(self):
- account_balance = {}
- party_balance = {}
- for d in self.get("accounts"):
- if d.account not in account_balance:
- account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date)
-
- if (d.party_type, d.party) not in party_balance:
- party_balance[(d.party_type, d.party)] = get_balance_on(
- party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company
- )
-
- d.account_balance = account_balance[d.account]
- d.party_balance = party_balance[(d.party_type, d.party)]
-
@frappe.whitelist()
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
@@ -1329,8 +1341,6 @@ def get_payment_entry(ref_doc, args):
"account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"),
"account_currency": args.get("party_account_currency")
or get_account_currency(args.get("party_account")),
- "balance": get_balance_on(args.get("party_account")),
- "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")),
"exchange_rate": exchange_rate,
args.get("amount_field_party"): args.get("amount"),
"is_advance": args.get("is_advance"),
@@ -1478,30 +1488,23 @@ def get_outstanding(args):
@frappe.whitelist()
-def get_party_account_and_balance(company, party_type, party, cost_center=None):
+def get_party_account_and_currency(company, party_type, party):
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
account = get_party_account(party_type, party, company)
- account_balance = get_balance_on(account=account, cost_center=cost_center)
- party_balance = get_balance_on(
- party_type=party_type, party=party, company=company, cost_center=cost_center
- )
-
return {
"account": account,
- "balance": account_balance,
- "party_balance": party_balance,
"account_currency": frappe.get_cached_value("Account", account, "account_currency"),
}
@frappe.whitelist()
-def get_account_balance_and_party_type(
- account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None
+def get_account_details_and_party_type(
+ account, date, company, debit=None, credit=None, exchange_rate=None
):
- """Returns dict of account balance and party type to be set in Journal Entry on selection of account."""
+ """Returns dict of account details and party type to be set in Journal Entry on selection of account."""
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
@@ -1521,7 +1524,6 @@ def get_account_balance_and_party_type(
party_type = ""
grid_values = {
- "balance": get_balance_on(account, date, cost_center=cost_center),
"party_type": party_type,
"account_type": account_details.account_type,
"account_currency": account_details.account_currency or company_currency,
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index a6e920b7ef6..798d3bb6c82 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -166,43 +166,37 @@ class TestJournalEntry(unittest.TestCase):
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit()
- gl_entries = frappe.db.sql(
- """select account, account_currency, debit, credit,
- debit_in_account_currency, credit_in_account_currency
- from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""",
- jv.name,
- as_dict=1,
- )
+ self.voucher_no = jv.name
- self.assertTrue(gl_entries)
+ self.fields = [
+ "account",
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ]
- expected_values = {
- "_Test Bank USD - _TC": {
- "account_currency": "USD",
- "debit": 5000,
- "debit_in_account_currency": 100,
- "credit": 0,
- "credit_in_account_currency": 0,
- },
- "_Test Bank - _TC": {
+ self.expected_gle = [
+ {
+ "account": "_Test Bank - _TC",
"account_currency": "INR",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 5000,
},
- }
+ {
+ "account": "_Test Bank USD - _TC",
+ "account_currency": "USD",
+ "debit": 5000,
+ "debit_in_account_currency": 100,
+ "credit": 0,
+ "credit_in_account_currency": 0,
+ },
+ ]
- for field in (
- "account_currency",
- "debit",
- "debit_in_account_currency",
- "credit",
- "credit_in_account_currency",
- ):
- for i, gle in enumerate(gl_entries):
- self.assertEqual(expected_values[gle.account][field], gle[field])
+ self.check_gl_entries()
# cancel
jv.cancel()
@@ -228,43 +222,37 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate()
rjv.submit()
- gl_entries = frappe.db.sql(
- """select account, account_currency, debit, credit,
- debit_in_account_currency, credit_in_account_currency
- from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""",
- rjv.name,
- as_dict=1,
- )
+ self.voucher_no = rjv.name
- self.assertTrue(gl_entries)
+ self.fields = [
+ "account",
+ "account_currency",
+ "debit",
+ "credit",
+ "debit_in_account_currency",
+ "credit_in_account_currency",
+ ]
- expected_values = {
- "_Test Bank USD - _TC": {
+ self.expected_gle = [
+ {
+ "account": "_Test Bank USD - _TC",
"account_currency": "USD",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 100,
},
- "Sales - _TC": {
+ {
+ "account": "Sales - _TC",
"account_currency": "INR",
"debit": 5000,
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
},
- }
+ ]
- for field in (
- "account_currency",
- "debit",
- "debit_in_account_currency",
- "credit",
- "credit_in_account_currency",
- ):
- for i, gle in enumerate(gl_entries):
- self.assertEqual(expected_values[gle.account][field], gle[field])
+ self.check_gl_entries()
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
@@ -344,23 +332,25 @@ class TestJournalEntry(unittest.TestCase):
jv.insert()
jv.submit()
- expected_values = {
- "_Test Cash - _TC": {"cost_center": cost_center},
- "_Test Bank - _TC": {"cost_center": cost_center},
- }
+ self.voucher_no = jv.name
- gl_entries = frappe.db.sql(
- """select account, cost_center, debit, credit
- from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""",
- jv.name,
- as_dict=1,
- )
+ self.fields = [
+ "account",
+ "cost_center",
+ ]
- self.assertTrue(gl_entries)
+ self.expected_gle = [
+ {
+ "account": "_Test Bank - _TC",
+ "cost_center": cost_center,
+ },
+ {
+ "account": "_Test Cash - _TC",
+ "cost_center": cost_center,
+ },
+ ]
- for gle in gl_entries:
- self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ self.check_gl_entries()
def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project
@@ -387,23 +377,22 @@ class TestJournalEntry(unittest.TestCase):
jv.insert()
jv.submit()
- expected_values = {
- "_Test Cash - _TC": {"project": project_name},
- "_Test Bank - _TC": {"project": project_name},
- }
+ self.voucher_no = jv.name
- gl_entries = frappe.db.sql(
- """select account, project, debit, credit
- from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""",
- jv.name,
- as_dict=1,
- )
+ self.fields = ["account", "project"]
- self.assertTrue(gl_entries)
+ self.expected_gle = [
+ {
+ "account": "_Test Bank - _TC",
+ "project": project_name,
+ },
+ {
+ "account": "_Test Cash - _TC",
+ "project": project_name,
+ },
+ ]
- for gle in gl_entries:
- self.assertEqual(expected_values[gle.account]["project"], gle.project)
+ self.check_gl_entries()
def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -426,6 +415,79 @@ class TestJournalEntry(unittest.TestCase):
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
self.assertEqual(expected_account_balance, account_balance)
+ def test_repost_accounting_entries(self):
+ from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
+ # Configure Repost Accounting Ledger for JVs
+ settings = frappe.get_doc("Repost Accounting Ledger Settings")
+ if not [x for x in settings.allowed_types if x.document_type == "Journal Entry"]:
+ settings.append("allowed_types", {"document_type": "Journal Entry", "allowed": True})
+ settings.save()
+
+ # Create JV with defaut cost center - _Test Cost Center
+ jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
+ jv.multi_currency = 0
+ jv.submit()
+
+ # Check GL entries before reposting
+ self.voucher_no = jv.name
+
+ self.fields = [
+ "account",
+ "debit_in_account_currency",
+ "credit_in_account_currency",
+ "cost_center",
+ ]
+
+ self.expected_gle = [
+ {
+ "account": "_Test Bank - _TC",
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 100,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ {
+ "account": "_Test Cash - _TC",
+ "debit_in_account_currency": 100,
+ "credit_in_account_currency": 0,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ ]
+
+ self.check_gl_entries()
+
+ # Change cost center for bank account - _Test Cost Center for BS Account
+ create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
+ jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC"
+ jv.save()
+
+ # Check if repost flag gets set on update after submit
+ self.assertTrue(jv.repost_required)
+ jv.repost_accounting_entries()
+
+ # Check GL entries after reposting
+ jv.load_from_db()
+ self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
+ self.check_gl_entries()
+
+ def check_gl_entries(self):
+ gl = frappe.qb.DocType("GL Entry")
+ query = frappe.qb.from_(gl)
+ for field in self.fields:
+ query = query.select(gl[field])
+
+ query = query.where(
+ (gl.voucher_type == "Journal Entry")
+ & (gl.voucher_no == self.voucher_no)
+ & (gl.is_cancelled == 0)
+ ).orderby(gl.account)
+
+ gl_entries = query.run(as_dict=True)
+
+ for i in range(len(self.expected_gle)):
+ for field in self.fields:
+ self.assertEqual(self.expected_gle[i][field], gl_entries[i][field])
+
def make_journal_entry(
account1,
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 3132fe9b12b..00fd7b9d0ab 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -9,12 +9,10 @@
"field_order": [
"account",
"account_type",
- "balance",
"col_break1",
"bank_account",
"party_type",
"party",
- "party_balance",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -64,17 +62,7 @@
"print_hide": 1
},
{
- "fieldname": "balance",
- "fieldtype": "Currency",
- "label": "Account Balance",
- "no_copy": 1,
- "oldfieldname": "balance",
- "oldfieldtype": "Data",
- "options": "account_currency",
- "print_hide": 1,
- "read_only": 1
- },
- {
+ "allow_on_submit": 1,
"default": ":Company",
"description": "If Income or Expense",
"fieldname": "cost_center",
@@ -107,14 +95,6 @@
"label": "Party",
"options": "party_type"
},
- {
- "fieldname": "party_balance",
- "fieldtype": "Currency",
- "label": "Party Balance",
- "options": "account_currency",
- "print_hide": 1,
- "read_only": 1
- },
{
"fieldname": "currency_section",
"fieldtype": "Section Break",
@@ -223,6 +203,7 @@
"no_copy": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
@@ -287,7 +268,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-23 11:44:25.841187",
+ "modified": "2024-02-05 01:10:50.224840",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 8af0713b0b4..d2ec699a6c3 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -389,7 +389,10 @@ class PaymentEntry(AccountsController):
)
def set_missing_ref_details(
- self, force: bool = False, update_ref_details_only_for: list | None = None
+ self,
+ force: bool = False,
+ update_ref_details_only_for: list | None = None,
+ ref_exchange_rate: float | None = None,
) -> None:
for d in self.get("references"):
if d.allocated_amount:
@@ -401,6 +404,8 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
+ if ref_exchange_rate:
+ ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():
if d.exchange_gain_loss:
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index ca640151888..1bf1acee70d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -631,7 +631,12 @@ class PaymentReconciliation(Document):
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
- filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
+ filters={
+ "parent": ("in", journals),
+ "account": ("in", [self.receivable_payable_account]),
+ "party_type": self.party_type,
+ "party": self.party,
+ },
fields=[
"parent as `name`",
"exchange_rate",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index fb75a0f7caf..301e6ef625c 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -56,6 +56,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
+ self.cash = "Cash - _PR"
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
@@ -486,6 +487,91 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
+ def test_payment_against_foreign_currency_journal(self):
+ transaction_date = nowdate()
+
+ self.supplier = "_Test Supplier USD"
+ self.supplier2 = make_supplier("_Test Supplier2 USD", "USD")
+ amount = 100
+ exc_rate1 = 80
+ exc_rate2 = 83
+
+ je = frappe.new_doc("Journal Entry")
+ je.posting_date = transaction_date
+ je.company = self.company
+ je.user_remark = "test"
+ je.multi_currency = 1
+ je.set(
+ "accounts",
+ [
+ {
+ "account": self.creditors_usd,
+ "party_type": "Supplier",
+ "party": self.supplier,
+ "exchange_rate": exc_rate1,
+ "cost_center": self.cost_center,
+ "credit": amount * exc_rate1,
+ "credit_in_account_currency": amount,
+ },
+ {
+ "account": self.creditors_usd,
+ "party_type": "Supplier",
+ "party": self.supplier2,
+ "exchange_rate": exc_rate2,
+ "cost_center": self.cost_center,
+ "credit": amount * exc_rate2,
+ "credit_in_account_currency": amount,
+ },
+ {
+ "account": self.expense_account,
+ "cost_center": self.cost_center,
+ "debit": (amount * exc_rate1) + (amount * exc_rate2),
+ "debit_in_account_currency": (amount * exc_rate1) + (amount * exc_rate2),
+ },
+ ],
+ )
+ je.save().submit()
+
+ pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
+ pe.payment_type = "Pay"
+ pe.party_type = "Supplier"
+ pe.party = self.supplier
+ pe.paid_to = self.creditors_usd
+ pe.paid_from = self.cash
+ pe.paid_amount = 8000
+ pe.received_amount = 100
+ pe.target_exchange_rate = exc_rate1
+ pe.paid_to_account_currency = "USD"
+ pe.save().submit()
+
+ pr = self.create_payment_reconciliation(party_is_customer=False)
+ pr.receivable_payable_account = self.creditors_usd
+ pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
+ pr.from_invoice_date = pr.to_invoice_date = transaction_date
+ pr.from_payment_date = pr.to_payment_date = transaction_date
+
+ pr.get_unreconciled_entries()
+ invoices = [x.as_dict() for x in pr.get("invoices")]
+ payments = [x.as_dict() for x in pr.get("payments")]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # There should no difference_amount as the Journal and Payment have same exchange rate - 'exc_rate1'
+ for row in pr.allocation:
+ self.assertEqual(flt(row.get("difference_amount")), 0.0)
+
+ pr.reconcile()
+
+ # check PR tool output
+ self.assertEqual(len(pr.get("invoices")), 0)
+ self.assertEqual(len(pr.get("payments")), 0)
+
+ journals = frappe.db.get_all(
+ "Journal Entry Account",
+ filters={"reference_type": je.doctype, "reference_name": je.name, "docstatus": 1},
+ fields=["parent"],
+ )
+ self.assertEqual([], journals)
+
def test_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
@@ -1248,3 +1334,17 @@ def make_customer(customer_name, currency=None):
return customer.name
else:
return customer_name
+
+
+def make_supplier(supplier_name, currency=None):
+ if not frappe.db.exists("Supplier", supplier_name):
+ supplier = frappe.new_doc("Supplier")
+ supplier.supplier_name = supplier_name
+ supplier.type = "Individual"
+
+ if currency:
+ supplier.default_currency = currency
+ supplier.save()
+ return supplier.name
+ else:
+ return supplier_name
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 2b6c6b64377..18ecd91d728 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -731,6 +731,7 @@ class PurchaseInvoice(BuyingController):
"cash_bank_account",
"write_off_account",
"unrealized_profit_loss_account",
+ "is_opening",
]
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
@@ -1023,9 +1024,14 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
- provisional_account = frappe.db.get_value(
- "Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
- ) or self.get_company_default("default_provisional_account")
+ provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
+ "Purchase Receipt Item",
+ item.pr_detail,
+ ["provisional_expense_account", "qty", "base_rate"],
+ )
+ provisional_account = provisional_account or self.get_company_default(
+ "default_provisional_account"
+ )
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
@@ -1042,13 +1048,18 @@ class PurchaseInvoice(BuyingController):
"voucher_detail_no": item.pr_detail,
"account": provisional_account,
},
- ["name"],
+ "name",
)
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
- item, gl_entries, self.posting_date, provisional_account, reverse=1
+ item,
+ gl_entries,
+ self.posting_date,
+ provisional_account,
+ reverse=1,
+ item_amount=(min(item.qty, pr_qty) * pr_base_rate),
)
if not self.is_internal_transfer():
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index e4e2cd79fb4..e0fbef4a9b4 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1529,18 +1529,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
- create_item("_Test Non Stock Item", is_stock_item=0)
-
- provisional_account = create_account(
- account_name="Provision Account",
- parent_account="Current Liabilities - _TC",
- company="_Test Company",
- )
-
- company = frappe.get_doc("Company", "_Test Company")
- company.enable_provisional_accounting_for_non_stock_items = 1
- company.default_provisional_account = provisional_account
- company.save()
+ setup_provisional_accounting()
pr = make_purchase_receipt(
item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)
@@ -1584,8 +1573,97 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
)
- company.enable_provisional_accounting_for_non_stock_items = 0
- company.save()
+ toggle_provisional_accounting_setting()
+
+ def test_provisional_accounting_entry_for_over_billing(self):
+ setup_provisional_accounting()
+
+ # Configure Buying Settings to allow rate change
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
+
+ # Create PR: rate = 1000, qty = 5
+ pr = make_purchase_receipt(
+ item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
+ )
+
+ # Overbill PR: rate = 2000, qty = 10
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ pi.set_posting_time = 1
+ pi.posting_date = add_days(pr.posting_date, -1)
+ pi.items[0].qty = 10
+ pi.items[0].rate = 2000
+ pi.items[0].expense_account = "Cost of Goods Sold - _TC"
+ pi.save()
+ pi.submit()
+
+ expected_gle = [
+ ["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
+ ["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
+ ]
+
+ check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
+
+ expected_gle_for_purchase_receipt = [
+ ["Provision Account - _TC", 5000, 0, pr.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
+ ["Provision Account - _TC", 0, 5000, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
+ ]
+
+ check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
+
+ # Cancel purchase invoice to check reverse provisional entry cancellation
+ pi.cancel()
+
+ expected_gle_for_purchase_receipt_post_pi_cancel = [
+ ["Provision Account - _TC", 0, 5000, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
+ ]
+
+ check_gl_entries(
+ self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
+ )
+
+ toggle_provisional_accounting_setting()
+
+ def test_provisional_accounting_entry_for_partial_billing(self):
+ setup_provisional_accounting()
+
+ # Configure Buying Settings to allow rate change
+ frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
+
+ # Create PR: rate = 1000, qty = 5
+ pr = make_purchase_receipt(
+ item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
+ )
+
+ # Partially bill PR: rate = 500, qty = 2
+ pi = create_purchase_invoice_from_receipt(pr.name)
+ pi.set_posting_time = 1
+ pi.posting_date = add_days(pr.posting_date, -1)
+ pi.items[0].qty = 2
+ pi.items[0].rate = 500
+ pi.items[0].expense_account = "Cost of Goods Sold - _TC"
+ pi.save()
+ pi.submit()
+
+ expected_gle = [
+ ["Cost of Goods Sold - _TC", 1000, 0, add_days(pr.posting_date, -1)],
+ ["Creditors - _TC", 0, 1000, add_days(pr.posting_date, -1)],
+ ]
+
+ check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
+
+ expected_gle_for_purchase_receipt = [
+ ["Provision Account - _TC", 5000, 0, pr.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
+ ["Provision Account - _TC", 0, 1000, pi.posting_date],
+ ["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
+ ]
+
+ check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
+
+ toggle_provisional_accounting_setting()
def test_adjust_incoming_rate(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
@@ -2264,4 +2342,26 @@ def make_purchase_invoice_against_cost_center(**args):
return pi
+def setup_provisional_accounting(**args):
+ args = frappe._dict(args)
+ create_item("_Test Non Stock Item", is_stock_item=0)
+ company = args.company or "_Test Company"
+ provisional_account = create_account(
+ account_name=args.account_name or "Provision Account",
+ parent_account=args.parent_account or "Current Liabilities - _TC",
+ company=company,
+ )
+ toggle_provisional_accounting_setting(
+ enable=1, company=company, provisional_account=provisional_account
+ )
+
+
+def toggle_provisional_accounting_setting(**args):
+ args = frappe._dict(args)
+ company = frappe.get_doc("Company", args.company or "_Test Company")
+ company.enable_provisional_accounting_for_non_stock_items = args.enable or 0
+ company.default_provisional_account = args.provisional_account
+ company.save()
+
+
test_records = frappe.get_test_records("Purchase Invoice")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 3ab9f518d05..1343b39352c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -721,6 +721,7 @@ class SalesInvoice(SellingController):
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
+ "is_opening",
]
child_tables = {
"items": ("income_account", "expense_account", "discount_account"),
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 4b7a1df44a0..6a01ccf3409 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3602,6 +3602,33 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
set_advance_flag(company="_Test Company", flag=0, default_account="")
+ def test_pulling_advance_based_on_debit_to(self):
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+
+ debtors2 = create_account(
+ parent_account="Accounts Receivable - _TC",
+ account_name="Debtors 2",
+ company="_Test Company",
+ account_type="Receivable",
+ )
+ si = create_sales_invoice(do_not_submit=True)
+ si.debit_to = debtors2
+ si.save()
+
+ pe = create_payment_entry(
+ company=si.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=si.customer,
+ paid_from=debtors2,
+ paid_to="Cash - _TC",
+ paid_amount=1000,
+ )
+ pe.submit()
+ advances = si.get_advance_entries()
+ self.assertEqual(1, len(advances))
+ self.assertEqual(advances[0].reference_name, pe.name)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
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 c39a9db9800..405f58715bd 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -546,6 +546,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
"GL Entry",
{
"is_cancelled": 0,
+ "party_type": "Customer",
"party": ["in", parties],
"company": inv.company,
"voucher_no": ["in", vouchers],
@@ -560,6 +561,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions = []
conditions.append(ple.amount.lt(0))
conditions.append(ple.delinked == 0)
+ conditions.append(ple.party_type == "Customer")
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
@@ -579,6 +581,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
{
"is_cancelled": 0,
"credit": [">", 0],
+ "party_type": "Customer",
"party": ["in", parties],
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"company": inv.company,
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index c06c9774c60..b1779e655dd 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -714,7 +714,7 @@ def update_reference_in_payment_entry(
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_missing_ref_details(ref_exchange_rate=d.exchange_rate or None)
payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal(
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json
index 0ae1c1428ee..31c9d52bcaa 100644
--- a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json
+++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json
@@ -63,7 +63,7 @@
},
{
"columns": 1,
- "fetch_from": "stock_item_code.stock_uom",
+ "fetch_from": "item_code.stock_uom",
"fieldname": "uom",
"fieldtype": "Link",
"in_list_view": 1,
@@ -110,7 +110,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-08 15:52:08.598100",
+ "modified": "2024-03-05 11:23:40.766844",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Service Item",
@@ -118,5 +118,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
index f79a84855d5..c838f8b0f26 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
@@ -65,7 +65,7 @@
},
{
"columns": 1,
- "fetch_from": "stock_item_code.stock_uom",
+ "fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"in_list_view": 1,
@@ -178,7 +178,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-02-25 15:57:35.007501",
+ "modified": "2024-03-05 11:22:57.346889",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 89f13bbb4b4..045e4fb7b6b 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -46,6 +46,7 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import (
create_gain_loss_journal,
get_account_currency,
+ get_currency_precision,
get_fiscal_years,
validate_fiscal_year,
)
@@ -1133,21 +1134,24 @@ class AccountsController(TransactionBase):
self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True):
+ party_account = []
if self.doctype == "Sales Invoice":
party_type = "Customer"
party = self.customer
amount_field = "credit_in_account_currency"
order_field = "sales_order"
order_doctype = "Sales Order"
+ party_account.append(self.debit_to)
else:
party_type = "Supplier"
party = self.supplier
amount_field = "debit_in_account_currency"
order_field = "purchase_order"
order_doctype = "Purchase Order"
+ party_account.append(self.credit_to)
- party_account = get_party_account(
- party_type, party=party, company=self.company, include_advance=True
+ party_account.extend(
+ get_party_account(party_type, party=party, company=self.company, include_advance=True)
)
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
@@ -1296,10 +1300,12 @@ class AccountsController(TransactionBase):
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
# and below logic is only for such scenarios
if args:
+ precision = get_currency_precision()
for arg in args:
# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
if (
- arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0
+ flt(arg.get("difference_amount", 0), precision) != 0
+ or flt(arg.get("exchange_gain_loss", 0), precision) != 0
) and arg.get("difference_account"):
party_account = arg.get("account")
@@ -2413,27 +2419,20 @@ class AccountsController(TransactionBase):
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
- # Check if opening entry check updated
- needs_repost = doc_before_update.get("is_opening") != self.is_opening
+ # Parent Level Accounts excluding party account
+ fields_to_check += accounting_dimensions
+ for field in fields_to_check:
+ if doc_before_update.get(field) != self.get(field):
+ return True
- if not needs_repost:
- # Parent Level Accounts excluding party account
- fields_to_check += accounting_dimensions
- for field in fields_to_check:
- if doc_before_update.get(field) != self.get(field):
- needs_repost = 1
- break
+ # Check for child tables
+ for table in child_tables:
+ if check_if_child_table_updated(
+ doc_before_update.get(table), self.get(table), child_tables[table]
+ ):
+ return True
- if not needs_repost:
- # Check for child tables
- for table in child_tables:
- needs_repost = check_if_child_table_updated(
- doc_before_update.get(table), self.get(table), child_tables[table]
- )
- if needs_repost:
- break
-
- return needs_repost
+ return False
@frappe.whitelist()
def repost_accounting_entries(self):
@@ -3459,15 +3458,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
def check_if_child_table_updated(
child_table_before_update, child_table_after_update, fields_to_check
):
- accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
- # Check if any field affecting accounting entry is altered
- for index, item in enumerate(child_table_after_update):
- for field in fields_to_check:
- if child_table_before_update[index].get(field) != item.get(field):
- return True
+ fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"]
- for dimension in accounting_dimensions:
- if child_table_before_update[index].get(dimension) != item.get(dimension):
+ # Check if any field affecting accounting entry is altered
+ for index, item in enumerate(child_table_before_update):
+ for field in fields_to_check:
+ if child_table_after_update[index].get(field) != item.get(field):
return True
return False
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index fbdf22d5aec..65b2696e79a 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -56,7 +56,8 @@ class TestAccountsController(FrappeTestCase):
20 series - Sales Invoice against Journals
30 series - Sales Invoice against Credit Notes
40 series - Company default Cost center is unset
- 50 series - Dimension inheritence
+ 50 series = Journals against Journals
+ 90 series - Dimension inheritence
"""
def setUp(self):
@@ -1204,7 +1205,7 @@ class TestAccountsController(FrappeTestCase):
x.mandatory_for_pl = False
loc.save()
- def test_50_dimensions_filter(self):
+ def test_90_dimensions_filter(self):
"""
Test workings of dimension filters
"""
@@ -1275,7 +1276,7 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
- def test_51_cr_note_should_inherit_dimension(self):
+ def test_91_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1
@@ -1317,7 +1318,7 @@ class TestAccountsController(FrappeTestCase):
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
)
- def test_52_dimension_inhertiance_exc_gain_loss(self):
+ def test_92_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate = 80
@@ -1355,7 +1356,7 @@ class TestAccountsController(FrappeTestCase):
),
)
- def test_53_dimension_inheritance_on_advance(self):
+ def test_93_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
@@ -1400,3 +1401,70 @@ class TestAccountsController(FrappeTestCase):
pluck="department",
),
)
+
+ def test_50_journal_against_journal(self):
+ # Invoice in Foreign Currency
+ journal_as_invoice = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=83,
+ acc2=self.cash,
+ acc1_amount=1,
+ acc2_amount=83,
+ acc2_exc_rate=1,
+ )
+ journal_as_invoice.accounts[0].party_type = "Customer"
+ journal_as_invoice.accounts[0].party = self.customer
+ journal_as_invoice = journal_as_invoice.save().submit()
+
+ # Payment
+ journal_as_payment = self.create_journal_entry(
+ acc1=self.debit_usd,
+ acc1_exc_rate=75,
+ acc2=self.cash,
+ acc1_amount=-1,
+ acc2_amount=-75,
+ acc2_exc_rate=1,
+ )
+ journal_as_payment.accounts[0].party_type = "Customer"
+ journal_as_payment.accounts[0].party = self.customer
+ journal_as_payment = journal_as_payment.save().submit()
+
+ # Reconcile the remaining amount
+ pr = self.create_payment_reconciliation()
+ # pr.receivable_payable_account = self.debit_usd
+ pr.get_unreconciled_entries()
+ self.assertEqual(len(pr.invoices), 1)
+ self.assertEqual(len(pr.payments), 1)
+ 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}))
+ pr.reconcile()
+ self.assertEqual(len(pr.invoices), 0)
+ self.assertEqual(len(pr.payments), 0)
+
+ # There should be no outstanding in both currencies
+ journal_as_invoice.reload()
+ self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 0.0, 0.0)
+
+ # Exchange Gain/Loss Journal should've been created.
+ exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name)
+ exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
+ self.assertNotEqual(exc_je_for_si, [])
+ self.assertEqual(
+ len(exc_je_for_si), 2
+ ) # payment also has reference. so, there are 2 journals referencing invoice
+ self.assertEqual(len(exc_je_for_je), 1)
+ self.assertIn(exc_je_for_je[0], exc_je_for_si)
+
+ # Cancel Payment
+ journal_as_payment.reload()
+ journal_as_payment.cancel()
+
+ journal_as_invoice.reload()
+ self.assert_ledger_outstanding(journal_as_invoice.doctype, journal_as_invoice.name, 83.0, 1.0)
+
+ # Exchange Gain/Loss Journal should've been cancelled
+ exc_je_for_si = self.get_journals_for(journal_as_invoice.doctype, journal_as_invoice.name)
+ exc_je_for_je = self.get_journals_for(journal_as_payment.doctype, journal_as_payment.name)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(exc_je_for_je, [])
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f0b104e2c5f..d5c0a4e80e4 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -353,6 +353,7 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
+erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
diff --git a/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py b/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py
new file mode 100644
index 00000000000..e75610d0a53
--- /dev/null
+++ b/erpnext/patches/v15_0/allow_on_submit_dimensions_for_repostable_doctypes.py
@@ -0,0 +1,14 @@
+import frappe
+
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_accounting_dimensions,
+)
+from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
+ get_allowed_types_from_settings,
+)
+
+
+def execute():
+ for dt in get_allowed_types_from_settings():
+ for dimension in get_accounting_dimensions():
+ frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 2dac399d88f..e0dd1989e80 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -35,6 +35,14 @@ frappe.ui.form.on("Project", {
};
});
+ frm.set_query("department", function (doc) {
+ return {
+ filters: {
+ "company": doc.company,
+ }
+ };
+ });
+
// sales order
frm.set_query('sales_order', function () {
var filters = {
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index d24c4e6075d..527b762ace0 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -22,7 +22,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
item_rate = flt(item.rate_with_margin , precision("rate", item));
- if (item.discount_percentage) {
+ if (item.discount_percentage && !item.discount_amount) {
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2e2b9dc300d..2906e49a222 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1163,6 +1163,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if(!r.exc) {
+ me.apply_price_list(item, true)
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
}
}
@@ -1513,6 +1514,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
remove_pricing_rule_for_item(item) {
+ // capture pricing rule before removing it to delete free items
+ let removed_pricing_rule = item.pricing_rules;
if (item.pricing_rules){
let me = this;
return this.frm.call({
@@ -1533,7 +1536,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if (!r.exc && r.message) {
- me.remove_pricing_rule(r.message);
+ me.remove_pricing_rule(r.message, removed_pricing_rule);
me.calculate_taxes_and_totals();
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on");
}
@@ -1791,7 +1794,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
});
}
- remove_pricing_rule(item) {
+ remove_pricing_rule(item, removed_pricing_rule) {
let me = this;
const fields = ["discount_percentage",
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
@@ -1800,7 +1803,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let items = [];
me.frm.doc.items.forEach(d => {
- if(d.item_code != item.remove_free_item || !d.is_free_item) {
+ // if same item was added a free item through a different pricing rule, keep it
+ if(d.item_code != item.remove_free_item || !d.is_free_item || removed_pricing_rule?.includes(d.pricing_rules)) {
items.push(d);
}
});
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 44ffc6d1616..aafacdfa907 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -249,7 +249,8 @@ class SalesOrder(SellingController):
frappe.msgprint(
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
frappe.bold(so[0][0]), frappe.bold(self.po_no)
- )
+ ),
+ alert=True,
)
else:
frappe.throw(
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 5b993fa64b7..ef0fb5fa802 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -20,7 +20,9 @@ default_mail_footer = """ 0)
& (table.is_cancelled == 0)
+ & (
+ table.posting_datetime <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
+ )
)
- .orderby(table.posting_date, order=Order.desc)
+ .orderby(table.posting_datetime, order=Order.desc)
.limit(1)
- ).run()
+ ).run(as_dict=1)
- self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0
- incoming_values += self.serial_no_incoming_rate[serial_no]
+ for sle in stock_ledgers:
+ self.serial_no_incoming_rate[serial_no] += flt(sle.incoming_rate)
+ incoming_values += self.serial_no_incoming_rate[serial_no]
return incoming_values
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 7873d3e6de4..d07a825331d 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -28,7 +28,6 @@
"column_break_18",
"project",
"dimension_col_break",
- "custom_dimensions_section",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -927,7 +926,7 @@
"width": "50%"
},
{
- "fetch_from": "transporter.name",
+ "fetch_from": "transporter.supplier_name",
"fieldname": "transporter_name",
"fieldtype": "Data",
"label": "Transporter Name",
@@ -1355,10 +1354,6 @@
"fieldname": "column_break_43",
"fieldtype": "Column Break"
},
- {
- "fieldname": "custom_dimensions_section",
- "fieldtype": "Section Break"
- },
{
"fieldname": "shipping_address_section",
"fieldtype": "Section Break",
@@ -1402,7 +1397,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2023-12-18 17:19:39.368239",
+ "modified": "2024-03-05 11:58:47.784349",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js
index 22d38e88935..3946a4291d3 100644
--- a/erpnext/stock/doctype/item/item_list.js
+++ b/erpnext/stock/doctype/item/item_list.js
@@ -18,8 +18,7 @@ frappe.listview_settings['Item'] = {
reports: [
{
name: 'Stock Summary',
- report_type: 'Page',
- route: 'stock-balance'
+ route: '/app/stock-balance'
},
{
name: 'Stock Ledger',
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 257f263bd22..4058aa82d7e 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -415,23 +415,6 @@ class TestLandedCostVoucher(FrappeTestCase):
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges
- sn_obj = SerialNoValuation(
- sle=frappe._dict(
- {
- "posting_date": today(),
- "posting_time": nowtime(),
- "item_code": "_Test Serialized Item",
- "warehouse": "Stores - TCP1",
- "serial_nos": [serial_no],
- }
- )
- )
-
- new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no)
-
- # Since the serial no is already delivered the rate must be zero
- self.assertFalse(new_serial_no_rate)
-
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
filters={
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 67e6ff90e78..e249cf386fa 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -727,16 +727,19 @@ class PurchaseReceipt(BuyingController):
)
def add_provisional_gl_entry(
- self, item, gl_entries, posting_date, provisional_account, reverse=0
+ self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
):
credit_currency = get_account_currency(provisional_account)
expense_account = item.expense_account
debit_currency = get_account_currency(item.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1
+ amount = item.base_amount
if reverse:
multiplication_factor = -1
+ # Post reverse entry for previously posted amount
+ amount = item_amount
expense_account = frappe.db.get_value(
"Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
)
@@ -746,7 +749,7 @@ class PurchaseReceipt(BuyingController):
account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
- credit=multiplication_factor * item.base_amount,
+ credit=multiplication_factor * amount,
remarks=remarks,
against_account=expense_account,
account_currency=credit_currency,
@@ -760,7 +763,7 @@ class PurchaseReceipt(BuyingController):
gl_entries=gl_entries,
account=expense_account,
cost_center=item.cost_center,
- debit=multiplication_factor * item.base_amount,
+ debit=multiplication_factor * amount,
credit=0.0,
remarks=remarks,
against_account=provisional_account,
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
index 725e91ee8d9..753569c8812 100644
--- a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
@@ -11,8 +11,7 @@ frappe.listview_settings['Putaway Rule'] = {
reports: [
{
name: 'Warehouse Capacity Summary',
- report_type: 'Page',
- route: 'warehouse-capacity-summary'
+ route: '/app/warehouse-capacity-summary'
}
]
};
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 13937d928b8..8a73cb6b37b 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -219,14 +219,9 @@ class RepostItemValuation(Document):
if self.status not in ("Queued", "In Progress"):
return
- if not (self.voucher_no and self.voucher_no):
- return
-
- transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus")
- if transaction_status == 2:
- msg = _("Cannot cancel as processing of cancelled documents is pending.")
- msg += "
" + _("Please try again in an hour.")
- frappe.throw(msg, title=_("Pending processing"))
+ msg = _("Cannot cancel as processing of cancelled documents is pending.")
+ msg += "
" + _("Please try again in an hour.")
+ frappe.throw(msg, title=_("Pending processing"))
@frappe.whitelist()
def restart_reposting(self):
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index b6e4d6f40c6..d01dfefc926 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -332,13 +332,8 @@ class SerialandBatchBundle(Document):
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
for d in self.entries:
- if not rate or (
- flt(rate, precision) == flt(d.incoming_rate, precision) and d.stock_value_difference
- ):
- continue
-
d.incoming_rate = flt(rate, precision)
- if self.has_batch_no:
+ if d.qty:
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
if save:
@@ -2118,7 +2113,7 @@ def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, ba
make_serial_no(serial_no, item_code)
- if batch_no and frappe.db.exists("Batch", batch_no):
+ if batch_no and not frappe.db.exists("Batch", batch_no):
if type_of_transaction != "Inward":
frappe.throw(_("Batch No {0} does not exists").format(batch_no))
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index b932c1371d6..826ac03b5f7 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -540,6 +540,110 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertRaises(frappe.exceptions.ValidationError, pr2.save)
+ def test_serial_no_valuation_for_legacy_ledgers(self):
+ sn_item = make_item(
+ "Test Serial No Valuation for Legacy Ledgers",
+ properties={"has_serial_no": 1, "serial_no_series": "SNN-TSNVL.-#####"},
+ ).name
+
+ serial_nos = []
+ for serial_no in [f"{sn_item}-0001", f"{sn_item}-0002"]:
+ if not frappe.db.exists("Serial No", serial_no):
+ sn_doc = frappe.get_doc(
+ {
+ "doctype": "Serial No",
+ "serial_no": serial_no,
+ "item_code": sn_item,
+ }
+ ).insert(ignore_permissions=True)
+ serial_nos.append(serial_no)
+
+ frappe.flags.ignore_serial_batch_bundle_validation = True
+
+ qty_after_transaction = 0.0
+ stock_value = 0.0
+ for row in [{"qty": 2, "rate": 100}, {"qty": -2, "rate": 100}, {"qty": 2, "rate": 200}]:
+ row = frappe._dict(row)
+ qty_after_transaction += row.qty
+ stock_value += row.rate * row.qty
+
+ doc = frappe.get_doc(
+ {
+ "doctype": "Stock Ledger Entry",
+ "posting_date": today(),
+ "posting_time": nowtime(),
+ "incoming_rate": row.rate if row.qty > 0 else 0,
+ "qty_after_transaction": qty_after_transaction,
+ "stock_value_difference": row.rate * row.qty,
+ "stock_value": stock_value,
+ "valuation_rate": row.rate,
+ "actual_qty": row.qty,
+ "item_code": sn_item,
+ "warehouse": "_Test Warehouse - _TC",
+ "serial_no": "\n".join(serial_nos),
+ "company": "_Test Company",
+ }
+ )
+ doc.flags.ignore_permissions = True
+ doc.flags.ignore_mandatory = True
+ doc.flags.ignore_links = True
+ doc.flags.ignore_validate = True
+ doc.submit()
+
+ for sn in serial_nos:
+ sn_doc = frappe.get_doc("Serial No", sn)
+ if row.qty > 0:
+ sn_doc.db_set("warehouse", "_Test Warehouse - _TC")
+ else:
+ sn_doc.db_set("warehouse", "")
+
+ frappe.flags.ignore_serial_batch_bundle_validation = False
+
+ se = make_stock_entry(
+ item_code=sn_item,
+ qty=2,
+ source="_Test Warehouse - _TC",
+ serial_no="\n".join(serial_nos),
+ use_serial_batch_fields=True,
+ do_not_submit=True,
+ )
+
+ se.save()
+ se.submit()
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(flt(stock_value_difference, 2), 400.0 * -1)
+
+ se = make_stock_entry(
+ item_code=sn_item,
+ qty=1,
+ rate=353,
+ target="_Test Warehouse - _TC",
+ )
+
+ serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0]
+
+ se = make_stock_entry(
+ item_code=sn_item,
+ qty=1,
+ source="_Test Warehouse - _TC",
+ serial_no=serial_no,
+ use_serial_batch_fields=True,
+ )
+
+ stock_value_difference = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1)
+
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 24dd9d1d203..1fcc439fda3 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -4,8 +4,9 @@ from typing import List
import frappe
from frappe import _, bold
from frappe.model.naming import make_autoname
-from frappe.query_builder.functions import CombineDatetime, Sum
+from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
+from pypika import Order
from erpnext.stock.deprecated_serial_batch import (
DeprecatedBatchNoValuation,
@@ -424,19 +425,21 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
)
else:
- entries = self.get_serial_no_ledgers()
-
self.serial_no_incoming_rate = defaultdict(float)
self.stock_value_change = 0.0
- for ledger in entries:
- self.stock_value_change += ledger.incoming_rate
- self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
+ serial_nos = self.get_serial_nos()
+ for serial_no in serial_nos:
+ incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
+ if not incoming_rate:
+ continue
+
+ self.stock_value_change += incoming_rate
+ self.serial_no_incoming_rate[serial_no] += incoming_rate
self.calculate_stock_value_from_deprecarated_ledgers()
- def get_serial_no_ledgers(self):
- serial_nos = self.get_serial_nos()
+ def get_incoming_rate_from_bundle(self, serial_no) -> float:
bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
@@ -444,20 +447,18 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
frappe.qb.from_(bundle)
.inner_join(bundle_child)
.on(bundle.name == bundle_child.parent)
- .select(
- bundle.name,
- bundle_child.serial_no,
- (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
- )
+ .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
.where(
(bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
- & (bundle_child.serial_no.isin(serial_nos))
- & (bundle.type_of_transaction.isin(["Inward", "Outward"]))
+ & (bundle_child.serial_no == serial_no)
+ & (bundle.type_of_transaction == "Inward")
+ & (bundle_child.qty > 0)
& (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse)
)
- .orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
+ .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
+ .limit(1)
)
# Important to exclude the current voucher to calculate correct the stock value difference
@@ -474,7 +475,8 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
query = query.where(timestamp_condition)
- return query.run(as_dict=True)
+ incoming_rate = query.run()
+ return flt(incoming_rate[0][0]) if incoming_rate else 0.0
def get_serial_nos(self):
if self.sle.get("serial_nos"):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index fa5938d5226..90c41f8e3f2 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -952,7 +952,12 @@ class update_entries_after(object):
get_rate_for_return, # don't move this import to top
)
- if self.valuation_method == "Moving Average":
+ if (
+ self.valuation_method == "Moving Average"
+ and not sle.get("serial_no")
+ and not sle.get("batch_no")
+ and not sle.get("serial_and_batch_bundle")
+ ):
rate = get_incoming_rate(
{
"item_code": sle.item_code,
@@ -979,6 +984,18 @@ class update_entries_after(object):
voucher_detail_no=sle.voucher_detail_no,
sle=sle,
)
+
+ if (
+ sle.get("serial_and_batch_bundle")
+ and rate > 0
+ and sle.voucher_type in ["Delivery Note", "Sales Invoice"]
+ ):
+ frappe.db.set_value(
+ sle.voucher_type + " Item",
+ sle.voucher_detail_no,
+ "incoming_rate",
+ rate,
+ )
elif (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no