app_name = "mobile_apis"
app_title = "Mobile APIs"
app_publisher = "Wahni IT Solutions Pvt Ltd"
app_description = "APIs for Frappe/ERPNext based applications"
app_email = "info@wahni.com"
app_license = "MIT"
# required_apps = []

# Includes in <head>
# ------------------

# include js, css files in header of desk.html
# app_include_css = "/assets/mobile_apis/css/mobile_apis.css"
# app_include_js = "/assets/mobile_apis/js/mobile_apis.js"

# include js, css files in header of web template
# web_include_css = "/assets/mobile_apis/css/mobile_apis.css"
# web_include_js = "/assets/mobile_apis/js/mobile_apis.js"

# include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "mobile_apis/public/scss/website"

# include js, css files in header of web form
# webform_include_js = {"doctype": "public/js/doctype.js"}
# webform_include_css = {"doctype": "public/css/doctype.css"}

# include js in page
# page_js = {"page" : "public/js/file.js"}

# include js in doctype views
# doctype_js = {"doctype" : "public/js/doctype.js"}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}

# Home Pages
# ----------

# application home page (will override Website Settings)
# home_page = "login"

# website user home page (by Role)
# role_home_page = {
#	"Role": "home_page"
# }

# Generators
# ----------

# automatically create page for each record of this doctype
# website_generators = ["Web Page"]

# Jinja
# ----------

# add methods and filters to jinja environment
# jinja = {
#	"methods": "mobile_apis.utils.jinja_methods",
#	"filters": "mobile_apis.utils.jinja_filters"
# }

# Installation
# ------------

# before_install = "mobile_apis.install.before_install"
# after_install = "mobile_apis.install.after_install"

# Uninstallation
# ------------

# before_uninstall = "mobile_apis.uninstall.before_uninstall"
# after_uninstall = "mobile_apis.uninstall.after_uninstall"

# Integration Setup
# ------------------
# To set up dependencies/integrations with other apps
# Name of the app being installed is passed as an argument

# before_app_install = "mobile_apis.utils.before_app_install"
# after_app_install = "mobile_apis.utils.after_app_install"

# Integration Cleanup
# -------------------
# To clean up dependencies/integrations with other apps
# Name of the app being uninstalled is passed as an argument

# before_app_uninstall = "mobile_apis.utils.before_app_uninstall"
# after_app_uninstall = "mobile_apis.utils.after_app_uninstall"

# Desk Notifications
# ------------------
# See frappe.core.notifications.get_notification_config

# notification_config = "mobile_apis.notifications.get_notification_config"

# Permissions
# -----------
# Permissions evaluated in scripted ways

# permission_query_conditions = {
#	"Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
# }
#
# has_permission = {
#	"Event": "frappe.desk.doctype.event.event.has_permission",
# }

# DocType Class
# ---------------
# Override standard doctype classes

# override_doctype_class = {
#	"ToDo": "custom_app.overrides.CustomToDo"
# }

# Document Events
# ---------------
# Hook on document methods and events

# doc_events = {
#	"*": {
#		"on_update": "method",
#		"on_cancel": "method",
#		"on_trash": "method"
#	}
# }

# Scheduled Tasks
# ---------------

# scheduler_events = {
#	"all": [
#		"mobile_apis.tasks.all"
#	],
#	"daily": [
#		"mobile_apis.tasks.daily"
#	],
#	"hourly": [
#		"mobile_apis.tasks.hourly"
#	],
#	"weekly": [
#		"mobile_apis.tasks.weekly"
#	],
#	"monthly": [
#		"mobile_apis.tasks.monthly"
#	],
# }

# Testing
# -------

# before_tests = "mobile_apis.install.before_tests"

# Overriding Methods
# ------------------------------
#
# override_whitelisted_methods = {
#	"frappe.desk.doctype.event.event.get_events": "mobile_apis.event.get_events"
# }
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps
# override_doctype_dashboards = {
#	"Task": "mobile_apis.task.get_dashboard_data"
# }

# exempt linked doctypes from being automatically cancelled
#
# auto_cancel_exempted_doctypes = ["Auto Repeat"]

# Ignore links to specified DocTypes when deleting documents
# -----------------------------------------------------------

# ignore_links_on_delete = ["Communication", "ToDo"]

# Request Events
# ----------------
# before_request = ["mobile_apis.utils.before_request"]
# after_request = ["mobile_apis.utils.after_request"]

# Job Events
# ----------
# before_job = ["mobile_apis.utils.before_job"]
# after_job = ["mobile_apis.utils.after_job"]

