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

This commit is contained in:
Diptanil Saha
2025-11-19 08:26:48 +05:30
committed by GitHub
33 changed files with 344 additions and 67 deletions

View File

@@ -592,6 +592,8 @@ frappe.ui.form.on("Payment Entry", {
paid_from: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_from,
@@ -609,6 +611,8 @@ frappe.ui.form.on("Payment Entry", {
paid_to: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_to,
@@ -1350,6 +1354,8 @@ frappe.ui.form.on("Payment Entry", {
},
bank_account: function (frm) {
if (frm.set_company_bank_account_based_on_coa) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
frappe.call({
@@ -1388,6 +1394,34 @@ frappe.ui.form.on("Payment Entry", {
}
},
set_company_bank_account: function (frm) {
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (!frm.doc.company || !frm.doc[field]) return;
frm.set_company_bank_account_based_on_coa = true;
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Bank Account",
filters: {
company: frm.doc.company,
account: frm.doc[field],
disabled: 0,
},
fieldname: ["name"],
},
callback: async function (r) {
if (r.message) await frm.set_value("bank_account", r.message.name);
frm.set_company_bank_account_based_on_coa = false;
},
});
},
sales_taxes_and_charges_template: function (frm) {
frm.trigger("fetch_taxes_from_template");
},

View File

@@ -475,8 +475,15 @@ def process_gl_and_closing_entries(doc):
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
frappe.log_error(title=_("Period Closing Voucher {0} GL Entry Processing Failed").format(doc.name))
frappe.db.set_value(
doc.doctype,
doc.name,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def process_cancellation(voucher_type, voucher_no):
@@ -488,8 +495,17 @@ def process_cancellation(voucher_type, voucher_no):
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
frappe.log_error(
title=_("Period Closing Voucher {0} GL Entry Cancellation Failed").format(voucher_no)
)
frappe.db.set_value(
voucher_type,
voucher_no,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def delete_closing_entries(voucher_no):

View File

@@ -189,6 +189,9 @@ class POSInvoice(SalesInvoice):
super().__init__(*args, **kwargs)
def validate(self):
if not self.customer:
frappe.throw(_("Please select Customer first"))
if not cint(self.is_pos):
frappe.throw(
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
@@ -345,14 +348,14 @@ class POSInvoice(SalesInvoice):
):
return
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
if not d.serial_and_batch_bundle:
if is_negative_stock_allowed(item_code=d.item_code):
return
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
if is_negative_stock_allowed:
continue
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
@@ -760,20 +763,22 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code)
else:
is_stock_item = True
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
return get_bundle_availability(item_code, warehouse), is_stock_item, False
else:
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
return 0, is_stock_item, False
def get_bundle_availability(bundle_item_code, warehouse):

View File

@@ -41,9 +41,19 @@ class POSOpeningEntry(StatusUpdater):
self.set_status()
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
if not frappe.db.exists("POS Profile", self.pos_profile):
frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile))
pos_profile_company, pos_profile_disabled = frappe.db.get_value(
"POS Profile", self.pos_profile, ["company", "disabled"]
)
if pos_profile_disabled:
frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile)))
if self.company != pos_profile_company:
frappe.throw(
_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)
_("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company)
)
if not cint(frappe.db.get_value("User", self.user, "enabled")):

View File

@@ -70,6 +70,7 @@ class POSProfile(Document):
# end: auto-generated types
def validate(self):
self.validate_disabled()
self.validate_default_profile()
self.validate_all_link_fields()
self.validate_duplicate_groups()
@@ -94,6 +95,21 @@ class POSProfile(Document):
title=_("Mandatory Accounting Dimension"),
)
def validate_disabled(self):
old_doc = self.get_doc_before_save()
if (
old_doc
and self.disabled
and old_doc.disabled != self.disabled
and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"})
):
frappe.throw(
_("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format(
frappe.bold(self.name)
)
)
def validate_default_profile(self):
for row in self.applicable_for_users:
res = frappe.db.sql(

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.utils import cint
from erpnext.accounts.doctype.pos_profile.pos_profile import (
get_child_nodes,
@@ -38,6 +39,51 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_disabled_pos_profile_creation(self):
make_pos_profile(name="_Test POS Profile 001", disabled=1)
pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001")
if pos_profile:
self.assertEqual(pos_profile.disabled, 1)
def test_disabled_pos_profile_after_opening(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
test_user, pos_profile = init_user_and_profile()
if pos_profile:
create_opening_entry(pos_profile, test_user.name)
self.assertEqual(pos_profile.disabled, 0)
pos_profile.disabled = 1
self.assertRaises(frappe.ValidationError, pos_profile.save)
def test_disabled_pos_profile_after_completing_session(self):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import (
create_opening_entry,
)
test_user, pos_profile = init_user_and_profile()
frappe.db.delete("POS Opening Entry", {"pos_profile": pos_profile.name})
if pos_profile:
opening_entry = create_opening_entry(pos_profile, test_user.name)
closing_entry = make_closing_entry_from_opening(opening_entry)
closing_entry.submit()
pos_profile.disabled = 1
pos_profile.save()
pos_profile.reload()
self.assertEqual(pos_profile.disabled, 1)
def get_customers_list(pos_profile=None):
if pos_profile is None:
@@ -117,6 +163,7 @@ def make_pos_profile(**args):
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
"location": "Block 1" if not args.do_not_set_accounting_dimension else None,
"disabled": cint(args.disabled) or 0,
}
)

View File

@@ -174,7 +174,7 @@ def add_solvency_ratios(
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
for year in years:
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year))
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision)
@@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
avg_data = {}
for d in ["Receivable", "Payable", "Stock"]:
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters)
avg_debtors, avg_creditors, avg_stock = (
avg_data.get("receivable"),

View File

@@ -566,6 +566,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
else:
update_value_in_dict(consolidated_gle, key, gle)
if filters.get("include_dimensions"):
dimensions = [*accounting_dimensions, "cost_center", "project"]
for dimension in dimensions:
if val := gle.get(dimension):
gle[dimension] = _(val)
for value in consolidated_gle.values():
update_value_in_dict(totals, "total", value)
update_value_in_dict(totals, "closing", value)

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _
from frappe.utils import getdate
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
@@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
precision = get_currency_precision()
out = []
entries = {}
@@ -72,17 +75,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
if net_total_map.get((voucher_type, name)):
values = net_total_map.get((voucher_type, name))
if values:
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
# back calculate total amount from rate and tax_amount
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
total_amount = grand_total = base_total
elif voucher_type == "Purchase Invoice":
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
(voucher_type, name)
)
else:
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
if tax_amount and rate:
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
total_amount = values[0]
grand_total = values[1]
base_total = values[2]
if voucher_type == "Purchase Invoice":
bill_no = values[3]
bill_date = values[4]
else:
total_amount += entry.credit

