Compare commits

...

13 Commits

Author SHA1 Message Date
rohitwaghchaure
06ffe52d6e Merge pull request #54681 from rohitwaghchaure/fixed-support-66529
fix: incorrect expense account book in purchase return
2026-05-01 08:12:31 +05:30
Raffael Meyer
c120cc7ed1 fix: add missing fields in set_currency_labels (#54689) 2026-05-01 03:54:14 +02:00
Raffael Meyer
25be38e23c fix: Backfill not_applicable on Item Tax Template Details for German companies (#54682) 2026-04-30 19:21:24 +00:00
Rohit Waghchaure
2a720e7008 fix: incorrect expense account book in purchase return 2026-04-30 20:36:20 +05:30
Raffael Meyer
f38eca9124 fix: mark item tax templates as not applicable (#54673)
* fix: mark item tax templates as not applicable

For new German charts of accounts, mark accounts for different tax rates as *Not Applicable* in **Item Tax Templates**.

* fix: wrong applicable rate 19 in template 7
2026-04-30 11:44:08 +00:00
rohitwaghchaure
ad89f88c93 Merge pull request #54664 from rohitwaghchaure/fixed-support-66924
fix: show in and out qty in the stock ledger report for stock recos
2026-04-30 14:13:42 +05:30
Trusted Computer
78f654765d fix: correct titles set to {customer_name} or {supplier_name} text strings (#54656) 2026-04-30 10:28:14 +02:00
Hemil-Sangani
231dd1856f fix(project): use user.email for invitations and skip disabled users. (#54561)
* fix(project): use user.email for invitations and skip disabled users.

* Update erpnext/projects/doctype/project/project.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(project): remove duplicate loop causing indentation error

* fix(project): resolve pre-commit hook failure

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-30 07:53:32 +00:00
Rohit Waghchaure
da081254a6 fix: show in and out qty in the stock ledger report for stock recos 2026-04-30 13:16:13 +05:30
Raffael Meyer
c543d15f3c feat: copy terms attachments to transactions (#53403) 2026-04-29 21:14:58 +00:00
Khushi Rawat
ddf0e35009 Merge pull request #54658 from khushi8112/skip-rescheduling-for-fully-depreciated-asset-sale
fix: skip depreciation rescheduling when asset is fully depreciated on sale
2026-04-30 02:34:10 +05:30
khushi8112
88b82383f5 fix: skip rescheduling only for asset being disposed 2026-04-30 02:11:57 +05:30
khushi8112
c4155b6c81 fix: skip depreciation rescheduling when asset is fully depreciated on sale 2026-04-30 02:01:57 +05:30
18 changed files with 602 additions and 153 deletions

View File

@@ -664,6 +664,7 @@
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1531,6 +1532,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1639,7 +1641,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-03-30 12:15:57.253316",
"modified": "2026-05-01 02:37:30.580568",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -1150,6 +1150,7 @@
"hide_seconds": 1,
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1162,6 +1163,7 @@
"label": "Rounded Total",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -2355,7 +2357,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-03-30 12:17:16.201016",
"modified": "2026-05-01 02:37:29.742764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -194,6 +194,9 @@ def reschedule_depreciation(asset_doc, notes, disposal_date=None):
for row in asset_doc.get("finance_books"):
current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book)
if disposal_date and flt(row.value_after_depreciation) <= flt(row.expected_value_after_useful_life):
continue
if current_schedule:
if current_schedule.docstatus == 1:
new_schedule = frappe.copy_doc(current_schedule)

View File

@@ -470,7 +470,7 @@ erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2
erpnext.patches.v16_0.enable_serial_batch_setting
erpnext.patches.v16_0.correct_po_titles
erpnext.patches.v16_0.fix_titles
erpnext.patches.v16_0.co_by_product_patch
erpnext.patches.v16_0.update_requested_qty_packed_item
erpnext.patches.v16_0.remove_payables_receivables_workspace
@@ -479,4 +479,5 @@ erpnext.patches.v16_0.uom_category
erpnext.patches.v16_0.merge_repost_settings_to_accounts_settings
erpnext.patches.v16_0.set_root_type_in_account_categories
erpnext.patches.v16_0.scr_inv_dimension
erpnext.patches.v16_0.packed_item_inv_dimen
erpnext.patches.v16_0.packed_item_inv_dimen
erpnext.patches.v16_0.set_not_applicable_on_german_item_tax_templates

View File

@@ -1,15 +0,0 @@
import frappe
def execute():
"""
This patch corrects the titles of purchase orders that were set to
the text string "{supplier_name}" instead of the actual supplier name.
"""
purchase_order = frappe.qb.DocType("Purchase Order")
(
frappe.qb.update(purchase_order)
.set(purchase_order.title, purchase_order.supplier_name)
.where(purchase_order.title == "{supplier_name}")
).run()

View File

@@ -0,0 +1,28 @@
import frappe
def execute():
"""
This patch corrects the titles of doctypes set to
the text strings "{customer_name}" or "{supplier_name}"
instead of the actual customer or supplier name.
"""
customer_doctypes = ["POS Invoice", "Sales Invoice", "Quotation", "Sales Order", "Delivery Note"]
supplier_doctypes = ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]
for doctype in customer_doctypes:
customer_doctype = frappe.qb.DocType(doctype)
(
frappe.qb.update(customer_doctype)
.set(customer_doctype.title, customer_doctype.customer_name)
.where(customer_doctype.title == "{customer_name}")
).run()
for doctype in supplier_doctypes:
supplier_doctype = frappe.qb.DocType(doctype)
(
frappe.qb.update(supplier_doctype)
.set(supplier_doctype.title, supplier_doctype.supplier_name)
.where(supplier_doctype.title == "{supplier_name}")
).run()

View File

@@ -0,0 +1,218 @@
import frappe
# Snapshot of the relevant German defaults when this migration was written.
# Migration patches must not read mutable setup data, otherwise future edits to
# country_wise_tax.json would change what this patch does on sites that have not
# run it yet.
#
# For numbered charts, compare account_number + root_type because Account.account_name
# is not unique within a company.
SKR04_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS = frozenset(
{
("3801", "Liability"),
("3802", "Liability"),
("3835", "Liability"),
("1401", "Asset"),
("1402", "Asset"),
("1541", "Asset"),
}
)
SKR04_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS = frozenset(
{
("3806", "Liability"),
("3804", "Liability"),
("3837", "Liability"),
("1406", "Asset"),
("1404", "Asset"),
("1540", "Asset"),
}
)
SKR03_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS = frozenset(
{
("1771", "Liability"),
("1772", "Liability"),
("1785", "Liability"),
("1571", "Asset"),
("1572", "Asset"),
("1541", "Asset"),
}
)
SKR03_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS = frozenset(
{
("1776", "Liability"),
("1774", "Liability"),
("1787", "Liability"),
("1576", "Asset"),
("1574", "Asset"),
("1540", "Asset"),
}
)
STANDARD_NOT_APPLICABLE_7_PERCENT_ACCOUNT_LABELS = frozenset(
{
("Umsatzsteuer 7 %", "Liability"),
("Umsatzsteuer aus innergemeinschaftlichem Erwerb", "Liability"),
("Umsatzsteuer nach § 13b UStG", "Liability"),
("Abziehbare Vorsteuer 7 %", "Asset"),
("Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", "Asset"),
("Abziehbare Vorsteuer nach § 13b UStG", "Asset"),
}
)
STANDARD_NOT_APPLICABLE_19_PERCENT_ACCOUNT_LABELS = frozenset(
{
("Umsatzsteuer 19 %", "Liability"),
("Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", "Liability"),
("Umsatzsteuer nach § 13b UStG 19 %", "Liability"),
("Abziehbare Vorsteuer 19 %", "Asset"),
("Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", "Asset"),
("Abziehbare Vorsteuer nach § 13b UStG 19 %", "Asset"),
}
)
STANDARD_WITH_NUMBERS_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS = frozenset(
{
("2321", "Liability"),
("2331", "Liability"),
("2341", "Liability"),
("1521", "Asset"),
("1531", "Asset"),
("1541", "Asset"),
}
)
STANDARD_WITH_NUMBERS_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS = frozenset(
{
("2320", "Liability"),
("2330", "Liability"),
("2340", "Liability"),
("1520", "Asset"),
("1530", "Asset"),
("1540", "Asset"),
}
)
GERMAN_ITEM_TAX_TEMPLATE_NOT_APPLICABLE_ACCOUNTS = {
"SKR03 mit Kontonummern": {
"identifier_field": "account_number",
"templates": {
"19 %": SKR03_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS,
"7 %": SKR03_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS,
"0 %": SKR03_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS
| SKR03_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS
| frozenset({("1588", "Asset")}),
},
},
"SKR04 mit Kontonummern": {
"identifier_field": "account_number",
"templates": {
"19 %": SKR04_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS,
"7 %": SKR04_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS,
"0 %": SKR04_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS
| SKR04_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS
| frozenset({("1433", "Asset")}),
},
},
"Standard": {
"identifier_field": "account_name",
"templates": {
"19 %": STANDARD_NOT_APPLICABLE_7_PERCENT_ACCOUNT_LABELS,
"7 %": STANDARD_NOT_APPLICABLE_19_PERCENT_ACCOUNT_LABELS,
"0%": STANDARD_NOT_APPLICABLE_7_PERCENT_ACCOUNT_LABELS
| STANDARD_NOT_APPLICABLE_19_PERCENT_ACCOUNT_LABELS
| frozenset({("Entstandene Einfuhrumsatzsteuer", "Asset")}),
},
},
"Standard with Numbers": {
"identifier_field": "account_number",
"templates": {
"19%": STANDARD_WITH_NUMBERS_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS,
"7%": STANDARD_WITH_NUMBERS_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS,
"0 %": STANDARD_WITH_NUMBERS_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS
| STANDARD_WITH_NUMBERS_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS
| frozenset({("1550", "Asset")}),
},
},
}
def update_account_cache(accounts, account_cache):
missing_accounts = set(accounts) - set(account_cache)
if not missing_accounts:
return
for account in frappe.get_all(
"Account",
filters={"name": ("in", tuple(sorted(missing_accounts)))},
fields=["name", "account_name", "account_number", "root_type"],
):
account_cache[account.name] = account
def get_account_identifier(account, identifier_field, account_cache):
cached_account = account_cache.get(account)
if not cached_account:
return None
return cached_account.get(identifier_field), cached_account.root_type
def execute():
"""Backfill `not_applicable` on Item Tax Template Details for German companies.
Before the `not_applicable` flag existed, German default templates used
`tax_rate: 0` to mean "this tax does not apply to the item" (as opposed to
an explicit 0% rate). For each German company, this patch looks up the
historical defaults for its Chart of Accounts and sets
`not_applicable = 1` on detail rows that still match those defaults
(same template title, same zero-rate tax account identifier set, flag still unset),
leaving any user-customised rows untouched.
"""
companies = frappe.get_all(
"Company",
filters={"country": "Germany"},
fields=["name", "chart_of_accounts"],
)
account_cache = {}
for company in companies:
chart = GERMAN_ITEM_TAX_TEMPLATE_NOT_APPLICABLE_ACCOUNTS.get(company.chart_of_accounts)
if not chart:
continue
identifier_field = chart["identifier_field"]
for template_title, target_accounts in chart["templates"].items():
itt_names = frappe.get_all(
"Item Tax Template",
filters={"company": company.name, "title": template_title},
pluck="name",
)
for itt_name in itt_names:
zero_rate_details = frappe.get_all(
"Item Tax Template Detail",
filters={"parent": itt_name, "tax_rate": 0},
fields=["name", "tax_type", "not_applicable"],
)
update_account_cache((d.tax_type for d in zero_rate_details), account_cache)
zero_rate_accounts_by_detail = {
d.name: get_account_identifier(d.tax_type, identifier_field, account_cache)
for d in zero_rate_details
}
if any(identifier is None for identifier in zero_rate_accounts_by_detail.values()):
continue
if set(zero_rate_accounts_by_detail.values()) != target_accounts:
continue
for d in zero_rate_details:
if not d.not_applicable:
frappe.db.set_value(
"Item Tax Template Detail",
d.name,
"not_applicable",
1,
update_modified=False,
)

View File

@@ -363,13 +363,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]:

View File

@@ -870,10 +870,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
me.apply_rule_on_other_items({ key: item });
}
},
() => {
var company_currency = me.get_company_currency();
me.update_item_grid_labels(company_currency);
},
]);
}
},
@@ -1824,63 +1820,51 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (
this._last_currency === this.frm.doc.currency &&
this._last_price_list_currency === this.frm.doc.price_list_currency
this._last_price_list_currency === this.frm.doc.price_list_currency &&
this._last_party_account_currency === this.frm.doc.party_account_currency &&
this._last_company_currency === company_currency
) {
return;
}
this._last_currency = this.frm.doc.currency;
this._last_price_list_currency = this.frm.doc.price_list_currency;
this._last_party_account_currency = this.frm.doc.party_account_currency;
this._last_company_currency = company_currency;
this.change_form_labels(company_currency);
this.change_grid_labels(company_currency);
this.frm.refresh_fields();
}
get_currency_label_options(company_currency) {
return {
currency: this.frm.doc.currency,
"Company:company:default_currency": company_currency,
party_account_currency: this.frm.doc.party_account_currency,
};
}
set_currency_labels_from_options(currency_options, parentfield) {
const doctype = parentfield ? this.frm.fields_dict[parentfield].grid.doctype : this.frm.doc.doctype;
const docfields = frappe.meta.get_docfields(doctype);
Object.entries(currency_options).forEach(([options, currency]) => {
const fields = docfields
.filter((df) => df.fieldtype === "Currency" && df.options === options)
.map((df) => df.fieldname);
this.frm.set_currency_labels(fields, currency, parentfield);
});
}
change_form_labels(company_currency) {
let me = this;
const currency_options = this.get_currency_label_options(company_currency);
this.frm.set_currency_labels(
[
"advance_paid",
"base_total",
"base_net_total",
"base_total_taxes_and_charges",
"base_discount_amount",
"base_taxes_and_charges_added",
"base_taxes_and_charges_deducted",
"total_amount_to_pay",
"base_paid_amount",
"base_write_off_amount",
"base_change_amount",
"base_operating_cost",
"base_raw_material_cost",
"base_total_cost",
"base_secondary_items_cost",
"base_totals_section",
],
company_currency
);
this.frm.set_currency_labels(
[
"total",
"net_total",
"total_taxes_and_charges",
"discount_amount",
"taxes_and_charges_added",
"taxes_and_charges_deducted",
"tax_withholding_net_total",
"paid_amount",
"write_off_amount",
"operating_cost",
"secondary_items_cost",
"raw_material_cost",
"total_cost",
"totals_section",
],
this.frm.doc.currency
);
this.set_currency_labels_from_options(currency_options);
this.frm.set_currency_labels(["totals_section"], this.frm.doc.currency);
this.frm.set_currency_labels(["base_totals_section"], company_currency);
this.frm.set_df_property(
"conversion_rate",
@@ -1956,23 +1940,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
change_grid_labels(company_currency) {
var me = this;
this.update_item_grid_labels(company_currency);
const currency_options = this.get_currency_label_options(company_currency);
this.toggle_item_grid_columns(company_currency);
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
this.frm.set_currency_labels(
["operating_cost", "hour_rate"],
this.frm.doc.currency,
"operations"
);
this.frm.set_currency_labels(
["base_operating_cost", "base_hour_rate"],
company_currency,
"operations"
);
for (const child_table of [
"items",
"operations",
"secondary_items",
"taxes",
"advances",
"payment_schedule",
"sales_team",
]) {
if (this.frm.fields_dict[child_table]) {
this.set_currency_labels_from_options(currency_options, child_table);
}
}
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
var item_grid = this.frm.fields_dict["operations"].grid;
$.each(["base_operating_cost", "base_hour_rate"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
@@ -1981,9 +1967,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) {
this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "secondary_items");
this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "secondary_items");
var item_grid = this.frm.fields_dict["secondary_items"].grid;
$.each(["base_rate", "base_amount"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
@@ -1991,74 +1974,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
});
}
if (this.frm.doc.taxes && this.frm.doc.taxes.length > 0) {
this.frm.set_currency_labels(
["tax_amount", "total", "tax_amount_after_discount"],
this.frm.doc.currency,
"taxes"
);
this.frm.set_currency_labels(
["base_tax_amount", "base_total", "base_tax_amount_after_discount"],
company_currency,
"taxes"
);
}
if (this.frm.doc.advances && this.frm.doc.advances.length > 0) {
this.frm.set_currency_labels(
["advance_amount", "allocated_amount"],
this.frm.doc.party_account_currency,
"advances"
);
}
this.update_payment_schedule_grid_labels(company_currency);
}
update_item_grid_labels(company_currency) {
this.frm.set_currency_labels(
[
"base_rate",
"base_net_rate",
"base_price_list_rate",
"base_amount",
"base_net_amount",
"base_rate_with_margin",
],
company_currency,
"items"
);
this.frm.set_currency_labels(
[
"rate",
"net_rate",
"price_list_rate",
"amount",
"net_amount",
"stock_uom_rate",
"rate_with_margin",
],
this.frm.doc.currency,
"items"
);
}
update_payment_schedule_grid_labels(company_currency) {
const me = this;
if (this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length > 0) {
this.frm.set_currency_labels(
["base_payment_amount", "base_outstanding", "base_paid_amount"],
company_currency,
"payment_schedule"
);
this.frm.set_currency_labels(
["payment_amount", "outstanding", "paid_amount"],
this.frm.doc.currency,
"payment_schedule"
);
var schedule_grid = this.frm.fields_dict["payment_schedule"].grid;
$.each(["base_payment_amount", "base_outstanding", "base_paid_amount"], function (i, fname) {
if (frappe.meta.get_docfield(schedule_grid.doctype, fname))

View File

@@ -183,6 +183,61 @@ class TestQuotation(ERPNextTestSuite):
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))
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
@@ -1148,6 +1203,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)

View File

@@ -847,6 +847,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Loyalty Amount",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1480,6 +1481,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1763,7 +1765,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-30 12:19:27.522646",
"modified": "2026-05-01 02:37:30.937916",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -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": "2026-04-14 18:22:49.285298",
"modified": "2026-04-29 22:51:49.285298",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",

View File

@@ -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

View File

@@ -1387,6 +1387,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1405,6 +1406,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1423,6 +1425,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1441,6 +1444,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1459,6 +1463,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1477,6 +1482,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1499,6 +1505,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1517,6 +1524,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1535,6 +1543,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1553,6 +1562,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1571,6 +1581,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1589,6 +1600,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1620,6 +1632,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1629,6 +1642,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1638,6 +1652,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1647,6 +1662,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1656,6 +1672,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1665,6 +1682,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1674,6 +1692,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1683,6 +1702,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1692,6 +1712,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1701,6 +1722,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1710,6 +1732,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1719,6 +1742,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1727,6 +1751,7 @@
"account_number": "1433",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]
@@ -2150,6 +2175,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2168,6 +2194,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2186,6 +2213,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2204,6 +2232,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2222,6 +2251,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2240,6 +2270,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2262,6 +2293,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2280,6 +2312,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2298,6 +2331,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2316,6 +2350,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2334,6 +2369,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2352,6 +2388,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2383,6 +2420,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2392,6 +2430,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2401,6 +2440,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2410,6 +2450,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2419,6 +2460,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2428,6 +2470,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2437,6 +2480,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2446,6 +2490,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2455,6 +2500,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2464,6 +2510,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2473,6 +2520,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2482,6 +2530,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2490,6 +2539,7 @@
"account_number": "1588",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]
@@ -2913,6 +2963,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2931,6 +2982,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2949,6 +3001,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2967,6 +3020,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2985,6 +3039,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3003,6 +3058,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3025,6 +3081,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3043,6 +3100,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3061,7 +3119,8 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"tax_rate": 19.00
"not_applicable": 1,
"tax_rate": 0.00
},
{
"tax_type": {
@@ -3079,6 +3138,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3097,6 +3157,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3115,6 +3176,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3146,6 +3208,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3155,6 +3218,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3164,6 +3228,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3173,6 +3238,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3182,6 +3248,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3191,6 +3258,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3200,6 +3268,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3209,6 +3278,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3218,6 +3288,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3227,6 +3298,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3236,6 +3308,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3245,6 +3318,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3253,6 +3327,7 @@
"account_number": "1550",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]
@@ -3645,6 +3720,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3661,6 +3737,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3677,6 +3754,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3693,6 +3771,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3709,6 +3788,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3725,6 +3805,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3745,6 +3826,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3761,6 +3843,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3777,6 +3860,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3793,6 +3877,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3809,6 +3894,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3825,6 +3911,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3853,6 +3940,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3861,6 +3949,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3869,6 +3958,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3877,6 +3967,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3885,6 +3976,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3893,6 +3985,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3901,6 +3994,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3909,6 +4003,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3917,6 +4012,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3925,6 +4021,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3933,6 +4030,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3941,6 +4039,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3948,6 +4047,7 @@
"account_name": "Entstandene Einfuhrumsatzsteuer",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]

View File

@@ -1266,6 +1266,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1470,7 +1471,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2026-03-30 12:19:56.889644",
"modified": "2026-05-01 02:37:31.430649",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -725,6 +725,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"
)

View File

@@ -72,6 +72,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]
@@ -113,6 +114,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:

View File

@@ -18,6 +18,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 and restore
if (frappe.flags.in_import or self.flags.from_restore) and self.posting_date:
@@ -36,6 +44,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 []