# User Data Protection
# --------------------

# user_data_fields = [
#	{
#		"doctype": "{doctype_1}",
#		"filter_by": "{filter_by}",
#		"redact_fields": ["{field_1}", "{field_2}"],
#		"partial": 1,
#	},
#	{
#		"doctype": "{doctype_2}",
#		"filter_by": "{filter_by}",
#		"partial": 1,
#	},
#	{
#		"doctype": "{doctype_3}",
#		"strict": False,
#	},
#	{
#		"doctype": "{doctype_4}"
#	}
# ]

# Authentication and authorization
# --------------------------------

# auth_hooks = [
#	"mobile_apis.auth.validate"
# ]

mobile_apis/hooks.py
wahni-green/mobile-apis
# Copyright (c) 2022, Wahni IT Solutions and contributors
# For license information, please see license.txt

import json
import frappe
from frappe import _
from frappe.utils import today


# FETCH ATTENDANCE
@frappe.whitelist(allowed_methods=["GET"])
def get_attendance_status(employee=None, date=None):
	try:
		if not employee:
			employee = frappe.get_value("Employee", {"user_id": frappe.session.user}, 'name')

		if not employee:
			return {"status": True, "attendance": None, "late_entry": 0}
		
		if not date:
			date = today()

		attendance = frappe.get_value("Attendance", {
			"employee": employee,
			"docstatus": 1,
			"attendance_date": date
		}, ["status", "late_entry"], as_dict=1)

		if not attendance:
			return {"status": True, "attendance": None, "late_entry": 0}
		
		return {"status": True, "attendance": attendance.status, "late_entry": attendance.late_entry}
	except Exception as e:
		frappe.log_error(str(frappe.get_traceback()), "Attendance status error")
		return {"success": False, "error": str(e)}


# MARK ATTENDANCE
@frappe.whitelist(allowed_methods=["POST"])
def mark_attendance():
	try:
		employee = json.loads(frappe.request.data).get("employee")
		if not employee:
			employee = frappe.get_value("Employee", {"user_id": frappe.session.user})
		
		if not employee:
			return {"success": False, "message": "Employee not found."}

		if frappe.get_value("Attendance", {
			"employee": employee,
			"docstatus": 1,
			"attendance_date": today()
		}):
			return {"success": False, "message": "Attendance already marked."}

		att = frappe.new_doc("Attendance")
		att.employee = employee
		att.attendance_date = today()
		att.insert(ignore_permissions=True, ignore_mandatory=True)
		att.submit()
		return {"success": True, "message": "Attendance Marked."}
	except Exception as e:
		frappe.log_error(str(frappe.get_traceback()), "Mark Attendance error")
		return {"success": False, "message": "Could not mark attendance"}

mobile_apis/v1/attendance.py
wahni-green/mobile-apis
# Copyright (c) 2023, Wahni IT Solutions and contributors
# For license information, please see license.txt

import base64
import json

import frappe
from frappe.utils.password import get_decrypted_password
from frappe.utils.password import update_password
from frappe.utils.password_strength import test_password_strength
from frappe.core.doctype.user.user import reset_password


@frappe.whitelist(allow_guest=True, allowed_methods=["POST"])
def authenticate():
	try:
		data = json.loads(frappe.request.data)
		username = data.get("username")
		password = data.get("password")

		try:
			login_manager = frappe.auth.LoginManager()
			login_manager.authenticate(user=username, pwd=password)
		except Exception:
			frappe.local.response["status_code"] = 400
			return {"success": False, "message": "Invalid username or password"}

		login_manager.post_login()

		return {
			"success": True,
			"message": "Logged in succesfully",
			"token": generate_keys(username),
		}
	except Exception as e:
		frappe.log_error(str(e), "Auth Error")
		return {"success": False, "error": str(e)}


def generate_keys(user):
	api_secret = get_decrypted_password("User", user, "api_secret", raise_exception=False)
	if not api_secret:
		user_details = frappe.get_doc("User", user)
		api_secret = frappe.generate_hash(length=15)
		# if api key is not set generate api key
		if not user_details.api_key:
			api_key = frappe.generate_hash(length=15)
			user_details.api_key = api_key
		user_details.api_secret = api_secret
		user_details.save()
	else:
		api_key = frappe.db.get_value("User", user, "api_key")

	return base64.b64encode(('{}:{}'.format(api_key, api_secret)).encode('utf-8')).decode('utf-8')