View File

@@ -371,7 +371,6 @@
"label": "Other Details"
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
@@ -379,7 +378,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"read_only": 1
},
{
@@ -597,7 +596,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-10-23 22:43:33.634452",
"modified": "2025-11-17 18:01:51.417942",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -103,6 +103,7 @@ class Asset(AccountsController):
status: DF.Literal[
"Draft",
"Submitted",
"Cancelled",
"Partially Depreciated",
"Fully Depreciated",
"Sold",

View File

@@ -139,7 +139,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
"options": "Asset",
"reqd": 1
},
@@ -250,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-07-29 15:14:34.044564",
"modified": "2025-11-17 18:35:54.575265",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -60,6 +60,17 @@ class AssetRepair(AccountsController):
if self.get("stock_items"):
self.set_stock_items_cost()
self.calculate_total_repair_cost()
self.validate_purchase_invoice_status()
def validate_purchase_invoice_status(self):
if self.purchase_invoice:
docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", self.purchase_invoice)
)
)
def validate_asset(self):
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):

View File

@@ -95,6 +95,7 @@ class SellingController(StockController):
# set contact and address details for customer, if they are not mentioned
self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate)
self.set_company_contact_person()
def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None
@@ -137,6 +138,7 @@ class SellingController(StockController):
lead,
posting_date=self.get("transaction_date") or self.get("posting_date"),
company=self.company,
doctype=self.doctype,
)
)
@@ -149,6 +151,13 @@ class SellingController(StockController):
self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate)
def set_company_contact_person(self):
"""Set the Company's Default Sales Contact as Company Contact Person."""
if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person:
self.company_contact_person = frappe.get_cached_value(
"Company", self.company, "default_sales_contact"
)
def remove_shipping_charge(self):
if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)

