Compare commits

..

1 Commits

Author SHA1 Message Date
Ankush Menat
41f44ba1c6 perf: cache journal entry balances for 5 minutes
If user is repeatedly adding multiple JV lines then everytime the value
will be refetched from database, reading GLE table can be very expensive
on large DBs.

AFAIK this function is only for UX and balances are anyways overidden
when saving the document.

Proper fix: maybe use closing balance here?
2024-03-02 15:53:15 +05:30
42 changed files with 464 additions and 1116 deletions

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "15.16.2"
__version__ = "15.14.3"
def get_default_company(user=None):

View File

@@ -11,10 +11,6 @@ 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
@@ -110,7 +106,6 @@ 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:
@@ -126,7 +121,6 @@ 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)

View File

@@ -3,21 +3,16 @@
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: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query('cost_center', 'allocation_percentages', function() {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
filters: filters
};
});
}

View File

@@ -14,25 +14,6 @@ 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 = {
@@ -203,6 +184,7 @@ 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");
}
@@ -211,6 +193,7 @@ 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);
}
@@ -309,6 +292,19 @@ 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) {
var d = frappe.get_doc(cdt, cdn);
@@ -404,22 +400,23 @@ 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_currency",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_party_account_and_balance",
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_details(frm, dt, dn);
erpnext.journal_entry.set_account_balance(frm, dt, dn);
},
account: function(frm, dt, dn) {
erpnext.journal_entry.set_account_details(frm, dt, dn);
erpnext.journal_entry.set_account_balance(frm, dt, dn);
},
debit_in_account_currency: function(frm, cdt, cdn) {
@@ -603,14 +600,14 @@ $.extend(erpnext.journal_entry, {
});
$.extend(erpnext.journal_entry, {
set_account_details: function(frm, dt, dn) {
set_account_balance: 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_details_and_party_type",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_account_balance_and_party_type",
args: {
account: d.account,
date: frm.doc.posting_date,
@@ -618,6 +615,7 @@ $.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) {

View File

@@ -64,8 +64,7 @@
"stock_entry",
"subscription_section",
"auto_repeat",
"amended_from",
"repost_required"
"amended_from"
],
"fields": [
{
@@ -544,15 +543,6 @@
"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",

View File

@@ -7,16 +7,13 @@ import json
import frappe
from frappe import _, msgprint, scrub
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
from frappe.utils.caching import redis_cache
import erpnext
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,
)
@@ -144,6 +141,7 @@ 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()
@@ -153,10 +151,6 @@ 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)
@@ -180,15 +174,6 @@ 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()
@@ -575,28 +560,17 @@ 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 and not self.system_generated_gain_loss():
if account_root_type == "Asset" and flt(d.debit) > 0:
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
and not self.system_generated_gain_loss()
):
elif account_root_type == "Liability" and flt(d.credit) > 0:
frappe.throw(
_(
"Row #{0}: For {1}, you can select reference document only if account gets debited"
@@ -628,7 +602,7 @@ class JournalEntry(AccountsController):
for jvd in against_entries:
if flt(jvd[dr_or_cr]) > 0:
valid = True
if not valid and not self.system_generated_gain_loss():
if not valid:
frappe.throw(
_("Against Journal Entry {0} does not have any unmatched {1} entry").format(
d.reference_name, dr_or_cr
@@ -1176,6 +1150,21 @@ 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):
@@ -1341,6 +1330,8 @@ 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"),
@@ -1488,23 +1479,31 @@ def get_outstanding(args):
@frappe.whitelist()
def get_party_account_and_currency(company, party_type, party):
@redis_cache(ttl=5 * 60, user=True)
def get_party_account_and_balance(company, party_type, party, cost_center=None):
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_details_and_party_type(
account, date, company, debit=None, credit=None, exchange_rate=None
def get_account_balance_and_party_type(
account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None
):
"""Returns dict of account details and party type to be set in Journal Entry on selection of account."""
"""Returns dict of account balance 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)
@@ -1524,6 +1523,7 @@ def get_account_details_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,

View File

@@ -166,37 +166,43 @@ class TestJournalEntry(unittest.TestCase):
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit()
self.voucher_no = jv.name
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.fields = [
"account",
"account_currency",
"debit",
"debit_in_account_currency",
"credit",
"credit_in_account_currency",
]
self.assertTrue(gl_entries)
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",
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": {
"account_currency": "INR",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 5000,
},
}
self.check_gl_entries()
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])
# cancel
jv.cancel()
@@ -222,37 +228,43 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate()
rjv.submit()
self.voucher_no = rjv.name
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.fields = [
"account",
"account_currency",
"debit",
"credit",
"debit_in_account_currency",
"credit_in_account_currency",
]
self.assertTrue(gl_entries)
self.expected_gle = [
{
"account": "_Test Bank USD - _TC",
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 100,
},
{
"account": "Sales - _TC",
"Sales - _TC": {
"account_currency": "INR",
"debit": 5000,
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
},
]
}
self.check_gl_entries()
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])
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
@@ -332,25 +344,23 @@ class TestJournalEntry(unittest.TestCase):
jv.insert()
jv.submit()
self.voucher_no = jv.name
expected_values = {
"_Test Cash - _TC": {"cost_center": cost_center},
"_Test Bank - _TC": {"cost_center": cost_center},
}
self.fields = [
"account",
"cost_center",
]
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.expected_gle = [
{
"account": "_Test Bank - _TC",
"cost_center": cost_center,
},
{
"account": "_Test Cash - _TC",
"cost_center": cost_center,
},
]
self.assertTrue(gl_entries)
self.check_gl_entries()
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project
@@ -377,22 +387,23 @@ class TestJournalEntry(unittest.TestCase):
jv.insert()
jv.submit()
self.voucher_no = jv.name
expected_values = {
"_Test Cash - _TC": {"project": project_name},
"_Test Bank - _TC": {"project": project_name},
}
self.fields = ["account", "project"]
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.expected_gle = [
{
"account": "_Test Bank - _TC",
"project": project_name,
},
{
"account": "_Test Cash - _TC",
"project": project_name,
},
]
self.assertTrue(gl_entries)
self.check_gl_entries()
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["project"], gle.project)
def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -415,79 +426,6 @@ 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,

View File

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

View File

@@ -731,7 +731,6 @@ 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)
@@ -1024,14 +1023,9 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
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"
)
provisional_account = frappe.db.get_value(
"Purchase Receipt Item", item.pr_detail, "provisional_expense_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:
@@ -1048,18 +1042,13 @@ 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_amount=(min(item.qty, pr_qty) * pr_base_rate),
item, gl_entries, self.posting_date, provisional_account, reverse=1
)
if not self.is_internal_transfer():

View File

@@ -1529,7 +1529,18 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
setup_provisional_accounting()
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()
pr = make_purchase_receipt(
item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)
@@ -1573,97 +1584,8 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
)
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()
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def test_adjust_incoming_rate(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
@@ -2098,92 +2020,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
return_pi.submit()
self.assertEqual(return_pi.docstatus, 1)
def test_purchase_invoice_with_use_serial_batch_field_for_rejected_qty(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
batch_item = make_item(
"_Test Purchase Invoice Batch Item For Rejected Qty",
properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1},
).name
serial_item = make_item(
"_Test Purchase Invoice Serial Item for Rejected Qty",
properties={"has_serial_no": 1, "is_stock_item": 1},
).name
rej_warehouse = create_warehouse("_Test Purchase INV Warehouse For Rejected Qty")
batch_no = "BATCH-PI-BNU-TPRBI-0001"
serial_nos = ["SNU-PI-TPRSI-0001", "SNU-PI-TPRSI-0002", "SNU-PI-TPRSI-0003"]
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": batch_item,
}
).insert()
for serial_no in serial_nos:
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"item_code": serial_item,
"serial_no": serial_no,
}
).insert()
pi = make_purchase_invoice(
item_code=batch_item,
received_qty=10,
qty=8,
rejected_qty=2,
update_stock=1,
rejected_warehouse=rej_warehouse,
use_serial_batch_fields=1,
batch_no=batch_no,
rate=100,
do_not_submit=1,
)
pi.append(
"items",
{
"item_code": serial_item,
"qty": 2,
"rate": 100,
"base_rate": 100,
"item_name": serial_item,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1,
"rejected_qty": 1,
"warehouse": pi.items[0].warehouse,
"rejected_warehouse": rej_warehouse,
"use_serial_batch_fields": 1,
"serial_no": "\n".join(serial_nos[:2]),
"rejected_serial_no": serial_nos[2],
},
)
pi.save()
pi.submit()
pi.reload()
for row in pi.items:
self.assertTrue(row.serial_and_batch_bundle)
self.assertTrue(row.rejected_serial_and_batch_bundle)
if row.item_code == batch_item:
self.assertEqual(row.batch_no, batch_no)
else:
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
self.assertEqual(row.rejected_serial_no, serial_nos[2])
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@@ -2291,7 +2127,7 @@ def make_purchase_invoice(**args):
pi.cost_center = args.parent_cost_center
bundle_id = None
if not args.use_serial_batch_fields and ((args.get("batch_no") or args.get("serial_no"))):
if args.get("batch_no") or args.get("serial_no"):
batches = {}
qty = args.qty or 5
item_code = args.item or args.item_code or "_Test Item"
@@ -2338,9 +2174,6 @@ def make_purchase_invoice(**args):
"rejected_warehouse": args.rejected_warehouse or "",
"asset_location": args.location or "",
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0,
"use_serial_batch_fields": args.get("use_serial_batch_fields") or 0,
"batch_no": args.get("batch_no") if args.get("use_serial_batch_fields") else "",
"serial_no": args.get("serial_no") if args.get("use_serial_batch_fields") else "",
},
)
@@ -2431,26 +2264,4 @@ 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")

View File

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

View File

@@ -546,7 +546,6 @@ 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],
@@ -561,7 +560,6 @@ 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)
@@ -581,7 +579,6 @@ 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,

View File

@@ -63,7 +63,7 @@
},
{
"columns": 1,
"fetch_from": "item_code.stock_uom",
"fetch_from": "stock_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": "2024-03-05 11:23:40.766844",
"modified": "2021-09-08 15:52:08.598100",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Service Item",
@@ -118,6 +118,5 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -65,7 +65,7 @@
},
{
"columns": 1,
"fetch_from": "item_code.stock_uom",
"fetch_from": "stock_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-03-05 11:22:57.346889",
"modified": "2024-02-25 15:57:35.007501",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",

View File

@@ -46,7 +46,6 @@ 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,
)
@@ -1300,12 +1299,10 @@ 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 (
flt(arg.get("difference_amount", 0), precision) != 0
or flt(arg.get("exchange_gain_loss", 0), precision) != 0
arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0
) and arg.get("difference_account"):
party_account = arg.get("account")
@@ -2419,20 +2416,27 @@ class AccountsController(TransactionBase):
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# 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
# Check if opening entry check updated
needs_repost = doc_before_update.get("is_opening") != self.is_opening
# 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:
# 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
return False
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
@frappe.whitelist()
def repost_accounting_entries(self):
@@ -3458,12 +3462,15 @@ 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
):
fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"]
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if any field affecting accounting entry is altered
for index, item in enumerate(child_table_before_update):
for index, item in enumerate(child_table_after_update):
for field in fields_to_check:
if child_table_after_update[index].get(field) != item.get(field):
if child_table_before_update[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if child_table_before_update[index].get(dimension) != item.get(dimension):
return True
return False

View File

@@ -559,7 +559,7 @@ class BuyingController(SubcontractingController):
{
"incoming_rate": incoming_rate,
"recalculate_rate": 1
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse
else 0,
}
)

View File

@@ -434,21 +434,9 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
filtered_batches = get_filterd_batches(batches)
if filters.get("is_inward"):
filtered_batches.extend(get_empty_batches(filters))
return filtered_batches
def get_empty_batches(filters):
return frappe.get_all(
"Batch",
fields=["name", "batch_qty"],
filters={"item": filters.get("item_code"), "batch_qty": 0.0},
as_list=1,
)
def get_filterd_batches(data):
batches = OrderedDict()

View File

@@ -159,6 +159,9 @@ class StockController(AccountsController):
row.serial_no = clean_serial_no_string(row.serial_no)
def make_bundle_using_old_serial_batch_fields(self, table_name=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if self.get("_action") == "update_after_submit":
return
@@ -187,78 +190,58 @@ class StockController(AccountsController):
if row.use_serial_batch_fields and (
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
):
bundle_details = {
"item_code": row.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"use_serial_batch_fields": row.use_serial_batch_fields,
"do_not_submit": True,
}
if self.doctype == "Stock Reconciliation":
qty = row.qty
type_of_transaction = "Inward"
warehouse = row.warehouse
elif table_name == "packed_items":
qty = row.qty
warehouse = row.warehouse
type_of_transaction = "Outward"
if self.is_return:
type_of_transaction = "Inward"
else:
qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty
type_of_transaction = get_type_of_transaction(self, row)
warehouse = (
row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse
)
if row.qty:
self.update_bundle_details(bundle_details, table_name, row)
self.create_serial_batch_bundle(bundle_details, row)
sn_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": qty,
"type_of_transaction": type_of_transaction,
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
"batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
"batch_no": row.batch_no,
"use_serial_batch_fields": row.use_serial_batch_fields,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
if row.get("rejected_qty"):
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
self.create_serial_batch_bundle(bundle_details, row)
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# Since qty field is different for different doctypes
qty = row.get("qty")
warehouse = row.get("warehouse")
if table_name == "packed_items":
type_of_transaction = "Inward"
if not self.is_return:
type_of_transaction = "Outward"
else:
type_of_transaction = get_type_of_transaction(self, row)
if hasattr(row, "stock_qty"):
qty = row.stock_qty
if self.doctype == "Stock Entry":
qty = row.transfer_qty
warehouse = row.s_warehouse or row.t_warehouse
serial_nos = row.serial_no
if is_rejected:
serial_nos = row.get("rejected_serial_no")
type_of_transaction = "Inward" if not self.is_return else "Outward"
qty = row.get("rejected_qty")
warehouse = row.get("rejected_warehouse")
bundle_details.update(
{
"qty": qty,
"is_rejected": is_rejected,
"type_of_transaction": type_of_transaction,
"warehouse": warehouse,
"batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
"serial_nos": get_serial_nos(serial_nos) if serial_nos else None,
"batch_no": row.batch_no,
}
)
def create_serial_batch_bundle(self, bundle_details, row):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
sn_doc = SerialBatchCreation(bundle_details).make_serial_and_batch_bundle()
field = "serial_and_batch_bundle"
if bundle_details.get("is_rejected"):
field = "rejected_serial_and_batch_bundle"
row.set(field, sn_doc.name)
row.db_set({field: sn_doc.name})
if sn_doc.is_rejected:
row.rejected_serial_and_batch_bundle = sn_doc.name
row.db_set(
{
"rejected_serial_and_batch_bundle": sn_doc.name,
}
)
else:
row.serial_and_batch_bundle = sn_doc.name
row.db_set(
{
"serial_and_batch_bundle": sn_doc.name,
}
)
def validate_serial_nos_and_batches_with_bundle(self, row):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View File

@@ -859,7 +859,6 @@ class SubcontractingController(StockController):
item,
{
"warehouse": item.rejected_warehouse,
"serial_and_batch_bundle": item.get("rejected_serial_and_batch_bundle"),
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
"incoming_rate": 0.0,
},

View File

@@ -56,8 +56,7 @@ 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 = Journals against Journals
90 series - Dimension inheritence
50 series - Dimension inheritence
"""
def setUp(self):
@@ -1205,7 +1204,7 @@ class TestAccountsController(FrappeTestCase):
x.mandatory_for_pl = False
loc.save()
def test_90_dimensions_filter(self):
def test_50_dimensions_filter(self):
"""
Test workings of dimension filters
"""
@@ -1276,7 +1275,7 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
def test_91_cr_note_should_inherit_dimension(self):
def test_51_cr_note_should_inherit_dimension(self):
self.setup_dimensions()
rate_in_account_currency = 1
@@ -1318,7 +1317,7 @@ class TestAccountsController(FrappeTestCase):
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
)
def test_92_dimension_inhertiance_exc_gain_loss(self):
def test_52_dimension_inhertiance_exc_gain_loss(self):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate = 80
@@ -1356,7 +1355,7 @@ class TestAccountsController(FrappeTestCase):
),
)
def test_93_dimension_inheritance_on_advance(self):
def test_53_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
@@ -1401,70 +1400,3 @@ 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, [])

View File

@@ -1382,9 +1382,8 @@ class TestWorkOrder(FrappeTestCase):
# Inward raw materials in Stores warehouse
ste_doc.submit()
ste_doc.reload()
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
serial_nos_list = sorted(get_serial_nos(ste_doc.items[0].serial_no))
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc(
@@ -1396,22 +1395,19 @@ class TestWorkOrder(FrappeTestCase):
# First Manufacture stock entry
manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1))
manufacture_ste_doc1.submit()
manufacture_ste_doc1.reload()
# Serial nos should be same as transferred Serial nos
self.assertEqual(
sorted(get_serial_nos_from_bundle(manufacture_ste_doc1.items[0].serial_and_batch_bundle)),
serial_nos_list[0:1],
)
self.assertEqual(get_serial_nos(manufacture_ste_doc1.items[0].serial_no), serial_nos_list[0:1])
self.assertEqual(manufacture_ste_doc1.items[0].qty, 1)
manufacture_ste_doc1.submit()
# Second Manufacture stock entry
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 3))
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2))
# Serial nos should be same as transferred Serial nos
self.assertEqual(get_serial_nos(manufacture_ste_doc2.items[0].serial_no), serial_nos_list[1:4])
self.assertEqual(manufacture_ste_doc2.items[0].qty, 3)
self.assertEqual(get_serial_nos(manufacture_ste_doc2.items[0].serial_no), serial_nos_list[1:3])
self.assertEqual(manufacture_ste_doc2.items[0].qty, 2)
def test_backflushed_serial_no_batch_raw_materials_based_on_transferred(self):
frappe.db.set_single_value(
@@ -1545,9 +1541,19 @@ class TestWorkOrder(FrappeTestCase):
row.qty -= 2
row.transfer_qty -= 2
if row.serial_no:
serial_nos = get_serial_nos(row.serial_no)
row.serial_no = "\n".join(serial_nos[:5])
if not row.serial_and_batch_bundle:
continue
bundle_id = row.serial_and_batch_bundle
bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle_id)
if bundle_doc.has_serial_no:
bundle_doc.set("entries", bundle_doc.entries[0:5])
else:
for bundle_row in bundle_doc.entries:
bundle_row.qty += 2
bundle_doc.save()
bundle_doc.load_from_db()
ste_doc.save()
ste_doc.submit()
@@ -1890,71 +1896,6 @@ class TestWorkOrder(FrappeTestCase):
"Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0}
)
def test_partial_material_consumption_with_batch(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 1)
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
fg_item = make_item(
"Test FG Item For Partial Material Consumption",
{"is_stock_item": 1},
).name
rm_item = make_item(
"Test RM Item For Partial Material Consumption",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TST-BATCH-PMCC-.####",
},
).name
make_bom(
item=fg_item,
source_warehouse="Stores - _TC",
raw_materials=[rm_item],
)
make_stock_entry_test_record(
purpose="Material Receipt",
item_code=rm_item,
target="Stores - _TC",
qty=10,
basic_rate=100,
)
wo_order = make_wo_order_test_record(item=fg_item, qty=10)
stock_entry = frappe.get_doc(
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 10)
)
stock_entry.submit()
stock_entry.reload()
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
stock_entry = frappe.get_doc(
make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 10)
)
self.assertEqual(stock_entry.items[0].batch_no, batch_no)
self.assertEqual(stock_entry.items[0].use_serial_batch_fields, 1)
frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0)
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"BOM",
)
def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)