@frappe.whitelist(allowed_methods=["POST"])
def change_password():
	try:
		data = json.loads(frappe.request.data)			
		new_password = data.get("new_password")
		old_password = data.get("old_password")
		
		if new_password == old_password:
			return {"success": False, "message": "New password must be different from the old password."}

		try:
			frappe.local.login_manager.check_password(frappe.session.user, old_password)
		except:
			return {"success": False, "message": "Wrong password"}

		user_data = frappe.db.get_value(
			"User", frappe.session.user, ["first_name", "middle_name", "last_name", "email", "birth_date"]
		)

		result = test_password_strength(new_password, user_inputs=user_data)
		feedback = result.get("feedback", {})

		if feedback and not feedback.get("password_policy_validation_passed", False):
			return {"success": False, "message": "Password does not meet the requirements", **feedback}


		update_password(user=frappe.session.user, pwd=new_password, doctype="User", fieldname="password", logout_all_sessions=True)
		
		return {"success": True, "message": "Password Changed", **feedback}

	except Exception as e:
		frappe.log_error(message=str(frappe.get_traceback()), title="Password Change Error")
		return {"success": False, "message": str(e)}


@frappe.whitelist(allow_guest=True)
def forgot_password():
	try:
		data = json.loads(frappe.request.data)
		reset_password(data.get("user"))
		return {"success": True, "message": "Password reset instructions have been sent to your email."}
	except Exception as e:
		frappe.log_error(message=str(frappe.get_traceback()), title="Forgot Password Error")
		return {"success": False, "message": str(e)}

mobile_apis/v1/auth.py
wahni-green/mobile-apis
# Copyright (c) 2022, Wahni IT Solutions and contributors
# For license information, please see license.txt

import json
import frappe
from frappe import _


@frappe.whitelist(allowed_methods=["GET"])
def fetch_customers():
	try:
		return {"success": True, "customers": frappe.get_list('Customer', fields=['name', 'customer_name', 'customer_group', 'territory'])}
	except Exception as e:
		return {"success": False, "message": str(e)}


@frappe.whitelist(allowed_methods=["POST"])
def create_customer():
	try:
		data = json.loads(frappe.request.data)
		customer = frappe.new_doc("Customer")

		for key, value in data.items():
			customer.set(key, value)

		customer.insert(ignore_permissions=True, ignore_mandatory=True)
		return {"success": True, "message": "Customer Created"}
	except Exception as e:
		frappe.log_error(str(frappe.get_traceback()), "Customer Creation Error")
		return {"success": False, "message": str(e)}


@frappe.whitelist(allowed_methods=["GET"])
def fetch_customer_groups():
	try:
		return {"success": True, "customer_groups": frappe.get_list('Customer Group')}
	except Exception as e:
		return {"success": False, "message": str(e)}


@frappe.whitelist(allowed_methods=["GET"])
def fetch_item_groups():
	try:
		return {"success": True, "item_groups": frappe.get_list('Item Group')}
	except Exception as e:
		return {"success": False, "message": str(e)}


@frappe.whitelist(allowed_methods=["GET"])
def fetch_items():
	try:
		return {"success": True, "items": frappe.get_list('Item', fields=['name', 'item_name', 'item_group', 'stock_uom', 'sales_uom'])}
	except Exception as e:
		return {"success": False, "message": str(e)}
mobile_apis/v1/masters.py
wahni-green/mobile-apis
# Copyright (c) 2022, Wahni IT Solutions and contributors
# For license information, please see license.txt

import frappe
from frappe import _
from frappe.desk.query_report import build_xlsx_data
from frappe.utils import now, today, global_date_format, format_time
from frappe.utils.csvutils import to_csv
from frappe.utils.pdf import get_pdf
from frappe.utils.xlsxutils import make_xlsx


def get_report_content(filters, report, format="CSV"):
	"""Returns file in for the report in given format"""
	report = frappe.get_doc("Report", report)
	filters = frappe.parse_json(filters) if filters else {}

	columns, data = report.get_data(
		limit=100,
		user=frappe.session.user,
		filters=filters,
		as_dict=True,
		ignore_prepared_report=True,
	)

	# add serial numbers
	columns.insert(0, frappe._dict(fieldname="idx", label="", width="30px"))
	for i in range(len(data)):
		data[i]["idx"] = i + 1

	if len(data) == 0:
		frappe.throw(_("No data found."))

	if format == "PDF":
		columns = update_field_types(columns)
		html = get_html_table(report.name, filters, columns, data)
		return get_pdf(html, {"orientation": "Landscape", "page-size": "A3"})

	elif format == "XLSX":
		report_data = frappe._dict()
		report_data["columns"] = columns
		report_data["result"] = data

		xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
		xlsx_file = make_xlsx(xlsx_data, "Report", column_widths=column_widths)
		return xlsx_file.getvalue()

	elif format == "CSV":
		report_data = frappe._dict()
		report_data["columns"] = columns
		report_data["result"] = data

		xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
		return to_csv(xlsx_data)

	else:
		frappe.throw(_("Invalid Output Format"))


