mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-09 08:02:51 +00:00
Merge pull request #52926 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -136,6 +136,8 @@ class BankTransaction(Document):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.ignore_linked_doctypes = ["GL Entry"]
|
||||||
|
|
||||||
for payment_entry in self.payment_entries:
|
for payment_entry in self.payment_entries:
|
||||||
self.delink_payment_entry(payment_entry)
|
self.delink_payment_entry(payment_entry)
|
||||||
|
|
||||||
@@ -370,11 +372,12 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
|
|||||||
("unallocated_amount", "bank_account"),
|
("unallocated_amount", "bank_account"),
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
|
||||||
|
|
||||||
if bt.bank_account != gl_bank_account:
|
if bt_bank_account != gl_bank_account:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||||
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
bt_bank_account, payment_entry.payment_entry, gl_bank_account
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from frappe import _
|
from frappe import _, cint
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, add_years, cstr, getdate
|
from frappe.utils import add_days, add_years, cstr, getdate
|
||||||
|
|
||||||
@@ -33,24 +33,6 @@ class FiscalYear(Document):
|
|||||||
self.validate_dates()
|
self.validate_dates()
|
||||||
self.validate_overlap()
|
self.validate_overlap()
|
||||||
|
|
||||||
if not self.is_new():
|
|
||||||
year_start_end_dates = frappe.db.sql(
|
|
||||||
"""select year_start_date, year_end_date
|
|
||||||
from `tabFiscal Year` where name=%s""",
|
|
||||||
(self.name),
|
|
||||||
)
|
|
||||||
|
|
||||||
if year_start_end_dates:
|
|
||||||
if (
|
|
||||||
getdate(self.year_start_date) != year_start_end_dates[0][0]
|
|
||||||
or getdate(self.year_end_date) != year_start_end_dates[0][1]
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_dates(self):
|
def validate_dates(self):
|
||||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||||
if self.is_short_year:
|
if self.is_short_year:
|
||||||
@@ -66,28 +48,20 @@ class FiscalYear(Document):
|
|||||||
frappe.exceptions.InvalidDates,
|
frappe.exceptions.InvalidDates,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
check_duplicate_fiscal_year(self)
|
|
||||||
frappe.cache().delete_value("fiscal_years")
|
|
||||||
|
|
||||||
def on_trash(self):
|
|
||||||
frappe.cache().delete_value("fiscal_years")
|
|
||||||
|
|
||||||
def validate_overlap(self):
|
def validate_overlap(self):
|
||||||
existing_fiscal_years = frappe.db.sql(
|
fy = frappe.qb.DocType("Fiscal Year")
|
||||||
"""select name from `tabFiscal Year`
|
|
||||||
where (
|
name = self.name or self.year
|
||||||
(%(year_start_date)s between year_start_date and year_end_date)
|
|
||||||
or (%(year_end_date)s between year_start_date and year_end_date)
|
existing_fiscal_years = (
|
||||||
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
|
frappe.qb.from_(fy)
|
||||||
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
|
.select(fy.name)
|
||||||
) and name!=%(name)s""",
|
.where(
|
||||||
{
|
(fy.year_start_date <= self.year_end_date)
|
||||||
"year_start_date": self.year_start_date,
|
& (fy.year_end_date >= self.year_start_date)
|
||||||
"year_end_date": self.year_end_date,
|
& (fy.name != name)
|
||||||
"name": self.name or "No Name",
|
)
|
||||||
},
|
.run(as_dict=True)
|
||||||
as_dict=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_fiscal_years:
|
if existing_fiscal_years:
|
||||||
@@ -110,37 +84,30 @@ class FiscalYear(Document):
|
|||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||||
).format(existing.name),
|
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
|
||||||
frappe.NameError,
|
frappe.NameError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def check_duplicate_fiscal_year(doc):
|
|
||||||
year_start_end_dates = frappe.db.sql(
|
|
||||||
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
|
|
||||||
(doc.name),
|
|
||||||
)
|
|
||||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
|
||||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
|
||||||
not frappe.flags.in_test
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
|
||||||
).format(fiscal_year)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def auto_create_fiscal_year():
|
def auto_create_fiscal_year():
|
||||||
for d in frappe.db.sql(
|
fy = frappe.qb.DocType("Fiscal Year")
|
||||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
|
||||||
):
|
# Skipped auto-creating Short Year, as it has very rare use case.
|
||||||
|
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
|
||||||
|
follow_up_date = add_days(getdate(), days=3)
|
||||||
|
fiscal_year = (
|
||||||
|
frappe.qb.from_(fy)
|
||||||
|
.select(fy.name)
|
||||||
|
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in fiscal_year:
|
||||||
try:
|
try:
|
||||||
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
||||||
|
|
||||||
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
|
new_fy = frappe.new_doc("Fiscal Year")
|
||||||
|
new_fy.disabled = cint(current_fy.disabled)
|
||||||
|
|
||||||
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
|
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
|
||||||
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
|
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
|
||||||
@@ -148,6 +115,10 @@ def auto_create_fiscal_year():
|
|||||||
start_year = cstr(new_fy.year_start_date.year)
|
start_year = cstr(new_fy.year_start_date.year)
|
||||||
end_year = cstr(new_fy.year_end_date.year)
|
end_year = cstr(new_fy.year_end_date.year)
|
||||||
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
||||||
|
|
||||||
|
for row in current_fy.companies:
|
||||||
|
new_fy.append("companies", {"company": row.company})
|
||||||
|
|
||||||
new_fy.auto_created = 1
|
new_fy.auto_created = 1
|
||||||
|
|
||||||
new_fy.insert(ignore_permissions=True)
|
new_fy.insert(ignore_permissions=True)
|
||||||
|
|||||||
@@ -15,13 +15,14 @@
|
|||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company"
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-28 18:01:53.495929",
|
"modified": "2026-02-20 23:02:26.193606",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Fiscal Year Company",
|
"name": "Fiscal Year Company",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
company: DF.Link | None
|
company: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Reference Type",
|
"label": "Reference Type",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
|
||||||
"fieldname": "reference_due_date",
|
"fieldname": "reference_due_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Reference Due Date",
|
"label": "Reference Due Date",
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-11-27 12:23:33.157655",
|
"modified": "2026-02-19 17:01:22.642454",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
|
|||||||
"Fees",
|
"Fees",
|
||||||
"Full and Final Statement",
|
"Full and Final Statement",
|
||||||
"Payment Entry",
|
"Payment Entry",
|
||||||
|
"Bank Transaction",
|
||||||
]
|
]
|
||||||
user_remark: DF.SmallText | None
|
user_remark: DF.SmallText | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -516,12 +516,16 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
frm.set_value("contact_email", "");
|
frm.set_value("contact_email", "");
|
||||||
frm.set_value("contact_person", "");
|
frm.set_value("contact_person", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
|
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
|
||||||
if (!frm.doc.posting_date) {
|
if (!frm.doc.posting_date) {
|
||||||
frappe.msgprint(__("Please select Posting Date before selecting Party"));
|
frappe.msgprint(__("Please select Posting Date before selecting Party"));
|
||||||
frm.set_value("party", "");
|
frm.set_value("party", "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
erpnext.utils.get_employee_contact_details(frm);
|
||||||
|
|
||||||
frm.set_party_account_based_on_party = true;
|
frm.set_party_account_based_on_party = true;
|
||||||
|
|
||||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
@@ -1465,17 +1469,16 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
// set taxes table
|
// set taxes table
|
||||||
if (r.message) {
|
let taxes = r.message;
|
||||||
for (let tax of r.message) {
|
taxes.forEach((tax) => {
|
||||||
if (tax.charge_type === "On Net Total") {
|
if (tax.charge_type === "On Net Total") {
|
||||||
tax.charge_type = "On Paid Amount";
|
tax.charge_type = "On Paid Amount";
|
||||||
}
|
}
|
||||||
frm.add_child("taxes", tax);
|
});
|
||||||
}
|
frm.set_value("taxes", taxes);
|
||||||
frm.events.apply_taxes(frm);
|
frm.events.apply_taxes(frm);
|
||||||
frm.events.set_unallocated_amount(frm);
|
frm.events.set_unallocated_amount(frm);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ class PaymentRequest(Document):
|
|||||||
row_number += TO_SKIP_NEW_ROW
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist()
|
||||||
def make_payment_request(**args):
|
def make_payment_request(**args):
|
||||||
"""Make payment request"""
|
"""Make payment request"""
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
"sec_warehouse",
|
"sec_warehouse",
|
||||||
"set_warehouse",
|
"set_warehouse",
|
||||||
"items_section",
|
"items_section",
|
||||||
"update_stock",
|
|
||||||
"scan_barcode",
|
"scan_barcode",
|
||||||
"last_scanned_warehouse",
|
"last_scanned_warehouse",
|
||||||
"items",
|
"items",
|
||||||
@@ -574,7 +573,6 @@
|
|||||||
"label": "Warehouse"
|
"label": "Warehouse"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "update_stock",
|
|
||||||
"fieldname": "set_warehouse",
|
"fieldname": "set_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Source Warehouse",
|
"label": "Source Warehouse",
|
||||||
@@ -588,15 +586,6 @@
|
|||||||
"oldfieldtype": "Section Break",
|
"oldfieldtype": "Section Break",
|
||||||
"options": "fa fa-shopping-cart"
|
"options": "fa fa-shopping-cart"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "update_stock",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Update Stock",
|
|
||||||
"oldfieldname": "update_stock",
|
|
||||||
"oldfieldtype": "Check",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "scan_barcode",
|
"fieldname": "scan_barcode",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@@ -1582,7 +1571,7 @@
|
|||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-04 22:22:31.471752",
|
"modified": "2026-02-22 04:18:50.691218",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
@@ -1627,6 +1616,7 @@
|
|||||||
"role": "All"
|
"role": "All"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
total_taxes_and_charges: DF.Currency
|
total_taxes_and_charges: DF.Currency
|
||||||
update_billed_amount_in_delivery_note: DF.Check
|
update_billed_amount_in_delivery_note: DF.Check
|
||||||
update_billed_amount_in_sales_order: DF.Check
|
update_billed_amount_in_sales_order: DF.Check
|
||||||
update_stock: DF.Check
|
|
||||||
write_off_account: DF.Link | None
|
write_off_account: DF.Link | None
|
||||||
write_off_amount: DF.Currency
|
write_off_amount: DF.Currency
|
||||||
write_off_cost_center: DF.Link | None
|
write_off_cost_center: DF.Link | None
|
||||||
@@ -652,7 +651,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
"tax_category",
|
"tax_category",
|
||||||
"ignore_pricing_rule",
|
"ignore_pricing_rule",
|
||||||
"company_address",
|
"company_address",
|
||||||
"update_stock",
|
|
||||||
):
|
):
|
||||||
if not for_validate:
|
if not for_validate:
|
||||||
self.set(fieldname, profile.get(fieldname))
|
self.set(fieldname, profile.get(fieldname))
|
||||||
|
|||||||
@@ -1101,7 +1101,6 @@ def create_pos_invoice(**args):
|
|||||||
|
|
||||||
pos_inv = frappe.new_doc("POS Invoice")
|
pos_inv = frappe.new_doc("POS Invoice")
|
||||||
pos_inv.update(args)
|
pos_inv.update(args)
|
||||||
pos_inv.update_stock = 1
|
|
||||||
pos_inv.is_pos = 1
|
pos_inv.is_pos = 1
|
||||||
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
|
|
||||||
sales_invoice.is_consolidated = 1
|
sales_invoice.is_consolidated = 1
|
||||||
sales_invoice.set_posting_time = 1
|
sales_invoice.set_posting_time = 1
|
||||||
|
sales_invoice.update_stock = 1
|
||||||
|
|
||||||
if not sales_invoice.posting_date:
|
if not sales_invoice.posting_date:
|
||||||
sales_invoice.posting_date = getdate(self.posting_date)
|
sales_invoice.posting_date = getdate(self.posting_date)
|
||||||
@@ -174,6 +175,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
|
|
||||||
credit_note.is_consolidated = 1
|
credit_note.is_consolidated = 1
|
||||||
credit_note.set_posting_time = 1
|
credit_note.set_posting_time = 1
|
||||||
|
credit_note.update_stock = 1
|
||||||
credit_note.posting_date = getdate(self.posting_date)
|
credit_note.posting_date = getdate(self.posting_date)
|
||||||
credit_note.posting_time = get_time(self.posting_time)
|
credit_note.posting_time = get_time(self.posting_time)
|
||||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
"validate_stock_on_save",
|
"validate_stock_on_save",
|
||||||
"print_receipt_on_order_complete",
|
"print_receipt_on_order_complete",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"update_stock",
|
|
||||||
"ignore_pricing_rule",
|
"ignore_pricing_rule",
|
||||||
"allow_rate_change",
|
"allow_rate_change",
|
||||||
"allow_discount_change",
|
"allow_discount_change",
|
||||||
@@ -297,7 +296,6 @@
|
|||||||
"options": "Print Format"
|
"options": "Print Format"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "update_stock",
|
|
||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Warehouse",
|
"label": "Warehouse",
|
||||||
@@ -312,14 +310,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Pricing Rule"
|
"label": "Ignore Pricing Rule"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"fieldname": "update_stock",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Update Stock",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "hide_unavailable_items",
|
"fieldname": "hide_unavailable_items",
|
||||||
@@ -432,7 +422,7 @@
|
|||||||
"link_fieldname": "pos_profile"
|
"link_fieldname": "pos_profile"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-04-14 15:58:20.497426",
|
"modified": "2026-02-22 04:17:03.308876",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile",
|
"name": "POS Profile",
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class POSProfile(Document):
|
|||||||
tax_category: DF.Link | None
|
tax_category: DF.Link | None
|
||||||
taxes_and_charges: DF.Link | None
|
taxes_and_charges: DF.Link | None
|
||||||
tc_name: DF.Link | None
|
tc_name: DF.Link | None
|
||||||
update_stock: DF.Check
|
|
||||||
validate_stock_on_save: DF.Check
|
validate_stock_on_save: DF.Check
|
||||||
warehouse: DF.Link
|
warehouse: DF.Link
|
||||||
write_off_account: DF.Link
|
write_off_account: DF.Link
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Apply On",
|
"label": "Apply On",
|
||||||
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
|
"options": "Item Code\nItem Group\nBrand\nTransaction",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -657,7 +657,7 @@
|
|||||||
"icon": "fa fa-gift",
|
"icon": "fa fa-gift",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-20 11:40:07.096854",
|
"modified": "2026-02-17 12:24:07.553505",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule",
|
"name": "Pricing Rule",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class PricingRule(Document):
|
|||||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||||
apply_discount_on_rate: DF.Check
|
apply_discount_on_rate: DF.Check
|
||||||
apply_multiple_pricing_rules: DF.Check
|
apply_multiple_pricing_rules: DF.Check
|
||||||
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
|
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
|
||||||
apply_recursion_over: DF.Float
|
apply_recursion_over: DF.Float
|
||||||
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
||||||
brands: DF.Table[PricingRuleBrand]
|
brands: DF.Table[PricingRuleBrand]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
"depends_on": "eval:parent.apply_on == 'Brand'",
|
||||||
"fieldname": "brand",
|
"fieldname": "brand",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2019-03-24 14:48:59.649168",
|
"modified": "2026-02-17 12:17:13.073587",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule Brand",
|
"name": "Pricing Rule Brand",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
"depends_on": "eval:parent.apply_on == 'Item Group'",
|
||||||
"fieldname": "item_group",
|
"fieldname": "item_group",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2019-03-24 14:48:59.649168",
|
"modified": "2026-02-17 12:16:57.778471",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule Item Group",
|
"name": "Pricing Rule Item Group",
|
||||||
|
|||||||
@@ -1729,10 +1729,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
project_doc.db_update()
|
project_doc.db_update()
|
||||||
|
|
||||||
def validate_supplier_invoice(self):
|
def validate_supplier_invoice(self):
|
||||||
if self.bill_date:
|
|
||||||
if getdate(self.bill_date) > getdate(self.posting_date):
|
|
||||||
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
|
|
||||||
|
|
||||||
if self.bill_no:
|
if self.bill_no:
|
||||||
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
|
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
|
||||||
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
|
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
|
||||||
|
|||||||
@@ -854,9 +854,6 @@ class SalesInvoice(SellingController):
|
|||||||
if selling_price_list:
|
if selling_price_list:
|
||||||
self.set("selling_price_list", selling_price_list)
|
self.set("selling_price_list", selling_price_list)
|
||||||
|
|
||||||
if not for_validate:
|
|
||||||
self.update_stock = cint(pos.get("update_stock"))
|
|
||||||
|
|
||||||
# set pos values in items
|
# set pos values in items
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get("item_code"):
|
if item.get("item_code"):
|
||||||
@@ -1097,7 +1094,9 @@ class SalesInvoice(SellingController):
|
|||||||
d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
|
d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
|
||||||
|
|
||||||
def update_packing_list(self):
|
def update_packing_list(self):
|
||||||
if cint(self.update_stock) == 1:
|
if self.doctype == "POS Invoice" or (
|
||||||
|
self.doctype == "Sales Invoice" and cint(self.update_stock) == 1
|
||||||
|
):
|
||||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||||
|
|
||||||
make_packing_list(self)
|
make_packing_list(self)
|
||||||
|
|||||||
@@ -840,6 +840,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Incoming Rate (Costing)",
|
"label": "Incoming Rate (Costing)",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"non_negative": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -983,7 +984,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-12 16:33:55.503777",
|
"modified": "2026-02-23 14:37:14.853941",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
|
||||||
|
|
||||||
|
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
|
||||||
|
|
||||||
|
<p>{{ _("Fiscal Year Details") }}</p>
|
||||||
|
|
||||||
|
<table style="margin-bottom: 1rem; width: 70%">
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
|
||||||
|
<td>{{ doc.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
|
||||||
|
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
|
||||||
|
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if doc.companies|length > 0 %}
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
|
||||||
|
{% if doc.companies|length < 2 %}
|
||||||
|
{{ _("Company") }}
|
||||||
|
{% else %}
|
||||||
|
{{ _("Companies") }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ doc.companies[0].company }}</td>
|
||||||
|
</tr>
|
||||||
|
{% for idx in range(1, doc.companies|length) %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ doc.companies[idx].company }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if doc.disabled %}
|
||||||
|
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"attach_print": 0,
|
"attach_print": 0,
|
||||||
"channel": "Email",
|
"channel": "Email",
|
||||||
"condition": "doc.auto_created",
|
"condition": "doc.auto_created == 1",
|
||||||
"creation": "2018-04-25 14:19:05.440361",
|
"creation": "2018-04-25 14:19:05.440361",
|
||||||
"days_in_advance": 0,
|
"days_in_advance": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@@ -11,19 +11,22 @@
|
|||||||
"event": "New",
|
"event": "New",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"message": "<h3>{{_(\"Fiscal Year\")}}</h3>\n\n<p>{{ _(\"New fiscal year created :- \") }} {{ doc.name }}</p>",
|
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
|
||||||
"modified": "2018-04-25 14:30:38.588534",
|
"message_type": "HTML",
|
||||||
|
"modified": "2026-02-21 15:59:07.775679",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Notification for new fiscal year",
|
"name": "Notification for new fiscal year",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"recipients": [
|
"recipients": [
|
||||||
{
|
{
|
||||||
"email_by_role": "Accounts User"
|
"receiver_by_role": "Accounts Manager"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email_by_role": "Accounts Manager"
|
"receiver_by_role": "Accounts User"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subject": "Notification for new fiscal year {{ doc.name }}"
|
"send_system_notification": 0,
|
||||||
|
"send_to_all_assignees": 0,
|
||||||
|
"subject": "{{ _(\"New Fiscal Year {0} - Review Required\").format(doc.name) }}"
|
||||||
}
|
}
|
||||||
@@ -7,18 +7,16 @@ from frappe import _, msgprint, qb, scrub
|
|||||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.query_builder.functions import Abs, Count, Date, Sum
|
from frappe.query_builder.functions import Abs, Date, Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
add_years,
|
|
||||||
cint,
|
cint,
|
||||||
cstr,
|
cstr,
|
||||||
date_diff,
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
formatdate,
|
formatdate,
|
||||||
get_last_day,
|
get_last_day,
|
||||||
get_timestamp,
|
|
||||||
getdate,
|
getdate,
|
||||||
nowdate,
|
nowdate,
|
||||||
)
|
)
|
||||||
@@ -302,19 +300,9 @@ def complete_contact_details(party_details):
|
|||||||
contact_details = frappe._dict()
|
contact_details = frappe._dict()
|
||||||
|
|
||||||
if party_details.party_type == "Employee":
|
if party_details.party_type == "Employee":
|
||||||
contact_details = frappe.db.get_value(
|
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
|
||||||
"Employee",
|
|
||||||
party_details.party,
|
|
||||||
[
|
|
||||||
"employee_name as contact_display",
|
|
||||||
"prefered_email as contact_email",
|
|
||||||
"cell_number as contact_mobile",
|
|
||||||
"designation as contact_designation",
|
|
||||||
"department as contact_department",
|
|
||||||
],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
contact_details = get_employee_contact(party_details.party)
|
||||||
contact_details.update({"contact_person": None, "contact_phone": None})
|
contact_details.update({"contact_person": None, "contact_phone": None})
|
||||||
elif party_details.contact_person:
|
elif party_details.contact_person:
|
||||||
contact_details = frappe.db.get_value(
|
contact_details = frappe.db.get_value(
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||||
)
|
)
|
||||||
sinv.is_return = 1
|
sinv.is_return = 1
|
||||||
|
sinv.items[0].allow_zero_valuation_rate = 1
|
||||||
sinv = sinv.save().submit()
|
sinv = sinv.save().submit()
|
||||||
|
|
||||||
filters = frappe._dict(
|
filters = frappe._dict(
|
||||||
|
|||||||
@@ -159,11 +159,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
|||||||
|
|
||||||
|
|
||||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||||
labels = [d.get("label") for d in columns[4:]]
|
labels = [d.get("label") for d in columns[2:]]
|
||||||
|
|
||||||
income_data, expense_data, net_profit = [], [], []
|
income_data, expense_data, net_profit = [], [], []
|
||||||
|
|
||||||
for p in columns[4:]:
|
for p in columns[2:]:
|
||||||
if income:
|
if income:
|
||||||
income_data.append(income[-2].get(p.get("fieldname")))
|
income_data.append(income[-2].get(p.get("fieldname")))
|
||||||
if expense:
|
if expense:
|
||||||
|
|||||||
@@ -454,7 +454,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
|
|||||||
dimensions_dict = frappe._dict()
|
dimensions_dict = frappe._dict()
|
||||||
if entry and active_dimensions:
|
if entry and active_dimensions:
|
||||||
for dim in active_dimensions:
|
for dim in active_dimensions:
|
||||||
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
|
if entry_dimension := entry.get(dim.fieldname):
|
||||||
|
dimensions_dict[dim.fieldname] = entry_dimension
|
||||||
return dimensions_dict
|
return dimensions_dict
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ class PurchaseOrder(BuyingController):
|
|||||||
self.set_has_unit_price_items()
|
self.set_has_unit_price_items()
|
||||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||||
|
|
||||||
|
if self.is_subcontracted:
|
||||||
|
self.status_updater[0]["source_field"] = "fg_item_qty"
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
super().validate()
|
super().validate()
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
|||||||
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
||||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||||
|
|
||||||
|
this.frm.add_custom_button(__("Update Items"), () => {
|
||||||
|
erpnext.utils.update_child_items({
|
||||||
|
frm: this.frm,
|
||||||
|
child_docname: "items",
|
||||||
|
cannot_add_row: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
} else if (this.frm.doc.docstatus === 0) {
|
} else if (this.frm.doc.docstatus === 0) {
|
||||||
erpnext.set_unit_price_items_note(this.frm);
|
erpnext.set_unit_price_items_note(this.frm);
|
||||||
|
|
||||||
|
|||||||
@@ -345,3 +345,15 @@ def set_expired_status():
|
|||||||
""",
|
""",
|
||||||
(nowdate()),
|
(nowdate()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_purchased_items(supplier_quotation: str):
|
||||||
|
return frappe._dict(
|
||||||
|
frappe.get_all(
|
||||||
|
"Purchase Order Item",
|
||||||
|
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
|
||||||
|
fields=["supplier_quotation_item", "sum(qty)"],
|
||||||
|
group_by="supplier_quotation_item",
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,15 +2,115 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, today
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseOrder(FrappeTestCase):
|
class TestPurchaseOrder(FrappeTestCase):
|
||||||
|
def test_update_child_supplier_quotation_add_item(self):
|
||||||
|
sq = frappe.copy_doc(test_records[0])
|
||||||
|
sq.submit()
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": sq.items[0].rate,
|
||||||
|
"qty": 5,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"rate": 300,
|
||||||
|
"qty": 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
sq.reload()
|
||||||
|
self.assertEqual(sq.get("items")[0].qty, 5)
|
||||||
|
self.assertEqual(sq.get("items")[1].rate, 300)
|
||||||
|
|
||||||
|
def test_update_supplier_quotation_child_rate_disallow(self):
|
||||||
|
sq = frappe.copy_doc(test_records[0])
|
||||||
|
sq.submit()
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": 300,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertRaises(
|
||||||
|
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_supplier_quotation_child_remove_item(self):
|
||||||
|
sq = frappe.copy_doc(test_records[0])
|
||||||
|
sq.submit()
|
||||||
|
po = make_purchase_order(sq.name)
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": sq.items[0].rate,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"rate": 300,
|
||||||
|
"qty": 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
po.get("items")[0].schedule_date = add_days(today(), 1)
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
po.submit()
|
||||||
|
sq.reload()
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"rate": 300,
|
||||||
|
"qty": 3,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.savepoint("before_cancel")
|
||||||
|
# check if item having purchase order can be removed
|
||||||
|
self.assertRaises(
|
||||||
|
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||||
|
)
|
||||||
|
frappe.db.rollback(save_point="before_cancel")
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": sq.items[0].rate,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
sq.reload()
|
||||||
|
self.assertEqual(len(sq.get("items")), 1)
|
||||||
|
|
||||||
def test_supplier_quotation_qty(self):
|
def test_supplier_quotation_qty(self):
|
||||||
sq = frappe.copy_doc(test_records[0])
|
sq = frappe.copy_doc(test_records[0])
|
||||||
sq.items[0].qty = 0
|
sq.items[0].qty = 0
|
||||||
|
|||||||
@@ -3678,7 +3678,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
|||||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||||
|
|
||||||
if child_doctype == "Purchase Order Item":
|
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
|
||||||
# Initialized value will update in parent validation
|
# Initialized value will update in parent validation
|
||||||
child_item.base_rate = 1
|
child_item.base_rate = 1
|
||||||
child_item.base_amount = 1
|
child_item.base_amount = 1
|
||||||
@@ -3696,7 +3696,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
|||||||
return child_item
|
return child_item
|
||||||
|
|
||||||
|
|
||||||
def validate_child_on_delete(row, parent):
|
def validate_child_on_delete(row, parent, ordered_item=None):
|
||||||
"""Check if partially transacted item (row) is being deleted."""
|
"""Check if partially transacted item (row) is being deleted."""
|
||||||
if parent.doctype == "Sales Order":
|
if parent.doctype == "Sales Order":
|
||||||
if flt(row.delivered_qty):
|
if flt(row.delivered_qty):
|
||||||
@@ -3724,7 +3724,7 @@ def validate_child_on_delete(row, parent):
|
|||||||
row.idx, row.item_code
|
row.idx, row.item_code
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if parent.doctype in ["Purchase Order", "Sales Order"]:
|
||||||
if flt(row.billed_amt):
|
if flt(row.billed_amt):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||||
@@ -3732,6 +3732,10 @@ def validate_child_on_delete(row, parent):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if parent.doctype == "Quotation":
|
||||||
|
if ordered_item.get(row.name):
|
||||||
|
frappe.throw(_("Cannot delete an item which has been ordered"))
|
||||||
|
|
||||||
|
|
||||||
def update_bin_on_delete(row, doctype):
|
def update_bin_on_delete(row, doctype):
|
||||||
"""Update bin for deleted item (row)."""
|
"""Update bin for deleted item (row)."""
|
||||||
@@ -3756,7 +3760,7 @@ def update_bin_on_delete(row, doctype):
|
|||||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||||
|
|
||||||
|
|
||||||
def validate_and_delete_children(parent, data) -> bool:
|
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||||
deleted_children = []
|
deleted_children = []
|
||||||
updated_item_names = [d.get("docname") for d in data]
|
updated_item_names = [d.get("docname") for d in data]
|
||||||
for item in parent.items:
|
for item in parent.items:
|
||||||
@@ -3764,7 +3768,7 @@ def validate_and_delete_children(parent, data) -> bool:
|
|||||||
deleted_children.append(item)
|
deleted_children.append(item)
|
||||||
|
|
||||||
for d in deleted_children:
|
for d in deleted_children:
|
||||||
validate_child_on_delete(d, parent)
|
validate_child_on_delete(d, parent, ordered_item)
|
||||||
d.cancel()
|
d.cancel()
|
||||||
d.delete()
|
d.delete()
|
||||||
|
|
||||||
@@ -3773,8 +3777,8 @@ def validate_and_delete_children(parent, data) -> bool:
|
|||||||
|
|
||||||
# need to update ordered qty in Material Request first
|
# need to update ordered qty in Material Request first
|
||||||
# bin uses Material Request Items to recalculate & update
|
# bin uses Material Request Items to recalculate & update
|
||||||
|
if parent.doctype not in ["Quotation", "Supplier Quotation"]:
|
||||||
parent.update_prevdoc_status()
|
parent.update_prevdoc_status()
|
||||||
|
|
||||||
for d in deleted_children:
|
for d in deleted_children:
|
||||||
update_bin_on_delete(d, parent.doctype)
|
update_bin_on_delete(d, parent.doctype)
|
||||||
|
|
||||||
@@ -3783,6 +3787,9 @@ def validate_and_delete_children(parent, data) -> bool:
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||||
|
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
|
||||||
|
|
||||||
def check_doc_permissions(doc, perm_type="create"):
|
def check_doc_permissions(doc, perm_type="create"):
|
||||||
try:
|
try:
|
||||||
doc.check_permission(perm_type)
|
doc.check_permission(perm_type)
|
||||||
@@ -3821,7 +3828,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_new_child_item(item_row):
|
def get_new_child_item(item_row):
|
||||||
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
|
child_doctype = parent_doctype + " Item"
|
||||||
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
||||||
|
|
||||||
def is_allowed_zero_qty():
|
def is_allowed_zero_qty():
|
||||||
@@ -3846,6 +3853,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
||||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||||
|
|
||||||
|
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||||
|
if (parent_doctype == "Quotation" and not ordered_items) or (
|
||||||
|
parent_doctype == "Supplier Quotation" and not purchased_items
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
qty_to_check = (
|
||||||
|
ordered_items.get(child_item.name)
|
||||||
|
if parent_doctype == "Quotation"
|
||||||
|
else purchased_items.get(child_item.name)
|
||||||
|
)
|
||||||
|
if qty_to_check:
|
||||||
|
if flt(new_data.get("qty")) < qty_to_check:
|
||||||
|
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||||
|
|
||||||
def should_update_supplied_items(doc) -> bool:
|
def should_update_supplied_items(doc) -> bool:
|
||||||
"""Subcontracted PO can allow following changes *after submit*:
|
"""Subcontracted PO can allow following changes *after submit*:
|
||||||
|
|
||||||
@@ -3888,7 +3910,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||||
|
|
||||||
data = json.loads(trans_items)
|
data = json.loads(trans_items)
|
||||||
|
|
||||||
any_qty_changed = False # updated to true if any item's qty changes
|
any_qty_changed = False # updated to true if any item's qty changes
|
||||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||||
any_conversion_factor_changed = False
|
any_conversion_factor_changed = False
|
||||||
@@ -3896,7 +3917,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||||
|
|
||||||
check_doc_permissions(parent, "write")
|
check_doc_permissions(parent, "write")
|
||||||
|
|
||||||
|
if parent_doctype == "Quotation":
|
||||||
|
ordered_items = get_ordered_items(parent.name)
|
||||||
|
_removed_items = validate_and_delete_children(parent, data, ordered_items)
|
||||||
|
elif parent_doctype == "Supplier Quotation":
|
||||||
|
purchased_items = get_purchased_items(parent.name)
|
||||||
|
_removed_items = validate_and_delete_children(parent, data, purchased_items)
|
||||||
|
else:
|
||||||
_removed_items = validate_and_delete_children(parent, data)
|
_removed_items = validate_and_delete_children(parent, data)
|
||||||
|
|
||||||
items_added_or_removed |= _removed_items
|
items_added_or_removed |= _removed_items
|
||||||
|
|
||||||
for d in data:
|
for d in data:
|
||||||
@@ -3936,7 +3966,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||||
date_unchanged = (
|
date_unchanged = (
|
||||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
(prev_date == getdate(new_date) if prev_date and new_date else False)
|
||||||
|
if parent_doctype not in ["Quotation", "Supplier Quotation"]
|
||||||
|
else None
|
||||||
) # in case of delivery note etc
|
) # in case of delivery note etc
|
||||||
if (
|
if (
|
||||||
rate_unchanged
|
rate_unchanged
|
||||||
@@ -3949,6 +3981,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
validate_quantity(child_item, d)
|
validate_quantity(child_item, d)
|
||||||
|
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||||
|
if not rate_unchanged:
|
||||||
|
frappe.throw(_("Rates cannot be modified for quoted items"))
|
||||||
|
|
||||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||||
any_qty_changed = True
|
any_qty_changed = True
|
||||||
|
|
||||||
@@ -3972,9 +4008,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
rate_unchanged = prev_rate == new_rate
|
rate_unchanged = prev_rate == new_rate
|
||||||
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
||||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||||
|
|
||||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||||
row_rate = flt(d.get("rate"), rate_precision)
|
row_rate = flt(d.get("rate"), rate_precision)
|
||||||
|
|
||||||
|
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||||
)
|
)
|
||||||
@@ -3986,6 +4023,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
child_item.rate = row_rate
|
child_item.rate = row_rate
|
||||||
|
else:
|
||||||
|
child_item.rate = row_rate
|
||||||
|
|
||||||
if d.get("conversion_factor"):
|
if d.get("conversion_factor"):
|
||||||
if child_item.stock_uom == child_item.uom:
|
if child_item.stock_uom == child_item.uom:
|
||||||
@@ -4017,6 +4056,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
if d.get("bom_no") and parent_doctype == "Sales Order":
|
if d.get("bom_no") and parent_doctype == "Sales Order":
|
||||||
child_item.bom_no = d.get("bom_no")
|
child_item.bom_no = d.get("bom_no")
|
||||||
|
|
||||||
|
if parent_doctype in ["Sales Order", "Purchase Order"]:
|
||||||
if flt(child_item.price_list_rate):
|
if flt(child_item.price_list_rate):
|
||||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||||
# if rate is greater than price_list_rate, set margin
|
# if rate is greater than price_list_rate, set margin
|
||||||
@@ -4044,7 +4084,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
child_item.idx = len(parent.items) + 1
|
child_item.idx = len(parent.items) + 1
|
||||||
child_item.insert()
|
child_item.insert()
|
||||||
else:
|
else:
|
||||||
child_item.save()
|
child_item.save(ignore_permissions=True)
|
||||||
|
|
||||||
parent.reload()
|
parent.reload()
|
||||||
parent.flags.ignore_validate_update_after_submit = True
|
parent.flags.ignore_validate_update_after_submit = True
|
||||||
@@ -4058,6 +4098,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.doctype, parent.company, parent.base_grand_total
|
parent.doctype, parent.company, parent.base_grand_total
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if parent_doctype != "Supplier Quotation":
|
||||||
parent.set_payment_schedule()
|
parent.set_payment_schedule()
|
||||||
if parent_doctype == "Purchase Order":
|
if parent_doctype == "Purchase Order":
|
||||||
parent.set_tax_withholding()
|
parent.set_tax_withholding()
|
||||||
@@ -4065,7 +4106,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.validate_budget()
|
parent.validate_budget()
|
||||||
if parent.is_against_so():
|
if parent.is_against_so():
|
||||||
parent.update_status_updater()
|
parent.update_status_updater()
|
||||||
else:
|
elif parent_doctype == "Sales Order":
|
||||||
parent.check_credit_limit()
|
parent.check_credit_limit()
|
||||||
|
|
||||||
# reset index of child table
|
# reset index of child table
|
||||||
@@ -4098,7 +4139,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||||
).format(frappe.bold(parent.name))
|
).format(frappe.bold(parent.name))
|
||||||
)
|
)
|
||||||
else: # Sales Order
|
elif parent_doctype == "Sales Order":
|
||||||
parent.validate_selling_price()
|
parent.validate_selling_price()
|
||||||
parent.validate_for_duplicate_items()
|
parent.validate_for_duplicate_items()
|
||||||
parent.validate_warehouse()
|
parent.validate_warehouse()
|
||||||
@@ -4110,6 +4151,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.reload()
|
parent.reload()
|
||||||
validate_workflow_conditions(parent)
|
validate_workflow_conditions(parent)
|
||||||
|
|
||||||
|
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||||
parent.update_blanket_order()
|
parent.update_blanket_order()
|
||||||
parent.update_billing_percentage()
|
parent.update_billing_percentage()
|
||||||
parent.set_status()
|
parent.set_status()
|
||||||
|
|||||||
@@ -483,10 +483,34 @@ class SellingController(StockController):
|
|||||||
sales_order.update_reserved_qty(so_item_rows)
|
sales_order.update_reserved_qty(so_item_rows)
|
||||||
|
|
||||||
def set_incoming_rate(self):
|
def set_incoming_rate(self):
|
||||||
|
def reset_incoming_rate():
|
||||||
|
old_item = next(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
||||||
|
if item.name == d.name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if old_item:
|
||||||
|
old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty"))
|
||||||
|
if (
|
||||||
|
old_item.item_code != d.item_code
|
||||||
|
or old_item.warehouse != d.warehouse
|
||||||
|
or old_qty != qty
|
||||||
|
or old_item.serial_no != d.serial_no
|
||||||
|
or get_serial_nos(old_item.serial_and_batch_bundle)
|
||||||
|
!= get_serial_nos(d.serial_and_batch_bundle)
|
||||||
|
or old_item.batch_no != d.batch_no
|
||||||
|
or get_batch_nos(old_item.serial_and_batch_bundle)
|
||||||
|
!= get_batch_nos(d.serial_and_batch_bundle)
|
||||||
|
):
|
||||||
|
d.incoming_rate = 0
|
||||||
|
|
||||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||||
return
|
return
|
||||||
|
|
||||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
|
||||||
|
|
||||||
allow_at_arms_length_price = frappe.get_cached_value(
|
allow_at_arms_length_price = frappe.get_cached_value(
|
||||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||||
@@ -495,6 +519,8 @@ class SellingController(StockController):
|
|||||||
"Selling Settings", "set_zero_rate_for_expired_batch"
|
"Selling Settings", "set_zero_rate_for_expired_batch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_standalone = self.is_return and not self.return_against
|
||||||
|
|
||||||
old_doc = self.get_doc_before_save()
|
old_doc = self.get_doc_before_save()
|
||||||
items = self.get("items") + (self.get("packed_items") or [])
|
items = self.get("items") + (self.get("packed_items") or [])
|
||||||
for d in items:
|
for d in items:
|
||||||
@@ -526,27 +552,7 @@ class SellingController(StockController):
|
|||||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||||
|
|
||||||
if old_doc:
|
if old_doc:
|
||||||
old_item = next(
|
reset_incoming_rate()
|
||||||
(
|
|
||||||
item
|
|
||||||
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
|
||||||
if item.name == d.name
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if old_item:
|
|
||||||
old_qty = flt(
|
|
||||||
old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
old_item.item_code != d.item_code
|
|
||||||
or old_item.warehouse != d.warehouse
|
|
||||||
or old_qty != qty
|
|
||||||
or old_item.batch_no != d.batch_no
|
|
||||||
or get_batch_nos(old_item.serial_and_batch_bundle)
|
|
||||||
!= get_batch_nos(d.serial_and_batch_bundle)
|
|
||||||
):
|
|
||||||
d.incoming_rate = 0
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not d.incoming_rate
|
not d.incoming_rate
|
||||||
@@ -565,11 +571,12 @@ class SellingController(StockController):
|
|||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
"voucher_no": self.name,
|
"voucher_no": self.name,
|
||||||
"voucher_detail_no": d.name,
|
"voucher_detail_no": d.name,
|
||||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
"allow_zero_valuation": d.get("allow_zero_valuation_rate"),
|
||||||
"batch_no": d.batch_no,
|
"batch_no": d.batch_no,
|
||||||
"serial_no": d.serial_no,
|
"serial_no": d.serial_no,
|
||||||
},
|
},
|
||||||
raise_error_if_no_rate=False,
|
raise_error_if_no_rate=is_standalone,
|
||||||
|
fallbacks=not is_standalone,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ status_map = {
|
|||||||
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
||||||
[
|
[
|
||||||
"Ordered",
|
"Ordered",
|
||||||
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']",
|
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture', 'Subcontracting']",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Transferred",
|
"Transferred",
|
||||||
|
|||||||
@@ -602,6 +602,11 @@ class calculate_taxes_and_totals:
|
|||||||
else:
|
else:
|
||||||
self.grand_total_diff = 0
|
self.grand_total_diff = 0
|
||||||
|
|
||||||
|
# Apply rounding adjustment to grand_total_for_distributing_discount
|
||||||
|
# to prevent precision errors during discount distribution
|
||||||
|
if hasattr(self, "grand_total_for_distributing_discount") and not self.discount_amount_applied:
|
||||||
|
self.grand_total_for_distributing_discount += self.grand_total_diff
|
||||||
|
|
||||||
def calculate_totals(self):
|
def calculate_totals(self):
|
||||||
grand_total_diff = self.grand_total_diff
|
grand_total_diff = self.grand_total_diff
|
||||||
|
|
||||||
|
|||||||
@@ -59,3 +59,41 @@ class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase):
|
|||||||
self.assertEqual(so.total, 1500)
|
self.assertEqual(so.total, 1500)
|
||||||
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
|
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
|
||||||
self.assertEqual(so.grand_total, 1400)
|
self.assertEqual(so.grand_total, 1400)
|
||||||
|
|
||||||
|
def test_100_percent_discount_with_inclusive_tax(self):
|
||||||
|
"""Test that 100% discount with inclusive taxes results in zero net_total"""
|
||||||
|
so = make_sales_order(do_not_save=1)
|
||||||
|
so.apply_discount_on = "Grand Total"
|
||||||
|
so.items[0].qty = 2
|
||||||
|
so.items[0].rate = 1300
|
||||||
|
so.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Account VAT",
|
||||||
|
"included_in_print_rate": True,
|
||||||
|
"rate": 9,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
so.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Account Service Tax",
|
||||||
|
"included_in_print_rate": True,
|
||||||
|
"rate": 9,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
so.save()
|
||||||
|
|
||||||
|
# Apply 100% discount
|
||||||
|
so.discount_amount = 2600
|
||||||
|
calculate_taxes_and_totals(so)
|
||||||
|
|
||||||
|
# net_total should be exactly 0, not 0.01
|
||||||
|
self.assertEqual(so.net_total, 0)
|
||||||
|
self.assertEqual(so.grand_total, 0)
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ standard_portal_menu_items = [
|
|||||||
"role": "Customer",
|
"role": "Customer",
|
||||||
},
|
},
|
||||||
{"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
|
{"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
|
||||||
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"},
|
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address", "role": "Customer"},
|
||||||
{
|
{
|
||||||
"title": "Timesheets",
|
"title": "Timesheets",
|
||||||
"route": "/timesheets",
|
"route": "/timesheets",
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class TestRouting(FrappeTestCase):
|
|||||||
self.assertEqual(job_card_doc.total_completed_qty, 10)
|
self.assertEqual(job_card_doc.total_completed_qty, 10)
|
||||||
|
|
||||||
wo_doc.cancel()
|
wo_doc.cancel()
|
||||||
wo_doc.delete()
|
|
||||||
|
|
||||||
def test_update_bom_operation_time(self):
|
def test_update_bom_operation_time(self):
|
||||||
"""Update cost shouldn't update routing times."""
|
"""Update cost shouldn't update routing times."""
|
||||||
|
|||||||
@@ -595,6 +595,33 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
work_order1.cancel()
|
work_order1.cancel()
|
||||||
work_order.cancel()
|
work_order.cancel()
|
||||||
|
|
||||||
|
def test_planned_qty_updates_after_closing_work_order(self):
|
||||||
|
item_code = "_Test FG Item"
|
||||||
|
fg_warehouse = "_Test Warehouse 1 - _TC"
|
||||||
|
|
||||||
|
planned_before = (
|
||||||
|
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(item=item_code, fg_warehouse=fg_warehouse, qty=10)
|
||||||
|
|
||||||
|
planned_after_submit = (
|
||||||
|
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
self.assertEqual(planned_after_submit, planned_before + 10)
|
||||||
|
|
||||||
|
close_work_order(wo.name, "Closed")
|
||||||
|
|
||||||
|
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "status"), "Closed")
|
||||||
|
|
||||||
|
planned_after_close = (
|
||||||
|
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
self.assertEqual(planned_after_close, planned_before)
|
||||||
|
|
||||||
def test_work_order_with_non_transfer_item(self):
|
def test_work_order_with_non_transfer_item(self):
|
||||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||||
|
|
||||||
|
|||||||
@@ -517,7 +517,6 @@ class WorkOrder(Document):
|
|||||||
self.db_set("status", "Cancelled")
|
self.db_set("status", "Cancelled")
|
||||||
|
|
||||||
self.on_close_or_cancel()
|
self.on_close_or_cancel()
|
||||||
self.delete_job_card()
|
|
||||||
|
|
||||||
def on_close_or_cancel(self):
|
def on_close_or_cancel(self):
|
||||||
if self.production_plan and frappe.db.exists(
|
if self.production_plan and frappe.db.exists(
|
||||||
@@ -531,7 +530,6 @@ class WorkOrder(Document):
|
|||||||
self.update_planned_qty()
|
self.update_planned_qty()
|
||||||
self.update_ordered_qty()
|
self.update_ordered_qty()
|
||||||
self.update_reserved_qty_for_production()
|
self.update_reserved_qty_for_production()
|
||||||
self.delete_auto_created_batch_and_serial_no()
|
|
||||||
|
|
||||||
def create_serial_no_batch_no(self):
|
def create_serial_no_batch_no(self):
|
||||||
if not (self.has_serial_no or self.has_batch_no):
|
if not (self.has_serial_no or self.has_batch_no):
|
||||||
@@ -588,13 +586,6 @@ class WorkOrder(Document):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_auto_created_batch_and_serial_no(self):
|
|
||||||
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
|
|
||||||
frappe.delete_doc("Serial No", row.name)
|
|
||||||
|
|
||||||
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
|
|
||||||
frappe.delete_doc("Batch", row.name)
|
|
||||||
|
|
||||||
def make_serial_nos(self, args):
|
def make_serial_nos(self, args):
|
||||||
item_details = frappe.get_cached_value(
|
item_details = frappe.get_cached_value(
|
||||||
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
|
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
|
||||||
@@ -1027,10 +1018,6 @@ class WorkOrder(Document):
|
|||||||
if self.actual_start_date and self.actual_end_date:
|
if self.actual_start_date and self.actual_end_date:
|
||||||
self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60)
|
self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60)
|
||||||
|
|
||||||
def delete_job_card(self):
|
|
||||||
for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}):
|
|
||||||
frappe.delete_doc("Job Card", d.name)
|
|
||||||
|
|
||||||
def validate_production_item(self):
|
def validate_production_item(self):
|
||||||
if frappe.get_cached_value("Item", self.production_item, "has_variants"):
|
if frappe.get_cached_value("Item", self.production_item, "has_variants"):
|
||||||
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
|
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
|
||||||
@@ -1173,6 +1160,7 @@ class WorkOrder(Document):
|
|||||||
"operation": item.operation or operation,
|
"operation": item.operation or operation,
|
||||||
"item_code": item.item_code,
|
"item_code": item.item_code,
|
||||||
"item_name": item.item_name,
|
"item_name": item.item_name,
|
||||||
|
"stock_uom": item.stock_uom,
|
||||||
"description": item.description,
|
"description": item.description,
|
||||||
"allow_alternative_item": item.allow_alternative_item,
|
"allow_alternative_item": item.allow_alternative_item,
|
||||||
"required_qty": item.qty,
|
"required_qty": item.qty,
|
||||||
@@ -1197,7 +1185,7 @@ class WorkOrder(Document):
|
|||||||
.select(
|
.select(
|
||||||
ste_child.item_code,
|
ste_child.item_code,
|
||||||
ste_child.original_item,
|
ste_child.original_item,
|
||||||
fn.Sum(ste_child.qty).as_("qty"),
|
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(ste.docstatus == 1)
|
(ste.docstatus == 1)
|
||||||
@@ -1227,7 +1215,7 @@ class WorkOrder(Document):
|
|||||||
.select(
|
.select(
|
||||||
ste_child.item_code,
|
ste_child.item_code,
|
||||||
ste_child.original_item,
|
ste_child.original_item,
|
||||||
fn.Sum(ste_child.qty).as_("qty"),
|
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(ste.docstatus == 1)
|
(ste.docstatus == 1)
|
||||||
@@ -1607,8 +1595,8 @@ def close_work_order(work_order, status):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
work_order.on_close_or_cancel()
|
|
||||||
work_order.update_status(status)
|
work_order.update_status(status)
|
||||||
|
work_order.on_close_or_cancel()
|
||||||
frappe.msgprint(_("Work Order has been {0}").format(status))
|
frappe.msgprint(_("Work Order has been {0}").format(status))
|
||||||
work_order.notify_update()
|
work_order.notify_update()
|
||||||
return work_order.status
|
return work_order.status
|
||||||
@@ -1765,6 +1753,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
|||||||
target_doc,
|
target_doc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
doc.purpose = "Material Transfer for Manufacture"
|
||||||
doc.for_qty = for_qty
|
doc.for_qty = for_qty
|
||||||
|
|
||||||
doc.set_item_locations()
|
doc.set_item_locations()
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
|||||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by
|
erpnext.patches.v15_0.rename_group_by_to_categorize_by
|
||||||
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
||||||
erpnext.patches.v14_0.set_update_price_list_based_on
|
erpnext.patches.v14_0.set_update_price_list_based_on
|
||||||
|
erpnext.patches.v15_0.add_bank_transaction_as_journal_entry_reference
|
||||||
erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
||||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||||
erpnext.patches.v14_0.update_full_name_in_contract
|
erpnext.patches.v14_0.update_full_name_in_contract
|
||||||
@@ -430,3 +431,4 @@ erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12
|
|||||||
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
||||||
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
||||||
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
|
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
|
||||||
|
erpnext.patches.v16_0.add_portal_redirects
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""Append Bank Transaction in custom reference_type options."""
|
||||||
|
new_reference_type = "Bank Transaction"
|
||||||
|
property_setters = frappe.get_all(
|
||||||
|
"Property Setter",
|
||||||
|
filters={
|
||||||
|
"doc_type": "Journal Entry Account",
|
||||||
|
"field_name": "reference_type",
|
||||||
|
"property": "options",
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
for property_setter in property_setters:
|
||||||
|
existing_value = frappe.db.get_value("Property Setter", property_setter, "value") or ""
|
||||||
|
|
||||||
|
raw_options = [option.strip() for option in existing_value.split("\n")]
|
||||||
|
# Preserve a single leading blank (for the empty select option) but drop spurious trailing blanks
|
||||||
|
options = raw_options[:1] + [o for o in raw_options[1:] if o]
|
||||||
|
|
||||||
|
if new_reference_type in options:
|
||||||
|
continue
|
||||||
|
|
||||||
|
options.append(new_reference_type)
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Property Setter",
|
||||||
|
property_setter,
|
||||||
|
"value",
|
||||||
|
"\n".join(options),
|
||||||
|
)
|
||||||
14
erpnext/patches/v16_0/add_portal_redirects.py
Normal file
14
erpnext/patches/v16_0/add_portal_redirects.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.exists("Portal Menu Item", {"route": "/addresses", "reference_doctype": "Address"}) and (
|
||||||
|
doc := frappe.get_doc("Portal Menu Item", {"route": "/addresses", "reference_doctype": "Address"})
|
||||||
|
):
|
||||||
|
doc.role = "Customer"
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
website_settings = frappe.get_single("Website Settings")
|
||||||
|
website_settings.append("route_redirects", {"source": "addresses", "target": "address/list"})
|
||||||
|
website_settings.append("route_redirects", {"source": "projects", "target": "project"})
|
||||||
|
website_settings.save()
|
||||||
@@ -584,6 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
} else {
|
} else {
|
||||||
me.grand_total_diff = 0;
|
me.grand_total_diff = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply rounding adjustment to grand_total_for_distributing_discount
|
||||||
|
// to prevent precision errors during discount distribution
|
||||||
|
if (me.grand_total_for_distributing_discount && !me.discount_amount_applied) {
|
||||||
|
me.grand_total_for_distributing_discount += me.grand_total_diff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ erpnext.utils.update_child_items = function (opts) {
|
|||||||
filters: filters,
|
filters: filters,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onchange: function () {
|
change: function () {
|
||||||
const me = this;
|
const me = this;
|
||||||
|
|
||||||
frm.call({
|
frm.call({
|
||||||
|
|||||||
@@ -293,7 +293,11 @@ erpnext.utils.set_taxes = function (frm, triggered_from_field) {
|
|||||||
erpnext.utils.get_contact_details = function (frm) {
|
erpnext.utils.get_contact_details = function (frm) {
|
||||||
if (frm.updating_party_details) return;
|
if (frm.updating_party_details) return;
|
||||||
|
|
||||||
if (frm.doc["contact_person"]) {
|
if (!frm.doc.contact_person) {
|
||||||
|
reset_contact_fields(frm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
|
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
|
||||||
args: { contact: frm.doc.contact_person },
|
args: { contact: frm.doc.contact_person },
|
||||||
@@ -301,7 +305,26 @@ erpnext.utils.get_contact_details = function (frm) {
|
|||||||
if (r.message) frm.set_value(r.message);
|
if (r.message) frm.set_value(r.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
|
|
||||||
|
erpnext.utils.get_employee_contact_details = function (frm) {
|
||||||
|
if (frm.updating_party_details || frm.doc.party_type !== "Employee") return;
|
||||||
|
|
||||||
|
if (!frm.doc.party) {
|
||||||
|
reset_contact_fields(frm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.setup.doctype.employee.employee.get_contact_details",
|
||||||
|
args: { employee: frm.doc.party },
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) frm.set_value(r.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function reset_contact_fields(frm) {
|
||||||
frm.set_value({
|
frm.set_value({
|
||||||
contact_person: "",
|
contact_person: "",
|
||||||
contact_display: "",
|
contact_display: "",
|
||||||
@@ -312,7 +335,6 @@ erpnext.utils.get_contact_details = function (frm) {
|
|||||||
contact_department: "",
|
contact_department: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
erpnext.utils.validate_mandatory = function (frm, label, value, trigger_on) {
|
erpnext.utils.validate_mandatory = function (frm, label, value, trigger_on) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|||||||
@@ -123,6 +123,13 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
||||||
) {
|
) {
|
||||||
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
||||||
|
this.frm.add_custom_button(__("Update Items"), () => {
|
||||||
|
erpnext.utils.update_child_items({
|
||||||
|
frm: this.frm,
|
||||||
|
child_docname: "items",
|
||||||
|
cannot_add_row: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
|
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
|
||||||
|
|||||||
@@ -614,6 +614,7 @@ def handle_mandatory_error(e, customer, lead_name):
|
|||||||
frappe.throw(message, title=_("Mandatory Missing"))
|
frappe.throw(message, title=_("Mandatory Missing"))
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def get_ordered_items(quotation: str):
|
def get_ordered_items(quotation: str):
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
frappe.get_all(
|
frappe.get_all(
|
||||||
|
|||||||
@@ -1,17 +1,114 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
test_dependencies = ["Product Bundle"]
|
test_dependencies = ["Product Bundle"]
|
||||||
|
|
||||||
|
|
||||||
class TestQuotation(FrappeTestCase):
|
class TestQuotation(FrappeTestCase):
|
||||||
|
def test_update_child_quotation_add_item(self):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
item_1 = make_item("_Test Item")
|
||||||
|
item_2 = make_item("_Test Item 1")
|
||||||
|
|
||||||
|
item_list = [
|
||||||
|
{"item_code": item_1.item_code, "warehouse": "", "qty": 10, "rate": 300},
|
||||||
|
{"item_code": item_2.item_code, "warehouse": "", "qty": 5, "rate": 400},
|
||||||
|
]
|
||||||
|
|
||||||
|
qo = make_quotation(item_list=item_list)
|
||||||
|
first_item = qo.get("items")[0]
|
||||||
|
second_item = qo.get("items")[1]
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": first_item.item_code,
|
||||||
|
"rate": first_item.rate,
|
||||||
|
"qty": 11,
|
||||||
|
"docname": first_item.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": second_item.item_code,
|
||||||
|
"rate": second_item.rate,
|
||||||
|
"qty": second_item.qty,
|
||||||
|
"docname": second_item.name,
|
||||||
|
},
|
||||||
|
{"item_code": "_Test Item 2", "rate": 100, "qty": 7},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(qo.get("items")[0].qty, 11)
|
||||||
|
self.assertEqual(qo.get("items")[-1].rate, 100)
|
||||||
|
|
||||||
|
def test_update_child_disallow_rate_change(self):
|
||||||
|
qo = make_quotation(qty=4)
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": 5000,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
||||||
|
|
||||||
|
def test_update_child_removing_item(self):
|
||||||
|
qo = make_quotation(qty=10)
|
||||||
|
sales_order = make_sales_order(qo.name)
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": qo.items[0].rate,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
},
|
||||||
|
{"item_code": "_Test Item 2", "rate": 100, "qty": 7},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
sales_order.submit()
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(qo.status, "Partially Ordered")
|
||||||
|
|
||||||
|
trans_item = json.dumps([{"item_code": "_Test Item 2", "rate": 100, "qty": 7}])
|
||||||
|
|
||||||
|
# check if items having a sales order can be removed
|
||||||
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
||||||
|
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": qo.items[0].rate,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove item with no sales order
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(len(qo.get("items")), 1)
|
||||||
|
|
||||||
def test_quotation_qty(self):
|
def test_quotation_qty(self):
|
||||||
qo = make_quotation(qty=0, do_not_save=True)
|
qo = make_quotation(qty=0, do_not_save=True)
|
||||||
with self.assertRaises(InvalidQtyError):
|
with self.assertRaises(InvalidQtyError):
|
||||||
@@ -897,6 +994,31 @@ class TestQuotation(FrappeTestCase):
|
|||||||
so1.submit()
|
so1.submit()
|
||||||
self.assertRaises(frappe.ValidationError, so2.submit)
|
self.assertRaises(frappe.ValidationError, so2.submit)
|
||||||
|
|
||||||
|
def test_quotation_status(self):
|
||||||
|
quotation = make_quotation()
|
||||||
|
|
||||||
|
so1 = make_sales_order(quotation.name)
|
||||||
|
so1.delivery_date = nowdate()
|
||||||
|
so1.submit()
|
||||||
|
quotation.reload()
|
||||||
|
self.assertEqual(quotation.status, "Ordered")
|
||||||
|
so1.cancel()
|
||||||
|
|
||||||
|
quotation.reload()
|
||||||
|
self.assertEqual(quotation.status, "Open")
|
||||||
|
|
||||||
|
so2 = make_sales_order(quotation.name)
|
||||||
|
so2.delivery_date = nowdate()
|
||||||
|
so2.items[0].qty = 1
|
||||||
|
so2.submit()
|
||||||
|
quotation.reload()
|
||||||
|
self.assertEqual(quotation.status, "Partially Ordered")
|
||||||
|
|
||||||
|
so2.cancel()
|
||||||
|
|
||||||
|
quotation.reload()
|
||||||
|
self.assertEqual(quotation.status, "Open")
|
||||||
|
|
||||||
|
|
||||||
test_records = frappe.get_test_records("Quotation")
|
test_records = frappe.get_test_records("Quotation")
|
||||||
|
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ class SalesOrder(SellingController):
|
|||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
)
|
)
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
|
super().update_prevdoc_status()
|
||||||
# Cannot cancel closed SO
|
# Cannot cancel closed SO
|
||||||
if self.status == "Closed":
|
if self.status == "Closed":
|
||||||
frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel."))
|
frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel."))
|
||||||
|
|||||||
@@ -524,7 +524,7 @@
|
|||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Delivery Warehouse",
|
"label": "Source Warehouse",
|
||||||
"oldfieldname": "reserved_warehouse",
|
"oldfieldname": "reserved_warehouse",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
@@ -971,7 +971,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-28 09:45:44.934947",
|
"modified": "2026-02-20 16:39:00.200328",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ class Department(NestedSet):
|
|||||||
nsm_parent_field = "parent_department"
|
nsm_parent_field = "parent_department"
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
root = get_root_of("Department")
|
if self.company:
|
||||||
if root and self.department_name != root:
|
|
||||||
self.name = get_abbreviated_name(self.department_name, self.company)
|
self.name = get_abbreviated_name(self.department_name, self.company)
|
||||||
else:
|
else:
|
||||||
self.name = self.department_name
|
self.name = self.department_name
|
||||||
|
|||||||
@@ -436,3 +436,59 @@ def has_upload_permission(doc, ptype="read", user=None):
|
|||||||
if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
|
if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
|
||||||
return True
|
return True
|
||||||
return doc.user_id == user
|
return doc.user_id == user
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_contact_details(employee: str) -> dict:
|
||||||
|
"""
|
||||||
|
Returns basic contact details for the given employee.
|
||||||
|
|
||||||
|
Email is selected based on the following priority:
|
||||||
|
1. Prefered Email
|
||||||
|
2. Company Email
|
||||||
|
3. Personal Email
|
||||||
|
4. User ID
|
||||||
|
"""
|
||||||
|
if not employee:
|
||||||
|
frappe.throw(msg=_("Employee is required"), title=_("Missing Parameter"))
|
||||||
|
|
||||||
|
frappe.has_permission("Employee", "read", employee, throw=True)
|
||||||
|
|
||||||
|
return _get_contact_details(employee)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_contact_details(employee: str) -> dict:
|
||||||
|
contact_data = frappe.db.get_value(
|
||||||
|
"Employee",
|
||||||
|
employee,
|
||||||
|
[
|
||||||
|
"employee_name",
|
||||||
|
"prefered_email",
|
||||||
|
"company_email",
|
||||||
|
"personal_email",
|
||||||
|
"user_id",
|
||||||
|
"cell_number",
|
||||||
|
"designation",
|
||||||
|
"department",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not contact_data:
|
||||||
|
frappe.throw(msg=_("Employee {0} not found").format(employee), title=_("Not Found"))
|
||||||
|
|
||||||
|
# Email with priority
|
||||||
|
employee_email = (
|
||||||
|
contact_data.get("prefered_email")
|
||||||
|
or contact_data.get("company_email")
|
||||||
|
or contact_data.get("personal_email")
|
||||||
|
or contact_data.get("user_id")
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"contact_display": contact_data.get("employee_name"),
|
||||||
|
"contact_email": employee_email,
|
||||||
|
"contact_mobile": contact_data.get("cell_number"),
|
||||||
|
"contact_designation": contact_data.get("designation"),
|
||||||
|
"contact_department": contact_data.get("department"),
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ frappe.listview_settings["Material Request"] = {
|
|||||||
) {
|
) {
|
||||||
return [__("Partially Received"), "yellow", "per_ordered,<,100"];
|
return [__("Partially Received"), "yellow", "per_ordered,<,100"];
|
||||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
|
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
|
||||||
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
|
return [__("Partially Ordered"), "yellow", "per_ordered,<,100"];
|
||||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {
|
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {
|
||||||
if (
|
if (
|
||||||
doc.material_request_type == "Purchase" &&
|
doc.material_request_type == "Purchase" &&
|
||||||
@@ -35,7 +35,7 @@ frappe.listview_settings["Material Request"] = {
|
|||||||
return [__("Partially Received"), "yellow", "per_received,<,100"];
|
return [__("Partially Received"), "yellow", "per_received,<,100"];
|
||||||
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
|
} else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
|
||||||
return [__("Received"), "green", "per_received,=,100"];
|
return [__("Received"), "green", "per_received,=,100"];
|
||||||
} else if (["Purchase", "Manufacture"].includes(doc.material_request_type)) {
|
} else if (["Purchase", "Manufacture", "Subcontracting"].includes(doc.material_request_type)) {
|
||||||
return [__("Ordered"), "green", "per_ordered,=,100"];
|
return [__("Ordered"), "green", "per_ordered,=,100"];
|
||||||
} else if (doc.material_request_type == "Material Transfer") {
|
} else if (doc.material_request_type == "Material Transfer") {
|
||||||
return [__("Transfered"), "green", "per_ordered,=,100"];
|
return [__("Transfered"), "green", "per_ordered,=,100"];
|
||||||
|
|||||||
@@ -997,12 +997,11 @@ def validate_picked_materials(item_code, required_qty, locations, picked_item_de
|
|||||||
if remaining_qty > 0:
|
if remaining_qty > 0:
|
||||||
if picked_item_details:
|
if picked_item_details:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("{0} units of Item {1} is picked in another Pick List.").format(
|
_(
|
||||||
remaining_qty, get_link_to_form("Item", item_code)
|
"{0} units of Item {1} is not available in any of the warehouses. Other Pick Lists exist for this item."
|
||||||
),
|
).format(remaining_qty, get_link_to_form("Item", item_code)),
|
||||||
title=_("Already Picked"),
|
title=_("Already Picked"),
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("{0} units of Item {1} is not available in any of the warehouses.").format(
|
_("{0} units of Item {1} is not available in any of the warehouses.").format(
|
||||||
|
|||||||
@@ -365,6 +365,15 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
|||||||
apply_putaway_rule() {
|
apply_putaway_rule() {
|
||||||
if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm);
|
if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items_add(doc, cdt, cdn) {
|
||||||
|
const row = frappe.get_doc(cdt, cdn);
|
||||||
|
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||||
|
"expense_account",
|
||||||
|
"cost_center",
|
||||||
|
"project",
|
||||||
|
]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// for backward compatibility: combine new and previous states
|
// for backward compatibility: combine new and previous states
|
||||||
|
|||||||
@@ -274,7 +274,9 @@ class QualityInspection(Document):
|
|||||||
|
|
||||||
def set_status_based_on_acceptance_values(self, reading):
|
def set_status_based_on_acceptance_values(self, reading):
|
||||||
if not cint(reading.numeric):
|
if not cint(reading.numeric):
|
||||||
result = reading.get("reading_value") == reading.get("value")
|
reading_value = reading.get("reading_value") or ""
|
||||||
|
value = reading.get("value") or ""
|
||||||
|
result = reading_value == value
|
||||||
else:
|
else:
|
||||||
# numeric readings
|
# numeric readings
|
||||||
result = self.min_max_criteria_passed(reading)
|
result = self.min_max_criteria_passed(reading)
|
||||||
|
|||||||
@@ -299,6 +299,16 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
for serial_no in serial_nos:
|
for serial_no in serial_nos:
|
||||||
if not serial_no_warehouse.get(serial_no) or serial_no_warehouse.get(serial_no) != self.warehouse:
|
if not serial_no_warehouse.get(serial_no) or serial_no_warehouse.get(serial_no) != self.warehouse:
|
||||||
|
reservation = get_serial_no_reservation(self.item_code, serial_no, self.warehouse)
|
||||||
|
if reservation:
|
||||||
|
self.throw_error_message(
|
||||||
|
f"Serial No {bold(serial_no)} is in warehouse {bold(self.warehouse)}"
|
||||||
|
f" but is reserved for {reservation.voucher_type} {bold(reservation.voucher_no)}"
|
||||||
|
f" via {get_link_to_form('Stock Reservation Entry', reservation.name)}."
|
||||||
|
f" Please use an unreserved serial number or cancel the reservation.",
|
||||||
|
SerialNoWarehouseError,
|
||||||
|
)
|
||||||
|
else:
|
||||||
self.throw_error_message(
|
self.throw_error_message(
|
||||||
f"Serial No {bold(serial_no)} is not present in the warehouse {bold(self.warehouse)}.",
|
f"Serial No {bold(serial_no)} is not present in the warehouse {bold(self.warehouse)}.",
|
||||||
SerialNoWarehouseError,
|
SerialNoWarehouseError,
|
||||||
@@ -1032,6 +1042,8 @@ class SerialandBatchBundle(Document):
|
|||||||
qty_field = "consumed_qty"
|
qty_field = "consumed_qty"
|
||||||
elif row.get("doctype") == "Stock Entry Detail":
|
elif row.get("doctype") == "Stock Entry Detail":
|
||||||
qty_field = "transfer_qty"
|
qty_field = "transfer_qty"
|
||||||
|
elif row.get("doctype") in ["Sales Invoice Item", "Purchase Invoice Item"]:
|
||||||
|
qty_field = "stock_qty"
|
||||||
|
|
||||||
return qty_field
|
return qty_field
|
||||||
|
|
||||||
@@ -2445,6 +2457,32 @@ def get_reserved_serial_nos_for_sre(kwargs) -> list:
|
|||||||
return [row[0] for row in query.run()]
|
return [row[0] for row in query.run()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_serial_no_reservation(item_code: str, serial_no: str, warehouse: str) -> _dict | None:
|
||||||
|
"""Returns the Stock Reservation Entry that has reserved the given serial number, if any."""
|
||||||
|
|
||||||
|
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||||
|
sb_entry = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
|
result = (
|
||||||
|
frappe.qb.from_(sre)
|
||||||
|
.inner_join(sb_entry)
|
||||||
|
.on(sre.name == sb_entry.parent)
|
||||||
|
.select(sre.name, sre.voucher_type, sre.voucher_no)
|
||||||
|
.where(
|
||||||
|
(sre.docstatus == 1)
|
||||||
|
& (sre.item_code == item_code)
|
||||||
|
& (sre.warehouse == warehouse)
|
||||||
|
& (sre.status.notin(["Delivered", "Cancelled", "Closed"]))
|
||||||
|
& (sre.reservation_based_on == "Serial and Batch")
|
||||||
|
& (sb_entry.serial_no == serial_no)
|
||||||
|
& (sb_entry.qty != sb_entry.delivered_qty)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
|
||||||
def get_reserved_batches_for_pos(kwargs) -> dict:
|
def get_reserved_batches_for_pos(kwargs) -> dict:
|
||||||
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
|
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,6 @@
|
|||||||
"label": "Serial and Batch Reservation"
|
"label": "Serial and Batch Reservation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
|
||||||
"default": "Qty",
|
"default": "Qty",
|
||||||
"depends_on": "eval: parent.has_serial_no || parent.has_batch_no",
|
"depends_on": "eval: parent.has_serial_no || parent.has_batch_no",
|
||||||
"fieldname": "reservation_based_on",
|
"fieldname": "reservation_based_on",
|
||||||
@@ -269,7 +268,7 @@
|
|||||||
"label": "Reservation Based On",
|
"label": "Reservation Based On",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Qty\nSerial and Batch",
|
"options": "Qty\nSerial and Batch",
|
||||||
"read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")"
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_7dxj",
|
"fieldname": "column_break_7dxj",
|
||||||
@@ -315,11 +314,11 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-07 16:05:17.772098",
|
"modified": "2026-02-19 10:17:28.695394",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reservation Entry",
|
"name": "Stock Reservation Entry",
|
||||||
"naming_rule": "Expression (old style)",
|
"naming_rule": "Expression",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -455,7 +455,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.",
|
"description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate. This will allow the user to specify a different rate for printing or taxation purposes.",
|
||||||
"fieldname": "allow_internal_transfer_at_arms_length_price",
|
"fieldname": "allow_internal_transfer_at_arms_length_price",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Internal Transfers at Arm's Length Price"
|
"label": "Allow Internal Transfers at Arm's Length Price"
|
||||||
@@ -553,7 +553,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-16 10:36:59.921491",
|
"modified": "2026-02-25 09:56:34.105949",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -1920,6 +1920,7 @@ def get_valuation_rate(
|
|||||||
allow_zero_rate=False,
|
allow_zero_rate=False,
|
||||||
currency=None,
|
currency=None,
|
||||||
company=None,
|
company=None,
|
||||||
|
fallbacks=True,
|
||||||
raise_error_if_no_rate=True,
|
raise_error_if_no_rate=True,
|
||||||
batch_no=None,
|
batch_no=None,
|
||||||
serial_and_batch_bundle=None,
|
serial_and_batch_bundle=None,
|
||||||
@@ -1982,23 +1983,20 @@ def get_valuation_rate(
|
|||||||
):
|
):
|
||||||
return flt(last_valuation_rate[0][0])
|
return flt(last_valuation_rate[0][0])
|
||||||
|
|
||||||
|
if fallbacks:
|
||||||
# If negative stock allowed, and item delivered without any incoming entry,
|
# If negative stock allowed, and item delivered without any incoming entry,
|
||||||
# system does not found any SLE, then take valuation rate from Item
|
# system does not found any SLE, then take valuation rate from Item
|
||||||
valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
|
if rate := (
|
||||||
|
frappe.db.get_value("Item", item_code, "valuation_rate")
|
||||||
if not valuation_rate:
|
or frappe.db.get_value("Item", item_code, "standard_rate")
|
||||||
# try Item Standard rate
|
or frappe.db.get_value(
|
||||||
valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
|
|
||||||
|
|
||||||
if not valuation_rate:
|
|
||||||
# try in price list
|
|
||||||
valuation_rate = frappe.db.get_value(
|
|
||||||
"Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
|
"Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
|
||||||
)
|
)
|
||||||
|
):
|
||||||
|
return flt(rate)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not allow_zero_rate
|
not allow_zero_rate
|
||||||
and not valuation_rate
|
|
||||||
and raise_error_if_no_rate
|
and raise_error_if_no_rate
|
||||||
and cint(erpnext.is_perpetual_inventory_enabled(company))
|
and cint(erpnext.is_perpetual_inventory_enabled(company))
|
||||||
):
|
):
|
||||||
@@ -2028,8 +2026,6 @@ def get_valuation_rate(
|
|||||||
|
|
||||||
frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
|
frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
|
||||||
|
|
||||||
return valuation_rate
|
|
||||||
|
|
||||||
|
|
||||||
def update_qty_in_future_sle(args, allow_negative_stock=False):
|
def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||||
"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
|
"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ def _create_bin(item_code, warehouse):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_incoming_rate(args, raise_error_if_no_rate=True):
|
def get_incoming_rate(args, raise_error_if_no_rate=True, fallbacks: bool = True):
|
||||||
"""Get Incoming Rate based on valuation method"""
|
"""Get Incoming Rate based on valuation method"""
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
|
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
|
||||||
|
|
||||||
@@ -325,6 +325,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
|
|||||||
args.get("allow_zero_valuation"),
|
args.get("allow_zero_valuation"),
|
||||||
currency=erpnext.get_company_currency(args.get("company")),
|
currency=erpnext.get_company_currency(args.get("company")),
|
||||||
company=args.get("company"),
|
company=args.get("company"),
|
||||||
|
fallbacks=fallbacks,
|
||||||
raise_error_if_no_rate=raise_error_if_no_rate,
|
raise_error_if_no_rate=raise_error_if_no_rate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -81,23 +81,6 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
transaction_date: DF.Date
|
transaction_date: DF.Date
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.status_updater = [
|
|
||||||
{
|
|
||||||
"source_dt": "Subcontracting Order Item",
|
|
||||||
"target_dt": "Material Request Item",
|
|
||||||
"join_field": "material_request_item",
|
|
||||||
"target_field": "ordered_qty",
|
|
||||||
"target_parent_dt": "Material Request",
|
|
||||||
"target_parent_field": "per_ordered",
|
|
||||||
"target_ref_field": "stock_qty",
|
|
||||||
"source_field": "qty",
|
|
||||||
"percent_join_field": "material_request",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
def onload(self):
|
def onload(self):
|
||||||
self.set_onload(
|
self.set_onload(
|
||||||
"over_transfer_allowance",
|
"over_transfer_allowance",
|
||||||
@@ -117,12 +100,10 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_prevdoc_status()
|
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.update_subcontracted_quantity_in_po()
|
self.update_subcontracted_quantity_in_po()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_prevdoc_status()
|
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.update_subcontracted_quantity_in_po(cancel=True)
|
self.update_subcontracted_quantity_in_po(cancel=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user