View File

@@ -432,7 +432,7 @@ def _set_missing_values(source, target):
@frappe.whitelist()
def get_lead_details(lead, posting_date=None, company=None):
def get_lead_details(lead, posting_date=None, company=None, doctype=None):
if not lead:
return {}
@@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None):
}
)
set_address_details(out, lead, "Lead", company=company)
set_address_details(out, lead, "Lead", doctype=doctype, company=company)
taxes_and_charges = set_taxes(
None,

View File

@@ -591,7 +591,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.track_semi_finished_goods === 0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
"label": "Finished Goods based Operating Cost"

View File

@@ -420,25 +420,36 @@ $.extend(erpnext.utils, {
if (!frappe.boot.setup_complete) {
return;
}
const today = frappe.datetime.get_today();
if (!date) {
date = frappe.datetime.get_today();
date = today;
}
let fiscal_year = "";
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
args: {
date: date,
boolean: boolean,
},
async: false,
callback: function (r) {
if (r.message) {
if (with_dates) fiscal_year = r.message;
else fiscal_year = r.message[0];
}
},
});
if (
frappe.boot.current_fiscal_year &&
date >= frappe.boot.current_fiscal_year[1] &&
date <= frappe.boot.current_fiscal_year[2]
) {
if (with_dates) fiscal_year = frappe.boot.current_fiscal_year;
else fiscal_year = frappe.boot.current_fiscal_year[0];
} else if (today != date) {
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
type: "GET", // make it cacheable
args: {
date: date,
boolean: boolean,
},
async: false,
callback: function (r) {
if (r.message) {
if (with_dates) fiscal_year = r.message;
else fiscal_year = r.message[0];
}
},
});
}
return fiscal_year;
},

View File

@@ -16,11 +16,13 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
insert() {
/**
* Using alias fieldnames because the doctype definition define "email_id" and "mobile_no" as readonly fields.
* Therefor, resulting in the fields being "hidden".
* This results in the fields being "hidden".
*/
const map_field_names = {
email_address: "email_id",
mobile_number: "mobile_no",
map_to_first_name: "first_name",
map_to_last_name: "last_name",
};
Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => {
@@ -38,15 +40,27 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
label: __("Primary Contact Details"),
collapsible: 1,
},
{
label: __("First Name"),
fieldname: "map_to_first_name",
fieldtype: "Data",
depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
label: __("Last Name"),
fieldname: "map_to_last_name",
fieldtype: "Data",
depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
fieldtype: "Column Break",
},
{
label: __("Email Id"),
fieldname: "email_address",
fieldtype: "Data",
options: "Email",
},
{
fieldtype: "Column Break",
},
{
label: __("Mobile Number"),
fieldname: "mobile_number",

View File

@@ -115,6 +115,10 @@ erpnext.sales_common = {
company() {
super.company();
this.set_default_company_address();
if (!this.is_onload) {
// we don't want to override the mapped contact from prevdoc
this.set_default_company_contact_person();
}
}
set_default_company_address() {
@@ -139,6 +143,24 @@ erpnext.sales_common = {
}
}
set_default_company_contact_person() {
if (!frappe.meta.has_field(this.frm.doc.doctype, "company_contact_person")) {
return;
}
if (this.frm.doc.company) {
frappe.db
.get_value("Company", this.frm.doc.company, "default_sales_contact")
.then((r) => {
if (r.message?.default_sales_contact) {
this.frm.set_value("company_contact_person", r.message.default_sales_contact);
} else {
this.frm.set_value("company_contact_person", "");
}
});
}
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function () {

View File

@@ -56,6 +56,8 @@
"customer_primary_contact",
"mobile_no",
"email_id",
"first_name",
"last_name",
"tax_tab",
"taxation_section",
"tax_id",
@@ -581,6 +583,20 @@
"no_copy": 1,
"options": "Prospect",
"print_hide": 1
},
{
"fetch_from": "customer_primary_contact.first_name",
"fieldname": "first_name",
"fieldtype": "Read Only",
"hidden": 1,
"label": "First Name"
},
{
"fetch_from": "customer_primary_contact.last_name",
"fieldname": "last_name",
"fieldtype": "Read Only",
"hidden": 1,
"label": "Last Name"
}
],
"icon": "fa fa-user",
@@ -594,7 +610,7 @@
"link_fieldname": "party"
}
],
"modified": "2024-06-17 03:24:59.612974",
"modified": "2025-03-05 10:01:47.885574",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -672,6 +688,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "customer_group,territory, mobile_no,primary_address",
"show_name_in_global_search": 1,
"sort_field": "modified",
@@ -679,4 +696,4 @@
"states": [],
"title_field": "customer_name",
"track_changes": 1
}
}