def update_field_types(columns):
	for col in columns:
		if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency":
			col.fieldtype = "Data"
			col.options = ""
	return columns


def get_html_table(report, filters=None, columns=None, data=None):
    date_time = global_date_format(now()) + " " + format_time(now())
    # create new HTML template if required
    return frappe.render_template(
        "frappe/templates/emails/auto_email_report.html",
        {
            "title": report,
            "description": report,
            "date_time": date_time,
            "columns": columns,
            "data": data,
            "report_url": "",
            "report_name": report,
            "edit_report_settings": "",
        },
    )


@frappe.whitelist()
def download_report(filters, report, format="PDF"):
	try:
		frappe.local.response.filecontent = get_report_content(filters, report, format)
		frappe.local.response.type = "download"
		frappe.local.response.filename = f"{report}-{today()}.{format.lower()}"
	except Exception as e:
		frappe.log_error(str(frappe.get_traceback()), f"Error downloading {report} report")
		frappe.throw(str(e))
		# return {"success": False, "error": str(e)}

mobile_apis/v1/reports.py
wahni-green/mobile-apis
# Copyright (c) 2022, Wahni IT Solutions and contributors
# For license information, please see license.txt

import json
import frappe
from frappe import _
from frappe.utils import today, add_days
from erpnext.stock.get_item_details import get_price_list_rate_for


# GET ORDERS DETAIL VIEW
@frappe.whitelist(allowed_methods=["GET"])
def get_order_details():
	try:
		data = json.loads(frappe.request.data)
		doc = frappe.db.get_value('Sales Order', data.get("order_id"), [
			'transaction_date', 'delivery_date', 'customer', 'status'
		], as_dict=1)

		if doc:
			doc['items'] = frappe.get_all("Sales Order Item", filters={
				'parent': data.get("order_id"),
				"is_free_item": 0
			}, fields=['item_code', 'item_name', 'qty', 'rate', 'amount'])

		return {"success": True, "order_details": doc}
	except Exception as e:
		frappe.log_error(str(frappe.get_traceback()),  "Order Details error")
		return {"success": False, "message": str(e)}


# SALES ORDER CREATION
@frappe.whitelist(allowed_methods=["POST"])
def create_sales_order():
	try:
		frappe.db.commit()
		data = json.loads(frappe.request.data)
		items = data.get("items")

		sales_order = frappe.new_doc("Sales Order")
		sales_order.customer = data.get("distributor")
		sales_order.delivery_date = add_days(today(), 20)
		if data.get("company"):
			sales_order.company = data.get("company")

		sales_order.selling_price_list = frappe.db.get_value(
			"Customer", sales_order.customer, "default_price_list"
		) or "Distributor"

		for item in items:
			rate = get_price_list_rate_for(
				{
					"price_list": sales_order.selling_price_list,
					"uom": frappe.db.get_value("Item", item.get("item_code"), "sales_uom") or "Box",
					"transaction_date": today(),
					"qty": 1
				},
				item.get("item_code"),
			) or 0

			sales_order.append('items', {
				"item_code": item.get("item_code"),
				"qty": item.get("qty"),
				"rate": rate,
			})

		if not data.get("sales_person"):
			data["sales_person"] = frappe.get_value(
				"Sales Person",
				filters={
					"employee": frappe.get_value(
						"Employee",
						filters={"user_id": frappe.session.user}
					)
				}
			)

		if data.get("sales_person"):
			sp = sales_order.append('sales_team', {})
			sp.sales_person = data.get("sales_person")
			sp.allocated_percentage = 100

		sales_order.flags.ignore_permissions = True
		sales_order.set_missing_values()
		sales_order.calculate_taxes_and_totals()

		sales_order.insert(ignore_permissions=True, ignore_mandatory=True)
		sales_order.submit()

		return {"success": True, "message": "Sales Order Created"}
	except Exception as e:
		frappe.log_error(str(frappe.get_traceback()), "Sales Order Error")
		frappe.db.rollback()
		return {"success": False, "message": str(e)}


mobile_apis/v1/sales.py
wahni-green/mobile-apis