mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-07 07:20:26 +00:00
Merge pull request #54741 from frappe/version-15-hotfix
This commit is contained in:
@@ -34,6 +34,13 @@
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Anlagen im Bau": {
|
||||
"is_group": 1,
|
||||
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
|
||||
"account_number": "0498",
|
||||
"account_type": "Capital Work in Progress"
|
||||
}
|
||||
},
|
||||
"Accumulated Depreciation": {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
}
|
||||
@@ -317,13 +324,21 @@
|
||||
"account_number": "3800",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
|
||||
"account_number": "3960",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Herstellungskosten": {
|
||||
"account_number": "4996",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
|
||||
"account_number": "2310",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
|
||||
"account_number": "2320",
|
||||
"account_type": "Stock Adjustment"
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verwaltungskosten": {
|
||||
"account_number": "4997",
|
||||
@@ -340,7 +355,7 @@
|
||||
"is_group": 1,
|
||||
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
|
||||
"account_number": "4830",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Gebäude": {
|
||||
"account_number": "4831",
|
||||
|
||||
@@ -1,108 +1,41 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-11-22 23:47:02.804568",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2018-11-22 23:47:02.804568",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"tax_type",
|
||||
"tax_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "tax_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "tax_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax Rate",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-12-21 23:51:39.445198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-30 23:49:27.020639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -2328,16 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
}
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
|
||||
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
|
||||
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += f" and {fieldname} between {frappe.db.escape(args.get(date_fields[0]))} and {frappe.db.escape(args.get(date_fields[1]))}"
|
||||
condition += f" and {fieldname} between {from_date} and {to_date}"
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += f" and {fieldname} >= {frappe.db.escape(args.get(date_fields[0]))}"
|
||||
condition += f" and {fieldname} >= {from_date}"
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += f" and {fieldname} <= {frappe.db.escape(args.get(date_fields[1]))}"
|
||||
condition += f" and {fieldname} <= {to_date}"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
|
||||
@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
|
||||
if (
|
||||
doc.docstatus == 1 &&
|
||||
doc.outstanding_amount != 0 &&
|
||||
!doc.on_hold &&
|
||||
frappe.model.can_create("Payment Entry")
|
||||
) {
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
@@ -126,7 +131,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
if (
|
||||
doc.docstatus == 1 &&
|
||||
doc.outstanding_amount > 0 &&
|
||||
!cint(doc.is_return) &&
|
||||
!doc.on_hold &&
|
||||
frappe.boot.user.in_create.includes("Payment Request")
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -460,13 +471,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"expense_account",
|
||||
"discount_account",
|
||||
"cost_center",
|
||||
"project",
|
||||
]);
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["expense_account", "discount_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
|
||||
on_submit() {
|
||||
@@ -575,12 +587,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Purchase Invoice", {
|
||||
setup: function (frm) {
|
||||
frm.custom_make_buttons = {
|
||||
|
||||
@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
@@ -136,13 +136,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
if (doc.outstanding_amount > 0) {
|
||||
cur_frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
if (frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
this.frm.add_custom_button(
|
||||
__("Invoice Discounting"),
|
||||
this.make_invoice_discounting.bind(this),
|
||||
@@ -540,12 +542,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"income_account",
|
||||
"discount_account",
|
||||
"cost_center",
|
||||
]);
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["income_account", "discount_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
|
||||
set_dynamic_labels() {
|
||||
|
||||
@@ -440,7 +440,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||
if (
|
||||
frappe.model.can_create("Payment Entry") &&
|
||||
flt(doc.per_billed) < 100 &&
|
||||
doc.status != "Delivered"
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
@@ -448,7 +452,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (flt(doc.per_billed) < 100) {
|
||||
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -705,12 +709,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (doc.schedule_date) {
|
||||
row.schedule_date = doc.schedule_date;
|
||||
refresh_field("schedule_date", cdn, "items");
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
|
||||
field_copy.push("project");
|
||||
}
|
||||
if (doc.schedule_date) {
|
||||
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
|
||||
} else {
|
||||
field_copy.push("schedule_date");
|
||||
}
|
||||
if (field_copy.length) {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,12 +797,6 @@ cur_frm.cscript.update_status = function (label, status) {
|
||||
});
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
if (cur_frm.doc.is_old_subcontracting_flow) {
|
||||
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
||||
@@ -115,9 +115,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -364,13 +364,18 @@ class Project(Document):
|
||||
)
|
||||
|
||||
for user in self.users:
|
||||
# process only users who haven't received the welcome email yet
|
||||
if user.welcome_email_sent == 0:
|
||||
frappe.sendmail(
|
||||
user.user,
|
||||
subject=_("Project Collaboration Invitation"),
|
||||
content=content,
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
# fetch canonical User data (enabled status + latest email)
|
||||
user_info = frappe.db.get_value("User", user.user, ["enabled", "email"], as_dict=True)
|
||||
# send email only if user is enabled and has a valid email
|
||||
if user_info and user_info.enabled and user_info.email:
|
||||
frappe.sendmail(
|
||||
recipients=[user_info.email],
|
||||
subject=_("Project Collaboration Invitation"),
|
||||
content=content,
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
|
||||
|
||||
def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
|
||||
|
||||
@@ -25,13 +25,15 @@ erpnext.buying = {
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("project", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
const project_filters = {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
|
||||
this.frm.set_query("project", (_) => project_filters);
|
||||
this.frm.set_query("project", "items", (_, __, ___) => project_filters);
|
||||
|
||||
if (this.frm.doc.__islocal
|
||||
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
|
||||
|
||||
@@ -560,7 +560,9 @@ def _make_customer(source_name, ignore_permissions=False):
|
||||
if quotation.quotation_to == "Customer":
|
||||
return frappe.get_doc("Customer", quotation.party_name)
|
||||
elif quotation.quotation_to == "CRM Deal":
|
||||
return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
|
||||
customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name})
|
||||
if customer_name:
|
||||
return frappe.get_doc("Customer", customer_name)
|
||||
|
||||
# Check if a Customer already exists for the Lead or Prospect.
|
||||
existing_customer = None
|
||||
|
||||
@@ -175,6 +175,61 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
self.assertTrue(quotation.payment_schedule)
|
||||
|
||||
def test_terms_attachments_are_copied_to_quotation(self):
|
||||
terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
|
||||
first_attachment = make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
terms.name,
|
||||
content="First terms attachment",
|
||||
)
|
||||
|
||||
quotation = make_quotation(do_not_save=1)
|
||||
quotation.tc_name = terms.name
|
||||
quotation.insert()
|
||||
|
||||
self.assertEqual(get_attachment_urls("Quotation", quotation.name), {first_attachment.file_url})
|
||||
|
||||
second_attachment = make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
terms.name,
|
||||
content="Second terms attachment",
|
||||
)
|
||||
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
|
||||
quotation.save()
|
||||
|
||||
quotation_attachments = get_attachment_urls("Quotation", quotation.name)
|
||||
self.assertEqual(quotation_attachments, {first_attachment.file_url})
|
||||
self.assertNotIn(second_attachment.file_url, quotation_attachments)
|
||||
|
||||
new_terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
|
||||
new_terms_attachment = make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
new_terms.name,
|
||||
content="Attachment from updated terms",
|
||||
)
|
||||
quotation.tc_name = new_terms.name
|
||||
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
|
||||
quotation.save()
|
||||
|
||||
self.assertEqual(
|
||||
get_attachment_urls("Quotation", quotation.name),
|
||||
{first_attachment.file_url, new_terms_attachment.file_url},
|
||||
)
|
||||
|
||||
def test_terms_attachments_are_not_copied_when_disabled(self):
|
||||
terms = make_terms_and_conditions(copy_attachments_to_transaction=False)
|
||||
make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
terms.name,
|
||||
content="Terms attachment should stay on the template",
|
||||
)
|
||||
|
||||
quotation = make_quotation(do_not_save=1)
|
||||
quotation.tc_name = terms.name
|
||||
quotation.insert()
|
||||
|
||||
self.assertFalse(get_attachment_urls("Quotation", quotation.name))
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"automatically_fetch_payment_terms": 1},
|
||||
@@ -1142,6 +1197,42 @@ def get_quotation_dict(party_name=None, item_code=None):
|
||||
}
|
||||
|
||||
|
||||
def make_terms_and_conditions(copy_attachments_to_transaction=False):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Terms and Conditions",
|
||||
"title": f"_Test Terms and Conditions {frappe.generate_hash(length=8)}",
|
||||
"selling": 1,
|
||||
"terms": "Test terms",
|
||||
"copy_attachments_to_transaction": 1 if copy_attachments_to_transaction else 0,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_file_attachment(doctype, docname, content):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": f"terms-attachment-{frappe.generate_hash(length=8)}.txt",
|
||||
"attached_to_doctype": doctype,
|
||||
"attached_to_name": docname,
|
||||
"content": content,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def get_attachment_urls(doctype, docname):
|
||||
return {
|
||||
file.file_url
|
||||
for file in frappe.get_all(
|
||||
"File",
|
||||
filters={"attached_to_doctype": doctype, "attached_to_name": docname},
|
||||
fields=["file_url"],
|
||||
)
|
||||
if file.file_url
|
||||
}
|
||||
|
||||
|
||||
def make_quotation(**args):
|
||||
qo = frappe.new_doc("Quotation")
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -773,11 +773,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
// payment request
|
||||
if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
() => this.make_payment_request(),
|
||||
__("Create")
|
||||
);
|
||||
if (frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
() => this.make_payment_request_with_schedule(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -833,6 +835,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
this.order_type(doc);
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
if (doc.delivery_date) {
|
||||
frappe.model.set_value(cdt, cdn, "delivery_date", doc.delivery_date);
|
||||
} else {
|
||||
field_copy.push("delivery_date");
|
||||
}
|
||||
if (field_copy.length) {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
}
|
||||
|
||||
create_pick_list() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
|
||||
|
||||
@@ -533,6 +533,7 @@ class SalesOrder(SellingController):
|
||||
self.update_reserved_qty()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
self.update_blanket_order()
|
||||
|
||||
def update_reserved_qty(self, so_item_rows=None):
|
||||
"""update requested qty (before ordered_qty is updated)"""
|
||||
|
||||
@@ -1,122 +1,176 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType, Field, Order
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.utils import QueryBuilder
|
||||
from frappe.utils.data import comma_or
|
||||
|
||||
SALES_TRANSACTION_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note", "POS Invoice"]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_entries(filters)
|
||||
|
||||
return columns, data
|
||||
return SalesPartnerCommissionSummaryReport(filters).run()
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
if not filters.get("doctype"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
class SalesPartnerSummaryReport:
|
||||
"""
|
||||
Base class to generate Sales Partner Summary related Reports.
|
||||
"""
|
||||
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doctype"]),
|
||||
"options": filters["doctype"],
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Customer"),
|
||||
"options": "Customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Data",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"options": "Territory",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Sales Partner"),
|
||||
"options": "Sales Partner",
|
||||
"fieldname": "sales_partner",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Commission Rate %"),
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total Commission"),
|
||||
"fieldname": "total_commission",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
dt: DocType
|
||||
date_field: str
|
||||
date_label: str
|
||||
columns: list
|
||||
data: list
|
||||
query: QueryBuilder
|
||||
filters: dict
|
||||
|
||||
return columns
|
||||
def __init__(self, filters: dict):
|
||||
self.filters = filters
|
||||
self.columns = []
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
self.prepare_columns()
|
||||
self.get_data()
|
||||
|
||||
def get_entries(filters):
|
||||
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
|
||||
conditions = get_conditions(filters, date_field)
|
||||
entries = frappe.db.sql(
|
||||
return self.columns, self.data
|
||||
|
||||
def validate_filters(self):
|
||||
if not self.filters.get("doctype"):
|
||||
frappe.throw(_("Please select the document type first."))
|
||||
|
||||
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
|
||||
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
|
||||
|
||||
if not self.filters.get("company"):
|
||||
frappe.throw(_("Please select a company."))
|
||||
|
||||
if (
|
||||
self.filters.get("from_date")
|
||||
and self.filters.get("to_date")
|
||||
and self.filters.get("from_date") > self.filters.get("to_date")
|
||||
):
|
||||
frappe.throw(_("From Date cannot be greater than To Date."))
|
||||
|
||||
self._set_date_field_and_label()
|
||||
|
||||
def _set_date_field_and_label(self):
|
||||
self.date_field = (
|
||||
"transaction_date" if self.filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
)
|
||||
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
|
||||
|
||||
def prepare_columns(self):
|
||||
"""
|
||||
SELECT
|
||||
name, customer, territory, {} as posting_date, base_net_total as amount,
|
||||
sales_partner, commission_rate, total_commission, '{}' as currency
|
||||
FROM
|
||||
`tab{}`
|
||||
WHERE
|
||||
{} and docstatus = 1 and sales_partner is not null
|
||||
and sales_partner != '' order by name desc, sales_partner
|
||||
""".format(date_field, company_currency, filters.get("doctype"), conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
Extend this method to add columns on the report. Use `make_column` to add more columns.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
return entries
|
||||
def get_data(self):
|
||||
self.build_report_query()
|
||||
|
||||
self.data = self.query.run(as_dict=1)
|
||||
|
||||
def build_report_query(self):
|
||||
self._build_report_base_query()
|
||||
self.extend_report_query()
|
||||
self._apply_common_filters()
|
||||
self.apply_filters()
|
||||
|
||||
def _build_report_base_query(self):
|
||||
self.dt = DocType(self.filters.get("doctype"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
|
||||
|
||||
self.query = (
|
||||
frappe.qb.from_(self.dt)
|
||||
.select(
|
||||
self.dt.name,
|
||||
self.dt.customer,
|
||||
self.dt.territory,
|
||||
Field(self.date_field, "posting_date", table=self.dt),
|
||||
self.dt.sales_partner,
|
||||
self.dt.commission_rate,
|
||||
ConstantColumn(company_currency).as_("currency"),
|
||||
)
|
||||
.where(
|
||||
(self.dt.docstatus == 1) & (self.dt.sales_partner.notnull()) & (self.dt.sales_partner != "")
|
||||
)
|
||||
.orderby(self.dt.name, order=Order.desc)
|
||||
.orderby(self.dt.sales_partner)
|
||||
)
|
||||
|
||||
def extend_report_query(self):
|
||||
"""
|
||||
Extend this method to select more columns on the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _apply_common_filters(self):
|
||||
for field in ["company", "customer", "territory", "sales_partner"]:
|
||||
if self.filters.get(field):
|
||||
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
|
||||
|
||||
if self.filters.get("from_date"):
|
||||
self.query = self.query.where(
|
||||
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
|
||||
)
|
||||
|
||||
if self.filters.get("to_date"):
|
||||
self.query = self.query.where(
|
||||
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
|
||||
)
|
||||
|
||||
def apply_filters(self):
|
||||
"""
|
||||
Extend this method to add more conditions on the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
def make_column(
|
||||
self, label: str, fieldname: str, fieldtype: str, width: int = 140, options: str = "", hidden: int = 0
|
||||
):
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=label,
|
||||
fieldname=fieldname,
|
||||
fieldtype=fieldtype,
|
||||
options=options,
|
||||
width=width,
|
||||
hidden=hidden,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_conditions(filters, date_field):
|
||||
conditions = "1=1"
|
||||
class SalesPartnerCommissionSummaryReport(SalesPartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
|
||||
for field in ["company", "customer", "territory"]:
|
||||
if filters.get(field):
|
||||
conditions += f" and {field} = %({field})s"
|
||||
self.make_column(_("Customer"), "customer", "Link", options="Customer")
|
||||
|
||||
if filters.get("sales_partner"):
|
||||
conditions += " and sales_partner = %(sales_partner)s"
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions += f" and {date_field} >= %(from_date)s"
|
||||
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions += f" and {date_field} <= %(to_date)s"
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
|
||||
return conditions
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
|
||||
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
|
||||
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
|
||||
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
|
||||
|
||||
def extend_report_query(self):
|
||||
self.query = self.query.select(
|
||||
self.dt.base_net_total.as_("amount"),
|
||||
self.dt.total_commission,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
|
||||
frappe.query_reports["Sales Partner Transaction Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "sales_partner",
|
||||
label: __("Sales Partner"),
|
||||
@@ -28,14 +36,6 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
|
||||
@@ -3,144 +3,84 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
|
||||
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
|
||||
SalesPartnerSummaryReport,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_entries(filters)
|
||||
|
||||
return columns, data
|
||||
return SalesPartnerTransactionSummaryReport(filters=filters).run()
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
if not filters.get("doctype"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
class SalesPartnerTransactionSummaryReport(SalesPartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doctype"]),
|
||||
"options": filters["doctype"],
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Customer"),
|
||||
"options": "Customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"options": "Territory",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item Group",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Brand"),
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"options": "Brand",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
|
||||
{"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Sales Partner"),
|
||||
"options": "Sales Partner",
|
||||
"fieldname": "sales_partner",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Commission Rate %"),
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
self.make_column(_("Customer"), "customer", "Link", options="Customer")
|
||||
|
||||
return columns
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
|
||||
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
|
||||
|
||||
def get_entries(filters):
|
||||
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
|
||||
conditions = get_conditions(filters, date_field)
|
||||
entries = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency,
|
||||
dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount,
|
||||
((dt_item.base_net_amount * dt.commission_rate) / 100) as commission,
|
||||
dt_item.brand, dt.sales_partner, dt.commission_rate, dt_item.item_group, dt_item.item_code
|
||||
FROM
|
||||
`tab{doctype}` dt, `tab{doctype} Item` dt_item
|
||||
WHERE
|
||||
{cond} and dt.name = dt_item.parent and dt.docstatus = 1
|
||||
and dt.sales_partner is not null and dt.sales_partner != ''
|
||||
order by dt.name desc, dt.sales_partner
|
||||
""".format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
|
||||
|
||||
return entries
|
||||
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
|
||||
|
||||
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
|
||||
|
||||
def get_conditions(filters, date_field):
|
||||
conditions = "1=1"
|
||||
self.make_column(_("Quantity"), "qty", "Float", 120)
|
||||
|
||||
for field in ["company", "customer", "territory", "sales_partner"]:
|
||||
if filters.get(field):
|
||||
conditions += f" and dt.{field} = %({field})s"
|
||||
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions += f" and dt.{date_field} >= %(from_date)s"
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions += f" and dt.{date_field} <= %(to_date)s"
|
||||
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
|
||||
|
||||
if not filters.get("show_return_entries"):
|
||||
conditions += " and dt_item.qty > 0.0"
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
|
||||
if filters.get("brand"):
|
||||
conditions += " and dt_item.brand = %(brand)s"
|
||||
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
|
||||
|
||||
if filters.get("item_group"):
|
||||
lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"])
|
||||
def extend_report_query(self):
|
||||
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
|
||||
|
||||
conditions += f""" and dt_item.item_group in (select name from
|
||||
`tabItem Group` where lft >= {lft} and rgt <= {rgt})"""
|
||||
self.query = (
|
||||
self.query.join(self.dt_item)
|
||||
.on(self.dt.name == self.dt_item.parent)
|
||||
.select(
|
||||
self.dt_item.base_net_rate.as_("rate"),
|
||||
self.dt_item.qty,
|
||||
self.dt_item.base_net_amount.as_("amount"),
|
||||
Case()
|
||||
.when(
|
||||
self.dt_item.grant_commission.eq(1),
|
||||
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
|
||||
)
|
||||
.else_(0)
|
||||
.as_("commission"),
|
||||
self.dt_item.brand,
|
||||
self.dt_item.item_group,
|
||||
self.dt_item.item_code,
|
||||
)
|
||||
)
|
||||
|
||||
return conditions
|
||||
def apply_filters(self):
|
||||
if not self.filters.get("show_return_entries"):
|
||||
self.query = self.query.where(self.dt_item.qty > 0.0)
|
||||
|
||||
if self.filters.get("brand"):
|
||||
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
|
||||
|
||||
if self.filters.get("item_group"):
|
||||
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
|
||||
if item_groups := frappe.get_all(
|
||||
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
|
||||
):
|
||||
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"disabled",
|
||||
"column_break_ofhb",
|
||||
"copy_attachments_to_transaction",
|
||||
"applicable_modules_section",
|
||||
"selling",
|
||||
"buying",
|
||||
@@ -72,12 +74,22 @@
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ofhb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "copy_attachments_to_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Copy Attachments to Transaction"
|
||||
}
|
||||
],
|
||||
"icon": "icon-legal",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-30 12:47:52.325531",
|
||||
"modified": "2026-04-29 22:51:49.285298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Terms and Conditions",
|
||||
|
||||
@@ -21,6 +21,7 @@ class TermsandConditions(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
buying: DF.Check
|
||||
copy_attachments_to_transaction: DF.Check
|
||||
disabled: DF.Check
|
||||
selling: DF.Check
|
||||
terms: DF.TextEditor | None
|
||||
|
||||
@@ -372,6 +372,15 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
});
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["project"]);
|
||||
}
|
||||
}
|
||||
|
||||
make_sales_invoice() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
|
||||
@@ -368,11 +368,13 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
|
||||
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",
|
||||
]);
|
||||
const field_copy = ["expense_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -673,6 +673,9 @@ class PurchaseReceipt(BuyingController):
|
||||
or stock_asset_rbnb
|
||||
)
|
||||
|
||||
if self.is_return and item.expense_account:
|
||||
loss_account = item.expense_account
|
||||
|
||||
cost_center = item.cost_center or frappe.get_cached_value(
|
||||
"Company", self.company, "cost_center"
|
||||
)
|
||||
|
||||
@@ -1042,7 +1042,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
|
||||
"fieldname": "add_serial_batch_for_rejected_qty",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No (Rejected Qty)"
|
||||
@@ -1057,7 +1057,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
@@ -1148,7 +1148,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-07 15:41:47.032889",
|
||||
"modified": "2026-04-29 16:01:34.154697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -392,10 +392,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT distinct item_code, item_name
|
||||
SELECT distinct `tab{from_doctype}`.item_code, `tab{from_doctype}`.item_name
|
||||
FROM `tab{from_doctype}`
|
||||
JOIN `tab{parent_doctype}` ON `tab{parent_doctype}`.name = `tab{from_doctype}`.parent
|
||||
WHERE parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and item_code like %(txt)s
|
||||
WHERE `tab{from_doctype}`.parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and `tab{from_doctype}`.item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
|
||||
""",
|
||||
|
||||
@@ -356,8 +356,15 @@ def repost(doc):
|
||||
message = message.get("message")
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
|
||||
# If failed because of a recoverable error (timeout, deadlock), set status to In Progress
|
||||
# so the scheduler automatically retries instead of leaving it permanently failed.
|
||||
# NOTE: isinstance check comes first because the traceback string matching is unreliable
|
||||
# when SIGALRM kills the process mid-C-extension (JobTimeoutException may not appear
|
||||
# in the traceback if the exception handler itself was interrupted).
|
||||
traceback_lower = traceback.lower() if traceback else ""
|
||||
if isinstance(e, RecoverableErrors) or (
|
||||
traceback_lower and ("timeout" in traceback_lower or "deadlock found" in traceback_lower)
|
||||
):
|
||||
status = "In Progress"
|
||||
|
||||
if traceback:
|
||||
|
||||
@@ -1031,6 +1031,7 @@ def insert_item_price(args):
|
||||
currency=args.currency,
|
||||
uom=args.stock_uom,
|
||||
price_list=args.price_list,
|
||||
valid_from=transaction_date,
|
||||
)
|
||||
item_price.insert()
|
||||
frappe.msgprint(
|
||||
@@ -1055,6 +1056,7 @@ def insert_item_price(args):
|
||||
"currency": args.currency,
|
||||
"price_list_rate": price_list_rate,
|
||||
"uom": args.stock_uom,
|
||||
"valid_from": transaction_date,
|
||||
}
|
||||
)
|
||||
item_price.insert()
|
||||
|
||||
@@ -7,6 +7,7 @@ import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_no_status
|
||||
from erpnext.stock.stock_ledger import get_stock_ledger_entries
|
||||
|
||||
BUYING_VOUCHER_TYPES = ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]
|
||||
@@ -111,7 +112,7 @@ def get_data(filters):
|
||||
"posting_time": row.posting_time,
|
||||
"voucher_type": row.voucher_type,
|
||||
"voucher_no": row.voucher_no,
|
||||
"status": "Active" if row.actual_qty > 0 else "Delivered",
|
||||
"status": get_serial_no_status(row),
|
||||
"company": row.company,
|
||||
"warehouse": row.warehouse,
|
||||
"qty": 1 if row.actual_qty > 0 else -1,
|
||||
|
||||
@@ -73,6 +73,7 @@ def execute(filters=None):
|
||||
inv_dimension_wise_dict, filters, inv_dimension_key=inv_dimension_key, opening_row=opening_row
|
||||
)
|
||||
|
||||
item_wh_wise_prev_sle = {}
|
||||
for sle in sl_entries:
|
||||
item_detail = item_details[sle.item_code]
|
||||
|
||||
@@ -114,6 +115,21 @@ def execute(filters=None):
|
||||
elif sle.voucher_type == "Stock Reconciliation":
|
||||
sle["in_out_rate"] = sle.valuation_rate
|
||||
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and not sle.in_qty
|
||||
and not sle.out_qty
|
||||
and not sle.actual_qty
|
||||
):
|
||||
if prev_sle := item_wh_wise_prev_sle.get((sle.item_code, sle.warehouse)):
|
||||
bal_qty = prev_sle.get("qty_after_transaction", 0)
|
||||
qty = sle.qty_after_transaction - bal_qty
|
||||
if qty > 0:
|
||||
sle.in_qty = qty
|
||||
elif qty < 0:
|
||||
sle.out_qty = qty
|
||||
|
||||
item_wh_wise_prev_sle[(sle.item_code, sle.warehouse)] = sle
|
||||
data.append(sle)
|
||||
|
||||
if include_uom:
|
||||
|
||||
@@ -15,6 +15,45 @@ from erpnext.stock.deprecated_serial_batch import (
|
||||
)
|
||||
from erpnext.stock.valuation import round_off_if_near_zero
|
||||
|
||||
CONSUMED_SERIAL_NO_STOCK_ENTRY_PURPOSES = (
|
||||
"Manufacture",
|
||||
"Material Issue",
|
||||
"Repack",
|
||||
"Material Consumption for Manufacture",
|
||||
)
|
||||
INACTIVE_SERIAL_NO_STOCK_ENTRY_PURPOSES = ("Disassemble", "Material Receipt")
|
||||
|
||||
|
||||
def get_serial_no_status(sle):
|
||||
warehouse = sle.warehouse if sle.actual_qty > 0 else None
|
||||
if warehouse:
|
||||
return "Active"
|
||||
|
||||
status = get_status_for_serial_nos(sle)
|
||||
if sle.voucher_type == "Stock Entry" and sle.actual_qty < 0:
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in INACTIVE_SERIAL_NO_STOCK_ENTRY_PURPOSES:
|
||||
status = "Inactive"
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def get_status_for_serial_nos(sle):
|
||||
status = "Inactive"
|
||||
if sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
if sle.voucher_type == "Stock Entry":
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in CONSUMED_SERIAL_NO_STOCK_ENTRY_PURPOSES:
|
||||
status = "Consumed"
|
||||
|
||||
if sle.is_cancelled == 1 and (
|
||||
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
|
||||
):
|
||||
status = "Inactive"
|
||||
|
||||
return status
|
||||
|
||||
|
||||
class SerialBatchBundle:
|
||||
def __init__(self, **kwargs):
|
||||
@@ -410,25 +449,7 @@ class SerialBatchBundle:
|
||||
self.update_serial_no_status_warehouse(self.sle, serial_nos)
|
||||
|
||||
def get_status_for_serial_nos(self, sle):
|
||||
status = "Inactive"
|
||||
if sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
if sle.voucher_type == "Stock Entry":
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in [
|
||||
"Manufacture",
|
||||
"Material Issue",
|
||||
"Repack",
|
||||
"Material Consumption for Manufacture",
|
||||
]:
|
||||
status = "Consumed"
|
||||
|
||||
if sle.is_cancelled == 1 and (
|
||||
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
|
||||
):
|
||||
status = "Inactive"
|
||||
|
||||
return status
|
||||
return get_status_for_serial_nos(sle)
|
||||
|
||||
def update_serial_no_status_warehouse(self, sle, serial_nos):
|
||||
warehouse = sle.warehouse if sle.actual_qty > 0 else None
|
||||
@@ -436,19 +457,12 @@ class SerialBatchBundle:
|
||||
if isinstance(serial_nos, str):
|
||||
serial_nos = [serial_nos]
|
||||
|
||||
status = "Active"
|
||||
if not warehouse:
|
||||
status = self.get_status_for_serial_nos(sle)
|
||||
status = get_serial_no_status(sle)
|
||||
|
||||
customer = None
|
||||
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
||||
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
||||
|
||||
if sle.voucher_type in ["Stock Entry"] and sle.actual_qty < 0:
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in ["Disassemble", "Material Receipt"]:
|
||||
status = "Inactive"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
|
||||
@@ -15,6 +15,14 @@ class UOMMustBeIntegerError(frappe.ValidationError):
|
||||
|
||||
|
||||
class TransactionBase(StatusUpdater):
|
||||
def on_change(self):
|
||||
# `on_change` also fires for `db_set()`, so only run during an actual insert/save.
|
||||
is_real_save = self.flags.in_insert or (self.doctype, self.name) in frappe.flags.currently_saving
|
||||
if not is_real_save:
|
||||
return
|
||||
|
||||
self.copy_terms_and_conditions_attachments()
|
||||
|
||||
def validate_posting_time(self):
|
||||
# set Edit Posting Date and Time to 1 while data import
|
||||
if frappe.flags.in_import and self.posting_date:
|
||||
@@ -33,6 +41,56 @@ class TransactionBase(StatusUpdater):
|
||||
def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
|
||||
validate_uom_is_integer(self, uom_field, qty_fields, child_dt)
|
||||
|
||||
def copy_terms_and_conditions_attachments(self):
|
||||
if (
|
||||
not self.name
|
||||
or not self.meta.has_field("tc_name")
|
||||
or not self.tc_name
|
||||
or not self.has_value_changed("tc_name")
|
||||
):
|
||||
return
|
||||
|
||||
copy_attachments_to_transaction = frappe.db.get_value(
|
||||
"Terms and Conditions", self.tc_name, "copy_attachments_to_transaction"
|
||||
)
|
||||
if not cint(copy_attachments_to_transaction):
|
||||
return
|
||||
|
||||
source_attachments = frappe.get_all(
|
||||
"File",
|
||||
filters={
|
||||
"attached_to_doctype": "Terms and Conditions",
|
||||
"attached_to_name": self.tc_name,
|
||||
},
|
||||
fields=["name", "file_url"],
|
||||
)
|
||||
if not source_attachments:
|
||||
return
|
||||
|
||||
existing_file_urls = {
|
||||
attachment.file_url
|
||||
for attachment in frappe.get_all(
|
||||
"File",
|
||||
filters={
|
||||
"attached_to_doctype": self.doctype,
|
||||
"attached_to_name": self.name,
|
||||
},
|
||||
fields=["file_url"],
|
||||
)
|
||||
if attachment.file_url
|
||||
}
|
||||
|
||||
for source_attachment in source_attachments:
|
||||
if not source_attachment.file_url or source_attachment.file_url in existing_file_urls:
|
||||
continue
|
||||
|
||||
# Reuse the existing file metadata so the same on-disk blob is shared.
|
||||
new_attachment = frappe.get_doc("File", source_attachment.name).create_attachment_copy(
|
||||
attached_to_doctype=self.doctype,
|
||||
attached_to_name=self.name,
|
||||
)
|
||||
existing_file_urls.add(new_attachment.file_url)
|
||||
|
||||
def validate_with_previous_doc(self, ref):
|
||||
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user