View File

@@ -61,12 +61,14 @@ class Customer(TransactionBase):
disabled: DF.Check
dn_required: DF.Check
email_id: DF.ReadOnly | None
first_name: DF.ReadOnly | None
gender: DF.Link | None
image: DF.AttachImage | None
industry: DF.Link | None
is_frozen: DF.Check
is_internal_customer: DF.Check
language: DF.Link | None
last_name: DF.ReadOnly | None
lead_name: DF.Link | None
loyalty_program: DF.Link | None
loyalty_program_tier: DF.Data | None
@@ -248,7 +250,7 @@ class Customer(TransactionBase):
def create_primary_contact(self):
if not self.customer_primary_contact and not self.lead_name:
if self.mobile_no or self.email_id:
if self.mobile_no or self.email_id or self.first_name or self.last_name:
contact = make_contact(self)
self.db_set("customer_primary_contact", contact.name)
self.db_set("mobile_no", self.mobile_no)
@@ -736,6 +738,10 @@ def make_contact(args, is_primary_contact=1):
contact.add_email(args.get("email_id"), is_primary=True)
if args.get("mobile_no"):
contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True)
if args.get("first_name"):
contact.first_name = args.get("first_name")
if args.get("last_name"):
contact.last_name = args.get("last_name")
if flags := args.get("flags"):
contact.insert(ignore_permissions=flags.get("ignore_permissions"))

View File