View File

@@ -353,7 +353,6 @@ 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

View File

@@ -1,14 +0,0 @@
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

@@ -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 && !item.discount_amount) {
if (item.discount_percentage) {
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}

View File

@@ -371,18 +371,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
label: __('Batch No'),
in_list_view: 1,
get_query: () => {
let is_inward = false;
if ((["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) && !this.frm.doc.is_return)
|| (this.frm.doc.doctype === 'Stock Entry' && this.frm.doc.purpose === 'Material Receipt')) {
is_inward = true;
}
return {
query : "erpnext.controllers.queries.get_batch_no",
filters: {
'item_code': this.item.item_code,
'warehouse': this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
'is_inward': is_inward
'warehouse': this.item.s_warehouse || this.item.t_warehouse,
}
}
},

View File

@@ -13,9 +13,7 @@ class DeprecatedSerialNoValuation:
):
return
serial_nos = self.get_filterd_serial_nos()
if not serial_nos:
return
serial_nos = self.get_serial_nos()
actual_qty = flt(self.sle.actual_qty)
@@ -27,21 +25,8 @@ class DeprecatedSerialNoValuation:
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
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
incoming_values = 0.0
for serial_no in serial_nos:
@@ -57,20 +42,18 @@ class DeprecatedSerialNoValuation:
| (table.serial_no.like("%\n" + serial_no + "\n%"))
)
& (table.company == self.sle.company)
& (table.warehouse == self.sle.warehouse)
& (table.serial_and_batch_bundle.isnull())
& (table.actual_qty > 0)
& (table.is_cancelled == 0)
& (
table.posting_datetime <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
)
)
.orderby(table.posting_datetime, order=Order.desc)
.limit(1)
).run(as_dict=1)
for sle in stock_ledgers:
self.serial_no_incoming_rate[serial_no] += flt(sle.incoming_rate)
self.serial_no_incoming_rate[serial_no] += (
flt(sle.incoming_rate)
if sle.actual_qty > 0
else (sle.stock_value_difference / sle.actual_qty) * -1
)
incoming_values += self.serial_no_incoming_rate[serial_no]
return incoming_values

