mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-02 04:58:29 +00:00
Merge pull request #50595 from frappe/version-15-hotfix
This commit is contained in:
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -103,6 +103,7 @@ class Asset(AccountsController):
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
"Submitted",
|
||||
"Cancelled",
|
||||
"Partially Depreciated",
|
||||
"Fully Depreciated",
|
||||
"Sold",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } };
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user