Merge branch 'develop' into version-15-beta

This commit is contained in:
Ankush Menat
2023-07-16 14:00:39 +05:30
39 changed files with 695 additions and 1517 deletions

38
.github/workflows/release_notes.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
# This action needs to be maintained on all branches that do releases.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v13.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}

View File

@@ -271,6 +271,12 @@ def get_dimensions(with_cost_center_and_project=False):
as_dict=1, as_dict=1,
) )
if isinstance(with_cost_center_and_project, str):
if with_cost_center_and_project.lower().strip() == "true":
with_cost_center_and_project = True
else:
with_cost_center_and_project = False
if with_cost_center_and_project: if with_cost_center_and_project:
dimension_filters.extend( dimension_filters.extend(
[ [

View File

@@ -416,6 +416,7 @@ def set_gl_entries_by_account(
filters, filters,
gl_entries_by_account, gl_entries_by_account,
ignore_closing_entries=False, ignore_closing_entries=False,
ignore_opening_entries=False,
): ):
"""Returns a dict like { "account": [gl entries], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
gl_entries = [] gl_entries = []
@@ -426,7 +427,6 @@ def set_gl_entries_by_account(
pluck="name", pluck="name",
) )
ignore_opening_entries = False
if accounts_list: if accounts_list:
# For balance sheet # For balance sheet
if not from_date: if not from_date:

View File

@@ -12,14 +12,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
erpnext.financial_statements); erpnext.financial_statements);
frappe.query_reports["Gross and Net Profit Report"]["filters"].push( frappe.query_reports["Gross and Net Profit Report"]["filters"].push(
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt);
}
},
{ {
"fieldname": "accumulated_values", "fieldname": "accumulated_values",
"label": __("Accumulated Values"), "label": __("Accumulated Values"),

View File

@@ -9,16 +9,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
erpnext.utils.add_dimensions('Profit and Loss Statement', 10); erpnext.utils.add_dimensions('Profit and Loss Statement', 10);
frappe.query_reports["Profit and Loss Statement"]["filters"].push( frappe.query_reports["Profit and Loss Statement"]["filters"].push(
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt, {
company: frappe.query_report.get_filter_value("company")
});
},
},
{ {
"fieldname": "include_default_book_entries", "fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"), "label": __("Include Default Book Entries"),

View File

@@ -117,6 +117,7 @@ def get_data(filters):
filters, filters,
gl_entries_by_account, gl_entries_by_account,
ignore_closing_entries=not flt(filters.with_period_closing_entry), ignore_closing_entries=not flt(filters.with_period_closing_entry),
ignore_opening_entries=True,
) )
calculate_values(accounts, gl_entries_by_account, opening_balances) calculate_values(accounts, gl_entries_by_account, opening_balances)
@@ -159,6 +160,8 @@ def get_rootwise_opening_balances(filters, report_type):
accounting_dimensions, accounting_dimensions,
period_closing_voucher=last_period_closing_voucher[0].name, period_closing_voucher=last_period_closing_voucher[0].name,
) )
# Report getting generate from the mid of a fiscal year
if getdate(last_period_closing_voucher[0].posting_date) < getdate( if getdate(last_period_closing_voucher[0].posting_date) < getdate(
add_days(filters.from_date, -1) add_days(filters.from_date, -1)
): ):
@@ -220,7 +223,10 @@ def get_opening_balance(
if start_date: if start_date:
opening_balance = opening_balance.where(closing_balance.posting_date >= start_date) opening_balance = opening_balance.where(closing_balance.posting_date >= start_date)
opening_balance = opening_balance.where(closing_balance.is_opening == "No") opening_balance = opening_balance.where(closing_balance.is_opening == "No")
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date) else:
opening_balance = opening_balance.where(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
if ( if (
not filters.show_unclosed_fy_pl_balances not filters.show_unclosed_fy_pl_balances

View File

@@ -63,7 +63,7 @@ frappe.ui.form.on('Asset Movement', {
fieldnames_to_be_altered = { fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 }, target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 1, reqd: 0 }, source_location: { read_only: 1, reqd: 0 },
from_employee: { read_only: 0, reqd: 1 }, from_employee: { read_only: 0, reqd: 0 },
to_employee: { read_only: 1, reqd: 0 } to_employee: { read_only: 1, reqd: 0 }
}; };
} }

View File

@@ -62,29 +62,20 @@ class AssetMovement(Document):
frappe.throw(_("Source and Target Location cannot be same")) frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt": if self.purpose == "Receipt":
# only when asset is bought and first entry is made if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee):
if not d.source_location and not (d.target_location or d.to_employee):
frappe.throw( frappe.throw(
_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)
) )
elif d.source_location: elif d.from_employee and not d.target_location:
# when asset is received from an employee frappe.throw(
if d.target_location and not d.from_employee: _("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
frappe.throw( )
_("From employee is required while receiving Asset {0} to a target location").format( elif d.to_employee and d.target_location:
d.asset frappe.throw(
) _(
) "Asset {0} cannot be received at a location and given to an employee in a single movement"
if d.from_employee and not d.target_location: ).format(d.asset)
frappe.throw( )
_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
)
if d.to_employee and d.target_location:
frappe.throw(
_(
"Asset {0} cannot be received at a location and given to employee in a single movement"
).format(d.asset)
)
def validate_employee(self): def validate_employee(self):
for d in self.assets: for d in self.assets:

View File

@@ -61,7 +61,7 @@
"fieldname": "communication_channel", "fieldname": "communication_channel",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Communication Channel", "label": "Communication Channel",
"options": "\nExotel" "options": ""
} }
], ],
"links": [], "links": [],

View File

@@ -1,89 +0,0 @@
{
"actions": [],
"creation": "2019-05-21 07:41:53.536536",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"account_sid",
"api_key",
"api_token",
"section_break_6",
"map_custom_field_to_doctype",
"target_doctype"
],
"fields": [
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"depends_on": "enabled",
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Credentials"
},
{
"fieldname": "account_sid",
"fieldtype": "Data",
"label": "Account SID"
},
{
"fieldname": "api_token",
"fieldtype": "Data",
"label": "API Token"
},
{
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
},
{
"depends_on": "enabled",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Custom Field"
},
{
"default": "0",
"fieldname": "map_custom_field_to_doctype",
"fieldtype": "Check",
"label": "Map Custom Field to DocType"
},
{
"depends_on": "map_custom_field_to_doctype",
"fieldname": "target_doctype",
"fieldtype": "Link",
"label": "Target DocType",
"mandatory_depends_on": "map_custom_field_to_doctype",
"options": "DocType"
}
],
"issingle": 1,
"links": [],
"modified": "2022-12-14 17:24:50.176107",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Exotel Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@@ -1,22 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import requests
from frappe import _
from frappe.model.document import Document
class ExotelSettings(Document):
def validate(self):
self.verify_credentials()
def verify_credentials(self):
if self.enabled:
response = requests.get(
"https://api.exotel.com/v1/Accounts/{sid}".format(sid=self.account_sid),
auth=(self.api_key, self.api_token),
)
if response.status_code != 200:
frappe.throw(_("Invalid credentials"))

View File

@@ -1,151 +0,0 @@
import frappe
import requests
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call
@frappe.whitelist(allow_guest=True)
def handle_incoming_call(**kwargs):
try:
exotel_settings = get_exotel_settings()
if not exotel_settings.enabled:
return
call_payload = kwargs
status = call_payload.get("Status")
if status == "free":
return
call_log = get_call_log(call_payload)
if not call_log:
create_call_log(call_payload)
else:
update_call_log(call_payload, call_log=call_log)
except Exception as e:
frappe.db.rollback()
exotel_settings.log_error("Error in Exotel incoming call")
frappe.db.commit()
@frappe.whitelist(allow_guest=True)
def handle_end_call(**kwargs):
update_call_log(kwargs, "Completed")
@frappe.whitelist(allow_guest=True)
def handle_missed_call(**kwargs):
status = ""
call_type = kwargs.get("CallType")
dial_call_status = kwargs.get("DialCallStatus")
if call_type == "incomplete" and dial_call_status == "no-answer":
status = "No Answer"
elif call_type == "client-hangup" and dial_call_status == "canceled":
status = "Canceled"
elif call_type == "incomplete" and dial_call_status == "failed":
status = "Failed"
update_call_log(kwargs, status)
def update_call_log(call_payload, status="Ringing", call_log=None):
call_log = call_log or get_call_log(call_payload)
# for a new sid, call_log and get_call_log will be empty so create a new log
if not call_log:
call_log = create_call_log(call_payload)
if call_log:
call_log.status = status
call_log.to = call_payload.get("DialWhomNumber")
call_log.duration = call_payload.get("DialCallDuration") or 0
call_log.recording_url = call_payload.get("RecordingUrl")
call_log.save(ignore_permissions=True)
frappe.db.commit()
return call_log
def get_call_log(call_payload):
call_log_id = call_payload.get("CallSid")
if frappe.db.exists("Call Log", call_log_id):
return frappe.get_doc("Call Log", call_log_id)
def map_custom_field(call_payload, call_log):
field_value = call_payload.get("CustomField")
if not field_value:
return call_log
settings = get_exotel_settings()
target_doctype = settings.target_doctype
mapping_enabled = settings.map_custom_field_to_doctype
if not mapping_enabled or not target_doctype:
return call_log
call_log.append("links", {"link_doctype": target_doctype, "link_name": field_value})
return call_log
def create_call_log(call_payload):
call_log = frappe.new_doc("Call Log")
call_log.id = call_payload.get("CallSid")
call_log.to = call_payload.get("DialWhomNumber")
call_log.medium = call_payload.get("To")
call_log.status = "Ringing"
setattr(call_log, "from", call_payload.get("CallFrom"))
map_custom_field(call_payload, call_log)
call_log.save(ignore_permissions=True)
frappe.db.commit()
return call_log
@frappe.whitelist()
def get_call_status(call_id):
endpoint = get_exotel_endpoint("Calls/{call_id}.json".format(call_id=call_id))
response = requests.get(endpoint)
status = response.json().get("Call", {}).get("Status")
return status
@frappe.whitelist()
def make_a_call(from_number, to_number, caller_id, **kwargs):
endpoint = get_exotel_endpoint("Calls/connect.json?details=true")
response = requests.post(
endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id, **kwargs}
)
return response.json()
def get_exotel_settings():
return frappe.get_single("Exotel Settings")
def whitelist_numbers(numbers, caller_id):
endpoint = get_exotel_endpoint("CustomerWhitelist")
response = requests.post(
endpoint,
data={
"VirtualNumber": caller_id,
"Number": numbers,
},
)
return response
def get_all_exophones():
endpoint = get_exotel_endpoint("IncomingPhoneNumbers")
response = requests.post(endpoint)
return response
def get_exotel_endpoint(action):
settings = get_exotel_settings()
return "https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}".format(
api_key=settings.api_key, api_token=settings.api_token, sid=settings.account_sid, action=action
)

View File

@@ -230,17 +230,6 @@
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Exotel Settings",
"link_count": 0,
"link_to": "Exotel Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@@ -252,7 +241,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2023-05-24 14:47:25.984717", "modified": "2023-05-24 14:47:26.984717",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "ERPNext Integrations", "name": "ERPNext Integrations",

View File

@@ -611,3 +611,8 @@ global_search_doctypes = {
additional_timeline_content = { additional_timeline_content = {
"*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"]
} }
extend_bootinfo = [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
]

View File

@@ -621,7 +621,7 @@ class ProductionPlan(Document):
def create_work_order(self, item): def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
if item.get("qty") <= 0: if flt(item.get("qty")) <= 0:
return return
wo = frappe.new_doc("Work Order") wo = frappe.new_doc("Work Order")
@@ -697,10 +697,9 @@ class ProductionPlan(Document):
material_request.flags.ignore_permissions = 1 material_request.flags.ignore_permissions = 1
material_request.run_method("set_missing_values") material_request.run_method("set_missing_values")
material_request.save()
if self.get("submit_material_request"): if self.get("submit_material_request"):
material_request.submit() material_request.submit()
else:
material_request.save()
frappe.flags.mute_messages = False frappe.flags.mute_messages = False

View File

@@ -1026,7 +1026,7 @@ class WorkOrder(Document):
consumed_qty = frappe.db.sql( consumed_qty = frappe.db.sql(
""" """
SELECT SELECT
SUM(qty) SUM(detail.qty)
FROM FROM
`tabStock Entry` entry, `tabStock Entry` entry,
`tabStock Entry Detail` detail `tabStock Entry Detail` detail

View File

@@ -317,7 +317,7 @@ erpnext.patches.v13_0.update_docs_link
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances #17-05-2023 erpnext.patches.v14_0.update_closing_balances #14-07-2023
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last # below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
@@ -334,3 +334,4 @@ erpnext.patches.v14_0.cleanup_workspaces
erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 erpnext.patches.v15_0.remove_loan_management_module #2023-07-03
erpnext.patches.v14_0.set_report_in_process_SOA erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
erpnext.patches.v15_0.remove_exotel_integration

View File

@@ -13,56 +13,63 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(): def execute():
frappe.db.truncate("Account Closing Balance") frappe.db.truncate("Account Closing Balance")
i = 0 for company in frappe.get_all("Company", pluck="name"):
company_wise_order = {} i = 0
for pcv in frappe.db.get_all( company_wise_order = {}
"Period Closing Voucher", for pcv in frappe.db.get_all(
fields=["company", "posting_date", "name"], "Period Closing Voucher",
filters={"docstatus": 1}, fields=["company", "posting_date", "name"],
order_by="posting_date", filters={"docstatus": 1, "company": company},
): order_by="posting_date",
):
company_wise_order.setdefault(pcv.company, []) company_wise_order.setdefault(pcv.company, [])
if pcv.posting_date not in company_wise_order[pcv.company]: if pcv.posting_date not in company_wise_order[pcv.company]:
pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name)
pcv_doc.year_start_date = get_fiscal_year( pcv_doc.year_start_date = get_fiscal_year(
pcv.posting_date, pcv.fiscal_year, company=pcv.company pcv.posting_date, pcv.fiscal_year, company=pcv.company
)[1] )[1]
# get gl entries against pcv # get gl entries against pcv
gl_entries = frappe.db.get_all( gl_entries = frappe.db.get_all(
"GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"]
)
for entry in gl_entries:
entry["is_period_closing_voucher_entry"] = 1
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
# get all gl entries for the year
closing_entries = frappe.db.get_all(
"GL Entry",
filters={
"is_cancelled": 0,
"voucher_no": ["!=", pcv.name],
"posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]],
"is_opening": "No",
},
fields=["*"],
)
if i == 0:
# add opening entries only for the first pcv
closing_entries += frappe.db.get_all(
"GL Entry",
filters={"is_cancelled": 0, "is_opening": "Yes"},
fields=["*"],
) )
for entry in gl_entries:
entry["is_period_closing_voucher_entry"] = 1
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
for entry in closing_entries: closing_entries = []
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) if pcv.posting_date not in company_wise_order[pcv.company]:
company_wise_order[pcv.company].append(pcv.posting_date) # get all gl entries for the year
closing_entries = frappe.db.get_all(
"GL Entry",
filters={
"is_cancelled": 0,
"voucher_no": ["!=", pcv.name],
"posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]],
"is_opening": "No",
"company": company,
},
fields=["*"],
)
i += 1 if i == 0:
# add opening entries only for the first pcv
closing_entries += frappe.db.get_all(
"GL Entry",
filters={"is_cancelled": 0, "is_opening": "Yes", "company": company},
fields=["*"],
)
for entry in closing_entries:
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
entries = gl_entries + closing_entries
if entries:
make_closing_entries(entries, voucher_name=pcv.name)
i += 1
company_wise_order[pcv.company].append(pcv.posting_date)

View File

@@ -0,0 +1,37 @@
from contextlib import suppress
import click
import frappe
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.utils.user import get_system_managers
SETTINGS_DOCTYPE = "Exotel Settings"
def execute():
if "exotel_integration" in frappe.get_installed_apps():
return
with suppress(Exception):
exotel = frappe.get_doc(SETTINGS_DOCTYPE)
if exotel.enabled:
notify_existing_users()
frappe.delete_doc("DocType", SETTINGS_DOCTYPE)
def notify_existing_users():
click.secho(
"Exotel integration is moved to a separate app and will be removed from ERPNext in version-15.\n"
"Please install the app to continue using the integration: https://github.com/frappe/exotel_integration",
fg="yellow",
)
notification = {
"subject": _(
"WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration."
),
"type": "Alert",
}
make_notification_logs(notification, get_system_managers(only_name=True))

View File

@@ -182,6 +182,16 @@ function get_filters() {
company: frappe.query_report.get_filter_value("company") company: frappe.query_report.get_filter_value("company")
}); });
} }
},
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt, {
company: frappe.query_report.get_filter_value("company")
});
},
} }
] ]

View File

@@ -632,7 +632,6 @@ erpnext.utils.update_child_items = function(opts) {
fields.splice(3, 0, { fields.splice(3, 0, {
fieldtype: 'Float', fieldtype: 'Float',
fieldname: "conversion_factor", fieldname: "conversion_factor",
in_list_view: 1,
label: __("Conversion Factor"), label: __("Conversion Factor"),
precision: get_precision('conversion_factor') precision: get_precision('conversion_factor')
}) })
@@ -640,6 +639,7 @@ erpnext.utils.update_child_items = function(opts) {
new frappe.ui.Dialog({ new frappe.ui.Dialog({
title: __("Update Items"), title: __("Update Items"),
size: "extra-large",
fields: [ fields: [
{ {
fieldname: "trans_items", fieldname: "trans_items",
@@ -854,95 +854,87 @@ $(document).on('app_ready', function() {
// Show SLA dashboard // Show SLA dashboard
$(document).on('app_ready', function() { $(document).on('app_ready', function() {
frappe.call({ $.each(frappe.boot.service_level_agreement_doctypes, function(_i, d) {
method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes', frappe.ui.form.on(d, {
callback: function(r) { onload: function(frm) {
if (!r.message) if (!frm.doc.service_level_agreement)
return; return;
$.each(r.message, function(_i, d) { frappe.call({
frappe.ui.form.on(d, { method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters',
onload: function(frm) { args: {
if (!frm.doc.service_level_agreement) doctype: frm.doc.doctype,
return; name: frm.doc.service_level_agreement,
customer: frm.doc.customer
frappe.call({
method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters',
args: {
doctype: frm.doc.doctype,
name: frm.doc.service_level_agreement,
customer: frm.doc.customer
},
callback: function (r) {
if (r && r.message) {
frm.set_query('priority', function() {
return {
filters: {
'name': ['in', r.message.priority],
}
};
});
frm.set_query('service_level_agreement', function() {
return {
filters: {
'name': ['in', r.message.service_level_agreements],
}
};
});
}
}
});
}, },
callback: function (r) {
refresh: function(frm) { if (r && r.message) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement frm.set_query('priority', function() {
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { return {
frappe.call({ filters: {
'method': 'frappe.client.get', 'name': ['in', r.message.priority],
args: {
doctype: 'Service Level Agreement',
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution);
} }
} };
});
frm.set_query('service_level_agreement', function() {
return {
filters: {
'name': ['in', r.message.service_level_agreements],
}
};
}); });
} else if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ?
{'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} :
{'indicator': 'red', 'msg': 'Service Level Agreement Failed'};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} }
}, }
}); });
}); },
}
refresh: function(frm) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
frappe.call({
'method': 'frappe.client.get',
args: {
doctype: 'Service Level Agreement',
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution);
}
}
});
} else if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ?
{'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} :
{'indicator': 'red', 'msg': 'Service Level Agreement Failed'};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
}
},
});
}); });
}); });

View File

@@ -6,13 +6,41 @@ frappe.ui.form.on("Holiday List", {
if (frm.doc.holidays) { if (frm.doc.holidays) {
frm.set_value("total_holidays", frm.doc.holidays.length); frm.set_value("total_holidays", frm.doc.holidays.length);
} }
frm.call("get_supported_countries").then(r => {
frm.subdivisions_by_country = r.message.subdivisions_by_country;
frm.fields_dict.country.set_data(
r.message.countries.sort((a, b) => a.label.localeCompare(b.label))
);
if (frm.doc.country) {
frm.trigger("set_subdivisions");
}
});
}, },
from_date: function(frm) { from_date: function(frm) {
if (frm.doc.from_date && !frm.doc.to_date) { if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1)); frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1));
} }
} },
country: function(frm) {
frm.set_value("subdivision", "");
if (frm.doc.country) {
frm.trigger("set_subdivisions");
}
},
set_subdivisions: function(frm) {
const subdivisions = [...frm.subdivisions_by_country[frm.doc.country]];
if (subdivisions && subdivisions.length > 0) {
frm.fields_dict.subdivision.set_data(subdivisions);
frm.set_df_property("subdivision", "hidden", 0);
} else {
frm.fields_dict.subdivision.set_data([]);
frm.set_df_property("subdivision", "hidden", 1);
}
},
}); });
frappe.tour["Holiday List"] = [ frappe.tour["Holiday List"] = [

View File

@@ -1,480 +1,166 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:holiday_list_name", "autoname": "field:holiday_list_name",
"beta": 0,
"creation": "2013-01-10 16:34:14", "creation": "2013-01-10 16:34:14",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"holiday_list_name",
"from_date",
"to_date",
"column_break_4",
"total_holidays",
"add_weekly_holidays",
"weekly_off",
"get_weekly_off_dates",
"add_local_holidays",
"country",
"subdivision",
"get_local_holidays",
"holidays_section",
"holidays",
"clear_table",
"section_break_9",
"color"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holiday_list_name", "fieldname": "holiday_list_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holiday List Name", "label": "Holiday List Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "holiday_list_name", "oldfieldname": "holiday_list_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date", "fieldname": "from_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date", "label": "From Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date", "fieldname": "to_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date", "label": "To Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_holidays", "fieldname": "total_holidays",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Holidays", "label": "Total Holidays",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "depends_on": "eval: doc.from_date && doc.to_date",
"fieldname": "add_weekly_holidays", "fieldname": "add_weekly_holidays",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Add Weekly Holidays"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Add Weekly Holidays",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "weekly_off", "fieldname": "weekly_off",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Weekly Off", "label": "Weekly Off",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday", "options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "report_hide": 1
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "get_weekly_off_dates", "fieldname": "get_weekly_off_dates",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Add to Holidays", "label": "Add to Holidays",
"length": 0, "options": "get_weekly_off_dates"
"no_copy": 0,
"options": "get_weekly_off_dates",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holidays_section", "fieldname": "holidays_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Holidays"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holidays",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holidays", "fieldname": "holidays",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holidays", "label": "Holidays",
"length": 0,
"no_copy": 0,
"oldfieldname": "holiday_list_details", "oldfieldname": "holiday_list_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Holiday", "options": "Holiday"
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "clear_table", "fieldname": "clear_table",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Clear Table", "label": "Clear Table",
"length": 0, "options": "clear_table"
"no_copy": 0,
"options": "clear_table",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "color", "fieldname": "color",
"fieldtype": "Color", "fieldtype": "Color",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Color", "label": "Color",
"length": 0, "print_hide": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "country",
"print_hide": 1, "fieldtype": "Autocomplete",
"print_hide_if_no_value": 0, "label": "Country"
"read_only": 0, },
"remember_last_selected_value": 0, {
"report_hide": 0, "depends_on": "country",
"reqd": 0, "fieldname": "subdivision",
"search_index": 0, "fieldtype": "Autocomplete",
"set_only_once": 0, "label": "Subdivision"
"translatable": 0, },
"unique": 0 {
"collapsible": 1,
"depends_on": "eval: doc.from_date && doc.to_date",
"fieldname": "add_local_holidays",
"fieldtype": "Section Break",
"label": "Add Local Holidays"
},
{
"fieldname": "get_local_holidays",
"fieldtype": "Button",
"label": "Add to Holidays",
"options": "get_local_holidays"
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2023-07-14 13:28:53.156421",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-03 07:22:46.474096",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Holiday List", "name": "Holiday List",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": []
"track_seen": 0,
"track_views": 0
} }

View File

@@ -3,11 +3,15 @@
import json import json
from datetime import date
import frappe import frappe
from babel import Locale
from frappe import _, throw from frappe import _, throw
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, formatdate, getdate, today from frappe.utils import formatdate, getdate, today
from holidays import country_holidays
from holidays.utils import list_supported_countries
class OverlapError(frappe.ValidationError): class OverlapError(frappe.ValidationError):
@@ -21,25 +25,66 @@ class HolidayList(Document):
@frappe.whitelist() @frappe.whitelist()
def get_weekly_off_dates(self): def get_weekly_off_dates(self):
self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
last_idx = max(
[cint(d.idx) for d in self.get("holidays")]
or [
0,
]
)
for i, d in enumerate(date_list):
ch = self.append("holidays", {})
ch.description = _(self.weekly_off)
ch.holiday_date = d
ch.weekly_off = 1
ch.idx = last_idx + i + 1
def validate_values(self):
if not self.weekly_off: if not self.weekly_off:
throw(_("Please select weekly off day")) throw(_("Please select weekly off day"))
existing_holidays = self.get_holidays()
for d in self.get_weekly_off_date_list(self.from_date, self.to_date):
if d in existing_holidays:
continue
self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1})
self.sort_holidays()
@frappe.whitelist()
def get_supported_countries(self):
subdivisions_by_country = list_supported_countries()
countries = [
{"value": country, "label": local_country_name(country)}
for country in subdivisions_by_country.keys()
]
return {
"countries": countries,
"subdivisions_by_country": subdivisions_by_country,
}
@frappe.whitelist()
def get_local_holidays(self):
if not self.country:
throw(_("Please select a country"))
existing_holidays = self.get_holidays()
from_date = getdate(self.from_date)
to_date = getdate(self.to_date)
for holiday_date, holiday_name in country_holidays(
self.country,
subdiv=self.subdivision,
years=[from_date.year, to_date.year],
language=frappe.local.lang,
).items():
if holiday_date in existing_holidays:
continue
if holiday_date < from_date or holiday_date > to_date:
continue
self.append(
"holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
)
self.sort_holidays()
def sort_holidays(self):
self.holidays.sort(key=lambda x: getdate(x.holiday_date))
for i in range(len(self.holidays)):
self.holidays[i].idx = i + 1
def get_holidays(self) -> list[date]:
return [getdate(holiday.holiday_date) for holiday in self.holidays]
def validate_days(self): def validate_days(self):
if getdate(self.from_date) > getdate(self.to_date): if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date")) throw(_("To Date cannot be before From Date"))
@@ -120,3 +165,8 @@ def is_holiday(holiday_list, date=None):
) )
else: else:
return False return False
def local_country_name(country_code: str) -> str:
"""Return the localized country name for the given country code."""
return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)

View File

@@ -3,7 +3,7 @@
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
from datetime import timedelta from datetime import date, timedelta
import frappe import frappe
from frappe.utils import getdate from frappe.utils import getdate
@@ -23,6 +23,41 @@ class TestHolidayList(unittest.TestCase):
fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name) fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name)
self.assertEqual(holiday_list.name, fetched_holiday_list) self.assertEqual(holiday_list.name, fetched_holiday_list)
def test_weekly_off(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-01-01"
holiday_list.to_date = "2023-02-28"
holiday_list.weekly_off = "Sunday"
holiday_list.get_weekly_off_dates()
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
self.assertNotIn(date(2022, 12, 25), holidays)
self.assertIn(date(2023, 1, 1), holidays)
self.assertIn(date(2023, 1, 8), holidays)
self.assertIn(date(2023, 1, 15), holidays)
self.assertIn(date(2023, 1, 22), holidays)
self.assertIn(date(2023, 1, 29), holidays)
self.assertIn(date(2023, 2, 5), holidays)
self.assertIn(date(2023, 2, 12), holidays)
self.assertIn(date(2023, 2, 19), holidays)
self.assertIn(date(2023, 2, 26), holidays)
self.assertNotIn(date(2023, 3, 5), holidays)
def test_local_holidays(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-04-01"
holiday_list.to_date = "2023-04-30"
holiday_list.country = "DE"
holiday_list.subdivision = "SN"
holiday_list.get_local_holidays()
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
self.assertNotIn(date(2023, 1, 1), holidays)
self.assertIn(date(2023, 4, 7), holidays)
self.assertIn(date(2023, 4, 10), holidays)
self.assertNotIn(date(2023, 5, 1), holidays)
def make_holiday_list( def make_holiday_list(
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None

View File

@@ -194,7 +194,8 @@
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled",
"search_index": 1
}, },
{ {
"default": "0", "default": "0",
@@ -911,7 +912,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-02-14 04:48:26.343620", "modified": "2023-07-14 17:18:18.658942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@@ -773,7 +773,7 @@ class Item(Document):
rows = "" rows = ""
for docname, attr_list in not_included.items(): for docname, attr_list in not_included.items():
link = "<a href='/app/Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname))) link = f"<a href='/app/item/{docname}'>{frappe.bold(docname)}</a>"
rows += table_row(link, body(attr_list)) rows += table_row(link, body(attr_list))
error_description = _( error_description = _(

View File

@@ -1,370 +1,90 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2015-05-19 05:12:30.344797", "creation": "2015-05-19 05:12:30.344797",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"variant_of",
"attribute",
"column_break_2",
"attribute_value",
"numeric_values",
"section_break_4",
"from_range",
"increment",
"column_break_8",
"to_range"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "variant_of", "fieldname": "variant_of",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Variant Of", "label": "Variant Of",
"length": 0,
"no_copy": 0,
"options": "Item", "options": "Item",
"permlevel": 0, "search_index": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attribute", "fieldname": "attribute",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Attribute", "label": "Attribute",
"length": 0,
"no_copy": 0,
"options": "Item Attribute", "options": "Item Attribute",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "attribute_value", "fieldname": "attribute_value",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Attribute Value"
"label": "Attribute Value",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "has_variants", "depends_on": "has_variants",
"fieldname": "numeric_values", "fieldname": "numeric_values",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Numeric Values"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Numeric Values",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "numeric_values", "depends_on": "numeric_values",
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "from_range", "fieldname": "from_range",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "From Range"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "From Range",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "increment", "fieldname": "increment",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "Increment"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Increment",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "to_range", "fieldname": "to_range",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "To Range"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "To Range",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-01-03 15:36:59.129006", "modified": "2023-07-14 17:15:19.112119",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Variant Attribute", "name": "Item Variant Attribute",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": []
"track_seen": 0,
"track_views": 0
} }

View File

@@ -1965,6 +1965,32 @@ class TestPurchaseReceipt(FrappeTestCase):
ste5.reload() ste5.reload()
self.assertEqual(ste5.items[0].valuation_rate, 275.00) self.assertEqual(ste5.items[0].valuation_rate, 275.00)
ste6 = make_stock_entry(
purpose="Material Transfer",
posting_date=add_days(today(), -3),
source=warehouse1,
target=warehouse,
item_code=item_code,
qty=20,
company=pr.company,
)
ste6.reload()
self.assertEqual(ste6.items[0].valuation_rate, 275.00)
ste7 = make_stock_entry(
purpose="Material Transfer",
posting_date=add_days(today(), -3),
source=warehouse,
target=warehouse1,
item_code=item_code,
qty=20,
company=pr.company,
)
ste7.reload()
self.assertEqual(ste7.items[0].valuation_rate, 275.00)
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1)
pr.reload() pr.reload()
@@ -1985,6 +2011,12 @@ class TestPurchaseReceipt(FrappeTestCase):
ste5.reload() ste5.reload()
self.assertEqual(ste5.items[0].valuation_rate, valuation_rate) self.assertEqual(ste5.items[0].valuation_rate, valuation_rate)
ste6.reload()
self.assertEqual(ste6.items[0].valuation_rate, valuation_rate)
ste7.reload()
self.assertEqual(ste7.items[0].valuation_rate, valuation_rate)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -9,13 +9,27 @@ frappe.query_reports["Batch Item Expiry Status"] = {
"fieldtype": "Date", "fieldtype": "Date",
"width": "80", "width": "80",
"default": frappe.sys_defaults.year_start_date, "default": frappe.sys_defaults.year_start_date,
"reqd": 1,
}, },
{ {
"fieldname":"to_date", "fieldname":"to_date",
"label": __("To Date"), "label": __("To Date"),
"fieldtype": "Date", "fieldtype": "Date",
"width": "80", "width": "80",
"default": frappe.datetime.get_today() "default": frappe.datetime.get_today(),
"reqd": 1,
},
{
"fieldname":"item",
"label": __("Item"),
"fieldtype": "Link",
"options": "Item",
"width": "100",
"get_query": function () {
return {
filters: {"has_batch_no": 1}
}
}
} }
] ]
} }

View File

@@ -4,113 +4,86 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import IfNull from frappe.query_builder.functions import Date
from frappe.utils import cint, getdate
def execute(filters=None): def execute(filters=None):
if not filters: validate_filters(filters)
filters = {}
float_precision = cint(frappe.db.get_default("float_precision")) or 3 columns = get_columns()
data = get_data(filters)
columns = get_columns(filters)
item_map = get_item_details(filters)
iwb_map = get_item_warehouse_batch_map(filters, float_precision)
data = []
for item in sorted(iwb_map):
for wh in sorted(iwb_map[item]):
for batch in sorted(iwb_map[item][wh]):
qty_dict = iwb_map[item][wh][batch]
data.append(
[
item,
item_map[item]["item_name"],
item_map[item]["description"],
wh,
batch,
frappe.db.get_value("Batch", batch, "expiry_date"),
qty_dict.expiry_status,
]
)
return columns, data return columns, data
def get_columns(filters): def validate_filters(filters):
"""return columns based on filters""" if not filters:
frappe.throw(_("Please select the required filters"))
columns = (
[_("Item") + ":Link/Item:100"]
+ [_("Item Name") + "::150"]
+ [_("Description") + "::150"]
+ [_("Warehouse") + ":Link/Warehouse:100"]
+ [_("Batch") + ":Link/Batch:100"]
+ [_("Expires On") + ":Date:90"]
+ [_("Expiry (In Days)") + ":Int:120"]
)
return columns
def get_stock_ledger_entries(filters):
if not filters.get("from_date"): if not filters.get("from_date"):
frappe.throw(_("'From Date' is required")) frappe.throw(_("'From Date' is required"))
if not filters.get("to_date"): if not filters.get("to_date"):
frappe.throw(_("'To Date' is required")) frappe.throw(_("'To Date' is required"))
sle = frappe.qb.DocType("Stock Ledger Entry")
query = ( def get_columns():
frappe.qb.from_(sle) return (
.select(sle.item_code, sle.batch_no, sle.warehouse, sle.posting_date, sle.actual_qty) [_("Item") + ":Link/Item:150"]
.where( + [_("Item Name") + "::150"]
(sle.is_cancelled == 0) + [_("Batch") + ":Link/Batch:150"]
& (sle.docstatus < 2) + [_("Stock UOM") + ":Link/UOM:100"]
& (IfNull(sle.batch_no, "") != "") + [_("Quantity") + ":Float:100"]
& (sle.posting_date <= filters["to_date"]) + [_("Expires On") + ":Date:100"]
) + [_("Expiry (In Days)") + ":Int:130"]
.orderby(sle.item_code, sle.warehouse)
) )
return query.run(as_dict=True)
def get_data(filters):
data = []
def get_item_warehouse_batch_map(filters, float_precision): for batch in get_batch_details(filters):
sle = get_stock_ledger_entries(filters) data.append(
iwb_map = {} [
batch.item,
from_date = getdate(filters["from_date"]) batch.item_name,
to_date = getdate(filters["to_date"]) batch.name,
batch.stock_uom,
for d in sle: batch.batch_qty,
iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( batch.expiry_date,
d.batch_no, frappe._dict({"expires_on": None, "expiry_status": None}) max((batch.expiry_date - frappe.utils.datetime.date.today()).days, 0)
if batch.expiry_date
else None,
]
) )
qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] return data
expiry_date_unicode = frappe.db.get_value("Batch", d.batch_no, "expiry_date")
qty_dict.expires_on = expiry_date_unicode
exp_date = frappe.utils.data.getdate(expiry_date_unicode)
qty_dict.expires_on = exp_date
expires_in_days = (exp_date - frappe.utils.datetime.date.today()).days
if expires_in_days > 0:
qty_dict.expiry_status = expires_in_days
else:
qty_dict.expiry_status = 0
return iwb_map
def get_item_details(filters): def get_batch_details(filters):
item_map = {} batch = frappe.qb.DocType("Batch")
for d in (frappe.qb.from_("Item").select("name", "item_name", "description")).run(as_dict=True): query = (
item_map.setdefault(d.name, d) frappe.qb.from_(batch)
.select(
batch.name,
batch.creation,
batch.expiry_date,
batch.item,
batch.item_name,
batch.stock_uom,
batch.batch_qty,
)
.where(
(batch.disabled == 0)
& (batch.batch_qty > 0)
& (
(Date(batch.creation) >= filters["from_date"]) & (Date(batch.creation) <= filters["to_date"])
)
)
.orderby(batch.creation)
)
return item_map if filters.get("item"):
query = query.where(batch.item == filters["item"])
return query.run(as_dict=True)

View File

@@ -645,7 +645,7 @@ class update_entries_after(object):
def update_distinct_item_warehouses(self, dependant_sle): def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse) key = (dependant_sle.item_code, dependant_sle.warehouse)
val = frappe._dict({"sle": dependant_sle}) val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
if key not in self.distinct_item_warehouses: if key not in self.distinct_item_warehouses:
self.distinct_item_warehouses[key] = val self.distinct_item_warehouses[key] = val
@@ -654,13 +654,26 @@ class update_entries_after(object):
existing_sle_posting_date = ( existing_sle_posting_date = (
self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
) )
dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
val.sle_changed = True val.sle_changed = True
self.distinct_item_warehouses[key] = val self.distinct_item_warehouses[key] = val
self.new_items_found = True self.new_items_found = True
elif self.distinct_item_warehouses[key].get("reposting_status"): elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
self.distinct_item_warehouses[key] = val # Future dependent voucher needs to be repost to get the correct stock value
# If dependent voucher has not reposted, then add it to the list
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
self.new_items_found = True self.new_items_found = True
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
self.distinct_item_warehouses[key] = val
def get_dependent_voucher_detail_nos(self, key):
if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
def process_sle(self, sle): def process_sle(self, sle):
# previous sle data for this warehouse # previous sle data for this warehouse
@@ -1370,6 +1383,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
"qty_after_transaction", "qty_after_transaction",
"posting_date", "posting_date",
"posting_time", "posting_time",
"voucher_detail_no",
"timestamp(posting_date, posting_time) as timestamp", "timestamp(posting_date, posting_time) as timestamp",
], ],
as_dict=1, as_dict=1,

View File

@@ -21,6 +21,7 @@ from frappe.utils import (
time_diff_in_seconds, time_diff_in_seconds,
to_timedelta, to_timedelta,
) )
from frappe.utils.caching import redis_cache
from frappe.utils.nestedset import get_ancestors_of from frappe.utils.nestedset import get_ancestors_of
from frappe.utils.safe_exec import get_safe_globals from frappe.utils.safe_exec import get_safe_globals
@@ -209,6 +210,10 @@ class ServiceLevelAgreement(Document):
def on_update(self): def on_update(self):
set_documents_with_active_service_level_agreement() set_documents_with_active_service_level_agreement()
def clear_cache(self):
get_sla_doctypes.clear_cache()
return super().clear_cache()
def create_docfields(self, meta, service_level_agreement_fields): def create_docfields(self, meta, service_level_agreement_fields):
last_index = len(meta.fields) last_index = len(meta.fields)
@@ -990,6 +995,7 @@ def get_user_time(user, to_string=False):
@frappe.whitelist() @frappe.whitelist()
@redis_cache()
def get_sla_doctypes(): def get_sla_doctypes():
doctypes = [] doctypes = []
data = frappe.get_all("Service Level Agreement", {"enabled": 1}, ["document_type"], distinct=1) data = frappe.get_all("Service Level Agreement", {"enabled": 1}, ["document_type"], distinct=1)
@@ -998,3 +1004,7 @@ def get_sla_doctypes():
doctypes.append(entry.document_type) doctypes.append(entry.document_type)
return doctypes return doctypes
def add_sla_doctypes(bootinfo):
bootinfo.service_level_agreement_doctypes = get_sla_doctypes()

View File

@@ -24,12 +24,10 @@ class CallLog(Document):
lead_number = self.get("from") if self.is_incoming_call() else self.get("to") lead_number = self.get("from") if self.is_incoming_call() else self.get("to")
lead_number = strip_number(lead_number) lead_number = strip_number(lead_number)
contact = get_contact_with_phone_number(strip_number(lead_number)) if contact := get_contact_with_phone_number(strip_number(lead_number)):
if contact:
self.add_link(link_type="Contact", link_name=contact) self.add_link(link_type="Contact", link_name=contact)
lead = get_lead_with_phone_number(lead_number) if lead := get_lead_with_phone_number(lead_number):
if lead:
self.add_link(link_type="Lead", link_name=lead) self.add_link(link_type="Lead", link_name=lead)
# Add Employee Name # Add Employee Name
@@ -70,28 +68,30 @@ class CallLog(Document):
self.append("links", {"link_doctype": link_type, "link_name": link_name}) self.append("links", {"link_doctype": link_type, "link_name": link_name})
def trigger_call_popup(self): def trigger_call_popup(self):
if self.is_incoming_call(): if not self.is_incoming_call():
scheduled_employees = get_scheduled_employees_for_popup(self.medium) return
employees = get_employees_with_number(self.to)
employee_emails = [employee.get("user_id") for employee in employees]
# check if employees with matched number are scheduled to receive popup scheduled_employees = get_scheduled_employees_for_popup(self.medium)
emails = set(scheduled_employees).intersection(employee_emails) employees = get_employees_with_number(self.to)
employee_emails = [employee.get("user_id") for employee in employees]
if frappe.conf.developer_mode: # check if employees with matched number are scheduled to receive popup
self.add_comment( emails = set(scheduled_employees).intersection(employee_emails)
text=f"""
if frappe.conf.developer_mode:
self.add_comment(
text=f"""
Scheduled Employees: {scheduled_employees} Scheduled Employees: {scheduled_employees}
Matching Employee: {employee_emails} Matching Employee: {employee_emails}
Show Popup To: {emails} Show Popup To: {emails}
""" """
) )
if employee_emails and not emails: if employee_emails and not emails:
self.add_comment(text=_("No employee was scheduled for call popup")) self.add_comment(text=_("No employee was scheduled for call popup"))
for email in emails: for email in emails:
frappe.publish_realtime("show_call_popup", self, user=email) frappe.publish_realtime("show_call_popup", self, user=email)
def update_received_by(self): def update_received_by(self):
if employees := get_employees_with_number(self.get("to")): if employees := get_employees_with_number(self.get("to")):
@@ -154,8 +154,8 @@ def link_existing_conversations(doc, state):
ELSE 0 ELSE 0
END END
)=0 )=0
""", """,
dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype), dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype),
) )
for log in logs: for log in logs:
@@ -175,7 +175,7 @@ def get_linked_call_logs(doctype, docname):
filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname}, filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname},
) )
logs = set([log.parent for log in logs]) logs = {log.parent for log in logs}
logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]}) logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]})

View File

@@ -1,122 +0,0 @@
import frappe
call_initiation_data = frappe._dict(
{
"CallSid": "23c162077629863c1a2d7f29263a162m",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"Created": "Wed, 23 Feb 2022 12:31:59",
"From": "09999999991",
"To": "09999999988",
"CurrentTime": "2022-02-23 12:32:02",
"DialWhomNumber": "09999999999",
"Status": "busy",
"EventType": "Dial",
"AgentEmail": "test_employee_exotel@company.com",
}
)
call_end_data = frappe._dict(
{
"CallSid": "23c162077629863c1a2d7f29263a162m",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"ForwardedFrom": "null",
"Created": "Wed, 23 Feb 2022 12:31:59",
"DialCallDuration": "17",
"RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3",
"StartTime": "2022-02-23 12:31:58",
"EndTime": "1970-01-01 05:30:00",
"DialCallStatus": "completed",
"CallType": "completed",
"DialWhomNumber": "09999999999",
"ProcessStatus": "null",
"flow_id": "228040",
"tenant_id": "67291",
"From": "09999999991",
"To": "09999999988",
"RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25",
"CurrentTime": "2022-02-23 12:32:25",
"OutgoingPhoneNumber": "09999999988",
"Legs": [
{
"Number": "09999999999",
"Type": "single",
"OnCallDuration": "10",
"CallerId": "09999999980",
"CauseCode": "NORMAL_CLEARING",
"Cause": "16",
}
],
}
)
call_disconnected_data = frappe._dict(
{
"CallSid": "d96421addce69e24bdc7ce5880d1162l",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"ForwardedFrom": "null",
"Created": "Mon, 21 Feb 2022 15:58:12",
"DialCallDuration": "0",
"StartTime": "2022-02-21 15:58:12",
"EndTime": "1970-01-01 05:30:00",
"DialCallStatus": "canceled",
"CallType": "client-hangup",
"DialWhomNumber": "09999999999",
"ProcessStatus": "null",
"flow_id": "228040",
"tenant_id": "67291",
"From": "09999999991",
"To": "09999999988",
"CurrentTime": "2022-02-21 15:58:47",
"OutgoingPhoneNumber": "09999999988",
"Legs": [
{
"Number": "09999999999",
"Type": "single",
"OnCallDuration": "0",
"CallerId": "09999999980",
"CauseCode": "RING_TIMEOUT",
"Cause": "1003",
}
],
}
)
call_not_answered_data = frappe._dict(
{
"CallSid": "fdb67a2b4b2d057b610a52ef43f81622",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"ForwardedFrom": "null",
"Created": "Mon, 21 Feb 2022 15:47:02",
"DialCallDuration": "0",
"StartTime": "2022-02-21 15:47:02",
"EndTime": "1970-01-01 05:30:00",
"DialCallStatus": "no-answer",
"CallType": "incomplete",
"DialWhomNumber": "09999999999",
"ProcessStatus": "null",
"flow_id": "228040",
"tenant_id": "67291",
"From": "09999999991",
"To": "09999999988",
"CurrentTime": "2022-02-21 15:47:40",
"OutgoingPhoneNumber": "09999999988",
"Legs": [
{
"Number": "09999999999",
"Type": "single",
"OnCallDuration": "0",
"CallerId": "09999999980",
"CauseCode": "RING_TIMEOUT",
"Cause": "1003",
}
],
}
)

View File

@@ -1,68 +0,0 @@
import frappe
from frappe.contacts.doctype.contact.test_contact import create_contact
from frappe.tests.test_api import FrappeAPITestCase
from erpnext.setup.doctype.employee.test_employee import make_employee
class TestExotel(FrappeAPITestCase):
@classmethod
def setUpClass(cls):
cls.CURRENT_DB_CONNECTION = frappe.db
cls.test_employee_name = make_employee(
user="test_employee_exotel@company.com", cell_number="9999999999"
)
frappe.db.set_single_value("Exotel Settings", "enabled", 1)
phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}]
create_contact(name="Test Contact", salutation="Mr", phones=phones)
frappe.db.commit()
def test_for_successful_call(self):
from .exotel_test_data import call_end_data, call_initiation_data
api_method = "handle_incoming_call"
end_call_api_method = "handle_end_call"
self.emulate_api_call_from_exotel(api_method, call_initiation_data)
self.emulate_api_call_from_exotel(end_call_api_method, call_end_data)
call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid)
self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom)
self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber)
self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
self.assertEqual(call_log.get("status"), "Completed")
def test_for_disconnected_call(self):
from .exotel_test_data import call_disconnected_data
api_method = "handle_missed_call"
self.emulate_api_call_from_exotel(api_method, call_disconnected_data)
call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid)
self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom)
self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber)
self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
self.assertEqual(call_log.get("status"), "Canceled")
def test_for_call_not_answered(self):
from .exotel_test_data import call_not_answered_data
api_method = "handle_missed_call"
self.emulate_api_call_from_exotel(api_method, call_not_answered_data)
call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid)
self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom)
self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber)
self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
self.assertEqual(call_log.get("status"), "No Answer")
def emulate_api_call_from_exotel(self, api_method, data):
self.post(
f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}",
data=frappe.as_json(data),
content_type="application/json",
)
# restart db connection to get latest data
frappe.connect()
@classmethod
def tearDownClass(cls):
frappe.db = cls.CURRENT_DB_CONNECTION

View File

@@ -1219,7 +1219,7 @@ High Sensitivity,Hohe Empfindlichkeit,
Hold,Anhalten, Hold,Anhalten,
Hold Invoice,Rechnung zurückhalten, Hold Invoice,Rechnung zurückhalten,
Holiday,Urlaub, Holiday,Urlaub,
Holiday List,Urlaubsübersicht, Holiday List,Feiertagsliste,
Hotel Rooms of type {0} are unavailable on {1},Hotelzimmer vom Typ {0} sind auf {1} nicht verfügbar, Hotel Rooms of type {0} are unavailable on {1},Hotelzimmer vom Typ {0} sind auf {1} nicht verfügbar,
Hotels,Hotels, Hotels,Hotels,
Hourly,Stündlich, Hourly,Stündlich,
@@ -3317,7 +3317,7 @@ Workflow,Workflow,
Working,In Bearbeitung, Working,In Bearbeitung,
Working Hours,Arbeitszeit, Working Hours,Arbeitszeit,
Workstation,Arbeitsplatz, Workstation,Arbeitsplatz,
Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Urlaubsliste geschlossen: {0}, Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Feiertagsliste geschlossen: {0},
Wrapping up,Aufwickeln, Wrapping up,Aufwickeln,
Wrong Password,Falsches Passwort, Wrong Password,Falsches Passwort,
Year start date or end date is overlapping with {0}. To avoid please set company,"Jahresbeginn oder Enddatum überlappt mit {0}. Bitte ein Unternehmen wählen, um dies zu verhindern", Year start date or end date is overlapping with {0}. To avoid please set company,"Jahresbeginn oder Enddatum überlappt mit {0}. Bitte ein Unternehmen wählen, um dies zu verhindern",
@@ -3583,6 +3583,7 @@ Accounting Period overlaps with {0},Abrechnungszeitraum überschneidet sich mit
Activity,Aktivität, Activity,Aktivität,
Add / Manage Email Accounts.,Hinzufügen/Verwalten von E-Mail-Konten, Add / Manage Email Accounts.,Hinzufügen/Verwalten von E-Mail-Konten,
Add Child,Unterpunkt hinzufügen, Add Child,Unterpunkt hinzufügen,
Add Local Holidays,Lokale Feiertage hinzufügen,
Add Multiple,Mehrere hinzufügen, Add Multiple,Mehrere hinzufügen,
Add Participants,Teilnehmer hinzufügen, Add Participants,Teilnehmer hinzufügen,
Add to Featured Item,Zum empfohlenen Artikel hinzufügen, Add to Featured Item,Zum empfohlenen Artikel hinzufügen,
@@ -4046,6 +4047,7 @@ Stock Ledger ID,Bestandsbuch-ID,
Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,Der Bestandswert ({0}) und der Kontostand ({1}) sind für das Konto {2} und die verknüpften Lager nicht synchron., Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,Der Bestandswert ({0}) und der Kontostand ({1}) sind für das Konto {2} und die verknüpften Lager nicht synchron.,
Stores - {0},Stores - {0}, Stores - {0},Stores - {0},
Student with email {0} does not exist,Der Student mit der E-Mail-Adresse {0} existiert nicht, Student with email {0} does not exist,Der Student mit der E-Mail-Adresse {0} existiert nicht,
Subdivision,Teilgebiet,
Submit Review,Bewertung abschicken, Submit Review,Bewertung abschicken,
Submitted,Gebucht, Submitted,Gebucht,
Supplier Addresses And Contacts,Lieferanten-Adressen und Kontaktdaten, Supplier Addresses And Contacts,Lieferanten-Adressen und Kontaktdaten,
@@ -4192,6 +4194,7 @@ Mode Of Payment,Zahlungsart,
No students Found,Keine Schüler gefunden, No students Found,Keine Schüler gefunden,
Not in Stock,Nicht lagernd, Not in Stock,Nicht lagernd,
Please select a Customer,Bitte wählen Sie einen Kunden aus, Please select a Customer,Bitte wählen Sie einen Kunden aus,
Please select a country,Bitte wählen Sie ein Land aus,
Printed On,Gedruckt auf, Printed On,Gedruckt auf,
Received From,Erhalten von, Received From,Erhalten von,
Sales Person,Verkäufer, Sales Person,Verkäufer,
@@ -6497,7 +6500,7 @@ Reports to,Vorgesetzter,
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails, Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
Leave Policy,Urlaubsrichtlinie, Leave Policy,Urlaubsrichtlinie,
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID), Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
Applicable Holiday List,Geltende Urlaubsliste, Applicable Holiday List,Geltende Feiertagsliste,
Default Shift,Standardverschiebung, Default Shift,Standardverschiebung,
Salary Details,Gehaltsdetails, Salary Details,Gehaltsdetails,
Salary Mode,Gehaltsmodus, Salary Mode,Gehaltsmodus,
@@ -6662,12 +6665,12 @@ Unclaimed amount,Nicht beanspruchter Betrag,
Expense Claim Detail,Auslage, Expense Claim Detail,Auslage,
Expense Date,Datum der Auslage, Expense Date,Datum der Auslage,
Expense Claim Type,Art der Auslagenabrechnung, Expense Claim Type,Art der Auslagenabrechnung,
Holiday List Name,Urlaubslistenname, Holiday List Name,Name der Feiertagsliste,
Total Holidays,Insgesamt Feiertage, Total Holidays,Insgesamt freie Tage,
Add Weekly Holidays,Wöchentliche Feiertage hinzufügen, Add Weekly Holidays,Wöchentlich freie Tage hinzufügen,
Weekly Off,Wöchentlich frei, Weekly Off,Wöchentlich frei,
Add to Holidays,Zu Feiertagen hinzufügen, Add to Holidays,Zu freien Tagen hinzufügen,
Holidays,Ferien, Holidays,Arbeitsfreie Tage,
Clear Table,Tabelle leeren, Clear Table,Tabelle leeren,
HR Settings,Einstellungen zum Modul Personalwesen, HR Settings,Einstellungen zum Modul Personalwesen,
Employee Settings,Mitarbeitereinstellungen, Employee Settings,Mitarbeitereinstellungen,
@@ -6777,7 +6780,7 @@ Transaction Name,Transaktionsname,
Is Carry Forward,Ist Übertrag, Is Carry Forward,Ist Übertrag,
Is Expired,Ist abgelaufen, Is Expired,Ist abgelaufen,
Is Leave Without Pay,Ist unbezahlter Urlaub, Is Leave Without Pay,Ist unbezahlter Urlaub,
Holiday List for Optional Leave,Urlaubsliste für optionalen Urlaub, Holiday List for Optional Leave,Feiertagsliste für optionalen Urlaub,
Leave Allocations,Zuteilungen verlassen, Leave Allocations,Zuteilungen verlassen,
Leave Policy Details,Urlaubsrichtliniendetails, Leave Policy Details,Urlaubsrichtliniendetails,
Leave Policy Detail,Urlaubsrichtliniendetail, Leave Policy Detail,Urlaubsrichtliniendetail,
@@ -7646,7 +7649,7 @@ Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Org
Change Abbreviation,Abkürzung ändern, Change Abbreviation,Abkürzung ändern,
Parent Company,Muttergesellschaft, Parent Company,Muttergesellschaft,
Default Values,Standardwerte, Default Values,Standardwerte,
Default Holiday List,Standard-Urlaubsliste, Default Holiday List,Standard Feiertagsliste,
Default Selling Terms,Standardverkaufsbedingungen, Default Selling Terms,Standardverkaufsbedingungen,
Default Buying Terms,Standard-Einkaufsbedingungen, Default Buying Terms,Standard-Einkaufsbedingungen,
Create Chart Of Accounts Based On,"Kontenplan erstellen, basierend auf", Create Chart Of Accounts Based On,"Kontenplan erstellen, basierend auf",
Can't render this file because it is too large.

View File

@@ -13,6 +13,7 @@ dependencies = [
"Unidecode~=1.3.6", "Unidecode~=1.3.6",
"barcodenumber~=0.5.0", "barcodenumber~=0.5.0",
"rapidfuzz~=2.15.0", "rapidfuzz~=2.15.0",
"holidays~=0.28",
# integration dependencies # integration dependencies
"gocardless-pro~=1.22.0", "gocardless-pro~=1.22.0",