View File

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

View File

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

View File

@@ -415,6 +415,23 @@ 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={

View File

@@ -727,19 +727,16 @@ class PurchaseReceipt(BuyingController):
)
def add_provisional_gl_entry(
self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
self, item, gl_entries, posting_date, provisional_account, reverse=0
):
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"]
)
@@ -749,7 +746,7 @@ class PurchaseReceipt(BuyingController):
account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
credit=multiplication_factor * amount,
credit=multiplication_factor * item.base_amount,
remarks=remarks,
against_account=expense_account,
account_currency=credit_currency,
@@ -763,7 +760,7 @@ class PurchaseReceipt(BuyingController):
gl_entries=gl_entries,
account=expense_account,
cost_center=item.cost_center,
debit=multiplication_factor * amount,
debit=multiplication_factor * item.base_amount,
credit=0.0,
remarks=remarks,
against_account=provisional_account,

View File

@@ -2440,88 +2440,6 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.reload()
self.assertEqual(pr.per_billed, 100)
def test_purchase_receipt_with_use_serial_batch_field_for_rejected_qty(self):
batch_item = make_item(
"_Test Purchase Receipt Batch Item For Rejected Qty",
properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1},
).name
serial_item = make_item(
"_Test Purchase Receipt Serial Item for Rejected Qty",
properties={"has_serial_no": 1, "is_stock_item": 1},
).name
rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty")
batch_no = "BATCH-BNU-TPRBI-0001"
serial_nos = ["SNU-TPRSI-0001", "SNU-TPRSI-0002", "SNU-TPRSI-0003"]
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": batch_item,
}
).insert()
for serial_no in serial_nos:
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"item_code": serial_item,
"serial_no": serial_no,
}
).insert()
pr = make_purchase_receipt(
item_code=batch_item,
received_qty=10,
qty=8,
rejected_qty=2,
rejected_warehouse=rej_warehouse,
use_serial_batch_fields=1,
batch_no=batch_no,
rate=100,
do_not_submit=1,
)
pr.append(
"items",
{
"item_code": serial_item,
"qty": 2,
"rate": 100,
"base_rate": 100,
"item_name": serial_item,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1,
"rejected_qty": 1,
"warehouse": pr.items[0].warehouse,
"rejected_warehouse": rej_warehouse,
"use_serial_batch_fields": 1,
"serial_no": "\n".join(serial_nos[:2]),
"rejected_serial_no": serial_nos[2],
},
)
pr.save()
pr.submit()
pr.reload()
for row in pr.items:
self.assertTrue(row.serial_and_batch_bundle)
self.assertTrue(row.rejected_serial_and_batch_bundle)
if row.item_code == batch_item:
self.assertEqual(row.batch_no, batch_no)
else:
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
self.assertEqual(row.rejected_serial_no, serial_nos[2])
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

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