@@ -252,6 +252,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
lead: this.frm.doc.party_name,
posting_date: this.frm.doc.transaction_date,
company: this.frm.doc.company,
doctype: this.frm.doc.doctype,
},
callback: function (r) {
if (r.message) {

View File

@@ -55,7 +55,7 @@ def search_by_term(search_term, warehouse, price_list):
}
)
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
item_stock_qty, is_stock_item, is_negative_stock_allowed = get_stock_availability(item_code, warehouse)
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
item.update({"actual_qty": item_stock_qty})
@@ -198,7 +198,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
current_date = frappe.utils.today()
for item in items_data:
item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse)
item_prices = frappe.get_all(
"Item Price",

View File

@@ -759,12 +759,16 @@ erpnext.PointOfSale.Controller = class {
const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
const available_qty = resp[0];
const is_stock_item = resp[1];
const is_negative_stock_allowed = resp[2];
frappe.dom.unfreeze();
const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold();
if (is_negative_stock_allowed) return;
if (!(available_qty > 0)) {
if (is_stock_item) {
frappe.model.clear_doc(item_row.doctype, item_row.name);

View File

@@ -37,6 +37,13 @@ frappe.ui.form.on("Company", {
return { filters: { selling: 1 } };
});
frm.set_query("default_sales_contact", function (doc) {
return {
query: "frappe.contacts.doctype.contact.contact.contact_query",
filters: { link_doctype: "Company", link_name: doc.name },
};
});
frm.set_query("default_buying_terms", function () {
return { filters: { buying: 1 } };
});

View File

@@ -103,6 +103,7 @@
"total_monthly_sales",
"column_break_goals",
"default_selling_terms",
"default_sales_contact",
"default_warehouse_for_sales_return",
"credit_limit",
"transactions_annual_history",
@@ -851,6 +852,12 @@
"fieldtype": "Select",
"label": "Reconciliation Takes Effect On",
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
},
{
"fieldname": "default_sales_contact",
"fieldtype": "Link",
"label": "Default Sales Contact",
"options": "Contact"
}
],
"icon": "fa fa-building",
@@ -858,7 +865,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-08-25 18:34:03.602046",
"modified": "2025-11-16 16:51:27.624096",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -66,6 +66,7 @@ class Company(NestedSet):
default_payable_account: DF.Link | None
default_provisional_account: DF.Link | None
default_receivable_account: DF.Link | None
default_sales_contact: DF.Link | None
default_selling_terms: DF.Link | None
default_warehouse_for_sales_return: DF.Link | None
depreciation_cost_center: DF.Link | None

View File

@@ -3,8 +3,11 @@
import frappe
from frappe.defaults import get_user_default
from frappe.utils import cint
from erpnext.accounts.utils import get_fiscal_years
def boot_session(bootinfo):
"""boot session - send website info if guest"""
@@ -53,6 +56,11 @@ def boot_session(bootinfo):
)
party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""")
fiscal_year = get_fiscal_years(
frappe.utils.nowdate(), company=get_user_default("company"), boolean=True
)
if fiscal_year:
bootinfo.current_fiscal_year = fiscal_year[0]
bootinfo.party_account_types = frappe._dict(party_account_types)
bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company")

View File

@@ -55,6 +55,11 @@ class ItemPrice(Document):
if not frappe.db.exists("Item", self.item_code):
frappe.throw(_("Item {0} not found.").format(self.item_code))
if self.uom and not frappe.db.exists(
"UOM Conversion Detail", {"parenttype": "Item", "parent": self.item_code, "uom": self.uom}
):
frappe.throw(_("UOM {0} not found in Item {1}").format(self.uom, self.item_code))
def update_price_list_details(self):
if self.price_list:
price_list_details = frappe.db.get_value(

View File

@@ -1338,8 +1338,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
this.frm.script_manager.copy_from_first_row("items", row, ["expense_account", "cost_center"]);
}
if (!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if (!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
if (this.frm.doc.from_warehouse) row.s_warehouse = this.frm.doc.from_warehouse;
if (this.frm.doc.to_warehouse) row.t_warehouse = this.frm.doc.to_warehouse;
if (cint(frappe.user_defaults?.use_serial_batch_fields)) {
frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1);

View File

@@ -16,7 +16,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
get_available_serial_nos,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_stock_balance
class OpeningEntryAccountError(frappe.ValidationError):
@@ -1061,6 +1061,7 @@ class StockReconciliation(StockController):
self.posting_date,
self.posting_time,
self.name,
sle_creation,
)
precesion = row.precision("current_qty")
@@ -1222,8 +1223,11 @@ class StockReconciliation(StockController):
return current_qty
def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, posting_time, voucher_no):
def get_batch_qty_for_stock_reco(
item_code, warehouse, batch_no, posting_date, posting_time, voucher_no, sle_creation
):
ledger = frappe.qb.DocType("Stock Ledger Entry")
posting_datetime = get_combine_datetime(posting_date, posting_time)
query = (
frappe.qb.from_(ledger)
@@ -1236,12 +1240,11 @@ def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no, posting_date, p
& (ledger.docstatus == 1)
& (ledger.is_cancelled == 0)
& (ledger.batch_no == batch_no)
& (ledger.posting_date <= posting_date)
& (
CombineDatetime(ledger.posting_date, ledger.posting_time)
<= CombineDatetime(posting_date, posting_time)
)
& (ledger.voucher_no != voucher_no)
& (
(ledger.posting_datetime < posting_datetime)
| ((ledger.posting_datetime == posting_datetime) & (ledger.creation < sle_creation))
)
)
.groupby(ledger.batch_no)
)

View File

@@ -45,6 +45,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
def test_reco_for_moving_average(self):
self._test_reco_sle_gle("Moving Average")
@change_settings("Stock Settings", {"allow_negative_stock": 1})
def _test_reco_sle_gle(self, valuation_method):
item_code = self.make_item(properties={"valuation_method": valuation_method}).name

View File

@@ -2240,9 +2240,11 @@ def validate_reserved_stock(kwargs):
kwargs.ignore_voucher_nos = [kwargs.voucher_no]
if kwargs.serial_no:
kwargs.serial_nos = kwargs.serial_no.split("\n")
validate_reserved_serial_nos(kwargs)
elif kwargs.batch_no:
kwargs.batch_nos = [kwargs.batch_no]
validate_reserved_batch_nos(kwargs)
elif kwargs.serial_and_batch_bundle: