mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-26 01:58:31 +00:00
Merge pull request #48602 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -252,6 +252,10 @@ frappe.treeview_settings["Account"] = {
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
const node = treeview.tree.get_selected_node();
|
||||
if (node.is_root) {
|
||||
frappe.throw(__("Cannot create root account."));
|
||||
}
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
@@ -270,7 +274,8 @@ frappe.treeview_settings["Account"] = {
|
||||
].treeview.page.fields_dict.root_company.get_value() ||
|
||||
frappe.flags.ignore_root_company_validation) &&
|
||||
node.expandable &&
|
||||
!node.hide_add
|
||||
!node.hide_add &&
|
||||
!node.is_root
|
||||
);
|
||||
},
|
||||
click: function () {
|
||||
|
||||
@@ -26,9 +26,20 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
@@ -552,7 +554,7 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
@@ -609,6 +611,17 @@
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
|
||||
@@ -58,7 +58,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
@@ -152,3 +152,11 @@ class AccountsSettings(Document):
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -1071,10 +1071,3 @@ def create_pos_invoice(**args):
|
||||
pos_inv.payment_schedule = []
|
||||
|
||||
return pos_inv
|
||||
|
||||
|
||||
def make_batch_item(item_name):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists(item_name):
|
||||
return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))
|
||||
|
||||
@@ -461,6 +461,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -3055,6 +3055,28 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
# cases where distributed discount amount is not set
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Item",
|
||||
{"name": ["in", [d.name for d in si.items]]},
|
||||
"distributed_discount_amount",
|
||||
0,
|
||||
)
|
||||
|
||||
si.load_from_db()
|
||||
si.additional_discount_account = additional_discount_account
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
expected_gle = [
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
["Discount Account - _TC", 22.0, 0.0, nowdate()],
|
||||
["Service - _TC", 0.0, 100.0, nowdate()],
|
||||
["TDS Payable - _TC", 0.0, 10.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
|
||||
@@ -23,6 +23,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_advance_payment_doctypes,
|
||||
get_currency_precision,
|
||||
get_party_types_from_account_type,
|
||||
@@ -127,6 +128,8 @@ class ReceivablePayableReport:
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
@@ -134,8 +137,7 @@ class ReceivablePayableReport:
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
query, param = self.ple_query
|
||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||
self.ple_entries = self.ple_query.run(as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
@@ -148,9 +150,8 @@ class ReceivablePayableReport:
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
@@ -318,6 +319,79 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -861,18 +935,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
query, param = query.walk()
|
||||
|
||||
match_conditions = build_match_conditions("Payment Ledger Entry")
|
||||
if match_conditions:
|
||||
query += " AND " + match_conditions
|
||||
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
|
||||
query = query.where(Criterion.all(match_conditions))
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`"
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`"
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_query = (query, param)
|
||||
self.ple_query = query
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
@@ -1253,3 +1324,134 @@ def get_customer_group_with_children(customer_groups):
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Function
|
||||
genkey_function_name = "ar_genkey"
|
||||
genkey_function_sql = f"""
|
||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
||||
begin
|
||||
if allocate then
|
||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
||||
else
|
||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
||||
end if;
|
||||
end
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.genkey_function_name not in existing_procedures:
|
||||
frappe.db.sql(self.genkey_function_sql)
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -23,6 +23,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -179,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
|
||||
def get_condition(dimension):
|
||||
conditions = []
|
||||
|
||||
conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
|
||||
conditions.append(f"{frappe.scrub(dimension)} in (%(dimensions)s)")
|
||||
|
||||
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
|
||||
from frappe.query_builder.functions import Count, Max, Sum
|
||||
@@ -2347,3 +2348,19 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
|
||||
"frequency": "Cron",
|
||||
}
|
||||
).save()
|
||||
|
||||
|
||||
def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
match_filters = build_match_conditions(doctype, user, False)
|
||||
criterion = []
|
||||
if match_filters:
|
||||
from frappe import qb
|
||||
|
||||
_dt = qb.DocType(doctype)
|
||||
|
||||
for filter in match_filters:
|
||||
for d, names in filter.items():
|
||||
fieldname = d.lower().replace(" ", "_")
|
||||
criterion.append(_dt[fieldname].isin(names))
|
||||
|
||||
return criterion
|
||||
|
||||
@@ -1215,6 +1215,10 @@ def update_existing_asset(asset, remaining_qty, new_asset_name):
|
||||
opening_accumulated_depreciation = flt(
|
||||
(asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity
|
||||
)
|
||||
value_after_depreciation = flt(
|
||||
(asset.value_after_depreciation * remaining_qty) / asset.asset_quantity,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
@@ -1222,6 +1226,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name):
|
||||
{
|
||||
"opening_accumulated_depreciation": opening_accumulated_depreciation,
|
||||
"gross_purchase_amount": remaining_gross_purchase_amount,
|
||||
"value_after_depreciation": value_after_depreciation,
|
||||
"asset_quantity": remaining_qty,
|
||||
},
|
||||
)
|
||||
@@ -1283,6 +1288,10 @@ def create_new_asset_after_split(asset, split_qty):
|
||||
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||
new_asset.asset_quantity = split_qty
|
||||
new_asset.split_from = asset.name
|
||||
new_asset.value_after_depreciation = flt(
|
||||
(asset.value_after_depreciation * split_qty) / asset.asset_quantity,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
|
||||
for row in new_asset.get("finance_books"):
|
||||
row.value_after_depreciation = flt((row.value_after_depreciation * split_qty) / asset.asset_quantity)
|
||||
|
||||
@@ -1946,6 +1946,15 @@ class AccountsController(TransactionBase):
|
||||
and self.get("discount_amount")
|
||||
and self.get("additional_discount_account")
|
||||
):
|
||||
# cases where distributed_discount_amount is not patched
|
||||
if not hasattr(self, "__has_distributed_discount_set"):
|
||||
self.__has_distributed_discount_set = any(
|
||||
i.distributed_discount_amount for i in self.get("items")
|
||||
)
|
||||
|
||||
if not self.__has_distributed_discount_set:
|
||||
return item.amount, item.base_amount
|
||||
|
||||
amount += item.distributed_discount_amount
|
||||
base_amount += flt(
|
||||
item.distributed_discount_amount * self.get("conversion_rate"),
|
||||
|
||||
@@ -36,6 +36,17 @@ def validate_return_against(doc):
|
||||
|
||||
party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
|
||||
|
||||
if ref_doc.get(party_type) != doc.get(party_type):
|
||||
frappe.throw(
|
||||
_("The {0} {1} does not match with the {0} {2} in the {3} {4}").format(
|
||||
doc.meta.get_label(party_type),
|
||||
doc.get(party_type),
|
||||
ref_doc.get(party_type),
|
||||
ref_doc.doctype,
|
||||
ref_doc.name,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
ref_doc.company == doc.company
|
||||
and ref_doc.get(party_type) == doc.get(party_type)
|
||||
|
||||
@@ -57,6 +57,35 @@ class SellingController(StockController):
|
||||
if self.get(table_field):
|
||||
self.set_serial_and_batch_bundle(table_field)
|
||||
|
||||
def validate_standalone_serial_nos_customer(self):
|
||||
if not self.is_return or self.return_against:
|
||||
return
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Delivery Note"]:
|
||||
bundle_ids = [d.serial_and_batch_bundle for d in self.get("items") if d.serial_and_batch_bundle]
|
||||
if not bundle_ids:
|
||||
return
|
||||
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": ("in", bundle_ids)},
|
||||
pluck="serial_no",
|
||||
)
|
||||
|
||||
if serial_nos := frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", serial_nos), "customer": ("is", "set")},
|
||||
fields=["name", "customer"],
|
||||
):
|
||||
for sn in serial_nos:
|
||||
if sn.customer and sn.customer != self.customer:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}"
|
||||
).format(frappe.bold(sn.name), frappe.bold(sn.customer)),
|
||||
title=_("Serial No Already Assigned"),
|
||||
)
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -464,7 +464,7 @@ class SubcontractingController(StockController):
|
||||
i += 1
|
||||
|
||||
def __remove_serial_and_batch_bundle(self, item):
|
||||
if item.serial_and_batch_bundle:
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
@@ -953,7 +953,7 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows)
|
||||
sco_doc.update_reserved_qty_for_subcontracting()
|
||||
sco_doc.update_reserved_qty_for_subcontracting(sco_item_rows)
|
||||
|
||||
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
|
||||
if hasattr(self, "supplied_items"):
|
||||
@@ -1046,7 +1046,7 @@ class SubcontractingController(StockController):
|
||||
|
||||
return supplied_items_cost
|
||||
|
||||
def set_subcontracting_order_status(self):
|
||||
def set_subcontracting_order_status(self, update_bin=True):
|
||||
if self.doctype == "Subcontracting Order":
|
||||
self.update_status()
|
||||
elif self.doctype == "Subcontracting Receipt":
|
||||
@@ -1055,7 +1055,7 @@ class SubcontractingController(StockController):
|
||||
if self.subcontract_orders:
|
||||
for sco in set(self.subcontract_orders):
|
||||
sco_doc = frappe.get_doc("Subcontracting Order", sco)
|
||||
sco_doc.update_status()
|
||||
sco_doc.update_status(update_bin=update_bin)
|
||||
|
||||
def calculate_additional_costs(self):
|
||||
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
|
||||
|
||||
@@ -1274,7 +1274,7 @@ def get_children(parent=None, is_root=False, **filters):
|
||||
|
||||
bom_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["item_code", "bom_no as value", "stock_qty"],
|
||||
fields=["item_code", "bom_no as value", "stock_qty", "qty"],
|
||||
filters=[["parent", "=", frappe.form_dict.parent]],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
@@ -16,7 +16,14 @@ frappe.treeview_settings["BOM"] = {
|
||||
show_expand_all: false,
|
||||
get_label: function (node) {
|
||||
if (node.data.qty) {
|
||||
return node.data.qty + " x " + node.data.item_code;
|
||||
const escape = frappe.utils.escape_html;
|
||||
let label = escape(node.data.item_code);
|
||||
if (node.data.item_name && node.data.item_code !== node.data.item_name) {
|
||||
label += `: ${escape(node.data.item_name)}`;
|
||||
}
|
||||
return `${label} <span class="badge badge-pill badge-light">${node.data.qty} ${escape(
|
||||
__(node.data.stock_uom)
|
||||
)}</span>`;
|
||||
} else {
|
||||
return node.data.item_code || node.data.value;
|
||||
}
|
||||
|
||||
@@ -412,3 +412,4 @@ erpnext.patches.v15_0.drop_sle_indexes
|
||||
erpnext.patches.v15_0.update_pick_list_fields
|
||||
erpnext.patches.v15_0.update_pegged_currencies
|
||||
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
|
||||
erpnext.patches.v15_0.rename_price_list_to_buying_price_list
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Material Request", "price_list"):
|
||||
rename_field(
|
||||
"Material Request",
|
||||
"price_list",
|
||||
"buying_price_list",
|
||||
)
|
||||
@@ -582,7 +582,6 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
|
||||
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
|
||||
doctype: frm.doc.doctype
|
||||
},
|
||||
price_list: frm.doc.price_list,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
|
||||
@@ -42,6 +42,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate);
|
||||
|
||||
cur_frm.cscript.set_gross_profit(item);
|
||||
cur_frm.cscript.calculate_taxes_and_totals();
|
||||
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
|
||||
|
||||
if (item.item_code && item.rate) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.get_item_details.get_item_tax_template",
|
||||
@@ -63,10 +67,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cur_frm.cscript.set_gross_profit(item);
|
||||
cur_frm.cscript.calculate_taxes_and_totals();
|
||||
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
|
||||
});
|
||||
|
||||
frappe.ui.form.on(this.frm.cscript.tax_table, "rate", function(frm, cdt, cdn) {
|
||||
@@ -989,7 +989,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
var party = me.frm.doc[frappe.model.scrub(party_type)];
|
||||
if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc.get(party_account_field))) {
|
||||
if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc[party_account_field])) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
|
||||
@@ -345,7 +345,10 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
const items = this.search_index[selling_price_list][search_term];
|
||||
this.items = items;
|
||||
this.render_item_list(items);
|
||||
this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart();
|
||||
this.auto_add_item &&
|
||||
this.search_field.$input[0].value &&
|
||||
this.items.length == 1 &&
|
||||
this.add_filtered_item_to_cart();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -358,7 +361,10 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
}
|
||||
this.items = items;
|
||||
this.render_item_list(items);
|
||||
this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart();
|
||||
this.auto_add_item &&
|
||||
this.search_field.$input[0].value &&
|
||||
this.items.length == 1 &&
|
||||
this.add_filtered_item_to_cart();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -600,7 +600,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "exit",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Exit",
|
||||
"label": "Employee Exit",
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
@@ -822,7 +822,7 @@
|
||||
"image_field": "image",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-07 13:54:40.122345",
|
||||
"modified": "2025-07-04 08:29:34.347269",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
|
||||
@@ -2,8 +2,10 @@ frappe.listview_settings["Employee"] = {
|
||||
add_fields: ["status", "branch", "department", "designation", "image"],
|
||||
filters: [["status", "=", "Active"]],
|
||||
get_indicator: function (doc) {
|
||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status];
|
||||
return indicator;
|
||||
return [
|
||||
__(doc.status, null, "Employee"),
|
||||
{ Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status],
|
||||
"status,=," + doc.status,
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
if not transaction_date:
|
||||
transaction_date = nowdate()
|
||||
|
||||
currency_settings = frappe.get_doc("Accounts Settings").as_dict()
|
||||
currency_settings = frappe.get_cached_doc("Accounts Settings")
|
||||
allow_stale_rates = currency_settings.get("allow_stale")
|
||||
|
||||
filters = [
|
||||
|
||||
@@ -35,9 +35,11 @@ class TestBatch(FrappeTestCase):
|
||||
def make_batch_item(cls, item_name=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists(item_name):
|
||||
if not frappe.db.exists("Item", item_name):
|
||||
return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))
|
||||
|
||||
return frappe.get_doc("Item", item_name)
|
||||
|
||||
def test_purchase_receipt(self, batch_qty=100):
|
||||
"""Test automated batch creation from Purchase Receipt"""
|
||||
self.make_batch_item("ITEM-BATCH-1")
|
||||
@@ -305,8 +307,18 @@ class TestBatch(FrappeTestCase):
|
||||
self.assertEqual(
|
||||
get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"),
|
||||
[
|
||||
{"batch_no": "batch a", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"},
|
||||
{"batch_no": "batch b", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"},
|
||||
{
|
||||
"batch_no": "batch a",
|
||||
"qty": 90.0,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"expiry_date": None,
|
||||
},
|
||||
{
|
||||
"batch_no": "batch b",
|
||||
"qty": 90.0,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"expiry_date": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -466,6 +466,8 @@ class DeliveryNote(SellingController):
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -43,7 +43,7 @@ frappe.ui.form.on("Material Request", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("price_list", () => {
|
||||
frm.set_query("buying_price_list", () => {
|
||||
return {
|
||||
filters: {
|
||||
buying: 1,
|
||||
@@ -79,7 +79,7 @@ frappe.ui.form.on("Material Request", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
|
||||
frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list");
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
@@ -255,8 +255,8 @@ frappe.ui.form.on("Material Request", {
|
||||
from_warehouse: item.from_warehouse,
|
||||
warehouse: item.warehouse,
|
||||
doctype: frm.doc.doctype,
|
||||
buying_price_list: frm.doc.price_list
|
||||
? frm.doc.price_list
|
||||
buying_price_list: frm.doc.buying_price_list
|
||||
? frm.doc.buying_price_list
|
||||
: frappe.defaults.get_default("buying_price_list"),
|
||||
currency: frappe.defaults.get_default("Currency"),
|
||||
name: frm.doc.name,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"column_break_2",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"price_list",
|
||||
"buying_price_list",
|
||||
"amended_from",
|
||||
"warehouse_section",
|
||||
"scan_barcode",
|
||||
@@ -354,7 +354,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list",
|
||||
"fieldname": "buying_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List"
|
||||
@@ -364,7 +364,7 @@
|
||||
"idx": 70,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-07 13:15:28.615984",
|
||||
"modified": "2025-07-11 21:03:26.588307",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request",
|
||||
|
||||
@@ -153,8 +153,8 @@ class MaterialRequest(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
if not self.price_list:
|
||||
self.price_list = frappe.defaults.get_defaults().buying_price_list
|
||||
if not self.buying_price_list:
|
||||
self.buying_price_list = frappe.defaults.get_defaults().buying_price_list
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_schedule_date()
|
||||
|
||||
@@ -6,11 +6,11 @@ frappe.listview_settings["Material Request"] = {
|
||||
return [__("Stopped"), "red", "status,=,Stopped"];
|
||||
} else if (doc.transfer_status && doc.docstatus != 2) {
|
||||
if (doc.transfer_status == "Not Started") {
|
||||
return [__("Not Started"), "orange"];
|
||||
return [__("Not Started"), "orange", "transfer_status,=,Not Started"];
|
||||
} else if (doc.transfer_status == "In Transit") {
|
||||
return [__("In Transit"), "yellow"];
|
||||
return [__("In Transit"), "yellow", "transfer_status,=,In Transit"];
|
||||
} else if (doc.transfer_status == "Completed") {
|
||||
return [__("Completed"), "green"];
|
||||
return [__("Completed"), "green", "transfer_status,=,Completed"];
|
||||
}
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
|
||||
return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"];
|
||||
|
||||
@@ -340,7 +340,7 @@ def on_doctype_update():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_from_product_bundle(row, price_list):
|
||||
def get_items_from_product_bundle(row):
|
||||
row, items = json.loads(row), []
|
||||
|
||||
bundled_items = get_product_bundle_items(row["item_code"])
|
||||
@@ -350,7 +350,6 @@ def get_items_from_product_bundle(row, price_list):
|
||||
"item_code": item.item_code,
|
||||
"qty": flt(row["quantity"]) * flt(item.qty),
|
||||
"conversion_rate": 1,
|
||||
"price_list": price_list,
|
||||
"currency": frappe.defaults.get_defaults().currency,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -116,13 +116,15 @@ class PickList(TransactionBase):
|
||||
|
||||
continue
|
||||
|
||||
bin_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": row.item_code, "warehouse": row.warehouse},
|
||||
"actual_qty",
|
||||
bin_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": row.item_code, "warehouse": row.warehouse},
|
||||
"actual_qty",
|
||||
)
|
||||
)
|
||||
|
||||
if row.picked_qty > flt(bin_qty):
|
||||
if row.picked_qty > bin_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}."
|
||||
|
||||
@@ -55,14 +55,14 @@ frappe.ui.form.on("Serial and Batch Bundle", {
|
||||
|
||||
let fields = frm.events.get_prompt_fields(frm);
|
||||
|
||||
frm.add_custom_button(__("Make " + label), () => {
|
||||
frm.add_custom_button(__("Make {0}", [label]), () => {
|
||||
frappe.prompt(
|
||||
fields,
|
||||
(data) => {
|
||||
frm.events.add_serial_batch(frm, data);
|
||||
},
|
||||
"Add " + label,
|
||||
"Make " + label
|
||||
__("Add {0}", [label]),
|
||||
__("Make {0}", [label])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
nowtime,
|
||||
parse_json,
|
||||
@@ -804,7 +804,7 @@ class SerialandBatchBundle(Document):
|
||||
if qty_field == "qty" and row.get("stock_qty"):
|
||||
qty = row.get("stock_qty")
|
||||
|
||||
precision = row.precision
|
||||
precision = row.precision(qty_field)
|
||||
if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01:
|
||||
total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row)
|
||||
set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row)
|
||||
@@ -2148,6 +2148,9 @@ def get_auto_batch_nos(kwargs):
|
||||
picked_batches,
|
||||
)
|
||||
|
||||
if kwargs.based_on == "Expiry":
|
||||
available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31")))
|
||||
|
||||
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"):
|
||||
filter_zero_near_batches(available_batches, kwargs)
|
||||
|
||||
@@ -2247,6 +2250,7 @@ def get_available_batches(kwargs):
|
||||
batch_ledger.batch_no,
|
||||
batch_ledger.warehouse,
|
||||
Sum(batch_ledger.qty).as_("qty"),
|
||||
batch_table.expiry_date,
|
||||
)
|
||||
.where(batch_table.disabled == 0)
|
||||
.where(stock_ledger_entry.is_cancelled == 0)
|
||||
@@ -2537,6 +2541,7 @@ def get_stock_ledgers_batches(kwargs):
|
||||
stock_ledger_entry.item_code,
|
||||
Sum(stock_ledger_entry.actual_qty).as_("qty"),
|
||||
stock_ledger_entry.batch_no,
|
||||
batch_table.expiry_date,
|
||||
)
|
||||
.where((stock_ledger_entry.is_cancelled == 0) & (stock_ledger_entry.batch_no.isnotnull()))
|
||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"batch_no",
|
||||
"warehouse",
|
||||
"purchase_rate",
|
||||
"customer",
|
||||
"column_break1",
|
||||
"status",
|
||||
"item_name",
|
||||
@@ -267,12 +268,21 @@
|
||||
"label": "Creation Document No",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"no_copy": 1,
|
||||
"options": "Customer",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-15 16:22:49.873889",
|
||||
"modified": "2025-07-15 13:40:21.938700",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
@@ -310,6 +320,7 @@
|
||||
"role": "Stock User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_code",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -40,6 +40,7 @@ class SerialNo(StockController):
|
||||
batch_no: DF.Link | None
|
||||
brand: DF.Link | None
|
||||
company: DF.Link
|
||||
customer: DF.Link | None
|
||||
description: DF.Text | None
|
||||
employee: DF.Link | None
|
||||
item_code: DF.Link
|
||||
|
||||
@@ -435,7 +435,9 @@ class StockEntry(StockController):
|
||||
additional_cost_amt = additional_costs[0][0] if additional_costs else 0
|
||||
|
||||
amount += additional_cost_amt
|
||||
frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount)
|
||||
project = frappe.get_doc("Project", self.project)
|
||||
project.total_consumed_material_cost = amount
|
||||
project.save()
|
||||
|
||||
def validate_item(self):
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
@@ -1089,17 +1089,14 @@ class StockReconciliation(StockController):
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
add_new_sle
|
||||
and not frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
|
||||
"name",
|
||||
)
|
||||
and not row.current_serial_and_batch_bundle
|
||||
if add_new_sle and not frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
|
||||
"name",
|
||||
):
|
||||
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
|
||||
row.reload()
|
||||
if not row.current_serial_and_batch_bundle:
|
||||
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
|
||||
row.reload()
|
||||
|
||||
self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation)
|
||||
|
||||
|
||||
@@ -120,7 +120,11 @@ class StockSettings(Document):
|
||||
)
|
||||
|
||||
def cant_change_valuation_method(self):
|
||||
previous_valuation_method = self.get_doc_before_save().get("valuation_method")
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save:
|
||||
return
|
||||
|
||||
previous_valuation_method = doc_before_save.get("valuation_method")
|
||||
|
||||
if previous_valuation_method and previous_valuation_method != self.valuation_method:
|
||||
# check if there are any stock ledger entries against items
|
||||
|
||||
@@ -19,21 +19,17 @@ def execute(filters=None):
|
||||
columns = get_columns(filters)
|
||||
items = get_items(filters)
|
||||
sl_entries = get_stock_ledger_entries(filters, items)
|
||||
|
||||
if not sl_entries:
|
||||
return columns, []
|
||||
|
||||
item_details = get_item_details(items, sl_entries, False)
|
||||
|
||||
opening_row = get_opening_balance_data(filters, columns, sl_entries)
|
||||
|
||||
opening_row = get_opening_balance(filters, columns, sl_entries)
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_opening_balance_data(filters, columns, sl_entries):
|
||||
opening_row = get_opening_balance(filters, columns, sl_entries)
|
||||
return opening_row
|
||||
|
||||
|
||||
def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision):
|
||||
data = []
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, today
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty
|
||||
@@ -21,6 +22,10 @@ def execute(filters=None):
|
||||
columns = get_columns()
|
||||
bin_list = get_bin_list(filters)
|
||||
item_map = get_item_map(filters.get("item_code"), include_uom)
|
||||
item_groups = []
|
||||
if filters.get("item_group"):
|
||||
item_groups.append(filters.item_group)
|
||||
item_groups.extend(get_descendants_of("Item Group", filters.item_group))
|
||||
|
||||
warehouse_company = {}
|
||||
data = []
|
||||
@@ -40,7 +45,7 @@ def execute(filters=None):
|
||||
if filters.brand and filters.brand != item.brand:
|
||||
continue
|
||||
|
||||
elif filters.item_group and filters.item_group != item.item_group:
|
||||
elif item_groups and item.item_group not in item_groups:
|
||||
continue
|
||||
|
||||
elif filters.company and filters.company != company:
|
||||
|
||||
@@ -377,6 +377,10 @@ class SerialBatchBundle:
|
||||
]:
|
||||
status = "Consumed"
|
||||
|
||||
customer = None
|
||||
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
||||
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
@@ -387,10 +391,11 @@ class SerialBatchBundle:
|
||||
"Active"
|
||||
if warehouse
|
||||
else status
|
||||
if (sn_table.purchase_document_no != sle.voucher_no and sle.is_cancelled != 1)
|
||||
if (sn_table.purchase_document_no != sle.voucher_no or sle.is_cancelled != 1)
|
||||
else "Inactive",
|
||||
)
|
||||
.set(sn_table.company, sle.company)
|
||||
.set(sn_table.customer, customer)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
)
|
||||
|
||||
|
||||
@@ -919,6 +919,7 @@ class update_entries_after:
|
||||
)
|
||||
|
||||
sle.doctype = "Stock Ledger Entry"
|
||||
sle.modified = now()
|
||||
frappe.get_doc(sle).db_update()
|
||||
|
||||
if not self.args.get("sle_id") or (
|
||||
|
||||
@@ -236,8 +236,11 @@ class SubcontractingOrder(SubcontractingController):
|
||||
|
||||
return flt(query[0][0]) if query else 0
|
||||
|
||||
def update_reserved_qty_for_subcontracting(self):
|
||||
def update_reserved_qty_for_subcontracting(self, sco_item_rows=None):
|
||||
for item in self.supplied_items:
|
||||
if sco_item_rows and item.reference_name not in sco_item_rows:
|
||||
continue
|
||||
|
||||
if item.rm_item_code:
|
||||
stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
@@ -299,7 +302,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
|
||||
self.set_missing_values()
|
||||
|
||||
def update_status(self, status=None, update_modified=True):
|
||||
def update_status(self, status=None, update_modified=True, update_bin=True):
|
||||
if self.status == "Closed" and self.status != status:
|
||||
check_on_hold_or_closed_status("Purchase Order", self.purchase_order)
|
||||
|
||||
@@ -329,8 +332,9 @@ class SubcontractingOrder(SubcontractingController):
|
||||
self.db_set("status", status, update_modified=update_modified)
|
||||
|
||||
self.update_requested_qty()
|
||||
self.update_ordered_qty_for_subcontracting()
|
||||
self.update_reserved_qty_for_subcontracting()
|
||||
if update_bin:
|
||||
self.update_ordered_qty_for_subcontracting()
|
||||
self.update_reserved_qty_for_subcontracting()
|
||||
|
||||
def update_subcontracted_quantity_in_po(self, cancel=False):
|
||||
for service_item in self.service_items:
|
||||
|
||||
@@ -152,7 +152,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.validate_available_qty_for_consumption()
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.set_subcontracting_order_status()
|
||||
self.set_subcontracting_order_status(update_bin=False)
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
|
||||
for table_name in ["items", "supplied_items"]:
|
||||
@@ -179,7 +179,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
self.set_subcontracting_order_status()
|
||||
self.set_subcontracting_order_status(update_bin=False)
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
@@ -157,6 +157,8 @@ def link_existing_conversations(doc, state):
|
||||
"""
|
||||
Called from hooks on creation of Contact or Lead to link all the existing conversations.
|
||||
"""
|
||||
if doc.flags.ignore_auto_link_call_log:
|
||||
return
|
||||
if doc.doctype != "Contact":
|
||||
return
|
||||
try:
|
||||
@@ -183,12 +185,12 @@ def link_existing_conversations(doc, state):
|
||||
""",
|
||||
dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype),
|
||||
)
|
||||
|
||||
for log in logs:
|
||||
call_log = frappe.get_doc("Call Log", log)
|
||||
call_log.add_link(link_type=doc.doctype, link_name=doc.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
if logs:
|
||||
for log in logs:
|
||||
call_log = frappe.get_doc("Call Log", log)
|
||||
call_log.add_link(link_type=doc.doctype, link_name=doc.name)
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error during caller information update"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user