View File

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

View File

@@ -332,8 +332,13 @@ 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 d.qty:
if self.has_batch_no:
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
if save:
@@ -847,7 +852,7 @@ class SerialandBatchBundle(Document):
available_batches = get_available_batches_qty(available_batches)
for batch_no in batches:
if batch_no in available_batches and available_batches[batch_no] < 0:
if batch_no not in available_batches or available_batches[batch_no] < 0:
if flt(available_batches.get(batch_no)) < 0:
self.validate_negative_batch(batch_no, available_batches[batch_no])
@@ -1263,13 +1268,6 @@ def get_type_of_transaction(parent_doc, child_row):
if parent_doc.get("is_return"):
type_of_transaction = "Inward" if type_of_transaction == "Outward" else "Outward"
if parent_doc.get("doctype") == "Subcontracting Receipt":
type_of_transaction = "Outward"
if child_row.get("doctype") == "Subcontracting Receipt Item":
type_of_transaction = "Inward"
elif parent_doc.get("doctype") == "Stock Reconciliation":
type_of_transaction = "Inward"
return type_of_transaction

View File

@@ -10,7 +10,17 @@ import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
from frappe.utils import (
cint,
comma_or,
cstr,
flt,
format_time,
formatdate,
getdate,
month_diff,
nowdate,
)
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
@@ -227,6 +237,41 @@ class StockEntry(StockController):
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def submit(self):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Draft stage"
)
)
self.queue_action("submit", timeout=2000)
else:
self._submit()
def cancel(self):
if self.is_enqueue_action():
frappe.msgprint(
_(
"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Entry and revert to the Submitted stage"
)
)
self.queue_action("cancel", timeout=2000)
else:
self._cancel()
def is_enqueue_action(self, force=False) -> bool:
if force:
return True
if frappe.flags.in_test:
return False
# If line items are more than 100 or record is older than 6 months
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
return True
return False
def on_submit(self):
self.validate_closed_subcontracting_order()
self.make_bundle_using_old_serial_batch_fields()
@@ -2118,42 +2163,23 @@ class StockEntry(StockController):
if not qty:
return
use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
ste_item_details = {
"from_warehouse": item.warehouse,
"to_warehouse": "",
"qty": qty,
"item_name": item.item_name,
"serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward")
if not use_serial_batch_fields
else "",
"serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward"),
"description": item.description,
"stock_uom": item.stock_uom,
"expense_account": item.expense_account,
"cost_center": item.buying_cost_center,
"original_item": item.original_item,
"serial_no": "\n".join(row.serial_nos)
if row.serial_nos and not row.batches_to_be_consume
else "",
"use_serial_batch_fields": use_serial_batch_fields,
}
if self.is_return:
ste_item_details["to_warehouse"] = item.s_warehouse
if use_serial_batch_fields and not row.serial_no and row.batches_to_be_consume:
for batch_no, batch_qty in row.batches_to_be_consume.items():
ste_item_details.update(
{
"batch_no": batch_no,
"qty": batch_qty,
}
)
self.add_to_stock_entry_detail({item.item_code: ste_item_details})
else:
self.add_to_stock_entry_detail({item.item_code: ste_item_details})
self.add_to_stock_entry_detail({item.item_code: ste_item_details})
@staticmethod
def get_serial_nos_based_on_transferred_batch(batch_no, serial_nos) -> list:
@@ -2304,9 +2330,6 @@ class StockEntry(StockController):
"item_name",
"serial_and_batch_bundle",
"allow_zero_valuation_rate",
"use_serial_batch_fields",
"batch_no",
"serial_no",
]:
if item_row.get(field):
se_child.set(field, item_row.get(field))
@@ -2955,7 +2978,7 @@ def get_available_materials(work_order) -> dict:
if row.batch_no:
item_data.batch_details[row.batch_no] += row.qty
elif row.batch_nos:
if row.batch_nos:
for batch_no, qty in row.batch_nos.items():
item_data.batch_details[batch_no] += qty
@@ -2963,7 +2986,7 @@ def get_available_materials(work_order) -> dict:
item_data.serial_nos.extend(get_serial_nos(row.serial_no))
item_data.serial_nos.sort()
elif row.serial_nos:
if row.serial_nos:
item_data.serial_nos.extend(get_serial_nos(row.serial_nos))
item_data.serial_nos.sort()
else:
@@ -2973,7 +2996,7 @@ def get_available_materials(work_order) -> dict:
if row.batch_no:
item_data.batch_details[row.batch_no] -= row.qty
elif row.batch_nos:
if row.batch_nos:
for batch_no, qty in row.batch_nos.items():
item_data.batch_details[batch_no] += qty
@@ -2981,7 +3004,7 @@ def get_available_materials(work_order) -> dict:
for serial_no in get_serial_nos(row.serial_no):
item_data.serial_nos.remove(serial_no)
elif row.serial_nos:
if row.serial_nos:
for serial_no in get_serial_nos(row.serial_nos):
item_data.serial_nos.remove(serial_no)

