mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-03 12:19:12 +00:00
Merge pull request #50087 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can
|
|||||||
|
|
||||||
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
|
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
|
||||||
|
|
||||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
|
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6).
|
||||||
|
|
||||||
### Reply and Closing Policy
|
### Reply and Closing Policy
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -9,7 +9,7 @@ body:
|
|||||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||||
|
|
||||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
|
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
|
||||||
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
|
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
|
||||||
2. When making a bug report, make sure you provide all required information. The easier it is for
|
2. When making a bug report, make sure you provide all required information. The easier it is for
|
||||||
maintainers to reproduce, the faster it'll be fixed.
|
maintainers to reproduce, the faster it'll be fixed.
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Community Forum
|
- name: Community Forum
|
||||||
url: https://discuss.erpnext.com/
|
url: https://discuss.frappe.io/c/erpnext/6
|
||||||
about: For general QnA, discussions and community help.
|
about: For general QnA, discussions and community help.
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -11,7 +11,7 @@ assignees: ''
|
|||||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||||
|
|
||||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||||
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
|
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
|
||||||
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||||
the original discussion.
|
the original discussion.
|
||||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||||
@@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on
|
|||||||
|
|
||||||
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
|
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
|
||||||
1. Certified ERPNext partners: https://erpnext.com/partners
|
1. Certified ERPNext partners: https://erpnext.com/partners
|
||||||
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
|
2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5
|
||||||
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
|
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
|||||||
|
|
||||||
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||||
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
||||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers.
|
||||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ def validate_service_stop_date(doc):
|
|||||||
if (
|
if (
|
||||||
old_stop_dates
|
old_stop_dates
|
||||||
and old_stop_dates.get(item.name)
|
and old_stop_dates.get(item.name)
|
||||||
and item.service_stop_date != old_stop_dates.get(item.name)
|
and item.service_stop_date
|
||||||
|
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
|
||||||
):
|
):
|
||||||
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-29 11:37:42.678556",
|
"modified": "2025-10-13 15:11:58.300836",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Advance Payment Ledger Entry",
|
"name": "Advance Payment Ledger Entry",
|
||||||
|
|||||||
@@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document):
|
|||||||
and not frappe.flags.is_reverse_depr_entry
|
and not frappe.flags.is_reverse_depr_entry
|
||||||
):
|
):
|
||||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def on_doctype_update():
|
||||||
|
frappe.db.add_index(
|
||||||
|
"Advance Payment Ledger Entry",
|
||||||
|
["against_voucher_type", "against_voucher_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.add_index(
|
||||||
|
"Advance Payment Ledger Entry",
|
||||||
|
["voucher_type", "voucher_no"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ class GLEntry(Document):
|
|||||||
|
|
||||||
if not self.is_cancelled and not (self.party_type and self.party):
|
if not self.is_cancelled and not (self.party_type and self.party):
|
||||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||||
# skipping validation for payroll entry creation in case party is not required
|
|
||||||
if not frappe.flags.party_not_required_for_receivable_payable:
|
if not frappe.flags.party_not_required: # skipping validation if party is not required
|
||||||
if account_type == "Receivable":
|
if account_type == "Receivable":
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"addtional_info",
|
"addtional_info",
|
||||||
"mode_of_payment",
|
"mode_of_payment",
|
||||||
"payment_order",
|
"payment_order",
|
||||||
|
"party_not_required",
|
||||||
"column_break3",
|
"column_break3",
|
||||||
"is_opening",
|
"is_opening",
|
||||||
"stock_entry",
|
"stock_entry",
|
||||||
@@ -543,6 +544,14 @@
|
|||||||
"label": "Is System Generated",
|
"label": "Is System Generated",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "party_not_required",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Party Not Required",
|
||||||
|
"no_copy": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
@@ -557,7 +566,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-07-18 15:32:29.413598",
|
"modified": "2025-09-29 13:05:46.982277",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry",
|
"name": "Journal Entry",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class JournalEntry(AccountsController):
|
|||||||
multi_currency: DF.Check
|
multi_currency: DF.Check
|
||||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||||
paid_loan: DF.Data | None
|
paid_loan: DF.Data | None
|
||||||
|
party_not_required: DF.Check
|
||||||
pay_to_recd_from: DF.Data | None
|
pay_to_recd_from: DF.Data | None
|
||||||
payment_order: DF.Link | None
|
payment_order: DF.Link | None
|
||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
@@ -543,10 +544,10 @@ class JournalEntry(AccountsController):
|
|||||||
for d in self.get("accounts"):
|
for d in self.get("accounts"):
|
||||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||||
|
|
||||||
# skipping validation for payroll entry creation
|
|
||||||
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
|
||||||
if account_type in ["Receivable", "Payable"]:
|
if account_type in ["Receivable", "Payable"]:
|
||||||
if not (d.party_type and d.party) and not skip_validation:
|
if (
|
||||||
|
not (d.party_type and d.party) and not self.party_not_required
|
||||||
|
): # skipping validation if party_not_required is passed via payroll entry
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||||
@@ -1139,6 +1140,11 @@ class JournalEntry(AccountsController):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# set flag to skip party validation
|
||||||
|
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||||
|
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||||
|
frappe.flags.party_not_required = True
|
||||||
|
|
||||||
gl_map.append(
|
gl_map.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
row,
|
row,
|
||||||
@@ -1166,6 +1172,7 @@ class JournalEntry(AccountsController):
|
|||||||
merge_entries=merge_entries,
|
merge_entries=merge_entries,
|
||||||
update_outstanding=update_outstanding,
|
update_outstanding=update_outstanding,
|
||||||
)
|
)
|
||||||
|
frappe.flags.party_not_required = False
|
||||||
if cancel:
|
if cancel:
|
||||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-25 04:45:28.117715",
|
"modified": "2025-09-29 13:01:48.916517",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -2640,6 +2640,38 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1}
|
||||||
|
)
|
||||||
|
def test_pr_status_rate_adjusted_from_pi(self):
|
||||||
|
pr = make_purchase_receipt(qty=5, rate=100)
|
||||||
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
|
pi.submit()
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
# Inital check
|
||||||
|
self.assertEqual(pr.status, "Completed")
|
||||||
|
|
||||||
|
pi.reload()
|
||||||
|
pi.cancel()
|
||||||
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
|
pi.items[0].rate = 80
|
||||||
|
pi.submit()
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
# Test 1 : Adjustment amount is negative
|
||||||
|
self.assertEqual(pr.status, "Completed")
|
||||||
|
|
||||||
|
pi.reload()
|
||||||
|
pi.cancel()
|
||||||
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
|
pi.items[0].rate = 120
|
||||||
|
pi.submit()
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
# Test 2 : Adjustment amount is positive
|
||||||
|
self.assertEqual(pr.status, "Completed")
|
||||||
|
|
||||||
def test_opening_invoice_rounding_adjustment_validation(self):
|
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||||
pi = make_purchase_invoice(do_not_save=1)
|
pi = make_purchase_invoice(do_not_save=1)
|
||||||
pi.items[0].rate = 99.98
|
pi.items[0].rate = 99.98
|
||||||
|
|||||||
@@ -912,7 +912,8 @@
|
|||||||
"label": "Rejected Serial and Batch Bundle",
|
"label": "Rejected Serial and Batch Bundle",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"print_hide": 1
|
"print_hide": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "wip_composite_asset",
|
"fieldname": "wip_composite_asset",
|
||||||
@@ -983,7 +984,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-12 16:33:13.453290",
|
"modified": "2025-10-14 13:01:54.441511",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
@@ -993,4 +994,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -798,6 +798,15 @@ frappe.ui.form.on("Sales Invoice", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
frm.set_query("sales_person", "sales_team", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_group: 0,
|
||||||
|
enabled: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
frm.redemption_conversion_factor = null;
|
frm.redemption_conversion_factor = null;
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ def get_asset_details_for_grouped_by_category(filters):
|
|||||||
# nosemgrep
|
# nosemgrep
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
SELECT a.name,
|
SELECT a.name, a.asset_name,
|
||||||
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||||
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||||
a.gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
@@ -583,6 +583,14 @@ def get_columns(filters):
|
|||||||
"width": 120,
|
"width": 120,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"label": _("Asset Name"),
|
||||||
|
"fieldname": "asset_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 140,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
columns += [
|
columns += [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -507,7 +507,8 @@ def depreciate_asset(asset_doc, date, notes):
|
|||||||
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
|
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
|
||||||
|
|
||||||
asset_doc.reload()
|
asset_doc.reload()
|
||||||
cancel_depreciation_entries(asset_doc, date)
|
if not frappe.flags.is_composite_component:
|
||||||
|
cancel_depreciation_entries(asset_doc, date)
|
||||||
|
|
||||||
|
|
||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
|
|||||||
@@ -492,14 +492,18 @@ class AssetCapitalization(StockController):
|
|||||||
asset = frappe.get_doc("Asset", item.asset)
|
asset = frappe.get_doc("Asset", item.asset)
|
||||||
|
|
||||||
if asset.calculate_depreciation:
|
if asset.calculate_depreciation:
|
||||||
notes = _(
|
frappe.flags.is_composite_component = True
|
||||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
try:
|
||||||
).format(
|
notes = _(
|
||||||
get_link_to_form(asset.doctype, asset.name),
|
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||||
get_link_to_form(self.doctype, self.get("name")),
|
).format(
|
||||||
)
|
get_link_to_form(asset.doctype, asset.name),
|
||||||
depreciate_asset(asset, self.posting_date, notes)
|
get_link_to_form(self.doctype, self.get("name")),
|
||||||
asset.reload()
|
)
|
||||||
|
depreciate_asset(asset, self.posting_date, notes)
|
||||||
|
asset.reload()
|
||||||
|
finally:
|
||||||
|
frappe.flags.is_composite_component = False
|
||||||
|
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||||
asset,
|
asset,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import get_link_to_form
|
from frappe.utils import cstr, get_link_to_form
|
||||||
|
|
||||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||||
|
|
||||||
@@ -142,18 +142,10 @@ class AssetMovement(Document):
|
|||||||
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
||||||
asset = frappe.get_doc("Asset", asset_id)
|
asset = frappe.get_doc("Asset", asset_id)
|
||||||
|
|
||||||
updates = {}
|
if cstr(employee) != asset.custodian:
|
||||||
if employee and employee != asset.custodian:
|
frappe.db.set_value("Asset", asset_id, "custodian", cstr(employee))
|
||||||
updates["custodian"] = employee
|
|
||||||
|
|
||||||
elif not employee and asset.custodian:
|
|
||||||
updates["custodian"] = ""
|
|
||||||
|
|
||||||
if location and location != asset.location:
|
if location and location != asset.location:
|
||||||
updates["location"] = location
|
frappe.db.set_value("Asset", asset_id, "location", location)
|
||||||
|
|
||||||
if updates:
|
|
||||||
frappe.db.set_value("Asset", asset_id, updates)
|
|
||||||
|
|
||||||
def log_asset_activity(self, asset_id, location, employee):
|
def log_asset_activity(self, asset_id, location, employee):
|
||||||
if location and employee:
|
if location and employee:
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
|
|||||||
.select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount"))
|
.select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount"))
|
||||||
.where(gle.account == aca.fixed_asset_account)
|
.where(gle.account == aca.fixed_asset_account)
|
||||||
.where(gle.is_cancelled == 0)
|
.where(gle.is_cancelled == 0)
|
||||||
|
.where(gle.is_opening == "No")
|
||||||
.where(company.name == filters.company)
|
.where(company.name == filters.company)
|
||||||
.where(asset.docstatus == 1)
|
.where(asset.docstatus == 1)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -284,15 +284,15 @@ def get_columns(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_message():
|
def get_message():
|
||||||
return """<span class="indicator">
|
return f"""<span class="indicator">
|
||||||
Valid till :
|
{_("Valid Till")}:
|
||||||
</span>
|
</span>
|
||||||
<span class="indicator orange">
|
<span class="indicator orange">
|
||||||
Expires in a week or less
|
{_("Expires in a week or less")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="indicator red">
|
<span class="indicator red">
|
||||||
Expires today / Already Expired
|
{_("Expires today or already expired")}
|
||||||
</span>"""
|
</span>"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -637,7 +637,8 @@ class SubcontractingController(StockController):
|
|||||||
|
|
||||||
if use_serial_batch_fields:
|
if use_serial_batch_fields:
|
||||||
rm_obj.use_serial_batch_fields = 1
|
rm_obj.use_serial_batch_fields = 1
|
||||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
if not self.flags.get("reset_raw_materials"):
|
||||||
|
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||||
|
|
||||||
if self.doctype == "Subcontracting Receipt":
|
if self.doctype == "Subcontracting Receipt":
|
||||||
if not use_serial_batch_fields:
|
if not use_serial_batch_fields:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
|
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
|
||||||
filters: {
|
filters: {
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
|
item_code: frm.doc.item_code,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -105,6 +106,8 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
__("View")
|
__("View")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let has_create_buttons = false;
|
||||||
|
|
||||||
if (frm.doc.status !== "Completed") {
|
if (frm.doc.status !== "Completed") {
|
||||||
if (frm.doc.status === "Closed") {
|
if (frm.doc.status === "Closed") {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
@@ -134,6 +137,7 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
},
|
},
|
||||||
__("Create")
|
__("Create")
|
||||||
);
|
);
|
||||||
|
has_create_buttons = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -148,12 +152,13 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
},
|
},
|
||||||
__("Create")
|
__("Create")
|
||||||
);
|
);
|
||||||
|
has_create_buttons = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (frm.doc.status !== "Closed") {
|
if (has_create_buttons && frm.doc.status !== "Closed") {
|
||||||
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frm.trigger("material_requirement");
|
frm.trigger("material_requirement");
|
||||||
|
|
||||||
|
|||||||
@@ -1543,6 +1543,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
|||||||
include_safety_stock = doc.get("include_safety_stock")
|
include_safety_stock = doc.get("include_safety_stock")
|
||||||
|
|
||||||
so_item_details = frappe._dict()
|
so_item_details = frappe._dict()
|
||||||
|
existing_sub_assembly_items = set()
|
||||||
|
|
||||||
sub_assembly_items = defaultdict(int)
|
sub_assembly_items = defaultdict(int)
|
||||||
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
|
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
|
||||||
@@ -1576,7 +1577,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
|||||||
item_details = {}
|
item_details = {}
|
||||||
if doc.get("sub_assembly_items"):
|
if doc.get("sub_assembly_items"):
|
||||||
item_details = get_raw_materials_of_sub_assembly_items(
|
item_details = get_raw_materials_of_sub_assembly_items(
|
||||||
so_item_details[doc.get("sales_order")].keys() if so_item_details else [],
|
existing_sub_assembly_items,
|
||||||
item_details,
|
item_details,
|
||||||
company,
|
company,
|
||||||
bom_no,
|
bom_no,
|
||||||
@@ -1839,7 +1840,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
|||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
.inner_join(child)
|
.inner_join(child)
|
||||||
.on(table.name == child.parent)
|
.on(table.name == child.parent)
|
||||||
.select(Sum(child.required_bom_qty))
|
.select(Sum(child.quantity * child.conversion_factor))
|
||||||
.where(
|
.where(
|
||||||
(table.docstatus == 1)
|
(table.docstatus == 1)
|
||||||
& (child.item_code == item_code)
|
& (child.item_code == item_code)
|
||||||
@@ -1955,6 +1956,7 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
sub_assembly_items,
|
sub_assembly_items,
|
||||||
planned_qty=planned_qty,
|
planned_qty=planned_qty,
|
||||||
)
|
)
|
||||||
|
existing_sub_assembly_items.add(item.item_code)
|
||||||
else:
|
else:
|
||||||
if not item.conversion_factor and item.purchase_uom:
|
if not item.conversion_factor and item.purchase_uom:
|
||||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||||
@@ -1992,6 +1994,9 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
|
|||||||
if filters.get("sales_orders"):
|
if filters.get("sales_orders"):
|
||||||
query = query.where(so_table.name.isin(filters.get("sales_orders")))
|
query = query.where(so_table.name.isin(filters.get("sales_orders")))
|
||||||
|
|
||||||
|
if filters.get("item_code"):
|
||||||
|
query = query.where(table.item_code == filters.get("item_code"))
|
||||||
|
|
||||||
if txt:
|
if txt:
|
||||||
query = query.where(table.parent.like(f"%{txt}%"))
|
query = query.where(table.parent.like(f"%{txt}%"))
|
||||||
|
|
||||||
|
|||||||
@@ -1637,11 +1637,17 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
def test_calculation_of_sub_assembly_items(self):
|
def test_calculation_of_sub_assembly_items(self):
|
||||||
make_item("Sub Assembly Item ", properties={"is_stock_item": 1})
|
make_item("Sub Assembly Item ", properties={"is_stock_item": 1})
|
||||||
|
make_item("Sub Assembly Item 2", properties={"is_stock_item": 1})
|
||||||
make_item("RM Item 1", properties={"is_stock_item": 1})
|
make_item("RM Item 1", properties={"is_stock_item": 1})
|
||||||
make_item("RM Item 2", properties={"is_stock_item": 1})
|
make_item("RM Item 2", properties={"is_stock_item": 1})
|
||||||
|
make_item("_Test FG Item 3", properties={"is_stock_item": 1})
|
||||||
|
make_item("_Test FG Item 4", properties={"is_stock_item": 1})
|
||||||
make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"])
|
make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"])
|
||||||
|
make_bom(item="Sub Assembly Item 2", raw_materials=["RM Item 2"])
|
||||||
make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"])
|
make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"])
|
||||||
make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"])
|
make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"])
|
||||||
|
make_bom(item="_Test FG Item 3", raw_materials=["RM Item 1"])
|
||||||
|
make_bom(item="_Test FG Item 4", raw_materials=["Sub Assembly Item 2"])
|
||||||
|
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
@@ -1677,12 +1683,39 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
"warehouse": "_Test Warehouse - _TC",
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
# Assembly item with similar RM item
|
||||||
|
plan.append(
|
||||||
|
"po_items",
|
||||||
|
{
|
||||||
|
"use_multi_level_bom": 1,
|
||||||
|
"item_code": "_Test FG Item 3",
|
||||||
|
"bom_no": frappe.db.get_value("Item", "_Test FG Item 3", "default_bom"),
|
||||||
|
"planned_qty": 10,
|
||||||
|
"planned_start_date": now_datetime(),
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Sub-assembly item with similar RM item
|
||||||
|
plan.append(
|
||||||
|
"po_items",
|
||||||
|
{
|
||||||
|
"use_multi_level_bom": 1,
|
||||||
|
"item_code": "_Test FG Item 4",
|
||||||
|
"bom_no": frappe.db.get_value("Item", "_Test FG Item 4", "default_bom"),
|
||||||
|
"planned_qty": 10,
|
||||||
|
"planned_start_date": now_datetime(),
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
plan.save()
|
plan.save()
|
||||||
|
|
||||||
plan.get_sub_assembly_items()
|
plan.get_sub_assembly_items()
|
||||||
|
|
||||||
self.assertEqual(plan.sub_assembly_items[0].qty, 20)
|
self.assertEqual(plan.sub_assembly_items[0].qty, 20) # Sub Assembly For FG 1
|
||||||
self.assertEqual(plan.sub_assembly_items[1].qty, 50)
|
self.assertEqual(plan.sub_assembly_items[1].qty, 50) # Sub Assembly For FG 2
|
||||||
|
self.assertEqual(plan.sub_assembly_items[2].qty, 10) # Sub Assembly For FG 4
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||||
get_items_for_material_requests,
|
get_items_for_material_requests,
|
||||||
@@ -1690,8 +1723,11 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||||
|
|
||||||
self.assertEqual(mr_items[0].get("quantity"), 80)
|
# RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock)
|
||||||
self.assertEqual(mr_items[1].get("quantity"), 70)
|
self.assertEqual(mr_items[0].get("quantity"), 90)
|
||||||
|
|
||||||
|
# RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock)
|
||||||
|
self.assertEqual(mr_items[1].get("quantity"), 80)
|
||||||
|
|
||||||
def test_production_plan_for_partial_sub_assembly_items(self):
|
def test_production_plan_for_partial_sub_assembly_items(self):
|
||||||
from erpnext.controllers.status_updater import OverAllowanceError
|
from erpnext.controllers.status_updater import OverAllowanceError
|
||||||
|
|||||||
@@ -113,6 +113,13 @@ class ProductionPlanReport:
|
|||||||
self.orders = query.run(as_dict=True)
|
self.orders = query.run(as_dict=True)
|
||||||
|
|
||||||
def get_raw_materials(self):
|
def get_raw_materials(self):
|
||||||
|
"""Retrieve raw materials and source warehouses for production orders.
|
||||||
|
|
||||||
|
This method collects BOM or Work Order items depending on the selected
|
||||||
|
filter and updates `self.raw_materials_dict`, `self.warehouses`,
|
||||||
|
and `self.item_codes` accordingly.
|
||||||
|
"""
|
||||||
|
|
||||||
if not self.orders:
|
if not self.orders:
|
||||||
return
|
return
|
||||||
self.warehouses = [d.warehouse for d in self.orders]
|
self.warehouses = [d.warehouse for d in self.orders]
|
||||||
@@ -135,7 +142,7 @@ class ProductionPlanReport:
|
|||||||
)
|
)
|
||||||
or []
|
or []
|
||||||
)
|
)
|
||||||
self.warehouses.extend([d.source_warehouse for d in raw_materials])
|
self.warehouses.extend([d.warehouse for d in raw_materials])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
bom_nos = []
|
bom_nos = []
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import urllib.parse
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
def get_context(context):
|
||||||
if frappe.form_dict.project:
|
if project := frappe.form_dict.project:
|
||||||
context.parents = [
|
title = frappe.utils.data.escape_html(project)
|
||||||
{"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project}
|
route = "/projects?" + urllib.parse.urlencode({"project": project})
|
||||||
]
|
context.parents = [{"title": title, "route": route}]
|
||||||
context.success_url = "/projects?project=" + frappe.form_dict.project
|
context.success_url = route
|
||||||
|
|
||||||
elif context.doc and context.doc.get("project"):
|
elif context.doc and (project := context.doc.get("project")):
|
||||||
context.parents = [
|
title = frappe.utils.data.escape_html(project)
|
||||||
{"title": context.doc.project, "route": "/projects?project=" + context.doc.project}
|
route = "/projects?" + urllib.parse.urlencode({"project": project})
|
||||||
]
|
context.parents = [{"title": title, "route": route}]
|
||||||
context.success_url = "/projects?project=" + context.doc.project
|
context.success_url = route
|
||||||
|
|||||||
@@ -171,13 +171,15 @@ erpnext.buying = {
|
|||||||
shipping_address: this.frm.doc.shipping_address
|
shipping_address: this.frm.doc.shipping_address
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
if (!this.frm.doc.billing_address)
|
||||||
|
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||||
|
|
||||||
if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
|
if (
|
||||||
this.frm.set_value(
|
!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") ||
|
||||||
"shipping_address",
|
this.frm.doc.shipping_address
|
||||||
r.message.shipping_address || this.frm.doc.shipping_address || ""
|
)
|
||||||
);
|
return;
|
||||||
|
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
erpnext.utils.set_letter_head(this.frm)
|
erpnext.utils.set_letter_head(this.frm)
|
||||||
|
|||||||
@@ -1022,19 +1022,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
} else {
|
} else {
|
||||||
set_pricing();
|
set_pricing();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
if (
|
||||||
|
frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
|
||||||
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
|
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
|
||||||
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
|
!this.frm.doc.shipping_address
|
||||||
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
|
) {
|
||||||
|
let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
|
||||||
if (!is_drop_ship) {
|
|
||||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
|
||||||
set_party_account(set_pricing);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!is_drop_ship) {
|
||||||
|
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||||
|
set_party_account(set_pricing);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
set_party_account(set_pricing);
|
set_party_account(set_pricing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ erpnext.sales_common = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.toggle_editable_price_list_rate();
|
this.toggle_editable_price_list_rate();
|
||||||
|
this.change_warehouse_labels_for_return();
|
||||||
}
|
}
|
||||||
|
|
||||||
company() {
|
company() {
|
||||||
@@ -500,6 +501,33 @@ erpnext.sales_common = {
|
|||||||
this.frm.set_value("discount_amount", 0);
|
this.frm.set_value("discount_amount", 0);
|
||||||
this.frm.set_value("additional_discount_percentage", 0);
|
this.frm.set_value("additional_discount_percentage", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_return() {
|
||||||
|
let reset = !this.frm.doc.is_return;
|
||||||
|
this.change_warehouse_labels_for_return(reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
change_warehouse_labels_for_return(reset) {
|
||||||
|
// swap source and target warehouse labels for return
|
||||||
|
let source_warehouse_label = __("Source Warehouse");
|
||||||
|
let target_warehouse_label = __("Set Target Warehouse");
|
||||||
|
|
||||||
|
if (this.frm.doc.doctype == "Delivery Note") {
|
||||||
|
source_warehouse_label = __("Set Source Warehouse");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
// reset to original labels
|
||||||
|
this.frm.set_df_property("set_warehouse", "label", source_warehouse_label);
|
||||||
|
this.frm.set_df_property("set_target_warehouse", "label", target_warehouse_label);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.frm.doc.is_return) {
|
||||||
|
this.frm.set_df_property("set_warehouse", "label", target_warehouse_label);
|
||||||
|
this.frm.set_df_property("set_target_warehouse", "label", source_warehouse_label);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -457,7 +457,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
(["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) &&
|
(["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) &&
|
||||||
!this.frm.doc.is_return) ||
|
!this.frm.doc.is_return) ||
|
||||||
(this.frm.doc.doctype === "Stock Entry" &&
|
(this.frm.doc.doctype === "Stock Entry" &&
|
||||||
this.frm.doc.purpose === "Material Receipt")
|
(this.frm.doc.purpose === "Material Receipt" ||
|
||||||
|
(this.frm.doc.purpose === "Manufacture" && this.item.is_finished_item)))
|
||||||
) {
|
) {
|
||||||
is_inward = true;
|
is_inward = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("sales_person", "sales_team", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_group: 0,
|
||||||
|
enabled: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
frm.set_df_property("packed_items", "cannot_add_rows", true);
|
frm.set_df_property("packed_items", "cannot_add_rows", true);
|
||||||
frm.set_df_property("packed_items", "cannot_delete_rows", true);
|
frm.set_df_property("packed_items", "cannot_delete_rows", true);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -262,6 +262,20 @@ def update_roles():
|
|||||||
|
|
||||||
def create_default_role_profiles():
|
def create_default_role_profiles():
|
||||||
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||||
|
if frappe.db.exists("Role Profile", role_profile_name):
|
||||||
|
role_profile = frappe.get_doc("Role Profile", role_profile_name)
|
||||||
|
existing_roles = [row.role for row in role_profile.roles]
|
||||||
|
|
||||||
|
role_profile.roles = [row for row in role_profile.roles if row.role in roles]
|
||||||
|
|
||||||
|
for role in roles:
|
||||||
|
if role not in existing_roles:
|
||||||
|
role_profile.append("roles", {"role": role})
|
||||||
|
|
||||||
|
role_profile.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
role_profile = frappe.new_doc("Role Profile")
|
role_profile = frappe.new_doc("Role Profile")
|
||||||
role_profile.role_profile = role_profile_name
|
role_profile.role_profile = role_profile_name
|
||||||
for role in roles:
|
for role in roles:
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ def update_page_info(bootinfo):
|
|||||||
def bootinfo(bootinfo):
|
def bootinfo(bootinfo):
|
||||||
if bootinfo.get("user") and bootinfo["user"].get("name"):
|
if bootinfo.get("user") and bootinfo["user"].get("name"):
|
||||||
bootinfo["user"]["employee"] = ""
|
bootinfo["user"]["employee"] = ""
|
||||||
|
frappe.session.data.employee = ""
|
||||||
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
|
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
|
||||||
if employee:
|
if employee:
|
||||||
bootinfo["user"]["employee"] = employee
|
bootinfo["user"]["employee"] = employee
|
||||||
|
frappe.session.data.employee = employee
|
||||||
|
|||||||
@@ -158,7 +158,9 @@ class Batch(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def recalculate_batch_qty(self):
|
def recalculate_batch_qty(self):
|
||||||
batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True)
|
batches = get_batch_qty(
|
||||||
|
batch_no=self.name, item_code=self.item, for_stock_levels=True, consider_negative_batches=True
|
||||||
|
)
|
||||||
batch_qty = 0.0
|
batch_qty = 0.0
|
||||||
if batches:
|
if batches:
|
||||||
for row in batches:
|
for row in batches:
|
||||||
@@ -260,6 +262,7 @@ def get_batch_qty(
|
|||||||
"posting_date": posting_date,
|
"posting_date": posting_date,
|
||||||
"posting_time": posting_time,
|
"posting_time": posting_time,
|
||||||
"batch_no": batch_no,
|
"batch_no": batch_no,
|
||||||
|
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
||||||
"ignore_voucher_nos": ignore_voucher_nos,
|
"ignore_voucher_nos": ignore_voucher_nos,
|
||||||
"for_stock_levels": for_stock_levels,
|
"for_stock_levels": for_stock_levels,
|
||||||
"consider_negative_batches": consider_negative_batches,
|
"consider_negative_batches": consider_negative_batches,
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
|||||||
if (
|
if (
|
||||||
doc.docstatus == 1 &&
|
doc.docstatus == 1 &&
|
||||||
!doc.is_return &&
|
!doc.is_return &&
|
||||||
|
doc.per_returned != 100 &&
|
||||||
doc.status != "Closed" &&
|
doc.status != "Closed" &&
|
||||||
flt(doc.per_billed) < 100 &&
|
flt(doc.per_billed) < 100 &&
|
||||||
frappe.model.can_create("Sales Invoice")
|
frappe.model.can_create("Sales Invoice")
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from frappe.contacts.doctype.address.address import get_company_address
|
|||||||
from frappe.desk.notifications import clear_doctype_notifications
|
from frappe.desk.notifications import clear_doctype_notifications
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext.accounts.party import get_due_date
|
from erpnext.accounts.party import get_due_date
|
||||||
@@ -790,35 +792,39 @@ def get_list_context(context=None):
|
|||||||
|
|
||||||
def get_invoiced_qty_map(delivery_note):
|
def get_invoiced_qty_map(delivery_note):
|
||||||
"""returns a map: {dn_detail: invoiced_qty}"""
|
"""returns a map: {dn_detail: invoiced_qty}"""
|
||||||
invoiced_qty_map = {}
|
sii = DocType("Sales Invoice Item")
|
||||||
|
|
||||||
for dn_detail, qty in frappe.db.sql(
|
invoiced_qty_map = frappe._dict(
|
||||||
"""select dn_detail, qty from `tabSales Invoice Item`
|
(
|
||||||
where delivery_note=%s and docstatus=1""",
|
frappe.qb.from_(sii)
|
||||||
delivery_note,
|
.select(sii.dn_detail, Sum(sii.qty).as_("qty"))
|
||||||
):
|
.where((sii.delivery_note == delivery_note) & (sii.docstatus == 1))
|
||||||
if not invoiced_qty_map.get(dn_detail):
|
.groupby(sii.dn_detail)
|
||||||
invoiced_qty_map[dn_detail] = 0
|
).run()
|
||||||
invoiced_qty_map[dn_detail] += qty
|
)
|
||||||
|
|
||||||
return invoiced_qty_map
|
return invoiced_qty_map
|
||||||
|
|
||||||
|
|
||||||
def get_returned_qty_map(delivery_note):
|
def get_returned_qty_map(delivery_note):
|
||||||
"""returns a map: {so_detail: returned_qty}"""
|
"""returns a map: {so_detail: returned_qty}"""
|
||||||
|
dn = DocType("Delivery Note")
|
||||||
|
dni = DocType("Delivery Note Item")
|
||||||
|
|
||||||
returned_qty_map = frappe._dict(
|
returned_qty_map = frappe._dict(
|
||||||
frappe.db.sql(
|
(
|
||||||
"""select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty
|
frappe.qb.from_(dni)
|
||||||
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
|
.join(dn)
|
||||||
where dn.name = dn_item.parent
|
.on(dn.name == dni.parent)
|
||||||
and dn.docstatus = 1
|
.select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty"))
|
||||||
and dn.is_return = 1
|
.where(
|
||||||
and dn.return_against = %s
|
(dn.docstatus == 1)
|
||||||
and dn_item.qty <= 0
|
& (dn.is_return == 1)
|
||||||
group by dn_item.item_code
|
& (dn.return_against == delivery_note)
|
||||||
""",
|
& (dni.qty <= 0)
|
||||||
delivery_note,
|
)
|
||||||
)
|
.groupby(dni.dn_detail)
|
||||||
|
).run()
|
||||||
)
|
)
|
||||||
|
|
||||||
return returned_qty_map
|
return returned_qty_map
|
||||||
|
|||||||
@@ -1115,7 +1115,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
|||||||
buying_settings = frappe.get_single("Buying Settings")
|
buying_settings = frappe.get_single("Buying Settings")
|
||||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||||
|
|
||||||
total_amount, total_billed_amount = 0, 0
|
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
|
||||||
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
|
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
|
||||||
|
|
||||||
if adjust_incoming_rate:
|
if adjust_incoming_rate:
|
||||||
@@ -1155,6 +1155,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
|||||||
) * item.qty
|
) * item.qty
|
||||||
|
|
||||||
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
|
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
|
||||||
|
pi_landed_cost_amount += adjusted_amt
|
||||||
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
||||||
elif amount and item.billed_amt > amount:
|
elif amount and item.billed_amt > amount:
|
||||||
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
|
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
|
||||||
@@ -1165,6 +1166,9 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if pi_landed_cost_amount < 0:
|
||||||
|
total_billed_amount += abs(pi_landed_cost_amount)
|
||||||
|
|
||||||
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
|
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
|
||||||
pr_doc.db_set("per_billed", percent_billed)
|
pr_doc.db_set("per_billed", percent_billed)
|
||||||
|
|
||||||
|
|||||||
@@ -778,7 +778,8 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Material Request Item",
|
"label": "Material Request Item",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "expense_account",
|
"fieldname": "expense_account",
|
||||||
@@ -1038,7 +1039,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Rejected Serial and Batch Bundle",
|
"label": "Rejected Serial and Batch Bundle",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle"
|
"options": "Serial and Batch Bundle",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||||
@@ -1147,7 +1149,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-12 17:10:43.780622",
|
"modified": "2025-10-14 12:59:20.384056",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt Item",
|
"name": "Purchase Receipt Item",
|
||||||
@@ -1158,4 +1160,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class SerialandBatchBundle(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.returned_against and self.docstatus == 1:
|
if (self.returned_against or self.voucher_type == "Stock Reconciliation") and self.docstatus == 1:
|
||||||
kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no
|
kwargs["ignore_voucher_detail_no"] = self.voucher_detail_no
|
||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
@@ -2677,7 +2677,10 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
|||||||
else:
|
else:
|
||||||
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
|
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
|
||||||
|
|
||||||
if kwargs.voucher_no:
|
if kwargs.ignore_voucher_detail_no:
|
||||||
|
query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no)
|
||||||
|
|
||||||
|
elif kwargs.voucher_no:
|
||||||
query = query.where(stock_ledger_entry.voucher_no != kwargs.voucher_no)
|
query = query.where(stock_ledger_entry.voucher_no != kwargs.voucher_no)
|
||||||
|
|
||||||
return query.run(as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|||||||
@@ -1323,18 +1323,9 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
posting_date="2021-07-02", # Illegal SE
|
posting_date="2021-07-02", # Illegal SE
|
||||||
purpose="Material Transfer",
|
purpose="Material Transfer",
|
||||||
),
|
),
|
||||||
dict(
|
|
||||||
item_code=item_code,
|
|
||||||
qty=2,
|
|
||||||
from_warehouse=warehouse_names[0],
|
|
||||||
to_warehouse=warehouse_names[1],
|
|
||||||
batch_no=batch_no,
|
|
||||||
posting_date="2021-07-02", # Illegal SE
|
|
||||||
purpose="Material Transfer",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries)
|
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
|
||||||
|
|
||||||
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||||
def test_future_negative_sle_batch(self):
|
def test_future_negative_sle_batch(self):
|
||||||
|
|||||||
@@ -188,6 +188,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Basic Rate (as per Stock UOM)",
|
"label": "Basic Rate (as per Stock UOM)",
|
||||||
|
"non_negative": 1,
|
||||||
"oldfieldname": "incoming_rate",
|
"oldfieldname": "incoming_rate",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
@@ -446,7 +447,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Stock Entry",
|
"options": "Stock Entry",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "ste_detail",
|
"fieldname": "ste_detail",
|
||||||
@@ -454,7 +456,8 @@
|
|||||||
"label": "Stock Entry Child",
|
"label": "Stock Entry Child",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_51",
|
"fieldname": "column_break_51",
|
||||||
@@ -613,7 +616,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-26 21:01:58.544797",
|
"modified": "2025-10-14 15:10:38.373099",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
|
|||||||
@@ -589,6 +589,10 @@ class StockReconciliation(StockController):
|
|||||||
if row.get(field):
|
if row.get(field):
|
||||||
key.append(row.get(field))
|
key.append(row.get(field))
|
||||||
|
|
||||||
|
for dimension in get_inventory_dimensions():
|
||||||
|
if row.get(dimension.get("fieldname")):
|
||||||
|
key.append(row.get(dimension.get("fieldname")))
|
||||||
|
|
||||||
if key in item_warehouse_combinations:
|
if key in item_warehouse_combinations:
|
||||||
self.validation_messages.append(
|
self.validation_messages.append(
|
||||||
_get_msg(row_num, _("Same item and warehouse combination already entered."))
|
_get_msg(row_num, _("Same item and warehouse combination already entered."))
|
||||||
|
|||||||
@@ -1385,12 +1385,12 @@ def get_batch_current_qty(batch):
|
|||||||
|
|
||||||
|
|
||||||
def throw_negative_batch_validation(batch_no, qty):
|
def throw_negative_batch_validation(batch_no, qty):
|
||||||
frappe.msgprint(
|
# This validation is important for backdated stock transactions with batch items
|
||||||
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry."
|
"The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry."
|
||||||
).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)),
|
).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)),
|
||||||
title=_("Warning!"),
|
title=_("Negative Stock Error"),
|
||||||
indicator="orange",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -732,6 +732,10 @@ class update_entries_after:
|
|||||||
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
|
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
|
||||||
dependant_sle.voucher_no
|
dependant_sle.voucher_no
|
||||||
):
|
):
|
||||||
|
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
|
||||||
|
return
|
||||||
|
|
||||||
|
val["transfer_entry_to_repost"] = True
|
||||||
self.distinct_item_warehouses[key] = val
|
self.distinct_item_warehouses[key] = val
|
||||||
self.new_items_found = True
|
self.new_items_found = True
|
||||||
|
|
||||||
@@ -888,9 +892,8 @@ class update_entries_after:
|
|||||||
sle.stock_value = self.wh_data.stock_value
|
sle.stock_value = self.wh_data.stock_value
|
||||||
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
|
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
|
||||||
|
|
||||||
if not sle.is_adjustment_entry:
|
sle.stock_value_difference = stock_value_difference
|
||||||
sle.stock_value_difference = stock_value_difference
|
if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
|
||||||
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
|
|
||||||
sle.stock_value_difference = (
|
sle.stock_value_difference = (
|
||||||
get_stock_value_difference(
|
get_stock_value_difference(
|
||||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def reset_raw_materials(self):
|
def reset_raw_materials(self):
|
||||||
self.supplied_items = []
|
self.supplied_items = []
|
||||||
|
self.flags.reset_raw_materials = True
|
||||||
self.create_raw_materials_supplied()
|
self.create_raw_materials_supplied()
|
||||||
|
|
||||||
def validate_closed_subcontracting_order(self):
|
def validate_closed_subcontracting_order(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user