mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-17 20:26:28 +00:00
Compare commits
1 Commits
v15.16.2
...
cache_jour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f44ba1c6 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.16.2"
|
||||
__version__ = "15.14.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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, [])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user