View File

@@ -1633,6 +1633,36 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, sr_doc.submit)
def test_enqueue_action(self):
frappe.flags.in_test = False
item_code = "Test Enqueue Item - 001"
create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)
doc = make_stock_entry(
item_code=item_code,
posting_date=add_to_date(today(), months=-7),
posting_time="00:00:00",
purpose="Material Receipt",
qty=10,
to_warehouse="_Test Warehouse - _TC",
do_not_submit=True,
)
self.assertTrue(doc.is_enqueue_action())
doc = make_stock_entry(
item_code=item_code,
posting_date=today(),
posting_time="00:00:00",
purpose="Material Receipt",
qty=10,
to_warehouse="_Test Warehouse - _TC",
do_not_submit=True,
)
self.assertFalse(doc.is_enqueue_action())
frappe.flags.in_test = True
def test_negative_batch(self):
item_code = "Test Negative Batch Item - 001"
make_item(

View File

@@ -58,15 +58,7 @@ frappe.query_reports["Stock Ledger"] = {
"fieldname":"batch_no",
"label": __("Batch No"),
"fieldtype": "Link",
"options": "Batch",
on_change() {
const batch_no = frappe.query_report.get_filter_value('batch_no');
if (batch_no) {
frappe.query_report.set_filter_value('segregate_serial_batch_bundle', 1);
} else {
frappe.query_report.set_filter_value('segregate_serial_batch_bundle', 0);
}
}
"options": "Batch"
},
{
"fieldname":"brand",

View File

@@ -3,7 +3,6 @@
import copy
from collections import defaultdict
import frappe
from frappe import _
@@ -32,7 +31,7 @@ def execute(filters=None):
bundle_details = {}
if filters.get("segregate_serial_batch_bundle"):
bundle_details = get_serial_batch_bundle_details(sl_entries, filters)
bundle_details = get_serial_batch_bundle_details(sl_entries)
data = []
conversion_factors = []
@@ -48,13 +47,12 @@ def execute(filters=None):
available_serial_nos = {}
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
batch_balance_dict = defaultdict(float)
for sle in sl_entries:
item_detail = item_details[sle.item_code]
sle.update(item_detail)
if bundle_info := bundle_details.get(sle.serial_and_batch_bundle):
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict))
data.extend(get_segregated_bundle_entries(sle, bundle_info))
continue
if filters.get("batch_no") or inventory_dimension_filters_applied:
@@ -87,7 +85,7 @@ def execute(filters=None):
return columns, data
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
def get_segregated_bundle_entries(sle, bundle_details):
segregated_entries = []
qty_before_transaction = sle.qty_after_transaction - sle.actual_qty
stock_value_before_transaction = sle.stock_value - sle.stock_value_difference
@@ -95,6 +93,7 @@ def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
for row in bundle_details:
new_sle = copy.deepcopy(sle)
new_sle.update(row)
new_sle.update(
{
"in_out_rate": flt(new_sle.stock_value_difference / row.qty) if row.qty else 0,
@@ -106,10 +105,6 @@ def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
}
)
if row.batch_no:
batch_balance_dict[row.batch_no] += row.qty
new_sle.update({"qty_after_transaction": batch_balance_dict[row.batch_no]})
qty_before_transaction += row.qty
stock_value_before_transaction += new_sle.stock_value_difference
@@ -122,7 +117,7 @@ def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
return segregated_entries
def get_serial_batch_bundle_details(sl_entries, filters=None):
def get_serial_batch_bundle_details(sl_entries):
bundle_details = []
for sle in sl_entries:
if sle.serial_and_batch_bundle:
@@ -131,14 +126,10 @@ def get_serial_batch_bundle_details(sl_entries, filters=None):
if not bundle_details:
return frappe._dict({})
query_filers = {"parent": ("in", bundle_details)}
if filters.get("batch_no"):
query_filers["batch_no"] = filters.batch_no
_bundle_details = frappe._dict({})
batch_entries = frappe.get_all(
"Serial and Batch Entry",
filters=query_filers,
filters={"parent": ("in", bundle_details)},
fields=["parent", "qty", "incoming_rate", "stock_value_difference", "batch_no", "serial_no"],
order_by="parent, idx",
)

