Merge pull request #40280 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
Deepesh Garg
2024-03-06 09:00:14 +05:30
committed by GitHub
39 changed files with 838 additions and 317 deletions

View File

@@ -11,6 +11,10 @@ from frappe.model import core_doctypes_list
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr from frappe.utils import cstr
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
get_allowed_types_from_settings,
)
class AccountingDimension(Document): class AccountingDimension(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -106,6 +110,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
doc_count = len(get_accounting_dimensions()) doc_count = len(get_accounting_dimensions())
count = 0 count = 0
repostable_doctypes = get_allowed_types_from_settings()
for doctype in doclist: for doctype in doclist:
@@ -121,6 +126,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"options": doc.document_type, "options": doc.document_type,
"insert_after": insert_after_field, "insert_after": insert_after_field,
"owner": "Administrator", "owner": "Administrator",
"allow_on_submit": 1 if doctype in repostable_doctypes else 0,
} }
meta = frappe.get_meta(doctype, cached=False) meta = frappe.get_meta(doctype, cached=False)

View File

@@ -57,7 +57,9 @@ class BankAccount(Document):
def validate_account(self): def validate_account(self):
if self.account: 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( frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format( _("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account), frappe.bold(self.account),

View File

@@ -3,16 +3,21 @@
frappe.ui.form.on('Cost Center Allocation', { frappe.ui.form.on('Cost Center Allocation', {
setup: function(frm) { 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() { frm.set_query('main_cost_center', function() {
return { 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
}
}; };
}); });
} }

View File

@@ -14,6 +14,25 @@ frappe.ui.form.on("Journal Entry", {
refresh: function(frm) { refresh: function(frm) {
erpnext.toggle_naming_series(); 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) { if(frm.doc.docstatus > 0) {
frm.add_custom_button(__('Ledger'), function() { frm.add_custom_button(__('Ledger'), function() {
frappe.route_options = { frappe.route_options = {
@@ -184,7 +203,6 @@ var update_jv_details = function(doc, r) {
$.each(r, function(i, d) { $.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); 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, "account", d.account)
frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
}); });
refresh_field("accounts"); refresh_field("accounts");
} }
@@ -193,7 +211,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
onload() { onload() {
this.load_defaults(); this.load_defaults();
this.setup_queries(); this.setup_queries();
this.setup_balance_formatter();
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); 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 ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
return "<div style='text-align: right'>"
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ " " + dr_or_cr
+ "</div>";
};
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) { reference_name(doc, cdt, cdn) {
var d = frappe.get_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(!d.account && d.party_type && d.party) {
if(!frm.doc.company) frappe.throw(__("Please select Company")); if(!frm.doc.company) frappe.throw(__("Please select Company"));
return frm.call({ 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, child: d,
args: { args: {
company: frm.doc.company, company: frm.doc.company,
party_type: d.party_type, party_type: d.party_type,
party: d.party, party: d.party,
cost_center: d.cost_center
} }
}); });
} }
}, },
cost_center: function(frm, dt, dn) { 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) { 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) { debit_in_account_currency: function(frm, cdt, cdn) {
@@ -600,14 +603,14 @@ $.extend(erpnext.journal_entry, {
}); });
$.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]; var d = locals[dt][dn];
if(d.account) { if(d.account) {
if(!frm.doc.company) frappe.throw(__("Please select Company first")); if(!frm.doc.company) frappe.throw(__("Please select Company first"));
if(!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first")); if(!frm.doc.posting_date) frappe.throw(__("Please select Posting Date first"));
return frappe.call({ 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: { args: {
account: d.account, account: d.account,
date: frm.doc.posting_date, date: frm.doc.posting_date,
@@ -615,7 +618,6 @@ $.extend(erpnext.journal_entry, {
debit: flt(d.debit_in_account_currency), debit: flt(d.debit_in_account_currency),
credit: flt(d.credit_in_account_currency), credit: flt(d.credit_in_account_currency),
exchange_rate: d.exchange_rate, exchange_rate: d.exchange_rate,
cost_center: d.cost_center
}, },
callback: function(r) { callback: function(r) {
if(r.message) { if(r.message) {

View File

@@ -64,7 +64,8 @@
"stock_entry", "stock_entry",
"subscription_section", "subscription_section",
"auto_repeat", "auto_repeat",
"amended_from" "amended_from",
"repost_required"
], ],
"fields": [ "fields": [
{ {
@@ -543,6 +544,15 @@
"label": "Is System Generated", "label": "Is System Generated",
"no_copy": 1, "no_copy": 1,
"read_only": 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", "icon": "fa fa-file-text",

View File

@@ -13,6 +13,10 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
get_party_account_based_on_invoice_discounting, 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 ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details, get_party_tax_withholding_details,
) )
@@ -140,7 +144,6 @@ class JournalEntry(AccountsController):
self.set_print_format_fields() self.set_print_format_fields()
self.validate_credit_debit_note() self.validate_credit_debit_note()
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts() self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type() self.validate_depr_entry_voucher_type()
@@ -150,6 +153,10 @@ class JournalEntry(AccountsController):
if not self.title: if not self.title:
self.title = self.get_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): def submit(self):
if len(self.accounts) > 100: if len(self.accounts) > 100:
msgprint(_("The task has been enqueued as a background job."), alert=True) 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_inter_company_jv()
self.update_invoice_discounting() 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): def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller # References for this Journal are removed on the `on_cancel` event in accounts_controller
super(JournalEntry, self).on_cancel() super(JournalEntry, self).on_cancel()
@@ -559,17 +575,28 @@ class JournalEntry(AccountsController):
elif d.party_type == "Supplier" and flt(d.credit) > 0: elif d.party_type == "Supplier" and flt(d.credit) > 0:
frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx)) 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): def validate_against_jv(self):
for d in self.get("accounts"): for d in self.get("accounts"):
if d.reference_type == "Journal Entry": if d.reference_type == "Journal Entry":
account_root_type = frappe.get_cached_value("Account", d.account, "root_type") 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( frappe.throw(
_( _(
"Row #{0}: For {1}, you can select reference document only if account gets credited" "Row #{0}: For {1}, you can select reference document only if account gets credited"
).format(d.idx, d.account) ).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( frappe.throw(
_( _(
"Row #{0}: For {1}, you can select reference document only if account gets debited" "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: for jvd in against_entries:
if flt(jvd[dr_or_cr]) > 0: if flt(jvd[dr_or_cr]) > 0:
valid = True valid = True
if not valid: if not valid and not self.system_generated_gain_loss():
frappe.throw( frappe.throw(
_("Against Journal Entry {0} does not have any unmatched {1} entry").format( _("Against Journal Entry {0} does not have any unmatched {1} entry").format(
d.reference_name, dr_or_cr d.reference_name, dr_or_cr
@@ -1149,21 +1176,6 @@ class JournalEntry(AccountsController):
if not self.get("accounts"): if not self.get("accounts"):
frappe.throw(_("Accounts table cannot be blank.")) 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() @frappe.whitelist()
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None): 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_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"),
"account_currency": args.get("party_account_currency") "account_currency": args.get("party_account_currency")
or get_account_currency(args.get("party_account")), 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, "exchange_rate": exchange_rate,
args.get("amount_field_party"): args.get("amount"), args.get("amount_field_party"): args.get("amount"),
"is_advance": args.get("is_advance"), "is_advance": args.get("is_advance"),
@@ -1478,30 +1488,23 @@ def get_outstanding(args):
@frappe.whitelist() @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"): if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1) frappe.msgprint(_("No Permission"), raise_exception=1)
account = get_party_account(party_type, party, company) 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 { return {
"account": account, "account": account,
"balance": account_balance,
"party_balance": party_balance,
"account_currency": frappe.get_cached_value("Account", account, "account_currency"), "account_currency": frappe.get_cached_value("Account", account, "account_currency"),
} }
@frappe.whitelist() @frappe.whitelist()
def get_account_balance_and_party_type( def get_account_details_and_party_type(
account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None 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"): if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1) frappe.msgprint(_("No Permission"), raise_exception=1)
@@ -1521,7 +1524,6 @@ def get_account_balance_and_party_type(
party_type = "" party_type = ""
grid_values = { grid_values = {
"balance": get_balance_on(account, date, cost_center=cost_center),
"party_type": party_type, "party_type": party_type,
"account_type": account_details.account_type, "account_type": account_details.account_type,
"account_currency": account_details.account_currency or company_currency, "account_currency": account_details.account_currency or company_currency,

View File

@@ -166,43 +166,37 @@ class TestJournalEntry(unittest.TestCase):
jv.get("accounts")[1].credit_in_account_currency = 5000 jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit() jv.submit()
gl_entries = frappe.db.sql( self.voucher_no = jv.name
"""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.assertTrue(gl_entries) self.fields = [
"account",
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
]
expected_values = { self.expected_gle = [
"_Test Bank USD - _TC": { {
"account_currency": "USD", "account": "_Test Bank - _TC",
"debit": 5000,
"debit_in_account_currency": 100,
"credit": 0,
"credit_in_account_currency": 0,
},
"_Test Bank - _TC": {
"account_currency": "INR", "account_currency": "INR",
"debit": 0, "debit": 0,
"debit_in_account_currency": 0, "debit_in_account_currency": 0,
"credit": 5000, "credit": 5000,
"credit_in_account_currency": 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 ( self.check_gl_entries()
"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])
# cancel # cancel
jv.cancel() jv.cancel()
@@ -228,43 +222,37 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate() rjv.posting_date = nowdate()
rjv.submit() rjv.submit()
gl_entries = frappe.db.sql( self.voucher_no = rjv.name
"""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.assertTrue(gl_entries) self.fields = [
"account",
"account_currency",
"debit",
"credit",
"debit_in_account_currency",
"credit_in_account_currency",
]
expected_values = { self.expected_gle = [
"_Test Bank USD - _TC": { {
"account": "_Test Bank USD - _TC",
"account_currency": "USD", "account_currency": "USD",
"debit": 0, "debit": 0,
"debit_in_account_currency": 0, "debit_in_account_currency": 0,
"credit": 5000, "credit": 5000,
"credit_in_account_currency": 100, "credit_in_account_currency": 100,
}, },
"Sales - _TC": { {
"account": "Sales - _TC",
"account_currency": "INR", "account_currency": "INR",
"debit": 5000, "debit": 5000,
"debit_in_account_currency": 5000, "debit_in_account_currency": 5000,
"credit": 0, "credit": 0,
"credit_in_account_currency": 0, "credit_in_account_currency": 0,
}, },
} ]
for field in ( self.check_gl_entries()
"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])
def test_disallow_change_in_account_currency_for_a_party(self): def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD # create jv in USD
@@ -344,23 +332,25 @@ class TestJournalEntry(unittest.TestCase):
jv.insert() jv.insert()
jv.submit() jv.submit()
expected_values = { self.voucher_no = jv.name
"_Test Cash - _TC": {"cost_center": cost_center},
"_Test Bank - _TC": {"cost_center": cost_center},
}
gl_entries = frappe.db.sql( self.fields = [
"""select account, cost_center, debit, credit "account",
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s "cost_center",
order by account asc""", ]
jv.name,
as_dict=1,
)
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.check_gl_entries()
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_jv_with_project(self): def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
@@ -387,23 +377,22 @@ class TestJournalEntry(unittest.TestCase):
jv.insert() jv.insert()
jv.submit() jv.submit()
expected_values = { self.voucher_no = jv.name
"_Test Cash - _TC": {"project": project_name},
"_Test Bank - _TC": {"project": project_name},
}
gl_entries = frappe.db.sql( self.fields = ["account", "project"]
"""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.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.check_gl_entries()
self.assertEqual(expected_values[gle.account]["project"], gle.project)
def test_jv_account_and_party_balance_with_cost_centre(self): def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center 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) account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
self.assertEqual(expected_account_balance, account_balance) 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( def make_journal_entry(
account1, account1,

View File

@@ -9,12 +9,10 @@
"field_order": [ "field_order": [
"account", "account",
"account_type", "account_type",
"balance",
"col_break1", "col_break1",
"bank_account", "bank_account",
"party_type", "party_type",
"party", "party",
"party_balance",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
@@ -64,17 +62,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"fieldname": "balance", "allow_on_submit": 1,
"fieldtype": "Currency",
"label": "Account Balance",
"no_copy": 1,
"oldfieldname": "balance",
"oldfieldtype": "Data",
"options": "account_currency",
"print_hide": 1,
"read_only": 1
},
{
"default": ":Company", "default": ":Company",
"description": "If Income or Expense", "description": "If Income or Expense",
"fieldname": "cost_center", "fieldname": "cost_center",
@@ -107,14 +95,6 @@
"label": "Party", "label": "Party",
"options": "party_type" "options": "party_type"
}, },
{
"fieldname": "party_balance",
"fieldtype": "Currency",
"label": "Party Balance",
"options": "account_currency",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "currency_section", "fieldname": "currency_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -223,6 +203,7 @@
"no_copy": 1 "no_copy": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
@@ -287,7 +268,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-23 11:44:25.841187", "modified": "2024-02-05 01:10:50.224840",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@@ -389,7 +389,10 @@ class PaymentEntry(AccountsController):
) )
def set_missing_ref_details( 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: ) -> None:
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount: if d.allocated_amount:
@@ -401,6 +404,8 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details( ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency 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(): for field, value in ref_details.items():
if d.exchange_gain_loss: if d.exchange_gain_loss:

View File

@@ -631,7 +631,12 @@ class PaymentReconciliation(Document):
journals_map = frappe._dict( journals_map = frappe._dict(
frappe.db.get_all( frappe.db.get_all(
"Journal Entry Account", "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=[ fields=[
"parent as `name`", "parent as `name`",
"exchange_rate", "exchange_rate",

View File

@@ -56,6 +56,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.expense_account = "Cost of Goods Sold - _PR" self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR" self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR" self.creditors = "Creditors - _PR"
self.cash = "Cash - _PR"
# create bank account # create bank account
if frappe.db.exists("Account", "HDFC - _PR"): 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("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 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): def test_journal_against_invoice(self):
transaction_date = nowdate() transaction_date = nowdate()
amount = 100 amount = 100
@@ -1248,3 +1334,17 @@ def make_customer(customer_name, currency=None):
return customer.name return customer.name
else: else:
return customer_name 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

View File

@@ -731,6 +731,7 @@ class PurchaseInvoice(BuyingController):
"cash_bank_account", "cash_bank_account",
"write_off_account", "write_off_account",
"unrealized_profit_loss_account", "unrealized_profit_loss_account",
"is_opening",
] ]
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) 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 provisional_accounting_for_non_stock_items:
if item.purchase_receipt: if item.purchase_receipt:
provisional_account = frappe.db.get_value( provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
"Purchase Receipt Item", item.pr_detail, "provisional_expense_account" "Purchase Receipt Item",
) or self.get_company_default("default_provisional_account") 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) purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc: if not purchase_receipt_doc:
@@ -1042,13 +1048,18 @@ class PurchaseInvoice(BuyingController):
"voucher_detail_no": item.pr_detail, "voucher_detail_no": item.pr_detail,
"account": provisional_account, "account": provisional_account,
}, },
["name"], "name",
) )
if expense_booked_in_pr: if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing # Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry( 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(): if not self.is_internal_transfer():

View File

@@ -1529,18 +1529,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self): def test_provisional_accounting_entry(self):
create_item("_Test Non Stock Item", is_stock_item=0) setup_provisional_accounting()
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()
pr = make_purchase_receipt( pr = make_purchase_receipt(
item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2) 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 self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
) )
company.enable_provisional_accounting_for_non_stock_items = 0 toggle_provisional_accounting_setting()
company.save()
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): def test_adjust_incoming_rate(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) 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 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") test_records = frappe.get_test_records("Purchase Invoice")

View File

@@ -721,6 +721,7 @@ class SalesInvoice(SellingController):
"write_off_account", "write_off_account",
"loyalty_redemption_account", "loyalty_redemption_account",
"unrealized_profit_loss_account", "unrealized_profit_loss_account",
"is_opening",
] ]
child_tables = { child_tables = {
"items": ("income_account", "expense_account", "discount_account"), "items": ("income_account", "expense_account", "discount_account"),

View File

@@ -3602,6 +3602,33 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
set_advance_flag(company="_Test Company", flag=0, default_account="") 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): def set_advance_flag(company, flag, default_account):
frappe.db.set_value( frappe.db.set_value(

View File

@@ -546,6 +546,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
"GL Entry", "GL Entry",
{ {
"is_cancelled": 0, "is_cancelled": 0,
"party_type": "Customer",
"party": ["in", parties], "party": ["in", parties],
"company": inv.company, "company": inv.company,
"voucher_no": ["in", vouchers], "voucher_no": ["in", vouchers],
@@ -560,6 +561,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions = [] conditions = []
conditions.append(ple.amount.lt(0)) conditions.append(ple.amount.lt(0))
conditions.append(ple.delinked == 0) conditions.append(ple.delinked == 0)
conditions.append(ple.party_type == "Customer")
conditions.append(ple.party.isin(parties)) conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no) conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company) conditions.append(ple.company == inv.company)
@@ -579,6 +581,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
{ {
"is_cancelled": 0, "is_cancelled": 0,
"credit": [">", 0], "credit": [">", 0],
"party_type": "Customer",
"party": ["in", parties], "party": ["in", parties],
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)], "posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"company": inv.company, "company": inv.company,

View File

@@ -714,7 +714,7 @@ def update_reference_in_payment_entry(
payment_entry.setup_party_account_field() payment_entry.setup_party_account_field()
payment_entry.set_missing_values() payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe: 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.set_amounts()
payment_entry.make_exchange_gain_loss_journal( payment_entry.make_exchange_gain_loss_journal(
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict

View File

@@ -63,7 +63,7 @@
}, },
{ {
"columns": 1, "columns": 1,
"fetch_from": "stock_item_code.stock_uom", "fetch_from": "item_code.stock_uom",
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@@ -110,7 +110,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-09-08 15:52:08.598100", "modified": "2024-03-05 11:23:40.766844",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Capitalization Service Item", "name": "Asset Capitalization Service Item",
@@ -118,5 +118,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -65,7 +65,7 @@
}, },
{ {
"columns": 1, "columns": 1,
"fetch_from": "stock_item_code.stock_uom", "fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@@ -178,7 +178,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-02-25 15:57:35.007501", "modified": "2024-03-05 11:22:57.346889",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Capitalization Stock Item", "name": "Asset Capitalization Stock Item",

View File

@@ -46,6 +46,7 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import ( from erpnext.accounts.utils import (
create_gain_loss_journal, create_gain_loss_journal,
get_account_currency, get_account_currency,
get_currency_precision,
get_fiscal_years, get_fiscal_years,
validate_fiscal_year, validate_fiscal_year,
) )
@@ -1133,21 +1134,24 @@ class AccountsController(TransactionBase):
self.append("advances", advance_row) self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True): def get_advance_entries(self, include_unallocated=True):
party_account = []
if self.doctype == "Sales Invoice": if self.doctype == "Sales Invoice":
party_type = "Customer" party_type = "Customer"
party = self.customer party = self.customer
amount_field = "credit_in_account_currency" amount_field = "credit_in_account_currency"
order_field = "sales_order" order_field = "sales_order"
order_doctype = "Sales Order" order_doctype = "Sales Order"
party_account.append(self.debit_to)
else: else:
party_type = "Supplier" party_type = "Supplier"
party = self.supplier party = self.supplier
amount_field = "debit_in_account_currency" amount_field = "debit_in_account_currency"
order_field = "purchase_order" order_field = "purchase_order"
order_doctype = "Purchase Order" order_doctype = "Purchase Order"
party_account.append(self.credit_to)
party_account = get_party_account( party_account.extend(
party_type, party=party, company=self.company, include_advance=True 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))) 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. # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
# and below logic is only for such scenarios # and below logic is only for such scenarios
if args: if args:
precision = get_currency_precision()
for arg in args: for arg in args:
# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
if ( 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"): ) and arg.get("difference_account"):
party_account = arg.get("account") party_account = arg.get("account")
@@ -2413,27 +2419,20 @@ class AccountsController(TransactionBase):
doc_before_update = self.get_doc_before_save() doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated # Parent Level Accounts excluding party account
needs_repost = doc_before_update.get("is_opening") != self.is_opening 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: # Check for child tables
# Parent Level Accounts excluding party account for table in child_tables:
fields_to_check += accounting_dimensions if check_if_child_table_updated(
for field in fields_to_check: doc_before_update.get(table), self.get(table), child_tables[table]
if doc_before_update.get(field) != self.get(field): ):
needs_repost = 1 return True
break
if not needs_repost: return False
# 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
@frappe.whitelist() @frappe.whitelist()
def repost_accounting_entries(self): 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( def check_if_child_table_updated(
child_table_before_update, child_table_after_update, fields_to_check child_table_before_update, child_table_after_update, fields_to_check
): ):
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] fields_to_check = list(fields_to_check) + 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
for dimension in accounting_dimensions: # Check if any field affecting accounting entry is altered
if child_table_before_update[index].get(dimension) != item.get(dimension): 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 True
return False return False

View File

@@ -56,7 +56,8 @@ class TestAccountsController(FrappeTestCase):
20 series - Sales Invoice against Journals 20 series - Sales Invoice against Journals
30 series - Sales Invoice against Credit Notes 30 series - Sales Invoice against Credit Notes
40 series - Company default Cost center is unset 40 series - Company default Cost center is unset
50 series - Dimension inheritence 50 series = Journals against Journals
90 series - Dimension inheritence
""" """
def setUp(self): def setUp(self):
@@ -1204,7 +1205,7 @@ class TestAccountsController(FrappeTestCase):
x.mandatory_for_pl = False x.mandatory_for_pl = False
loc.save() loc.save()
def test_50_dimensions_filter(self): def test_90_dimensions_filter(self):
""" """
Test workings of dimension filters Test workings of dimension filters
""" """
@@ -1275,7 +1276,7 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1) 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() self.setup_dimensions()
rate_in_account_currency = 1 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"), 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 # Sales Invoice in Foreign Currency
self.setup_dimensions() self.setup_dimensions()
rate = 80 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() self.setup_dimensions()
dpt = "Research & Development" dpt = "Research & Development"
@@ -1400,3 +1401,70 @@ class TestAccountsController(FrappeTestCase):
pluck="department", 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, [])

View File

@@ -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") 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.update_total_asset_cost_field
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool 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 # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20

View File

@@ -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)

View File

@@ -35,6 +35,14 @@ frappe.ui.form.on("Project", {
}; };
}); });
frm.set_query("department", function (doc) {
return {
filters: {
"company": doc.company,
}
};
});
// sales order // sales order
frm.set_query('sales_order', function () { frm.set_query('sales_order', function () {
var filters = { var filters = {

View File

@@ -22,7 +22,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
item_rate = flt(item.rate_with_margin , precision("rate", item)); 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; item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
} }

View File

@@ -1163,6 +1163,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}, },
callback: function(r) { callback: function(r) {
if(!r.exc) { if(!r.exc) {
me.apply_price_list(item, true)
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor); 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) { 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){ if (item.pricing_rules){
let me = this; let me = this;
return this.frm.call({ return this.frm.call({
@@ -1533,7 +1536,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}, },
callback: function(r) { callback: function(r) {
if (!r.exc && r.message) { 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(); me.calculate_taxes_and_totals();
if(me.frm.doc.apply_discount_on) me.frm.trigger("apply_discount_on"); 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; let me = this;
const fields = ["discount_percentage", const fields = ["discount_percentage",
"discount_amount", "margin_rate_or_amount", "rate_with_margin"]; "discount_amount", "margin_rate_or_amount", "rate_with_margin"];
@@ -1800,7 +1803,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let items = []; let items = [];
me.frm.doc.items.forEach(d => { 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); items.push(d);
} }
}); });

View File

@@ -249,7 +249,8 @@ class SalesOrder(SellingController):
frappe.msgprint( frappe.msgprint(
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
frappe.bold(so[0][0]), frappe.bold(self.po_no) frappe.bold(so[0][0]), frappe.bold(self.po_no)
) ),
alert=True,
) )
else: else:
frappe.throw( frappe.throw(

View File

@@ -20,7 +20,9 @@ default_mail_footer = """<div style="padding: 7px; text-align: right; color: #88
def after_install(): def after_install():
frappe.get_doc({"doctype": "Role", "role_name": "Analytics"}).insert() if not frappe.db.exists("Role", "Analytics"):
frappe.get_doc({"doctype": "Role", "role_name": "Analytics"}).insert()
set_single_defaults() set_single_defaults()
create_print_setting_custom_fields() create_print_setting_custom_fields()
add_all_roles_to("Administrator") add_all_roles_to("Administrator")

View File

@@ -8,9 +8,14 @@ from pypika import Order
class DeprecatedSerialNoValuation: class DeprecatedSerialNoValuation:
@deprecated @deprecated
def calculate_stock_value_from_deprecarated_ledgers(self): def calculate_stock_value_from_deprecarated_ledgers(self):
serial_nos = list( if not frappe.db.get_value(
filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos()) "Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
) ):
return
serial_nos = self.get_filterd_serial_nos()
if not serial_nos:
return
actual_qty = flt(self.sle.actual_qty) actual_qty = flt(self.sle.actual_qty)
@@ -22,26 +27,28 @@ class DeprecatedSerialNoValuation:
self.stock_value_change += stock_value_change self.stock_value_change += stock_value_change
def get_filterd_serial_nos(self):
serial_nos = []
non_filtered_serial_nos = self.get_serial_nos()
# If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered
for serial_no in non_filtered_serial_nos:
if serial_no and serial_no not in self.serial_no_incoming_rate:
serial_nos.append(serial_no)
return serial_nos
@deprecated @deprecated
def get_incoming_value_for_serial_nos(self, serial_nos): def get_incoming_value_for_serial_nos(self, serial_nos):
from erpnext.stock.utils import get_combine_datetime
# get rate from serial nos within same company # get rate from serial nos within same company
all_serial_nos = frappe.get_all(
"Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
)
incoming_values = 0.0 incoming_values = 0.0
for d in all_serial_nos: for serial_no in serial_nos:
if d.company == self.sle.company:
self.serial_no_incoming_rate[d.name] += flt(d.purchase_rate)
incoming_values += flt(d.purchase_rate)
# Get rate for serial nos which has been transferred to other company
invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company]
for serial_no in invalid_serial_nos:
table = frappe.qb.DocType("Stock Ledger Entry") table = frappe.qb.DocType("Stock Ledger Entry")
incoming_rate = ( stock_ledgers = (
frappe.qb.from_(table) frappe.qb.from_(table)
.select(table.incoming_rate) .select(table.incoming_rate, table.actual_qty, table.stock_value_difference)
.where( .where(
( (
(table.serial_no == serial_no) (table.serial_no == serial_no)
@@ -50,16 +57,21 @@ class DeprecatedSerialNoValuation:
| (table.serial_no.like("%\n" + serial_no + "\n%")) | (table.serial_no.like("%\n" + serial_no + "\n%"))
) )
& (table.company == self.sle.company) & (table.company == self.sle.company)
& (table.warehouse == self.sle.warehouse)
& (table.serial_and_batch_bundle.isnull()) & (table.serial_and_batch_bundle.isnull())
& (table.actual_qty > 0) & (table.actual_qty > 0)
& (table.is_cancelled == 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) .limit(1)
).run() ).run(as_dict=1)
self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0 for sle in stock_ledgers:
incoming_values += self.serial_no_incoming_rate[serial_no] self.serial_no_incoming_rate[serial_no] += flt(sle.incoming_rate)
incoming_values += self.serial_no_incoming_rate[serial_no]
return incoming_values return incoming_values

View File

@@ -28,7 +28,6 @@
"column_break_18", "column_break_18",
"project", "project",
"dimension_col_break", "dimension_col_break",
"custom_dimensions_section",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@@ -927,7 +926,7 @@
"width": "50%" "width": "50%"
}, },
{ {
"fetch_from": "transporter.name", "fetch_from": "transporter.supplier_name",
"fieldname": "transporter_name", "fieldname": "transporter_name",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Transporter Name", "label": "Transporter Name",
@@ -1355,10 +1354,6 @@
"fieldname": "column_break_43", "fieldname": "column_break_43",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "custom_dimensions_section",
"fieldtype": "Section Break"
},
{ {
"fieldname": "shipping_address_section", "fieldname": "shipping_address_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -1402,7 +1397,7 @@
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-12-18 17:19:39.368239", "modified": "2024-03-05 11:58:47.784349",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -18,8 +18,7 @@ frappe.listview_settings['Item'] = {
reports: [ reports: [
{ {
name: 'Stock Summary', name: 'Stock Summary',
report_type: 'Page', route: '/app/stock-balance'
route: 'stock-balance'
}, },
{ {
name: 'Stock Ledger', name: 'Stock Ledger',

View File

@@ -415,23 +415,6 @@ class TestLandedCostVoucher(FrappeTestCase):
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + 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_value_difference = frappe.db.get_value(
"Stock Ledger Entry", "Stock Ledger Entry",
filters={ filters={

View File

@@ -727,16 +727,19 @@ class PurchaseReceipt(BuyingController):
) )
def add_provisional_gl_entry( 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) credit_currency = get_account_currency(provisional_account)
expense_account = item.expense_account expense_account = item.expense_account
debit_currency = get_account_currency(item.expense_account) debit_currency = get_account_currency(item.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service") remarks = self.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1 multiplication_factor = 1
amount = item.base_amount
if reverse: if reverse:
multiplication_factor = -1 multiplication_factor = -1
# Post reverse entry for previously posted amount
amount = item_amount
expense_account = frappe.db.get_value( expense_account = frappe.db.get_value(
"Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"] "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
) )
@@ -746,7 +749,7 @@ class PurchaseReceipt(BuyingController):
account=provisional_account, account=provisional_account,
cost_center=item.cost_center, cost_center=item.cost_center,
debit=0.0, debit=0.0,
credit=multiplication_factor * item.base_amount, credit=multiplication_factor * amount,
remarks=remarks, remarks=remarks,
against_account=expense_account, against_account=expense_account,
account_currency=credit_currency, account_currency=credit_currency,
@@ -760,7 +763,7 @@ class PurchaseReceipt(BuyingController):
gl_entries=gl_entries, gl_entries=gl_entries,
account=expense_account, account=expense_account,
cost_center=item.cost_center, cost_center=item.cost_center,
debit=multiplication_factor * item.base_amount, debit=multiplication_factor * amount,
credit=0.0, credit=0.0,
remarks=remarks, remarks=remarks,
against_account=provisional_account, against_account=provisional_account,

View File

@@ -11,8 +11,7 @@ frappe.listview_settings['Putaway Rule'] = {
reports: [ reports: [
{ {
name: 'Warehouse Capacity Summary', name: 'Warehouse Capacity Summary',
report_type: 'Page', route: '/app/warehouse-capacity-summary'
route: 'warehouse-capacity-summary'
} }
] ]
}; };

View File

@@ -219,14 +219,9 @@ class RepostItemValuation(Document):
if self.status not in ("Queued", "In Progress"): if self.status not in ("Queued", "In Progress"):
return return
if not (self.voucher_no and self.voucher_no): msg = _("Cannot cancel as processing of cancelled documents is pending.")
return msg += "<br>" + _("Please try again in an hour.")
frappe.throw(msg, title=_("Pending processing"))
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 += "<br>" + _("Please try again in an hour.")
frappe.throw(msg, title=_("Pending processing"))
@frappe.whitelist() @frappe.whitelist()
def restart_reposting(self): def restart_reposting(self):

View File

@@ -332,13 +332,8 @@ class SerialandBatchBundle(Document):
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
for d in self.entries: 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) 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) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
if save: 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) 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": if type_of_transaction != "Inward":
frappe.throw(_("Batch No {0} does not exists").format(batch_no)) frappe.throw(_("Batch No {0} does not exists").format(batch_no))

View File

@@ -540,6 +540,110 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertRaises(frappe.exceptions.ValidationError, pr2.save) 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): def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@@ -4,8 +4,9 @@ from typing import List
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.naming import make_autoname 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 frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
from pypika import Order
from erpnext.stock.deprecated_serial_batch import ( from erpnext.stock.deprecated_serial_batch import (
DeprecatedBatchNoValuation, DeprecatedBatchNoValuation,
@@ -424,19 +425,21 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
) )
else: else:
entries = self.get_serial_no_ledgers()
self.serial_no_incoming_rate = defaultdict(float) self.serial_no_incoming_rate = defaultdict(float)
self.stock_value_change = 0.0 self.stock_value_change = 0.0
for ledger in entries: serial_nos = self.get_serial_nos()
self.stock_value_change += ledger.incoming_rate for serial_no in serial_nos:
self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate 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() self.calculate_stock_value_from_deprecarated_ledgers()
def get_serial_no_ledgers(self): def get_incoming_rate_from_bundle(self, serial_no) -> float:
serial_nos = self.get_serial_nos()
bundle = frappe.qb.DocType("Serial and Batch Bundle") bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry") bundle_child = frappe.qb.DocType("Serial and Batch Entry")
@@ -444,20 +447,18 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
frappe.qb.from_(bundle) frappe.qb.from_(bundle)
.inner_join(bundle_child) .inner_join(bundle_child)
.on(bundle.name == bundle_child.parent) .on(bundle.name == bundle_child.parent)
.select( .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
bundle.name,
bundle_child.serial_no,
(bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
)
.where( .where(
(bundle.is_cancelled == 0) (bundle.is_cancelled == 0)
& (bundle.docstatus == 1) & (bundle.docstatus == 1)
& (bundle_child.serial_no.isin(serial_nos)) & (bundle_child.serial_no == serial_no)
& (bundle.type_of_transaction.isin(["Inward", "Outward"])) & (bundle.type_of_transaction == "Inward")
& (bundle_child.qty > 0)
& (bundle.item_code == self.sle.item_code) & (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse) & (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 # 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) 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): def get_serial_nos(self):
if self.sle.get("serial_nos"): if self.sle.get("serial_nos"):

View File

@@ -952,7 +952,12 @@ class update_entries_after(object):
get_rate_for_return, # don't move this import to top 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( rate = get_incoming_rate(
{ {
"item_code": sle.item_code, "item_code": sle.item_code,
@@ -979,6 +984,18 @@ class update_entries_after(object):
voucher_detail_no=sle.voucher_detail_no, voucher_detail_no=sle.voucher_detail_no,
sle=sle, 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 ( elif (
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and sle.voucher_detail_no and sle.voucher_detail_no