mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-23 08:38:30 +00:00
Merge branch 'develop' into st31369
This commit is contained in:
1
.github/workflows/lock.yml
vendored
1
.github/workflows/lock.yml
vendored
@@ -19,3 +19,4 @@ jobs:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: 14
|
||||
pr-inactive-days: 14
|
||||
exclude-pr-created-before: 2025-01-01
|
||||
|
||||
@@ -50,6 +50,7 @@ def get_group_by_asset_category_data(filters):
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
@@ -247,6 +248,7 @@ def get_group_by_asset_data(filters):
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
@@ -276,6 +278,7 @@ def get_assets_for_grouped_by_category(filters):
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
@@ -284,6 +287,11 @@ def get_assets_for_grouped_by_category(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -307,7 +315,6 @@ def get_assets_for_grouped_by_category(filters):
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
@@ -319,6 +326,7 @@ def get_assets_for_grouped_by_category(filters):
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -354,6 +362,7 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.name as name,
|
||||
@@ -362,6 +371,11 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
@@ -385,7 +399,6 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{finance_book_filter} {condition}
|
||||
@@ -397,6 +410,7 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_as_on_from_date_credit,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
@@ -503,6 +517,12 @@ def get_columns(filters):
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation eliminated via reversal"),
|
||||
"fieldname": "depreciation_eliminated_via_reversal",
|
||||
"fieldtype": "Currency",
|
||||
"width": 270,
|
||||
},
|
||||
{
|
||||
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
|
||||
"fieldname": "net_asset_value_as_on_from_date",
|
||||
|
||||
@@ -307,6 +307,7 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
.where(
|
||||
(inv.docstatus == 1)
|
||||
& (deferred_flag_field == 1)
|
||||
& (inv.company == self.filters.company)
|
||||
& (
|
||||
(
|
||||
(self.period_list[0].from_date >= inv_item.service_start_date)
|
||||
|
||||
@@ -2,5 +2,27 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Delivered Items To Be Billed"] = {
|
||||
filters: [],
|
||||
filters: [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_default("Company"),
|
||||
},
|
||||
{
|
||||
label: __("As on Date"),
|
||||
fieldname: "posting_date",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
label: __("Delivery Note"),
|
||||
fieldname: "delivery_note",
|
||||
fieldtype: "Link",
|
||||
options: "Delivery Note",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
from frappe import _
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
|
||||
|
||||
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
|
||||
def execute(filters=None):
|
||||
columns = get_column()
|
||||
args = get_args()
|
||||
data = get_ordered_to_be_billed_data(args)
|
||||
data = get_ordered_to_be_billed_data(args, filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
@@ -76,13 +77,6 @@ def get_column():
|
||||
"options": "Project",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -92,5 +86,6 @@ def get_args():
|
||||
"party": "customer",
|
||||
"date": "posting_date",
|
||||
"order": "name",
|
||||
"order_by": "desc",
|
||||
"order_by": Order.desc,
|
||||
"reference_field": "delivery_note",
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder.functions import IfNull, Round
|
||||
|
||||
from erpnext import get_default_currency
|
||||
|
||||
|
||||
def get_ordered_to_be_billed_data(args):
|
||||
def get_ordered_to_be_billed_data(args, filters=None):
|
||||
doctype, party = args.get("doctype"), args.get("party")
|
||||
child_tab = doctype + " Item"
|
||||
precision = (
|
||||
@@ -18,47 +19,57 @@ def get_ordered_to_be_billed_data(args):
|
||||
or 2
|
||||
)
|
||||
|
||||
project_field = get_project_field(doctype, party)
|
||||
doctype = frappe.qb.DocType(doctype)
|
||||
child_doctype = frappe.qb.DocType(child_tab)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
Select
|
||||
`{parent_tab}`.name, `{parent_tab}`.{date_field},
|
||||
`{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
|
||||
`{child_tab}`.item_code,
|
||||
`{child_tab}`.base_amount,
|
||||
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)),
|
||||
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)),
|
||||
(`{child_tab}`.base_amount -
|
||||
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) -
|
||||
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))),
|
||||
`{child_tab}`.item_name, `{child_tab}`.description,
|
||||
{project_field}, `{parent_tab}`.company
|
||||
from
|
||||
`{parent_tab}`, `{child_tab}`
|
||||
where
|
||||
`{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1
|
||||
and `{parent_tab}`.status not in ('Closed', 'Completed')
|
||||
and `{child_tab}`.amount > 0
|
||||
and (`{child_tab}`.base_amount -
|
||||
round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) -
|
||||
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0
|
||||
order by
|
||||
`{parent_tab}`.{order} {order_by}
|
||||
""".format(
|
||||
parent_tab="tab" + doctype,
|
||||
child_tab="tab" + child_tab,
|
||||
precision=precision,
|
||||
party=party,
|
||||
date_field=args.get("date"),
|
||||
project_field=project_field,
|
||||
order=args.get("order"),
|
||||
order_by=args.get("order_by"),
|
||||
docname = filters.get(args.get("reference_field"), None)
|
||||
project_field = get_project_field(doctype, child_doctype, party)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(doctype)
|
||||
.inner_join(child_doctype)
|
||||
.on(doctype.name == child_doctype.parent)
|
||||
.select(
|
||||
doctype.name,
|
||||
doctype[args.get("date")].as_("date"),
|
||||
doctype[party],
|
||||
doctype[party + "_name"],
|
||||
child_doctype.item_code,
|
||||
child_doctype.base_amount.as_("amount"),
|
||||
(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1)).as_("billed_amount"),
|
||||
(child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0)).as_("returned_amount"),
|
||||
(
|
||||
child_doctype.base_amount
|
||||
- (child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1))
|
||||
- (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
|
||||
).as_("pending_amount"),
|
||||
child_doctype.item_name,
|
||||
child_doctype.description,
|
||||
project_field,
|
||||
)
|
||||
.where(
|
||||
(doctype.docstatus == 1)
|
||||
& (doctype.status.notin(["Closed", "Completed"]))
|
||||
& (doctype.company == filters.get("company"))
|
||||
& (doctype.posting_date <= filters.get("posting_date"))
|
||||
& (child_doctype.amount > 0)
|
||||
& (
|
||||
child_doctype.base_amount
|
||||
- Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)
|
||||
- (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
|
||||
)
|
||||
> 0
|
||||
)
|
||||
.orderby(doctype[args.get("order")], order=args.get("order_by"))
|
||||
)
|
||||
|
||||
if docname:
|
||||
query = query.where(doctype.name == docname)
|
||||
|
||||
def get_project_field(doctype, party):
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_project_field(doctype, child_doctype, party):
|
||||
if party == "supplier":
|
||||
doctype = doctype + " Item"
|
||||
return "`tab%s`.project" % (doctype)
|
||||
return child_doctype.project
|
||||
return doctype.project
|
||||
|
||||
@@ -2,5 +2,27 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Received Items To Be Billed"] = {
|
||||
filters: [],
|
||||
filters: [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_default("Company"),
|
||||
},
|
||||
{
|
||||
label: __("As on Date"),
|
||||
fieldname: "posting_date",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
label: __("Purchase Receipt"),
|
||||
fieldname: "purchase_receipt",
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Receipt",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
from frappe import _
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
|
||||
|
||||
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
|
||||
def execute(filters=None):
|
||||
columns = get_column()
|
||||
args = get_args()
|
||||
data = get_ordered_to_be_billed_data(args)
|
||||
data = get_ordered_to_be_billed_data(args, filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
@@ -76,13 +77,6 @@ def get_column():
|
||||
"options": "Project",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -92,5 +86,6 @@ def get_args():
|
||||
"party": "supplier",
|
||||
"date": "posting_date",
|
||||
"order": "name",
|
||||
"order_by": "desc",
|
||||
"order_by": Order.desc,
|
||||
"reference_field": "purchase_receipt",
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Accounts Payable",
|
||||
"link_count": 0,
|
||||
"link_to": "Accounts Payable",
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Accounts Payable Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Accounts Payable Summary",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Register",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Register",
|
||||
@@ -123,7 +123,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Item-wise Purchase Register",
|
||||
"link_count": 0,
|
||||
"link_to": "Item-wise Purchase Register",
|
||||
@@ -133,7 +133,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Order Analysis",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Order Analysis",
|
||||
@@ -143,7 +143,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Received Items To Be Billed",
|
||||
"link_count": 0,
|
||||
"link_to": "Received Items To Be Billed",
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Supplier Ledger Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Supplier Ledger Summary",
|
||||
|
||||
@@ -406,7 +406,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
|
||||
if (!doc.items.every((item) => item.qty == item.subcontracted_quantity)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
|
||||
@@ -922,7 +922,7 @@ def is_po_fully_subcontracted(po_name):
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.sco_qty))
|
||||
.where((table.parent == po_name) & (table.qty != table.subcontracted_quantity))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
@@ -977,7 +977,7 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.sco_qty,
|
||||
"condition": lambda item: item.qty != item.subcontracted_quantity,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -1097,9 +1097,9 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].sco_qty, 5)
|
||||
self.assertEqual(po.items[1].sco_qty, 0)
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
self.assertEqual(po.items[0].subcontracted_quantity, 5)
|
||||
self.assertEqual(po.items[1].subcontracted_quantity, 0)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
|
||||
|
||||
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
|
||||
self.assertEqual(sco.items[0].amount, 2000)
|
||||
@@ -1135,10 +1135,10 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 25)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"sco_qty",
|
||||
"subcontracted_quantity",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -933,7 +933,7 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sco_qty",
|
||||
"fieldname": "subcontracted_quantity",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
@@ -941,11 +941,12 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-18 12:35:04.432636",
|
||||
"modified": "2025-03-02 16:58:26.059601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
@@ -953,6 +954,7 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -82,10 +82,10 @@ class PurchaseOrderItem(Document):
|
||||
sales_order_item: DF.Data | None
|
||||
sales_order_packed_item: DF.Data | None
|
||||
schedule_date: DF.Date
|
||||
sco_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
subcontracted_quantity: DF.Float
|
||||
supplier_part_no: DF.Data | None
|
||||
supplier_quotation: DF.Link | None
|
||||
supplier_quotation_item: DF.Link | None
|
||||
|
||||
@@ -1237,7 +1237,7 @@ class StockController(AccountsController):
|
||||
child_tab.item_code,
|
||||
child_tab.qty,
|
||||
)
|
||||
.where(parent_tab.docstatus < 2)
|
||||
.where(parent_tab.docstatus == 1)
|
||||
)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
|
||||
@@ -104,18 +104,18 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Order" and not item.sc_conversion_factor
|
||||
self.doctype == "Subcontracting Order" and not item.subcontracting_conversion_factor
|
||||
): # this condition will only be true if user has recently updated from develop branch
|
||||
service_item_qty = frappe.get_value(
|
||||
"Subcontracting Order Service Item",
|
||||
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
|
||||
fieldname=["qty"],
|
||||
)
|
||||
item.sc_conversion_factor = service_item_qty / item.qty
|
||||
item.subcontracting_conversion_factor = service_item_qty / item.qty
|
||||
|
||||
if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
|
||||
get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item)
|
||||
/ item.sc_conversion_factor,
|
||||
get_pending_subcontracted_quantity(self.purchase_order).get(item.purchase_order_item)
|
||||
/ item.subcontracting_conversion_factor,
|
||||
frappe.get_precision("Purchase Order Item", "qty"),
|
||||
):
|
||||
frappe.throw(
|
||||
@@ -1138,10 +1138,14 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_sco_qty(po_name):
|
||||
def get_pending_subcontracted_quantity(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
|
||||
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name, table.qty, table.subcontracted_quantity)
|
||||
.where(table.parent == po_name)
|
||||
)
|
||||
return {item.name: item.qty - item.subcontracted_quantity for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: info@erpnext.com\n"
|
||||
"POT-Creation-Date: 2025-03-02 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-03-03 04:14\n"
|
||||
"PO-Revision-Date: 2025-03-05 04:06\n"
|
||||
"Last-Translator: info@erpnext.com\n"
|
||||
"Language-Team: Arabic\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -47019,7 +47019,7 @@ msgstr "إرسال"
|
||||
|
||||
#: erpnext/templates/includes/footer/footer_extension.html:20
|
||||
msgid "Sending..."
|
||||
msgstr ""
|
||||
msgstr "إرسال..."
|
||||
|
||||
#. Label of the sent (Check) field in DocType 'Project Update'
|
||||
#: erpnext/projects/doctype/project_update/project_update.json
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: info@erpnext.com\n"
|
||||
"POT-Creation-Date: 2025-03-02 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-03-04 04:10\n"
|
||||
"PO-Revision-Date: 2025-03-05 04:06\n"
|
||||
"Last-Translator: info@erpnext.com\n"
|
||||
"Language-Team: Bosnian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -37106,7 +37106,7 @@ msgstr "Postavi Knjigovodstvenu Dimenziju {} u {}"
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.js:89
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:752
|
||||
msgid "Please set Company"
|
||||
msgstr "Postavite Kompaniju"
|
||||
msgstr "Postavi Kompaniju"
|
||||
|
||||
#: erpnext/assets/doctype/asset/depreciation.py:364
|
||||
msgid "Please set Depreciation related Accounts in Asset Category {0} or Company {1}"
|
||||
@@ -37119,7 +37119,7 @@ msgstr "Postavi E-poštu/Telefon za kontakt"
|
||||
#: erpnext/regional/italy/utils.py:278
|
||||
#, python-format
|
||||
msgid "Please set Fiscal Code for the customer '%s'"
|
||||
msgstr "Postavite Fiskalni Kod za Klijenta '%s'"
|
||||
msgstr "Postavi Fiskalni Kod za Klijenta '%s'"
|
||||
|
||||
#: erpnext/regional/italy/utils.py:286
|
||||
#, python-format
|
||||
@@ -47894,7 +47894,7 @@ msgstr "Postavi Iz Skladišta"
|
||||
#. 'Territory'
|
||||
#: erpnext/setup/doctype/territory/territory.json
|
||||
msgid "Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution."
|
||||
msgstr "Postavite budžete po grupama za ovaj Distrikt. Takođe možete uključiti sezonske varijacije postavljanjem Distribucije."
|
||||
msgstr "Postavi budžete po grupama za ovaj Distrikt. Takođe možete uključiti sezonske varijacije postavljanjem Distribucije."
|
||||
|
||||
#. Label of the set_landed_cost_based_on_purchase_invoice_rate (Check) field in
|
||||
#. DocType 'Buying Settings'
|
||||
@@ -47937,7 +47937,7 @@ msgstr "Postavi Datum Knjiženja"
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom/bom.js:898
|
||||
msgid "Set Process Loss Item Quantity"
|
||||
msgstr "Postavite količinu gubitka artikla u procesu"
|
||||
msgstr "Postavi količinu gubitka artikla u procesu"
|
||||
|
||||
#: erpnext/projects/doctype/project/project.js:149
|
||||
#: erpnext/projects/doctype/project/project.js:157
|
||||
@@ -48042,7 +48042,7 @@ msgstr "Postavi Standard Račun Zaliha za Stalno Upravljanje Zalihama"
|
||||
|
||||
#: erpnext/setup/doctype/company/company.py:450
|
||||
msgid "Set default {0} account for non stock items"
|
||||
msgstr "Postavite Standard Račun {0} za artikle bez zaliha"
|
||||
msgstr "Postavi Standard Račun {0} za artikle koji nisu na zalihama"
|
||||
|
||||
#. Description of the 'Fetch Value From' (Select) field in DocType 'Inventory
|
||||
#. Dimension'
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: info@erpnext.com\n"
|
||||
"POT-Creation-Date: 2025-03-02 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-03-03 04:15\n"
|
||||
"PO-Revision-Date: 2025-03-05 04:06\n"
|
||||
"Last-Translator: info@erpnext.com\n"
|
||||
"Language-Team: Persian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -18462,7 +18462,7 @@ msgstr "کارمند {0} متعلق به شرکت {1} نیست"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:297
|
||||
msgid "Employee {0} is currently working on another workstation. Please assign another employee."
|
||||
msgstr ""
|
||||
msgstr "کارمند {0} در حال حاضر روی ایستگاه کاری دیگری کار می کند. لطفا کارمند دیگری را تعیین کنید."
|
||||
|
||||
#: erpnext/manufacturing/doctype/workstation/workstation.js:351
|
||||
msgid "Employees"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: info@erpnext.com\n"
|
||||
"POT-Creation-Date: 2025-03-02 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-03-03 04:14\n"
|
||||
"PO-Revision-Date: 2025-03-05 04:06\n"
|
||||
"Last-Translator: info@erpnext.com\n"
|
||||
"Language-Team: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -7571,7 +7571,7 @@ msgstr "critére de restriction"
|
||||
|
||||
#: erpnext/setup/doctype/holiday_list/holiday_list.js:60
|
||||
msgid "Based on your HR Policy, select your leave allocation period's end date"
|
||||
msgstr ""
|
||||
msgstr "En fonction de votre politique RH, sélectionnez la date de fin de la période d'attribution des congés"
|
||||
|
||||
#: erpnext/setup/doctype/holiday_list/holiday_list.js:55
|
||||
msgid "Based on your HR Policy, select your leave allocation period's start date"
|
||||
|
||||
60470
erpnext/locale/hr.po
Normal file
60470
erpnext/locale/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
100445
erpnext/locale/pt.po
100445
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
60470
erpnext/locale/th.po
Normal file
60470
erpnext/locale/th.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1433,7 +1433,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
||||
},
|
||||
)
|
||||
|
||||
def get_max_op_qty():
|
||||
def get_max_operation_quantity():
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Job Card")
|
||||
@@ -1449,7 +1449,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
||||
)
|
||||
return min([d.qty for d in query.run(as_dict=True)], default=0)
|
||||
|
||||
def get_utilised_cc():
|
||||
def get_utilised_corrective_cost():
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
@@ -1479,15 +1479,15 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
||||
)
|
||||
)
|
||||
):
|
||||
max_qty = get_max_op_qty() - work_order.produced_qty
|
||||
remaining_cc = work_order.corrective_operation_cost - get_utilised_cc()
|
||||
max_qty = get_max_operation_quantity() - work_order.produced_qty
|
||||
remaining_corrective_cost = work_order.corrective_operation_cost - get_utilised_corrective_cost()
|
||||
stock_entry.append(
|
||||
"additional_costs",
|
||||
{
|
||||
"expense_account": expense_account,
|
||||
"description": "Corrective Operation Cost",
|
||||
"has_corrective_cost": 1,
|
||||
"amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty),
|
||||
"amount": remaining_corrective_cost / max_qty * flt(stock_entry.fg_completed_qty),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -260,6 +260,7 @@ erpnext.patches.v14_0.show_loan_management_deprecation_warning
|
||||
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
|
||||
erpnext.patches.v14_0.update_proprietorship_to_individual
|
||||
erpnext.patches.v15_0.rename_subcontracting_fields
|
||||
|
||||
[post_model_sync]
|
||||
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
||||
@@ -403,4 +404,6 @@ erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
|
||||
erpnext.patches.v15_0.recalculate_amount_difference_field
|
||||
erpnext.patches.v15_0.rename_sla_fields
|
||||
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
erpnext.patches.v15_0.update_query_report
|
||||
|
||||
13
erpnext/patches/v15_0/rename_sla_fields.py
Normal file
13
erpnext/patches/v15_0/rename_sla_fields.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import rename_fieldname
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
doctypes = frappe.get_all("Service Level Agreement", pluck="document_type")
|
||||
for doctype in doctypes:
|
||||
rename_fieldname(doctype + "-resolution_by", "sla_resolution_by")
|
||||
rename_fieldname(doctype + "-resolution_date", "sla_resolution_date")
|
||||
|
||||
rename_field("Issue", "resolution_by", "sla_resolution_by")
|
||||
rename_field("Issue", "resolution_date", "sla_resolution_date")
|
||||
7
erpnext/patches/v15_0/rename_subcontracting_fields.py
Normal file
7
erpnext/patches/v15_0/rename_subcontracting_fields.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
rename_field("Purchase Order Item", "sco_qty", "subcontracted_quantity")
|
||||
rename_field("Subcontracting Order Item", "sc_conversion_factor", "subcontracting_conversion_factor")
|
||||
25
erpnext/patches/v15_0/update_query_report.py
Normal file
25
erpnext/patches/v15_0/update_query_report.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
reports = [
|
||||
"Accounts Payable",
|
||||
"Accounts Payable Summary",
|
||||
"Purchase Register",
|
||||
"Item-wise Purchase Register",
|
||||
"Purchase Order Analysis",
|
||||
"Received Items To Be Billed",
|
||||
"Supplier Ledger Summary",
|
||||
]
|
||||
frappe.db.set_value(
|
||||
"Workspace Link",
|
||||
{
|
||||
"parent": "Payables",
|
||||
"link_type": "Report",
|
||||
"type": "Link",
|
||||
"link_to": ["in", reports],
|
||||
"is_query_report": 0,
|
||||
},
|
||||
"is_query_report",
|
||||
1,
|
||||
)
|
||||
@@ -1063,7 +1063,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
ignore_permissions=True,
|
||||
)
|
||||
|
||||
dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1))
|
||||
dn_item.qty = flt(sre.reserved_qty) / flt(dn_item.get("conversion_factor", 1))
|
||||
dn_item.warehouse = sre.warehouse
|
||||
|
||||
if sre.reservation_based_on == "Serial and Batch" and (sre.has_serial_no or sre.has_batch_no):
|
||||
|
||||
@@ -16,14 +16,14 @@ frappe.ui.form.on("Subcontracting Order Item", {
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"qty",
|
||||
row.qty * row.sc_conversion_factor
|
||||
row.qty * row.subcontracting_conversion_factor
|
||||
);
|
||||
frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty);
|
||||
frappe.model.set_value(
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"amount",
|
||||
row.qty * row.sc_conversion_factor * service_item.rate
|
||||
row.qty * row.subcontracting_conversion_factor * service_item.rate
|
||||
);
|
||||
},
|
||||
before_items_remove(frm, cdt, cdn) {
|
||||
|
||||
@@ -119,12 +119,12 @@ class SubcontractingOrder(SubcontractingController):
|
||||
def on_submit(self):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_sco_qty_in_po()
|
||||
self.update_subcontracted_quantity_in_po()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_sco_qty_in_po(cancel=True)
|
||||
self.update_subcontracted_quantity_in_po(cancel=True)
|
||||
|
||||
def validate_purchase_order_for_subcontracting(self):
|
||||
if self.purchase_order:
|
||||
@@ -162,7 +162,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
item = next(
|
||||
item for item in self.items if item.purchase_order_item == service_item.purchase_order_item
|
||||
)
|
||||
service_item.qty = item.qty * item.sc_conversion_factor
|
||||
service_item.qty = item.qty * item.subcontracting_conversion_factor
|
||||
service_item.fg_item_qty = item.qty
|
||||
service_item.amount = service_item.qty * service_item.rate
|
||||
|
||||
@@ -250,7 +250,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
|
||||
available_qty = po_item.qty - po_item.sco_qty
|
||||
available_qty = po_item.qty - po_item.subcontracted_quantity
|
||||
|
||||
if available_qty == 0:
|
||||
continue
|
||||
@@ -276,7 +276,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"schedule_date": self.schedule_date,
|
||||
"description": item.description,
|
||||
"qty": si.fg_item_qty,
|
||||
"sc_conversion_factor": conversion_factor,
|
||||
"subcontracting_conversion_factor": conversion_factor,
|
||||
"stock_uom": item.stock_uom,
|
||||
"bom": bom,
|
||||
"purchase_order_item": si.purchase_order_item,
|
||||
@@ -330,10 +330,14 @@ class SubcontractingOrder(SubcontractingController):
|
||||
self.update_ordered_qty_for_subcontracting()
|
||||
self.update_reserved_qty_for_subcontracting()
|
||||
|
||||
def update_sco_qty_in_po(self, cancel=False):
|
||||
def update_subcontracted_quantity_in_po(self, cancel=False):
|
||||
for service_item in self.service_items:
|
||||
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
|
||||
doc.sco_qty = (doc.sco_qty + service_item.qty) if not cancel else (doc.sco_qty - service_item.qty)
|
||||
doc.subcontracted_quantity = (
|
||||
(doc.subcontracted_quantity + service_item.qty)
|
||||
if not cancel
|
||||
else (doc.subcontracted_quantity - service_item.qty)
|
||||
)
|
||||
doc.save()
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"section_break_34",
|
||||
"purchase_order_item",
|
||||
"page_break",
|
||||
"sc_conversion_factor"
|
||||
"subcontracting_conversion_factor"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -403,18 +403,19 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sc_conversion_factor",
|
||||
"fieldname": "subcontracting_conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "SC Conversion Factor",
|
||||
"label": "Subcontracting Conversion Factor",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-13 13:35:28.935898",
|
||||
"modified": "2025-03-02 17:05:28.386492",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Item",
|
||||
@@ -422,6 +423,7 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -42,10 +42,10 @@ class SubcontractingOrderItem(Document):
|
||||
received_qty: DF.Float
|
||||
returned_qty: DF.Float
|
||||
rm_cost_per_qty: DF.Currency
|
||||
sc_conversion_factor: DF.Float
|
||||
schedule_date: DF.Date | None
|
||||
service_cost_per_qty: DF.Currency
|
||||
stock_uom: DF.Link
|
||||
subcontracting_conversion_factor: DF.Float
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"reset_service_level_agreement",
|
||||
"cb",
|
||||
"agreement_status",
|
||||
"resolution_by",
|
||||
"sla_resolution_by",
|
||||
"service_level_agreement_creation",
|
||||
"on_hold_since",
|
||||
"total_hold_time",
|
||||
@@ -41,7 +41,7 @@
|
||||
"column_break1",
|
||||
"opening_date",
|
||||
"opening_time",
|
||||
"resolution_date",
|
||||
"sla_resolution_date",
|
||||
"resolution_time",
|
||||
"user_resolution_time",
|
||||
"additional_info",
|
||||
@@ -176,13 +176,6 @@
|
||||
"options": "fa fa-pushpin",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
|
||||
"fieldname": "resolution_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution By",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "response",
|
||||
@@ -287,16 +280,6 @@
|
||||
"oldfieldtype": "Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "resolution_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "resolution_date",
|
||||
"oldfieldtype": "Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "content_type",
|
||||
"fieldtype": "Data",
|
||||
@@ -386,12 +369,29 @@
|
||||
"fieldtype": "Duration",
|
||||
"label": "First Response Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
|
||||
"fieldname": "sla_resolution_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution By",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "sla_resolution_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "resolution_date",
|
||||
"oldfieldtype": "Date",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 7,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:52.921791",
|
||||
"modified": "2025-02-18 21:18:52.797745",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Issue",
|
||||
|
||||
@@ -48,13 +48,13 @@ class Issue(Document):
|
||||
priority: DF.Link | None
|
||||
project: DF.Link | None
|
||||
raised_by: DF.Data | None
|
||||
resolution_by: DF.Datetime | None
|
||||
resolution_date: DF.Datetime | None
|
||||
resolution_details: DF.TextEditor | None
|
||||
resolution_time: DF.Duration | None
|
||||
response_by: DF.Datetime | None
|
||||
service_level_agreement: DF.Link | None
|
||||
service_level_agreement_creation: DF.Datetime | None
|
||||
sla_resolution_by: DF.Datetime | None
|
||||
sla_resolution_date: DF.Datetime | None
|
||||
status: DF.Literal["Open", "Replied", "On Hold", "Resolved", "Closed"]
|
||||
subject: DF.Data
|
||||
total_hold_time: DF.Duration | None
|
||||
|
||||
@@ -33,48 +33,48 @@ class TestIssue(TestSetUp):
|
||||
issue = make_issue(creation, "_Test Customer", 1)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 15:00"))
|
||||
|
||||
# make issue with customer_group specific SLA
|
||||
create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory")
|
||||
issue = make_issue(creation, "__Test Customer", 2)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 15:00"))
|
||||
|
||||
# make issue with territory specific SLA
|
||||
create_customer("___Test Customer", "__Test SLA Customer Group", "_Test SLA Territory")
|
||||
issue = make_issue(creation, "___Test Customer", 3)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 15:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 15:00"))
|
||||
|
||||
# make issue with default SLA
|
||||
issue = make_issue(creation=creation, index=4)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-04 16:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 18:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 18:00"))
|
||||
|
||||
# make issue with default SLA before working hours
|
||||
creation = get_datetime("2019-03-04 7:00")
|
||||
issue = make_issue(creation=creation, index=5)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-04 14:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-04 16:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-04 16:00"))
|
||||
|
||||
# make issue with default SLA after working hours
|
||||
creation = get_datetime("2019-03-04 20:00")
|
||||
issue = make_issue(creation, index=6)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-06 14:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-06 16:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-06 16:00"))
|
||||
|
||||
# make issue with default SLA next day
|
||||
creation = get_datetime("2019-03-04 14:00")
|
||||
issue = make_issue(creation=creation, index=7)
|
||||
|
||||
self.assertEqual(issue.response_by, get_datetime("2019-03-04 18:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2019-03-06 12:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2019-03-06 12:00"))
|
||||
|
||||
frappe.flags.current_time = get_datetime("2019-03-04 15:00")
|
||||
issue.reload()
|
||||
@@ -98,7 +98,7 @@ class TestIssue(TestSetUp):
|
||||
issue.save()
|
||||
|
||||
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
|
||||
self.assertFalse(issue.resolution_by)
|
||||
self.assertFalse(issue.sla_resolution_by)
|
||||
|
||||
creation = get_datetime("2020-03-04 5:00")
|
||||
frappe.flags.current_time = get_datetime("2020-03-04 5:00")
|
||||
@@ -106,7 +106,7 @@ class TestIssue(TestSetUp):
|
||||
|
||||
issue.reload()
|
||||
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2020-03-04 16:45"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2020-03-04 16:45"))
|
||||
|
||||
creation = get_datetime("2020-03-04 5:05")
|
||||
create_communication(issue.name, "test@admin.com", "Sent", creation)
|
||||
@@ -140,8 +140,8 @@ class TestIssue(TestSetUp):
|
||||
issue.status = "Closed"
|
||||
issue.save()
|
||||
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2021-11-22 06:00:00"))
|
||||
self.assertEqual(issue.resolution_date, get_datetime("2021-11-22 01:00:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-22 06:00:00"))
|
||||
self.assertEqual(issue.sla_resolution_date, get_datetime("2021-11-22 01:00:00"))
|
||||
self.assertEqual(issue.agreement_status, "Fulfilled")
|
||||
|
||||
def test_issue_open_after_closed(self):
|
||||
@@ -153,7 +153,7 @@ class TestIssue(TestSetUp):
|
||||
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
|
||||
self.assertEqual(issue.agreement_status, "First Response Due")
|
||||
self.assertEqual(issue.response_by, get_datetime("2021-11-01 17:00"))
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2021-11-01 19:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-01 19:00"))
|
||||
|
||||
# Replied on → 2 pm
|
||||
frappe.flags.current_time = get_datetime("2021-11-01 14:00")
|
||||
@@ -173,7 +173,7 @@ class TestIssue(TestSetUp):
|
||||
# Hold Time + 1 Hrs
|
||||
self.assertEqual(issue.total_hold_time, 3600)
|
||||
# Resolution By should increase by one hrs
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2021-11-01 20:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-01 20:00"))
|
||||
|
||||
# Replied on → 4 pm, Open → 1 hr, Resolution Due → 8 pm
|
||||
frappe.flags.current_time = get_datetime("2021-11-01 16:00")
|
||||
@@ -190,9 +190,9 @@ class TestIssue(TestSetUp):
|
||||
# Hold Time + 6 Hrs
|
||||
self.assertEqual(issue.total_hold_time, 3600 + 21600)
|
||||
# Resolution By should increase by 6 hrs
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 02:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-02 02:00"))
|
||||
self.assertEqual(issue.agreement_status, "Fulfilled")
|
||||
self.assertEqual(issue.resolution_date, frappe.flags.current_time)
|
||||
self.assertEqual(issue.sla_resolution_date, frappe.flags.current_time)
|
||||
|
||||
# Customer Open → 3 am i.e after resolution by is crossed
|
||||
frappe.flags.current_time = get_datetime("2021-11-02 03:00")
|
||||
@@ -201,17 +201,17 @@ class TestIssue(TestSetUp):
|
||||
# Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm)
|
||||
self.assertEqual(issue.total_hold_time, 3600 + 21600 + 18000)
|
||||
# Resolution By should increase by 5 hrs
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 07:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-02 07:00"))
|
||||
self.assertEqual(issue.agreement_status, "Resolution Due")
|
||||
self.assertFalse(issue.resolution_date)
|
||||
self.assertFalse(issue.sla_resolution_date)
|
||||
|
||||
# We Closed → 4 am, SLA should be Fulfilled
|
||||
frappe.flags.current_time = get_datetime("2021-11-02 04:00")
|
||||
issue.status = "Closed"
|
||||
issue.save()
|
||||
self.assertEqual(issue.resolution_by, get_datetime("2021-11-02 07:00"))
|
||||
self.assertEqual(issue.sla_resolution_by, get_datetime("2021-11-02 07:00"))
|
||||
self.assertEqual(issue.agreement_status, "Fulfilled")
|
||||
self.assertEqual(issue.resolution_date, frappe.flags.current_time)
|
||||
self.assertEqual(issue.sla_resolution_date, frappe.flags.current_time)
|
||||
|
||||
def test_recording_of_assignment_on_first_reponse_failure(self):
|
||||
from frappe.desk.form.assign_to import add as add_assignment
|
||||
|
||||
@@ -514,7 +514,7 @@ def apply(doc, method=None):
|
||||
def remove_sla_if_applied(doc):
|
||||
doc.service_level_agreement = None
|
||||
doc.response_by = None
|
||||
doc.resolution_by = None
|
||||
doc.sla_resolution_by = None
|
||||
|
||||
|
||||
def process_sla(doc, sla):
|
||||
@@ -557,7 +557,7 @@ def handle_status_change(doc, apply_sla_for_resolution):
|
||||
# In case issue was closed and after few days it has been opened
|
||||
# The hold time should be calculated from resolution_date
|
||||
|
||||
on_hold_since = doc.resolution_date or doc.on_hold_since
|
||||
on_hold_since = doc.sla_resolution_date or doc.on_hold_since
|
||||
if on_hold_since:
|
||||
current_hold_hours = time_diff_in_seconds(now_time, on_hold_since)
|
||||
doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours
|
||||
@@ -582,7 +582,7 @@ def handle_status_change(doc, apply_sla_for_resolution):
|
||||
# Open to Closed
|
||||
if is_open_status(prev_status) and is_fulfilled_status(doc.status):
|
||||
# Issue is closed -> Set resolution_date
|
||||
doc.resolution_date = now_time
|
||||
doc.sla_resolution_date = now_time
|
||||
set_resolution_time(doc)
|
||||
|
||||
# Closed to Open
|
||||
@@ -606,7 +606,7 @@ def handle_status_change(doc, apply_sla_for_resolution):
|
||||
calculate_hold_hours()
|
||||
# Issue is closed -> Set resolution_date
|
||||
if apply_sla_for_resolution:
|
||||
doc.resolution_date = now_time
|
||||
doc.sla_resolution_date = now_time
|
||||
set_resolution_time(doc)
|
||||
|
||||
|
||||
@@ -713,7 +713,7 @@ def get_support_days(service_level):
|
||||
def set_resolution_time(doc):
|
||||
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
|
||||
if doc.meta.has_field("resolution_time"):
|
||||
doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time)
|
||||
doc.resolution_time = time_diff_in_seconds(doc.sla_resolution_date, start_date_time)
|
||||
|
||||
# total time taken by a user to close the issue apart from wait_time
|
||||
if not doc.meta.has_field("user_resolution_time"):
|
||||
@@ -737,7 +737,7 @@ def set_resolution_time(doc):
|
||||
pending_time.append(wait_time)
|
||||
|
||||
total_pending_time = sum(pending_time)
|
||||
resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time)
|
||||
resolution_time_in_secs = time_diff_in_seconds(doc.sla_resolution_date, start_date_time)
|
||||
doc.user_resolution_time = resolution_time_in_secs - total_pending_time
|
||||
|
||||
|
||||
@@ -791,8 +791,8 @@ def reset_service_level_agreement(doctype: str, docname: str, reason, user):
|
||||
|
||||
|
||||
def reset_resolution_metrics(doc):
|
||||
if doc.meta.has_field("resolution_date"):
|
||||
doc.resolution_date = None
|
||||
if doc.meta.has_field("sla_resolution_date"):
|
||||
doc.sla_resolution_date = None
|
||||
|
||||
if doc.meta.has_field("resolution_time"):
|
||||
doc.resolution_time = None
|
||||
@@ -859,8 +859,8 @@ def on_communication_update(doc, status):
|
||||
def reset_expected_response_and_resolution(doc):
|
||||
if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"):
|
||||
doc.response_by = None
|
||||
if doc.meta.has_field("resolution_by") and not doc.get("resolution_date"):
|
||||
doc.resolution_by = None
|
||||
if doc.meta.has_field("sla_resolution_by") and not doc.get("sla_resolution_date"):
|
||||
doc.sla_resolution_by = None
|
||||
|
||||
|
||||
def set_response_by(doc, start_date_time, priority):
|
||||
@@ -877,12 +877,14 @@ def set_response_by(doc, start_date_time, priority):
|
||||
|
||||
|
||||
def set_resolution_by(doc, start_date_time, priority):
|
||||
if doc.meta.has_field("resolution_by"):
|
||||
doc.resolution_by = get_expected_time_for(
|
||||
if doc.meta.has_field("sla_resolution_by"):
|
||||
doc.sla_resolution_by = get_expected_time_for(
|
||||
parameter="resolution", service_level=priority, start_date_time=start_date_time
|
||||
)
|
||||
if doc.meta.has_field("total_hold_time") and doc.get("total_hold_time"):
|
||||
doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get("total_hold_time")))
|
||||
doc.sla_resolution_by = add_to_date(
|
||||
doc.sla_resolution_by, seconds=round(doc.get("total_hold_time"))
|
||||
)
|
||||
|
||||
|
||||
def record_assigned_users_on_failure(doc):
|
||||
@@ -941,7 +943,7 @@ def get_service_level_agreement_fields():
|
||||
"read_only": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_by",
|
||||
"fieldname": "sla_resolution_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution By",
|
||||
"read_only": 1,
|
||||
@@ -955,7 +957,7 @@ def get_service_level_agreement_fields():
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "resolution_date",
|
||||
"fieldname": "sla_resolution_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution Date",
|
||||
"no_copy": 1,
|
||||
@@ -975,9 +977,9 @@ def update_agreement_status(doc, apply_sla_for_resolution):
|
||||
if apply_sla_for_resolution:
|
||||
if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"):
|
||||
doc.agreement_status = "First Response Due"
|
||||
elif doc.meta.has_field("resolution_date") and not doc.get("resolution_date"):
|
||||
elif doc.meta.has_field("sla_resolution_date") and not doc.get("sla_resolution_date"):
|
||||
doc.agreement_status = "Resolution Due"
|
||||
elif get_datetime(doc.get("resolution_date")) <= get_datetime(doc.get("resolution_by")):
|
||||
elif get_datetime(doc.get("sla_resolution_date")) <= get_datetime(doc.get("sla_resolution_by")):
|
||||
doc.agreement_status = "Fulfilled"
|
||||
else:
|
||||
doc.agreement_status = "Failed"
|
||||
|
||||
@@ -227,7 +227,7 @@ class TestServiceLevelAgreement(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(lead.service_level_agreement, lead_sla.name)
|
||||
self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0))
|
||||
self.assertEqual(lead.resolution_by, datetime.datetime(2019, 3, 4, 18, 0))
|
||||
self.assertEqual(lead.sla_resolution_by, datetime.datetime(2019, 3, 4, 18, 0))
|
||||
|
||||
frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 0)
|
||||
lead.reload()
|
||||
@@ -268,7 +268,7 @@ class TestServiceLevelAgreement(IntegrationTestCase):
|
||||
|
||||
lead.reload()
|
||||
self.assertEqual(flt(lead.total_hold_time, 2), 3000)
|
||||
self.assertEqual(lead.resolution_by, datetime.datetime(2020, 3, 4, 16, 50))
|
||||
self.assertEqual(lead.sla_resolution_by, datetime.datetime(2020, 3, 4, 16, 50))
|
||||
|
||||
def test_failed_sla_for_response_only(self):
|
||||
doctype = "Lead"
|
||||
|
||||
Reference in New Issue
Block a user