View File

@@ -4,9 +4,8 @@ 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, Timestamp
from frappe.query_builder.functions import CombineDatetime, Sum
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,
@@ -325,7 +324,9 @@ class SerialBatchBundle:
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
batches_qty = get_available_batches(
frappe._dict({"item_code": self.item_code, "batch_no": list(batches.keys())})
frappe._dict(
{"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
)
)
for batch_no in batches:
@@ -423,21 +424,19 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
)
else:
entries = self.get_serial_no_ledgers()
self.serial_no_incoming_rate = defaultdict(float)
self.stock_value_change = 0.0
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
for ledger in entries:
self.stock_value_change += ledger.incoming_rate
self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
self.calculate_stock_value_from_deprecarated_ledgers()
def get_incoming_rate_from_bundle(self, serial_no) -> float:
def get_serial_no_ledgers(self):
serial_nos = self.get_serial_nos()
bundle = frappe.qb.DocType("Serial and Batch Bundle")
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
@@ -445,18 +444,20 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
frappe.qb.from_(bundle)
.inner_join(bundle_child)
.on(bundle.name == bundle_child.parent)
.select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
.select(
bundle.name,
bundle_child.serial_no,
(bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
)
.where(
(bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
& (bundle_child.serial_no == serial_no)
& (bundle.type_of_transaction == "Inward")
& (bundle_child.qty > 0)
& (bundle_child.serial_no.isin(serial_nos))
& (bundle.type_of_transaction.isin(["Inward", "Outward"]))
& (bundle.item_code == self.sle.item_code)
& (bundle_child.warehouse == self.sle.warehouse)
)
.orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
.limit(1)
.orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
)
# Important to exclude the current voucher to calculate correct the stock value difference
@@ -473,8 +474,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
query = query.where(timestamp_condition)
incoming_rate = query.run()
return flt(incoming_rate[0][0]) if incoming_rate else 0.0
return query.run(as_dict=True)
def get_serial_nos(self):
if self.sle.get("serial_nos"):

View File

@@ -893,9 +893,6 @@ class update_entries_after(object):
query.run()
def calculate_valuation_for_serial_batch_bundle(self, sle):
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
return
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
@@ -955,12 +952,7 @@ class update_entries_after(object):
get_rate_for_return, # don't move this import to top
)
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")
):
if self.valuation_method == "Moving Average":
rate = get_incoming_rate(
{
"item_code": sle.item_code,
@@ -987,18 +979,6 @@ 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

View File

@@ -1061,157 +1061,6 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertTrue(frappe.db.get_value("Purchase Receipt", {"subcontracting_receipt": scr.name}))
def test_use_serial_batch_fields_for_subcontracting_receipt(self):
fg_item = make_item(
"Test Subcontracted Item With Batch No",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-BNGS-.####",
"is_sub_contracted_item": 1,
},
).name
make_item(
"Test Subcontracted Item With Batch No Service Item 1",
properties={"is_stock_item": 0},
)
make_bom(
item=fg_item,
raw_materials=[
make_item(
"Test Subcontracted Item With Batch No RM Item 1",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-RM-BNGS-.####",
},
).name
],
)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Test Subcontracted Item With Batch No Service Item 1",
"qty": 1,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 1,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
batch_no = "BATCH-BNGS-0001"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": fg_item,
}
).insert()
scr = make_subcontracting_receipt(sco.name)
self.assertFalse(scr.items[0].serial_and_batch_bundle)
scr.items[0].use_serial_batch_fields = 1
scr.items[0].batch_no = batch_no
scr.save()
scr.submit()
scr.reload()
self.assertTrue(scr.items[0].serial_and_batch_bundle)
def test_use_serial_batch_fields_for_subcontracting_receipt_with_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
fg_item = make_item(
"Test Subcontracted Item With Batch No for Rejected Qty",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-REJ-BNGS-.####",
"is_sub_contracted_item": 1,
},
).name
make_item(
"Test Subcontracted Item With Batch No Service Item 2",
properties={"is_stock_item": 0},
)
make_bom(
item=fg_item,
raw_materials=[
make_item(
"Test Subcontracted Item With Batch No RM Item 2",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-REJ-RM-BNGS-.####",
},
).name
],
)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Test Subcontracted Item With Batch No Service Item 2",
"qty": 10,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
batch_no = "BATCH-REJ-BNGS-0001"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": fg_item,
}
).insert()
rej_warehouse = create_warehouse("_Test Subcontract Warehouse For Rejected Qty")
scr = make_subcontracting_receipt(sco.name)
self.assertFalse(scr.items[0].serial_and_batch_bundle)
scr.items[0].use_serial_batch_fields = 1
scr.items[0].batch_no = batch_no
scr.items[0].received_qty = 10
scr.items[0].rejected_qty = 2
scr.items[0].qty = 8
scr.items[0].rejected_warehouse = rej_warehouse
scr.save()
scr.submit()
scr.reload()
self.assertTrue(scr.items[0].serial_and_batch_bundle)
self.assertTrue(scr.items[0].rejected_serial_and_batch_bundle)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)

View File

@@ -335,7 +335,8 @@
"fieldtype": "Small Text",
"label": "Rejected Serial No",
"no_copy": 1,
"print_hide": 1
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "subcontracting_order_item",
@@ -568,7 +569,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-03-07 11:43:38.954262",
"modified": "2024-02-04 16:23:30.374865",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",